5

I have been documenting how to create different types of Tx outputs here, leveraging the Bitcoin Core Test Framework. Particularly for Taproot, I'm working on a scenario of a P2TR Script Path with a tree of PK scripts like this:

                   taptweak(Internal P|ABC)
                             |
                             |
                      TH("TapBranch") ABC
                             |
                             |
                   ----------------------
                  |                      |
           TH("TapBranch") AB.    TH("TapLeaf") C    
                  |
           ---------------
          |              |
   TH("TapLeaf) A TH("TapLeaf) B

Reviewing the Bips 340, 341, and 342, as well as the Opthech Taproot Worksop videos, documentation, and code, it is unclear how to construct the code for the witness and the controlblock, when a "Script Inclusion Proof" has to be provided.

Bip-341 states:

To spend this output using script D, the control block would contain the following data in this order: <control byte with leaf version and parity bit> <internal key p> <C> <E> <AB>

In my example I'm spending script_B, I assume I have to provide the Tag Hashes for script_A and script_C in the controlblock as inclusion proof, also, It is not clear if a compact size of the whole controlblock is needed, but I did provide it using the ser_string() function. I have tried to implement it in Python (see code below), but I'm getting this error: 'non-mandatory-script-verify-flag (Invalid Taproot control block size)'

Can anyone suggest what I might be missing or misinterpreting?

