Skip to main content
Time to read: 1 min

How to Handle Bitcoin Transactions in Solidity

Bitcoin, a decentralized digital currency, serves as both a store of value and a means of transferring wealth. Its security is rooted in the blockchain, a distributed ledger maintained by a network of miners. These miners expend significant computational power and energy to create new blocks, which are added to the blockchain every 10 minutes. The more hashing power contributed by miners, the more secure the network becomes. Learn more about Bitcoin.

Rootstock, the pioneering open-source smart contract platform built on Bitcoin, aims to enhance the Bitcoin ecosystem by introducing smart contract functionality, near-instant payments, and improved scalability. Its comprehensive technology stack, encompassing Rootstock smart contracts and the Rootstock Infrastructure Framework, is designed to foster a more equitable and inclusive financial system. Read more about the Rootstock Stack.

The Bitcoin Solidity helper library facilitates seamless interaction between Bitcoin transactions and Solidity smart contracts on the Rootstock platform. In this guide, we will learn how to handle Bitcoin transactions in a Solidity Smart contract, we will also learn how to parse transactions, hash transactions and validate scripts for bitcoin transactions. You can find the public repository for the bitcoin transaction solidity helper library.

Features of the Library

The features of the Bitcoin Solidity Helper library include:

  1. Bitcoin transaction output parsing: This accurately extracts and organizes transaction outputs from raw Bitcoin transactions. It is able to receive a raw tx and return an array of structures with the tx outputs.
  2. Bitcoin transaction hashing: This calculates the cryptographic hash of a Bitcoin transaction, ensuring its authenticity and integrity. It receives a raw tx and returns its hash.
  3. Bitcoin transaction output script validation: This verifies the validity and type of output scripts within a Bitcoin transaction, allowing for specific data extraction. It receives a raw output script, validates that it is from a specific type and returns a result. E.g. receive a raw null-data script and return the embedded data in it
  4. Bitcoin address generation: is able to generate Bitcoin the address from a specific script and also to validate if a given address was generated from a script or not.
  5. Bitcoin address validation: This checks if a Bitcoin address conforms to a particular type or format. It validates if a Bitcoin address is of a given type or not.

Prerequisites

Setup

To setup the Solidity helper library in your project, run the following npm command:

   npm install @rsksmart/btc-transaction-solidity-helper

Usage

Import the library:

import "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";

Using the library:

BtcUtils.TxRawOutput[] memory outputs = BtcUtils.getOutputs(btcTx);
bytes memory scriptData = BtcUtils.parseNullDataScript(outputs[0].pkScript);

This fragment parses a raw Bitcoin transaction to extract its outputs and then parses the first output to get the data of the null data script.

Parsing a Bitcoin Transaction Output

All the bitcoin transactions have a specific format when they are serialized. By having knowledge of this format, we can process a raw transaction in order to extract the information about its outputs.

A raw transaction has the following top-level format:

BytesNameData TypeDescription
4Versionint32_tTransaction version number (note, this is signed); currently version 1 or 2. Programs creating transactions using newer consensus rules may use higher version numbers. Version 2 means that BIP68 applies.
Variestx_in countcompactSize uintNumber of inputs in this transaction.
Variestx_intxInTransaction inputs. See description of txIn below.
Variestx_out countcompactSize uintNumber of outputs in this transaction.
Variestx_outtxOutTransaction outputs. See description of txOut below.
4lock_timeuint32_tA time (Unix epoch time) or block number. See the locktime parsing rules.

See the Reference Implementation

The approach that the library takes is to calculate, based on the length of each section, where does the output part start. After this, it starts parsing each output separately and adding its script and value into a solidity data structure.

BytesNameData TypeDescription
8Valueint64_tNumber of satoshis to spend. May be zero; the sum of all outputs may not exceed the sum of satoshis previously spent to the outpoints provided in the input section. (Exception: coinbase transactions spend the block subsidy and collect transaction fees.)
1+pk_script bytescompactSize uintNumber of bytes in the pubkey script. Maximum is 10,000 bytes.
1+pk_scriptchar[]Defines the conditions which must be satisfied to spend this output.

See the Reference Implementation

struct TxRawOutput {
uint64 value;
bytes pkScript;
uint256 scriptSize;
uint256 totalSize;
}

