Invariants / DC-FOLLOW-FORGE-01

DC-FOLLOW-FORGE-01

DC derived enforced

Participant forge-decision mechanics. The keyed Participant venue uses an initial-catch-up -> extend forge mode mirroring the single-producer two-state mode: participant_forge_decision returns UseInitialCatchupGate (the existing DC-NODE-15 gate) until the FIRST caught-up instant (participant_forge_mode_on_caughtup latches ForgeMode::ParticipantExtendOnSelectedHead on the durable servable head -- exact-equality-once then latch, NOT a frontier-proximity re-test), then ExtendOnSelectedHead { forge_base = the LIVE AO-selected durable servable ChainDb::tip read at the decision boundary } iff venue == Participant AND no fork-choice decision is pending (DC-NODE-28 MANDATORY: pending_reselection / pending_fork_switch / pending_missing_bridge ALL fence, yielding a typed ForkChoicePending refusal) AND the durable servable tip is present; otherwise a typed ParticipantFenceViolation (VenueNotDeclaredParticipant / ForkChoicePending / NoDurableServableTip). The decision does NOT gate on the extend mode's latched current_tip: that latch is a DERIVED OBSERVATION (advanced only on Ade's OWN forge+admit, never the forge authority). Gating the decision on a current_tip byte-equality re-check deadlocks the forge -- the durable head also advances on every FOLLOWED peer admit, so the latch goes stale the instant a peer block is admitted and the decision refuses forever (the live proof-#1 finding, preview epoch 1333: 539 extend ticks, only 37 leader checks before the first peer admit, then 502 no_tip). Because the forge base is now derived from the live durable tip, a sign-time base-consistency re-check (participant_sign_time_base_consistent) runs in the RED ForgeTick immediately before signing/admit: re-read ChainDb::tip and refuse deterministically (ForgeRefused::ParticipantForgeBaseChangedBeforeSign) if a participant admit / fork-selection advanced the durable head between the decision and the sign -- so dropping the decision-time equality check does not become a stale-SIGNING bug; the next ForgeTick re-evaluates from the new tip. The decision is PURE / TOTAL / deterministic GREEN -- no HashMap / wall-clock / Instant / rand / float, closed typed enums only (no String / anyhow in the result), no observed-peer competing fence (the AO resolves competitors), and it NEVER reaches select_best_chain / chain_selector / fork_choice and carries NO KES/VRF signing material (signing stays RED). The forged block is durably admitted via the unchanged pump_block (DC-NODE-05); the in-memory ForgeMode is replay-derived on restart (re-catches-up, re-transitions), so replaying the same admitted chain + leader schedule yields byte-identical decisions and forged blocks.

Source

docs/clusters/PRODUCER-PARTICIPANT-FOLLOW/CN-FOLLOW-01-participant-forge-on-ao-selected-head.md (§6 DC-FOLLOW-FORGE-01, §8 changes, §10 MAC)

Introduced in
CN-FOLLOW-01

Enforcement trace

Tests 10

  • participant_venue_forges_on_ao_selected_head_when_leader
  • participant_forge_base_is_ao_selected_chaindb_tip
  • participant_forge_base_is_servable_before_forge
  • participant_forge_refused_while_fork_choice_pending
  • participant_venue_requires_forge_activation
  • orphaned_startup_holds_forge_fence_participant
  • participant_forge_two_runs_byte_identical
  • single_producer_forge_decision_unchanged
  • keyed_participant_extend_survives_peer_admit_and_reaches_leader_check
  • participant_forge_refuses_if_tip_changes_between_decision_and_sign

Cross-references

Attack rationale

The mechanics must not (a) re-select chains (a second fork-choice in the forge path would diverge from the AO/BLUE selection -- forbidden by the CI grep + the closed decision having no selector call), (b) forge across a pending fork-choice (forging on a stale pre-resolution tip during a rollback/apply is the producer race DC-NODE-28 fences -- ALL THREE pending signals refuse, MANDATORY), (c) forge on an absent base (NoDurableServableTip fails closed rather than fabricate a base or fall back to Origin), (d) gate the decision on the latched current_tip (a stale-latch deadlock: the latch advances only on Ade's own forge while the durable head advances on every followed peer admit, so a byte-equality re-check refuses forever once a peer block is admitted -- the live proof-#1 bug), (e) sign a stale block after dropping that equality check (a participant admit / fork-selection racing in between the decision and the sign is caught by participant_sign_time_base_consistent, which refuses with ParticipantForgeBaseChangedBeforeSign and re-evaluates next tick), or (f) leak signing keys into the GREEN decision (signing stays RED). The latch-on-first-caught-up (not a per-tick re-test) plus deriving the forge base from the LIVE durable tip is what closes the ~95% no_tip_available refusal that made the participant producer a non-producing follower.

Evidence notes

Introduced at CN-FOLLOW-01 (2026-06-19). Derived mechanics for the CN-FOLLOW-01b forge-only-on-selected-head clause, Participant venue. Mirrors the proven single-producer ForgeMode two-state machine (DC-NODE-18/20) but swaps the single-producer observed-feed fence for the AO/DC-NODE-28 pending-resolution fence -- because a competing candidate is RESOLVED by the AO (DC-NODE-23/38) and the resolution HOLDS the forge fence while pending, so the Participant extend never needs the single-producer fail-closed-on-any-competitor rule. REFINED (2026-06-20) by the live proof-#1 finding (preview epoch 1333): the original landing gated participant_forge_decision on a current_tip byte-equality re-check, which DEADLOCKED -- the latch advances ONLY on Ade's own forge+admit (participant_forge_mode_after_admit, admitted=own-forge) but the durable head advances via the FOLLOW (peer admits through run_participant_sync), so the moment Ade admitted a peer block the durable tip diverged from the stale latch and the decision returned DurableTipDivergedFromExtendHead forever (539 extend ticks, 37 leader checks before the first peer admit, then 502 no_tip). The fix derives the forge base from the LIVE AO-selected durable servable tip at the decision boundary, removes the DurableTipDivergedFromExtendHead arm, redefines the latch fields as derived observations (never authority), and adds a sign-time base-consistency re-check (participant_sign_time_base_consistent) so the dropped equality check does not become a stale-signing bug. 10 hermetic unit tests over the pure decision + transitions (incl. keyed_participant_extend_survives_peer_admit_and_reaches_leader_check + participant_forge_refuses_if_tip_changes_between_decision_and_sign); the existing DC-NODE-29/38/39 participant rollback + LCA-walk integration tests (live_fork_choice_ai_s4bii, 47) + reselection_replay_s5 stay green with the forge active. The open design choice in the slice doc (exact-equality-once-then-latch vs frontier-proximity) was resolved to the single-producer 'first caught-up instant latches the extend mode' default to minimise divergence. NO BA02 claim until cardano-node-preview logs AddedToCurrentChain for Ade's exact forged hash; proof-#1 verified the forge REACHES the extend path live but deadlocked on the latch.