Skip to content

Predictable bridge-out instance IDs let any caller permanently wedge the committee watcher's bridge-out cursor #448

Description

@mr-hrydarie

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:

  1. Run the integration test above. It exercises the real store code, no mocks.
  2. 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).
  3. 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.
  4. 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

  1. Derive instance IDs server-side from escrow data at insert time; do not accept caller-supplied instanceId.
  2. Make the cursor resilient: skip, log, and dead-letter a failing record instead of aborting the loop permanently.
  3. 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.

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