Skip to content

[Security] PegOutGraph n_of_n_presigned set true after single verifier signs all txs, defeating n-of-n committee authorization #471

Description

@HusseinAdeiza

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

  1. A single verifier (or an operator who also acts as a verifier) calls verifier_sign with their own context.
  2. All peg-out transactions are pre-signed by that one verifier; n_of_n_presigned becomes true.
  3. operator_status returns statuses indicating the graph is past the assertion phase.
  4. 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.
  5. 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)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions