Skip to content

[BitVM3 Bounty] Critical: Operator can forge watchtower-inclusion bitmap to defeat disprove and claim illegitimate Take #439

Description

@lazyleon-hub

[BitVM3 Bounty] Critical: Operator can forge watchtower-inclusion bitmap to defeat disprove and claim illegitimate Take
Summary
A malicious operator can obtain a Take (Take1/Take2) reimbursement it is not entitled to by forging the included_watchtowers bitmap inside the operator-proof circuit. The forgery is enabled by an unbound Taproot signature check; the forged bitmap then disables the on-chain disprove hashlock — the only protection against a fraudulent operator claim. Result: unauthorized withdrawal / peg insolvency.
Affected components
Circuit (GOATNetwork/bitvm-node):
crates/bitcoin-light-client-circuit/src/signature.rs:45-72 — verify_taproot_leaf_schnorr_signature
crates/bitcoin-light-client-circuit/src/lib.rs:48-52 — OperatorPublicOutputs
crates/bitcoin-light-client-circuit/src/lib.rs:255-291 — watchtower inclusion loop
crates/bitcoin-light-client-circuit/src/lib.rs:595-620 — verify_included_watchtower_challenge
circuits/operator-proof/guest/src/main.rs:22-48 — watchtower tx/prevout/pubkey/script read as PRIVATE prover inputs
Bitcoin-side game (GOATNetwork/BitVM, branch GA):
goat/src/transactions/take1.rs:60-140, goat/src/transactions/take2.rs:69-137,246-259 — Take assembly (no proof verification)

  1. Malicious operator initiates a peg-out/kickoff for a claim a watchtower can legitimately challenge.
  2. Honest watchtower publishes its genuine challenge and attempts the disprove hashlock. Because the committed bit is 1, the disprove script fails (disprove_scripts.rs:194-209: disprove only succeeds when the bit is 0).
    Two linked defects: (1) the Taproot signature check authenticates a watchtower challenge against attacker-supplied prev_out/pubkey with no binding to the real spent UTXO, letting the operator fabricate "included" challenges and flip inclusion bits; (2) the included_watchtowers bitmap gates the disprove hashlock — setting a bit to 1 makes an honest watchtower's disprove fail. The Take transactions perform no proof verification, so once disprove is neutralized, reimbursement is gated only by a relative timelock.
  3. spending_tx.input[0].previous_output = OutPoint{ txid: T_real, vout: 0 }.
  4. verify_taproot_leaf_schnorr_signature(&script, &spending_tx, 0, &prev_out, &P_atk, &sig) returns Ok(()) — inclusion asserted for a fabricated challenge not signed by the real watchtower and not spending the claimed UTXO.
  5. Bind the verified prevout to the tx's real input (assert supplied prev_out equals the UTXO spent by input[0]; ideally derive it from an SPV inclusion proof of the watchtower-challenge tx).
  6. Require SPV/header-chain inclusion of each watchtower-challenge tx before setting its inclusion bit — never trust prover-supplied txs.
  7. Bind the watchtower pubkey to the registered watchtower set.
  8. Use the real input index (don't hardcode 0 / reuse vout) and prefer SIGHASH_DEFAULT/SIGHASH_ALL with Prevouts::All(...).

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