Bitcoin Scripting

Go up to the CCC HW page (md) | view tabbed version

Overview

In this assignment you will be writing a series of Bitcoin scripts to enact transfers. You will be using a Bitcoin test network to do so. While regular Bitcoin uses the abbreviation BTC, we will use the abbreviation ‘BCY’ for the Bitcoin on our test network; it is called BCY because it is a testnet from BlockCypher.com.

There are four separate Bitcoin scripts that you will need to write. You will need to be familiar with the Bitcoin slide set, specifically the Bitcoin Script and Cross-Chain Transactions sections. You will also likely need to refer to the Bitcoin Script wiki page.

You will be submitting an edited version of scripts.py (src).

Changelog

Any changes to this page will be put here for easy reference. Typo fixes and minor clarifications are not listed here. So far there aren’t any significant changes to report.

Languages

This assignment uses the python-bitcoinlib package (documentation is here, if you are interested, but you probably won’t need it). Thus, this assignment must be completed in Python. You can install the Python package via pip install python-bitcoinlib (you may need to use pip3 on your system). Note that bitcoinlib, python-bitcoinlib, and bitcoin are all different libraries! We are specifically using python-bitcoinlib.

You also will have to install the requests library: pip install requests, as one of the provided files, below, uses that library.

We provide you with a few files to use:

Packages

Windows: Some people have had issues (as of February 2026) with it working natively in Windows. The suggested way on a Windows platform is to install WSL. Run the following commands, but from a WSL prompt.

sudo apt update
sudo apt install python3-pip python3-virtualenv libssl-dev

You then have to install the two packages (python-bitcoinlib and requests). The recommended way to do this is to create a virtual environment and use that:

The next time you start a WSL prompt, you will just have to do the source venv/bin/activate line. You can also run deactivate to leave the virtual environment.

Mac OS X: This is done through homebrew. Ensure Python, pip, and virtualenv are installed. Python and pip are probably already installed; you can install virtuanenv via pip3 install virtualenv. Then try the three commands above (the ones that start with virtualenv, source, and pip). The only complication is if it can’t find the SSL library, but that should be installed already.

Linux: run the three commands above (the ones that start with virtualenv, source, and pip), but from a normal terminal prompt.

Hints

This can be a tricky assignment, and there are a lot of ways to run into problems. We include a number of hints here to try to head that off – please read through all of these!

Development Tips

General Hints

  1. UTXO indices: each transaction has one more more UTXO indices – each transaction output creates a separate UTXO index. To find out what UTXO index you need to use, view the transaction on the blockcypher.com website (the URL for that will be discussed shortly). All UTXO indices start from 0, like arrays. In particular, for your funding transaction, your UTXO index may not be 0. You need to set the UTXO variable for EACH transaction to the correct UTXO index. The preferred way to do this is via the second command line parameter, but can also be set via the utxo_index value in scripts.py. Note that the command-line parameter will override the utxo_index value.
  2. After each transaction, there is a place to store the transaction hash. Be diligent about doing this – it’s really easy to lose track of which of a dozen transaction hashes is which. Keeping them in the stated variables will help with this. You are welcome to store other values in additional variables, as long as they are different names than the ones currently in scripts.py.
  3. Don’t modify the variable or function names in the scripts.py file. Otherwise the provided functions, and our grading routines, will not work. You can add functions and variables with different names, but don’t change the ones currently there.
  4. For EACH transaction, you will need to set the UTXO. If you get an error stating that the UTXO index is already spent, it’s likely that you forgot to set this variable. If it sounds like we are repeating this, it is because we are – this is, by far, the most common mistake made in this assignment.
  5. Some errors with the Bitcoin scripts can be determined prior to broadcasting it on the Bitcoin test network. This is done by the VerifyScript() method, which the provided code base calls for you before any attempted broadcast transaction. So if you see an error such as, verifyerror: "bitcoin.core.scripteval.VerifyOpFailedError: EvalScript: OP_EQUALVERIFY failed, or similar, it means that the Bitcoin library was able to detect that your script would not work, and did not broadcast the transaction.
  6. We provide you with a create_CHECKSIG_signature() function in the scripts.py file – use it! See the comments in that file for details as to how. We also describe its usage below (the end of the Python Library section).
  7. To save you the tedious task of having to learn the Python Bitcoin library (documentation is here, if you are interested) – which you probably will never need to use again – much of the library interaction has been handled for you by the provided code in bitcoinctl.py (src). But in order for that to work, you have to proceed through this homework in the order written.
  8. If you want to put the number 2 onto the stack, you can’t just use the integer value 2. Instead, you have to use the OP_2 opcode. In fact, OP_2 happens to have integer value 82, and the integer value 2 has a different meaning.

