Verifying a Bitcoin Transaction
One of the coolest things about Clarity is that it allows us to have visibility into the state of the Bitcoin blockchain. Since Stacks blocks are mined in lockstep with Bitcoin blocks, we can directly read the burnchain header info of each Bitcoin block using Clarity's built-in get-burn-block-info?
function.
There are quite a few relatively complex things that need to happen to do this successfully, but a clarity-bitcoin library exists to make the process a lot easier and handle some of the heavy lifting for us.
Let's take a look at how to verify a Bitcoin transaction was mined using Clarity using the library. If you take a look at the clarity-bitcoin.clar
file in the linked repo, you'll find a function called was-tx-mined-compact
. That's what we'll be working with, and it looks like this:
The transaction itself is relatively simple, but there's a lot happening within other private function calls. I encourage you to read the contract for yourself and trace what is happening, step-by-step, when we call this function.
For now, we'll just go over how to actually call this function successfully.
You can see that it takes a few pieces of information:
(height uint)
the block height you are looking to verify the transaction within(tx (buff 1024))
the raw transaction hex of the transaction you are looking to verify(header (buff 80))
the block header of the block(proof { tx-index: uint, hashes: (list 14 (buff 32)), tree-depth: uint})
a merkle proof formatted as a tuple
:::info
A Merkle proof is a compact way to prove that a transaction is included in a block in the Bitcoin blockchain. Here's how it works:
Transactions in a block are hashed and paired, then the hashes of the pairs are hashed and paired, and so on until a single hash remains - this is called the Merkle root.
The Merkle root is included in the block header. By providing the hashes that lead from a transaction's hash up to the Merkle root, along with the block header, one can prove that the transaction is included in that block.
These hashes that connect a transaction to the Merkle root are called the Merkle proof or Merkle path. By providing the Merkle proof along with the transaction hash and block header, anyone can verify that the transaction is part of that block.
This allows for efficient decentralized verification of transactions without having to download the entire blockchain. One only needs the transaction hash, Merkle proof, and block header to verify.
:::
Once we have this information, we can call into the clarity-bitcoin.clar
contract and pass in this data. A common practice would be to get this data from a Bitcoin block explorer API like Mempool.space or Blockstream's esplora, parse it into the correct format for this helper, and then pass it to this function.
We could do that directly via this contract if we just need a direct response on if the transaction was included or not, but more likely we would want to integrate this functionality into a Clarity contract of our own where we can asserts!
that a transaction was mined before taking another action.
Here's a basic example where we are calling Blockstream's API using JavaScript, parsing the data into the right format, and then calling into our own mint
function to only mint an NFT if the selected transaction was mined.
We can get all the information we need with nothing but the transaction ID, which will usually be passed to us when we use a wallet like Hiro's web wallet to initiate the Bitcoin transaction.
Let's go through the code we can use to implement this. For full context, this code is taken from the example bitbadge repo, which you can take a look at. For a complete step-by-step walkthrough of how to implement this, check out the Bitcoin Primer.
Here's the mint function:
Note the (asserts! (is-eq tx-was-mined true) err-tx-not-mined)
line. This is what is doing the heavy lifting.
:::caution Right now the clarity-bitcoin library only supports legacy transactions. Work is in-progress to add support for segwit, but until then we have to do a bit of work on the front end to strip the witness data from the raw transaction hex. :::
Here's the JavaScript code we can use to get the data we need.
First we get the raw transaction and the merkle proof (we do this when we first get the transaction ID back).
The useEffect
here is so that we can check to see if the transaction was confirmed every 10 seconds before we get the rest of the information.
Then we get and parse the rest of the data when we call the actual mint function.
Last updated