fix: stop peer gossip from poisoning the CW seam parent height - #607
Open
blacks1ne wants to merge 1 commit into
Open
Conversation
The consensus digest is `Poseidon(header.output)` and commits to nothing
else — not `frame_number`. Channel-3 block ingress validates nothing before
recording digest→frame_number, so any peer can pair a real frame's `output`
with an arbitrary height and land it on that frame's digest.
On the GLOBAL chain that is a live halt. `propose` reads the index as
authoritative, so a poisoned entry is a HIT (not the miss the clock-store
fallback covers) and `prove_next_state` fails on every view this node leads:
cw propose: prove_next_state failed — cannot build a proposal
view=3289 prior_frame_number=1000001 error=not found: needs sync:
consensus parent is frame 1, told 1000001
Channel 3 is not even sender-attributed there (`CwInboundRouter::route`
forwards it without resolving the peer), so one unauthenticated message is
enough.
Ingress now stores block bytes and nothing else, on both seams — the
activation closures no longer hold the proposer at all, so the property is
structural rather than a rule. The index keeps taking validated writes only
(`propose`, `verify`, the activation seed), which is what the restart
fallback and the genesis seed already rely on.
Scope note: on the APP seam this is now defence-in-depth rather than a live
fix. `resolve build errors, unified tree model, fix sync` (932045a) deleted
`app_header_extends_simplex_parent`, dropped `parent_digest` from
`AppSeamProposer::verify`, and made `propose`'s height advisory
(`unwrap_or(0)`, with `AppLeaderProvider::prove_next_state` ignoring it), so
the forged height has no reachable consumer on that seam today. The ingress
write remains the primitive, so it is removed there too. Dropped from the
original version of this change accordingly: the identity-checked
`resolve_parent_frame_number` fallback and its `clock_store` plumbing (they
would reinstate the nullify-on-miss gate 932045a deliberately removed), the
`verify` height gate (the code is gone), and the app-shard e2e attack test
(it can no longer distinguish fixed from unfixed source; the two app-seam
unit tests guard the primitive directly).
`propose` also no longer swallows `prove_next_state` errors — that silence is
why the halt left no on-disk signal.
The global e2e rig is new: `activate_global_consensus_cw` had no harness. It
drives a single-member committee and attacks through the real public
`ingest_block`, keyed off the block broadcast rather than finalization (a
finalize-keyed attacker is ~2 views late on a fast chain and proves nothing).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
blacks1ne
force-pushed
the
blacks1ne/fix-cw-seam-parent-height-poisoning
branch
from
August 18, 2026 10:35
5c8456d to
e1eaf77
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Base:
932045a3Scope narrowed on rebase. This originally fixed the same primitive on both consensus seams. At this tip the app-shard half is no longer reachable:
cw_app_seams'verifyonly writesblock_meta, andAppConsensusEngine::prove_next_statetakes the height as_prior_frame_numberand resolves the parent from the shard clock store instead. With no consumer, a poisoned app-side entry does nothing. The global seam is unchanged and still vulnerable —cw_global_seams.rs:148readsblock_metato resolve the Simplex parent, and the global engine must build on exactly that height.The vector. The consensus digest is
Poseidon(header.output)and commits to nothing else — notframe_number. Channel-3 block ingress validated nothing before recordingdigest → frame_number, so any peer can pair a real frame'soutputwith an arbitrary height and land it on that frame's digest.proposethen builds at the height that index reports. A nullified view does not advance the Simplex parent, so the poisoned entry stays the parent forever — a permanent halt. The global channel 3 is not even sender-attributed (cw_consensus_bridge.rs:102).The fix. Ingress stores block bytes and nothing else — the activation closure no longer holds the proposer at all, so the property is structural rather than a rule. The index takes validated writes only (
propose,verify, the activation seed). Applied to both seams, the app seam as defence in depth so the primitive cannot come back if a consumer is reintroduced.Also:
proposeno longer swallowsprove_next_stateerrors. That silence is why the halt left no on-disk signal — and it is already earning its keep, sinceworker_active_storage_attestation's pre-existing failure on this tip is a mute 90-second timeout without it.Tests.
global_consensus_cw_survives_forged_parent_height_gossipfails on pre-fix source (chain dead after one forged message, 60s) and passes after (0.21s). The rig is new —activate_global_consensus_cwhad no harness. It drives a single-member committee and attacks through the real publicingest_block, keyed off the block broadcast rather than finalization, since a finalize-keyed attacker is ~2 views late on a fast chain and proves nothing. Unit tests on both seams cover ingress-cannot-forge-the-height and ingress-still-delivers-bytes.Not fixed here: the extra-view-per-frame cost from the #593 review (a design change, not a revert), and a candidate-swap race —
BlockStore::putreplaces unsealed candidates, so a peer can swap a mutated body under the same digest beforeverifyreads it.Refs #591.