diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index c6a4bf8863a..e36aa301eb6 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -52,10 +52,12 @@ from test_framework.script import ( OP_16, OP_2DROP, OP_2DUP, + OP_3DUP, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGADD, + OP_CHECKSIGFROMSTACK, OP_CHECKSIGVERIFY, OP_CODESEPARATOR, OP_DROP, @@ -410,7 +412,7 @@ DEFAULT_CONTEXT = { # The annex (only when mode=="taproot"). "annex": None, # The codeseparator position (only when mode=="taproot"). - "codeseppos": -1, + "codeseppos": 0xffffffff, # The redeemscript to add to the scriptSig (if P2SH; None implies not P2SH). "script_p2sh": None, # The script to add to the witness in (if P2WSH; None implies P2WPKH) @@ -750,6 +752,8 @@ def spenders_taproot_active(): tap = taproot_construct(pubs[0], scripts) add_spender(spenders, "sighash/pk_codesep", tap=tap, leaf="pk_codesep", key=secs[1], **common, **SINGLE_SIG, **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) add_spender(spenders, "sighash/codesep_pk", tap=tap, leaf="codesep_pk", key=secs[1], codeseppos=0, **common, **SINGLE_SIG, **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/codesep_pk_wrongpos1", tap=tap, leaf="codesep_pk", key=secs[1], codeseppos=0, **common, **SINGLE_SIG, failure={"codeseppos": 1}, **ERR_SIG_SCHNORR) + add_spender(spenders, "sighash/codesep_pk_wrongpos2", tap=tap, leaf="codesep_pk", key=secs[1], codeseppos=0, **common, **SINGLE_SIG, failure={"codeseppos": 0xfffffffe}, **ERR_SIG_SCHNORR) add_spender(spenders, "sighash/branched_codesep/left", tap=tap, leaf="branched_codesep", key=secs[0], codeseppos=3, **common, inputs=[getter("sign"), b'\x01'], **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) add_spender(spenders, "sighash/branched_codesep/right", tap=tap, leaf="branched_codesep", key=secs[1], codeseppos=6, **common, inputs=[getter("sign"), b''], **SIGHASH_BITFLIP, **ERR_SIG_SCHNORR) @@ -1054,6 +1058,13 @@ def spenders_taproot_active(): # == Test for sigops ratio limit == + # BIP348 CSFS signatures are embedded directly into the tapleaves vs the witness stack + # since they do not introspect directly + CSFS_MSG = b'\x00\x00' + # Signature should pass even if random unknown key is used, just use real privkey + # to pass in case it's the defined pubkey + CSFS_SIG = sign_schnorr(secs[1], CSFS_MSG) + # Given a number n, and a public key pk, functions that produce a (CScript, sigops). Each script takes as # input a valid signature with the passed pk followed by a dummy push of bytes that are to be dropped, and # will execute sigops signature checks. @@ -1070,7 +1081,15 @@ def spenders_taproot_active(): lambda n, pk: (CScript([OP_DROP, OP_0, pk, OP_CHECKSIG, OP_NOT, OP_VERIFY, pk] + [OP_2DUP, OP_CHECKSIG, OP_VERIFY] * n + [OP_CHECKSIG]), n + 1), # n OP_CHECKSIGADDs and 1 OP_CHECKSIG, but also an OP_CHECKSIGADD with an empty signature. lambda n, pk: (CScript([OP_DROP, OP_0, OP_10, pk, OP_CHECKSIGADD, OP_10, OP_EQUALVERIFY, pk] + [OP_2DUP, OP_16, OP_SWAP, OP_CHECKSIGADD, b'\x11', OP_EQUALVERIFY] * n + [OP_CHECKSIG]), n + 1), + # n OP_CHECKSIGFROMSTACKs, dropping the signature given, and just validate against embedded sigs + lambda n, pk: (CScript([OP_2DROP, CSFS_SIG, CSFS_MSG, pk] + [OP_3DUP, OP_CHECKSIGFROMSTACK, OP_DROP] * n + [OP_2DROP]), n), + # 1 CHECKSIGVERIFY followed by n OP_CHECKSIGFROMSTACKs, all signatures non-empty and validated + lambda n, pk: (CScript([OP_DROP, pk, OP_CHECKSIGVERIFY, CSFS_SIG, CSFS_MSG, pk] + [OP_3DUP, OP_CHECKSIGFROMSTACK, OP_DROP] * n + [OP_2DROP]), n+1), + # 1 empty CHECKSIG followed by 1 empty OP_CHECKSIGFROMSTACKs, then finally n OP_CHECKSIGFROMSTACKs + lambda n, pk: (CScript([OP_2DROP, OP_0, pk, OP_CHECKSIG, OP_DROP, OP_0, CSFS_MSG, pk, OP_CHECKSIGFROMSTACK, OP_DROP, CSFS_SIG, CSFS_MSG, pk] + [OP_3DUP, OP_CHECKSIGFROMSTACK, OP_DROP] * n + [OP_2DROP]), n), + ] + for annex in [None, bytes([ANNEX_TAG]) + random.randbytes(random.randrange(1000))]: for hashtype in [SIGHASH_DEFAULT, SIGHASH_ALL]: for pubkey in [pubs[1], random.randbytes(random.choice([x for x in range(2, 81) if x != 32]))]: @@ -1234,6 +1253,67 @@ def spenders_taproot_nonstandard(): return spenders +def bip348_csfs_spenders(): + secs = [generate_privkey() for _ in range(2)] + pubs = [compute_xonly_pubkey(sec)[0] for sec in secs] + + CSFS_MSG = random.randbytes(random.randrange(0, 520)) + + # Grow, shrink the message being signed, and pick random bytes + TRUNC_CSFS_MSG = CSFS_MSG[:] if len(CSFS_MSG) > 0 else None + if TRUNC_CSFS_MSG is not None: + prune_index = random.randrange(len(TRUNC_CSFS_MSG)) + TRUNC_CSFS_MSG = TRUNC_CSFS_MSG[:prune_index] + TRUNC_CSFS_MSG[prune_index+1:] + extendable_length = 520 - len(CSFS_MSG) + EXTEND_CSFS_MSG = None + if extendable_length > 0: + EXTEND_CSFS_MSG = CSFS_MSG + random.randbytes(random.randrange(1, extendable_length)) + OTHER_CSFS_MSG = CSFS_MSG + + while OTHER_CSFS_MSG == CSFS_MSG: + OTHER_CSFS_MSG = random.randbytes(random.randrange(0, 520)) + + UNK_PUBKEY = random.randbytes(random.randrange(1, 520)) + while len(UNK_PUBKEY) == 32: + UNK_PUBKEY = random.randbytes(random.randrange(1, 520)) + + # Sigops ratio test is included elsewhere to mix and match with other sigops + scripts = [ + ("simple_csfs", CScript([CSFS_MSG, pubs[0], OP_CHECKSIGFROMSTACK, OP_1, OP_EQUAL])), + ("simple_fail_csfs", CScript([CSFS_MSG, pubs[0], OP_CHECKSIGFROMSTACK, OP_0, OP_EQUAL])), + ("unk_pubkey_csfs", CScript([CSFS_MSG, UNK_PUBKEY, OP_CHECKSIGFROMSTACK])), + ("onearg_csfs", CScript([pubs[0], OP_CHECKSIGFROMSTACK])), + ("twoargs_csfs", CScript([CSFS_MSG, pubs[0], OP_CHECKSIGFROMSTACK])), + ("empty_pk_csfs", CScript([CSFS_MSG, OP_0, OP_CHECKSIGFROMSTACK, OP_0, OP_EQUAL])), + ] + + tap = taproot_construct(pubs[0], scripts) + + spenders = [] + + # "sighash" is actually the bip340 message being directly verified against + add_spender(spenders, comment="bip348_csfs/simple", tap=tap, leaf="simple_csfs", key=secs[0], inputs=[getter("sign")], sighash=CSFS_MSG, failure={"sighash": OTHER_CSFS_MSG}, **ERR_SIG_SCHNORR) + if TRUNC_CSFS_MSG is not None: + add_spender(spenders, comment="bip348_csfs/trunc_msg", tap=tap, leaf="onearg_csfs", key=secs[0], inputs=[getter("sign"), CSFS_MSG], standard=len(CSFS_MSG)<=80, sighash=CSFS_MSG, failure={"inputs": [getter("sign"), TRUNC_CSFS_MSG]}, **ERR_SIG_SCHNORR) + if EXTEND_CSFS_MSG is not None: + add_spender(spenders, comment="bip348_csfs/extend_msg", tap=tap, leaf="onearg_csfs", key=secs[0], inputs=[getter("sign"), CSFS_MSG], standard=len(CSFS_MSG)<=80, sighash=CSFS_MSG, failure={"inputs": [getter("sign"), EXTEND_CSFS_MSG]}, **ERR_SIG_SCHNORR) + + # Empty signature pushes zero onto stack and continues, unless the pubkey is empty + add_spender(spenders, comment="bip348_csfs/simple_fail", tap=tap, leaf="simple_fail_csfs", inputs=[b''], failure={"leaf": "empty_pk_csfs", "inputs": [OTHER_CSFS_MSG]}, **ERR_UNKNOWN_PUBKEY) + + # Unknown pubkey of non-zero size is unconditionally valid regardless of signature (but signature must exist) + add_spender(spenders, comment="bip348_csfs/unk_pubkey", tap=tap, leaf="unk_pubkey_csfs", standard=False, key=secs[0], inputs=[getter("sign")], sighash=CSFS_MSG, failure={"inputs": []}, **ERR_STACK_EMPTY) + + # You need three args for CSFS regardless of what is passed + add_spender(spenders, comment="bip348_csfs/onearg", tap=tap, leaf="onearg_csfs", key=secs[0], inputs=[getter("sign"), CSFS_MSG], standard=len(CSFS_MSG)<=80, sighash=CSFS_MSG, failure={"inputs": []}, **ERR_STACK_EMPTY) + add_spender(spenders, comment="bip348_csfs/twoarg", tap=tap, leaf="twoargs_csfs", key=secs[0], inputs=[getter("sign")], sighash=CSFS_MSG, failure={"inputs": []}, **ERR_STACK_EMPTY) + + # If a known pubkey's signature is not 64 bytes or empty it MUST fail immediately + add_spender(spenders, comment="bip348_csfs/simple_65_sig", tap=tap, leaf="simple_csfs", key=secs[0], inputs=[getter("sign")], sighash=CSFS_MSG, failure={"leaf": "simple_fail_csfs", "inputs": [zero_appender(getter("sign"))]}, **ERR_SIG_SCHNORR) + add_spender(spenders, comment="bip348_csfs/simple_63_sig", tap=tap, leaf="simple_csfs", key=secs[0], inputs=[getter("sign")], sighash=CSFS_MSG, failure={"leaf": "simple_fail_csfs", "inputs": [byte_popper(getter("sign"))]}, **ERR_SIG_SCHNORR) + + return spenders + def sample_spenders(): # Create key(s) for output creation, as well as key and script-spends @@ -1800,6 +1880,7 @@ class TaprootTest(BitcoinTestFramework): # to allow for increased coverage across input types. # See sample_spenders for a minimal example consensus_spenders = sample_spenders() + consensus_spenders += bip348_csfs_spenders() consensus_spenders += spenders_taproot_active() self.test_spenders(self.nodes[0], consensus_spenders, input_counts=[1, 2, 2, 2, 2, 3]) diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index 682c2de35fe..573834071d4 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -272,7 +272,6 @@ def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False): aux = bytes(32) assert len(key) == 32 - assert len(msg) == 32 assert len(aux) == 32 sec = int.from_bytes(key, 'big') diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 19ec5fbcbee..d549173b752 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -233,7 +233,6 @@ OP_CHECKSIG = CScriptOp(0xac) OP_CHECKSIGVERIFY = CScriptOp(0xad) OP_CHECKMULTISIG = CScriptOp(0xae) OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf) -OP_CHECKSIGFROMSTACK = CScriptOp(0xcc) # expansion OP_NOP1 = CScriptOp(0xb0) @@ -250,6 +249,9 @@ OP_NOP10 = CScriptOp(0xb9) # BIP 342 opcodes (Tapscript) OP_CHECKSIGADD = CScriptOp(0xba) +# BIP 348 (OP_SUCCESS204) +OP_CHECKSIGFROMSTACK = CScriptOp(0xcc) + OP_INVALIDOPCODE = CScriptOp(0xff) OPCODE_NAMES.update({ @@ -850,7 +852,7 @@ def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index=0, *, scriptpa if scriptpath: ss += TaggedHash("TapLeaf", bytes([leaf_ver]) + ser_string(leaf_script)) ss += bytes([0]) - ss += codeseparator_pos.to_bytes(4, "little", signed=True) + ss += codeseparator_pos.to_bytes(4, "little", signed=False) assert len(ss) == 175 - (in_type == SIGHASH_ANYONECANPAY) * 49 - (out_type != SIGHASH_ALL and out_type != SIGHASH_SINGLE) * 32 + (annex is not None) * 32 + scriptpath * 37 return ss