Summary
State machine logic error in PegOutGraph: n_of_n_presigned is set to true after a single verifier pre-signs all transactions, rather than after all n verifiers have contributed their signatures. Because this flag gates operator_status and verifier_status, the operator can proceed to assert, challenge, and take funds before the full n-of-n committee has authorized the graph. The n-of-n multisig protection is effectively defeated.
Severity
High (defeats the n-of-n committee authorization; operator can finalize peg-out and claim funds without all verifier signatures).
Affected component
bitvm/bridge crate (bitvm/bridge/src/graphs/peg_out.rs) — a workspace default-member of the GOAT BitVM3 repo.
Root cause
verifier_sign (peg_out.rs:247-296) accepts a single verifier_context and pre-signs every peg-out transaction (assert_initial, assert_final, disprove_chain, disprove, kick_off_timeout, start_time_timeout, take_1, take_2) with that one verifier's keys. It then unconditionally sets:
self.n_of_n_presigned = true; // TODO: set to true after collecting all n of n signatures
(peg_out.rs:296)
This flag gates the state machine at two decision points:
verifier_status (peg_out.rs:1101): if self.n_of_n_presigned { ... } — advances verifier-side status to PegOutAssertAvailable, etc.
operator_status (peg_out.rs:1216): if self.n_of_n_presigned && self.is_peg_out_initiated() { ... } — advances operator-side status to PegOutAssertInitialAvailable, PegOutAssertCommitAvailable, PegOutTake1Available, etc.
Because n_of_n_presigned is set after the first verifier signs (line 296), not after collecting all n verifier signatures, the operator's operator_status immediately returns the post-assertion statuses and the operator can call broadcast_assert_initial, broadcast_assert_final, broadcast_disprove_chain, and ultimately broadcast_take_1/broadcast_take_2 to finalize the peg-out and claim funds — even though only one of the n required committee signers has actually contributed.
How it can be exploited
- A single verifier (or an operator who also acts as a verifier) calls
verifier_sign with their own context.
- All peg-out transactions are pre-signed by that one verifier;
n_of_n_presigned becomes true.
operator_status returns statuses indicating the graph is past the assertion phase.
- The operator broadcasts
assert_initial / assert_final / disprove_chain and then take_1 / take_2, consuming the crowdfunding output and the operator's own stake is returned, plus challenger rewards are paid — all authorized by a single verifier's signature instead of the required n-of-n.
- The remaining
n-1 verifiers never contributed signatures; their keys were never used.
Impact: the operator (or a colluding verifier) can finalize a peg-out and claim funds without the honest committee's authorization. Honest verifier funds/collateral can be stolen.
Suggested fix
Change verifier_sign (or the call site) to only set self.n_of_n_presigned = true after all n verifier contexts have called verifier_sign. Track the set of verifiers that have signed (e.g. a HashSet<PublicKey> or a counter) and set the flag when signers.len() == committee_pubkeys.len(). The TODO at line 296 already describes the intended behavior.
References
bitvm/bridge/src/graphs/peg_out.rs:247-296 (verifier_sign, the n_of_n_presigned = true assignment with TODO)
bitvm/bridge/src/graphs/peg_out.rs:1101 (verifier_status gating on n_of_n_presigned)
bitvm/bridge/src/graphs/peg_out.rs:1216 (operator_status gating on n_of_n_presigned && is_peg_out_initiated)
bitvm/bridge/src/graphs/peg_out.rs:684,1054 (n_of_n_presigned: false initialization)
bitvm/Cargo.toml:5-6,16-18 (bridge is a workspace default-member)
Summary
State machine logic error in
PegOutGraph:n_of_n_presignedis set totrueafter a single verifier pre-signs all transactions, rather than after allnverifiers have contributed their signatures. Because this flag gatesoperator_statusandverifier_status, the operator can proceed to assert, challenge, and take funds before the full n-of-n committee has authorized the graph. The n-of-n multisig protection is effectively defeated.Severity
High (defeats the n-of-n committee authorization; operator can finalize peg-out and claim funds without all verifier signatures).
Affected component
bitvm/bridgecrate (bitvm/bridge/src/graphs/peg_out.rs) — a workspacedefault-memberof the GOAT BitVM3 repo.Root cause
verifier_sign(peg_out.rs:247-296) accepts a singleverifier_contextand pre-signs every peg-out transaction (assert_initial,assert_final,disprove_chain,disprove,kick_off_timeout,start_time_timeout,take_1,take_2) with that one verifier's keys. It then unconditionally sets:(
peg_out.rs:296)This flag gates the state machine at two decision points:
verifier_status(peg_out.rs:1101):if self.n_of_n_presigned { ... }— advances verifier-side status toPegOutAssertAvailable, etc.operator_status(peg_out.rs:1216):if self.n_of_n_presigned && self.is_peg_out_initiated() { ... }— advances operator-side status toPegOutAssertInitialAvailable,PegOutAssertCommitAvailable,PegOutTake1Available, etc.Because
n_of_n_presignedis set after the first verifier signs (line 296), not after collecting allnverifier signatures, the operator'soperator_statusimmediately returns the post-assertion statuses and the operator can callbroadcast_assert_initial,broadcast_assert_final,broadcast_disprove_chain, and ultimatelybroadcast_take_1/broadcast_take_2to finalize the peg-out and claim funds — even though only one of thenrequired committee signers has actually contributed.How it can be exploited
verifier_signwith their own context.n_of_n_presignedbecomestrue.operator_statusreturns statuses indicating the graph is past the assertion phase.assert_initial/assert_final/disprove_chainand thentake_1/take_2, consuming the crowdfunding output and the operator's own stake is returned, plus challenger rewards are paid — all authorized by a single verifier's signature instead of the requiredn-of-n.n-1verifiers never contributed signatures; their keys were never used.Impact: the operator (or a colluding verifier) can finalize a peg-out and claim funds without the honest committee's authorization. Honest verifier funds/collateral can be stolen.
Suggested fix
Change
verifier_sign(or the call site) to only setself.n_of_n_presigned = trueafter allnverifier contexts have calledverifier_sign. Track the set of verifiers that have signed (e.g. aHashSet<PublicKey>or a counter) and set the flag whensigners.len() == committee_pubkeys.len(). The TODO at line 296 already describes the intended behavior.References
bitvm/bridge/src/graphs/peg_out.rs:247-296(verifier_sign, then_of_n_presigned = trueassignment with TODO)bitvm/bridge/src/graphs/peg_out.rs:1101(verifier_statusgating onn_of_n_presigned)bitvm/bridge/src/graphs/peg_out.rs:1216(operator_statusgating onn_of_n_presigned && is_peg_out_initiated)bitvm/bridge/src/graphs/peg_out.rs:684,1054(n_of_n_presigned: falseinitialization)bitvm/Cargo.toml:5-6,16-18(bridgeis a workspacedefault-member)