pos mempool: stop duplicate pivot-decision transactions from crashing proposal construction - #3540
Conversation
A registered validator could submit many distinct mempool transactions for
the same pivot decision by varying the unsigned `chain_id`: the pivot-decision
signature covers only the `PivotBlockDecision` payload, not the rest of the
`RawTransaction`, so each variant carries a valid signature but a different tx
hash. Those entries were counted as separate voting power in `get_block`'s
quorum check and produced a repeated signer index, making
`SignedTransaction::new_multisig().unwrap()` panic ("Duplicate signature
index") and crashing proposal construction on every validator that pulled the
poisoned set.
- Enforce one pivot-decision per (decision, sender) at admission.
- `new_multisig` returns `Result`; `get_block` dedups signer indices and
logs-and-skips on aggregation error instead of panicking.
- `gc_by_system_ttl` drops only the expiring (sender, hash) entry, not the
whole pivot-decision set, so one vote's TTL expiry no longer discards other
validators' live votes.
- `get_block`'s quorum check counts only current-committee voters (matching
selection and aggregation), so a registered non-committee node's vote is
ignored rather than failing the set with `UnknownAuthor`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
retest this please |
|
Kilo Code Review could not run — your account is out of credits. Add credits or switch to a free model to enable reviews on this change. |
…ix-pos-duplicate-pivot-decision
da34fa3 to
65c28b6
Compare
Code Review SummaryStatus: No New Issues Found | Recommendation: Merge (pending resolution of existing discussion) OverviewThis PR addresses a real consensus-safety bug: a Byzantine validator could craft many distinct-hash pivot-decision transactions (by varying the unsigned The fixes are well-structured across two commits:
The invariants introduced (one sender → one hash per decision, Open DiscussionThere is one existing active thread on Files Reviewed (6 files)
Reviewed by claude-sonnet-4.6 · Input: 15 · Output: 8.1K · Cached: 510.1K |
65c28b6 to
1f330e4
Compare
Problem
Pivot-decision transactions are signed over only the
PivotBlockDecisionpayload, not the whole transaction, so a registered validator can produce many valid, distinct-hash transactions for one pivot decision by varying the unsignedchain_id. The mempool stored each as a separate entry for that decision, and proposal construction fed them all into one multisig, where duplicates from a single sender collapse to the same signer index and the aggregation panics — crashing the proposal path on every validator that pulled the set. One registered validator could sustain this to stall PoS proposal construction.Fix
chain_idduplicates are dropped and can't produce the duplicate signer index.node_map) sender, broader than the committee, so a non-committee sender used to fail the whole set withUnknownAuthor; the check now counts only committee members, matching selection and aggregation.Mempool-only: no change to signed bytes, hashing, or on-chain validation. The dedup leaves a committed set that a node stored under a different
chain_idenvelope uncleaned until TTL; that cleanup is a follow-up. Bringingchain_idinto the pivot-decision signature is tracked separately.🤖 Generated with Claude Code
This change is