Why SPV Works
Bitcoin’s blockchain is a chain of blocks. Each block has a header that contains:- The hash of the previous block (linking the chain)
- A Merkle root - a single hash that commits to every transaction in the block
- A nonce and target - proof that miners did computational work to produce this block
- The block header - to verify it has valid proof-of-work
- A Merkle proof - a set of sibling hashes that, combined with your transaction’s hash, reproduce the Merkle root in the header
- The raw transaction - to verify the transaction data matches the hash
The Writz Approach: an On-Chain Header Light Client
bitcoin-spv keeps a compact record of Bitcoin block headers on Stellar, and proves transactions against that record. A caller can no longer hand the contract a header chain it built itself - the earlier “stateless” design accepted exactly that, which is what allowed a privately mined chain to pass as real.
How it works:
- One trusted anchor. The admin sets a checkpoint - a recent real Bitcoin block - exactly once. Every stored header must descend from it.
- Anyone can extend the chain.
submit_headersaccepts a run of contiguous headers, checks each one (proof-of-work, link to a stored parent, the exact difficulty Bitcoin requires at that height, sane timestamp) and keeps the chain with the most cumulative work. The Writz relayer does this routinely, but any party can. - Proofs are cheap.
verify_transactionnames a stored block and checks a Merkle proof against its root. It no longer re-validates a header chain on every call.
docs/security/security-model.md for the full trust model.
What the Contract Verifies
verify_transaction takes:
SpvVerificationResult is defined once, in the shared spv-types crate, and reused by every contract that calls into bitcoin-spv - not redefined per contract.)
Header ingestion - submit_headers(headers):
- Each header’s
prev_block_hashmust be a header the contract already stores (ultimately the checkpoint) SHA256d(header)must meet the target encoded inbitsbitsmust equal what Bitcoin’s difficulty rules require at that height: unchanged inside a 2016-block period, the exact retarget at each boundary- The timestamp may not be more than two hours ahead of ledger time
- The chain with the most cumulative work becomes the best chain; a heavier fork replaces it
verify_transaction:
- The block must be stored, after the checkpoint, and on the best chain
- Its depth below the best tip must be at least
min_confirmations raw_txmust not be exactly 64 bytes (the size of a Merkle inner-node preimage)- Compute
txid = SHA256d(raw_tx), walk the Merkle proof (alternating left/right bytx_index), and the result must equal the stored block’smerkle_root - Return
SpvVerificationResult { txid, block_hash, confirmations }, whereconfirmationsis the block’s real depth
raw_tx itself; see the accuracy note in docs/developers/spv-sdk.md for the same point.
SHA256d: Bitcoin’s Hash Function
Bitcoin uses double-SHA256 (SHA256 applied twice) for all cryptographic operations: block hashes, transaction IDs, and Merkle tree nodes.bitcoin-spv contract implements it in Wasm. This was the primary concern about feasibility: would the compute cost be acceptable?
Benchmarked instruction counts:
These figures were measured for the earlier design, which validated a header chain inside every verification. With the light client, headers are validated once in
submit_headers and verify_transaction only checks a Merkle proof, so the per-deposit cost is lower; the header-submission costs are being re-measured.
The Relayer Service
Header submission is permissionless, so the relayer is not a trust component, but someone has to keep the contract’s header chain current and assemble proof bundles for users. The Writz relayer handles both. What the relayer does:- Watches a Bitcoin Esplora API for transactions to monitored addresses
- Fetches the raw transaction, the block header it was included in, and sufficient ancestor headers to meet
min_confirmations - Computes the Merkle proof from the block’s transaction list
- Packages everything into a
sorobanArgsbundle ready to submit to the SPV contract
Security Guarantees
6-confirmation requirement: Writz requires 6 Bitcoin block confirmations before accepting a deposit. Reorganizations that deep are very rare: the deepest on mainnet are the 2010 value-overflow fix and the 2013 BIP-50 fork. Six confirmations make reversal of an accepted deposit negligible at launch exposure caps. PoW validation: Each header’s proof-of-work is checked independently. A forged header chain would require generating valid PoW - computationally infeasible against Bitcoin’s current hash rate. Merkle proof soundness: The Merkle proof check is collision-resistant under SHA256. A fabricated proof would require a SHA256 preimage attack - computationally infeasible. What SPV does NOT protect against:- A user sending BTC to the wrong P2WSH address (user error)
- A deep reorg affecting more than 6 blocks (extraordinarily unlikely, but theoretically possible on a heavily attacked network)
- Bugs in the SPV contract itself (mitigated by audits and extensive testing)
Tested on Real Bitcoin Transactions
The SPV contract has been tested against:- Real Bitcoin mainnet transactions (correct
txidandblock_hashverified against Python-computed SHA256d) - Multiple block depths (1, 3, 6, 12 confirmations)
- Multi-transaction blocks with varying Merkle proof sizes
- Edge cases: single-transaction blocks, maximum-size transactions
CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO.
Next: The Stellar Side →