# A Bitcoin Swap on ETC

An SPV swap that pays ETC for a Bitcoin payment, checked on ETC from Bitcoin's own proof of work. An optional stronger check asks that the payment's block sits under a Bitcoin head that etchd operators recorded, and the same contracts prove etchd's hourly roll-up into Bitcoin on ETC.

**Alpha, unaudited.** BitcoinSwap, BitcoinHeaders and BitcoinOnEtc are examples. Nobody has audited them. Nothing here comes with a guarantee. [Alpha, No Guarantees](https://etchd.app/docs/trust#alpha) says what that means.

## Why

Bob has ETC and wants BTC. Alice has BTC and wants ETC. Bob opens a swap on ETC and locks his ETC in it for Alice, naming the number of satoshis he wants and the script of a fresh Bitcoin address of his own. Alice pays those satoshis to that address with an ordinary Bitcoin transaction from any wallet. After six confirmations anyone submits the transaction, its Merkle path and a run of seven linked headers, and the contract pays Bob's ETC to Alice. If nobody claims before the timeout, Bob takes his ETC back.

Nobody holds the coins in between and nobody reports the payment. The contract hashes the headers itself, checks their work, follows the Merkle path into the first one and reads the transaction's outputs. Alice needs no Bitcoin script beyond a plain payment and no second step after she pays. This is simplified payment verification as the Bitcoin paper describes it, run inside an ETC contract.

### Alice's Checklist

Before she pays, Alice reads the swap by its id, `swaps(id)`, and checks six fields against what she agreed with Bob.

| Field | What Alice checks |
| --- | --- |
| script | The scriptPubKey she will pay, byte for byte |
| sats | The amount she will send, at least this many satoshis |
| alice | Her own ETC address, which the claim pays |
| timeout | An ETC block time far enough out for six confirmations and a claim |
| deposit | The ETC Bob locked, the amount she expects to receive |
| bob | Bob's ETC address, the one that sent the open |

The last two are what a copied swap cannot match. Anyone can read Bob's `open` from the mempool and send the same script, sats, alice and timeout first with a dust deposit. That swap takes the script, and Bob's own `open` reverts `ScriptUsed`. Its deposit is dust and its `bob` is the stranger. So Alice runs the check at the moment she pays, with the deposit and Bob's address given, and pays only when it exits 0.

Alice, right before she pays

```bash
node scripts/examples/btc.ts swap-show 7 --swap 0xSWAP... \
  --expect-deposit 100000000000000000 --expect-bob 0xB0B... \
  --expect-script 0014a1b2... --expect-sats 150000 --expect-alice 0xA11CE... --expect-timeout 1793707200
```

`swap-show` prints script, sats, alice, timeout, the deposit and bob, and exits 3 when any expected value differs. It also exits 3 when `--expect-deposit` or `--expect-bob` is missing, since a check without them passes on a copied swap. A stranger who takes a script first costs nobody funds. Bob opens again on a fresh script, and Alice checks the new id.

## What It Trusts

The swap trusts no one. Its claim holds unless somebody spends real Bitcoin work to forge a run of headers, and the price of that forgery sets how much the contract can hold. The base swap has no operator and no oracle.

BitcoinHeaders refuses a header whose target is above a floor fixed at deploy. The demo sets the floor at four times the target of deploy day, a quarter of the difficulty, which survives a 75% drop in hashrate. The floor alone is weak. A forger mines every header at the floor, so one forged header costs a quarter of a block's work, about 0.78 BTC at the 3.125 BTC subsidy, and a forged run of six costs about 1.5 block rewards, about 4.7 BTC. The floor is fixed in hashes, so it weakens against the real chain as difficulty rises.

So the swap also requires `minWork` over the whole run. The demo asks for six confirmations, a run of seven headers, and sets `minWork` to half of seven times the work per block on deploy day, 2.1e24 hashes. An honest run passes until difficulty falls by half, and a forged run costs at least 3.5 blocks of real work, about 10.9 BTC at today's subsidy and about half that after the 2028 halving, fees aside.

One forged run pays out every open swap of a contract at once. A forger who mines seven headers chooses the first header's Merkle root freely, so that block can hold a fake payment for every swap open at that moment, one output each, and forged header times pass every `openedAt` window. The bound is therefore per contract. BitcoinSwap keeps `totalLocked`, the sum of open deposits, and refuses an `open` that takes it above the immutable `maxLocked`. A contract should hold less in total than a forged run costs. The demo caps each swap at 0.1 ETC with `maxValue` and the whole contract at 1 ETC, far below that cost at today's prices.

A single header proves only that someone did its work. It can be stale or orphaned and say nothing about the best chain, which is what the linked run and `minWork` are for.

## How the Contract Checks It

`claim(id, payment)` runs before the swap's `timeout`, and anyone can send it. The payment holds the transaction, the output index, the transaction's Merkle path and index, the block's coinbase with its path, and the run of headers.

1. `BitcoinTx.parse` reads the transaction in its non-witness serialization, the bytes its txid hashes. Version, input count, every input with its outpoint, script and sequence, output count, every output with its 8-byte amount and script, locktime, and nothing after it, else `BadTx`. Every count and length is a minimally encoded CompactSize that fits the bytes left. An input count of zero is refused, which also refuses the segwit serialization, whose marker byte sits where the input count goes.
2. The transaction is not a coinbase, so its single input is not the null outpoint, else `IsCoinbase`. A coinbase output cannot be spent for 100 blocks and vanishes with a reorg.
3. Output `vout` alone is checked. Its script equals the swap's script byte for byte, by length and keccak256, never as a substring, else `WrongScript`, and its amount is at least `sats`, else `Underpaid`.
4. The run holds at least `confirmations + 1` headers of 80 bytes. Each one is hashed with `sha256(sha256(header))` through the sha256 precompile, its `bits` decoded with negative, zero and out-of-range mantissas refused, its target at or under the floor, and its hash read little-endian at or under its target. Each header's `prev` equals the hash before it, and the work `2^256 / (target + 1)` summed over the run is at least `minWork`, else `InsufficientWork`.
5. The full transaction is hashed to its txid. It must not be 64 bytes, else `Tx64`. The Merkle fold with `sha256d` at each level uses exactly as many siblings as the index has bits and must reach the first header's Merkle root, else `NotInBlock`.
6. The coinbase is proven at index 0 to the same root with the same number of siblings, and bytes 4 to 40 of it must be an input count of 1 and the null outpoint, else `BadCoinbase`. The coinbase is not parsed further, so the gas does not grow with the outputs a pool puts in it.
7. The first header's time plus 7,200 s is at or after the swap's `openedAt`, else `BeforeOpen`, so a payment to the same script from long before the swap cannot claim it.
8. `spent[txid][vout]` is unset, else `AlreadySpent`, and the claim sets it, subtracts the deposit from `totalLocked` and pays Alice. A recipient that rejects ETC is credited and takes it later with `withdraw`.

`refund(id)` after the timeout returns an unclaimed deposit to Bob and subtracts it from `totalLocked`.

### The Script Lock

`open` refuses a script that any swap of this contract has used, through `usedScript[keccak256(script)]`, which it sets and which nothing clears, not a claim and not a refund. Without it, after Alice pays the script of swap 1, Bob or a stranger opens swap 2 on the same script with `sats` set to 1, his own address as `alice` and a dust deposit. The `openedAt` window allows that for two hours after the payment's block, and six confirmations take about one. He claims swap 2 with Alice's transaction and gets his dust back, the payment is marked spent, Alice's claim of swap 1 reverts, and Bob refunds swap 1 after the timeout, keeping the BTC and the ETC. With the lock, swap 2 cannot open, and since a refund leaves the lock set, an old payment can never claim a new swap on the same script.

The lock guards the swaps of one contract only. Header times run up to two hours ahead, so the `openedAt` window alone does not stop an older payment to a reused script. Bob uses a fresh script for every swap and never reuses one across contracts or for anything else.

### Replay and the Transaction Index

Replay protection keys on `(txid, vout)` and never on the transaction's index or on the block and index. Bitcoin's Merkle tree duplicates the last leaf of an odd level, so the same transaction can be proven at a phantom index (CVE-2012-2459), and the output index lets one transaction pay two swaps on two scripts. The swap's `timeout` is an ETC `block.timestamp`, since Bitcoin miners can set a header's time about two hours off.

## The Optional Quorum-Head Check

A swap deployed with an `onEtc` address also asks that the newest header of the run is a Bitcoin head that a quorum of etchd operators recorded. BitcoinOnEtc takes a list of operators, strictly ascending and pinned at deploy, and a quorum. One operator runs today, so the demo lists calendar `a`, run by Classix, with quorum 1. More operators plug in the same way, with a new BitcoinOnEtc whose list includes them.

`proveHead(proofs, header)` takes at least `quorum` head records, from operators in ascending order and each in the pinned list. Each record is 104 bytes, schema 1, with the chain field equal to `sha256("bip122:000000000019d6689c085ae165831e93")`, kind 0 for a head, header format 1 and a 32-byte hash, proven with `recordExistedAtBy(operator, 2, record, path)`. They all name the same height and hash, the hash is that of the header given, and the header passes its own proof of work and the floor. The contract stores the block's height and `headBy`, the latest anchor time of that quorum, so a quorum of operators saw that block at that height as the head by that time.

A block is recorded more than once, since every export window's first record and every reorg back record its head again. `proveHead` keeps the earliest `headBy`. A later call replaces it only with a smaller one, so nobody can move a block's `headBy` later, and anyone who proves the earlier records corrects a late one.

About 9.5% of Bitcoin tips are blocks that the calendar skipped as a head, with no head record of their own. With `--on-etc`, `etchd btc tx` extends the run to the first header that has a quorum of head records and prints the `proveHead` call that must land before the claim.

### Etchd's Roll-Up, Proven on ETC

Every hour etchd's roll-up carries the tick roots of the hour into a Bitcoin block through OpenTimestamps. `proveEtchdTick(operator, tick, ops, headers)` ties the two chains together on ETC. `tickExistedAtBy` on the verifier gives the tick root and its ETC anchor time from the listed operator, an OpenTimestamps branch takes that root to the Merkle root of the first header, and `checkChain` with `minWork` gives the confirmations. The operator must be listed, since the registry is open and anyone could anchor a fake tick root from an address of their own and get a real OpenTimestamps branch for it an hour later.

The contract stores the tick root's ETC time, its Bitcoin block, that block's time and the work of the run, and replaces an entry only with a heavier run. Until the honest proof lands, a forged run at `minWork` can set an early Bitcoin time, so the entry means at least this much work, and `etchd btc tick` builds the longest real run it can reach. The [Bitcoin Bridge](https://etchd.app/docs/bitcoin) describes the roll-up itself.

## Gas

Measured on Anvil with Shanghai rules and chain id 61 in [swap.test.ts](https://github.com/classix-dev/etchd/blob/main/packages/contract/test/swap.test.ts) and [bitcoin.test.ts](https://github.com/classix-dev/etchd/blob/main/packages/contract/test/bitcoin.test.ts), against the real fixture block. Each figure is a whole transaction.

| Call | Gas |
| --- | --- |
| open, with the usedScript lock | 184,086 |
| claim with 13 levels, the 319-byte fixture coinbase and 7 headers | 160,002 |
| claim with 13 levels and a 4,116-byte coinbase, the size pools that pay many miners produce | 219,150 |
| claim with the onEtc head check | 165,638 |
| check of one header | 26,463 |
| checkChain of seven linked headers | 56,127 |
| txInBlock with 13 levels, the coinbase and one header | 66,277 |
| proveHead with three listed test accounts | about 140,600 |
| proveEtchdTick with three headers | 160,570 |
| digestInBlock with the 1,353-byte OpenTimestamps branch and one header | 169,547 |

## Figures

BitcoinSwap parses Alice's transaction in its non-witness form and checks that one output pays Bob's script at least the agreed sats. The transaction's sha256d is its txid, which climbs a Merkle path to the merkle root of an 80-byte header, with the coinbase proven at index 0 at the same depth. The header's sha256d has to be at or below the target its bits encode, and that target at or below the contract's floor. Seven linked headers, the block and six on top, have to sum to minWork before the swap pays Alice.

Every tick root is anchored on Ethereum Classic within the minute, and the hourly roll-up carries the same root through OpenTimestamps into a Bitcoin block. BitcoinOnEtc checks both from ETC. The anchor goes through tickExistedAtBy for a listed operator, and the Bitcoin block through the OpenTimestamps branch and a run of headers that reaches minWork. The contract stores the tick root with its ETC time, its Bitcoin block and time, and the work of the heaviest run proven.

## The Source

Four files, compiled with solc 0.8.38, optimizer 200 and EVM version shanghai, with the settings in [packages/contract/verify](https://github.com/classix-dev/etchd/tree/main/packages/contract/verify). BitcoinSwap holds the swaps, BitcoinHeaders checks headers and inclusion with no trust in anyone, BitcoinTx is the strict transaction parser, and BitcoinOnEtc ties in etchd's records.

[BitcoinSwap.sol](https://github.com/classix-dev/etchd/blob/main/packages/contract/src/examples/BitcoinSwap.sol)

BitcoinSwap.sol

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

import {BitcoinHeaders} from "./BitcoinHeaders.sol";
import {BitcoinOnEtc} from "./BitcoinOnEtc.sol";
import {BitcoinTx} from "./BitcoinTx.sol";

/// @title Bitcoin to ETC swap by SPV
/// @author Classix
/// @notice Bob locks ETC for Alice, who pays an agreed number of satoshis to a fresh Bitcoin script of Bob's with an
///         ordinary transaction. Once the payment has enough confirmations, anyone submits the transaction, its merkle
///         path, the block's coinbase with its path and a run of linked headers, and the contract pays Alice. After
///         the timeout an unclaimed swap refunds Bob. Nothing here trusts anyone. A forger needs a run of headers with
///         minWork of real work, so a contract holds at most maxLocked, and one forged run pays out every swap open
///         at that moment.
/// @dev Before she pays, Alice reads the swap by id and checks script, sats, alice, timeout, the deposit and bob. A
///      script is locked to the first swap that uses it for good, so an old payment can never claim a new swap, and a
///      stranger who copies Bob's open with a dust deposit takes the script and leaves a swap whose deposit and bob
///      differ from Bob's. When onEtc is set, the newest header must also be a head that a quorum of etchd operators
///      recorded, proven through BitcoinOnEtc.proveHead first.
contract BitcoinSwap {
    using BitcoinTx for bytes;

    enum State {
        None,
        Open,
        Claimed,
        Refunded
    }

    struct Swap {
        bytes script; // the full scriptPubKey Alice pays
        uint64 sats; // at least this much to it
        address alice; // paid on claim
        address bob; // who opened it, refunded after the timeout
        uint64 timeout; // an ETC block time
        uint64 openedAt;
        uint96 deposit; // packed with openedAt and state, at most maxValue
        State state;
    }

    /// A payment may sit in a block whose own time is up to this much before the swap opened, since miners set header
    /// times about two hours off.
    uint64 public constant HEADER_TIME_SLACK = 7200;

    BitcoinHeaders public immutable headers;
    BitcoinOnEtc public immutable onEtc;
    uint256 public immutable minWork;
    uint256 public immutable confirmations;
    uint256 public immutable maxValue;
    uint256 public immutable maxLocked;

    uint256 public totalLocked;
    uint256 public count;
    mapping(uint256 => Swap) public swaps;
    mapping(bytes32 => bool) public usedScript;
    mapping(bytes32 => mapping(uint256 => bool)) public spent;
    mapping(address => uint256) public owed;

    event Opened(uint256 indexed id, address indexed bob, address alice, uint64 sats, uint64 timeout, uint256 deposit, bytes script);
    event Claimed(uint256 indexed id, bytes32 txid, uint256 vout);
    event Refunded(uint256 indexed id);
    event Owed(address to, uint256 amount);

    error BadTerms();
    error BadOpen();
    error TooMuch(uint256 value, uint256 maxValue);
    error CapReached(uint256 totalLocked, uint256 maxLocked);
    error ScriptUsed();
    error NotOpen();
    error Expired();
    error NotExpired();
    error IsCoinbase();
    error NoOutput(uint256 vout);
    error WrongScript();
    error Underpaid(uint64 amount, uint64 sats);
    error TooFewHeaders(uint256 n);
    error BeforeOpen(uint32 blockTime);
    error AlreadySpent(bytes32 txid, uint256 vout);
    error NotAHead(bytes32 blockHash);
    error NothingOwed();

    constructor(BitcoinHeaders headers_, BitcoinOnEtc onEtc_, uint256 minWork_, uint256 confirmations_, uint256 maxValue_, uint256 maxLocked_) {
        if (address(headers_) == address(0) || maxValue_ == 0 || maxValue_ > type(uint96).max || maxLocked_ < maxValue_) revert BadTerms();
        headers = headers_;
        onEtc = onEtc_;
        minWork = minWork_;
        confirmations = confirmations_;
        maxValue = maxValue_;
        maxLocked = maxLocked_;
    }

    /// @notice Locks msg.value for `alice`, who pays `sats` to `script`, until `timeout`. Returns the swap id.
    function open(bytes calldata script, uint64 sats, address alice, uint64 timeout) external payable returns (uint256 id) {
        if (script.length == 0 || script.length > 10_000 || sats == 0 || alice == address(0) || msg.value == 0 || timeout <= block.timestamp) {
            revert BadOpen();
        }
        if (msg.value > maxValue) revert TooMuch(msg.value, maxValue);
        if (totalLocked + msg.value > maxLocked) revert CapReached(totalLocked, maxLocked);
        bytes32 key = keccak256(script);
        if (usedScript[key]) revert ScriptUsed();
        usedScript[key] = true;
        totalLocked += msg.value;
        id = ++count;
        swaps[id] = Swap(script, sats, alice, msg.sender, timeout, uint64(block.timestamp), uint96(msg.value), State.Open);
        emit Opened(id, msg.sender, alice, sats, timeout, msg.value, script);
    }

    /// @notice Pays swap `id` to Alice for output `p.vout` of `p.tx_` at `p.index` in the block of the first header,
    ///         with the coinbase of that block at index 0 and `confirmations` more headers on top.
    function claim(uint256 id, BitcoinHeaders.Payment calldata p) external {
        Swap storage s = swaps[id];
        if (s.state != State.Open) revert NotOpen();
        if (block.timestamp >= s.timeout) revert Expired();
        checkOutput(s, p.tx_, p.vout);
        bytes32 txid = included(s, p);
        if (spent[txid][p.vout]) revert AlreadySpent(txid, p.vout);
        spent[txid][p.vout] = true;
        s.state = State.Claimed;
        totalLocked -= s.deposit;
        emit Claimed(id, txid, p.vout);
        pay(s.alice, s.deposit);
    }

    /// @notice After the timeout, returns an unclaimed deposit to Bob.
    function refund(uint256 id) external {
        Swap storage s = swaps[id];
        if (s.state != State.Open) revert NotOpen();
        if (block.timestamp < s.timeout) revert NotExpired();
        s.state = State.Refunded;
        totalLocked -= s.deposit;
        emit Refunded(id);
        pay(s.bob, s.deposit);
    }

    /// @notice Pays out a credit left by a payout the recipient refused.
    function withdraw() external {
        uint256 amount = owed[msg.sender];
        if (amount == 0) revert NothingOwed();
        owed[msg.sender] = 0;
        (bool ok,) = msg.sender.call{value: amount}("");
        if (!ok) revert NothingOwed();
    }

    function checkOutput(Swap storage s, bytes calldata tx_, uint256 vout) private view {
        BitcoinTx.Tx memory t = tx_.parse();
        if (t.coinbase) revert IsCoinbase();
        if (vout >= t.outputs.length) revert NoOutput(vout);
        BitcoinTx.Output memory o = t.outputs[vout];
        bytes memory want = s.script;
        if (o.scriptLength != want.length || keccak256(tx_[o.scriptStart:o.scriptStart + o.scriptLength]) != keccak256(want)) revert WrongScript();
        if (o.amount < s.sats) revert Underpaid(o.amount, s.sats);
    }

    function included(Swap storage s, BitcoinHeaders.Payment calldata p) private view returns (bytes32 txid) {
        bytes calldata run = p.headers;
        if (run.length % 80 != 0 || run.length / 80 < confirmations + 1) revert TooFewHeaders(run.length / 80);
        uint32 blockTime;
        bytes32 tip;
        (txid, blockTime, tip) = headers.txInChain(p, minWork);
        if (uint256(blockTime) + HEADER_TIME_SLACK < s.openedAt) revert BeforeOpen(blockTime);
        if (address(onEtc) != address(0)) {
            (, uint64 headBy) = onEtc.heads(tip);
            if (headBy == 0) revert NotAHead(tip);
        }
    }

    function pay(address to, uint256 amount) private {
        (bool ok,) = to.call{value: amount}("");
        if (!ok) {
            owed[to] += amount;
            emit Owed(to, amount);
        }
    }
}
```

[BitcoinHeaders.sol](https://github.com/classix-dev/etchd/blob/main/packages/contract/src/examples/BitcoinHeaders.sol)

BitcoinHeaders.sol

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

/// @title Bitcoin header checks on ETC
/// @author Classix
/// @notice Checks Bitcoin block headers on ETC with no trust in anyone: double SHA-256 through the sha256 precompile,
///         the hash under the header's own target, and that target at or under a work floor fixed at deploy, so a
///         cheap low-difficulty header fails. A run of linked headers must also sum to a minimum work the caller
///         names. Hashes are in Bitcoin's internal byte order unless a name says display.
/// @dev The floor alone is weak, since a forger mines every header at the floor. Contracts that act on a run of
///      headers therefore pass `minWork` to checkChain and hold less value than that much work costs.
contract BitcoinHeaders {
    /// The easiest target a header may claim. A header with a larger target has too little work behind it.
    uint256 public immutable maxTarget;

    struct Header {
        bytes32 hash; // sha256d of the 80 bytes, internal order
        bytes32 prev;
        bytes32 merkleRoot;
        uint32 time;
        uint256 target;
        uint256 work; // 2^256 / (target + 1)
    }

    /// A Bitcoin payment and its proof, as BitcoinSwap.claim takes it.
    struct Payment {
        bytes tx_; // the non-witness serialization
        uint256 vout; // the output that pays, read by the caller
        bytes32[] siblings; // merkle path of the tx, internal order, leaf upwards
        uint256 index; // the tx position in its block
        bytes coinbase; // the block's coinbase, non-witness serialization
        bytes32[] coinbaseSiblings; // its path at index 0, as long as the tx path
        bytes headers; // the tx's block first, then the headers on top of it, 80 bytes each
    }

    error BadHeader(uint256 index);
    error WeakHeader(uint256 index, uint256 target);
    error NotLinked(uint256 index);
    error InsufficientWork(uint256 work, uint256 minWork);
    error BadPath(uint256 offset);
    error NotInBlock();
    error Tx64();
    error BadCoinbase();

    constructor(uint256 maxTarget_) {
        maxTarget = maxTarget_;
    }

    /// @notice One header, checked for its own proof of work and the floor.
    function check(bytes calldata header) external view returns (Header memory) {
        return checkAt(header, 0);
    }

    /// @notice A run of linked headers, oldest first, each checked, with at least `minWork` summed work.
    /// @return first the oldest header
    /// @return tip the newest header
    /// @return work the summed work of the run
    function checkChain(bytes calldata headers, uint256 minWork) public view returns (Header memory first, Header memory tip, uint256 work) {
        uint256 n = headers.length / 80;
        if (n == 0 || headers.length % 80 != 0) revert BadHeader(0);
        for (uint256 i; i < n; i++) {
            Header memory h = checkAt(headers[i * 80:i * 80 + 80], i);
            if (i == 0) first = h;
            else if (h.prev != tip.hash) revert NotLinked(i);
            tip = h;
            work += h.work;
        }
        if (work < minWork) revert InsufficientWork(work, minWork);
    }

    /// @notice The block of `header` commits to `tx_` at `index`. The full tx is hashed here and may not be 64 bytes, so
    ///         an inner node of the tx tree cannot pass as a tx. The coinbase is proven at index 0 with the same number
    ///         of siblings, which pins the depth of the tree, so a 64-byte transaction cannot pass as an inner node
    ///         either. `siblings` are internal order, leaf upwards.
    /// @dev The coinbase is not parsed beyond bytes 4 to 40 of its non-witness form, which must be an input count of 1
    ///      and the null outpoint, so the gas does not grow with its outputs.
    function txInBlock(
        bytes calldata tx_,
        bytes32[] calldata siblings,
        uint256 index,
        bytes calldata coinbase,
        bytes32[] calldata coinbaseSiblings,
        bytes calldata header
    ) external view returns (bytes32 txid, bytes32 blockHash, uint32 blockTime) {
        // The same checks as txInChain, written out, since the flat arguments leave no stack for the helpers.
        if (tx_.length == 64) revert Tx64();
        if (coinbase.length == 64 || coinbase.length < 41 || coinbaseSiblings.length != siblings.length) revert BadCoinbase();
        if (coinbase[4] != 0x01 || bytes32(coinbase[5:37]) != bytes32(0) || bytes4(coinbase[37:41]) != bytes4(0xffffffff)) revert BadCoinbase();
        Header memory h = checkAt(header, 0);
        txid = sha256dCalldata(tx_);
        if (index >> siblings.length != 0 || foldTx(txid, siblings, index) != h.merkleRoot) revert NotInBlock();
        if (foldTx(sha256dCalldata(coinbase), coinbaseSiblings, 0) != h.merkleRoot) revert BadCoinbase();
        return (txid, h.hash, h.time);
    }

    /// @notice txInBlock and checkChain in one call, as a claim needs them: `p.tx_` is in the block of p.headers[0],
    ///         with the coinbase pin, under a run of linked headers of at least `minWork`. Returns the txid, the time of
    ///         the tx's block and the hash of the newest header.
    function txInChain(Payment calldata p, uint256 minWork) external view returns (bytes32 txid, uint32 blockTime, bytes32 tipHash) {
        (Header memory first, Header memory tip,) = checkChain(p.headers, minWork);
        checkCoinbase(p.coinbase, p.coinbaseSiblings, p.siblings.length, first.merkleRoot);
        return (txAt(p.tx_, p.siblings, p.index, first.merkleRoot), first.time, tip.hash);
    }

    /// The txid of `tx_`, which must not be 64 bytes and must sit at `index` under `root`.
    function txAt(bytes calldata tx_, bytes32[] calldata siblings, uint256 index, bytes32 root) internal view returns (bytes32 txid) {
        if (tx_.length == 64) revert Tx64();
        txid = sha256dCalldata(tx_);
        if (index >> siblings.length != 0 || foldTx(txid, siblings, index) != root) revert NotInBlock();
    }

    /// The coinbase spends the null outpoint and sits at index 0 of a tree of `depth` levels with root `root`.
    function checkCoinbase(bytes calldata coinbase, bytes32[] calldata siblings, uint256 depth, bytes32 root) internal view {
        if (coinbase.length == 64 || coinbase.length < 41 || siblings.length != depth) revert BadCoinbase();
        if (coinbase[4] != 0x01 || bytes32(coinbase[5:37]) != bytes32(0) || bytes4(coinbase[37:41]) != bytes4(0xffffffff)) revert BadCoinbase();
        if (foldTx(sha256dCalldata(coinbase), siblings, 0) != root) revert BadCoinbase();
    }

    /// @notice The block of `header` commits to `digest` through an OpenTimestamps Bitcoin branch. `ops` uses the etchd
    ///         contract path encoding (0x08 sha256, 0xf0 append, 0xf1 prepend with a u16be length) and ends at the
    ///         header's merkle root.
    function digestInBlock(bytes32 digest, bytes calldata ops, bytes calldata header) external view returns (bytes32 blockHash, uint32 blockTime) {
        Header memory h = checkAt(header, 0);
        if (run(digest, ops) != h.merkleRoot) revert NotInBlock();
        return (h.hash, h.time);
    }

    function checkAt(bytes calldata h, uint256 index) internal view returns (Header memory r) {
        if (h.length != 80) revert BadHeader(index);
        r.hash = sha256dCalldata(h);
        r.prev = bytes32(h[4:36]);
        r.merkleRoot = bytes32(h[36:68]);
        r.time = le32(h[68:72]);
        uint32 bits = le32(h[72:76]);
        uint256 exp = bits >> 24;
        uint256 mant = bits & 0x007fffff;
        if (bits & 0x00800000 != 0 || mant == 0 || exp < 3 || exp > 32) revert BadHeader(index);
        r.target = mant << (8 * (exp - 3));
        if (r.target > maxTarget) revert WeakHeader(index, r.target);
        if (uint256(reverse(r.hash)) > r.target) revert BadHeader(index);
        r.work = (~r.target / (r.target + 1)) + 1;
    }

    /// A node that is the left sibling of an equal node is the phantom copy of a duplicated last leaf
    /// (CVE-2012-2459), so a path through it is refused.
    function foldTx(bytes32 node, bytes32[] calldata siblings, uint256 index) internal view returns (bytes32) {
        for (uint256 k; k < siblings.length; k++) {
            if ((index >> k) & 1 == 1) {
                if (siblings[k] == node) revert NotInBlock();
                node = sha256dPair(siblings[k], node);
            } else {
                node = sha256dPair(node, siblings[k]);
            }
        }
        return node;
    }

    /// sha256d of 64 bytes, hashed in the scratch space with no memory allocation.
    function sha256dPair(bytes32 a, bytes32 b) internal view returns (bytes32 r) {
        assembly ("memory-safe") {
            mstore(0, a)
            mstore(32, b)
            if iszero(staticcall(gas(), 2, 0, 64, 0, 32)) { revert(0, 0) }
            if iszero(staticcall(gas(), 2, 0, 32, 0, 32)) { revert(0, 0) }
            r := mload(0)
        }
    }

    /// sha256d of calldata bytes, copied past the free memory pointer and left there.
    function sha256dCalldata(bytes calldata b) internal view returns (bytes32 r) {
        assembly ("memory-safe") {
            let m := mload(0x40)
            calldatacopy(m, b.offset, b.length)
            if iszero(staticcall(gas(), 2, m, b.length, 0, 32)) { revert(0, 0) }
            if iszero(staticcall(gas(), 2, 0, 32, 0, 32)) { revert(0, 0) }
            r := mload(0)
        }
    }

    function run(bytes32 digest, bytes calldata ops) internal pure returns (bytes32) {
        if (ops.length > 8192) revert BadPath(8192);
        bytes memory m = abi.encodePacked(digest);
        uint256 i;
        while (i < ops.length) {
            uint8 op = uint8(ops[i]);
            if (op == 0x08) {
                m = abi.encodePacked(sha256(m));
                i += 1;
            } else if (op == 0xf0 || op == 0xf1) {
                if (i + 3 > ops.length) revert BadPath(i);
                uint256 n = uint16(bytes2(ops[i + 1:i + 3]));
                if (n == 0 || n > 4096 || i + 3 + n > ops.length) revert BadPath(i);
                m = op == 0xf0 ? bytes.concat(m, ops[i + 3:i + 3 + n]) : bytes.concat(ops[i + 3:i + 3 + n], m);
                if (m.length > 4096) revert BadPath(i);
                i += 3 + n;
            } else {
                revert BadPath(i);
            }
        }
        if (m.length != 32) revert BadPath(ops.length);
        return bytes32(m);
    }

    function le32(bytes calldata b) internal pure returns (uint32) {
        return uint32(uint8(b[0])) | (uint32(uint8(b[1])) << 8) | (uint32(uint8(b[2])) << 16) | (uint32(uint8(b[3])) << 24);
    }

    /// Byte order reversal of a 32-byte word.
    function reverse(bytes32 x) internal pure returns (bytes32) {
        uint256 v = uint256(x);
        v = ((v & 0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00) >> 8) | ((v & 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff) << 8);
        v = ((v & 0xffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000) >> 16) | ((v & 0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff) << 16);
        v = ((v & 0xffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000) >> 32) | ((v & 0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff) << 32);
        v = ((v & 0xffffffffffffffff0000000000000000ffffffffffffffff0000000000000000) >> 64) | ((v & 0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff) << 64);
        v = (v >> 128) | (v << 128);
        return bytes32(v);
    }
}
```

[BitcoinTx.sol](https://github.com/classix-dev/etchd/blob/main/packages/contract/src/examples/BitcoinTx.sol)

BitcoinTx.sol

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

/// @title Strict Bitcoin transaction parser
/// @author Classix
/// @notice Reads the non-witness serialization of a Bitcoin transaction, the bytes its txid hashes: version, inputs,
///         outputs and locktime, with nothing after the locktime. Every count and script length is a CompactSize
///         varint that must be minimally encoded and fit the bytes left. An input count of zero is refused, which also
///         refuses the segwit serialization, whose marker byte 0x00 sits where the input count goes. A segwit tx is
///         read in its stripped form, which has the same txid.
library BitcoinTx {
    struct Output {
        uint64 amount; // satoshis
        uint256 scriptStart; // offset of the scriptPubKey in the tx
        uint256 scriptLength;
    }

    struct Tx {
        uint256 inputs;
        bool coinbase; // one input spending the null outpoint
        Output[] outputs;
    }

    error BadTx(uint256 offset);

    function parse(bytes calldata t) internal pure returns (Tx memory r) {
        uint256 i = 4; // version
        if (t.length < 10) revert BadTx(0);
        (r.inputs, i) = varint(t, i);
        if (r.inputs == 0 || r.inputs > (t.length - i) / 41) revert BadTx(4);
        for (uint256 k; k < r.inputs; k++) {
            if (i + 36 > t.length) revert BadTx(i);
            if (k == 0 && r.inputs == 1 && bytes32(t[i:i + 32]) == bytes32(0) && bytes4(t[i + 32:i + 36]) == bytes4(0xffffffff)) {
                r.coinbase = true;
            }
            i += 36;
            uint256 n;
            (n, i) = varint(t, i);
            if (i + n + 4 > t.length) revert BadTx(i);
            i += n + 4; // script and sequence
        }
        uint256 outs;
        (outs, i) = varint(t, i);
        if (outs == 0 || outs > (t.length - i) / 9) revert BadTx(i);
        r.outputs = new Output[](outs);
        for (uint256 k; k < outs; k++) {
            if (i + 8 > t.length) revert BadTx(i);
            uint64 amount = le64(t, i);
            i += 8;
            uint256 n;
            (n, i) = varint(t, i);
            if (i + n > t.length) revert BadTx(i);
            r.outputs[k] = Output(amount, i, n);
            i += n;
        }
        if (i + 4 != t.length) revert BadTx(i); // locktime, then nothing
    }

    /// A CompactSize integer at `i`, minimally encoded, and the offset after it.
    function varint(bytes calldata t, uint256 i) internal pure returns (uint256 v, uint256 next) {
        if (i >= t.length) revert BadTx(i);
        uint8 b = uint8(t[i]);
        if (b < 0xfd) return (b, i + 1);
        uint256 len = b == 0xfd ? 2 : b == 0xfe ? 4 : 8;
        if (i + 1 + len > t.length) revert BadTx(i);
        for (uint256 k; k < len; k++) v |= uint256(uint8(t[i + 1 + k])) << (8 * k);
        uint256 min = len == 2 ? 0xfd : len == 4 ? 0x10000 : 0x100000000;
        if (v < min) revert BadTx(i);
        next = i + 1 + len;
    }

    function le64(bytes calldata t, uint256 i) private pure returns (uint64 v) {
        for (uint256 k; k < 8; k++) v |= uint64(uint8(t[i + k])) << uint64(8 * k);
    }
}
```

[BitcoinOnEtc.sol](https://github.com/classix-dev/etchd/blob/main/packages/contract/src/examples/BitcoinOnEtc.sol)

BitcoinOnEtc.sol

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

import {EtchdVerifier} from "../EtchdVerifier.sol";
import {BitcoinHeaders} from "./BitcoinHeaders.sol";

/// @title Bitcoin heads and etchd ticks on ETC
/// @author Classix
/// @notice Ties etchd records to Bitcoin. `proveHead` stores a Bitcoin block that a quorum of the pinned etchd
///         operators recorded as the chain head, with its height and the latest anchor time of that quorum.
///         `proveEtchdTick` stores an etchd tick root that an operator anchored on ETC and that etchd's hourly roll-up
///         put in a Bitcoin block, with the work of the headers on top of it.
/// @dev The operator list is pinned at deploy, strictly ascending. More operators plug in the same way, with a new
///      deployment. Proof of work makes a header real, and the records add the height and the time.
contract BitcoinOnEtc {
    struct HeadProof {
        address operator;
        bytes record; // a 104-byte chain-head record of Bitcoin
        EtchdVerifier.RecordPath path;
    }

    struct Head {
        uint64 height;
        uint64 headBy; // the latest anchor time of the quorum that proved it, lowered by earlier proofs
    }

    struct Tick {
        uint64 etcTime; // when the operator anchored the tick on ETC
        bytes32 btcBlock; // the Bitcoin block whose merkle root commits to the tick root, internal order
        uint32 btcTime; // that block's own time
        uint256 work; // the summed work of the header run that proved it
    }

    uint32 public constant HEAD_BRANCH = 2;
    uint16 public constant HEAD_SCHEMA = 1;
    uint256 public constant HEAD_RECORD_LENGTH = 104;
    /// sha256("bip122:000000000019d6689c085ae165831e93"), the chain field of a Bitcoin head record.
    bytes32 public constant BITCOIN_CHAIN = sha256("bip122:000000000019d6689c085ae165831e93");

    EtchdVerifier public immutable verifier;
    BitcoinHeaders public immutable headers;
    uint256 public immutable quorum;
    uint256 public immutable minWork;
    address[] public operators;
    mapping(address => bool) public isOperator;

    /// Bitcoin heads by block hash, internal order.
    mapping(bytes32 => Head) public heads;
    /// etchd ticks by tick root.
    mapping(bytes32 => Tick) public ticks;

    event HeadProven(bytes32 indexed blockHash, uint64 height, uint64 headBy);
    event TickProven(bytes32 indexed tickRoot, uint64 etcTime, bytes32 btcBlock, uint32 btcTime, uint256 work);

    error BadTerms();
    error TooFewProofs(uint256 n);
    error NotOperator(address operator);
    error OperatorsOutOfOrder(uint256 index);
    error BadHead(uint256 index);
    error HeadMismatch(uint256 index);
    error NotLighter();

    constructor(EtchdVerifier verifier_, BitcoinHeaders headers_, address[] memory operators_, uint256 quorum_, uint256 minWork_) {
        if (quorum_ == 0 || quorum_ > operators_.length) revert BadTerms();
        for (uint256 i; i < operators_.length; i++) {
            if (i == 0 ? operators_[i] == address(0) : operators_[i] <= operators_[i - 1]) revert BadTerms();
            isOperator[operators_[i]] = true;
        }
        operators = operators_;
        verifier = verifier_;
        headers = headers_;
        quorum = quorum_;
        minWork = minWork_;
    }

    function operatorCount() external view returns (uint256) {
        return operators.length;
    }

    /// @notice Stores `header` as a Bitcoin head that at least `quorum` pinned operators recorded. Each proof is a
    ///         Bitcoin head record (schema 1, kind head, header format btc80, 32-byte hash) proven with
    ///         recordExistedAtBy, all naming the same height and the hash of `header`, which must pass its own proof
    ///         of work and the floor. A block already stored keeps the smaller headBy.
    function proveHead(HeadProof[] calldata proofs, bytes calldata header) external returns (uint64 height, uint64 headBy) {
        if (proofs.length < quorum) revert TooFewProofs(proofs.length);
        BitcoinHeaders.Header memory h = headers.check(header);
        bytes32 display = reverse(h.hash);
        for (uint256 i; i < proofs.length; i++) {
            HeadProof calldata p = proofs[i];
            if (!isOperator[p.operator]) revert NotOperator(p.operator);
            if (i > 0 && p.operator <= proofs[i - 1].operator) revert OperatorsOutOfOrder(i);
            bytes calldata r = p.record;
            // schema, chain, kind 0 head, header_format 1 btc80, hash_len 32
            if (
                r.length != HEAD_RECORD_LENGTH || uint16(bytes2(r[5:7])) != HEAD_SCHEMA || bytes32(r[11:43]) != BITCOIN_CHAIN || r[43] != 0x00
                    || r[70] != 0x01 || r[71] != 0x20
            ) revert BadHead(i);
            uint64 ht = uint64(bytes8(r[44:52]));
            if (bytes32(r[72:104]) != display || (i > 0 && ht != height)) revert HeadMismatch(i);
            height = ht;
            (uint64 t,) = verifier.recordExistedAtBy(p.operator, HEAD_BRANCH, r, p.path);
            if (t > headBy) headBy = t;
        }
        Head memory old = heads[h.hash];
        if (old.headBy != 0 && old.headBy <= headBy) return (old.height, old.headBy);
        heads[h.hash] = Head(height, headBy);
        emit HeadProven(h.hash, height, headBy);
    }

    /// @notice Stores an etchd tick root that `operator` anchored on ETC and that sits in the Bitcoin block of
    ///         headers[0] through the OpenTimestamps branch `ops`, with the run `headers` on top of at least minWork.
    ///         A tick already stored is replaced only by a run of more work, so a header mined at the floor cannot fix
    ///         a wrong Bitcoin block for good.
    function proveEtchdTick(address operator, EtchdVerifier.TickPath calldata t, bytes calldata ops, bytes calldata headerRun)
        external
        returns (bytes32 tickRoot)
    {
        if (!isOperator[operator]) revert NotOperator(operator);
        uint64 etcTime;
        (etcTime,, tickRoot) = verifier.tickExistedAtBy(operator, t);
        (,, uint256 work) = headers.checkChain(headerRun, minWork);
        (bytes32 blockHash, uint32 btcTime) = headers.digestInBlock(tickRoot, ops, headerRun[0:80]);
        if (work <= ticks[tickRoot].work) revert NotLighter();
        ticks[tickRoot] = Tick(etcTime, blockHash, btcTime, work);
        emit TickProven(tickRoot, etcTime, blockHash, btcTime, work);
    }

    function reverse(bytes32 x) internal pure returns (bytes32 r) {
        uint256 v = uint256(x);
        v = ((v & 0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00) >> 8) | ((v & 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff) << 8);
        v = ((v & 0xffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000) >> 16) | ((v & 0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff) << 16);
        v = ((v & 0xffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000) >> 32) | ((v & 0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff) << 32);
        v = ((v & 0xffffffffffffffff0000000000000000ffffffffffffffff0000000000000000) >> 64) | ((v & 0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff) << 64);
        r = bytes32((v >> 128) | (v << 128));
    }
}
```

## Tested on a Local Network

We do not deploy BitcoinHeaders, BitcoinOnEtc or BitcoinSwap on ETC mainnet. They run in the end-to-end test, step 9 of [scripts/e2e.ts](https://github.com/classix-dev/etchd/blob/main/scripts/e2e.ts), on the local devnet and on an anvil network whose clock starts one minute after Bitcoin block 972,856, and in the contract tests on anvil. The Bitcoin data is real, fetched into [packages/contract/test/fixtures/btc](https://github.com/classix-dev/etchd/tree/main/packages/contract/test/fixtures/btc). It holds headers 972,856 to 972,862, the OpenTimestamps calendar tx in block 972,856 with its 13-level Merkle path and the block's coinbase, and the OpenTimestamps branch of tick 29,880,029 into that block.

On the devnet, `btc.ts deploy headers` puts BitcoinHeaders with a floor of 4 times block 972,856's target and `btc.ts deploy on-etc` reads operator `a` from `/v1/signers` with quorum 1. `proveHead` stores 972,858 from a's head record, and the same head record anchored under a key that is not listed reverts `NotOperator`. The walked header 972,859, the stale sibling of 972,861 and a chain that jumps two heights each tick all check in `etchd records verify`.

On the anvil network, a stranger opens a swap on Bob's script with a 1 wei deposit, Bob's open on the same script reverts `ScriptUsed`, and `swap-show` with Bob's expectations refuses the stranger's swap with exit 3. Bob's own swap on the calendar tx's output script and amount opens, and `swap-claim` with that tx, its 13 levels, the coinbase and headers 972,856 to 972,862 pays Alice. A second claim of the same output is refused. On the swap that also requires an etchd head, the claim reverts `NotAHead` until a's head record of 972,862 is proven, and pays after. A refund before the timeout reverts `NotExpired`.

Run them again

```bash
pnpm --filter @etchd/contract test         # BitcoinHeaders, BitcoinOnEtc and BitcoinSwap on anvil
DEVNET_PORT_OFFSET=2800 pnpm e2e           # the whole end-to-end test, step 9 is Bitcoin
```

## Producing the Inputs

The demo scripts sit in [scripts/examples](https://github.com/classix-dev/etchd/tree/main/scripts/examples). `btc.ts` takes `--calendar <cal>=<url>`, repeatable, and `--quorum`, which default to calendar `a` and quorum 1, and `deploy on-etc` reads the operators from each calendar's `/v1/signers`. The scripts refuse a demo key that any listed calendar anchors with.

Deploy and open

```bash
ETCHD_DEMO_KEY=0x... node scripts/examples/btc.ts deploy headers --max-target 0x...
ETCHD_DEMO_KEY=0x... node scripts/examples/btc.ts deploy on-etc --headers 0xHEADERS... --min-work 2100000000000000000000000
ETCHD_DEMO_KEY=0x... node scripts/examples/btc.ts deploy swap --headers 0xHEADERS... --on-etc 0xONETC... \
  --min-work 2100000000000000000000000 --confirmations 6 \
  --max-value 100000000000000000 --max-locked 1000000000000000000

# Bob opens a swap on a fresh script of his own
ETCHD_DEMO_KEY=0xB0B... node scripts/examples/btc.ts swap-open --swap 0xSWAP... \
  --script 0014a1b2... --sats 150000 --alice 0xA11CE... --timeout +86400 --deposit 100000000000000000
```

Alice runs `swap-show` as in [her checklist](https://etchd.app/docs/bitcoin-swap#checklist) and pays. After six confirmations, `etchd btc tx` builds the claim from Esplora, with the transaction and the coinbase stripped of their witnesses and seven headers. `--script` finds the one output paying that script, and two matches are a usage error.

Claim

```bash
# TXID is the txid of Alice's payment
etchd btc tx $TXID --script 0014a1b2... --confirmations 6 --swap 7 --json > payment.json
ETCHD_DEMO_KEY=0x... node scripts/examples/btc.ts swap-claim 7 --swap 0xSWAP... --payment payment.json

# with the quorum-head check the run reaches a recorded head, HEIGHT is the height
# of the proveHead call that btc tx prints, and proveHead lands first
etchd btc tx $TXID --script 0014a1b2... --swap 7 --on-etc 0xONETC... --car a-day.car --json > payment.json
etchd records proof https://a.etc.calendar.etchd.app --chain bip122:000000000019d6689c085ae165831e93 --height $HEIGHT --abi proveHead --json > head.json
ETCHD_DEMO_KEY=0x... node scripts/examples/btc.ts head --on-etc 0xONETC... --proofs head.json
ETCHD_DEMO_KEY=0x... node scripts/examples/btc.ts swap-claim 7 --swap 0xSWAP... --payment payment.json

# Bob, after the timeout with no claim
ETCHD_DEMO_KEY=0xB0B... node scripts/examples/btc.ts swap-refund 7 --swap 0xSWAP...
```

`swap-claim`, `head` and `tick` send a transaction, so they need `ETCHD_DEMO_KEY`. Anyone's funded key works, as long as it is not an operator key. The roll-up proof takes the tick's day and the next, since the roll-up block is often in the next day's CAR.

Prove a tick in Bitcoin

```bash
etchd btc tick a-day.car a-next-day.car 29882000 --json > tick.json
ETCHD_DEMO_KEY=0x... node scripts/examples/btc.ts tick --on-etc 0xONETC... --proof tick.json
etchd btc headers --height 972856 --count 7 --car a-day.car --min-work 2100000000000000000000000
```

Each `btc` command exits 0 when every header checks, 3 when one does not and 4 when Esplora or a calendar cannot be reached. A header the CARs do not hold, with no `--esplora` given, exits 1 with a message to pass `--esplora URL`. `btc tick` exits 2 while the tick has no anchor block, no roll-up or no Bitcoin attestation yet, and `btc tx` exits 2 while the tx is unconfirmed. Try those again later. The head record layout is on [Records](https://etchd.app/docs/records#chain-head).

## Tests

The tests use real mainnet data kept in [test/fixtures/btc](https://github.com/classix-dev/etchd/tree/main/packages/contract/test/fixtures/btc). The payment is the 125-byte OpenTimestamps calendar transaction `9fbaf6c0…86e7` at position 1,264 of block 972,856 with 13 Merkle levels, the block's coinbase with its own path, and headers 972,856 to 972,862. A swap opens on one of the transaction's real outputs, with `openedAt` set on Anvil a few seconds after the block. The tick proof is tick 29,880,029 of calendar `a`, whose 1,353-byte Bitcoin branch reaches the Merkle root of the same block.

Block 972,856 was mined at 2026-10-24 01:15:14 UTC.

The fixtures were fetched from mempool.space and calendar a on 2026-10-25.

[swap.test.ts](https://github.com/classix-dev/etchd/blob/main/packages/contract/test/swap.test.ts) claims with the fixture and with a 4 KB coinbase under 250,000 gas. It refuses the theft that the script lock stops, and the copied open of the checklist, where a stranger's dust `open` on Bob's script lands first, Bob's reverts `ScriptUsed` and the swap read back shows a deposit and a `bob` that differ from Bob's. It keeps a script locked after a refund, refuses a claim after the timeout, a wrong script, a script that only contains the agreed one, an amount one satoshi short, a `vout` out of range or pointing at another output, the segwit serialization, a non-minimal varint, trailing bytes, too few headers, seven headers under `minWork`, a claimed coinbase, a coinbase path one sibling short or from another index, a deposit above `maxValue`, an open past `maxLocked` until a refund frees room, bad opens and a block more than 7,200 s before the open. It claims two swaps from one transaction with two outputs, refuses the phantom index of a duplicated last leaf, shows three swaps paid from one forged block on a contract with `minWork` 0, and runs the 64-byte leaf attack against a block at regtest difficulty.

[bitcoin.test.ts](https://github.com/classix-dev/etchd/blob/main/packages/contract/test/bitcoin.test.ts) checks headers, the floor, links, the tick root in block 972,856, the calendar transaction with its coinbase pin, and `proveHead` and `proveEtchdTick` with several test accounts as operators. It covers `headBy` kept and lowered, an unlisted operator, two proofs from one operator, records with the wrong length, schema, kind, header format, hash length or chain, a record anchored by another sender, operator `a` with quorum 1, and a heavier run replacing a lighter one while a lighter one never replaces a heavier. The end-to-end run in [scripts/e2e.ts](https://github.com/classix-dev/etchd/blob/main/scripts/e2e.ts) deploys BitcoinOnEtc with operator `a` and quorum 1 against a devnet calendar and refuses the head record of a key that anchors but is not listed.

## Limits

The claim rests on the work of the run. A forger who spends about 10.9 BTC of work at today's difficulty and subsidy pays out every open swap of a contract at once, and that cost halves at each halving and moves with difficulty, so `maxLocked` stays below it and is revisited.

The 64-byte leaf attack. Refusing a 64-byte transaction stops an inner node of the tree from passing as a transaction, and leaves the other direction open. An attacker gets one real 64-byte transaction `T` mined, which consensus allows, whose one 32-byte half equals `sha256d(F)` for a fake transaction `F` paying the victim, and proves `F` one level below `T`. The fold reaches a real Merkle root of a block with real work, and the attack costs some hash grinding and a miner willing to include `T`, nothing near `minWork`. Every leaf of a Bitcoin transaction tree sits at the same depth, so the coinbase proven at index 0 with the same number of siblings pins the real depth, and `F` one level deeper cannot match it. The test first shows `F` passing an inclusion check without the pin and then shows `claim` refusing it.

`maxLocked` is shared, so anyone can fill it with deposits of their own and block honest opens until those time out, which a demo capped at 1 ETC accepts. `usedScript` and `spent` are per contract, so a script and an output are guarded inside one deployment only. A transaction in its segwit form is still claimable, since its txid hashes the non-witness form, and `etchd btc tx` strips the witness before it prints. The quorum-head check adds the word of the listed operators and nothing more, and with one operator that is Classix.

## Other Ways to Move Bitcoin Value

A classic HTLC atomic swap needs no header check at all. Both sides lock coins under the same hash, one on each chain, and revealing the secret to claim one side reveals it for the other. Both chains enforce the lock themselves, so the swap trusts neither party and no light client. It needs both sides online through the swap and a script on the Bitcoin side, and when they are, it is the better tool.

A wrapped-BTC bridge can take deposits with the same SPV check as this swap. A user pays BTC to the bridge's address, proves the payment on ETC, and the bridge mints a token, with no trust in anyone for that half. Paying BTC back out is the hard half. Someone must hold the Bitcoin keys and sign the withdrawal, a custodian, a multisig or a set of threshold signers, and no header check on ETC removes that custody.
