Skip to content

Instantly share code, notes, and snippets.

@AdamISZ
Created November 22, 2021 19:53
Show Gist options
  • Save AdamISZ/6233d9d9b8d25483dc5d39cc6b9892a7 to your computer and use it in GitHub Desktop.
Save AdamISZ/6233d9d9b8d25483dc5d39cc6b9892a7 to your computer and use it in GitHub Desktop.
Testing script path spending in taproot with python-bitcointx
import bitcointx as btc
btc.allow_secp256k1_experimental_modules()
btc.select_chain_params("bitcoin/testnet")
from bitcointx.wallet import CCoinKey
from bitcointx.core import COutPoint, CTxIn, CTxOut, CMutableTransaction, CTxInWitness
from bitcointx.core.script import (CScript, OP_CHECKSIGADD, OP_CHECKSIG, OP_NUMEQUAL,
TaprootScriptTree, CScriptWitness)
from bitcointx.wallet import P2TRCoinAddress
from binascii import hexlify, unhexlify
# phase 1: create an address for a script of OP_CHECKSIGADD pub1, pub2/OP_CHECKSIGADD pub3, pub4/pub5
# generate 5 different privkeys:
keys = [CCoinKey.from_secret_bytes(bytes([i]*32)) for i in range(1, 6)]
scr1 = CScript([keys[0].xonly_pub, OP_CHECKSIG, keys[1].xonly_pub, OP_CHECKSIGADD, 2, OP_NUMEQUAL], name="multisig1")
scr2 = CScript([keys[2].xonly_pub, OP_CHECKSIG, keys[3].xonly_pub, OP_CHECKSIGADD, 2, OP_NUMEQUAL], name="multisig2")
scr3 = CScript([keys[4].xonly_pub, OP_CHECKSIG], name="single key")
# TaprootScriptTree automatically creates the tree for us, given a list of CScript objects:
tree = TaprootScriptTree([scr1, scr2, scr3])
# set a dummy internal pubkey; note in future we might want to use the provably unspendable form as in BIP341 recommendation:
tree.set_internal_pubkey(CCoinKey.from_secret_bytes(bytes([6]*32)).xonly_pub)
addr = P2TRCoinAddress.from_script_tree(tree)
# (this was run before completing the rest of the script to fund:)
print("The address to fund is: {}".format(addr))
# this transaction funds the above with 0.01 coins:
hextx1 = "02000000000102198b3500bfb5264bf4e782b857d0f0f897e3ca26a35c441b51059fe6b81350f10100000000feffffff905be7600b8c4fd1cac0937f17f3e2f8dfdba2062e413eb4be2a5fbfb8aaa7f20200000000feffffff0240420f000000000022512033efb169849874c88f60edb29cbe6e612c2f84e0fd6e1c5b0737cf69973e01958d82030000000000160014b847d36a181d996499836b5c2d05fad5d6b0111002473044022019f08a15f217eac47fe18862a4996e9e6818e94c0be98f54402f67e9d95ea54202203e8a3864238cad87efe8e547617ad6ee843ebf0d38f9ef1b5bdcfd517f473e180121025da8ce82bc23bba542155aaa2774e65f86a19b01502eb8fc05c72ad3d9e06e690247304402201398d6f9a41ff5c2959ebf6133f4d93aabebbd17af95febdc377b87ca763a196022048624afd49d0e27d435d57a6a73f9c5681d65e19b84f508e6df023d67ebf032c0121023fc390fb03735dfe14d48edec7bf5e6c06868a46941cdb5f88a1b7da9ad0fcdf4eff0000"
tx1id = unhexlify("18e1602c29fc063a24973179d244b1e09443fb396553e99038617084898c0c5c")
outpoint = COutPoint(tx1id[::-1], 0)
vin = [CTxIn(prevout=outpoint, nSequence=0xffffffff)]
sPK = addr.to_scriptPubKey()
vout = [CTxOut(998000, sPK)]
tx2 = CMutableTransaction(vin, vout, nVersion=2)
print(tx2)
# phase 2: given a transaction (tx1) in hex which funds an output for addr addr1, we construct a transaction spending that (tx2).
# we use the second of the three scripts:
s, cb = tree.get_script_with_control_block('multisig2')
sh = s.sighash_schnorr(tx2, 0, (CTxOut(1000000, sPK),))
sig_for_key_2 = keys[2].sign_schnorr_no_tweak(sh)
print(hexlify(sig_for_key_2))
print()
print("Verificationresult: ", keys[2].xonly_pub.verify_schnorr(sh, sig_for_key_2))
print(len(sig_for_key_2))
print()
sig_for_key_3 = keys[3].sign_schnorr_no_tweak(sh)
tx2.wit.vtxinwit[0] = CTxInWitness(CScriptWitness([sig_for_key_3, sig_for_key_2, s, cb]))
print(tx2)
print(hexlify(tx2.serialize()))
@dgpv
Copy link

