fix(consensus): cutover QC handling and parent proposal fetch#4702
Conversation
A node's view timer fired exactly once per view, so its TimeoutVote2 broadcast was single-shot. With a live set exactly at the TC2 threshold (e.g. 7 nodes with 2 silenced at cutover), one lost broadcast deadlocked consensus permanently: no re-send, no timer re-fire, TC2 unicast only to the next leader. Observed as perm_silence_m2_m1 stalling for 240s with zero decisions in both post-#4689 CI runs. Re-arm the timer on fire so the vote is re-broadcast every timeout period until the view advances. Collectors dedup by (view, signer); participation and timeout metrics count once per view.
…utover Under start-time skew the cutover seed can be snapshotted before the QC for the last legacy view lands, so the smooth-start precondition fails and views 24/25 get TC2-skipped: legitimate behavior, same physics permitted for the perm sweep in #4689. Smooth start stays deterministic under src/tests/cutover.rs unit tests. Failed CI run 29425671834 with node 0 skipping view 24.
Recovery gaps after the view-5 crash land on different views run to run; the consistency task shut the test down on the first out-of-list gap, which also starved the successful-view count (CI run 29425671834: unexpected failed view 9, then only 9/20 successful views). Apply the #4663 budget pattern; num_successful_views=20 remains the gate.
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. |
# Conflicts: # crates/hotshot/new-protocol/src/tests/legacy_cutover.rs
|
Claude finished @bfish713's task in 2m 19s —— View job Claude Code Review — PR #4702 (Fix FF Cutover Tests)
Reviewed the full diff against Production changes
Test changes
Minor notes (non-blocking)
Nice work — the fixes are minimal and each is backed by a test that would have caught the original flake. |
|
Created backport PR for
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-4702-to-release-ff
git worktree add --checkout .worktree/backport-4702-to-release-ff backport-4702-to-release-ff
cd .worktree/backport-4702-to-release-ff
git reset --hard HEAD^
git cherry-pick -x 37385605d677b1e0f2cd3f9d4d9233e7e2ec56a3
git push --force-with-lease |
* Start the coordinator at the upgrade view (#4670) * Start the coordinator only at the upgrade view. Previously `ConsensusHandle` spawned the coordinator loop at node startup, and a `CutoverGate` latched routing as a side effect of queries. Replace this with an explicit Init -> Running state machine: `activate()` starts the coordinator once legacy crosses the upgrade boundary, driven by the event loop and `start_consensus`. The legacy-event forwarders keep running from construction so boundary events queue until the coordinator starts. Route per-view queries (`state`, `request_proposal`, `update_leaf`) by the version of the requested view and current queries by whether the coordinator is running. Boxing the coordinator-start closure confines the `NewProtocolStorage` bound to `ConsensusHandle::new` and drops the storage type parameter from the handle. Also replace `CoordinatorOutput` with `OpaqueMessage`: the enum's only constructed variant was `ExternalMessageReceived`. * Apply cutover seed in `Coordinator::start`. Pass the pre-cutover seed into `start()` instead of dispatching it through a `SeedPreCutover` client request after the coordinator is already running. Seeding before start removes the unseeded window, the genesis-decide re-emit at cutover, and the seed-arrived-late repair paths (`resume_after_cutover_tc`, handler fallback). When the seed lacks the last legacy view's QC, `start()` parks on that view; a bridged high QC or timeout advances it. Delete the now-unused `CutoverGate` and make the cutover tests mirror production activation: the coordinator stays parked until legacy crosses the upgrade boundary. * [new-protocol] change empty block time to 1s (#4675) * [new-protocol ] fetch parent proposal if it is missing (#4644) * parent proposal fetch * Update crates/hotshot/new-protocol/src/consensus.rs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * [Fast Finality] Metric Parity b/t Protocols (#4674) * feat(new-protocol): emit legacy consensus metrics from coordinator The coordinator only exported coordinator_* pipeline histograms plus two renamed gauges, so the consensus health metrics dashboards alert on (current_view, last_decided_time, last_synced_block_height, ...) would freeze at the cutover. Share the node's ConsensusMetricsValue between the legacy handle and the coordinator -- the Prometheus registry panics on duplicate registration, so both protocols must write the same registered objects. The coordinator now sets every legacy metric that has a new-protocol analog: - current_view, number_of_views_since_last_decide, view_duration_as_leader, outstanding_transactions(_memory_size) on view changes - last_decided_view, last_decided_time, last_synced_block_height, number_of_views_per_decide_event, proposal_to_decide_time, finalized_bytes, invalid_qc reset on decides - last_voted_view on vote1/vote2 sends - number_of_timeouts(_as_leader) on local timeouts - number_of_empty_blocks_proposed when persisting an own empty proposal - vid_disperse_duration around the disperse computation outstanding_transactions was defined but never written even in legacy; the block builder's retry buffer now feeds it. The coordinator-specific coordinator_timeouts and leaf_decided_view are replaced by the legacy names number_of_timeouts and last_decided_view. Not carried over: invalid_qc increments, previous_proposal_to_proposal_time, internal_event_queue_len, validate_and_apply_header_duration and update_leaf_duration -- tied to the legacy task architecture and covered by the coordinator_* histograms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(new-protocol): track participation and serve node/participation API The node/participation/* endpoints read ValidatorParticipation and VoteParticipation from the legacy Consensus state, which stops updating at the cutover, so the API would go permanently stale on the new protocol. Expose the two trackers from hotshot-types (visibility only, no behavior change) and maintain them in the coordinator, mirroring the legacy update sites: - a validated proposal credits the view leader (proposal participation) - a local view timeout debits the view leader - each decided leaf's justify QC credits its signers (vote participation), rolling the tracked epoch forward from the QC epoch as legacy does; the decide loop runs oldest leaf first for this ConsensusHandle now routes the four participation queries to the coordinator via new ClientApi requests once the cutover is active, and to the legacy handle before it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cleanup * add missing metrics * remove comments, fix some participation metrics * lint * more memory and review comments * trim comments * remove dead code * remove coordinator timing metrics * use opt 1 for much smaller stack * address review: structured warns, drop comments, move payload gc Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * trim comment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * [Fast Finality] Stake Table Change Tests (#4651) * test(example-types): support per-epoch quorum committees Add a quorum committee schedule to StaticStakeTable, mirroring the existing per-epoch DA committee mechanism: committees are keyed by first-epoch and resolve to the greatest key <= the requested epoch, falling back to the construction-time members. Leader lookup rotates over the resolved committee. StrictMembership gains test-only helpers to install a schedule and to register an epoch without driving the epoch-root/DRB pipeline. An empty schedule preserves the previous behavior exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(new-protocol): thread stake table schedules through the runner Add StakeTableSchedule (per-epoch committees by node index) and a schedule-aware mock membership constructor that installs the quorum and DA committees and stamps each member's cliquenet connect info, so apply_epoch can add scheduled joiners as peers. TestRunner gains a stake_table_schedule field and per-node target_decisions overrides for validators that stop deciding when their membership ends. Without a schedule the runner behaves exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(new-protocol): validator join/leave at epoch boundary Two multi-node tests over a per-epoch stake table schedule (epoch 3 is the first epoch that can differ; its table is registered when the epoch-1 root decides): - join: a node outside the epoch-1/2 committee starts mid-epoch-2, catches up, is added to the other nodes' cliquenet peers via its scheduled connect info, and leads its views in epoch 3. - leave: a removed validator records no vote or propose action after its last member view, follows the chain through its one-epoch peer retention, and is cut off at the next boundary while the shrunken committee keeps deciding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(new-protocol): vote collector ignores removed validators A vote from a validator that was removed from the quorum committee in a later epoch does not count toward that epoch's certificate, while the same node's vote still counts in an epoch where it is a member. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(new-protocol): drop review-redundant inline comments Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(new-protocol): hard-assert schedule/down_nodes incompatibility Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(example-types): rename TestDaCommittees to TestCommitteeSchedule The type schedules both DA and quorum committees since it was reused for per-epoch quorum schedules; rename it so the name no longer needs an apology comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(node-testing): recognize NewDecide decides in wait_for_epochs At NEW_PROTOCOL_VERSION decides arrive as CoordinatorEvent::NewDecide, not LegacyEvent, so wait_for_epochs blocked forever on 0.6 networks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(node): validator exit activates at epoch boundary at v0.6 Full-application counterpart to the HotShot-level stake table change tests: a 5-node network at NEW_PROTOCOL_VERSION with a real StakeTable V3 on anvil. One validator sends deregisterValidator mid-run, must drop out of node/validators/{epoch} at the activation epoch, and the reduced committee must keep deciding across further boundaries. Fills the TEST:e2e-epoch-activation gap in doc/stake-table-fast-finality.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Fix main. (#4682) * fix(node): release coordinator network before legacy shutdown (#4681) * fix(node): release coordinator network before legacy shutdown Since #4670 the coordinator parks unstarted in ConsensusHandle's Init state, and shut_down disposed of it only after the legacy HotShot teardown. The parked coordinator owns the cliquenet network, so its listener held the p2p port for the entire legacy drain, and a node restarting on the same address failed to bind (EADDRINUSE). Dispose of the new-protocol state before the legacy shutdown, moving the value out explicitly: a non-binding pattern would keep it alive to the end of scope, deferring the port release all the same. Make test_state_reconstruction await the removed node's shutdown before re-binding its coordinator port, like test_merklized_state_catchup_on_restart already does. Fixes the deterministic test-postgres (10) / test-sqlite (10) CI failures in api::test::test_state_reconstruction::case_1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * just do the network shutdown --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * [new-protocol]: Bound block builder dedup state. (#4683) Bound block builder dedup state. The dedup bookkeeping kept transaction hashes in a `HashSet` used for lookups, and in an arrival-ordered `VecDeque` of `(view, hashes)` used for eviction. Eviction stopped at the first in-window deque entry, so a manifest claiming a far-future view parked at the front and blocked eviction of everything behind it, growing both structures without bound. Here we replace both structures with a single ordered map. A hash is deduplicated while any in-window view lists it, regardless of arrival order. Deduplication complexity changes from O(1) to O(n) with n being the length of the window plus any future views that arrived, i.e. n <= 41 by default. The `Coordinator` now also ignores dedup manifests more than 30 views ahead, as it already does for votes, bounding the map from above. * fix(new-protocol): re-trigger child vote1 on parent reconstruction (#4678) A replica's vote1 for view V+1 is gated on the parent block (view V) being reconstructed. When that reconstruction completed, the post-apply retry only re-ran maybe_vote_1 for the reconstructed view V, never the child V+1 waiting on it. If V+1's proposal arrived a few ms before V finished reconstructing, its single vote1 attempt failed the parent gate and was never retried, collapsing the view to a timeout. Drive maybe_vote_1(V+1) when block V is reconstructed. maybe_vote_1 is idempotent, so this is a no-op when the child has already voted or is otherwise not ready. * [new-protocol] use consensus last decided view to skip proposal requests in proposal fetcher (#4686) use consensus last decided view to skip proposal requests * [Fast Finality] Help restarted nodes reach highest view (#4685) * send view change evidence with timeout vote, and to help behind nodes * review comments, do catchup before too far check, just send highest cert * feat(types): client profile so espresso-types and light-client compile for zkVM targets (#4661) * chore: ignore repo-local tmp directory * refactor(hotshot-types): move Certificate1/2 aliases from new-protocol - the aliases are pure compositions of hotshot-types items - re-exports from new-protocol preserved, no downstream changes - lets clients name Certificate1/2 without depending on new-protocol * style(light-client): apply cargo-sort formatting * feat(query-service-types): make web deps optional behind web feature - tide-disco, surf-disco, hotshot-events-service only back error/status types - new default-on web feature gates those items; data types stay unconditional - default = ["sqlx", "web"] keeps current builds identical * chore(typos): allow bootrap, a serialized config field name * feat(types): add default-on node feature for SP1 client builds - default-features = false compiles for riscv32im-succinct-zkvm-elf - gate L1Client cluster, persistence traits, Fetcher L1 methods, proposer paths - SeqTypes: NodeType holds under both cfgs; node builds unchanged - trim alloy to minimal base features; node restores today's set - espresso-utils: new default-on full feature, ser stays unconditional - adapter, alloy-compat: direct alloy declarations with needed features only - re-point hotshot::types re-exports to hotshot-types; drop unused deps - testing implies node; hotshot-example-types becomes optional * feat(light-client): gate host client behind default-on client feature - new default-on client feature gates QueryServiceClient, FallbackClient, SqliteStorage and the query-service provider module - Client and Storage traits, the LightClient state machine and consensus verification stay available with default-features = false for SP1 builds - espresso-types and hotshot-query-service-types become direct declarations with default-features = false; client restores espresso-types/node and the query-service-types defaults (sqlx + web) - testing implies client; every cargo hack feature combination and the riscv32im/riscv64im-succinct-zkvm-elf targets compile * fix(utils): gate full-only test items behind full feature - test_contract_send and its sol! contract reference full-gated items (alloy providers, tokio); compile fails under --no-default-features --tests - gate the alloy/anyhow imports, sol! block, and test_contract_send behind full so cargo-features CI passes; keep the pure round-trip test unconditional * fix(node): fail at startup when built without the node feature - add espresso_types::assert_node_feature, called first in espresso-node, node-sqlite and dev-node entry points - declare espresso-types/node explicitly in node and dev-node Cargo.toml so a node-less build fails at compile time * refactor(types): move node cfg gates to function boundaries - gate reload_stake whole-fn under node; all callers require the feature - add Fetcher::initial_supply_or_fetch with fn-level cfg twins, deduping the read-or-fetch pattern in calculate_dynamic_block_reward and fetch_fixed_block_reward - non-node twin panics on missing initial supply instead of returning Err - split add_epoch_root into two fn-level cfg'd trait-method copies * fix(types): repair no-default-features build after main merge - EpochMembershipCoordinator now appears in the unconditional StateCatchup trait (fetch_leaf/try_fetch_leaf), move its import out of the node gate - drop HSStakeTable import left unused by the same upstream change * docs: document zkVM feature gates and non-node panics - doc/cargo-features.md: node/web/full/client features, workspace-dep caveat, functions that panic without the node feature - reference it from AGENTS.md - allow "ser" (module name) in typos config * ci: check zkVM client crates compile for the SP1 target - justfile recipe check-sp1-target: cargo +succinct check for riscv64im-succinct-zkvm-elf on espresso-types and light-client with --no-default-features and the getrandom backend opt-out - install-sp1 composite action: pinned sp1up (v6.3.1) with retries, mirroring install-foundry - new check-sp1-target job in the cargo-features workflow * ci(sp1): check the zkVM target via a probe crate - bare cargo check -p espresso-types -p light-client fails on the zkVM target: getrandom 0.2 needs its custom feature enabled by a consumer, and transitive-dep features cannot be set on the command line - sp1/target-check depends on both crates the way a guest does (default-features = false, light-client with rlp) plus the getrandom workaround; check-sp1-target now builds that crate * refactor(query-service-types): unconditional error types via disco-types - tide-disco 0.9.7 splits the server-free StatusCode/RequestError/Error trait into disco-types; depend on it unconditionally so the API error types compile on constrained targets without tide-disco/surf-disco - web feature now gates only the events-service error conversion - disco-types with default-features = false to drop http-types from no-web graphs * refactor(features): make node/web/full/client features opt-in instead of default * style(doc): format cargo-features.md with prettier * test: fix remaining flaky tests from #4585 (#4689) * test: fix remaining flaky tests from #4585 - demo: don't tear down on telemetry-collector failure (docker pull timeout) - new-protocol runner: don't bind down-node ports at setup (AddrInUse on late start) - legacy_cutover: permit first post-cutover view; keep deadlines under nextest ceiling - builder: widen first-proposal deadline for loaded CI - wait_for_epochs: fail loud with last decide on stream end * test(hotshot): failure budget for combined network reup/half_dc epoch tests CDN outage tests fail a few views during libp2p fallback handoff; give the reup and epochs half_dc variants the same max_unexpected_view_failures: 5 as their cdn_crash/half_dc siblings (#4663 pattern). * chore: update deployment info (#4679) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Add V4 and V5 message serialization test, V5 header serialization test (#4672) test(types): add v5 reference serialization vectors Add V5 (EpochRewardVersion) reference test vectors mirroring the existing v1-v4 coverage: - reference_tests: V5 header vector (exercises the new leader_counts field) plus a V5 serializer for the round-trip checks. - message_compat_tests: v4 and v5 message compatibility tests. Regenerate data/v4/messages.{bin,json}: the committed files embedded a v3 header (they predated the v4 header and were never guarded by a test), so the new test_v4_message_compat surfaced the drift. * fix(ci): upgrade Claude review model to fable (#4696) * chore(ci): use claude-fable-5 for review model * chore(ci): use fable alias so review model auto-follows latest * chore: switch claude review back to opus (#4697) * fix(new-protocol): don't stall legacy-event forwarders pre-cutover (#4694) * fix(new-protocol): don't stall legacy-event forwarders pre-cutover Since #4670 the coordinator is parked until the upgrade view and nothing services its client request queue. forward_legacy_epoch_changes sent a bump_network_epoch request and awaited a response that a parked coordinator never gives, blocking the forwarder at the first epoch boundary. Its active receiver then pinned up to 10,000 events (the external event channel capacity) on HotShot's broadcast stream, each Decide holding Arc'd validated state: about 1.2 GB per node on any network below V0_6, the 1.8 GB -> 7.8 GB epoch-reward memory-soak step. Make the bridge submissions (timeout votes, legacy high QC, epoch bumps) fire-and-forget try_send so a parked coordinator can never block a forwarder, and gate epoch-bump forwarding on a decided upgrade certificate targeting NEW_PROTOCOL_VERSION so networks that never cut over queue nothing. Regression tests cover the parked-coordinator drain and the gate. * fix(new-protocol): gate all legacy bridge forwarders on the V0_6 cert LegacyTimeoutVoteEmitted fires for any decided upgrade cert, so during a non-V0_6 upgrade window (mainnet V0_4 -> V0_5 next) timed-out views would queue SubmitTimeoutVote requests the parked coordinator never drains; at 256 queued, the requests that matter at the real V0_6 boundary drop with ChannelFull. Share one cutover_decided predicate across all three forwarders so nothing queues until the decided cert targets NEW_PROTOCOL_VERSION. * docs(new-protocol): trim bridge forwarder comments * refactor(new-protocol): drop legacy epoch-change forwarding Queued epoch bumps could never apply while the coordinator is parked, and coordinator start supersedes them: startup apply_epoch dials the cutover epoch from the seed, apply_epoch is monotone so stale bumps no-op, and post-start the proposal-driven path owns epoch changes. Delete the forwarder, its client method and request variant, and the handler arm; retarget the parked-coordinator and gate tests to forward_legacy_high_qc. * refactor(new-protocol): rename bridge submitters with try_ prefix * Restore epoch change notification before activation. (#4700) Keep Cliquenet peer configuration up to date before the coordinator is started. * chore: fix flaky tests (#4692) * fix(new-protocol): re-broadcast timeout votes until the view advances A node's view timer fired exactly once per view, so its TimeoutVote2 broadcast was single-shot. With a live set exactly at the TC2 threshold (e.g. 7 nodes with 2 silenced at cutover), one lost broadcast deadlocked consensus permanently: no re-send, no timer re-fire, TC2 unicast only to the next leader. Observed as perm_silence_m2_m1 stalling for 240s with zero decisions in both post-#4689 CI runs. Re-arm the timer on fire so the vote is re-broadcast every timeout period until the view advances. Collectors dedup by (view, signer); participation and timeout metrics count once per view. * test(new-protocol): permit TC2-skip of boundary views in happy-path cutover Under start-time skew the cutover seed can be snapshotted before the QC for the last legacy view lands, so the smooth-start precondition fails and views 24/25 get TC2-skipped: legitimate behavior, same physics permitted for the perm sweep in #4689. Smooth start stays deterministic under src/tests/cutover.rs unit tests. Failed CI run 29425671834 with node 0 skipping view 24. * test(hotshot): failure budget for test_with_failures_2_with_epochs Recovery gaps after the view-5 crash land on different views run to run; the consistency task shut the test down on the first out-of-list gap, which also starved the successful-view count (CI run 29425671834: unexpected failed view 9, then only 9/20 successful views). Apply the #4663 budget pattern; num_successful_views=20 remains the gate. * fix(consensus): cutover QC handling and parent proposal fetch (#4702) * fix(new-protocol): re-broadcast timeout votes until the view advances A node's view timer fired exactly once per view, so its TimeoutVote2 broadcast was single-shot. With a live set exactly at the TC2 threshold (e.g. 7 nodes with 2 silenced at cutover), one lost broadcast deadlocked consensus permanently: no re-send, no timer re-fire, TC2 unicast only to the next leader. Observed as perm_silence_m2_m1 stalling for 240s with zero decisions in both post-#4689 CI runs. Re-arm the timer on fire so the vote is re-broadcast every timeout period until the view advances. Collectors dedup by (view, signer); participation and timeout metrics count once per view. * test(new-protocol): permit TC2-skip of boundary views in happy-path cutover Under start-time skew the cutover seed can be snapshotted before the QC for the last legacy view lands, so the smooth-start precondition fails and views 24/25 get TC2-skipped: legitimate behavior, same physics permitted for the perm sweep in #4689. Smooth start stays deterministic under src/tests/cutover.rs unit tests. Failed CI run 29425671834 with node 0 skipping view 24. * test(hotshot): failure budget for test_with_failures_2_with_epochs Recovery gaps after the view-5 crash land on different views run to run; the consistency task shut the test down on the first out-of-list gap, which also starved the successful-view count (CI run 29425671834: unexpected failed view 9, then only 9/20 successful views). Apply the #4663 budget pattern; num_successful_views=20 remains the gate. * fix cutover qc handling * fetch leaf if we only have a DA proposal in leaf and state map --------- Co-authored-by: Mathis Antony <sveitser@gmail.com> Co-authored-by: Mathis <mathis@espressosys.com> * Add more channel length metrics. (#4701) --------- Co-authored-by: Toralf Wittner <tw@dtex.org> Co-authored-by: Abdul Basit <45506001+imabdulbasit@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Chengyu Lin <linmrain@gmail.com> Co-authored-by: Mathis <mathis@espressosys.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Mathis Antony <sveitser@gmail.com>
This PR hardens the legacy → new-protocol cutover so the last legacy QC always reaches the new protocol and the boundary views decide deterministically, and fixes a legacy proposal-path bug where a DA-only state-map entry aborted the leader's proposal.
This PR:
Cutover QC handling (
fix cutover qc handling):cur_view(task-impls/src/consensus/mod.rs).Qc2Formedfires once, and the aggregator can form this QC before processing its ownViewChangeinto the cutover view; keying oncur_viewdropped it, so the cutover seed'shigh_qc(and theLegacyHighQcFormedbridge) missed it.new_version_first_view(task-impls/src/consensus/handlers.rs,<=→<). Legacy re-times-out the parked boundary view forever; bridging those votes could form a TC2 that skips the first new-protocol view even when its leader is online.new-protocol/src/consensus.rs,maybe_propose). The TC-time header request targets the lock held at that moment; if a bridged legacy QC moves the lock afterwards, the leader now re-issuesRequestBlockAndHeaderfor the adopted lock instead of hitting "no block header" until the view times out. The block builder dedups by(view, parent).loosepermitted-failures mode is removed, every test (including the permutation sweep) asserts the precise attributable failure set in both directions, and the happy path asserts zero failed views. Silencers now watch new-protocol coordinator progress (published viaAtomicU64) in addition to legacycur_view, so post-cutover kills land punctually.Parent proposal fetch (
fetch leaf if we only have a DA proposal in leaf and state map,task-impls/src/helpers.rs):Da/Failedstate-map entry carries no leaf/state, so it cannot be proposed on;parent_leaf_and_statenow treats it like a missing view and fetches the parent proposal. This matters when the leader forms the QC from unicast votes before validating the parent proposal itself — previously the DA entry masked the fetch and the proposal aborted.This PR does not:
SubmitLegacyHighQc/SubmitTimeoutVotehandling — only how legacy feeds the bridge and how the new-protocol state machine reacts once the lock moves.Key places to review:
task-impls/src/consensus/mod.rs,Qc2Formedarm — the guardnew_protocol_active(qc.view_number() + 1) && !new_protocol_active(qc.view_number())selects exactly the last-legacy QC, independent of when the aggregator's own view advances.new-protocol/src/consensus.rs,maybe_propose— the re-request'srequest_epochintentionally matches the eventualproposal_epochderivation below it (not the raw TC epoch used by the original request site inhandle_timeout_certificate); that is what makes the leader check correct for a first-of-epoch proposal.new-protocol/src/tests/legacy_cutover.rs,expected_failures— the exact failure model the whole suite now leans on: the silenced view, legacy unicast-vote spillover to the preceding view (for silenced views at or before the cutover), and the dead node's later leader slots.How to test this PR:
cargo nextest run -p hotshot-new-protocol— cutover suite (tests::legacy_cutover) and the new state-machine tests (tests::consensus).