Invariants / DC-MEM-11

DC-MEM-11

DC derived enforced

The network forward-sync / forge per-block admit MUST derive the WAL post_fp from the CACHED UTxO-component fingerprint (ForwardSyncState.utxo_fp_cache -> fingerprint_v2_with_utxo), never the full per-block fingerprint() recompute, and the convergence-evidence post_fp MUST reuse the running state.prior_fp the reducer just computed (no second recompute per admit). Under the live track_utxo=false follow the imported UTxO is invariant, so OverlayUtxo::generation is stable across the per-block ledger clones AND rollbacks and the UTxO component (a Ristretto255 set commitment, O(n) over the ~1.9M-entry UTxO) is computed ONCE and reused; any UTxO mutation bumps the generation and forces a full recompute, so the cached post_fp is byte-identical to fingerprint() and the WAL post_fp chain + replay-equivalence are UNCHANGED. Pure optimization, NOT authoritative state. Extends the proven MEM-OPT-UTXO-DISK StaticUtxoFp/UtxoFpCache optimization (admission path) to the forward-sync path the cluster left on the full recompute.

Source

docs/active/live-follow-throughput-handoff.md (C2-PREVIEW-BA02: forge per-block admit ~20s CPU/block @ 99.8% CPU -- the producer kept pace with the chain but could never CLOSE the catch-up backlog, so it never reached the live tip / a live leader slot)

Introduced in
LIVE-FOLLOW-THROUGHPUT

Enforcement trace

Tests 4

  • pump_block_post_fp_is_byte_identical_to_full_fingerprint
  • forward_sync_post_fp_cache_hit_is_byte_identical
  • forward_sync_replay_two_runs_byte_identical
  • forward_sync_admission_through_chokepoints

Cross-references

Attack rationale

The forward-sync reducer computed the WAL post_fp via the full fingerprint() -- fingerprint_v2 -> fingerprint_utxo_v2, a Ristretto255 hash-to-curve PER UTxO entry over the full ~1.9M-entry imported UTxO -- on EVERY admitted block, and node_lifecycle::emit_participant_admit recomputed the SAME full fingerprint a second time for the convergence-evidence post_fp. At ~10-15us/entry that is ~20s of CPU per block (99.8% CPU), so the live producer admitted at ~0.05 blocks/s == the chain's own production rate: it kept pace but could NEVER close a catch-up backlog, so a producer that recovered even ~1000 blocks behind the live tip stayed ~1000 blocks behind forever and never reached a live leader slot (bounty-blocking). The admission path was already fast because it serves the UTxO component from a cached constant (StaticUtxoFp); the forward-sync/forge path was left on the full recompute. A naive 'fix' that emptied/dropped the UTxO would instead produce a WRONG post_fp (the empty-set commitment != the real one) -- silent divergence. The generation-keyed cache is byte-identical by construction (a mutation bumps the generation -> recompute), so it can never write a stale component.

Evidence notes

LIVE-FOLLOW-THROUGHPUT (2026-06-18). Diagnosed from code + first principles: fingerprint_utxo_v2 (ade_crypto::utxo_set_commitment, RistrettoPoint::from_uniform_bytes(blake2b_512(entry)) per UTxO) is O(n) crypto over the in-memory OverlayUtxo anchor (1.9M preview entries) -- CPU-bound (matches 99.8% CPU), and ~1.9M entries fit in ~1.3 GiB so low RSS never ruled the UTxO out (the prior hypothesis wrongly excluded it). The forge called it TWICE/admit (reducer.rs WAL post_fp + node_lifecycle emit_participant_admit evidence post_fp). Fix: ForwardSyncState carries a per-loop UtxoFpCache; the reducer derives post_fp via fingerprint_v2_with_utxo(ledger, cache.utxo_fingerprint(utxo)) (constant under track_utxo=false), and emit_participant_admit reuses state.prior_fp. Hermetic proof: pump_block_post_fp_is_byte_identical_to_full_fingerprint (the cached running post_fp AND the WAL AdmitBlock post_fp both == the full fingerprint()); forward_sync_replay_two_runs_byte_identical still green; 1205 tests green across ade_runtime+ade_node+ade_testkit. LIVE PROOF (C2-PREVIEW keyed --mode node follow on a store6 copy vs the real cardano-node-preview peer @ 127.0.0.1:3002): Ade recovered at slot 115039644 (26,229 slots / 1000 blocks behind the live peer tip 115066258), CLOSED the entire backlog in a ~5s burst (99.3% of the WAL growth, +111,090 of +111,895 B, in one t=125->130 sample window => 190 blocks/s) reaching slot 115065873 (within ~385 slots of the live tip), then HELD the tip admitting each new block at the chain's own rate (1/25s); replay_verdict agreed; owned rss_anon 1.36 GiB (well within the BA-08 budget -- the cache is a single (u64, Hash32), no MEM-OPT regression). Pre-fix this ~1000-block backlog could never close at 0.05 b/s. The forge stayed in initial_catchup_required (no block produced); peer-acceptance/BA-02 NOT claimed. HARDENING fast-follow (2026-06-18, post IDD review): (1) forward_sync_post_fp_cache_hit_is_byte_identical binds the reducer's REAL post-admit cache HIT branch to the full fingerprint_utxo_v2 oracle (the single-block pump test exercises only the MISS); (2) ForwardSyncState::invalidate_utxo_fp_cache, called by the RolledBack arm after commit_rollback, is the structural cross-fork guard (closes the open_obligation); (3) the rare fork-switch-applied evidence full-recompute (node_lifecycle.rs ~1601) is commented as intentional. Rollback replay-equivalence stays green (reselection_replay_s5, apply_driver_ai_s3, node_sync rollback) -- the invalidation is a byte-identical no-op recompute under track_utxo=false.

Open obligation