Building Blockchain Transactions with Python
Arthur Gordon
-
30 October 2024
This article provides a step-by-step guide to building blockchain transactions in Python.
Note the code in this article is provided as a Jupyter notebook to make it easier to follow, see here:
https://github.com/nchain-innovation/build-tx-example/blob/main/build_tx.ipynb
As with most software engineering, being hands on helps build a deeper understanding of what is going on. In this case, understanding the capability of transactions and the Forth-based scripting language they employ. This language can be used to validate transaction signatures, embed data and even verify zero knowledge proofs as my colleagues have demonstrated here:
https://medium.com/@w.zhang/snark-verification-on-a-bitcoin-mainnet-c467991d931c
Note the term ‘transaction’ is often abbreviated as ‘tx’. I’ll use both terms interchangeably throughout this article.
The steps to create a transaction (tx) are:
- Generate a wallet keypair
- Fund the wallet keypair
- Create the Transaction
- Broadcast the Tx
The BSV blockchain also has the following networks:
- Mainnet — Used for real world applications that need the blockchain guarantees of being persistent.
- Testnet — Used for testing transactions, there is no guarantee that this blockchain will be maintained and transactions may be lost over time.
- STN — Used for performance testing of blockchain infrastructure.
In this case we will be using BSV Testnet. The current status of BSV Testnet can be seen by going to the WhatsOnChain website
https://test.whatsonchain.com/
From the WhatsOnChain website we can see the latest blocks, their block height and current age.
By clicking on a block, we can view the transactions it contains.
Note that each transaction has a transaction id, or txid, which is a unique identifier created from the hash of the transaction.
Generate a Wallet Keypair
The wallet keypair is composed of a private and public key. These keys are associated with funds in the blockchain. The public key enables others to send funds to you. The private key is used to sign the transaction to demonstrate that you are entitled to the funds in that tx input, hence why this key should be protected.
For this we will use the tx-engine library from PyPi
https://pypi.org/project/tx-engine/
To install the library use:
pip install tx-engine
The tx-engine library has a Wallet class which provides the key generation and handling functions that we require.
% python3
>>> from tx_engine import Wallet
>>> my_wallet = Wallet.generate_keypair("BSV_Testnet")
This generates a wallet containing a unique keypair.
>>> my_wallet.to_wif()
‘cNqYa1cV7vbz1GLt6fCxgM143y……L7tTJBQ5EhtJ133QWoxU9’
The to_wif() method exports the private key in Wallet Independent Format (WIF). Keep this string as it is used to import the private key into this or another wallet. Note that as this is the private key it should be kept secret (and not placed in your GitHub repository etc.).
>>> my_wallet.get_address()
‘mro6fhFv852664upXfugUCMPHnVNbEohKp’
The address is the public key hashed and displayed in base58 format. The network it is intended for is represented by the first letter at the start of the address (in this case ‘m’ (or ’n’) for testnet).
Fund the Wallet Keypair
Some BSV developers generously provide Faucets for funding BSV Testnet transactions, for this article we have used:
https://witnessonchain.com/faucet/tbsv
This Faucet requires your wallet address (in our example mro6fhFv852664upXfugUCMPHnVNbEohKp) to know where to send the funding satoshi to. Once you press the “shoot me the coin” button the funds will be sent as a transaction which you can find on WhatsOnChain by searching by your address.
Make a note of the transaction id (txid) and the index of the output that is sent to your address.
This faucet produced a tx with one input and one output, at the time of writing, so the index will be 0.
From the diagram we can see:
- Input was 0.001 BSV, where 1 BSV is 100,000,000 satoshi, so the input is 100,000 satoshi.
- Under the tx output we can see the value as 0.0099904 so 99,904 satoshi.
So what happened to the difference between the input and output 100,00–99,904 = 96 satoshi?
This value was used to fund the transaction and is allocated to the miner for including the transaction in a block to be mined. Transaction fees are based on the size, in bytes, of the transaction. For small transactions approximately 100 satoshi should be sufficient on BSV Testnet.
So in summary the tx fee is calculated by:
tx_fee = input_value - output_value
Create Tx
In this case we will build a transaction to pay 1,000 satoshi back to the funding Faucet
The tx_engine has the following classes to help construct and decode transactions:
- TxIn — the tx input
- TxOut — the tx output
- Tx — represents a full transaction
TxIn
The transaction input provides the funds for this transaction.The transaction input, TxIn constructor takes the parameters:
- prev_tx — the transaction id (txid) of the funding transaction
- prev_index — the index of the transaction output that we are spending (note tx indexes start at 0)
This is where the tx from the Faucet comes into play. By noting the txid and index of the funding transaction we can generate a tx input using:
>>> from tx_engine import Tx, TxIn, TxOut
>>> vins = [TxIn(prev_tx="5c866b70189008586….93dcca4d3a1b701e3786566f819450eca9ba",
prev_index=0)]
Note that we have put the input into an array which we will be using in the transaction later.
TxOut
The output declares where we want the funds to go, which is 1,000 to the Faucet and the rest to the newly generated public key, minus the fee for the transaction, say 104 satoshi.
So we had 99,904 satoshi in and we will get 99,904 — (1000 + 104) = 98,800 satoshi as the remainder.
>>> from tx_engine import p2pkh_script, address_to_public_key_hash
Start by importing a couple of helper functions
- Address_to_public_key_hash — this takes an address and returns a public key hash (as required by p2pkh_script)
- p2pkh_script — this takes a hash of a public key and returns a pay to public key hash (P2PKH) script, this is the most common script for payment on BSV
So we will create two outputs for our transaction, the first will pay back to The witness on chain Faucet address (mnai8LzKea5e3C9qgrBo7JHgpiEnHKMhwR).
>>> vouts = []
>>> faucet_addr = "mnai8LzKea5e3C9qgrBo7JHgpiEnHKMhwR"
>>> locking_script = p2pkh_script(address_to_public_key_hash(faucet_addr))
>>> vouts.append(TxOut(amount=1000, script_pubkey=locking_script))
We can examine the locking_script code by typing:
>>> locking_script
OP_DUP OP_HASH160 0x4d7eb7ce5ce099dd218383f3f81d3c2f1e48113f OP_EQUALVERIFY OP_CHECKSIG
Now you can pay yourself the remainder using the Wallet method get_locking_script() to provide a P2PKH script to pay to your address.
>>> vouts.append(TxOut(amount=98800, script_pubkey=my_wallet.get_locking_script()))
Tx
Now we assemble the transaction by placing the inputs and outputs in a transaction as follows:
>>> tx = Tx(version=1, tx_ins=vins, tx_outs=vouts, locktime=0)
We can see that the transaction has a version field. Most transactions use version 1, there have been BIP (Bitcoin Improvement Proposal) that have associated other version numbers with special behaviour (See BIP68, BIP112).
The locktime defines the earliest time that a transaction is valid. In our case we want the transaction to be processed as soon as possible so we set this to 0.
Signing the Transaction
The next stage is to sign the transaction. This demonstrates that you have the private key associated with the p2pkh script of the output of the previous/funding transaction.
To sign the transaction we need access to data from the funding transaction, which we will get using the WhatsOnChain’s API, which is setup as follows:
>>> from tx_engine import WoCInterface
>>> woc_interface = WoCInterface()
>>> config = {“network_type”: “testnet”}
>>> woc_interface.set_config(config)
Now to get the funding transaction we use the get_raw_transaction method with the txid of the funding tx, this returns the transaction in hex string format.
>>> funding_tx = woc_interface.get_raw_transaction(“e11d8bbd69db4d6…a68fae9c22054d0”)
>>> funding_tx
'010000000184dae2675b22df71193ba6e0eddb1344fee9c77b877969e222c8f10adc53b07a1300000 06a47304402204a6cd4349c598d41960d53daa03c8ef6be603c1258671639f9df562915d4b4ba022015 fdb2f6af587f3600bd8069ff0d8764af459c1de8c19c526051ae47fef7e33141210292acdb57c788c1e 8c83cdb0ae8f23e079139ba7ba1bccf67b31653c7af12c4b4ffffffff0140860100000000001976a914 7bb7074e3ee92db046fd13752b665de27175cde188ac00000000'
Note that if you get a SSLError or SSLCertVerificationError exception on calling get_raw_transaction you may have a VPN/Firewall in place that prevents direct access to the WhatsOnChain API (‘api.whatsonchain.com’). Contact your IT Support to see if they can provide a fix.
The parse_hexstr method takes the hex string and returns the transaction in Tx format, which is required for signing the transaction we will broadcast.
>>> fund_tx = Tx.parse_hexstr(funding_tx)
>>> fund_tx
PyTx { version: 1, tx_ins: [PyTxIn { prev_tx: "7ab053dc0af1c822e26979877bc7e9fe4413dbede0a63b1971df225b67e2da84", prev_index: 19, sequence: 4294967295, script_sig: "0x304402204a6cd4349c598d41960d53daa03c8ef6be603c1258671639f9df562915d4b4ba022015fd b2f6af587f3600bd8069ff0d8764af459c1de8c19c526051ae47fef7e33141 0x0292acdb57c788c1e8c83cdb0ae8f23e079139ba7ba1bccf67b31653c7af12c4b4" }], tx_outs: [PyTxOut { amount: 99904, script_pubkey: "OP_DUP OP_HASH160 0x7bb7074e3ee92db046fd13752b665de27175cde1 OP_EQUALVERIFY OP_CHECKSIG" }], locktime: 0 }
The following signs the transaction tx, given the input to sign (0), the funding transaction and tx. If successful it returns a new signed transaction (signed_tx).
>>> input_to_sign = 0
>>> signed_tx = my_wallet.sign_tx(input_to_sign, fund_tx, tx)
>>> signed_tx
PyTx { version: 1, tx_ins: [PyTxIn { prev_tx: "d261d1388e5a77119b6d6efb41d3610eb363750056895f4043c8cc7899f4cb17", prev_index: 0, sequence: 4294967295, script_sig: "0x3045022100aacb290ed3aeb43fc91a179d6a3ffef4c5efcca612c901c719e198c2ee685e27022005 05bd74db673c6d723a141bd8ac469327ea4bb7987110042f23f8c3d7f91e3d41 0x03dcf21dbdbaa744333af236c3382c85d6308e6d05599df5d3cb19e0f19a205d43" }], tx_outs: [PyTxOut { amount: 1000, script_pubkey: "OP_DUP OP_HASH160 0x4d7eb7ce5ce099dd218383f3f81d3c2f1e48113f OP_EQUALVERIFY OP_CHECKSIG" }, PyTxOut { amount: 98800, script_pubkey: "OP_DUP OP_HASH160 0xd86625de492d8bd8bbc4930f2bef4328e37f1f53 OP_EQUALVERIFY OP_CHECKSIG" }], locktime: 0 }
Finally you have a complete transaction which you can now broadcast to the network!
Note that we can see the results of the signing operation in the script_sig section of the input, we will describe how this works with the script_pubkey in another article.
Broadcast the Transaction
To broadcast the transaction we will use the WhatsOnChain API broadcast_tx method. This returns the txid if successful or an error message if not.
>>> result = woc_interface.broadcast_tx(signed_tx.serialize().hex())
>>> result.status_code
200
>>> result.json()
'72f201f87e3bb42bbd7f8eaed0f555a268a0eba5d0fb101160167c758ed863bb'
Congratulations you have now broadcast your first transaction!
If you want to check that the txid is the same as that of the transaction that was sent, use the following id() method on the transaction. This call returns the txid of the transaction.
>>> signed_tx.id()
'72f201f87e3bb42bbd7f8eaed0f555a268a0eba5d0fb101160167c758ed863bb'
The txid can be used to find the transaction in WhatsOnChain.
From the screen shot we can see that we have created a transaction with one input and two outputs
Summary
Well done, if you have got this far you have created and funded a wallet and transaction which has now been broadcast on the network and will be mined into a block.
Stay tuned for future articles which will describe what is going on in the script and how to perform more complex operations using blockchain transactions and scripts.