Skip to content

pos mempool: stop duplicate pivot-decision votes from crashing proposal construction#3540

Open
peilun-conflux wants to merge 1 commit into
Conflux-Chain:masterfrom
peilun-conflux:fix-pos-duplicate-pivot-decision
Open

pos mempool: stop duplicate pivot-decision votes from crashing proposal construction#3540
peilun-conflux wants to merge 1 commit into
Conflux-Chain:masterfrom
peilun-conflux:fix-pos-duplicate-pivot-decision

Conversation

@peilun-conflux

@peilun-conflux peilun-conflux commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Pivot-decision transactions are signed over only the PivotBlockDecision payload, not the whole RawTransaction. A registered validator can therefore produce many valid, distinct-hash transactions for the same vote by varying the unsigned chain_id. The mempool stored each as a separate entry for the same pivot decision, so during proposal construction (Mempool::get_block) they were counted as separate voting power and produced a repeated signer index — making SignedTransaction::new_multisig().unwrap() panic with BitVecError("Duplicate signature index") and crashing the proposal path on every validator that pulled the poisoned set. A single registered validator could sustain this to stall PoS proposal construction.

Fix

  • Admission: at most one pivot-decision transaction per (decision, sender); later distinct-hash duplicates are dropped. Closes both the voting-power overcount and the duplicate-signer-index panic at the source.
  • Never panic in proposal construction: new_multisig returns Result, and get_block dedups signer indices and logs-and-skips on aggregation error instead of unwrap()-panicking.
  • GC consistency: gc_by_system_ttl removes only the expiring (sender, hash) entry, not the whole pivot-decision set, so one vote's TTL expiry no longer discards other validators' still-live votes.
  • Quorum scoping: get_block's quorum check counts only current-committee voters (consistent with selection and aggregation, which already filter), so a registered non-committee node's valid vote is ignored instead of failing the set with UnknownAuthor and stalling pivot decisions.

Whether chain_id (and the rest of the envelope) should be brought into the pivot-decision signature is tracked separately; this PR is the mempool-level hotfix and changes no on-chain validation rules.

🤖 Generated with Claude Code


This change is Reviewable

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>
Comment thread crates/cfxcore/core/src/pos/mempool/core_mempool/mempool.rs
@Pana

Pana commented Jun 3, 2026

Copy link
Copy Markdown
Member

retest this please

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants