BTC On-Chain Verification
This document explains how xBTC verifies Bitcoin block hashes on smart contract blockchains (Ethereum, Solana, and other EVM-compatible chains) in a trustless manner, without relying on oracles or centralized validators.
Contents
1. Overview
1.1 The Problem
Cross-chain applications need to verify Bitcoin transactions on other blockchains. Traditional approaches rely on:
- Trusted Oracles - Third parties that attest to Bitcoin state
- Centralized Validators - Single points of failure
- Multi-sig Committees - Trust assumptions on committee members
All these approaches introduce trust assumptions and potential failure points.
1.2 Our Solution: Optimistic Verification with Challenge
We use an optimistic verification model that leverages Bitcoin's own Proof-of-Work security:
Submit Block Hash
User submits a Bitcoin block hash with a deposit, claiming it exists on mainnet.
Challenge Period
Anyone can challenge by submitting competing block headers with valid Proof-of-Work.
Chain Work Comparison
The system tracks cumulative work for each chain branch. Highest work wins.
Resolution
After timeout, the winning chain determines validity. Winners get rewards; losers lose deposits.
1.3 Why This Works
The Attacker's Dilemma
To submit a fake block hash successfully, an attacker must produce more Proof-of-Work than the entire Bitcoin network. With Bitcoin's massive global hash rate (hundreds of EH/s and growing), the cost of attack far exceeds any potential gain.
The security comes from a simple insight:
- Honest Submission: If challenged, anyone can submit real Bitcoin block headers. The real chain always has the most accumulated work.
- Fraudulent Submission: An attacker mining alone cannot outpace all Bitcoin miners combined. They will lose their deposit.
Key Properties
Trustless: No oracles or centralized validators needed.
Permissionless: Anyone can submit blocks or challenge fraudulent claims.
Economically Secure: Attack cost scales with Bitcoin's total hash rate.
2. Implementation Details
Technical Content
This section contains detailed technical implementation information intended for developers and technical users. If you're not interested in the technical details, you can skip this section.
Our implementation is based on the SelfValidator smart contract. This section focuses on how block headers form a tree structure during the challenge period.
2.1 Core Concept: Block Tree
During the challenge period, submitted block headers form a tree structure with two types of branches:
| Branch Type | Condition | Description |
|---|---|---|
| Supporting Branch | firstBlockHash == validation.blockHash |
Blocks that support the validity of the submitted block hash. These blocks use the validation target as their root. |
| Opposing Branch | firstBlockHash != validation.blockHash |
Blocks that contest the validity. These blocks start from a different root, claiming an alternative chain has more work. |
2.2 Tree Structure Visualization
2.3 State Transitions in submitBlockHeader
When a user calls submitBlockHeader(id, blockHeaderBytes), the contract determines which state transition applies based on three key conditions:
State Variables
| Variable | Meaning |
|---|---|
hashCurrentBlock == validation.blockHash |
Is the submitted block the validation target? |
prevBlockInfo.firstBlockHash != bytes32(0) |
Has the previous block been added? |
currentBlockInfo.firstBlockHash != bytes32(0) |
Has the current block been added? |
currentBlockInfo.addTime > firstBlockInfo.addTime |
Can the block be overwritten? (addTime condition) |
All Possible State Combinations
| # | Current == Validation | Prev Exists | Current Exists | addTime Condition | Result |
|---|---|---|---|---|---|
| 1 | Yes | - | No | - | Add as Supporting Tree Root |
| 2 | Yes | - | Yes | - | Reject "dup" |
| 3 | No | No | No | - | Add as New Tree Root (Opposing) |
| 4 | No | No | Yes | - | Reject "dup" |
| 5 | No | Yes | No | - | Normal Add to prev's tree |
| 6 | No | Yes | Yes | Met | Overwrite (refund original) |
| 7 | No | Yes | Yes | Not Met | Reject "NO" |
Case 1: Supporting Tree Root
// hashCurrentBlock == validation.blockHash && not exists
if (hashCurrentBlock == validation.blockHash) {
require(currentBlockInfo.firstBlockHash == bytes32(0), "dup");
addNewBlock(id, hashCurrentBlock, bytes32(0), ...);
}
The validation target becomes the supporting tree root. Its firstBlockHash = self and addTime = 0 (highest priority).
Case 2: Duplicate Validation Target
If the validation target has already been submitted, the transaction reverts with "dup".
Case 3: New Opposing Tree Root
// hashCurrentBlock != validation.blockHash && prevBlock not exists && current not exists
if (prevBlockInfo.firstBlockHash == bytes32(0)) {
require(currentBlockInfo.firstBlockHash == bytes32(0), "dup");
addNewBlock(id, hashCurrentBlock, bytes32(0), ...);
}
When the previous block doesn't exist, this block becomes a new opposing tree root. Its firstBlockHash = self and addTime = block.timestamp.
Case 4: Duplicate Without Valid Path
If the previous block doesn't exist but current block already exists, the transaction reverts with "dup". Cannot overwrite without a valid prev path.
Case 5: Normal Extension
// prevBlock exists && current not exists
addNewBlock(id, hashCurrentBlock, hashPrevBlock, ...);
The block is added to the same tree as its previous block. It inherits firstBlockHash and addTime from the tree root.
Case 6: Block Overwrite
// prevBlock exists && current exists && addTime condition met
if (currentBlockInfo.addTime > firstBlockInfo.addTime) {
player.transfer(refundValue); // Refund original submitter
addNewBlock(id, hashCurrentBlock, hashPrevBlock, ..., refundValue, oldFirstBlockHash);
}
Overwrite Mechanism
If a block exists in Tree A but Tree B (created earlier) wants to claim it, and block.addTime > TreeB.root.addTime, the block is reassigned to Tree B.
The original submitter is refunded, and valueYes/valueNo is correctly adjusted between the two camps.
Case 7: Overwrite Rejected
If the addTime condition is not met (currentBlockInfo.addTime ≤ firstBlockInfo.addTime), the original tree has higher priority. Transaction reverts with "NO".
2.4 Chain Work and Winner Selection
After each block submission, the contract compares chain work:
// Update winner if this chain has more cumulative work
if (currentBlockInfo.chainWork > winnerBlockInfo.chainWork) {
validation.winnerBlockHash = hashCurrentBlock;
}
The winnerBlockHash always points to the block with the highest chainWork.
Chain Work Calculation
if (prevBlockHash == bytes32(0)) {
// First block in branch
currentBlock.chainWork = difficulty;
currentBlock.blockAmount = 1;
} else {
// Extending existing branch
currentBlock.chainWork = prevBlock.chainWork + difficulty;
currentBlock.blockAmount = prevBlock.blockAmount + 1;
}
2.5 Stake Tracking
The contract tracks total stakes on each side:
| Variable | Condition | Meaning |
|---|---|---|
valueYes |
firstBlockHash == validation.blockHash |
Total deposits supporting validity |
valueNo |
firstBlockHash != validation.blockHash |
Total deposits opposing validity |
// Step 1: Set addTime based on tree position
if (prevBlockHash == bytes32(0)) {
currentBlock.addTime = block.timestamp; // Tree root
} else {
currentBlock.addTime = prevBlock.addTime; // Inherit from tree root
}
// Step 2: Track stakes and override addTime for supporting branch
if (currentBlock.firstBlockHash == validation.blockHash) {
validation.valueYes += value;
currentBlock.addTime = 0; // Supporting branch has highest priority
} else {
validation.valueNo += value;
}
2.6 Final Resolution
After the deadline passes, setBlockHashStatus determines the outcome:
function setBlockHashStatus(uint256 id) public nonReentrant {
require(block.timestamp > validation.deadline);
bytes32 winnerBlockHash = validation.winnerBlockHash;
BlockHeaderInfo storage winnerBlockInfo = blockHeaderInfo[id][winnerBlockHash];
// Check if winner belongs to supporting branch (or no challenge occurred)
if (winnerBlockInfo.firstBlockHash == validation.blockHash ||
winnerBlockInfo.firstBlockHash == bytes32(0)) {
// Supporting branch wins → blockHash is VALID
blockMapping[validation.blockHash] = true;
payable(validation.player).transfer(validation.value); // Refund only
} else {
// Opposing branch wins → blockHash is INVALID
blockMapping[validation.blockHash] = false;
}
}
Outcome Summary
| Scenario | Winner | Result | Reward Pool |
|---|---|---|---|
| Supporting branch has more work | Supporters | Block hash marked valid | valueNo distributed to winners |
| Opposing branch has more work | Challengers | Block hash marked invalid | valueYes distributed to winners |
2.7 Deadline Extension Strategy
Each block submission resets the challenge deadline:
validation.deadline = block.timestamp + TIMEOUT;
This creates an important defense mechanism for honest participants:
Delay Strategy
When facing a malicious challenge but the next real Bitcoin block hasn't been mined yet, an honest submitter can submit any valid Bitcoin block header (even older blocks with lower difficulty) to extend the deadline.
The block only needs to satisfy hash <= target. The submitter's deposit will be refunded after winning, so the maximum cost is just gas fees.
How It Works
Key Points
- Valid PoW Required: The delay block only needs
hash <= target. Any valid Bitcoin block header works. - Deposit Refunded: Winners can call
claimRefund(id, blockHash)to recover deposits for each block they submitted on the winning branch. - Gas-Only Cost: The only irreversible cost is the gas fee for submitting the delay block.
- Buys Time: Each submission adds another
TIMEOUT(15 minutes) to wait for real Bitcoin blocks.
Note
This strategy ensures honest participants are never at a disadvantage due to Bitcoin's ~10 minute block time. They can always extend the deadline until real blocks with overwhelming chainWork arrive.
2.8 Configuration Parameters
| Parameter | Value | Description |
|---|---|---|
TIMEOUT |
15 minutes | Challenge period duration (extended with each submission) |
PAY_MIN_VALUE |
0.1 ETH | Minimum deposit required |
PAY_VALUE |
Configurable | Current deposit requirement (>= PAY_MIN_VALUE) |
Deadline Extension
Each block submission resets the deadline: deadline = block.timestamp + TIMEOUT
This prevents premature finalization and ensures all parties have time to respond.