Common Errors

We will add to this list as more errors (and their solutions) are reported to us.

Mac OS X issues

If you have a Mac, there are a few issues you should be aware of. It is unclear if these issues still exist – but, if they do, below are some solutions.

Installing OpenSSL via homebrew has caused errors in past semesters. These errors report a problem with the “libeay32” library. Here are some possible solutions that have helped in the past:

We cannot vouch for any of these solutions; we just collected a bunch of Piazza responses from previous semesters.

Testnet

As we do not want to have to buy, and likely lose, real BTC, we are going to use a Bitcoin test network. Because the coins we are going to be using are not “real” Bitcoins, we will use the abbreviation ‘BCY’ (for BlockCypher’s testnet) instead of ‘BTC’. When using a test network, you get coins for free via a faucet – in the same way that a water faucet provides water once turned on, so does a testnet faucet provide free BCY when requested.

  1. Create an account at https://accounts.blockcypher.com/, which will allow you to get an API token. Your token will be a hex number such as 0123456789abcdef0123456789abcdef. Save this token in the blockcypher_api_token field in scripts.py.
  2. You will need to generate a BCY key pair. Run python3 bitcoinctl.py bcy_key – there are four values provided, and you want the “private” and the “address” fields. You are welcome to save the others, but we will not need them in this assignment. While these keys are not valid on the main Bitcoin test network – the have a different value for the version byte in the invoice address – you will need them throughout this assignment.
  3. Only two invoice addresses need funds – the primary one (the first one you generated), and Bob’s; Alice and Charlie’s accounts do not need funds. To generate the funds for your primary account, run python3 bitcoinctl.py fund. We will hold off, for now, on funding Bob’s account.
  4. The faucet transaction paid to the invoice address via only one UTXO, and we would like multiple UTXO indices to use – this way we can use one per question part, and we have a few extra for if and when something ends up not working correctly.

Be careful not to lose the information (keys and TXIDs) that you recorded above. To prevent abuse, faucets typically only allow one request every so often (1 hour to 1 day, depending on the faucet) for a given IP address or BCY address. If you need more during that window, or you are running into ‘exceeded limit’ issues, you can try requesting it for a different invoice address – but then you will have to re-run your previous transactions. If it complains about your IP address being the same, you can try from a different network (home versus on Grounds, etc.).

Whew! The setup for this part is all done! Now onto the scripting part….

Python library

The python-bitcoinlib library for Python handles much of the heavy lifting – conversion from one type to another, encryption, signing, verification, etc. If you were to enter actual keys that have real BTC then you could use this library to make real BTC transactions.

While the library can do many things, below is a quick summary of the relevant aspects that you will need to know for this assignment.

Part 1: P2PKH

The UTXO indices that you created when you split your BCY are paid to a standard P2PKH transaction. Your task is to redeem them by writing the appropriate scripts (sigscript (input) and pubKey script (output)) to redeem the coins from one of the UTXOs. It should be paid back to the designated return address – bitcoinctl.py uses the bcy_dest_address variable, defined at the top of the scripts.py file, as the receiver of this transaction.

To complete this transaction, you need to complete four things:

IMPORTANT NOTE: For the sigScript, the public key used in the P2PKH script must come from the private_key parameter to the P2PKH_scriptSig() (it’s private_key.pub). If you use your global public key, it will work for this part, but it will fail for successive parts of this assignment.

When you have finished the script, you can run it via python3 bitcoinctl.py part1 <utxo>, where <utxo> is the particular UTXO index from the split transaction that you are using to fund this one; it will report an error (likely while crashing) if you get it wrong. The <utxo> field is the integer UTXO index, which is indexed from 0. Some common errors at this point are:

If it works, you will see a JSON dictionary printed to the screen. Record the transaction hash of that transaction in txid_p2pkh in scripts.py. The TXID is the ‘hash’ field in the dictionary that is printed to the screen when run, about half a dozen lines down from the top of the output. You can then run python3 bitcoinctl.py urls to get the URL for the transaction that you just executed. It may take up to 10 minutes for it to be mined into the blockchain.

You should notice your wallet balance has decreased.

Part 2: Puzzle

For this transaction, you are going to create an algebraic puzzle script – one that anybody can redeem as long as they complete the numerical puzzle.

You will first need to pick two 4-digit base-10 numbers (meaning between 1,000 and 10,000 in value) for the values of p and q in the equations below. You can take your UVA SIS ID and split the digits in half. These will be the solutions to the linear equations below. You will likely need to tweak these numbers in a moment. You will need to store those values into puzzle_txn_p and puzzle_txn_q in scripts.py.

The puzzle transaction will deal with the solution to the following two linear equations:

2x + y = p x + 3y = q

You can use an online linear question solver, such as this one, to find the solution. And make sure that the solutions are positive integer values! If not, then tweak one (or both) of your solutions (p and/or q) until you have integer solutions – feel free to modify p and/or q to make the linear equations work. You can also change which of the equations has the higher of p and q to see if that will help as well. Once you know those values, put them into puzzle_txn_p and puzzle_txn_q in scripts.py. You will also want to x and y solutions to these equations into puzzle_txn_x and puzzle_txn_y.

Send the puzzle transaction

For this part, you will create a transaction to redeem one of the split UTXO indices that were created, above. The pubKey (output) script of that newly created transaction will be specified in the puzzle_scriptPubKey() function in scripts.py. Note that because this output script does not depend on the receiver’s public key, that is not provided as a parameter to the function. Also note that the OP_MUL opcode has been disabled on the Bitcoin networks, so you can’t use that. This pubKey script should verify that the two values specified by the redeemer fulfill those two equations. Once this is created, run python3 bitcoinctl.py part2a <utxo> – remember to choose an unspent UTXO index first via the second command line parameter. That <utxo> field is the integer UTXO index from the split transaction, which is indexed from 0. As above, record the transaction hash into the txid_puzzle_txn1 variable.

Redeem the puzzle transaction

You will also need to create the sigScript that redeems this transaction. This should ONLY contain the two values x and y – their order is up to you, as long as it works with the script you created above. That script goes into puzzle_scriptSig().

IMPORTANT: Your your pubKey script should actually do the math to test that the two values (that will be provided in the sigScript) do, in fact, fulfill those linear equations. Just testing for equality to your pre-specified x and y is not the point of this portion of the assignment. This is something we explicitly check for when grading the assignment.

This also does not depend on any signatures, which is why there are no parameters to that function. Ensure that the previous transaction has been mined into the blockchain, which may take up to 10 minutes – if you have entered the previous transaction’s URL into the txid_puzzle_txn1 variable, you can get the URL of that transaction via python3 bitcoinctl.py urls. When ready, you can send the redeeming trasnaction to the BCY network via python3 bitcoinctl.py part2b <utxo> (remember to choose an unspent UTXO index first). The <utxo> field is the integer UTXO index, which is indexed from 0. As this UTXO is from the above transaction, and that transaction only created one UTXO index, the <utxo> value should be 0.

Record the transaction hash into txid_puzzle_txn2.

Notes

WARNING: There are occasionally people who redeeming all of our puzzle transactions (part 2a) on the Bitcoin test network – they are parsing the output script, computing the answers, and redeeming the transaction. Because this script does not have a signature, anybody can redeem it. If you keep getting oddball errors, and you have set your transaction hash and UTXO index correctly, check the transaction page itself to see if it’s already spent. When running part2b it might also report that the UTXO has been spent. For the puzzle transactions, blockcyper.com just says “unknown script type”. If it was redeemed by somebody else, make sure that you (1) have a script (in puzzle_scriptSig()) that could redeem it, and (2) include that redemption transaction hash in txid_puzzle_txn2.

You will notice that the amount in each UTXO index from the split transaction is 0.0001 BCY. For the first half of this puzzle transaction, the amount transacted is slightly less (90% of that, or 0.0009). The difference – 0.00001 BCY – is the transaction fee. Even though this is a test network, and no actual money is involved, your transaction will not be mined into the blockchain unless you have a sufficient transaction fee. For the second half of this, we need to lower the amount even further, so the amount transacted is 90% of 0.00009, or 0.000081; this lowering is done automatically by the code base provided. The difference here – 0.000009 BCY – is the transaction fee. This automatic lowering of the transaction amount will recur elsewhere in this assignment.

Part 3: Multisig

You are going to create a multi-signature transaction, which must use the OP_CHECKMULTISIG opcode (or the OP_CHECKMULTISIGVERIFY opcode).

If you did not create accounts (private keys and invoice addresses) for Alice, Bob, and Charlie – this was in the Testnet section – you should create them now. See above for how to do so.

To set this up, you will need to create three more key pairs using python3 bitcoinctl.py keygen. Save these in the variables for Alice, Bob, and Charlie in part 3 of the scripts.py file. These three addresses don’t need any BCY – we just need the key pairs to perform digital signatures.

The scenario is this: you are taking on the role of a bank. Three siblings (Alice, Bob, and Charlie) have deposited money into an account, and it can be redeemed if two of the three – and also the bank! – agree to it. Formally, the transaction must be signed by the bank (i.e., you – via the keys in the my_private_key_str variable) and any two of the three siblings (via their private keys).

This will actually require two transactions. The first redeems one of the split UTXOs and creates a multi-signature pubKey (output) script. The second redeems that multi-signature script and pays it to the BCY return address.

  1. Transaction 1: BCY funds are taken from one of your split UTXO indices and put into a new UTXO whose output script requires the multiple signatures
  2. Transaction 2: BCY the funds from the multisig UTXO are redeemed and paid back to the BCY return address.

You only have to write the pubKey (output) script of the first transaction, and the sigScript (input) script of the second transaction. The other two parts (sigScript of the first and pubKey script of the second) are taken from your code in part 1 (P2PKH) – so if that is not working, then this will not work either.

The first step is to create the transaction that sets up the multi-signature requirement in the pubKey script. This must use either the OP_CHECKMULTISIG or OP_CHECKMULTISIGVERIFYopcode! See the description of these opcodes in the lecture slides. The task, then, is to fill in the multisig_scriptPubKey() function. Recall that the sigScript will be used from your code for part 1 (P2PKH). We recommend that you write this and the next script – the redeeming script – together, and trace its stack execution (on paper or similar). When you are ready to run it, run python3 bitcoinctl.py part3a <utxo>. The <utxo> field is the integer UTXO index, which is indexed from 0. Once successful, record the transaction hash in the txid_multisig_txn1 variable in scripts.py.

The second step is to create a transaction that will redeem it. You will have to wait until the previous transaction receives at least one confirmation before you can execute this part, which can take up to 10 minutes. This part requires that the txid_multisig_txn1 variable, from the first step above, is set properly, as that is the UTXO that is going to redeem. You will fill in your sigScript into multisig_scriptSig(). Recall that the pubKey script for this transaction will be used from your answer for part 1 (P2PKH). When you are ready to run it, run python3 bitcoinctl.py part3b <utxo>. The <utxo> field is the integer UTXO index, which is indexed from 0; it is likely 0, since there is only one output from the UTXO from the transaction from part 3a. Once successful, record the transaction hash in the txid_multisig_txn2 variable in scripts.py.

IMPORTANT NOTE: For the OP_CHECKMULTISIG (or OP_CHECKMULTISIGVERIFY), it should have ONLY the keys/signatures of Alice, Bob, and Charlie; the bank signature should not be in there. Instead, the bank signature should be separate and verified with an OP_CHECKSIG (or OP_CHECKSIGVERIFY). The reason is that we want the bank’s signature, and 2 of the 3 people; also, if there is no other OP_CHECKSIG, then others can redeem it with an empty set of signatures and keys, which will verify correctly. This is relevant because there is some user on the Bitcoin test network who is trying to redeem your UTXOs. In particular, it has been observed when the UTXO was only verified with a single OP_CHECKMULTISIG.

Part 4: Cross-chain

In this part you will create the scripts for a cross-chain transaction. Typically this would be for two different cryptocurrencies. However, since we have only learned Bitcoin Script, we will use that for both parts. There are many cryptocurrencies that are forks of Bitcoin, and thus have the same scripting language, so the same program could work for any of them. A completely different cryptocurrency, with a different scripting language, would have an analogous scripting language.

Normally, for this part, we use two different Bitcoin testnets. However, the other Bitcoin test networks are no longer usable, so we are going to have to perform all of the cross-chain transactions to the same BCY test network.

There will be three script producing functions in scripts.py, although you only have to create one. The first one, atomicswap_scriptPubKey(), will create the TXN 1 and TXN 3 from the slides; this is the one that you have to create. This script will be used for BOTH of these transactions (with different parameters, of course) on the two different blockchains by the provided code in bitcoinctl.py (src). The second function, atomcswap_scriptSig_redeem(), will be when Alice or Bob knows the secret value and is redeeming the BTC; we provide this function for you in scripts.py. This is used in steps 5 and 6 on cross-chain atomic swap procedure slide. The third function, atomcswap_scriptSig_refund(), will create the time-out redeeming script, which is TXN 2 and TXN 4 from the slides; we also provide this function for you in scripts.py. Again, this will be used on both blockchains by the provided code. There are some requirements for what has to be in these scripts, described below (in “Notes and hints”).

In addition to the lecture slides, you may want to refer to the Atomic swap article in the Bitcoin wiki.

We have four BCY invoice addresses. One is our primary one, which is stored in my_invoice_address_str. The other three were created for part 3 (multi-sig): one each for Alice, Bob, and Charlie. We do not want to request more BCY than is needed, so we are going to use two of these accounts for the cross chain atomic swap. The first is our primary address – that is Alice for this part (which means the alice_invoice_address_str from part 3 is NOT Alice for this part).

The other account is Bob’s account. We will need to fund his account and split that UTXO, just like we did with our primary account at the beginning of this assignment.

In this part, you (Alice) and Bob will be exchanging coins through a cross-chain transaction. You will need to be familiar with the cross-chain transaction section of the Bitcoin slide set. You are going to take on the role of Alice in the lecture slides.

As an overview, this is what is going to happen.

  1. You (Alice) are going to create a transaction to send BCY to Bob. You will send it from the account you have been using so far (saved in my_private_key_str and my_invoice_address_str in scripts.py). Bob will receive it in the account that was created for him above (bob_private_key_str and bob_invoice_address_str in scripts.py). This corresponds to part 1 of the cross-chain transaction – again, you are taking on the role of Alice. You will only be creating TXN1 from that slide; we are omitting TXN2.
  2. Bob will create a transaction to send BCY to you. Both you and Bob will need to create invoice addresses and public keys for the BCY testnet, which we guide you through below. This corresponds to part 2 of the cross-chain transaction – again, you are taking on the role of Alice. You will only be creating TXN3 from that slide; we are omitting TXN4.
  3. You (Alice) will redeem TXN3 on the BCY network, exposing the hidden secret.
  4. Bob, now knowing the hidden secret, will then redeem TXN1 on the BCY network.

