Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Security Model

This chapter catalogues every check in the system, the attack it prevents, and where trust boundaries lie.

Trust boundaries

flowchart LR
    subgraph Untrusted["Untrusted (Host)"]
        HOST[Host binary]
        WITNESS[Witness data]
        HINT[Hints: redeem length,<br/>block ordering]
    end

    subgraph Crypto["Cryptographically Enforced"]
        ZK[ZK proof verifier]
        SCRIPT[On-chain scripts]
        TXID[Transaction ID binding]
    end

    subgraph Enforced["Guest (ZK) Guarantees"]
        STATE[State transitions correct]
        BALANCE[Balances conserved]
        AUTH[Source authorized]
        SEQ[Block sequence valid]
        PERM[Permission tree correct]
    end

    HOST -->|"provides witnesses"| ZK
    ZK -->|"verifies guest execution"| STATE
    ZK -->|"verifies guest execution"| BALANCE
    ZK -->|"verifies guest execution"| AUTH
    ZK -->|"verifies guest execution"| SEQ
    ZK -->|"verifies guest execution"| PERM
    SCRIPT -->|"verifies ZK proof"| ZK
    TXID -->|"binds witnesses to chain"| SCRIPT

What the host can lie about

The host provides private inputs (witnesses) to the guest. Some are trusted hints, others are cryptographically bound.

Host inputBound byAttack if omitted
Block transaction dataOpChainblockSeqCommit on-chainHost could fabricate transactions
Current tx rest_preimagerest_digesttx_id (guest computes)Host could hide inputs/outputs
Previous tx preimageFirst input outpoint (from current tx rest_preimage)Host could claim false output SPKs
Account SMT witnessesRoot hash chain + assertHost could fabricate balances
Permission redeem lengthAssert in guestGuest script wouldn’t match on-chain hash
Action ordering within blockSeq commitment leaf hashOrder inherited from L1 transaction order; host cannot reorder or skip actions

Check catalogue

Guest-side checks

Checks are categorized as assert (host cheating — proof fails entirely) or skip (user error — action rejected but tx_id committed).

CheckLocationResponseAttack prevented
is_action_tx_id(tx_id)guest/src/block.rsgateNon-action transactions processed as actions
tx_id[0..2] == "AC"core/src/lib.rsgateRandom transactions misclassified (~1/65536 collision)
Action version == ACTION_VERSIONguest/src/tx.rsgateFuture/incompatible action formats
rest_digest == hash(rest_preimage)guest/src/tx.rscomputedHost cannot forge rest_digest (guest computes it)
First input outpoint matches prev_tx witnessguest/src/auth.rsassertHost substitutes fake prev_tx
Prev tx witness hashes to first input’s tx_idguest/src/auth.rsassertHost claims false output SPK/amounts
Witness pubkey matches action sourceguest/src/state.rsassertHost provides wrong witness for action
SMT proof verifies against rootguest/src/state.rsassertFabricated account balances (every pubkey has valid proof)
Source pubkey matches prev tx SPKguest/src/auth.rsskipUser submitted action with wrong SPK
Insufficient balanceguest/src/state.rsskipUser tried to spend more than they have
deduct > 0guest/src/state.rsskipZero-value withdrawal claims
Balance conservation (transfer)guest/src/state.rsenforcedValue creation from nothing
Entry output SPK is P2SH(delegate)core/src/p2sh.rsskipDeposits to unrelated addresses credited
Entry tx input 0 not permission suffixcore/src/prev_tx.rsskipDelegate change output misinterpreted as deposit
Exit SPK length <= EXIT_SPK_MAXcore/src/action.rsenforcedOversized SPK overflows
Seq commitment matches chainOn-chain OpChainblockSeqCommiton-chainFabricated block data
Permission root matches exitsguest/src/main.rsassertIncorrect withdrawal tree committed
Redeem script length matches host hintguest/src/main.rsassertScript hash mismatch

On-chain checks (state verification script)

CheckPhaseAttack prevented
Domain prefix [0x00, 0x75]StartScript type confusion
Prev state embedded in scriptPrefixState rollback or skip
OpChainblockSeqCommitSeq commitFabricated block sequence
Output 0 SPK == P2SH(new redeem)SPK verifyCovenant chain broken
Journal SHA-256 matches ZK proofZK verifyProof for different state transition
Program ID matchesZK verifyProof from different program
Input index == 0GuardScript used at wrong position
CovOutCount == 1 or 2Output branchUnexpected covenant outputs
Output 1 P2SH format (if 2 outputs)Perm verifyNon-P2SH permission output

On-chain checks (permission script)

