Skip to content

[Security] L2 Gateway requires GUARDIAN_CONNECTOR_VOUT=4 but GC-V2 L1 graphs spend the kickoff guardian connector at vout 3 - every QuickChallenge/ChallengeIncompleteKickoff disprove reverts TxidMismatch #470

Description

@HusseinAdeiza

Summary

Cross-layer topology regression: the L2 Gateway hard-codes GUARDIAN_CONNECTOR_VOUT = 4, but every GC-V2 L1 graph spends the kickoff guardian connector at vout 3. Every committee-relayed finishWithdrawDisproved(QuickChallenge|ChallengeIncompleteKickoff) therefore reverts TxidMismatch() forever. The guardian-connector punishment path is dead end-to-end on the deployed testnet4 stack.

Severity

High (permanent DoS of a financial enforcement path; no operator slash, no challenger/disprover reward).

Affected components

  • bitvm-L2-contracts: src/libraries/BitvmTxParser.sol (GUARDIAN_CONNECTOR_VOUT = 4, line 16)
  • bitvm-node / bitvm-gc: relay path for QuickChallenge / ChallengeIncompleteKickoff disproves
  • goat (L1): GC-V2 kickoff topology (guardian connector at vout 3)

Root cause

BitvmTxParser.GUARDIAN_CONNECTOR_VOUT = 4 (src/libraries/BitvmTxParser.sol:16, unchanged since GA-era commit 010bf28). The L1 GC-V2 rewiring (goat/src/transactions/base.rs:GUARDIAN_CONNECTOR = 3, commit db6adf3) moved the kickoff guardian connector from vout 4 to vout 3:

  • node/crates/bitvm-gc/src/operator/api.rs:145-167 builds quick_challenge / challenge_incomplete_kickoff with input[0] = guardian_connector_input(), i.e. outpoint (kickoffTxid, 3).
  • L2 _parseQuickChallengeTx / _parseChallengeIncompleteKickoffTx (BitvmTxParser.sol:69-89, 143-151) read input[0]'s outpoint as (kickoffTxid, kickoffVout).
  • Gateway.sol:595-608 requires kickoffVout == BitvmTxParser.GUARDIAN_CONNECTOR_VOUT (4). Real vout is 3 → revert TxidMismatch() on every call.

Controls CHALLENGE_CONNECTOR_VOUT = 0 and DISPROVE_CONNECTOR_VOUT = 1 still match L1 (connector_a at vout 0, connector_b at vout 1) — only the guardian constant is stale.

Git history confirms the regression direction:

  • L1 git log -S "GUARDIAN_CONNECTOR": only db6adf3 (GC-V2, sets 3). Pre-GC-V2 kickoff (26b0bd6) had guardian at vout 4 (output_4).
  • L2 git log -S "GUARDIAN_CONNECTOR_VOUT": introduced 010bf28 (value 4), never updated through HEAD 89dbee2.

Secondary (same path): the broadcast quick-challenge / challenge-incomplete-kickoff transaction carries no OP_RETURN (node/crates/bitvm-gc/src/verifier/api.rs:38-134, change/anchor outputs only), so _parseOpReturnAddress(outputVector, 0) yields address(0) and the disprover is never rewarded even if the vout check were corrected.

Steps to reproduce

Any guard-detectable misbehavior that triggers the quick-challenge or challenge-incomplete-kickoff disprove flow:

  1. Operator force-skips a kickoff (or commits an incomplete kickoff). Verifier broadcasts the quick-challenge tx: node/node/src/utils.rs:2818 verifier_quick_challenge, inputs [guardian_connector (kickoff vout 3), next_force_skip_connector] — no OP_RETURN output.
  2. On-chain detection classifies the spend as QuickChallenge / ChallengeIncompleteKickoff: node/node/src/utils.rs:1286-1312 detect_guardian_disprove, node/node/src/scheduled_tasks/graph_maintenance_tasks.rs:1200-1314; enqueues DisproveSent{count:0}.
  3. Committee relayer: node/node/src/handle.rs:6567 handle_disprove_sent_committeenode/crates/client/src/goat_chain/mod.rs:348 gateway_finish_withdraw_disprovedtx_reconstruct(challenge_finish_tx) (mod.rs:826, preserves raw input order) → finishWithdrawDisproved(QuickChallenge|ChallengeIncompleteKickoff).
  4. L2: kickoffVout (3) != GUARDIAN_CONNECTOR_VOUT (4) → revert TxidMismatch(). Always.
  5. Result: withdrawal remains Processing; slashStake (Gateway.sol:646) never runs; challenger/disprover rewards (648-655) never pay; emit WithdrawDisproved (657-668) never fires.

Proof of concept / deployed verification

  • On-chain (testnet4): Gateway UUPS proxy 0x60a5b099eE011018DD064F9eBD50d381C37b5e8b, impl 0x1acdeb9e60359c8a490056ab5f14bad276037056. eth_getStorageAt slots 0-6 decode exactly to GatewayUpgradeable.initialize() from this source (minChallengeAmountSats, fees, staking, challenge/disprover rewards, PegBTC address) — so the deployed implementation is this exact lineage with the stale constant, not hypothetical.
  • L1 topology at interop pinned commit bab46c2 (GC-V2) confirmed read-only in goat/src/transactions/kickoff.rs (output vec with guardian at index 3) and prekickoff.rs:408-495,525-630 (challenge-finish input[0] = guardian connector).

Security impact

For any withdrawal that must be finalized via the guardian-connector punishment (operator skipped / incompletely committed a kickoff):

  • The withdrawal is permanently stuck in Processing — cannot be finalized as fraud, and the operator's fraudulent action is never punished on L2.
  • No operator stake slash, no challenger/disprover rewards, no WithdrawDisproved event — the financial enforcement that makes guard-detection meaningful is dead.
  • Affects every graph whose final disprove type is QuickChallenge or ChallengeIncompleteKickoff.

Suggested fix

Update BitvmTxParser.GUARDIAN_CONNECTOR_VOUT to 3 to match the GC-V2 L1 topology, and add an OP_RETURN carrying the verifier/disprover's EVM address in build_quick_challenge_tx / build_challenge_incomplete_kickoff_tx (or validate address equality another way, e.g. an explicit field set by the relayer only after on-chain verification).

References

  • l2/src/libraries/BitvmTxParser.sol:16,69-89,143-151,198-210
  • l2/src/Gateway.sol:555-669
  • goat/src/transactions/base.rs (GUARDIAN_CONNECTOR=3, HEAD and bab46c2)
  • goat/src/transactions/kickoff.rs; goat/src/transactions/prekickoff.rs:408-495,525-630
  • node/crates/bitvm-gc/src/operator/api.rs:145-167; node/crates/bitvm-gc/src/verifier/api.rs:38-134
  • node/node/src/utils.rs:1286-1312, 2818; node/node/src/handle.rs:6567; node/crates/client/src/goat_chain/mod.rs:348-422, 826-833
  • Git: L1 db6adf3 (GC-V2 reorder), 26b0bd6 (old guardian=vout4); L2 010bf28 (constant=4, unchanged)

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