Skip to content

distributed-lab/spv-gateway

Repository files navigation

ERC-8002: SPV Gateway

Introduce a singleton contract for on-chain verification of transactions that happened on Bitcoin. The contract acts as a trustless Simplified Payment Verification (SPV) gateway where anyone can submit Bitcoin block headers. The gateway maintains the mainchain of blocks and allows the existence of Bitcoin transactions to be verified via Merkle proofs.

Link to ERC-8002.

Note

Since the ERC is currently a draft, there is no deployment on mainnet available. Please use the contract on Sepolia for testing purposes.

How it Works

The gateway is a permissionless contract that operates by receiving raw Bitcoin block headers (anyone can submit them), which are then parsed and validated against Bitcoin's consensus rules:

  1. Header Parsing: Raw 80-byte Bitcoin block headers are parsed into a structured BlockHeader.HeaderData format, handling Bitcoin's little-endian byte ordering.
  2. Double SHA256 Hashing: Each block header is double SHA256 hashed to derive its unique block hash, which is then saved in a big-endian format.
  3. Proof-of-Work Verification: The calculated block hash is checked against the current network difficulty target (derived from the bits field in the block header).
  4. Chain Extension & Reorganization: New blocks are added to a data structure that allows for tracking multiple chains. When a new block extends a chain with larger cumulative work, the mainchainHead is updated, reflecting potential chain reorganizations.
  5. Difficulty Adjustment: Every 2016 blocks, the contract calculates a new difficulty target based on the time taken to mine the preceding epoch. This ensures the 10-minute average block time is maintained.

Under the hood, the contract builds the mainchain but doesn't define its finality. The number of required block confirmations is up to the integration dApps to decide.

Submitting Bitcoin Blocks

To submit a new Bitcoin block, call addBlockHeader function by passing a valid raw block header as a parameter. It is an open function that will revert in case Bitcoin PoW checks don't pass.

In case multiple blocks can be added, call addBlockHeaderBatch function to save ~15% on gas per block.

Verifying Bitcoin Tx Inclusion

In order to verify the tx existence, the checkTxInclusion function needs to be called.

The list parameters to be passed:

  1. merkleProof - Merkle path for a given transaction to be checked. The Merkle path can either be built locally or by calling gettxoutproof on a Bitcoin node.
  2. blockHash - Hash of the block to check the tx inclusion against. This block is required to exist in the SPV storage.
  3. txId - Tx hash (Merkle leaf) to be checked.
  4. txIndex - The Merkle "direction bits" to decide on left or right hashing order.
  5. minConfirmationsCount - Number of required mainchain confirmation for the block to have.

Tip

Please check out this test case for more integration information.

Permissionlessness

In order for the gateway to be truly permissionless, the contract's initialization needs to be permissionless as well. Alongside the regular SPVGateway, the repository hosts a HistoricalSPVGateway contract, that uses a "proof-of-bitcoin" ZK proof for its initialization. This enables verification of historical Bitcoin blocks and transactions otherwise too expensive to include. Since syncing up the gateway from Bitcoin's genesis would cost ~100 ETH on the mainnet.

HistoricalSPVGateway

HistoricalSPVGateway is an extension of the basic SPVGateway contract. It uses "proof-of-bitcoin" ZK proof that compresses the entire Bitcoin block history into a single Merkle root to be used during the contract's initialization. This root can then used to verify the "historical" existence of some blocks and transactions.

Important

Currently, the "proof-of-bitcoin" ZK proof is generated to the first 912384 Bitcoin blocks. The circuits source code can be found here.

Building the History Merkle Tree

In order to prove the historical block existence, you need to pass the corresponding Merkle path to a smart contract. For that, the entire historical Merkle tree needs to be built:

  1. Fetch all block hashes from the genesis block up to the height of provedBlocksCount - 1.
  2. Split these blocks into 1024-block chunks.
  3. Create Level1 Merkle trees for each chunk.
  4. Create an array containing all the Level1 tree roots.
  5. Pad the array from the previous step with zeros for its length to reach the next power of 2.
  6. Create a Level2 Merkle tree, using the array from the previous step as the tree's values.

Note

For the Level1 Merkle tree use SHA256("leaf1" | blockHash) and SHA256("node1" | left | right) for hashing leaves and nodes. And for the Level2 Merkle tree, SHA256("leaf2" | level1MerkleRoot) and SHA256("node2" | left | right) respectively.

Verifying History Bitcoin Blocks Inclusion

To verify the existence of a historical Bitcoin block, call the checkHistoryBlockInclusion function.

This function requires a HistoryBlockInclusionProofData struct as a parameter, which contains the following fields:

  1. level1MerkleProof - Level1 Merkle path for the block hash being checked.
  2. level2MerkleProof - Level2 Merkle path for the Level1 Merkle root (which is calculated from the level1MerkleProof)
  3. blockHash - Block hash to be checked.
  4. blockHeight - Block height of the passed block hash.

Tip

Please check out this test cases for more integration information.

Verifying History Bitcoin Tx Inclusion

In order to verify the tx existence in the proven Bitcoin history, the checkHistoryTxInclusion function needs to be called.

The list of parameters to be passed:

  1. merkleProof - Merkle path for a given transaction to be checked. The Merkle path can either be built locally or by calling gettxoutproof on a Bitcoin node.
  2. blockHeaderRaw - Raw block header of the block to check the transaction's inclusion against.
  3. txId - Tx hash (Merkle leaf) to be checked.
  4. txIndex - The Merkle "direction bits" to decide on left or right hashing order.
  5. blockInclusionProofData - The proof data for the historical block hash inclusion.

Tip

Please check out this test case for more integration information.

Disclaimer

Bitcoin + Ethereum = <3