3

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!

2
  • If you only have a single script in your script tree, the Merkle path should be length zero, and the control block should be 33 bytes. Jun 29, 2023 at 13:20
  • Thank you Pieter. In hindsight obvious since you provide the path in order to construct the root... quite some hours of frustration. I wish I had asked sooner! :-) Please write your comment in a reply to accept it.
    – karask
    Jun 29, 2023 at 13:40

1 Answer 1

2

Since you only have a single script in the tree, the Merkle path would be empty, and the control block would be 33 bytes long.

In general, the Merkle path elements in the control block are the other hashes to hash together with. For a balanced tree with 2n elements you need n such hashes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.