Note that you are only creating one function, called atomicswap_scriptPubKey(). This is going to be used for both of the steps 1 (where you (Alice) send BCY to Bob) and 2 (where Bob send BCY to you (Alice), above.

Cross-chain atomic swap

Because we are swapping between two different Bitcoin test networks, the atomic swap code is the same – both are in Bitcoin script. TXN 1 (from here in the slides) and TXN 3 (from here in the slides) differ only by the public keys:

Your script code for this will go into the atomicswap_scriptPubKey() function.

NOTE: the hash that is used in this part is RIPEMD-160, not SHA-256. So be sure to use the OP_HASH160 opcode to get the hash, and not the OP_SHA256 opcode.

To help you with this code, we provide the two redeeming functions:

The last thing to do before you write the atomicswap_scriptPubKey() function is to determine what the secret is – pick a number between 17 million and 2 billion, and save that in atomic_swap_secret. (It needs to be in that range to ensure it’s encoded as a 4-byte integer). Keep in mind that this secret is only known to Alice initially; Bob is only given the hash of the secret (the bitcoinctl.py file handles that part for you).

Once you have written the script in the atomicswap_scriptPubKey() function, you can perform your cross-chain transaction. This involves four steps, which are outlined below. After you perform each step, enter the transaction hash into the variable as specified, and then get the URL via python3 bitcoinctl.py urls. You can check that URL to ensure that it works properly. As with the previous transactions, you have to wait up to 10 minutes for at least one confirmation before you can redeem that UTXO.

  1. You (Alice) transmits TXN 1 to the BCY network, which was created by the atomicswap_scriptPubKey() function. Be sure to set the correct UTXO before running this part! This is sending BCY, so the UTXO index is from the very first split of BCY. This is run via python3 bitcoinctl.py part4a <utxo>. The <utxo> field is the integer UTXO index, which is indexed from 0. Save the transaction hash for this in the txid_atomicswap_alice_send variable.
  2. Bob transmits TXN 3 to the BCY network, which was also created by the atomicswap_scriptPubKey() function. Be sure to set the correct UTXO before running this part! This is sending BCY, so it’s the split that was done earlier in this section. This is run via python3 bitcoinctl.py part4b <utxo>. The <utxo> field is the integer UTXO index, which is indexed from 0. Save the transaction hash for this in the txid_atomicswap_bob_send variable. If you get an error such as “Error validating transaction: Sum of inputs 1007 lesser than outputs 9000” or Your split_amount_to_split is less than or equal to split_amount_after_split”, you need to restore the original BCY values for split_amount_to_split and split_amount_after_split (see step 7 in the cross-chain setup, above).
  3. Alice (you) can redeem TXN 3 on the BCY network, which reveals the secret. Be sure to set the correct UTXO before running this part! This is run via python3 bitcoinctl.py part4c <utxo>. The <utxo> field is the integer UTXO index, which is indexed from 0. As this is from TXN 2 above, the UTXO index is probably 0. Save the transaction hash into txid_atomicswap_alice_redeem.
  4. Bob can new redeem TXN 1 on the BCY network, since he knows the secret which Alice just revealed via her redemption above. Be sure to set the correct UTXO before running this part! As this is from TXN 1 above, the UTXO index is probably 0. This is run via python3 bitcoinctl.py part4d <utxo>. The <utxo> field is the integer UTXO index, which is indexed from 0. Save the transaction hash into txid_atomicswap_bob_redeem.

Hints and notes

Part 5: Return BCY

Once you have completed this assignment, you should pay any unspent BCY UTXOs back to the BCY return address, which is in the the bcy_dest_address variable in scripts.py. You can use the script from part 1 (P2PKH) for this, as it already pays the BCY return address – just change the UTXO value and re-run it; repeat until all the UTXO indices from the BCY split transaction are spent. If you have any other inputs – perhaps you used the faucet multiple times – just change the txid_split variable (and the UTXO and the send_amount), and then call python3 bitcoinctl.py part1 <utxo>. The <utxo> field is the integer UTXO index, which is indexed from 0. But be sure to change those values back!!!

You do not need to save the hashes from these transactions – we are going to verify it by checking the wallet’s balance.

When done, there should not be any unspent UTXOs remaining! We are going to test this by seeing if the amount of BCY left in your wallet address is zero.

You should do this from your main account (which is set up to do this via the P2PKH from part 1). You do not need to do this from Bob’s account.

Submission

NOTE: Make sure all the transactions are mined into the blockchain BEFORE you submit them. If you go to the URL for that particular transaction, as long as it has at least one confirmation, it is considered mined into the blockchain.

The only file you need to submit to Gradescope is scripts.py. There will be a few sanity checks made when you submit it. Those checks are:

Rate Limiter: The various aspects of this program are verified by checking via blockcyper.com’s API to obtain the wallet, transaction, balance, etc. As with most websites, there is a rate limiter, and if there are too many requests in too little a time period, then it will block requests to that IP address for some period. If everybody submits the assignment around the same time, this rate limiter will kick in, and the auto-grader will reports lots of errors. We will re-run the auto-grader at a later point to ensure that it is evaluated properly, but you will not see useful results when you submit your assignment. We have set up a proxy to help this issue (it caches previously made requests). However, if there are still too many requests, it will still run into the rate limiter. Unfortunately, there is nothing more we can do about this.

Autograder notes: As we know, a transaction is not considered valid until it is mined into the blockchain. It may be that your transaction has not yet been mined, which means it will report as not having happened. This means some of the visible tests when you submit your assignment could fail. As long as you submitted the transaction, you do not need to worry about it – we will re-run the auto-grader a day or two later to catch all these cases.