From 1247b8b670eb7bea1f268c960e0280f23f67e3d3 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Tue, 26 Nov 2024 14:25:13 -0800 Subject: [PATCH 1/3] Implement OP_CHECKSIGFROMSTACK Some code and ideas from Elements by stevenroose, and sanket1729 Porting help from moonsettler. Tests added to the transaction tests framework. Co-authored-by: James O'Beirne --- src/policy/policy.h | 3 +- src/pubkey.cpp | 5 +- src/pubkey.h | 3 +- src/script/interpreter.cpp | 95 +++++++++++++++++++++++- src/script/interpreter.h | 6 ++ src/script/script.cpp | 3 + src/script/script.h | 1 + src/test/data/tx_invalid.json | 8 ++ src/test/data/tx_valid.json | 29 ++++++++ src/test/transaction_tests.cpp | 2 + test/functional/feature_taproot.py | 6 ++ test/functional/test_framework/script.py | 16 +++- 12 files changed, 169 insertions(+), 8 deletions(-) diff --git a/src/policy/policy.h b/src/policy/policy.h index 2151ec13dd0..f417a038ab0 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -124,7 +124,8 @@ static constexpr unsigned int STANDARD_SCRIPT_VERIFY_FLAGS{MANDATORY_SCRIPT_VERI SCRIPT_VERIFY_CONST_SCRIPTCODE | SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION | SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS | - SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE}; + SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE | + SCRIPT_VERIFY_CHECKSIGFROMSTACK}; /** For convenience, standard but not mandatory verify flags. */ static constexpr unsigned int STANDARD_NOT_MANDATORY_VERIFY_FLAGS{STANDARD_SCRIPT_VERIFY_FLAGS & ~MANDATORY_SCRIPT_VERIFY_FLAGS}; diff --git a/src/pubkey.cpp b/src/pubkey.cpp index a4ca9a170a9..b23f2843379 100644 --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -227,12 +227,13 @@ bool XOnlyPubKey::IsFullyValid() const return secp256k1_xonly_pubkey_parse(secp256k1_context_static, &pubkey, m_keydata.data()); } -bool XOnlyPubKey::VerifySchnorr(const uint256& msg, std::span sigbytes) const +bool XOnlyPubKey::VerifySchnorr( + const std::span msg, std::span sigbytes) const { assert(sigbytes.size() == 64); secp256k1_xonly_pubkey pubkey; if (!secp256k1_xonly_pubkey_parse(secp256k1_context_static, &pubkey, m_keydata.data())) return false; - return secp256k1_schnorrsig_verify(secp256k1_context_static, sigbytes.data(), msg.begin(), 32, &pubkey); + return secp256k1_schnorrsig_verify(secp256k1_context_static, sigbytes.data(), msg.data(), msg.size(), &pubkey); } static const HashWriter HASHER_TAPTWEAK{TaggedHash("TapTweak")}; diff --git a/src/pubkey.h b/src/pubkey.h index cbc827dc606..753d7c967f0 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -263,7 +263,8 @@ public: * * sigbytes must be exactly 64 bytes. */ - bool VerifySchnorr(const uint256& msg, std::span sigbytes) const; + bool VerifySchnorr( + const std::span msg, std::span sigbytes) const; /** Compute the Taproot tweak as specified in BIP341, with *this as internal * key: diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 61ea7f4503c..481fe43d2db 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -343,6 +343,53 @@ static bool EvalChecksigPreTapscript(const valtype& vchSig, const valtype& vchPu return true; } +static bool EvalChecksigFromStack(const valtype& sig, const valtype& msg, const valtype& pubkey_in, ScriptExecutionData& execdata, unsigned int flags, SigVersion sigversion, ScriptError* serror, bool& success) +{ + /* + * The following validation sequence is consensus critical. Please note how -- + * upgradable public key versions precede other rules; + * the script execution fails when using empty signature with invalid public key; + * the script execution fails when using non-empty invalid signature. + */ + success = !sig.empty(); + if (success && sigversion == SigVersion::TAPSCRIPT) { + // Implement the sigops/witnesssize ratio test. + // Passing with an upgradable public key version is also counted. + assert(execdata.m_validation_weight_left_init); + execdata.m_validation_weight_left -= VALIDATION_WEIGHT_PER_SIGOP_PASSED; + if (execdata.m_validation_weight_left < 0) { + return set_error(serror, SCRIPT_ERR_TAPSCRIPT_VALIDATION_WEIGHT); + } + } + if (pubkey_in.size() == 0) { + return set_error(serror, SCRIPT_ERR_PUBKEYTYPE); + } else if (pubkey_in.size() == 32) { + if (!success) { + return true; + } + if (sig.size() != 64) { + return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_SIZE); + } + + XOnlyPubKey pubkey{pubkey_in}; + + if (!pubkey.VerifySchnorr(msg, sig)) { + return set_error(serror, SCRIPT_ERR_SCHNORR_SIG); + } + } else { + /* + * New public key version softforks should be defined before this `else` block. + * Generally, the new code should not do anything but failing the script execution. To avoid + * consensus bugs, it should not modify any existing values (including `success`). + */ + if ((flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE) != 0) { + return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_PUBKEYTYPE); + } + } + + return true; +} + static bool EvalChecksigTapscript(const valtype& sig, const valtype& pubkey, ScriptExecutionData& execdata, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptError* serror, bool& success) { assert(sigversion == SigVersion::TAPSCRIPT); @@ -1213,6 +1260,39 @@ bool EvalScript(std::vector >& stack, const CScript& } break; + case OP_CHECKSIGFROMSTACK: + { + // OP_CHECKSIGFROMSTACK is only available in Tapscript + if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) { + return set_error(serror, SCRIPT_ERR_BAD_OPCODE); + } + + // + if (stack.size() < 3) { + return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION); + } + + const valtype& vchSigIn = stacktop(-3); + const valtype& vchMsg = stacktop(-2); + const valtype& vchPubKey = stacktop(-1); + + bool fSuccess = true; + + // Note that (as with CHECKSIG) if a signature was supplied and its + // verification fails, we do _not_ push a "false" result to the stack. + // Rather, we terminate script execution immediately. This might be + // surprising if you're reading this for the first time. + if (!EvalChecksigFromStack(vchSigIn, vchMsg, vchPubKey, execdata, flags, sigversion, serror, fSuccess)) { + return false; + } + + popstack(stack); + popstack(stack); + popstack(stack); + stack.push_back(fSuccess ? vchTrue : vchFalse); + } + break; + default: return set_error(serror, SCRIPT_ERR_BAD_OPCODE); } @@ -1800,10 +1880,19 @@ static bool ExecuteWitnessScript(const std::span& stack_span, con } // New opcodes will be listed here. May use a different sigversion to modify existing opcodes. if (IsOpSuccess(opcode)) { - if (flags & SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS) { - return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS); + if (opcode == OP_CHECKSIGFROMSTACK) { + if (flags & SCRIPT_VERIFY_DISCOURAGE_CHECKSIGFROMSTACK) { + return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS); + } else if (!(flags & SCRIPT_VERIFY_CHECKSIGFROMSTACK)) { + return set_success(serror); + } + } else { + // OP_SUCCESS behaviour + if (flags & SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS) { + return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS); + } + return set_success(serror); } - return set_success(serror); } } diff --git a/src/script/interpreter.h b/src/script/interpreter.h index e8c5b09045f..1d36a858ac9 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -143,6 +143,12 @@ enum : uint32_t { // Making unknown public key versions (in BIP 342 scripts) non-standard SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE = (1U << 20), + // Validating OP_CHECKSIGFROMSTACK(VERIFY) + SCRIPT_VERIFY_CHECKSIGFROMSTACK = (1U << 21), + + // Making OP_CHECKSIGFROMSTACK(VERIFY) non-standard + SCRIPT_VERIFY_DISCOURAGE_CHECKSIGFROMSTACK = (1U << 22), + // Constants to point to the highest flag in use. Add new flags above this line. // SCRIPT_VERIFY_END_MARKER diff --git a/src/script/script.cpp b/src/script/script.cpp index d650db9a0d6..a2315b92a4d 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -149,6 +149,9 @@ std::string GetOpName(opcodetype opcode) // Opcode added by BIP 342 (Tapscript) case OP_CHECKSIGADD : return "OP_CHECKSIGADD"; + // Tapscript expansion + case OP_CHECKSIGFROMSTACK : return "OP_CHECKSIGFROMSTACK"; + case OP_INVALIDOPCODE : return "OP_INVALIDOPCODE"; default: diff --git a/src/script/script.h b/src/script/script.h index f4579849803..5811c5f7852 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -191,6 +191,7 @@ enum opcodetype OP_CHECKSIGVERIFY = 0xad, OP_CHECKMULTISIG = 0xae, OP_CHECKMULTISIGVERIFY = 0xaf, + OP_CHECKSIGFROMSTACK = 0xcc, // expansion OP_NOP1 = 0xb0, diff --git a/src/test/data/tx_invalid.json b/src/test/data/tx_invalid.json index 486469ddefb..6309cee032e 100644 --- a/src/test/data/tx_invalid.json +++ b/src/test/data/tx_invalid.json @@ -393,5 +393,13 @@ ["ceafe58e0f6e7d67c0409fbbf673c84c166e3c5d3c24af58f7175b18df3bb3db", 1, "2 0x48 0x3045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 3 CHECKMULTISIG"]], "0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000", "CONST_SCRIPTCODE"], + ["Test OP_CHECKSIGFROMSTACK, fails immediately with sig for wrong data"], + [[["a2522fa96033c5736f3142ff616426cd03a3d0f077f609e22c5a33a96e04e597", + 0, + "1 0x20 0x6e929e9354a357e9a1254feac061741a11c66508786c66b3b29edc79b9c46e19", + 155000]], +"0200000000010197e5046ea9335a2ce209f677f0d0a303cd266461ff42316f73c53360a92f52a20000000000ffffffff01f04902000000000022512040104c71081b266fdf4008db8b0a1c3291f2e1cb680753936de9b76dac45a6ef0340b5258eeb9df148d499d14b8e23fe5315a230b7f1dee497a04605426ffe068f2e0920c9b63ba28b1c6cea39c0e659af1658825d23e859c5ae773a0be996f1c4744520feadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef204835c505a762f5e55c2e8eda1c05437d973809a0236178510208a6ac3f7632bfcc008721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000", + "P2SH,WITNESS,TAPROOT,CHECKSIGFROMSTACK"], + ["Make diffs cleaner by leaving a comment here without comma at the end"] ] diff --git a/src/test/data/tx_valid.json b/src/test/data/tx_valid.json index 70df0d0f697..bb52927010a 100644 --- a/src/test/data/tx_valid.json +++ b/src/test/data/tx_valid.json @@ -520,5 +520,34 @@ [[["1111111111111111111111111111111111111111111111111111111111111111", 0, "0x00 0x14 0x751e76e8199196d454941c45d1b3a323f1433bd6", 5000000]], "0100000000010111111111111111111111111111111111111111111111111111111111111111110000000000ffffffff0130244c0000000000fd02014cdc1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111175210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac02483045022100c1a4a6581996a7fdfea77d58d537955a5655c1d619b6f3ab6874f28bb2e19708022056402db6fede03caae045a3be616a1a2d0919a475ed4be828dc9ff21f24063aa01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", "NONE"], + ["Test OP_CHECKSIGFROMSTACK"], + [[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5", + 0, + "1 0x20 0xed98cc178a5e3f2537ec8bf5ab9a14e56b8a188d666ba6ce788405e849ba7da8", + 155000]], +"02000000000101d58631133c4d4f6188abbd0fa0a7aa64bfde05ce4297e3349b38599ceebaf2e20000000000ffffffff01f0490200000000002251203408099b8f38a71ab6dfafdf0b266bd0a0f58096b5c453624c752bae6c0f19560340cd3e61f2754dd13e51a6d86d18092f795c626d36deaf0cf076a87648d9f4e4cfceaaa8e8a7eee1ee13dd09ef2c14eedd475f4e9adcf8a2391b910271b2203aa24320deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef208fdb638cf9201fcae809f31b7d5b5ef9ae712cd374c8c89b06d52b9d2c3885bfcc21c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000", + "DISCOURAGE_CHECKSIGFROMSTACK"], + ["Test OP_CHECKSIGFROMSTACK succeeds with unknown key type"], + [[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5", + 0, + "1 0x20 0xde96616e5e3961cbbd7bab3ea0e6b6e1ace088299857136fbb3703454c784afb", + 155000]], +"02000000000101d58631133c4d4f6188abbd0fa0a7aa64bfde05ce4297e3349b38599ceebaf2e20000000000ffffffff01f0490200000000002251203408099b8f38a71ab6dfafdf0b266bd0a0f58096b5c453624c752bae6c0f19560340cd3e61f2754dd13e51a6d86d18092f795c626d36deaf0cf076a87648d9f4e4cfceaaa8e8a7eee1ee13dd09ef2c14eedd475f4e9adcf8a2391b910271b2203aa24420deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef21038fdb638cf9201fcae809f31b7d5b5ef9ae712cd374c8c89b06d52b9d2c3885bfcc21c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000", + "DISCOURAGE_CHECKSIGFROMSTACK,DISCOURAGE_UPGRADABLE_PUBKEYTYPE"], + ["Test OP_CHECKSIGFROMSTACK yields 0 for 0-sig"], + [[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5", + 0, + "1 0x20 0x7f3db202bc0db8c15de91c5da0dd64bd52ae81f5847cda623e1304c524cad314", + 155000]], +"02000000000101d58631133c4d4f6188abbd0fa0a7aa64bfde05ce4297e3349b38599ceebaf2e20000000000ffffffff01f0490200000000002251203408099b8f38a71ab6dfafdf0b266bd0a0f58096b5c453624c752bae6c0f195603004520deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef208fdb638cf9201fcae809f31b7d5b5ef9ae712cd374c8c89b06d52b9d2c3885bfcc008721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000", + "DISCOURAGE_CHECKSIGFROMSTACK"], + ["Test OP_CHECKSIGFROMSTACK, shorter message"], + [[["e2f2baee9c59389b34e39742ce05debf64aaa7a00fbdab88614f4d3c133186d5", + 0, + "1 0x20 0x313a784205aef89c9d203c1e4cfacd2e31fa55f42dcd77e5e2db9d0513d50827", + 155000]], +"02000000000101d58631133c4d4f6188abbd0fa0a7aa64bfde05ce4297e3349b38599ceebaf2e20000000000ffffffff01f0490200000000002251203408099b8f38a71ab6dfafdf0b266bd0a0f58096b5c453624c752bae6c0f195603403c5a935ce7a3856bc3e75eae403a21ff2e5a9f919c0f6f4d6bf7f58c834c13484882fc6f98587fe48e6945a49c0ca4fc62fb5f641a216ea62ac2dbc0071976833411636865636b73696766726f6d737461636b208fdb638cf9201fcae809f31b7d5b5ef9ae712cd374c8c89b06d52b9d2c3885bfcc21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000", + "DISCOURAGE_CHECKSIGFROMSTACK"], + ["Make diffs cleaner by leaving a comment here without comma at the end"] ] diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index 1375672a418..6a388878567 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -70,6 +70,8 @@ static std::map mapFlagNames = { {std::string("DISCOURAGE_UPGRADABLE_PUBKEYTYPE"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE}, {std::string("DISCOURAGE_OP_SUCCESS"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS}, {std::string("DISCOURAGE_UPGRADABLE_TAPROOT_VERSION"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION}, + {std::string("CHECKSIGFROMSTACK"), (unsigned int)SCRIPT_VERIFY_CHECKSIGFROMSTACK}, + {std::string("DISCOURAGE_CHECKSIGFROMSTACK"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_CHECKSIGFROMSTACK}, }; unsigned int ParseScriptFlags(std::string strFlags) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index 68c2c2e5015..ae95a7da8f8 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -81,6 +81,7 @@ from test_framework.script import ( TaggedHash, TaprootSignatureMsg, is_op_success, + OP_SUCCESS_OVERRIDES, taproot_construct, ) from test_framework.script_util import ( @@ -1143,6 +1144,8 @@ def spenders_taproot_active(): opcode = CScriptOp(opval) if not is_op_success(opcode): continue + if opcode in OP_SUCCESS_OVERRIDES: + continue scripts = [ ("bare_success", CScript([opcode])), ("bare_nop", CScript([OP_NOP])), @@ -1174,6 +1177,9 @@ def spenders_taproot_active(): opcode = CScriptOp(opval) if is_op_success(opcode): continue + if opcode in OP_SUCCESS_OVERRIDES: + # TODO: remove this once CHECKSIGFROMSTACK gets a regtest deployment. + continue scripts = [ ("normal", CScript([OP_RETURN, opcode] + [OP_NOP] * 75)), ("op_success", CScript([OP_RETURN, CScriptOp(0x50)])) diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 12bfee7c776..19ec5fbcbee 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -233,6 +233,7 @@ 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) @@ -365,6 +366,7 @@ OPCODE_NAMES.update({ OP_NOP10: 'OP_NOP10', OP_CHECKSIGADD: 'OP_CHECKSIGADD', OP_INVALIDOPCODE: 'OP_INVALIDOPCODE', + OP_CHECKSIGFROMSTACK: 'OP_CHECKSIGFROMSTACK', }) class CScriptInvalidError(Exception): @@ -935,5 +937,17 @@ def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False): leaves = dict((name, TaprootLeafInfo(script, version, merklebranch, leaf)) for name, version, script, merklebranch, leaf in ret) return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked) + +# OP_SUCCESS opcodes which have been restricted by softforks. +OP_SUCCESS_OVERRIDES = frozenset({ + OP_CHECKSIGFROMSTACK, +}) + def is_op_success(o): - return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe) + if o in OP_SUCCESS_OVERRIDES: + return False + return ( + o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or + o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or + (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe) + ) From 7be5c60a57046493aea22ae94975062c072ef8d3 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Wed, 19 Mar 2025 16:16:58 -0400 Subject: [PATCH 2/3] consensus: add a CSFS deployment for regtest only --- src/consensus/params.h | 1 + src/deploymentinfo.cpp | 4 ++++ src/kernel/chainparams.cpp | 5 +++++ src/rpc/blockchain.cpp | 4 ++++ src/test/versionbits_tests.cpp | 6 ++++++ src/validation.cpp | 13 ++++++++++++- test/functional/feature_taproot.py | 3 --- test/functional/rpc_blockchain.py | 13 +++++++++++++ 8 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/consensus/params.h b/src/consensus/params.h index dd29b9408e2..b774784d753 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -33,6 +33,7 @@ enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, DEPLOYMENT_TAPROOT, // Deployment of Schnorr/Taproot (BIPs 340-342) // NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp + DEPLOYMENT_CSFS, // Deployment of CHECKSIGFROMSTACK (BIP 348) (regtest only) MAX_VERSION_BITS_DEPLOYMENTS }; constexpr bool ValidDeployment(DeploymentPos dep) { return dep < MAX_VERSION_BITS_DEPLOYMENTS; } diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index 185a7dcb54c..245ae20fc32 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -17,6 +17,10 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B /*.name =*/ "taproot", /*.gbt_force =*/ true, }, + { + /*.name =*/ "csfs", + /*.gbt_force =*/ true, + }, }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 4a747e7317c..e05d94325d8 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -529,6 +529,11 @@ public: consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay + consensus.vDeployments[Consensus::DEPLOYMENT_CSFS].bit = 1; + consensus.vDeployments[Consensus::DEPLOYMENT_CSFS].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_CSFS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_CSFS].min_activation_height = 0; // No activation delay + consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index ac1ce6285f7..107e49e7040 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1408,6 +1408,10 @@ UniValue DeploymentInfo(const CBlockIndex* blockindex, const ChainstateManager& SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_SEGWIT); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TESTDUMMY); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TAPROOT); + + if (chainman.GetParams().GetChainType() == ChainType::REGTEST) { + SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_CSFS); + } return softforks; } } // anon namespace diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp index 29240a45f09..84f2c563438 100644 --- a/src/test/versionbits_tests.cpp +++ b/src/test/versionbits_tests.cpp @@ -435,6 +435,12 @@ BOOST_FIXTURE_TEST_CASE(versionbits_computeblockversion, BlockVersionTest) // the same bit might overlap, even when non-overlapping start-end // times are picked. const uint32_t dep_mask{vbcache.Mask(chainParams->GetConsensus(), dep)}; + + if (chain_type != ChainType::REGTEST && dep == Consensus::DEPLOYMENT_CSFS) { + // CSFS only exists as a deployment on regtest, so skip over it for other + // chains. + continue; + } BOOST_CHECK(!(chain_all_vbits & dep_mask)); chain_all_vbits |= dep_mask; check_computeblockversion(vbcache, chainParams->GetConsensus(), dep); diff --git a/src/validation.cpp b/src/validation.cpp index aa1effb736f..ab99312348e 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1231,7 +1231,13 @@ bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws) const CTransaction& tx = *ws.m_ptx; TxValidationState& state = ws.m_state; - constexpr unsigned int scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS; + unsigned int scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS; + + // CHECKSIGFROMSTACK (BIP348) is always active on regtest, but no other chain. + if (args.m_chainparams.GetChainType() == ChainType::REGTEST) { + scriptVerifyFlags |= SCRIPT_VERIFY_CHECKSIGFROMSTACK; + scriptVerifyFlags &= ~SCRIPT_VERIFY_DISCOURAGE_CHECKSIGFROMSTACK; + } // Check input scripts and signatures. // This is done last to help prevent CPU exhaustion denial-of-service attacks. @@ -2410,6 +2416,11 @@ static unsigned int GetBlockScriptFlags(const CBlockIndex& block_index, const Ch flags = it->second; } + // Enforce CHECKSIGFROMSTACK (BIP348) + if (DeploymentActiveAt(block_index, chainman, Consensus::DEPLOYMENT_CSFS)) { + flags |= SCRIPT_VERIFY_CHECKSIGFROMSTACK; + } + // Enforce the DERSIG (BIP66) rule if (DeploymentActiveAt(block_index, chainman, Consensus::DEPLOYMENT_DERSIG)) { flags |= SCRIPT_VERIFY_DERSIG; diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index ae95a7da8f8..c6a4bf8863a 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -1177,9 +1177,6 @@ def spenders_taproot_active(): opcode = CScriptOp(opval) if is_op_success(opcode): continue - if opcode in OP_SUCCESS_OVERRIDES: - # TODO: remove this once CHECKSIGFROMSTACK gets a regtest deployment. - continue scripts = [ ("normal", CScript([OP_RETURN, opcode] + [OP_NOP] * 75)), ("op_success", CScript([OP_RETURN, CScriptOp(0x50)])) diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 2976f9188ae..e25c6525d91 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -251,6 +251,19 @@ class BlockchainTest(BitcoinTestFramework): }, 'height': 0, 'active': True + }, + 'csfs': { + 'type': 'bip9', + 'bip9': { + 'start_time': -1, + 'timeout': 9223372036854775807, + 'min_activation_height': 0, + 'status': 'active', + 'status_next': 'active', + 'since': 0, + }, + 'height': 0, + 'active': True } } }) From 7e7b3784dd3857fa504b56fa762166e9846a0955 Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Thu, 17 Apr 2025 15:12:22 -0400 Subject: [PATCH 3/3] test: add BIP-348 coverage to feature_taproot --- test/functional/feature_taproot.py | 83 +++++++++++++++++++++++- test/functional/test_framework/key.py | 1 - test/functional/test_framework/script.py | 6 +- 3 files changed, 86 insertions(+), 4 deletions(-) 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