Background
A node runner reported this error in their litd logs:
lit | 2026-07-20 14:23:12.468 [ERR] RPCS: [/lnrpc.Lightning/ForwardingHistory]: min is not allowed to be greater than max, (min: 973617325, max: -1617225555)
The RPCS: [/lnrpc.Lightning/...] prefix is lnd's RPC middleware interceptor logging an error returned by a registered middleware — here, litd's firewall / privacy-mapper, which obfuscates outgoing ForwardingHistory requests before they reach lnd.
Root cause: 32-bit int truncation in hideTimestamp
The error is raised by randBetween (firewall/privacy_mapper.go:1817), called from hideTimestamp at firewall/privacy_mapper.go:1804-1805:
timeNs, err := randBetween(
randIntn, int(timeMin.UnixNano()), int(timeMax.UnixNano()),
)
time.UnixNano() returns an int64 (~1.78e18 for a 2026 timestamp), but it is cast to int, and randBetween(min, max int) / randIntn func(int) operate entirely in int.
- On 64-bit builds (
int == 64-bit): no truncation, works correctly.
- On 32-bit builds (
int == 32-bit — ARM32 / Raspberry Pi 32-bit OS, i386): the nanosecond value is truncated to its low 32 bits, yielding a garbage value in the ±2.1e9 range whose sign flips unpredictably.
Why it produces max < min
Each second adds 1e9 ns, and 2^32 ≈ 4.29e9, so the low 32 bits of UnixNano() wrap roughly every ~4.3 seconds. timeMin = t - variation and timeMax = t + variation therefore truncate to arbitrary points in this wrapping pattern. Whenever int32(timeMax.UnixNano()) < int32(timeMin.UnixNano()), the guard at line 1817 fires.
Verification against the exact error timestamp (2026-07-20 14:23:12 UTC, unix ns 1784557392000000000), int32-truncated:
off -30s -> +995193856
off -10s -> -479642624
off +0s -> +930422784
off +10s -> -1954479104
The reported pair (min: 973617325, max: -1617225555) — both inside the int32 range, max negative — matches this signature.
Impact
- 32-bit litd nodes cannot reliably call
ForwardingHistory through the privacy mapper; the call fails intermittently depending on the wall-clock time and the variation window.
hideAmount (privacy_mapper.go:1768-1769) has the same latent int truncation and would fail on 32-bit nodes for amounts above ~2^31 msat (~0.02 BTC).
Suggested fix
Make the randomness path 64-bit safe: change randBetween and the randIntn callback signatures to int64 (and CryptoRandIntn to rand.Int on an int64 bound), and drop the int(...) casts of UnixNano() in hideTimestamp and of the amounts in hideAmount. This removes the truncation on 32-bit platforms while remaining correct on 64-bit.
Environment
- Observed via
litd logs, timestamp 2026-07-20; node runner is on a 32-bit build (ARM32 / i386).
- litd version: (runner to confirm)
Notes
Traced from the lnd side (the log format originates in lnd's rpcperms/interceptor.go), then confirmed against litd source at the lines referenced above.
Background
A node runner reported this error in their
litdlogs:The
RPCS: [/lnrpc.Lightning/...]prefix is lnd's RPC middleware interceptor logging an error returned by a registered middleware — here, litd's firewall / privacy-mapper, which obfuscates outgoingForwardingHistoryrequests before they reach lnd.Root cause: 32-bit
inttruncation inhideTimestampThe error is raised by
randBetween(firewall/privacy_mapper.go:1817), called fromhideTimestampatfirewall/privacy_mapper.go:1804-1805:time.UnixNano()returns anint64(~1.78e18 for a 2026 timestamp), but it is cast toint, andrandBetween(min, max int)/randIntn func(int)operate entirely inint.int== 64-bit): no truncation, works correctly.int== 32-bit — ARM32 / Raspberry Pi 32-bit OS, i386): the nanosecond value is truncated to its low 32 bits, yielding a garbage value in the ±2.1e9 range whose sign flips unpredictably.Why it produces
max < minEach second adds 1e9 ns, and
2^32 ≈ 4.29e9, so the low 32 bits ofUnixNano()wrap roughly every ~4.3 seconds.timeMin = t - variationandtimeMax = t + variationtherefore truncate to arbitrary points in this wrapping pattern. Wheneverint32(timeMax.UnixNano()) < int32(timeMin.UnixNano()), the guard at line 1817 fires.Verification against the exact error timestamp (
2026-07-20 14:23:12 UTC, unix ns1784557392000000000), int32-truncated:The reported pair
(min: 973617325, max: -1617225555)— both inside the int32 range,maxnegative — matches this signature.Impact
ForwardingHistorythrough the privacy mapper; the call fails intermittently depending on the wall-clock time and the variation window.hideAmount(privacy_mapper.go:1768-1769) has the same latentinttruncation and would fail on 32-bit nodes for amounts above ~2^31 msat (~0.02 BTC).Suggested fix
Make the randomness path 64-bit safe: change
randBetweenand therandIntncallback signatures toint64(andCryptoRandIntntorand.Inton anint64bound), and drop theint(...)casts ofUnixNano()inhideTimestampand of the amounts inhideAmount. This removes the truncation on 32-bit platforms while remaining correct on 64-bit.Environment
litdlogs, timestamp2026-07-20; node runner is on a 32-bit build (ARM32 / i386).Notes
Traced from the lnd side (the log format originates in lnd's
rpcperms/interceptor.go), then confirmed against litd source at the lines referenced above.