Introduce a singleton contract for on-chain verification of transactions that happened on Bitcoin. The contract is available at "0xTODO" , acting 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.
Ethereum's long-term mission has always been to revolutionize the financial world through decentralization, trustlessness, and programmable value enabled by smart contracts. Many great use cases have been discovered so far, including the renaissance of Decentralized Finance (DeFi), emergence of Real-World Assets (RWA), and rise of privacy-preserving protocols.
However, one gem has been unreachable to date -- Bitcoin. Due to its extremely constrained programmability, one can only hold and transfer bitcoins in a trustless manner. This EIP tries to expand its capabilities by laying a solid foundation for bitcoins to be also used in various EVM-based DeFi protocols, unlocking a whole new trillion-dollar market.
The singleton SPV gateway contract defined in this proposal acts as a trustless one-way bridge between Bitcoin and Ethereum, already enabling use cases such as using native BTC as a lending collateral for stablecoin loans. Moreover, with the recent breakthroughs in the BitVM technology, the full-fledged, ownerless two-way bridge may soon become a reality, powering the permissionless and wrapless issuance of BTC on Ethereum.
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
In Bitcoin, each block contains a header, which has a fixed size of 80 bytes and follows the following structure:
| Field | Size | Format | Description |
|---|---|---|---|
| Version | 4 bytes | little-endian | The version number of the block. |
| Previous Block | 32 bytes | natural byte order | The block hash of a previous block this block is building on top of. |
| Merkle Root | 32 bytes | natural byte order | A fingerprint for all of the transactions included in the block. |
| Time | 4 bytes | little-endian | The current time as a Unix timestamp. |
| Bits | 4 bytes | little-endian | A compact representation of the current target (difficulty). |
| Nonce | 4 bytes | little-endian | A 32-bit number which miners compute to find a valid block hash. |
The fields within the block header are sequentially ordered as presented in the table above.
Bitcoin's Proof-of-Work (PoW) consensus mechanism has a probabilistic finality, thus relying on a dynamic difficulty target adjustments. The target's initial value is set to 0x00000000ffff0000000000000000000000000000000000000000000000000000, which also serves as the minimum difficulty threshold.
The target is recalculated every 2016 blocks (approximately every two weeks), a period commonly referred to as a difficulty adjustment period.
The expected duration for each adjustment period is 1,209,600 seconds (2016 blocks * 10 minutes/block). The new target value is derived by multiplying the current target by the ratio of the actual time taken to mine the preceding 2016 blocks to this expected duration.
To prevent drastic difficulty fluctuations, the adjustment multiplier is capped at 4x and 1/4x respectively.
For a Bitcoin block header to be considered valid and accepted into the chain, it MUST adhere to the following consensus rules:
Chain Cohesion: The Previous Block hash field MUST reference the hash of a valid block that is present in the set of existing block headers.
Timestamp Rules:
Time field MUST be strictly greater than the Median Time Past (MTP) of the previous 11 blocks.Time field MUST NOT be more than 2 hours in the future relative to the validating node's network-adjusted time.PoW Constraint: When the block header is hashed twice using SHA256, the resulting hash MUST be less than or equal to the current target value, as derived from the difficulty adjustment mechanism.
Every Bitcoin block header has a field Merkle Root that corresponds to a Merkle root of the transactions tree included in this block.
The transactions Merkle tree is built recursively performing double SHA256 hash on each pair of sibling nodes, where the tree leaves are the double SHA256 hash of the raw transaction bytes. In case the node has no siblings, the hashing is done over the node with itself.
To verify the transaction inclusion into the block, one SHOULD build the Merkle root from the ground up and compare it with the Merkle Root stored in the selected block header. The corresponding Merkle path and hashing direction bits can be obtained and processed by querying a Bitcoin full node.
Bitcoin's mainchain is determined not just by its length, but by the greatest cumulative PoW among all valid competing chains. This cumulative work represents the total computational effort expended to mine all blocks within a specific chain.
The work contributed by a single block is inversely proportional to its target value. Specifically, the work of a block can be calculated as (2**256 - 1) / (target + 1). The target value for a block is derived from its Bits field, where the first byte encodes the required left hand bit shift, and the other three bytes the actual target value.
The total cumulative work of a chain is the sum of the work values of all blocks within that chain. A block is considered part of the mainchain if it extends the chain with the greatest cumulative PoW.
The SPVGateway contract MUST provide a permissionless mechanism for its initialization. This mechanism MUST allow for the submission of a valid Bitcoin block header, its corresponding block height, and the cumulative PoW up to that block, without requiring special permissions.
The SPVGateway MUST implement the following interface:
pragma solidity ^0.8.0;
/**
* @notice Interface for a Simplified Payment Verification Gateway contract.
*/
interface ISPVGateway {
/**
* @notice Represents the essential data contained within a Bitcoin block header
* @param prevBlockHash The hash of the previous block
* @param merkleRoot The Merkle root of the transactions in the block
* @param version The block version number
* @param time The block's timestamp
* @param nonce The nonce used for mining
* @param bits The encoded difficulty target for the block
*/
struct BlockHeaderData {
bytes32 prevBlockHash;
bytes32 merkleRoot;
uint32 version;
uint32 time;
uint32 nonce;
bytes4 bits;
}
/**
* MUST be emitted whenever the mainchain head changed (e.g. in the `addBlockHeader`, `addBlockHeaderBatch` functions)
*/
event MainchainHeadUpdated(
uint64 indexed newMainchainHeight,
bytes32 indexed newMainchainHead
);
/**
* MUST be emitted whenever the new block header added to the SPV contract state
* (e.g. in the `addBlockHeader`, `addBlockHeaderBatch` functions)
*/
event BlockHeaderAdded(uint64 indexed blockHeight, bytes32 indexed blockHash);
/**
* @notice Adds a single raw block header to the contract.
* The block header is validated before being added
* @param blockHeaderRaw The raw block header bytes
*/
function addBlockHeader(bytes calldata blockHeaderRaw) external;
/**
* @notice OPTIONAL Function that adds a batch of the block headers to the contract.
* Each block header is validated and added sequentially
* @param blockHeaderRawArray An array of raw block header bytes
*/
function addBlockHeaderBatch(bytes[] calldata blockHeaderRawArray) external;
/**
* @notice Checks that given txId is included in the specified block with a minimum number of confirmations.
* @param merkleProof The array of hashes used to build the Merkle root
* @param blockHash The hash of the block in which to verify the transaction
* @param txId The transaction id to verify
* @param txIndex The index of the transaction in the block's Merkle tree
* @param minConfirmationsCount The minimum number of confirmations required for the block
* @return True if the txId is present in the block's Merkle tree and the block has at least minConfirmationsCount confirmations, false otherwise
*/
function checkTxInclusion(
bytes32[] memory merkleProof,
bytes32 blockHash,
bytes32 txId,
uint256 txIndex,
uint256 minConfirmationsCount
) external view returns (bool);
/**
* @notice Returns the hash of the current mainchain head.
* This represents the highest block on the most accumulated work chain
* @return The hash of the mainchain head
*/
function getMainchainHead() external view returns (bytes32);
/**
* @notice Returns the height of the current mainchain head.
* This represents the highest block number on the most accumulated work chain
* @return The height of the mainchain head
*/
function getMainchainHeight() external view returns (uint64);
/**
* @notice Returns the block header data for a given block hash.
* @param blockHash The hash of the block
* @return The block header data
*/
function getBlockHeader(bytes32 blockHash) external view returns (BlockHeaderData memory);
/**
* @notice Returns the current status of a given block
* @param blockHash The hash of the block to check
* @return isInMainchain True if the block is in the mainchain, false otherwise
* @return confirmationsCount The number of blocks that have been mined on top of
* the given block if the block is in the mainchain
*/
function getBlockStatus(bytes32 blockHash) external view returns (bool, uint64);
/**
* @notice Returns the Merkle root of a given block hash.
* This function retrieves the Merkle root from the stored block header data
* @param blockHash The hash of the block
* @return The Merkle root of the block
*/
function getBlockMerkleRoot(bytes32 blockHash) external view returns (bytes32);
/**
* @notice Returns the block height for a given block hash
* This function retrieves the height at which the block exists in the chain
* @param blockHash The hash of the block
* @return The height of the block
*/
function getBlockHeight(bytes32 blockHash) external view returns (uint64);
/**
* @notice Returns the block hash for a given block height.
* This function retrieves the hash of the block from the mainchain at the specified height
* @param blockHeight The height of the block
* @return The hash of the block
*/
function getBlockHash(uint64 blockHeight) external view returns (bytes32);
/**
* @notice Checks if a block exists in the contract's storage.
* This function verifies the presence of a block by its hash
* @param blockHash The hash of the block to check
* @return True if the block exists, false otherwise
*/
function blockExists(bytes32 blockHash) external view returns (bool);
}
All fields within the BlockHeaderData struct MUST be converted to big-endian byte order for internal representation and processing within the smart contract.
The addBlockHeader function MUST perform the following checks:
- Validate that the submitted raw block header has a fixed size of 80 bytes.
- Enforce all block header validation rules as specified in the "Block Header Validation Rules" section.
- Integrate the new block header into the known chain by calculating its cumulative PoW and managing potential chain reorganizations as defined in the "Mainchain Definition" section.
- Emit a BlockHeaderAdded event upon successful addition of the block header.
- Emit a MainchainHeadUpdated event if the mainchain was updated.
The checkTxInclusion function MUST perform the following steps:
- Check whether the provided blockHash is part of the mainchain and ensure its number of confirmations is at least equal to the minConfirmationsCount parameter. If any of these checks fail, the function MUST return false.
- Using the provided merkleProof, txId, and txIndex, the function MUST compute the Merkle root.
- The computed Merkle root MUST be compared against the Merkle Root field stored within the block header identified by blockHash.
- If the computed Merkle root matches the stored Merkle Root, the function MUST return true. Otherwise, it MUST return false.
During the design process of the SPVGateway contract, several decisions have been made that require clarification. The following initialization options of the smart contract were considered:
Upon submitting the raw block header, the gateway expects the BlockHeaderData fields to be converted to big-endian byte order. This is required to maintain EVM's efficiency, which is contrary to Bitcoin's native little-endian integer serialization.
There are no "finality" rules in the SPVGateway contract. The determination of such is left to consuming protocols, allowing individual definition to meet required security thresholds.
The inclusion of an OPTIONAL addBlockHeaderBatch function offers significant gas optimizations. For batches exceeding 11 blocks, MTP can be calculated using timestamps from calldata, substantially reducing storage reads and transaction costs.
This EIP is fully backwards compatible.
TBD
TBD
A reference implementation of the SPVGateway contract can be found here.
TargetsHelper is a supporting library that provides utility functions for working with Bitcoin's difficulty targets. It includes methods to convert the Bits field from a block header to the corresponding target value and vice versa, as well as functions to calculate the new difficulty target during adjustment periods.
Please note that the reference implementation depends on the
@openzeppelin/contracts v5.2.0,@solarity/solidity-lib v3.2.0andsolady v0.1.23.
Among potential security issues, the following can be noted:
The security of the SPVGateway is directly dependent on the security of Bitcoin's underlying PoW consensus. A successful 51% attack on the Bitcoin network would allow an attacker to submit fraudulent block headers that would be accepted by the contract, compromising its state.
The block header validation rules require a Bitcoin node to check that the newly created block is not more than 2 hours ahead of the node's network-adjusted time. This check is impossible to implement on the SPVGateway smart contract, hence it is omitted.
Unlike other blockchain systems with deterministic finality, Bitcoin's consensus is probabilistic. The SPVGateway contract SHOULD be designed to handle chain reorganizations of arbitrary depth, but it cannot prevent them. As a result, transactions included in a block may not be permanently final. All dApps and protocols relying on this contract MUST implement their own security policies to determine a sufficient number of block confirmations before a transaction is considered "final" for their specific use case.
While the addBlockHeader function is permissionless and validates each new header cryptographically, the contract's initial state (its starting block header, height, and cumulative PoW) is a point of trust. The integrity of the entire chain history within the contract is built upon the correctness of this initial data. Although the EIP's design allows for flexible bootstrapping, the responsibility for verifying the initial state falls on the community and the dApps that choose to use a specific deployment of the SPVGateway.
Copyright and related rights waived via CC0.