CheckPhaseAttack prevented
deduct > 0Phase 3Zero-value claims drain UTXO
amount >= deductPhase 3Balance underflow
Output 0 SPK == leaf SPKPhase 4Withdrawal to wrong address
Old leaf hash verifies under rootPhase 6Fabricated Merkle proof
New root correctly computedPhase 7State corruption after claim
Unclaimed decremented correctlyPhase 8Count desync
Continuation SPK matchesPhase 9Permission UTXO chain broken
CovOutCount correctPhase 9Unexpected covenant outputs
output_count <= 4Phase 9Transaction stuffing
input_count <= N+2Phase 10Overcounting delegate inputs
Delegate SPK matches covenantPhase 10Spending unrelated UTXOs as delegates
Input N+1 not delegate SPKPhase 10Collateral miscounted as delegate
total_input >= deductPhase 10Insufficient delegate funds
Delegate change output correctPhase 10Change amount/address incorrect

On-chain checks (delegate/entry script)

CheckStepAttack prevented
Self not at input index 0Step 1Delegate used as primary covenant input
Input 0 covenant_id matchesStep 2Co-spent with wrong covenant
Input 0 suffix [0x51, 0x75]Step 3Co-spent with state verification (not permission)

Attack scenarios and mitigations

Malicious host substitutes fake prev_tx

Attack: The host provides a fabricated previous transaction witness with the “correct” pubkey in the output, but the action transaction doesn’t actually spend that UTXO. This would let the host authorize transfers/exits from any account.

Mitigation: The guest reads the current action transaction’s rest_preimage at the V1TxData level and computes rest_digest from it (never trusting a host-provided digest). It then parses the first input’s outpoint (prev_tx_id, output_index) from the rest_preimage. The host must provide a prev_tx witness that hashes to this exact prev_tx_id. Since the rest_preimage is committed via rest_digesttx_id, and the tx_id is bound to the chain via sequence commitment, the host cannot forge the first input. Mismatch causes an assertion failure (proof cannot be generated).

Malicious host provides invalid SMT proof

Attack: The host provides a fabricated SMT proof that claims a different balance for an account, enabling unauthorized spending.

Mitigation: Every pubkey in the sparse Merkle tree has a valid proof — either for the account’s actual leaf or for the empty leaf at that index. The guest asserts that every SMT proof verifies against the current root. Since a valid proof always exists, any verification failure means the host is provably lying. This is enforced with assert! (not skip), so the proof cannot be generated with invalid witnesses.

Malicious host credits fake deposits

Attack: Host provides a witness claiming a deposit output SPK that doesn’t actually pay to P2SH(delegate_script(covenant_id)).

Mitigation: Guest calls verify_entry_output_spk() which reconstructs the expected delegate script from the covenant_id, hashes it, and compares with the output’s P2SH hash. The tx_id binding ensures the output actually exists on-chain.

Delegate change output mistaken for deposit

Attack: A withdrawal transaction creates a delegate change output paying to the delegate script. A malicious host presents this as a new deposit in the next batch.

Mitigation: Guest calls input0_has_permission_suffix() on the entry transaction’s preimage. If input 0 ends with [0x51, 0x75] (permission domain), the transaction is a withdrawal — the output is delegate change, not a deposit.

Cross-covenant co-spending

Attack: A delegate input from covenant A is co-spent with a permission input from covenant B, draining A’s bridge reserve.

Mitigation: The delegate script embeds a specific covenant_id and verifies input 0 carries that same ID. Different covenants have different IDs, so cross-covenant co-spending fails.

Proof replay

Attack: A valid ZK proof from a previous state transition is replayed to revert state.

Mitigation: The journal includes prev_state_hash and prev_seq_commitment, both embedded in the redeem script prefix. The on-chain script verifies these match. Since the seq_commitment changes with every block, a replayed proof would fail the sequence check.

Collateral input overcounting

Attack: An attacker provides extra inputs beyond the delegate slots and hopes they’re counted toward the delegate balance.

Mitigation: Phase 10 enforces input_count <= MAX_DELEGATE_INPUTS + 2 and explicitly guards that input N+1 does NOT have the delegate SPK. The unrolled loop only sums inputs at indices 1..N.

Conservation properties

The system maintains two conservation invariants:

  1. L2 balance conservation: Every transfer preserves total L2 balance (amount sent equals amount received). Entries increase total L2 balance by the deposit amount. Exits decrease it by the withdrawal amount.

  2. Bridge reserve conservation: The permission script enforces delegate_input_total >= deduct and verifies the change output amount equals total - deduct. No value is created or destroyed during withdrawals.