After finishing the processing of each output, the library returns an ordered output array, so the user can take advantage of this information in its solidity contract.

In order to show the benefits of this library, we’ll use the example of the Flyover Protocol. In this protocol, there is a smart contract that one party uses to claim a refund, in order to claim this refund, they need to prove that there was a payment with a specific amount done to a specific address in the Bitcoin Network, in order to do this, the smart contract receives the Bitcoin raw transaction. Since making this validation is not a trivial process, as it requires to parse the whole transaction, here is where we can see the utility of the library.

The usage of the output parsing functionality is the following:

BtcUtils.TxRawOutput[] memory outputs = BtcUtils.getOutputs(btcTx);

Then the user is able to perform any validation:

require(expectedValue <= outputs[0].value, "incorrect amount");
Info

The value field of the output structure is in satoshis.

Hashing Transactions

The hash algorithm used in the Bitcoin Network is just the SHA256(SHA256()) of the serialized transaction. The library exposes one function that will apply this hash algorithm to any byte array passed to it, making it easy to calculate the transaction id of any raw transaction present in the contract.

This function is specifically useful to interact with the rootstock native bridge, as many of its functions have a transaction id as parameter. For example, by using the transaction hash function, it is easy to know how many confirmations a Bitcoin block has inside a smart contract function.

Example code with explanation

Based on the example stated in the previous section, after validating that a specific transaction has an output paying a certain amount to an address. We need to know if that transaction has enough confirmations:

Here's an example:

BtcUtils.TxRawOutput[] memory outputs = BtcUtils.getOutputs(btcTx);
require(expectedValue <= outputs[0].value, "incorrect amount");
bytes32 txId = BtcUtils.hashBtcTx(btcTx)
// assuming btcBlockHeaderHash,partialMerkleTree, merkleBranchHashes
// were provided in the function parameters
uint confirmations = bridge.getBtcTransactionConfirmations(
txId,
btcBlockHeaderHash,
partialMerkleTree,
merkleBranchHashes
)
require(confirmations > expectedConfirmations, "not enough confirmations");

Read more about the bridge functionality

Script Validation for Bitcoin Transaction Output

In the Bitcoin network, when a user wants to send funds to another, the user creates a transaction and adds an output with the value that it wants to send. The other user doesn’t “receive” this amount directly, instead, we call receiving to the ability of providing the proper input to the output script so it returns true:

A transaction is valid if nothing in the combined script triggers failure and the top stack item is True (non-zero) when the script exits. Read more info in Bitcoin Script

Bitcoin Script Documentation

By having knowledge of the structure of the outputs that each type of address has, we can process and validate any arbitrary output extracted with the functions explained in the previous sections. In the same way, we can parse those outputs to obtain the specific value that later is encoded (in base58check, bech32 or bech32m) and presented as the “destination address”.

The output that the library supports and is able to parse to an address are:

  • P2PKH (Pay to public key hash)
  • P2SH (Pay to script hash)
  • P2WPKH (Pay to witness public key hash)
  • P2WSH (Pay to witness script hash)
  • P2TR (Pay to taproot)

Some use cases for script validation:

As seen in the previous example, we validated inside our smart contract that a Bitcoin transaction has the correct amount and enough confirmations, now we need to validate that it was performed on the correct address. To do this, the library has the capability of parsing an arbitrary output and converting it into an address.

Here's an example:

bytes memory btcTxDestination = BtcUtils.outputScriptToAddress(
outputs[0].pkScript,
mainnetFlag
);
require(keccak256(expectedAddress) == keccak256(btcTxDestination), "incorrect address");

Conclusion

Congratulations, we have successfully learnt how to use the Solidity Helper library to parse, hash, and validate scripts within Bitcoin transactions. By using this library, developers can gain valuable insights into Bitcoin transaction data and build more sophisticated smart contract dApps on Rootstock.

Some future enhancements to the library includes:

  • Transaction Input Parsing: The ability to extract and analyze transaction input data to receive a raw tx and return an array of structs with the tx inputs.
  • Transaction Creation: Utilities to facilitate the creation of raw Bitcoin transactions within smart contracts.
Last updated on by Owanate Amachree