DustCtime

object 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. Anchoring ctime to 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's sync_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_time freezes at the last event's time — hours old — while the tip advances. ctime = sync_time then drifts more than grace behind tblock and 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

Link copied to clipboard

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.

Link copied to clipboard
const val TIP_BACKOFF_MS: Long

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.

Functions

Link copied to clipboard
fun anchorMs(dustSyncTimeMs: Long, tipMs: Long, safetyWindowMs: Long = SAFETY_WINDOW_MS, tipBackoffMs: Long = TIP_BACKOFF_MS): Long

The ctime to anchor a dust spend to.