|
1 | | -# 🛡️ SPV Gateway: Bitcoin Light Client on EVM |
2 | | -Welcome to the **SPV Gateway**, a robust and efficient Solidity implementation for verifying Bitcoin block headers directly on an EVM-compatible blockchain. This contract empowers dApps to act as a **Simplified Payment Verification (SPV)** client, allowing them to validate the existence and inclusion of Bitcoin transactions without needing to run a full Bitcoin node. |
3 | | - |
4 | | -# ✨ Why this SPV Gateway? |
5 | | -In the decentralized world, connecting different blockchain ecosystems securely is paramount. This SPV Gateway provides a trust-minimized bridge, enabling smart contracts on EVM chains to cryptographically verify the state of the Bitcoin blockchain. This opens doors for exciting use cases like: |
6 | | -- **Cross-chain bridges** for Bitcoin-backed assets |
7 | | -- **Light clients** for dApps that need to confirm Bitcoin transaction finality |
8 | | -- **Decentralized custodianship** solutions |
9 | | -- **Oracle services** for Bitcoin data on EVM |
10 | | - |
11 | | -# 🚀 Key Features |
12 | | -- **Block Header Submission:** Efficiently add individual or batches of Bitcoin block headers to the contract. |
13 | | -- **Mainchain Tracking:** Automatically identifies and updates the "main" Bitcoin chain based on accumulated work. |
14 | | -- **Block Validation:** Verifies block headers against Bitcoin's consensus rules, including: |
15 | | - - Proof-of-Work (target difficulty) |
16 | | - - Block time validity (median time past) |
17 | | - - Chain continuity (previous block hash) |
18 | | -- **Block Information Retrieval:** Query detailed information about any stored block, such as: |
19 | | - - Its Merkle root |
20 | | - - Its height |
21 | | - - Its inclusion status in the mainchain |
22 | | - - Its cumulative work (difficulty) |
23 | | - - Its confirmation count relative to the mainchain head |
24 | | -- **Difficulty Adjustment:** Integrates Bitcoin's precise difficulty adjustment algorithm to accurately calculate current and future targets. |
25 | | - |
26 | | -# ⚙️ How it Works (Under the Hood) |
27 | | -The contract operates by receiving raw Bitcoin block headers, which are then parsed and validated against Bitcoin's strict consensus rules. |
28 | | - |
29 | | -1. **Header Parsing:** Raw 80-byte Bitcoin block headers are parsed into a structured *BlockHeader.HeaderData* format. This involves handling Bitcoin's unique little-endian byte ordering. |
30 | | -2. **Double SHA256 Hashing:** Each block header is double SHA256 hashed to derive its unique block hash, which is then byte-reversed for standard representation. |
31 | | -3. **Proof-of-Work Verification:** The calculated block hash is checked against the current network difficulty target (derived from the *bits* field in the header). |
32 | | -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 higher cumulative work, the *mainchainHead* is updated, reflecting potential chain reorganizations. |
33 | | -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. |
34 | | - |
35 | | -# 📊 Flow Diagrams |
36 | | -These diagrams outline the step-by-step process for adding block headers to the SPV Gateway. |
37 | | - |
38 | | -### `addBlockHeader(bytes calldata blockHeaderRaw_)` Sequence Diagram |
39 | | - |
40 | | -```mermaid |
41 | | -sequenceDiagram |
42 | | - participant Caller |
43 | | - participant SPVGateway |
44 | | - participant BlockHeaderLib |
45 | | - participant TargetsHelperLib |
46 | | -
|
47 | | - Caller->>SPVGateway: addBlockHeader(blockHeaderRaw) |
48 | | - activate SPVGateway |
49 | | -
|
50 | | - SPVGateway->>BlockHeaderLib: 1. Parse blockHeaderRaw_ (parseBlockHeaderData) |
51 | | - activate BlockHeaderLib |
52 | | - BlockHeaderLib-->>SPVGateway: 1.1. Check length (80 bytes) & LE to BE |
53 | | - alt Length Invalid |
54 | | - BlockHeaderLib--xSPVGateway: Error: InvalidBlockHeaderDataLength |
55 | | - SPVGateway--xCaller: Revert |
56 | | - end |
57 | | - BlockHeaderLib-->>SPVGateway: 1.2. Return BlockHeaderData & blockHash |
58 | | - deactivate BlockHeaderLib |
59 | | -
|
60 | | - SPVGateway->>SPVGateway: 1.3. Check blockHash existence |
61 | | - alt BlockHash Exists |
62 | | - SPVGateway--xCaller: Error: BlockAlreadyExists |
63 | | - end |
64 | | -
|
65 | | - SPVGateway->>SPVGateway: 2. Check prevBlockHash existence |
66 | | - alt Prev Block Missing |
67 | | - SPVGateway--xCaller: Error: PrevBlockDoesNotExist |
68 | | - end |
69 | | -
|
70 | | - SPVGateway->>SPVGateway: 3. Calculate newBlockHeight = prevBlockHeight + 1 |
71 | | -
|
72 | | - SPVGateway->>SPVGateway: 4. Get Current Target |
73 | | - SPVGateway->>SPVGateway: 4.1. Get target from prevBlockBits |
74 | | - SPVGateway->>TargetsHelperLib: Check if newBlockHeight is Recalculation Block (isTargetAdjustmentBlock) |
75 | | - activate TargetsHelperLib |
76 | | - alt Recalculation Block |
77 | | - SPVGateway->>SPVGateway: Recalculate target & Save lastEpochCumulativeWork |
78 | | - TargetsHelperLib-->>SPVGateway: Return newNetworkTarget |
79 | | - else Not Recalculation Block |
80 | | - TargetsHelperLib-->>SPVGateway: Use prevBlockTarget as networkTarget |
81 | | - end |
82 | | - deactivate TargetsHelperLib |
83 | | -
|
84 | | - SPVGateway->>SPVGateway: 5. Check Block Rules |
85 | | - SPVGateway->>TargetsHelperLib: 5.1. Check Header Target == Contract Target |
86 | | - activate TargetsHelperLib |
87 | | - TargetsHelperLib-->>SPVGateway: Result |
88 | | - deactivate TargetsHelperLib |
89 | | - alt Invalid Target |
90 | | - SPVGateway--xCaller: Error: InvalidTarget |
91 | | - end |
92 | | -
|
93 | | - SPVGateway->>SPVGateway: 5.2. Check newBlockHash <= networkTarget (PoW) |
94 | | - alt Invalid Block Hash |
95 | | - SPVGateway--xCaller: Error: InvalidBlockHash |
96 | | - end |
97 | | -
|
98 | | - SPVGateway->>SPVGateway: 5.3. Check newBlockTime >= medianTime |
99 | | - alt Invalid Block Time |
100 | | - SPVGateway--xCaller: Error: InvalidBlockTime |
101 | | - end |
102 | | -
|
103 | | - SPVGateway->>SPVGateway: 6. Add Block To Chain |
104 | | - SPVGateway->>SPVGateway: 6.1. Save newBlockHeader & newBlockHash to Storage |
105 | | -
|
106 | | - SPVGateway->>SPVGateway: 6.2. Update Mainchain |
107 | | - alt 6.2.1. prevBlockHash == mainchainHead? |
108 | | - SPVGateway->>SPVGateway: Move mainchainHead to newBlockHash |
109 | | - else |
110 | | - SPVGateway->>SPVGateway: 6.2.2. Calculate New Block & Current Head Cumulative Work |
111 | | - SPVGateway->>SPVGateway: 6.2.3. newBlock Cumulative Work > Current Head? |
112 | | - alt New Block Has Higher Work |
113 | | - SPVGateway->>SPVGateway: Set New Block as mainchainHead |
114 | | - SPVGateway->>SPVGateway: Recursively update mainchain path backwards (do-while loop) |
115 | | - end |
116 | | - end |
117 | | -
|
118 | | - SPVGateway->>SPVGateway: Emit BlockHeaderAdded |
119 | | - SPVGateway-->>Caller: Transaction Complete |
120 | | - deactivate SPVGateway |
121 | | -``` |
122 | | - |
123 | | -### `addBlockHeaderBatch(bytes[] calldata blockHeaderRawArr_)` Sequence Diagram |
124 | | - |
125 | | -This function processes multiple block headers in a single transaction, iterating through the array and validating each sequentially. |
126 | | - |
127 | | -```mermaid |
128 | | -sequenceDiagram |
129 | | - participant Caller |
130 | | - participant SPVGateway |
131 | | - participant BlockHeaderLib |
132 | | - participant TargetsHelperLib |
133 | | -
|
134 | | - Caller->>SPVGateway: addBlockHeaderBatch(blockHeaderRawArray_) |
135 | | - activate SPVGateway |
136 | | -
|
137 | | - SPVGateway->>SPVGateway: Check if Header Array is Empty |
138 | | - alt Array Empty |
139 | | - SPVGateway--xCaller: Error: EmptyBlockHeaderArray |
140 | | - end |
141 | | -
|
142 | | - SPVGateway->>BlockHeaderLib: 1. Parse Block Headers Array (_parseBlockHeadersRaw) |
143 | | - activate BlockHeaderLib |
144 | | - BlockHeaderLib-->>SPVGateway: Returns BlockHeaderData[] & bytes32[] |
145 | | - deactivate BlockHeaderLib |
146 | | -
|
147 | | - loop For each blockHeader in parsed array (from i=0 to length-1) |
148 | | - SPVGateway->>SPVGateway: 2. Check prevBlockHash for current block |
149 | | - alt First block in batch |
150 | | - SPVGateway->>SPVGateway: Check prevBlockHash existence (like addBlockHeader) |
151 | | - alt Prev Block Missing |
152 | | - SPVGateway--xCaller: Error: PrevBlockDoesNotExist |
153 | | - end |
154 | | - else Subsequent blocks |
155 | | - SPVGateway->>SPVGateway: Check prevBlockHash == blockHash of (i-1)th block |
156 | | - alt Order Invalid |
157 | | - SPVGateway--xCaller: Error: InvalidBlockHeadersOrder |
158 | | - end |
159 | | - end |
160 | | -
|
161 | | - SPVGateway->>SPVGateway: 3. Calculate currentBlockHeight = prevBlockHeight + 1 |
162 | | -
|
163 | | - SPVGateway->>SPVGateway: 4. Get Current Target (like addBlockHeader) |
164 | | - SPVGateway->>TargetsHelperLib: Check for Recalculation Block & Recalculate if needed |
165 | | - activate TargetsHelperLib |
166 | | - TargetsHelperLib-->>SPVGateway: Return networkTarget |
167 | | - deactivate TargetsHelperLib |
168 | | -
|
169 | | - SPVGateway->>SPVGateway: 5. Get Median Time |
170 | | - alt 5.1. Num blocks added < 12 |
171 | | - SPVGateway->>SPVGateway: Use _getStorageMedianTime (like addBlockHeader) |
172 | | - else 5.2. Num blocks added >= 12 |
173 | | - SPVGateway->>SPVGateway: Use _getMemoryMedianTime (from batch data) |
174 | | - end |
175 | | -
|
176 | | - SPVGateway->>SPVGateway: 6. Validate Block Rules (_validateBlockRules) |
177 | | - alt Validation Fails |
178 | | - SPVGateway--xCaller: Error: InvalidTarget / InvalidBlockHash / InvalidBlockTime |
179 | | - end |
180 | | -
|
181 | | - SPVGateway->>SPVGateway: 7. Add Block To Chain (_addBlock) |
182 | | - SPVGateway->>SPVGateway: Emit BlockHeaderAdded |
183 | | - end |
184 | | -
|
185 | | - SPVGateway-->>Caller: Transaction Complete |
186 | | - deactivate SPVGateway |
187 | | -``` |
188 | | - |
189 | | - |
190 | | -# 📦 Contract Components |
191 | | -The solution primarily consists of the main SPV Gateway contract and the TargetsHelper library, which manages difficulty adjustments. |
192 | | - |
193 | | -## SPVGateway |
194 | | -This is the central contract that users will interact with. It serves as the primary interface for managing Bitcoin block headers on the EVM. It handles the core logic for adding and validating blocks, tracking the main Bitcoin chain, and providing querying functionalities. All custom errors and events related to the SPV operations are defined here, ensuring clear feedback and transparency during contract execution. |
195 | | - |
196 | | -## TargetsHelper Library |
197 | | -This library encapsulates all the complex mathematical and logical operations related to Bitcoin's difficulty targets. It provides functions to accurately calculate new difficulty targets based on elapsed time between blocks, ensuring the contract adheres to Bitcoin's dynamic difficulty adjustment rules. Additionally, it offers utilities for converting between the compact "bits" format (as found in Bitcoin block headers) and the full 256-bit target value, and it calculates the cumulative work associated with a given block or epoch, which is vital for determining the most valid chain. |
198 | | - |
199 | | -# 💻 Dev Info |
200 | | -## Compilation |
201 | | -To compile the contracts, use the next script: |
202 | | - |
203 | | -```bash |
204 | | -npm run compile |
205 | | -``` |
206 | | - |
207 | | -## Test |
208 | | -To run the tests, execute the following command: |
209 | | - |
210 | | -```bash |
211 | | -npm run test |
212 | | -``` |
213 | | - |
214 | | -Or to see the coverage, run: |
215 | | - |
216 | | -```bash |
217 | | -npm run coverage |
218 | | -``` |
219 | | - |
220 | | -## Local deployment |
221 | | -To deploy the contracts locally, run the following commands (in the different terminals): |
222 | | - |
223 | | -```bash |
224 | | -npm run private-network |
225 | | -npm run deploy-localhost |
226 | | -``` |
227 | | - |
228 | | -## Bindings |
229 | | -The command to generate the bindings is as follows: |
230 | | - |
231 | | -```bash |
232 | | -npm run generate-types |
233 | | -``` |
| 1 | +# ERC-8002: SPV Gateway |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +Link to [ERC-8002](https://ethereum-magicians.org/t/erc-8002-simplified-payment-verification-gateway/25038). |
| 6 | + |
| 7 | +> [!NOTE] |
| 8 | +> Since the ERC is currently a draft, there is no deployment on mainnet available. Please use [the contract on Sepolia](https://sepolia.etherscan.io/address/0xE8e6CA2113338c12eb397617371D92239f3E6A60) for testing purposes. |
| 9 | +
|
| 10 | +# How it Works |
| 11 | + |
| 12 | +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: |
| 13 | + |
| 14 | +1. Header Parsing: Raw 80-byte Bitcoin block headers are parsed into a structured *BlockHeader.HeaderData* format, handling Bitcoin's little-endian byte ordering. |
| 15 | +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. |
| 16 | +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). |
| 17 | +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. |
| 18 | +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. |
| 19 | + |
| 20 | +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. |
| 21 | + |
| 22 | +## Submitting Bitcoin Blocks |
| 23 | + |
| 24 | +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. |
| 25 | + |
| 26 | +In case multiple blocks can be added, call `addBlockHeaderBatch` function to save ~15% on gas per block. |
| 27 | + |
| 28 | +## Verifying Bitcoin Tx Inclusion |
| 29 | + |
| 30 | +In order to verify the tx existence, the `checkTxInclusion` function needs to be called. |
| 31 | + |
| 32 | +The list parameters to be passed: |
| 33 | + |
| 34 | +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. |
| 35 | +2. `blockHash` - Hash of the block to check the tx inclusion against. This block is required to exist in the SPV storage. |
| 36 | +3. `txId` - Tx hash (Merkle leaf) to be checked. |
| 37 | +4. `txIndex` - The Merkle "direction bits" to decide on left or right hashing order. |
| 38 | +5. `minConfirmationsCount` - Number of required mainchain confirmation for the block to have. |
| 39 | + |
| 40 | +> [!TIP] |
| 41 | +> Please check out [this test case](./test/SPVContract.test.ts#L223) for more integration information. |
| 42 | +
|
| 43 | +## Permissionlessness |
| 44 | + |
| 45 | +In order for the gateway to be truly permissionless, the contract's bootstrapping needs to be permissionless as well. We are working on a "proof-of-bitcoin" ZK proof to initialize the gateway in a trustless manner. |
| 46 | + |
| 47 | +This will enable verification of historical Bitcoin transactions otherwise too expensive to include. Syncing up the gateway from Bitcoin's genesis would cost ~100 ETH on the mainnet. |
| 48 | + |
| 49 | +# Disclaimer |
| 50 | + |
| 51 | +Bitcoin + Ethereum = <3 |
0 commit comments