Skip to content

[Security] Verifier KickoffSent challenge is one-shot and fail-open (unauthorized Take1) #429

Description

@asbestos22

Summary

Unauthorized kickoff (no L2 initWithdraw) is in the Gateway threat model. The automated defense is handle_kickoff_sent_verifier: if withdraw is None/Canceled, broadcast Challenge.

When kickoff_height >= btc_spv_latest_height() the handler logs and returns Ok(()) without push_local_unhandled_messages_with_reason. The local queue then marks the message Processed. detect_kickoff only scans OperatorDataPushed, so it never re-enqueues. After ConnectorA CSV the operator can Take1. PegBTC was never burned.

Committee handle_kickoff_sent_committee already retries the same SPV lag. Verifier does not. The verifier check is also strict > (skips when SPV == H), while committee proceeds at SPV >= H.

Not proven here: live kickoff broadcast, live Take1, BTC leaving the peg. This is a complete code path on stock gc-v2, not a live steal.

Affected component

GOATNetwork/bitvm-node gc-v2 — Bridge State Machine (verifier challenge path).

  • node/src/handle.rs handle_kickoff_sent_verifier — skip + no defer when kickoff_height >= goat_confirmed_btc_height
  • node/src/handle.rs handle_kickoff_sent_committeedoes defer the same SPV lag
  • node/src/action.rsOk(()) → message Processed
  • node/src/scheduled_tasks/graph_maintenance_tasks.rs detect_kickoff — only OperatorDataPushed
  • node/src/utils.rs upsert_message(..., is_update: false) — will not recreate {graph_id}_KickoffSent
  • handle_take1_ready_operator — no L2 withdraw-status check
  • should_always_challenge() is defined and never called
  • Watchtower / Operator / Actor::All hit handle_kickoff_sent_default (status only)

Checked on gc-v2 @ f2f0285e. Not #416 (recursion bind), not #418 (Take1/Challenge CSV race — scripts are fine if Challenge is actually sent), not #428, not #349.

Mechanism

if [WithdrawStatus::None, WithdrawStatus::Canceled].contains(&withdraw_status) {
    if kickoff_height >= goat_confirmed_btc_height {
        tracing::warn!(/* not confirmed by goat spv yet */);
        return Ok(()); // no push_local_unhandled_messages_with_reason
    } else {
        send_challenge_tx(...);
    }
}

First detection is when Bitcoin confirms kickoff. GOAT SPV lags the Bitcoin tip, so kickoff_height >= goat_confirmed is the common first-confirm case. Every online honest verifier processes once, skips, and is done.

Steps (code path)

  1. Graph is committee-presigned and OperatorDataPushed. L2 withdraw for graph_id is None.
  2. Operator broadcasts prekickoff+kickoff. PegBTC stays circulating.
  3. detect_kickoff enqueues KickoffSent to Actor::All.
  4. Each Verifier: refresh_graph_statusOperatorKickOff; withdraw None; kickoff_height >= SPV → log and Ok(()).
  5. Message → Processed. No later handler re-checks this graph for an L2-invalid kickoff.
  6. Wait ConnectorA timelock.
  7. Operator signs Take1 (n-of-n already pre-signed on connector_0) and broadcasts.
  8. proceedWithdraw / finishWithdrawHappyPath cannot run (status != Initialized).

A verifier that first comes online only after SPV > H and has an empty local DB could still challenge. Deployed verifiers keep persistent DBs. Restart does not recreate KickoffSent.

Impact (proven vs not)

Proven (source + local predicate)
Stock online verifier skips Challenge on first confirm (SPV lag or SPV == H) and never retries. Operator Take1 after CSV has no L2 withdraw check.

Not proven
Live kickoff / Take1 / BTC moved / undercollateralized peg on testnet or mainnet.

Suggested fix

On None/Canceled + SPV not strictly past kickoff: push_local_unhandled_messages_with_reason (same pattern as committee). Challenge when SPV >= kickoff_height (or keep > but retry). Periodically re-check OperatorKickOff + withdraw ∈ {None, Canceled} + connector_a unspent and send Challenge. Optionally have Watchtower call send_challenge_tx on the same predicate.

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