dgpv commented Apr 15, 2022

You seem to use amount 0.00005 (5000 satoshi) as the amount of the utxo spent, but actually spending your transaction's output 1 that has amount 0.0002 (20000 satoshi):

>>> tx=CTransaction.deserialize(x('02000000000101c2be0b692d3ad241d419b27dc87fe19afdd108b0f964261516105650e90f5fcb0000000000feffffff020b216d1050060000160014b6937f2aeb518a6ed53c2e03b8f7cc08633eff99204e00000000000022512033efb169849874c88f60edb29cbe6e612c2f84e0fd6e1c5b0737cf69973e01950247304402202a9117574e4cd00a2410eef0e7974175c8fccd6ff532f22eee58494e72f772d002202b4fc6b2332faaf96fb22eec410b29bfaa4cc6b66e7b3dda45095b43baab8167012102d480e70381f1fb4cb63bea797ab81b3d084f51f521a0d8605408d428e5604357e94f0100'))
>>> tx.vout[1]
CBitcoinTxOut(0.0002*COIN, CBitcoinScript([1, x('33efb169849874c88f60edb29cbe6e612c2f84e0fd6e1c5b0737cf69973e0195')]))

@HRezaei
Copy link

HRezaei commented Apr 15, 2022

Yes @dgpv, it worked with a proper amount. Thank you both! 🙏🏻
But wasn't the error message misleading? Does "non-mandatory-script-verify-flag (Invalid Schnorr signature)" make sense for you in this case?

@AdamISZ
Copy link
Author

AdamISZ commented Apr 15, 2022

Does "non-mandatory-script-verify-flag (Invalid Schnorr signature)" makes sense for you in this case?

That error message is a bit cryptic, yes, it's been a long standing cause of confusion amongst Bitcoin wallet developers :) On the other hand, 'invalid Schnorr signature' is pretty good.

I can see your confusion though: you're thinking, the verification in the Python code passes, so it's weird that it's a sig verification that fails. But that's because the Python code can only believe what you tell it about the amount and txid of the prevout that's being spent; whereas the node actually looks it up and gets an invalid signature on the correct amount.

@HRezaei
Copy link

HRezaei commented Apr 16, 2022

😊 OK, thanks again for the explanation.

One more question, I'm afraid: I used to think that as long as the sum of outputs is less than (or equal to) the sum of inputs, the transaction is valid and the discrepancy will be treated as the fee and paid to the node. So in my code above, the input had 20,000 satoshi's and the output was 4000 sat. It should be treated as a generous 16,000 sat fee :) and succeed, shouldn’t it?

@HRezaei
Copy link

HRezaei commented Apr 16, 2022

Found it! :)

The amount on the line sh = s.sighash_schnorr(tx2, 0, (CTxOut(amount, sPK),)) was the source of the problem, it must be the exact amount of the input being unlocked, regardless of how much is going to be spent and regardless of the fee. In other words, it must be the exact amount of the selected output in the previous transaction. Please correct me if I'm wrong.

@dgpv
Copy link

dgpv commented Apr 16, 2022

You're correct, the problem was that the hash that was calculated by that line sh = s.sighash_schnorr(...) (1) was different from the hash that was calculated by the node based (among other things) on the amount on the actual prevout (2), and thus the signature that was created for (1) was invalid for the (2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment