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 and has sufficient proof of work
let utu = IUtuRelayDispatcher { contract_address: self.utu_address.read() };
// Requires 100 sextillion (10^36) expected hashes (strong security)
utu.assert_safe(block_height, block_header.hash(), 100_000_000_000_000_000_000_000, 0);
// 4. Verify block timestamp is not from the future
let block_time = u32_byte_reverse(block_header.time).into();
assert(block_time <= get_block_timestamp(), 'Block comes from the future');
// Set depositor
self.depositor.write(get_caller_address());
}
fn get_depositor(self: @ContractState) -> ContractAddress {
self.depositor.read()
}
}
Providing Bitcoin 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 (opens in a new tab)
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
Generating the Proof with TypeScript
Let's use the bitcoin-on-starknet.js (opens in a new tab) library to generate and submit the proof for our transaction. Here's how to do it:
import {
BitcoinProxiedRpcProvider,
UtuProvider,
serializedHash,
} from "bitcoin-on-starknet";
import { Account, RpcProvider, TransactionType, Invocations } from "starknet";
// Pre-formatted transaction data for Starknet processing
const EXAMPLE_SERIALIZED_TRANSACTION = [
2n, 0n, 1n, 4n,
// ... transaction details ...
744843869111954496999033090920949585736336206836849668294828n,
25n, 0n, 0n,
] as const;
async function main() {
// Initialize providers
const bitcoinProvider = new BitcoinProxiedRpcProvider("https://btcrpc.lfg.rs/rpc");
const utuProvider = new UtuProvider(bitcoinProvider);
const starknetProvider = new RpcProvider({
nodeUrl: "https://sepolia.rpc.starknet.id",
});
// Initialize Starknet account
const account = new Account(
starknetProvider,
process.env.STARKNET_ADDRESS as string,
process.env.STARKNET_PRIVATE_KEY as string
);
try {
// Fetch transaction and generate proof
const txId = "fa89c32152bf324cd1d47d48187f977c7e0f380f6f78132c187ce27923f62fcc";
const rawTransaction = await bitcoinProvider.getRawTransaction(txId, true);
const header = await bitcoinProvider.getBlockHeader(rawTransaction.blockhash);
// Generate sync transactions and Merkle proof
const syncTransactions = await utuProvider.getSyncTxs(
starknetProvider,
header.height,
0n
);
const txInclusionProof = await utuProvider.getTxInclusionProof(txId);
// Prepare calldata for prove_deposit
let calldata = EXAMPLE_SERIALIZED_TRANSACTION.map(n => "0x" + n.toString(16));
calldata.push("0x0"); // output_id
calldata.push("0x" + header.height.toString(16));
calldata.push(...utuProvider.serializeBlockHeader(header));
// Add Merkle proof
calldata.push(txInclusionProof.length);
txInclusionProof.forEach(([hash, direction]: [string, boolean]) => {
calldata.push(...serializedHash(hash));
calldata.push(direction ? "0x1" : "0x0");
});
// Send a multicall syncing the chain before interacting with our contract
await account.execute([...syncTransactions, proveDepositCall]);
} catch (error) {
console.error("Error:", error);
}
}
main();
You can find the complete code example, including transaction simulation, in the bitcoin-on-starknet.js repository (opens in a new tab).