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:
- 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.
- 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}.
- Committee relayer:
node/node/src/handle.rs:6567 handle_disprove_sent_committee → node/crates/client/src/goat_chain/mod.rs:348 gateway_finish_withdraw_disproved → tx_reconstruct(challenge_finish_tx) (mod.rs:826, preserves raw input order) → finishWithdrawDisproved(QuickChallenge|ChallengeIncompleteKickoff).
- L2:
kickoffVout (3) != GUARDIAN_CONNECTOR_VOUT (4) → revert TxidMismatch(). Always.
- 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)
Summary
Cross-layer topology regression: the L2
Gatewayhard-codesGUARDIAN_CONNECTOR_VOUT = 4, but every GC-V2 L1 graph spends the kickoff guardian connector at vout 3. Every committee-relayedfinishWithdrawDisproved(QuickChallenge|ChallengeIncompleteKickoff)therefore revertsTxidMismatch()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 disprovesgoat(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 commit010bf28). The L1 GC-V2 rewiring (goat/src/transactions/base.rs:GUARDIAN_CONNECTOR = 3, commitdb6adf3) moved the kickoff guardian connector from vout 4 to vout 3:node/crates/bitvm-gc/src/operator/api.rs:145-167buildsquick_challenge/challenge_incomplete_kickoffwithinput[0] = guardian_connector_input(), i.e. outpoint(kickoffTxid, 3)._parseQuickChallengeTx/_parseChallengeIncompleteKickoffTx(BitvmTxParser.sol:69-89, 143-151) read input[0]'s outpoint as(kickoffTxid, kickoffVout).Gateway.sol:595-608requireskickoffVout == BitvmTxParser.GUARDIAN_CONNECTOR_VOUT(4). Real vout is 3 →revert TxidMismatch()on every call.Controls
CHALLENGE_CONNECTOR_VOUT = 0andDISPROVE_CONNECTOR_VOUT = 1still match L1 (connector_a at vout 0, connector_b at vout 1) — only the guardian constant is stale.Git history confirms the regression direction:
git log -S "GUARDIAN_CONNECTOR": onlydb6adf3(GC-V2, sets 3). Pre-GC-V2 kickoff (26b0bd6) had guardian at vout 4 (output_4).git log -S "GUARDIAN_CONNECTOR_VOUT": introduced010bf28(value 4), never updated through HEAD89dbee2.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)yieldsaddress(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:
node/node/src/utils.rs:2818verifier_quick_challenge, inputs[guardian_connector (kickoff vout 3), next_force_skip_connector]— no OP_RETURN output.QuickChallenge/ChallengeIncompleteKickoff:node/node/src/utils.rs:1286-1312detect_guardian_disprove,node/node/src/scheduled_tasks/graph_maintenance_tasks.rs:1200-1314; enqueuesDisproveSent{count:0}.node/node/src/handle.rs:6567handle_disprove_sent_committee→node/crates/client/src/goat_chain/mod.rs:348gateway_finish_withdraw_disproved→tx_reconstruct(challenge_finish_tx)(mod.rs:826, preserves raw input order) →finishWithdrawDisproved(QuickChallenge|ChallengeIncompleteKickoff).kickoffVout(3) !=GUARDIAN_CONNECTOR_VOUT(4) →revert TxidMismatch(). Always.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
0x60a5b099eE011018DD064F9eBD50d381C37b5e8b, impl0x1acdeb9e60359c8a490056ab5f14bad276037056.eth_getStorageAtslots 0-6 decode exactly toGatewayUpgradeable.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.bab46c2(GC-V2) confirmed read-only ingoat/src/transactions/kickoff.rs(output vec with guardian at index 3) andprekickoff.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):
Processing— cannot be finalized as fraud, and the operator's fraudulent action is never punished on L2.WithdrawDisprovedevent — the financial enforcement that makes guard-detection meaningful is dead.QuickChallengeorChallengeIncompleteKickoff.Suggested fix
Update
BitvmTxParser.GUARDIAN_CONNECTOR_VOUTto3to match the GC-V2 L1 topology, and add an OP_RETURN carrying the verifier/disprover's EVM address inbuild_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-210l2/src/Gateway.sol:555-669goat/src/transactions/base.rs(GUARDIAN_CONNECTOR=3, HEAD andbab46c2)goat/src/transactions/kickoff.rs;goat/src/transactions/prekickoff.rs:408-495,525-630node/crates/bitvm-gc/src/operator/api.rs:145-167;node/crates/bitvm-gc/src/verifier/api.rs:38-134node/node/src/utils.rs:1286-1312, 2818;node/node/src/handle.rs:6567;node/crates/client/src/goat_chain/mod.rs:348-422, 826-833db6adf3(GC-V2 reorder),26b0bd6(old guardian=vout4); L2010bf28(constant=4, unchanged)