Verifying Bitcoin Transactions on Starknet: BitcoinDepositor Tutorial
Overview
In this tutorial, we'll build a "BitcoinDepositor" contract where a user can become the official depositor by being the first to send 1 BTC or more to a specific Bitcoin address: 1LgXWxpELt2o9hPGiwqDT1B5Z7994MQPTN
. We'll learn how to verify these Bitcoin transactions on Starknet using the Utu Relayer Contract (opens in a new tab).
The complete code for this tutorial is available in the UTU example repository (opens in a new tab), which uses Raito (opens in a new tab) for Bitcoin transaction verification.
Contract Implementation
Contract Interface
#[starknet::interface]
pub trait IBitcoinDepositor<TContractState> {
fn prove_deposit(
ref self: TContractState,
deposit_tx: Transaction,
output_id: usize,
block_height: u64,
block_header: BlockHeader,
tx_inclusion: Array<(Digest, bool)>
);
fn get_depositor(self: @TContractState) -> ContractAddress;
}
Storage
#[storage]
struct Storage {
depositor: ContractAddress,
utu_address: ContractAddress,
}
Core Verification Logic
When verifying Bitcoin transactions on Starknet, several crucial checks need to be performed:
- Verify the transaction contains the correct output (amount and recipient)
- Prove the transaction is included in a Bitcoin block (Merkle proof)
- Ensure the block is part of the canonical chain
- (Recommended) Add security checks for block maturity and cumulative work
The following implementation shows the basic verification, but for production use, you should add additional security measures:
#[external(v0)]
impl BitcoinDepositor of IBitcoinDepositor<ContractState> {
fn prove_deposit(
ref self: ContractState,
deposit_tx: Transaction,
output_id: usize,
block_height: u64,
block_header: BlockHeader,
tx_inclusion: Array<(Digest, bool)>
) {
// Check no previous depositor
assert(self.depositor.read() == Zero::zero(), 'too late, someone deposited');
// 1. Verify amount and recipient
let output_to_check = deposit_tx.outputs[output_id];
assert(*output_to_check.value > 100_000_000_u64, 'you sent less than 1 BTC');
let target = extract_p2pkh_target(*output_to_check.pk_script);
assert(target == "1LgXWxpELt2o9hPGiwqDT1B5Z7994MQPTN", 'wrong receiver');
// 2. Verify transaction inclusion in block
let tx_bytes_legacy = @deposit_tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let merkle_root = compute_merkle_root(txid, tx_inclusion);
assert(
block_header.merkle_root_hash.value == merkle_root.value,
'invalid inclusion proof'
);
// 3. Verify block is in canonical chain
let utu = IUtuRelayDispatcher { contract_address: self.utu_address.read() };
let canonical_block_digest = utu.get_block(block_height);
assert(canonical_block_digest == block_header.hash(), 'invalid block digest');
// TODO: Add recommended security checks:
// - Ensure block is mature enough (e.g., 6+ confirmations)
// - Verify cumulative proof of work meets threshold
// - Check block timestamp is within acceptable range
// - Consider implementing transaction output spending verification
// Set depositor
self.depositor.write(get_caller_address());
}
fn get_depositor(self: @ContractState) -> ContractAddress {
self.depositor.read()
}
}
Generating Proof Data with TypeScript
Here's how to generate the transaction proof data using TypeScript:
todo
Testing with Actual Data
You can test the contract using this real Bitcoin transaction that sent 1.00043947 BTC to our target address:
Transaction Details:
- TXID:
fa89c32152bf324cd1d47d48187f977c7e0f380f6f78132c187ce27923f62fcc
- Block Height: 868239
- Block Hash:
00000000000000000001060b021606cca1bd2c9b4eb15b6a31d2d76bf7f485fd
- Amount: 1.00043947 BTC (100,043,947 satoshis)
- Recipient:
1LgXWxpELt2o9hPGiwqDT1B5Z7994MQPTN
- Block Time: 1730373503 (March 30, 2024)
The transaction can be viewed on any Bitcoin block explorer:
https://blockstream.info/tx/fa89c32152bf324cd1d47d48187f977c7e0f380f6f78132c187ce27923f62fcc
This transaction meets all our requirements:
- ✅ Sends more than 1 BTC
- ✅ Sends to the correct address
- ✅ Is included in a valid Bitcoin block
- ✅ Is part of the canonical chain
You can use this transaction data to test your implementation of the BitcoinDepositor contract.