-
Notifications
You must be signed in to change notification settings - Fork 113
Taproot Library Introduction
For the purposes of demonstrating the features of Schnorr and Taproot to the Bitcoin developer community, we have developed an extended Python library on top of Pieter Wuille's Taproot Bitcoin Core branch, which provides Python classes and methods to build more sophisticated Taproot transactions and various Schnorr signature schemes for preliminary evaluation.
Our Taproot/Schnorr library is an extension of the Bitcoin python test framework, located in the dedicated Optech Bitcoin Taproot Branch. Please clone and build this Bitcoin Core branch with taproot support before continuing.
The basic toolset introduced in this section is used repeatedly throughout the materials in this repository to introduce the user to more advanced Schnorr and Taproot examples and use-cases.
Note: This Library is intended for demonstrative and educational purposes only.
An easy way to import the desired library modules from the Bitcoin repository to an external python project is to simply add the full path of the bitcoin functional test directory to your path variable. Remember this should be the path to the Optech Bitcoin Taproot Branch.
import sys
sys.path.insert(0, "/full-path-to-bitcoin-with-taproot/bitcoin/test/functional")
We can now import classes from the Bitcoin Core Test framework and use them in our local Python project.
from test_framework.key import ECKey, ECPubkey
private_key = ECKey()
private_key.generate()
public_key = private_key.get_pubkey()
print(private_key.get_bytes().hex())
print(public_key.get_bytes().hex())
# '9479b1b2e6e811c0e834a4772d54e1c97c9283d1867bd9766a918b26a192594d'
# '02325b36ca3bab32f1e79f4ea3a4d6ea92d6c89044d54b487e787fd59adb2d1e97'
There are many more useful Python classes in Bitcoin Core of course, some of which will be introduced below. Feel free to navigate through the bitcoin/test/functional/test_framework
folder when constructing and signing Bitcoin transactions. However, it can also be helpful to fund and test signed transactions against a Bitcoind node or utilize the Bitcoin Core wallet. These are conveniently accessible from the Test Wrapper object described next.
We have added a convenience class called the TestWrapper to the library. The TestWrapper essentially manages rpc and bitcoind processes for you during its lifetime, enabling access to rpc methods of the underlying nodes without having to manually (multiple) daemon processes and respective data directories. The TestWrapper inherits its functionality from the Bitcoin Core functional test framework, which is used to run integration tests for Bitcoin Core. Calling the setup()
method initiates the daemon processes, temporary data and logging directories.
from test_framework.test_wrapper import TestWrapper
test = TestWrapper()
test.setup()
# TestFramework (INFO): Initializing test directory /var/folders/.../bitcoin_func_test...
Note that the TestWrapper can be initialized with many of the same arguments as the Bitcoin test framework. See here for more TestWrapper settings. Once the setup method returns, we can access the rpc methods of all bitcoind nodes managed by the TestWrapper object. This includes validation, wallet and mining functionality.
test.nodes[0].getmempoolinfo()
# {'size': 0, 'bytes': 0, 'usage': 0, 'maxmempool': 300000000, 'mempoolminfee': Decimal('0.00001000'), 'minrelaytxfee': Decimal('0.00001000')}
Shutdown or destruction of the TestWrapper object will terminate all underlying bitcoind processes and delete temporary folders.
test.shutdown()
# TestFramework (INFO): Stopping nodes
# TestFramework (INFO): Cleaning up /var/folders/.../bitcoin_func_test... on exit
As an introductory example to the library, we will spend a Segwit v1 output with a Schnorr signature. Both Taproot and Schnorr are proposed to be available to version 1 Segwit programs, but we will refer to this as a Schnorr transaction here, as we are not committing a taproot to the output public key. To the on-chain observer, however, this output will be indistinguishable from another version 1 Segwit output with a tweaked public key.
We begin by generating coins which we can send to our Segwit V1 output. This is accomplished by generating several blocks and directing the reward coins to the Bitcoin Core wallet.
test = TestWrapper()
test.setup()
test.nodes[0].generate(101)
balance = test.nodes[0].getbalance()
print('Balance:', bal)
# TestFramework (INFO): Initializing test directory /var/folders/.../bitcoin_func_test...
# Balance: 50.00000000
There are now 50 Bitcoins available in the wallet of test.nodes[0]
, which we can send to any address with the test.nodes[0].sendtoaddress()
/.sendmany()
rpc methods.
In order to construct a Segwit V1 output, we must first generate a set of key pairs.
from test_framework.key import ECKey, ECPubKey
sec = ECKey()
sec.generate()
pubkey = sec.get_pubkey()
The Segwit V1 output which features both taproot & schnorr has the following script pattern:
-
[01]
[00/01 + Pubkey_x_coordinate(32B)]
The witness program is simply a 33B public key, with the public key y-coordinate oddness being represented by the least significant bit of the first byte (The other bits are reserved for future versions). We use a convenience method to derive a Segwit address which encodes our output.
from test_framework.address import program_to_witness
# [02/03][Pubkey_x_coordinate]
pubkey_data = pubkey.get_bytes()
# [01][00/01 + Pubkey_x_coordinate]
pubkey_data_v1 = bytes([pubkey_data[0] & 1]) + pubkey_data[1:]
segwit_address = program_to_witness(1, pubkey_data_v1)
print('Segwit address:', segwit_address)
# Segwit address: bcrt1pqyxknmvgmf6tc6dnukkcqfukzrua43mhxqxvxnuduzw3qsuekqgnwtym7up
Let us now send wallet funds generated from the mined blocks to this address with the sendtoaddress
rpc command. Afterwards, we will reconstruct the transaction signed and broadcast by the Bitcoin Core wallet locally, so it we can conveniently refer to its output(s) when we construct the spending transaction.
Reconstructing the wallet transaction is down by requesting the raw wallet transaction hex from the bitcoin node, and deserializing the transaction hex locally into a CTransaction
object.
from test_framework.messages import CTransaction
from io import BytesIO
txid = test.nodes[0].sendtoaddress(segwit_address, balance / 100000)
tx_hex = test.nodes[0].getrawtransaction(txid)
tx = CTransaction()
# Deserialize method takes a byte stream.
tx.deserialize(BytesIO(bytes.fromhex(tx_hex)))
tx.rehash()
# Check if the transaction was correctly deserialized.
assert(txid == tx.sha256.to_bytes(32,'big').hex())
Let us now spend our Segwit output back to the Bitcoin Core wallet with a Schnorr signature. There is one more minor step, however, to determine which of the outputs from the funding transaction actually contain the output we want to spend manually. Consider that the wallet must always construct at least one change output for each transaction, and will set the change output index randomly to preserve coin privacy. We must therefore always check when output index has been assigned to our Segwit V1 output.
from test_framework.script import CScript, OP_1
index = 0
outputs = tx.vout
output = outputs[index]
while (output.scriptPubKey != CScript([OP_1, pubkey_data_v1])):
index += 1
output = outputs[index]
output_value = output.nValue
We then build the transaction which sends the amount in this output back to the wallet.
from test_framework.messages import COutPoint, CTxIn, CTxOut
# Construct Schnorr Transaction.
tx_schnorr = CTransaction()
tx_schnorr.nVersion = 1
tx_schnorr.nLockTime = 0
outpoint = COutPoint(tx.sha256, index)
tx_schnorr_in = CTxIn(outpoint = outpoint)
tx_schnorr.vin = [tx_schnorr_in]
# Generate new Bitcoin Core wallet address.
dest_addr = test.nodes[0].getnewaddress(address_type="bech32")
scriptpubkey = bytes.fromhex(test.nodes[0].getaddressinfo(dest_addr)['scriptPubKey'])
# Determine minimum fee required for mempool acceptance.
min_fee = int(test.nodes[0].getmempoolinfo()['mempoolminfee'] * 100000000)
# Complete output which returns funds to Bitcoin Core wallet.
dest_output= CTxOut(nValue=output_value-min_fee, scriptPubKey=scriptpubkey)
tx_schnorr.vout = [dest_output]
Finally, we sign the transaction and construct the witness, which is simply a stack with a single Schnorr signature [sig]
.
from test_framework.script import TaprootSignatureHash
from test_framework.messages import CScriptWitness, CTxInWitness
# Sign transaction with Schnorr.
hash_types = [0,1,2,3,0x81,0x82,0x83]
sighash = TaprootSignatureHash(tx_schnorr, [output], hash_types[0])
# Note: Any non-sighash-ALL Schnorr signatures requires
# the hash_type appended to the end of signature.
sig = sec.sign_schnorr(sighash)
# Construct transaction witness.
witness = CScriptWitness()
witness.stack.append(sig)
witness_in = CTxInWitness()
witness_in.scriptWitness = witness
tx_schnorr.wit.vtxinwit.append(witness_in)
# Serialize Schnorr transaction for broadcast.
tx_schnorr_str = tx_schnorr.serialize().hex()
# Test mempool acceptance.
print(test.nodes[0].testmempoolaccept([tx_schnorr_str]))
# [{'txid': '6b6087367d5cdb045576dd33d982f043bc1b00b7f99491ae65b669f52fdcf13c', 'allowed': True}]
Congratulations, you have now signed and broadcast a Schnorr Transaction in Bitcoin! Note how we have used the Python Libraries to construct and sign transactions, and then queried the (Schnorr & Taproot enabled) Bitcoin Core daemon via the TestWrapper object to mine coins and send these to user-defined outputs.
The remainder of the wiki documentation and Optech Jupyter exercises in this repository will use these Python basic tools repeatedly to introduce you to more advanced Schnorr and Taproot features.