[Backport release-ff] Light client fixes and V5→V6 upgrade tests#4742
Merged
Conversation
#4713) * fix(light-client): verify boundary 2-chain QCs against their own epoch A 2-chain proving the last leaf of an epoch includes a deciding QC produced in the subsequent epoch, signed by the next epoch's quorum. verify_qc_chain_and_get_version checked every certificate against the single quorum of the leaf's epoch, so fetching the last leaf of an epoch failed with "invalid threshold signature: Pairing check failed" whenever adjacent epochs have different stake tables. First observed when fetching the last leaf signed by the genesis stake table on a fresh node, where the next epoch uses the first contract-sourced stake table. Dispatch each certificate on the epoch committed in its signed payload: certificates from the leaf's epoch are checked against the proof's quorum, certificates from the following epoch against the next epoch's stake table. Any other epoch is rejected. The epoch number is part of the signed vote data, so a certificate claiming the wrong epoch fails signature verification. Regression tests (real BLS signatures over disjoint per-epoch quorums; both fail without the fix): - test_epoch_boundary_quorum_change - test_epoch_boundary_deciding_qc_wrong_quorum Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix mid epoch check * fix(light-client): align mock next-epoch QC placement with production The test client attached next_epoch_justify_qc to a leaf when the leaf's own block was in epoch transition. Production attaches it when the block justified by the leaf's QC (the parent) is in transition, so the mock dual-signed certificates one block too early and never dual-signed the QC for the last block of an epoch. The stricter verification introduced in this branch (rejecting a next-epoch QC on a non-transition certificate) correctly rejected these mock chains, failing every state::test stake table catchup test. Attach the QC based on the parent block, sign it with the quorum of the epoch after the parent's (which differs from the leaf's epoch + 1 at the epoch boundary), and commit the vote data under the parent's protocol version. Also gate the QC to pre-0.6 leaves: the new protocol justifies epoch changes with Certificate2 and always stores next_epoch_justify_qc: None in the Leaf2 representation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit f8cd09d)
* fix(light-client): report error cause when all fallback clients fail * refactor(light-client): use structured err field in fetch logs * refactor(light-client): use %err in fetch logs (cherry picked from commit 845a6e2)
* fix(light-client): reject HotStuff2 finality proofs across the cutover
The HotStuff2 2-chain commit rule is unsound for new-protocol (0.6) leaves:
consecutive phase-1 certificates exist even for leaves that never finalize,
so finality there requires a Certificate2. A malicious query server could
package two real phase-1 certs into a `HotStuff2` proof and convince a light
client that a non-finalized 0.6 leaf is final.
Tighten `LeafProof::verify`'s HotStuff2 arm to require both the proven leaf
and every certificate in the chain to predate the cutover:
- reject when the proven leaf is >= NEW_PROTOCOL_VERSION, and
- reject when any QC in the chain (i.e. the deciding QC) was verified at
>= NEW_PROTOCOL_VERSION, which happens when the chain spans the boundary.
To support the second check, `verify_qc_chain_and_get_version` now returns
`ChainVersions { leaf, max_cert }` exposing the highest version any cert was
verified under. The honest server never emits a spanning 2-chain (it switches
to the cert2 path at the first new-protocol leaf), so this costs no liveness.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(light-client): stop prover emitting 2-chains that span the cutover
LeafProof::push gated HotStuff2 completion only on the proven leaf's
version, but the deciding QC signs the following leaf. When proving the
last legacy leaf, that leaf is the first new-protocol leaf, so push
assembled a spanning 2-chain whose deciding QC is a post-cutover phase-1
certificate - exactly the proof the verifier now rejects - and the
server preferred it over the cert2 it had already fetched.
Gate completion on the deciding QC's leaf instead (which subsumes the
old check, since versions are non-decreasing along the chain), so the
prover falls through to a cert2 proof at the boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(light-client): trim verbose comments
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* verify light client proofs for v5 to v6
* test(light-client): cover upgrade_height-2 finality on both sides of the cutover
---------
Co-authored-by: Brendon Fish <bfish713@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 81eccae)
bfish713
requested review from
imabdulbasit,
jbearer,
lukaszrzasik and
sveitser
as code owners
July 22, 2026 16:36
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Contributor
|
Claude finished @bfish713's task in 2m 3s —— View job Review: backport of light-client fixes + V5→V6 upgrade tests
Verdict: LGTM. This is a faithful backport of #4713, #4726, and #4724. The three commits carry clean
|
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.
Backports the three light-client PRs that landed on
mainafter the last release-ff sync (#4712):test_light_client_new_protocol_upgrade)This PR:
release-ff(with-xback-references).light-client/src/consensus/quorum.rs: the earlier backport [Backport release-ff] fix(light-client): reject HotStuff2 finality proofs across the cutover #4728 had been adapted around the then-missing fix(light-client): verify boundary 2-chain QCs against their own epoch #4713 (ChainVersions/verify_next_epoch). The resolution restores main's original fix(light-client): reject HotStuff2 finality proofs across the cutover #4722 code, so fix(light-client): verify boundary 2-chain QCs against their own epoch #4713 and fix(light-client): reject HotStuff2 finality proofs across the cutover #4722 now compose exactly as on main.light-client/,crates/espresso/node/src/api{,/light_client}.rs, and.config/nextest.tomlare byte-identical tomain(verified withgit diff origin/main).This PR does not:
Consensus. #4698, chore(genesis): set mainnet base version to v5 #4693, refactor(demo): run native demo without Docker #4708, test(sql): run test postgres natively without docker #4715, chore: update Rust to 1.97.1 #4717).Key places to review:
light-client/src/consensus/quorum.rsin the first commit — the conflict resolution that reconciles the adapted [Backport release-ff] fix(light-client): reject HotStuff2 finality proofs across the cutover #4728 with fix(light-client): verify boundary 2-chain QCs against their own epoch #4713. Everything else applied cleanly.Things tested
cargo nextest run -p light-client: 96/96 pass.cargo check -p light-client -p espresso-node --testsclean.test_light_client_new_protocol_upgradeintegration test is left to CI (30-min budget).🤖 Generated with Claude Code