DC-NODE-07
DC derived enforcedNode-spine live serve-to-peer. --mode node serves real peers ONLY from the G-B self-accepted ServedChainView (the read side of the single ServedChainHandle fed by the sibling self-accept->serve push task, DC-NODE-06), OUTSIDE run_relay_loop, THROUGH the existing ChainSync + BlockFetch serve reducers, with NO second serve authority or serializer. A sibling listener + serve-dispatch task (spawned outside the relay loop, mirroring the G-B push task) reuses the existing N2N serve machinery (run_n2n_listener + dispatch_server_frame_event_to_outbound) and the BLUE producer_chain_sync_serve + producer_block_fetch_serve (+ producer_chain_sync_advance_tip) -- serving BOTH the ChainSync header advertisement (a follower discovers Ade's served tip on each ServedChainView update, DC-CONS-18) AND the BlockFetch body (the follower fetches the served block by hash, DC-CONS-17), under the closed server-agency reply surface (CN-PROTO-06) and the deterministic/total session reducers (DC-PROTO-07, DC-PROTO-08). There is NO second ServedChain authority, NO parallel tag-24 serializer (the single CN-WIRE-08 envelope authority), and NO serve / push_atomic / served-chain mutation inside run_relay_loop (the containment gate ci_check_node_run_loop_containment.sh stays SEMANTICALLY UNCHANGED, CN-NODE-02). --mode node reuses the EXISTING --listen flag (no new --mode node argv flag; the S1 path-fidelity fence ci_check_node_path_fidelity.sh stays green) and is NOT switched to --mode produce. Wiring the serve is NOT a peer-acceptance claim: acceptance is proven ONLY by the peer's validation log through ba02_evidence::correlate (RO-LIVE-06), and RO-LIVE-01 / RO-LIVE-06 do NOT flip at this cluster's implementation close.
- Source
docs/planning/phase4-n-f-g-h-invariants.md
- Introduced in
- PHASE4-N-F-G-H
Enforcement trace
Code
Tests 4
- served_view_projects_durable_chain
- node_serve_start_failure_is_surfaced_not_silent
- n2n_supported_for_magic_produces_configured_magic
- node_c1_serve_live
Cross-references
Evidence notes
PHASE4-N-F-G-H invariant sketch (/invariants gate). Declared at sketch; tests + ci_script populate at slice time, flip to enforced at close. Surfaced by the G-D C1 dry-run: --mode node forges + self-accepts + pushes into a ServedChainHandle but DISCARDS the ServedChainView read side (node_lifecycle.rs "no live reader") and runs NO listener / NO serve server on the node spine, so a real Haskell follower cannot fetch Ade's forged block -- forge-acceptance is unreachable in --mode node. This is the owed SHARED-PATH leg (C2 preprod needs it too); fixed on the node spine (NOT --mode produce) so the G-D path-fidelity guarantee (CN-REHEARSAL-FIDELITY-01) holds. Reuse, not new authority: the BLUE serve reducers (ade_network) + the GREEN ServedChainView (ade_runtime) already exist and produce_mode already drives both; the only new code is RED wiring (retain the view + spawn the sibling serve task). A follower needs BOTH ChainSync (header discovery, DC-CONS-18) AND BlockFetch (body, DC-CONS-17) -- verified in produce_mode's existing dual-serve, not assumed.
Slice-entry proof obligation (OQ2 / A7): C1 must prove a topology that makes BOTH required traffic directions true -- (a) Ade receives a Continuing UPSTREAM live feed that keeps NodeBlockSource::WirePump Continuing so the forge loop reaches ForgeTick, AND (b) a DOWNSTREAM Haskell follower connects to Ade's --listen, discovers the served header via ChainSync, and fetches the body via BlockFetch. These are DISTINCT traffic directions; "serve is wired" is insufficient. Do NOT assume one connection can satisfy both directions. Whether one reciprocal Haskell peer (Ade --peer Haskell; Haskell topology includes Ade) or two Haskell nodes realizes the topology is resolved at slice entry, not assumed.
Open at sketch: (OQ3) the node-spine ChainSync server MUST advertise the served tip when ServedChainView updates so a follower can request the block body by hash through BlockFetch -- so the shared serve adapter must include produce_mode's ChainSync advance/serve behavior (producer_chain_sync_advance_tip), not BlockFetch alone. (OQ1) the shared serve adapter (dispatch_server_frame_event_to_outbound + new_per_peer_outbound, currently private in RED produce_mode) is EXTRACTED to a shared serve module both modes call -- never duplicated (no second serve authority); its TCB color is resolved at cluster-plan. The kept ~/.cardano-private-testnet-c1 net is the regression harness for the live proof of OQ2/OQ3.
PHASE4-N-F-G-H S1 (extract shared serve-dispatch authority) LANDED: the coordinator-free
serve-dispatch core (dispatch_server_frame_event_to_outbound + the per-peer-state lifecycle
install_server_peer_state/remove_server_peer_state + ServerPeerStates + DispatchError +
ServedBlockEvidence) moved verbatim from ade_node::produce_mode to the shared
ade_runtime::network::serve_dispatch module; produce_mode's handle_listener_event re-pointed
at it and KEEPS its CoordinatorState/coordinator_step/producer-evidence wrapping (behavior
byte-unchanged: produce_loopback 4/4, ade_network server tests 21/21, ade_runtime 343,
ade_node 171 -- all green). NEW gate ci/ci_check_single_serve_dispatch_authority.sh: exactly
one fn dispatch_server_frame_event_to_outbound (under crates/ade_runtime/), node_lifecycle
defines none, produce_mode reuses it -- green; fail-closed verified on an injected duplicate.
NO BLUE change; NO node-spine wiring (that is S2); NO proactive advance_tip; the containment /
path-fidelity / handoff fences byte-unchanged. The ci_script binding + the declared->enforced
flip happen at the G-H close (after S2/S3).
PHASE4-N-F-G-H S2 (node-spine serve wiring + hermetic loopback) LANDED: the --mode node
On arm now RETAINS the ServedChainView (was discarded as _serve_view) and, when --listen
is set, spawns run_node_serve_task OUTSIDE run_relay_loop -- a sibling that accepts inbound
peers (reusing run_per_peer_session) and routes events to the S1 coordinator-free primitives
(install_server_peer_state / remove_server_peer_state / dispatch_server_frame_event_to_outbound)
over the served view. REQUEST-DRIVEN serve only (NO advance_tip, NO served_chain_view.changed()
reactor). Serve-start bind failure is surfaced via bind_serve_listener ->
NodeLifecycleError::ServeStart (fail-fast; no silent live-serve claim). The shared dispatch
core was made Send -- the watch::Ref read guard is now block-scoped in BOTH the chain-sync and
block-fetch arms (dropped before the outbound .await) so the serve sibling can tokio::spawn it;
produce_mode behavior byte-unchanged (produce_loopback 4/4 + ade_network server 21/21 green).
Hermetic loopback served_view_projects_durable_chain: Ade-serve
<-> Ade-consume over a real ephemeral TCP socket -- the follower (dial_for_admission +
run_admission_wire_pump) discovers the served tip via ChainSync + BlockFetches the body; the
tag-24-unwrapped payload == the served self-accepted block (DC-CONS-17 + CN-WIRE-08).
node_serve_start_failure_is_surfaced_not_silent: bind on an occupied port -> ServeStartError::Bind;
unparseable addr -> InvalidAddr. Containment / path-fidelity / handoff fences byte-unchanged +
green; single-serve-dispatch gate green; NO new --mode node flag (reuses --listen); NO BLUE
change; NO RO-LIVE flip. ade_node 171 + ade_runtime 343 green. Known shared-path item (NOT S2):
the serve listener advertises the static N2N_SUPPORTED responder table (mainnet magic, mirroring
produce_mode); a non-mainnet (preprod/C1) real follower's magic-match is an S3 operator-pass
finding to fix in the shared listener path if it fails. The tests binding + the
declared->enforced flip happen at the G-H close (after S3).
PHASE4-N-F-G-H S2b (magic-aware serve listener) LANDED -- resolves the mainnet-only serve shared-path item flagged in S2 BEFORE S3 (not deferred to the operator pass). The live serve listeners (node-spine run_node_serve_task + produce_mode) now advertise N2N versions using the CONFIGURED network magic (node: cli.network_magic; produce: parsed-genesis network_magic) via the additive BLUE builder ade_network::handshake::version_table::n2n_supported_for_magic -- the ONLY approved BLUE touch in G-H (additive, pure, deterministic, over the UNCHANGED closed V11..=V16 version set; only network_magic parameterized; N2N_SUPPORTED + n2n() unchanged; no canonical type, no version widening). N2nListenerConfig / PerPeerSessionConfig.our_supported changed &'static [(u16,VersionData)] -> Arc<[(u16,VersionData)]> (owned; no leak). This fixes the mainnet-only serve handshake the G-D C1 dry-run surfaced: a real preprod (magic 1) / C1 (magic 42) follower would otherwise refuse the handshake on a magic mismatch -- blocking both the C1 private dry-run and the C2 preprod bounty pass. NEW gate ci/ci_check_serve_listener_magic_aware.sh (no live serve site uses static N2N_SUPPORTED; both build via n2n_supported_for_magic) -- green; forbidden-pattern detection verified. Tests: n2n_supported_for_magic_produces_configured_magic (magic 1/42/mainnet correct + closed version set unchanged) + node_spine_serve_loopback now end-to-end on NON-mainnet magic 42 (serve advertises 42; follower proposes 42; handshake + fetch succeed) + node_serve_start_failure_is_surfaced_not_silent. produce_loopback 4/4; ade_network handshake+server 51; ade_node 171; ade_runtime 343; single-serve-dispatch / containment / path-fidelity / handoff gates green / byte-unchanged. The tests binding + the declared->enforced flip happen at the G-H close (after S3).
PHASE4-N-F-G-H S3 (C1 serve runbook + operator-gated harness) LANDED -- the operator-gated half of G-H. NEW serve-direction runbook docs/evidence/phase4-n-f-g-h-node-serve-README.md: a strict adaptation of the G-C/G-D operator-pass runbooks; the ONLY additions are the downstream-follower topology (a real Haskell cardano-node whose topology.json lists Ade's --listen dials Ade + ChainSync-discovers + BlockFetches Ade's forged block, the A7 serve direction) + --listen (S2, in the closed flag set) + the S2b magic-aware serve (magic 42). NEW env-gated harness crates/ade_node/tests/node_c1_serve_live.rs: node_c1_serve_live (ADE_LIVE_C1_SERVE) reuses correlate_peer_log_file_into_rehearsal + write_private_rehearsal_manifest (G-D) VERBATIM -- the operator-captured Haskell-FOLLOWER log -> a NON-promotable PrivateRehearsalManifest (C1 != bounty); NoEvidence panics (no manifest); skipped in CI; NOT a node mode. The hermetic correlate->manifest wiring is the existing c1_dry_run_correlate_to_rehearsal_envelope (no second hermetic test added). Request-driven serve only; a parked-at-tip follower needing a proactive advance_tip is a STOP-and-scope SEPARATE cluster (not patched into S3). NO synthetic/committed manifest; NO BLUE change; NO handshake change; NO --mode produce; NO RO-LIVE flip; the live C1 serve execution is blocked_until_operator_c1_serve_executed; the bounty deliverable remains the separate C2 preprod pass. ade_node 171 + ade_runtime green; the 5 serve / containment / path-fidelity / handoff gates green / byte-unchanged. With S1 + S2 + S2b + S3 mechanically green, DC-NODE-07's tests + ci_script binding + the declared->enforced flip happen at /cluster-close PHASE4-N-F-G-H.