Here is how I'm constructing the witness and the controlblock:

        control_block = [TAPSCRIPT_VERSION, internal_pubkey, TH_Leaf_A, TH_Leaf_C]
        control_block_bstr = ser_string(b''.join(element for element in control_block)
        witness_elements = [signature, script_A, control_block_bstr ]

        # Add witness elements, script and control block 
        tx2.wit.vtxinwit.append(CTxInWitness())
        tx2.wit.vtxinwit[0].scriptWitness.stack = witness_elements

and here is my full code:

from io import BytesIO
from test_framework.address import program_to_witness
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.messages import (
                                     CTransaction,
                                     COutPoint,
                                     CTxIn,
                                     CTxOut,
                                     CTxInWitness,
                                     ser_string
                             )
from test_framework.script import (
                                   CScript,
                                   SIGHASH_DEFAULT,
                                   TaprootSignatureHash,
                                   OP_CHECKSIG,
                                   OP_1
                            )

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from test_framework.key import ( ECKey, 
                               compute_xonly_pubkey, 
                               sign_schnorr,
                               verify_schnorr,
                               TaggedHash,
                               tweak_add_pubkey
                        )
TAPSCRIPT_VERSION = bytes([0xc0])

# Facilitate key pair generation
def generate_bip340_key_pair():
    # Key pair generation
    privkey = ECKey()
    privkey.generate()
    # Compute x only pubKey, Bip340 32 bytes Public Key
    pubkey, negated = compute_xonly_pubkey(privkey.get_bytes())
    assert_equal(len(pubkey), 32)

    return privkey.get_bytes(), pubkey

# Create TapBranch sorting lexicographically
def tapbranch_hash(left, right):
    return TaggedHash("TapBranch", b''.join(sorted([left, right])))

class P2TR_Script_Path(BitcoinTestFramework):

    def set_test_params(self):
        """This method has to be overwritten to specify test parameters"""
        self.setup_clean_chain = True
        self.num_nodes = 1
        self.extra_args = [[]]

    def skip_test_if_missing_module(self):
        """
           Note: this function, besides to skip the test if no wallet was compiled, creates 
           a default wallet.
           NOTE: if you remove it, you HAVE to create the wallet, otherwise RPCs calls will fail
        """
        self.skip_if_no_wallet()


    def run_test(self):
        """Main test logic"""

        self.log.info("Start test!")
        self.log.info("Generating some Blocks to create UTXOs")        
        self.generate(self.nodes[0], COINBASE_MATURITY + 1)
        
        # After generating 101 blocks there in a UTXO for 50BTC
        utxos = self.nodes[0].listunspent()
        assert len(utxos) == 1
        assert_equal(utxos[-1]["amount"], 50) 

        # Create input to spend from UTXO
        unspent_txid = self.nodes[0].listunspent()[-1]["txid"]
        input = [{"txid": unspent_txid, "vout": 0}]
        self.log.info("Selected UTXO as input: {}".format(input))        

        # Generate key pairs
        internal_privkey, internal_pubkey = generate_bip340_key_pair()

        privkey_A, pubkey_A = generate_bip340_key_pair()
        privkey_B, pubkey_B = generate_bip340_key_pair()
        privkey_C, pubkey_C = generate_bip340_key_pair()

        # create PK scripts
        script_A = CScript([pubkey_A, OP_CHECKSIG])
        script_B = CScript([pubkey_B, OP_CHECKSIG])
        script_C = CScript([pubkey_C, OP_CHECKSIG])

        # Hash TapLeaves with version, length and script (ser_string appends compac size length)
        hash_A = TAPSCRIPT_VERSION + ser_string(script_A)
        hash_B = TAPSCRIPT_VERSION + ser_string(script_B)
        hash_C = TAPSCRIPT_VERSION + ser_string(script_C)
        TH_Leaf_A = TaggedHash("TapLeaf", hash_A)
        TH_Leaf_B = TaggedHash("TapLeaf", hash_B)
        TH_Leaf_C = TaggedHash("TapLeaf", hash_C)

        # Compute branches
        branch_AB = tapbranch_hash(TH_Leaf_A, TH_Leaf_B)
        branch_ABC = tapbranch_hash(branch_AB, TH_Leaf_C)
        
        # Compute TapTweak
        tap_tweak = TaggedHash("TapTweak", internal_pubkey + branch_ABC)
        self.log.info("TapTweak: {}".format(tap_tweak.hex()))

        # Derive bech32m address
        taproot_PK_bytes, negated = tweak_add_pubkey(internal_pubkey, tap_tweak)
        bech32m_address = program_to_witness(1, taproot_PK_bytes)
        self.log.info("Address (bech32m): {}".format(bech32m_address))

        # Create Tx1 using the tweaked public key
        tx1_amount = 1
        tx1_hex = self.nodes[0].createrawtransaction(inputs=input, outputs=[{bech32m_address: tx1_amount}])
        res = self.nodes[0].signrawtransactionwithwallet(hexstring=tx1_hex)
        self.log.debug("Tx1 result: {}".format(res))

        tx1_hex = res["hex"]
        assert res["complete"]
        assert 'errors' not in res

        # Send the raw transaction. We haven't created a change output,
        # so maxfeerate must be set to 0 to allow any fee rate.
        tx1_id = self.nodes[0].sendrawtransaction(hexstring=tx1_hex, maxfeerate=0)
        decrawtx = self.nodes[0].decoderawtransaction(tx1_hex, True)
        self.log.debug("Tx1 decoded: {}".format(decrawtx))


        # Reconstruct transaction from hex 
        tx1 = CTransaction()
        tx1.deserialize(BytesIO(bytes.fromhex(tx1_hex)))
        tx1.rehash()

        # Assert the output we created is a P2TR witness_v1_taproot
        assert_equal(decrawtx['vout'][0]['scriptPubKey']['type'], 'witness_v1_taproot')
        self.log.info("Transaction {}, output 0".format(tx1_id))       
        self.log.info("sent to {}".format(bech32m_address))       
        self.log.info("Amount {}".format(decrawtx['vout'][0]['value']))       


        # Generate a P2TR scriptPubKey 01(segwit v1) 20(32 bytes in hex) <pubkey>
        script_pubkey = CScript([OP_1, internal_pubkey])

        # Manually assemble the Tx2, using Tx1 P2TR output as input.
        tx2 = CTransaction()
        tx2.nVersion = 2
        tx2.nLockTime = 0
        outpoint = COutPoint(int(tx1_id,16), 0)
        # No scriptSig, the signature will be on the witness stack
        tx2.vin.append(CTxIn(outpoint, b""))
        # scriptPubKey is witness v1: 0 and 32 byte public key
        dest_output = CTxOut(nValue=((tx1.vout[0].nValue)- 1000), scriptPubKey=script_pubkey)
        tx2.vout.append(dest_output)

        # Generate the taproot signature hash for signing
        # SIGHASH_ALL_TAPROOT is 0x00
        sighash = TaprootSignatureHash(  tx2, 
                                                [tx1.vout[0]], 
                                                SIGHASH_DEFAULT, 
                                                input_index = 0, 
                                                scriptpath = True,
                                                script = script_B 
                                             )
         
        # All schnorr sighashes except SIGHASH_DEFAULT require
        # the hash_type appended to the end of signature
        signature = sign_schnorr(privkey_B, sighash)

        witness_elements = [signature, script_A, TAPSCRIPT_VERSION, internal_pubkey, TH_Leaf_A, TH_Leaf_C]

        # Add witness elements, script and control block 
        tx2.wit.vtxinwit.append(CTxInWitness())
        tx2.wit.vtxinwit[0].scriptWitness.stack = witness_elements
        tx2.rehash()

        tx2_hex = tx2.serialize().hex()
        decrawtx = self.nodes[0].decoderawtransaction(tx2_hex, True)
        descriptor = decrawtx['vout'][0]['scriptPubKey']['desc']
        assert self.nodes[0].testmempoolaccept(rawtxs=[tx2_hex], maxfeerate=0)[0]['allowed']
        tx2_id = self.nodes[0].sendrawtransaction(hexstring=tx2_hex)
        address = decrawtx['vout'][0]['scriptPubKey']['address']
        self.log.info("P2TR Script Path Transaction {}".format(tx2_id))       
        self.log.info("sent to {}".format(address))       
        self.log.info("Descriptor {}".format(descriptor))       

if __name__ == '__main__':
    P2TR_Script_Path().main()

This is the raw tx decomposed.

version:            02000000 
marker and flag:    0001
number of inputs:   01
  txid:             2dc0a18c1b9bd09070380bc3a4c5cc4dda81649462397aa529b7234194a924c5
  vout:             00000000
  scriptsig:        00
  sequence:         00000000
number of outputs:  01
  amount:           18ddf50500000000
  scriptpubkey:     225120e661a607f63da586616de255ecb27ce2b835b5c266015b1c04b9c57a83697316
items witness stack:06
  signature:        40 9a5e6dbfc92135a6ec18c1c87b401e58eb38da51f79207002461961decf6782f8bdceffd3b751eae9bb3e0877190790def5bd9076aabac6ae3bc14a1ef0adac4 
  script:           22 20814c44e4784059f451e40f8d8da0dd3b79ed11f8788b02f9aa312d7d219012a3ac
Tapscrit version:   01 c0
Internal PK:        20 e661a607f63da586616de255ecb27ce2b835b5c266015b1c04b9c57a83697316
TH_Leaf_A:          20 28ddf0751e454d7c930449733c959a8e0b8bb996930f9333739141c0d43ca593
TH_Leaf_C           20 f48d3a446abe298f73ad7075b6f11a410f5defe6708f41832cd4044c9cf78998
locktime:           00000000

4
  • No need to ser_string your control_block_bstr; the serialization of witness stack items takes care of prefixing the lengths of stack elements. Mar 16, 2023 at 21:43
  • Fixed it, I updated the code in the question, but I still get the same error. 'non-mandatory-script-verify-flag (Invalid Taproot control block size)' I will add the decoded raw tx info to the question. Mar 17, 2023 at 1:34
  • You do need to concatenate the leaf version, internal public key, and taproot script merkle branch elements into a single control block stack item, though. Mar 17, 2023 at 1:34
  • 1
    Thx, it worked! What do we do to close this question? Do you want to post the answer or should I answer it so it does not remain open? Mar 17, 2023 at 3:10

1 Answer 1

2

Based on Pieter Wuille comments "You do need to concatenate the leaf version, internal public key, and taproot script merkle branch elements into a single control block stack item" this the final version.

# concatenate conrtolblock elements
control_block = b''.join([TAPSCRIPT_VERSION, internal_pubkey, TH_Leaf_A, TH_Leaf_C])
witness_elements = [signature, script_B, control_block] 

this is the final code:

from io import BytesIO
from test_framework.address import program_to_witness
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.messages import (
                                     CTransaction,
                                     COutPoint,
                                     CTxIn,
                                     CTxOut,
                                     CTxInWitness,
                                     ser_string
                             )
from test_framework.script import (
                                   CScript,
                                   SIGHASH_DEFAULT,
                                   TaprootSignatureHash,
                                   OP_CHECKSIG,
                                   OP_1
                            )

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from test_framework.key import ( ECKey, 
                               compute_xonly_pubkey, 
                               sign_schnorr,
                               verify_schnorr,
                               TaggedHash,
                               tweak_add_pubkey
                        )
TAPSCRIPT_VERSION = bytes([0xc0])

# Facilitate key pair generation
def generate_bip340_key_pair():
    # Key pair generation
    privkey = ECKey()
    privkey.generate()
    # Compute x only pubKey, Bip340 32 bytes Public Key
    pubkey, negated = compute_xonly_pubkey(privkey.get_bytes())
    assert_equal(len(pubkey), 32)

    return privkey.get_bytes(), pubkey

# Create TapBranch sorting lexicographically
def tapbranch_hash(left, right):
    return TaggedHash("TapBranch", b''.join(sorted([left, right])))

class P2TR_Script_Path(BitcoinTestFramework):

    def set_test_params(self):
        """This method has to be overwritten to specify test parameters"""
        self.setup_clean_chain = True
        self.num_nodes = 1
        self.extra_args = [[]]

    def skip_test_if_missing_module(self):
        """
           Note: this function, besides to skip the test if no wallet was compiled, creates 
           a default wallet.
           NOTE: if you remove it, you HAVE to create the wallet, otherwise RPCs calls will fail
        """
        self.skip_if_no_wallet()


    def run_test(self):
        """Main test logic"""

        self.log.info("Start test!")
        self.log.info("Generating some Blocks to create UTXOs")        
        self.generate(self.nodes[0], COINBASE_MATURITY + 1)
        
        # After generating 101 blocks there in a UTXO for 50BTC
        utxos = self.nodes[0].listunspent()
        assert len(utxos) == 1
        assert_equal(utxos[-1]["amount"], 50) 

        # Create input to spend from UTXO
        unspent_txid = self.nodes[0].listunspent()[-1]["txid"]
        input = [{"txid": unspent_txid, "vout": 0}]
        self.log.info("Selected UTXO as input: {}".format(input))        

        # Generate key pairs
        internal_privkey, internal_pubkey = generate_bip340_key_pair()

        privkey_A, pubkey_A = generate_bip340_key_pair()
        privkey_B, pubkey_B = generate_bip340_key_pair()
        privkey_C, pubkey_C = generate_bip340_key_pair()

        # create PK scripts
        script_A = CScript([pubkey_A, OP_CHECKSIG])
        script_B = CScript([pubkey_B, OP_CHECKSIG])
        script_C = CScript([pubkey_C, OP_CHECKSIG])

        # Hash TapLeaves with version, length and script (ser_string() appends compac size length)
        hash_A = TAPSCRIPT_VERSION + ser_string(script_A)
        hash_B = TAPSCRIPT_VERSION + ser_string(script_B)
        hash_C = TAPSCRIPT_VERSION + ser_string(script_C)
        TH_Leaf_A = TaggedHash("TapLeaf", hash_A)
        TH_Leaf_B = TaggedHash("TapLeaf", hash_B)
        TH_Leaf_C = TaggedHash("TapLeaf", hash_C)

        # Compute branches
        branch_AB = tapbranch_hash(TH_Leaf_A, TH_Leaf_B)
        branch_ABC = tapbranch_hash(branch_AB, TH_Leaf_C)
        
        # Compute TapTweak
        tap_tweak = TaggedHash("TapTweak", internal_pubkey + branch_ABC)
        self.log.info("TapTweak: {}".format(tap_tweak.hex()))

        # Derive bech32m address
        taproot_PK_bytes, negated = tweak_add_pubkey(internal_pubkey, tap_tweak)
        self.log.info("Negated: {}".format(negated))
        bech32m_address = program_to_witness(1, taproot_PK_bytes)
        self.log.info("Address (bech32m): {}".format(bech32m_address))

        # Create Tx1 using the tweaked public key
        tx1_amount = 1
        tx1_hex = self.nodes[0].createrawtransaction(inputs=input, outputs=[{bech32m_address: tx1_amount}])
        res = self.nodes[0].signrawtransactionwithwallet(hexstring=tx1_hex)
        self.log.debug("Tx1 result: {}".format(res))

        tx1_hex = res["hex"]
        assert res["complete"]
        assert 'errors' not in res

        # Send the raw transaction. We haven't created a change output,
        # so maxfeerate must be set to 0 to allow any fee rate.
        tx1_id = self.nodes[0].sendrawtransaction(hexstring=tx1_hex, maxfeerate=0)
        decrawtx = self.nodes[0].decoderawtransaction(tx1_hex, True)
        self.log.debug("Tx1 decoded: {}".format(decrawtx))


        # Reconstruct transaction from hex 
        tx1 = CTransaction()
        tx1.deserialize(BytesIO(bytes.fromhex(tx1_hex)))
        tx1.rehash()

        # Assert the output we created is a P2TR witness_v1_taproot
        assert_equal(decrawtx['vout'][0]['scriptPubKey']['type'], 'witness_v1_taproot')
        self.log.info("Transaction {}, output 0".format(tx1_id))       
        self.log.info("sent to {}".format(bech32m_address))       
        self.log.info("Amount {}".format(decrawtx['vout'][0]['value']))       


        # Generate a P2TR scriptPubKey 01(segwit v1) 20(32 bytes in hex) <pubkey>
        script_pubkey = CScript([OP_1, internal_pubkey])

        # Manually assemble the Tx2, using Tx1 P2TR output as input.
        tx2 = CTransaction()
        tx2.nVersion = 2
        tx2.nLockTime = 0
        outpoint = COutPoint(int(tx1_id,16), 0)
        # No scriptSig, the signature will be on the witness stack
        tx2.vin.append(CTxIn(outpoint, b""))
        # scriptPubKey is witness v1: [1 and 32 byte public key]
        dest_output = CTxOut(nValue=((tx1.vout[0].nValue)- 1000), scriptPubKey=script_pubkey)
        tx2.vout.append(dest_output)

        # Generate the taproot signature hash for signing
        # SIGHASH_ALL_TAPROOT is 0x00
        sighash = TaprootSignatureHash(  tx2, 
                                                [tx1.vout[0]], 
                                                SIGHASH_DEFAULT, 
                                                input_index = 0, 
                                                scriptpath = True,
                                                script = script_B 
                                             )
         
        # All schnorr sighashes except SIGHASH_DEFAULT require
        # the hash_type appended to the end of signature
        signature = sign_schnorr(privkey_B, sighash)

        control_block = b''.join([TAPSCRIPT_VERSION, internal_pubkey, TH_Leaf_A, TH_Leaf_C])
        witness_elements = [signature, script_B, control_block] 

        # Add witness elements, script and control block 
        tx2.wit.vtxinwit.append(CTxInWitness())
        tx2.wit.vtxinwit[0].scriptWitness.stack = witness_elements
        tx2.rehash()


        tx2_hex = tx2.serialize().hex()
        decrawtx = self.nodes[0].decoderawtransaction(tx2_hex, True)
        descriptor = decrawtx['vout'][0]['scriptPubKey']['desc']
        assert self.nodes[0].testmempoolaccept(rawtxs=[tx2_hex], maxfeerate=0)[0]['allowed']
        tx2_id = self.nodes[0].sendrawtransaction(hexstring=tx2_hex)
        address = decrawtx['vout'][0]['scriptPubKey']['address']
        self.log.info("P2TR Script Path Transaction {}".format(tx2_id))       
        self.log.info("sent to {}".format(address))       
        self.log.info("Descriptor {}".format(descriptor))       

if __name__ == '__main__':
    P2TR_Script_Path().main()

Other Output Scripts Case Studies can be reviewed here

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.