Bitcoin Integration
Verifiying a Transaction

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:

  1. Verify the transaction contains the correct output (amount and recipient)
  2. Prove the transaction is included in a Bitcoin block (Merkle proof)
  3. Ensure the block is part of the canonical chain
  4. (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:

  1. ✅ Sends more than 1 BTC
  2. ✅ Sends to the correct address
  3. ✅ Is included in a valid Bitcoin block
  4. ✅ Is part of the canonical chain

You can use this transaction data to test your implementation of the BitcoinDepositor contract.