I have been working in a bitcoin library for educational purposes (being taught in courses/meetups/etc.). I had quite some time to work on it and decided it was about time to support taproot.
I have successfully spend taproot UTXOs created by bitcoin core as well as created p2tr UTXOs that I could then spend. Key path spending seems to be working as expected.
I am having issues (creating/) spending from script path.
I am not sure if it is an implementation issue or just not following the specs properly but I was hoping that fresh (and more experienced) eyes will identify the issue.
I use a single tap leaf script for now.
I will post the critical code snippets below but the whole branch can be found here.
I am getting: "reject-reason": "non-mandatory-script-verify-flag (Witness program hash mismatch)"
The raw transaction is the following:
version: 02000000
segwith marker/flag 0001
inputs 01
txid b193e4af59323b28ed8a37432bf65e0418cbcdc310d8eb733b9b50e27a578f34
vout 00000000
scriptSig 00
nSequence ffffffff
outputs 01
amount 3421000000000000
scriptPubKey 225120d4213cd57207f22a9e905302007b99b84491534729bd5f4065bdcb42ed10fcd5
witness stack items 03
signature 400efe4d084bf03c3aa72d856a6aa17078eb002d6330a53230b4ac832cac8fd5b6fd49d95854afffeaeec2f2163c32ba8129383ae367f54a2b32dfbae042569ccd
script 22205d238354a7e74c9e373317053226537dec221c5c775bcca01e806ec358c5c08dac
control block size 41
version c0
internal pubkey 1036a7ed8d24eac9057e114f22342ebf20c16d37f0d25cfd2c900bf401ec09c9
tapleaf_hash 7f57a76fa4006c7f31e695ad977dfe9f8b736e2f82249a7c9c1fc7072544ca29
locktime 00000000
When I had issues with properly tweaking the keys I was getting 'Invalid schnorr signature' so I assume that the above issue has to do with the control block I use..?
The code of the spending script that produces the error is as follows:
from binascii import hexlify
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis, ControlBlock
from bitcoinutils.script import Script
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
from bitcoinutils.keys import P2pkhAddress, PrivateKey
from bitcoinutils.hdwallet import HDWallet
def main():
# always remember to setup the network
setup('testnet')
# Keys are hard-coded in the example for simplicity but it is very bad
# practice. Normally you would acquire them from env variables, db, etc
#######################
# Construct the input #
#######################
# INTERNAL PRIVKEY of UTXO
# get an HDWallet wrapper object by extended private key and path
xprivkey = "tprv8ZgxMBicQKsPdQR9RuHpGGxSnNq8Jr3X4WnT6Nf2eq7FajuXyBep5KWYpYEixxx5XdTm1Ntpe84f3cVcF7mZZ7mPkntaFXLGJD2tS7YJkWU"
path = "m/86'/1'/0'/0/7"
hdw = HDWallet(xprivkey, path)
priv1 = hdw.get_private_key()
pub1 = priv1.get_public_key()
# taproot script is a simple P2PK with the following keys
# TAPLEAF script (P2PK)
privkey_tr_script = PrivateKey('cQwzrJyTNWbEwhPEmQ3Qoo4jSfHdHEtdbL4kNBgHUKhirgzcQw7G')
pubkey_tr_script = privkey_tr_script.get_public_key()
tr_script_p2pk = Script([pubkey_tr_script.to_x_only_hex(), 'OP_CHECKSIG'])
# taproot script path address
# note that .get_taproot_address(script) negates if necessary, then tweaks
# and then negates again if necessary (I have tried w/o 2nd negation as well)
fromAddress = pub1.get_taproot_address(tr_script_p2pk)
print('From Taproot script address', fromAddress.to_string())
# UTXO of fromAddress
txid1 = '348f577ae2509b3b73ebd810c3cdcb18045ef62b43378aed283b3259afe493b1'
vout1 = 0
# create transaction input from tx id of UTXO
txin1 = TxInput(txid1, vout1)
# all amounts are needed to sign a taproot input
# (depending on sighash)
amount1 = to_satoshis(0.00009)
amounts = [ amount1 ]
# all scriptPubKeys (in hex) are needed to sign a taproot input
# (depending on sighash but always of the spend input)
scriptPubkey1 = fromAddress.to_script_pub_key()
utxos_scriptPubkeys = [ scriptPubkey1 ]
########################
# Construct the output #
########################
hdw.from_path("m/86'/1'/0'/0/5")
priv2 = hdw.get_private_key()
print('To Private key:', priv2.to_wif())
pub2 = priv2.get_public_key()
print('To Public key:', pub2.to_hex())
# taproot key path address
toAddress = pub2.get_taproot_address()
print('To Taproot address:', toAddress.to_string())
# create transaction output
txOut = TxOutput(to_satoshis(0.000085), toAddress.to_script_pub_key())
# create transaction without change output - if at least a single input is
# segwit we need to set has_segwit=True
tx = Transaction([txin1], [txOut], has_segwit=True)
print("\nRaw transaction:\n" + tx.serialize())
print('\ntxid: ' + tx.get_txid())
print('\ntxwid: ' + tx.get_wtxid())
# sign taproot input
# to create the digest message to sign in taproot we need to
# pass all the utxos' scriptPubKeys, their amounts and taproot script
# tweak=False means that the key should not be tweaked, but it is still negated
sig1 = privkey_tr_script.sign_taproot_input(tx, 0, utxos_scriptPubkeys, amounts, script_path=True, script=tr_script_p2pk, tweak=False)
control_block = ControlBlock(pub1, [ tr_script_p2pk ])
tx.witnesses.append( TxWitnessInput([ sig1, tr_script_p2pk.to_hex(), control_block.to_hex() ]) )
# print raw signed transaction ready to be broadcasted
print("\nRaw signed transaction:\n" + tx.serialize())
# sendrawtransaction is used to send it to a node
if __name__ == "__main__":
main()
The key path can be spend normally with another script.
The code for the ControlBlock is the following:
class ControlBlock:
'''Represents a control block for spending a taproot script path'''
def __init__(self, pubkey, scripts):
self.pubkey = pubkey
self.scripts = scripts
def to_bytes(self):
# leaf version is fixed but we check if the public key required negation
# if negated (y is odd) add one to the leaf_version
#if int(self.pubkey.to_hex()[-2:], 16) % 2 == 0:
# leaf_version = bytes([LEAF_VERSION_TAPSCRIPT])
#else:
# leaf_version = bytes([LEAF_VERSION_TAPSCRIPT + 1])
leaf_version = bytes([LEAF_VERSION_TAPSCRIPT])
# x-only public key is required
pub_key = bytes.fromhex( self.pubkey.to_x_only_hex() )
# TODO only single alternative script path for now
script_bytes = self.scripts[0].to_bytes()
# tag hash the script
th = tagged_hash(bytes([LEAF_VERSION_TAPSCRIPT]) + prepend_varint(script_bytes),
"TapLeaf").digest()
return leaf_version + pub_key + th
def to_hex(self):
"""Converts object to hexadecimal string"""
return hexlify(self.to_bytes()).decode('utf-8')
The message digest created includes the script that we are spending with ext_flag=1:
...
# Data about this input
spend_type = ext_flag * 2 + 0 # 0 for hard-coded - no annex_present
...
if ext_flag == 1: # script spending path (Signature Message Extension BIP-342)
# committing the tapleaf hash - makes it safe to reuse keys for separate
# scripts in the same output
leaf_ver = LEAF_VERSION_TAPSCRIPT # pass as a parameter if a new version comes
tx_for_signing += tagged_hash(bytes([leaf_ver]) + prepend_varint(script.to_bytes()),
"TapLeaf").digest()
# key version - type of public key used for this signature, currently only 0
tx_for_signing += bytes([0])
# code separator position - records position of when the last OP_CODESEPARATOR
# was executed; not supported for now, we always use 0xffffffff
tx_for_signing += b'\xff\xff\xff\xff'
Tweaking seems to be working fine since I have no issues with the key spending path, which is why I don't include the code for tweaking the keys but everything is here for the brave hearted.
Any help will be greatly appreciated!