Predictable bridge-out instance IDs let any caller permanently wedge the committee watcher's bridge-out cursor
Summary
An attacker can plant an instance record whose ID equals the publicly derivable ID of a future bridge-out request. When the legitimate request arrives, the store insert hits its conflict branch, the SwapInit handler aborts with an error, and the processing cursor stops advancing for that flow. Withdrawals behind the wedged cursor stay stuck until manual operator intervention. The attacker needs no privileges: the ID derives from public chain data before the legitimate request exists.
Details
- Type: Denial of service / state machine desync
- Asset: GOAT BitVM3 committee node, repository bitvm-node, commit
460d39e
- Location:
crates/store instance insertion path consumed by the bridge-out flow (postPeginRequest accepts a caller-supplied instanceId; the watcher derives bridge-out IDs deterministically in node/src/utils.rs lines 138 to 151)
- Auth required: none, the intake endpoint is permissionless
- Derivation observed in production:
instance_id = UUID(SHA256("bridge-out:" || escrow_hash as lowercase hex ASCII text)[..16]) with the first 4 bytes overwritten by ASCII BOID
Steps to reproduce
Environment:
- Run the integration test above. It exercises the real store code, no mocks.
- Test 1 plants an instance with the derived bridge-out ID (
424f4944-f604-9a71-1c10-020f1c1d6ea4), then submits the legitimate bridge-out request for the same escrow. The handler returns the collision error (bail! path).
- Test 2 advances the cursor after the failed handler and reads it again. Expected: cursor progresses past the poisoned entry. Actual: cursor stays frozen; the same record is retried forever.
- Test 3 (negative control) inserts a legitimate non-colliding ID. Insert succeeds, no wedge.
Full run output: https://gist.github.com/mr-hrydarie/c570e0b0c85f033f13117bb13c884219 (file poc/poc19_run.txt)
Impact
Demonstrated:
- A collision deterministically forces the SwapInit handler onto its error path.
- After the error, the cursor never advances; the flow stays wedged across restarts until manual repair.
- Only attacker-chosen IDs cause the wedge; normal traffic proceeds (negative control).
Observed in production, read-only, on 2026-08-25:
- All 11 bridge-out instances returned by
https://bitvm3-api-testnet4.goat.network/v1/instances?is_bridge_in=false&limit=50 matched the derivation above exactly. Full verification table: gist file evidence/live_testnet_corroboration.txt.
- Ten of the eleven carry visibly synthetic escrow hashes (placeholder patterns such as
deadbeef, repeated aa/dd/00/ff). Their acceptance by the production intake shows that arbitrary caller-chosen escrow content enters the publicly derivable bridge-out ID namespace, which is the precondition this finding requires.
- Bridge-in side: 50 instances listed, none carrying the reserved
BOID prefix, so no planted collision existed at check time.
Not shown:
- No live write was performed against the shared committee database.
- Fund theft is not part of this finding. The demonstrated impact is availability: targeted withdrawals can be stalled indefinitely and repeatedly, since the plant can be renewed after every cleanup.
Severity requested: Medium (CVSS:3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H, approximately 6.5; manual recovery bounds it below High).
Remediation
- Derive instance IDs server-side from escrow data at insert time; do not accept caller-supplied
instanceId.
- Make the cursor resilient: skip, log, and dead-letter a failing record instead of aborting the loop permanently.
- Reject planted records whose ID falls into the reserved bridge-out namespace, for example anything carrying the
BOID prefix.
Submitted as part of the GOAT BitVM3 bug bounty campaign (Aug 18 to Sep 18, 2026). Contact via GitHub or Tally submission under the same name.
Predictable bridge-out instance IDs let any caller permanently wedge the committee watcher's bridge-out cursor
Summary
An attacker can plant an instance record whose ID equals the publicly derivable ID of a future bridge-out request. When the legitimate request arrives, the store insert hits its conflict branch, the SwapInit handler aborts with an error, and the processing cursor stops advancing for that flow. Withdrawals behind the wedged cursor stay stuck until manual operator intervention. The attacker needs no privileges: the ID derives from public chain data before the legitimate request exists.
Details
460d39ecrates/storeinstance insertion path consumed by the bridge-out flow (postPeginRequestaccepts a caller-suppliedinstanceId; the watcher derives bridge-out IDs deterministically innode/src/utils.rslines 138 to 151)instance_id = UUID(SHA256("bridge-out:" || escrow_hash as lowercase hex ASCII text)[..16])with the first 4 bytes overwritten by ASCIIBOIDSteps to reproduce
Environment:
460d39e, test file atcrates/store/tests/poc_bridge_out_id_collision.rs(full source: https://gist.github.com/mr-hrydarie/c570e0b0c85f033f13117bb13c884219)cargo test -p store --test poc_bridge_out_id_collision424f4944-f604-9a71-1c10-020f1c1d6ea4), then submits the legitimate bridge-out request for the same escrow. The handler returns the collision error (bail!path).Full run output: https://gist.github.com/mr-hrydarie/c570e0b0c85f033f13117bb13c884219 (file
poc/poc19_run.txt)Impact
Demonstrated:
Observed in production, read-only, on 2026-08-25:
https://bitvm3-api-testnet4.goat.network/v1/instances?is_bridge_in=false&limit=50matched the derivation above exactly. Full verification table: gist fileevidence/live_testnet_corroboration.txt.deadbeef, repeatedaa/dd/00/ff). Their acceptance by the production intake shows that arbitrary caller-chosen escrow content enters the publicly derivable bridge-out ID namespace, which is the precondition this finding requires.BOIDprefix, so no planted collision existed at check time.Not shown:
Severity requested: Medium (CVSS:3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H, approximately 6.5; manual recovery bounds it below High).
Remediation
instanceId.BOIDprefix.Submitted as part of the GOAT BitVM3 bug bounty campaign (Aug 18 to Sep 18, 2026). Contact via GitHub or Tally submission under the same name.