Skip to content

Privacy mapper: 32-bit int truncation of UnixNano in hideTimestamp breaks ForwardingHistory ("min is not allowed to be greater than max") #1357

Description

@ziggie1984

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions