DustCtime
Computes the ctime a dust spend anchors to, keeping it inside the node's dust validity window so the spend is neither rejected as stale nor as out-of-window.
The node validates every dust action against ctime ∈ [tblock - grace, tblock] (MalformedTransaction::OutOfDustValidityWindow, node error 171; the check lives in the ledger's dust.rs and reads params.dust.dust_grace_period, default 3h). On top of the window check, the spend's committed dust root must equal dust.root_history.get(ctime).
Two failure modes pull ctime in opposite directions, so neither a fixed tip nor a fixed sync-time anchor is safe alone:
Tip anchor → error 170 (
InvalidDustSpendProof). On an active chain the tip races ahead of our locally-replayed dust state between sync and balance. Anchoringctimeto the tip resolves a newer root than the one our proof commits to, so the node rejects the spend (#287). The cure was to anchor to the dust state'ssync_time(the block time of the last replayed dust event), whose predecessor lookup lands on our root.Sync-time anchor → error 171 (
OutOfDustValidityWindow). When dust events are sparse (an idle wallet on a long-running chain),sync_timefreezes at the last event's time — hours old — while the tip advances.ctime = sync_timethen drifts more thangracebehindtblockand the node rejects it as out-of-window.
The resolution: keep sync_time while it is recent (a racing tip with unapplied events is always recent → preserves the 170 fix), and clamp toward the tip only once sync_time has drifted toward the edge of the window (idle dust). When dust is at the tip our replayed root already is the current dust root, so any ctime ≥ sync_time resolves to it via the predecessor lookup — clamping to just under the tip stays in the window without resolving a foreign root.
This is a pure function so the window logic is unit-testable without a live chain; the residual indexer-lag race is covered by the caller's error-171 re-sync-and-retry.
Properties
Conservative grace assumed when the live dust_grace_period isn't plumbed through. The ledger default is 3h; we treat the usable window as a fraction of that so a slightly stale sync-time (which still resolves to our root and is preferable for 170 safety) is retained, while a genuinely idle one is clamped well before the real 3h edge. There is no native getter for dust_grace_period (only global_ttl), so this is intentionally smaller than any realistic grace.
Headroom (ms) below the tip when clamping an idle sync-time. The chosen ctime must be ≤ tblock of the block that actually includes the tx, which is a few blocks past the tip we read. Backing off a small margin keeps ctime ≤ tblock even as the chain advances during proving + submission, staying inside the window's upper edge.