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.
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:
- Header Parsing: Raw 80-byte Bitcoin block headers are parsed into a structured BlockHeader.HeaderData format, handling Bitcoin's little-endian byte ordering.
- 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.
- 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).
- 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.
- 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.
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.
In order to verify the tx existence, the checkTxInclusion
function needs to be called.
The list parameters to be passed:
merkleProof
- Merkle path for a given transaction to be checked. The Merkle path can either be built locally or by callinggettxoutproof
on a Bitcoin node.blockHash
- Hash of the block to check the tx inclusion against. This block is required to exist in the SPV storage.txId
- Tx hash (Merkle leaf) to be checked.txIndex
- The Merkle "direction bits" to decide on left or right hashing order.minConfirmationsCount
- Number of required mainchain confirmation for the block to have.
Tip
Please check out this test case for more integration information.
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
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.
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:
- Fetch all block hashes from the genesis block up to the height of
provedBlocksCount - 1
. - Split these blocks into 1024-block chunks.
- Create Level1 Merkle trees for each chunk.
- Create an array containing all the Level1 tree roots.
- Pad the array from the previous step with zeros for its length to reach the next power of 2.
- 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.
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:
level1MerkleProof
- Level1 Merkle path for the block hash being checked.level2MerkleProof
- Level2 Merkle path for the Level1 Merkle root (which is calculated from thelevel1MerkleProof
)blockHash
- Block hash to be checked.blockHeight
- Block height of the passed block hash.
Tip
Please check out this test cases for more integration information.
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:
merkleProof
- Merkle path for a given transaction to be checked. The Merkle path can either be built locally or by callinggettxoutproof
on a Bitcoin node.blockHeaderRaw
- Raw block header of the block to check the transaction's inclusion against.txId
- Tx hash (Merkle leaf) to be checked.txIndex
- The Merkle "direction bits" to decide on left or right hashing order.blockInclusionProofData
- The proof data for the historical block hash inclusion.
Tip
Please check out this test case for more integration information.
Bitcoin + Ethereum = <3