From 534ee54e9b5319fd89f24962362e323e03eb21d5 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Sat, 21 Oct 2023 17:56:05 -0400 Subject: [PATCH 1/4] Re-enable OP_CAT --- src/deploymentinfo.cpp | 2 ++ src/policy/policy.h | 4 ++- src/script/interpreter.cpp | 40 ++++++++++++++++++++++++----- src/script/interpreter.h | 4 +++ src/script/script_error.h | 1 + src/test/script_tests.cpp | 4 +-- test/functional/data/invalid_txs.py | 0 7 files changed, 46 insertions(+), 9 deletions(-) mode change 100644 => 100755 test/functional/data/invalid_txs.py diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index 7d5df5a7b717c..3fd012cd93d52 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -89,6 +89,8 @@ const std::map g_verify_flag_names{ FLAG_NAME(DISCOURAGE_CHECK_TEMPLATE_VERIFY_HASH) FLAG_NAME(ANYPREVOUT) FLAG_NAME(DISCOURAGE_ANYPREVOUT) + FLAG_NAME(OP_CAT) + FLAG_NAME(DISCOURAGE_OP_CAT) }; #undef FLAG_NAME diff --git a/src/policy/policy.h b/src/policy/policy.h index 4d9ea85f61539..1261d1bae9d9b 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -100,7 +100,9 @@ static constexpr unsigned int STANDARD_SCRIPT_VERIFY_FLAGS{MANDATORY_SCRIPT_VERI SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE | SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_CHECK_TEMPLATE_VERIFY_HASH | SCRIPT_VERIFY_DEFAULT_CHECK_TEMPLATE_VERIFY_HASH | - SCRIPT_VERIFY_ANYPREVOUT}; + SCRIPT_VERIFY_ANYPREVOUT | + SCRIPT_VERIFY_OP_CAT + }; /** 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/script/interpreter.cpp b/src/script/interpreter.cpp index d9e3d2facb2b3..8a733dee9b151 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -467,10 +467,16 @@ bool EvalScript(std::vector >& stack, const CScript& if (opcode > OP_16 && ++nOpCount > MAX_OPS_PER_SCRIPT) { return set_error(serror, SCRIPT_ERR_OP_COUNT); } + + // When OP_SUCCESS disabled opcodes (CVE-2010-5137) are + // redefined in tapscript, remove them from the if below + // and put them here + if (opcode == OP_CAT) { + return set_error(serror, SCRIPT_ERR_DISABLED_OPCODE); // Disabled opcodes (CVE-2010-5137). + } } - if (opcode == OP_CAT || - opcode == OP_SUBSTR || + if (opcode == OP_SUBSTR || opcode == OP_LEFT || opcode == OP_RIGHT || opcode == OP_INVERT || @@ -534,6 +540,19 @@ bool EvalScript(std::vector >& stack, const CScript& case OP_NOP: break; + case OP_CAT: + { + if (stack.size() < 2) + return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION); + valtype& vch1 = stacktop(-2); + valtype& vch2 = stacktop(-1); + if (vch1.size() + vch2.size() > MAX_SCRIPT_ELEMENT_SIZE) + return set_error(serror, SCRIPT_ERR_PUSH_SIZE); + vch1.insert(vch1.end(), vch2.begin(), vch2.end()); + stack.pop_back(); + } + break; + case OP_CHECKLOCKTIMEVERIFY: { if (!(flags & SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY)) { @@ -1963,12 +1982,21 @@ static bool ExecuteWitnessScript(const Span& stack_span, const CS // Note how this condition would not be reached if an unknown OP_SUCCESSx was found return set_error(serror, SCRIPT_ERR_BAD_OPCODE); } - // 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_CAT) { + if (flags & SCRIPT_VERIFY_DISCOURAGE_OP_CAT) { + return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_CAT); + } else if (!(flags & SCRIPT_VERIFY_OP_CAT)) { + 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 5332a3a858bed..faff0ecc30c85 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -158,6 +158,10 @@ enum : uint32_t { // Making ANYPREVOUT public key versions (in BIP 342 scripts) non-standard SCRIPT_VERIFY_DISCOURAGE_ANYPREVOUT = (1U << 25), + // Support OP_CAT in tapscript + SCRIPT_VERIFY_OP_CAT = (1U << 26), + SCRIPT_VERIFY_DISCOURAGE_OP_CAT = (1U << 27), + // 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_error.h b/src/script/script_error.h index 73d98f224da0d..83826bfc301a5 100644 --- a/src/script/script_error.h +++ b/src/script/script_error.h @@ -61,6 +61,7 @@ typedef enum ScriptError_t SCRIPT_ERR_DISCOURAGE_OP_SUCCESS, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_PUBKEYTYPE, SCRIPT_ERR_DISCOURAGE_ANYPREVOUT, + SCRIPT_ERR_DISCOURAGE_OP_CAT, /* segregated witness */ SCRIPT_ERR_WITNESS_PROGRAM_WRONG_LENGTH, diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp index db476fed2585c..0fdcd1ec2490c 100644 --- a/src/test/script_tests.cpp +++ b/src/test/script_tests.cpp @@ -1840,8 +1840,8 @@ BOOST_AUTO_TEST_CASE(formatscriptflags) // quick check that FormatScriptFlags reports any unknown/unexpected bits BOOST_CHECK_EQUAL(FormatScriptFlags(SCRIPT_VERIFY_P2SH), "P2SH"); BOOST_CHECK_EQUAL(FormatScriptFlags(SCRIPT_VERIFY_P2SH | (1u<<31)), "P2SH,0x80000000"); - BOOST_CHECK_EQUAL(FormatScriptFlags(SCRIPT_VERIFY_TAPROOT | (1u<<27)), "TAPROOT,0x08000000"); - BOOST_CHECK_EQUAL(FormatScriptFlags(1u<<26), "0x04000000"); + BOOST_CHECK_EQUAL(FormatScriptFlags(SCRIPT_VERIFY_TAPROOT | (1u<<28)), "TAPROOT,0x10000000"); + BOOST_CHECK_EQUAL(FormatScriptFlags(1u<<28), "0x10000000"); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/data/invalid_txs.py b/test/functional/data/invalid_txs.py old mode 100644 new mode 100755 From 3257a41153b13178888fc252639c0b57b29ddaca Mon Sep 17 00:00:00 2001 From: Ethan Heilman Date: Fri, 19 Jan 2024 20:54:34 -0500 Subject: [PATCH 2/4] Adds OP_CAT script_tests unittests --- src/test/data/script_tests.json | 338 ++++++++++++++++++++++++++++++++ src/test/script_tests.cpp | 100 +++++++++- 2 files changed, 436 insertions(+), 2 deletions(-) diff --git a/src/test/data/script_tests.json b/src/test/data/script_tests.json index b4b3724d713ce..36b9cf17e6967 100644 --- a/src/test/data/script_tests.json +++ b/src/test/data/script_tests.json @@ -2527,6 +2527,344 @@ ["0x050000000001", "CHECKSEQUENCEVERIFY", "CHECKSEQUENCEVERIFY", "UNSATISFIED_LOCKTIME", "CSV fails if stack top bit 1 << 31 is not set, and tx version < 2"], +["OP_CAT tests"], +[ + [ + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT", + "OK", + "TAPSCRIPT (CAT) Test of OP_CAT flag by calling CAT on an empty stack. This does not error because no TAPSCRIPT_OP_CAT flag is set so CAT is OP_SUCCESS" +], +[ + [ + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "INVALID_STACK_OPERATION", + "TAPSCRIPT Test of OP_CAT flag by calling CAT on an empty stack. This throws an error because TAPSCRIPT_OP_CAT flag is set so CAT is executed" +], +[ + [ + "aa", + "bb", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT Test of OP_CAT flag by calling CAT on two elements. TAPSCRIPT_OP_CAT flag is set so CAT is executed." +], +[ + [ + "78a11a1260c1101260", + "78a11a1260", + "c1101260", + "#SCRIPT# CAT EQUAL", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT CATs 78a11a1260 and c1101260 together and checks it is EQUAL to stack element 78a11a1260c1101260" +], +[ + [ + "78a11a1260c1101260", + "78a11a1260", + "c1101260", + "#SCRIPT# CAT EQUAL", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT", + "OK", + "TAPSCRIPT Test of OP_CAT flag, CATs 78a11a1260 and c1101260 together and checks it is EQUAL to stack element 78a11a1260c1101260. No TAPSCRIPT_OP_CAT set so CAT should be OP_SUCCESS." +], +[ + [ + "", + "78a11a1260", + "c1101260", + "#SCRIPT# CAT EQUAL", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "EVAL_FALSE", + "TAPSCRIPT CATs 78a11a1260 and c1101260 together and checks it is EQUAL to the empty stack element" +], +[ + [ + "51", + "78a11a1260c1101260", + "78a11a1260", + "c1101260", + "#SCRIPT# CAT EQUALVERIFY", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT CATS 78a11a1260 and c1101260 together and checks it is EQUALVERIFY to stack element 78a11a1260c1101260" +], +[ + [ + "51", + "c110126078a11a1260", + "78a11a1260", + "c1101260", + "#SCRIPT# CAT EQUALVERIFY", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "EQUALVERIFY", + "TAPSCRIPT CATs 78a11a1260 and c1101260 together and checks it is EQUALVERIFY to stack element c110126078a11a1260" +], +[ + [ + "aa", + "bb", + "#SCRIPT# CAT 0x4c 0x02 0xaabb EQUAL", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT CATs aa and bb together and checks EQUAL to aabb" +], +[ + [ + "eeffeeff", + "aa", + "bbff", + "#SCRIPT# CAT CAT DUP DROP 0x4c 0x07 0xeeffeeffaabbff EQUAL", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT CATs aa and bbcc and eeffeff together and checks EQUAL to eeffeeffaabbcc" +], +[ + [ + "c24f2c1e363e09a5dd56f0", + "89a0385490a11b6dc6740f3513", + "#SCRIPT# CAT 0x4c 0x18 0xc24f2c1e363e09a5dd56f089a0385490a11b6dc6740f3513 EQUAL", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT Tests CAT on different sized random stack elements and compares the result." +], +[ + [ + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93", + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT Tests CAT on two hash outputs" +], +[ + [ + "51", + "bbbb", + "01", + "#SCRIPT# IF CAT ELSE DROP ENDIF", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT Tests CAT inside of an IF ELSE conditional (true IF)" +], +[ + [ + "51", + "", + "#SCRIPT# IF CAT ELSE DROP ENDIF", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "CLEANSTACK", + "TAPSCRIPT Tests CAT inside of an IF ELSE conditional (false IF)" +], +[ + [ + "1a1a", + "#SCRIPT# DUP CAT DUP CAT DUP CAT DUP CAT DUP CAT DUP CAT DUP CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT Runs DUP CAT seven times on 1a1a" +], +[ + [ + "1a1a1a1a1a1a1a", + "#SCRIPT# DUP CAT DUP CAT DUP CAT DUP CAT DUP CAT DUP CAT DUP CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "PUSH_SIZE", + "TAPSCRIPT Runs DUP CAT seven times on 1a1a1a1a1a1a1a triggering a stack size error as the result is larger than max stack element size" +], +[ + [ + "1ffe1234567890", + "00", + "#SCRIPT# HASH256 DUP SHA1 CAT DUP CAT TOALTSTACK HASH256 DUP CAT TOALTSTACK FROMALTSTACK", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT Tests CAT with a melange of other opcodes including FROMALTSTACK. " +], +[ + [ + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "INVALID_STACK_OPERATION", + "TAPSCRIPT ([], CAT) Tests CAT fails on empty stack" +], +[ + [ + "09ca7009ca7009ca7009ca7009ca70", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "INVALID_STACK_OPERATION", + "TAPSCRIPT ([09ca7009ca7009ca7009ca7009ca70], CAT) Tests CAT fails on a stack of only one element" +], +[ + [ + "", + "09ca7009ca7009ca7009ca7009ca70", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT (['', 09ca7009ca7009ca7009ca7009ca70], CAT) Tests CAT succeeds when one of the two values to concatenate is of size zero" +], +[ + [ + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93", + "0102030405060708", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "OK", + "TAPSCRIPT ([512 byte element, 09ca7009ca7009ca7009ca7009ca70], CAT) Tests edge case where concatenated value is exactly max stack element size (520 bytes)" +], +[ + [ + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b930102030405060708", + "01", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "PUSH_SIZE", + "TAPSCRIPT ([520 byte element, 01], CAT) Tests edge case where concatenated value is one byte larger than max stack element size (520 bytes)" +], +[ + [ + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b930102030405060708", + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b930102030405060708", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "PUSH_SIZE", + "TAPSCRIPT ([520 byte element, 520 byte element], CAT) Tests case where each element to concatenate is exactly max stack element size (520 bytes)" +], +[ + [ + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b930102030405060708", + "f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93f821125522f9490bcd108cdd0effbb002d45c6e66e6b48aeb51c865743796b93010203040506070809", + "#SCRIPT# CAT", + "#CONTROLBLOCK#", + 0.00000001 + ], + "", + "0x51 0x20 #TAPROOTOUTPUT#", + "P2SH,WITNESS,TAPROOT,OP_CAT", + "PUSH_SIZE", + "TAPSCRIPT ([520 byte element, 521 byte element], CAT) Tests edge case where one of the elements to concatenate is one byte larger than max stack element size (520 bytes)" +], + ["MINIMALIF tests"], ["MINIMALIF is not applied to non-segwit scripts"], ["1", "IF 1 ENDIF", "P2SH,WITNESS,MINIMALIF", "OK"], diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp index 0fdcd1ec2490c..87ea3653bfc92 100644 --- a/src/test/script_tests.cpp +++ b/src/test/script_tests.cpp @@ -931,16 +931,34 @@ BOOST_AUTO_TEST_CASE(script_json_test) // amount (nValue) to use in the crediting tx UniValue tests = read_json(std::string(json_tests::script_tests, json_tests::script_tests + sizeof(json_tests::script_tests))); + const KeyData keys; for (unsigned int idx = 0; idx < tests.size(); idx++) { const UniValue& test = tests[idx]; std::string strTest = test.write(); CScriptWitness witness; + TaprootBuilder taprootBuilder; CAmount nValue = 0; unsigned int pos = 0; if (test.size() > 0 && test[pos].isArray()) { unsigned int i=0; for (i = 0; i < test[pos].size()-1; i++) { - witness.stack.push_back(ParseHex(test[pos][i].get_str())); + auto element = test[pos][i].get_str(); + // We use #SCRIPT# to flag a non-hex script that we can read using ParseScript + // Taproot script must be third from the last element in witness stack + std::string scriptFlag = std::string("#SCRIPT#"); + if (element.find(scriptFlag) == 0) { + CScript script = ParseScript(element.substr(scriptFlag.size())); + witness.stack.push_back(ToByteVector(script)); + } else if (strcmp(element.c_str(), "#CONTROLBLOCK#") == 0) { + // Taproot script control block - second from the last element in witness stack + // If #CONTROLBLOCK# we auto-generate the control block + taprootBuilder.Add(/*depth=*/0, witness.stack.back(), TAPROOT_LEAF_TAPSCRIPT, /*track=*/true); + taprootBuilder.Finalize(XOnlyPubKey(keys.key0.GetPubKey())); + auto controlblocks = taprootBuilder.GetSpendData().scripts[{witness.stack.back(), TAPROOT_LEAF_TAPSCRIPT}]; + witness.stack.push_back(*(controlblocks.begin())); + } else { + witness.stack.push_back(ParseHex(element)); + } } nValue = AmountFromValue(test[pos][i]); pos++; @@ -955,7 +973,14 @@ BOOST_AUTO_TEST_CASE(script_json_test) std::string scriptSigString = test[pos++].get_str(); CScript scriptSig = ParseScript(scriptSigString); std::string scriptPubKeyString = test[pos++].get_str(); - CScript scriptPubKey = ParseScript(scriptPubKeyString); + CScript scriptPubKey; + // If requested, auto-generate the taproot output + if (strcmp(scriptPubKeyString.c_str(), "0x51 0x20 #TAPROOTOUTPUT#")== 0) { + BOOST_CHECK_MESSAGE(taprootBuilder.IsComplete(), "Failed to autogenerate Tapscript Script PubKey"); + scriptPubKey = CScript() << OP_1 << ToByteVector(taprootBuilder.GetOutput()); + } else { + scriptPubKey = ParseScript(scriptPubKeyString); + } unsigned int scriptflags = ParseScriptFlags(test[pos++].get_str()); int scriptError = ParseScriptError(test[pos++].get_str()); @@ -1844,4 +1869,75 @@ BOOST_AUTO_TEST_CASE(formatscriptflags) BOOST_CHECK_EQUAL(FormatScriptFlags(1u<<28), "0x10000000"); } + +void DoTapscriptTest(std::vector witVerifyScript, std::vector> witData, const std::string& message, int scriptError) +{ + const KeyData keys; + TaprootBuilder builder; + builder.Add(/*depth=*/0, witVerifyScript, TAPROOT_LEAF_TAPSCRIPT, /*track=*/true); + builder.Finalize(XOnlyPubKey(keys.key0.GetPubKey())); + + CScriptWitness witness; + witness.stack.insert(witness.stack.begin(), witData.begin(), witData.end()); + witness.stack.push_back(witVerifyScript); + auto controlblock = *(builder.GetSpendData().scripts[{witVerifyScript, TAPROOT_LEAF_TAPSCRIPT}].begin()); + witness.stack.push_back(controlblock); + + uint32_t flags = SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_TAPROOT | SCRIPT_VERIFY_OP_CAT; + CScript scriptPubKey = CScript() << OP_1 << ToByteVector(builder.GetOutput()); + CScript scriptSig = CScript(); // Script sig is always size 0 and empty in tapscript + DoTest(scriptPubKey, scriptSig, witness, flags, message, scriptError, /*nValue=*/1); +} + +BOOST_AUTO_TEST_CASE(cat_simple) +{ + std::vector> witData; + witData.push_back(ParseHex("aa")); + witData.push_back(ParseHex("bbbb")); + std::vector witVerifyScript = {OP_CAT, OP_PUSHDATA1, 0x03, 0xaa, 0xbb, 0xbb, OP_EQUAL}; + DoTapscriptTest(witVerifyScript, witData, "Simple CAT", SCRIPT_ERR_OK); +} + + +BOOST_AUTO_TEST_CASE(cat_empty_stack) +{ + // Ensures that OP_CAT successfully handles concatenating two empty stack elements + std::vector> witData; + witData.push_back({}); + witData.push_back({}); + std::vector witVerifyScript = {OP_CAT, OP_PUSHDATA1, 0x00,OP_EQUAL}; + DoTapscriptTest(witVerifyScript, witData, "CAT empty stack", SCRIPT_ERR_OK); +} + +BOOST_AUTO_TEST_CASE(cat_dup_test) +{ + // CAT DUP exhaustion attacks using all element sizes from 1 to 522 + // with CAT DUP repetitions up to 10. Ensures the correct error is thrown + // or not thrown, as appropriate. + unsigned int maxElementSize = 522; + unsigned int maxDupsToCheck = 10; + + std::vector> witData; + witData.push_back({}); + for (unsigned int elementSize = 1; elementSize <= maxElementSize; elementSize++) { + std::vector witVerifyScript; + // increase the size of stack element by one byte + witData.at(0).push_back(0x1A); + for (unsigned int dups = 1; dups <= maxDupsToCheck; dups++) { + witVerifyScript.push_back(OP_DUP); + witVerifyScript.push_back(OP_CAT); + int expectedErr = SCRIPT_ERR_OK; + unsigned int catedStackElementSize = witData.at(0).size()< MAX_SCRIPT_ELEMENT_SIZE || elementSize > MAX_SCRIPT_ELEMENT_SIZE){ + expectedErr = SCRIPT_ERR_PUSH_SIZE; + break; + } + DoTapscriptTest(witVerifyScript, witData, "CAT DUP test", expectedErr); + // Once we hit the stack element size limit, break + if (expectedErr == SCRIPT_ERR_OK) + break; + } + } +} + BOOST_AUTO_TEST_SUITE_END() From 7c3ca0da19c2b52130ca3693900b90c9d4e73fc8 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Sat, 28 Oct 2023 15:31:45 -0400 Subject: [PATCH 3/4] OP_CAT deployment --- src/consensus/params.h | 1 + src/deploymentinfo.cpp | 4 ++++ src/kernel/chainparams.cpp | 13 +++++++++++- src/rpc/blockchain.cpp | 1 + src/validation.cpp | 27 +++++++++++++++++++----- test/functional/rpc_blockchain.py | 17 +++++++++++++-- test/functional/test_framework/script.py | 3 +++ 7 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/consensus/params.h b/src/consensus/params.h index 4566a47d7a8d8..d104fa6aed828 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -34,6 +34,7 @@ enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, DEPLOYMENT_CHECKTEMPLATEVERIFY, // Deployment of CTV (BIP 119) DEPLOYMENT_ANYPREVOUT, + DEPLOYMENT_OP_CAT, // NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp MAX_VERSION_BITS_DEPLOYMENTS }; diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index 3fd012cd93d52..7271d2bbdfe31 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -23,6 +23,10 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B /*.name =*/ "anyprevout", /*.gbt_force =*/ true, }, + { + /*.name =*/ "opcat", + /*.gbt_force =*/ true, + }, }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 8710500da9eea..26c26c41e6b1c 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -132,6 +132,7 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY] = SetupDeployment{.activate = 0x30000000, .abandon = 0, .never = true}; consensus.vDeployments[Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY] = SetupDeployment{.activate = 0x60007700, .abandon = 0x40007700, .never = true}; consensus.vDeployments[Consensus::DEPLOYMENT_ANYPREVOUT] = SetupDeployment{.activate = 0x60007600, .abandon = 0x40007600, .never = true}; + consensus.vDeployments[Consensus::DEPLOYMENT_OP_CAT] = SetupDeployment{.activate = 0x62000100, .abandon = 0x42000100, .never = true}; consensus.nMinimumChainWork = uint256S("0x000000000000000000000000000000000000000044a50fe819c39ad624021859"); consensus.defaultAssumeValid = uint256S("0x000000000000000000035c3f0d31e71a5ee24c5aaf3354689f65bd7b07dee632"); // 784000 @@ -246,6 +247,7 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY] = SetupDeployment{.activate = 0x30000000, .abandon = 0, .never = true}; consensus.vDeployments[Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY] = SetupDeployment{.activate = 0x60007700, .abandon = 0x40007700, .never = true}; consensus.vDeployments[Consensus::DEPLOYMENT_ANYPREVOUT] = SetupDeployment{.activate = 0x60007600, .abandon = 0x40007600, .never = true}; + consensus.vDeployments[Consensus::DEPLOYMENT_OP_CAT] = SetupDeployment{.activate = 0x62000100, .abandon = 0x42000100, .never = true}; consensus.nMinimumChainWork = uint256S("0x000000000000000000000000000000000000000000000977edb0244170858d07"); consensus.defaultAssumeValid = uint256S("0x0000000000000021bc50a89cde4870d4a81ffe0153b3c8de77b435a2fd3f6761"); // 2429000 @@ -394,6 +396,15 @@ class SigNetParams : public CChainParams { .activate = 0x60007600, .abandon = 0x40007600, }; + consensus.vDeployments[Consensus::DEPLOYMENT_OP_CAT] = SetupDeployment{ + .year = 2024, + .number = 1, + .revision = 0, + .start = 1704085200, // 2024-01-01 + .timeout = 2019704400, // 2034-01-01 + .activate = 0x62000100, + .abandon = 0x42000100, + }; RenounceDeployments(options.renounce, consensus.vDeployments); @@ -461,7 +472,7 @@ class CRegTestParams : public CChainParams consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY] = SetupDeployment{.start = 0, .timeout = Consensus::HereticalDeployment::NO_TIMEOUT, .activate = 0x30000000, .abandon = 0x50000000}; consensus.vDeployments[Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY] = SetupDeployment{.activate = 0x60007700, .abandon = 0x40007700, .always = true}; consensus.vDeployments[Consensus::DEPLOYMENT_ANYPREVOUT] = SetupDeployment{.activate = 0x60007600, .abandon = 0x40007600, .always = true}; - + consensus.vDeployments[Consensus::DEPLOYMENT_OP_CAT] = SetupDeployment{.activate = 0x62000100, .abandon = 0x42000100, .always = true}; consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 2d920e42f80b2..f64462fabcc7b 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1338,6 +1338,7 @@ UniValue DeploymentInfo(const CBlockIndex* blockindex, const ChainstateManager& SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TAPROOT); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_ANYPREVOUT); + SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_OP_CAT); return softforks; } } // anon namespace diff --git a/src/validation.cpp b/src/validation.cpp index 6bf67eb475c59..be28d0271eb7a 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1000,6 +1000,17 @@ bool MemPoolAccept::PackageMempoolChecks(const std::vector& txn return true; } +static unsigned int DepDiscourageFlags(const CBlockIndex* tip, const ChainstateManager& chainman, const std::vector>& depflags) +{ + unsigned int result = SCRIPT_VERIFY_NONE; + for (auto [dep, flags] : depflags) { + if (!DeploymentActiveAfter(tip, chainman, dep)) { + result |= flags; + } + } + return result; +} + bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws) { AssertLockHeld(cs_main); @@ -1007,11 +1018,12 @@ bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws) const CTransaction& tx = *ws.m_ptx; TxValidationState& state = ws.m_state; - const bool ctv_active = DeploymentActiveAfter(m_active_chainstate.m_chain.Tip(), m_active_chainstate.m_chainman, Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY); - const bool apo_active = DeploymentActiveAfter(m_active_chainstate.m_chain.Tip(), m_active_chainstate.m_chainman, Consensus::DEPLOYMENT_ANYPREVOUT); - const unsigned int scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS - | (ctv_active ? SCRIPT_VERIFY_NONE : SCRIPT_VERIFY_DISCOURAGE_CHECK_TEMPLATE_VERIFY_HASH) - | (apo_active ? SCRIPT_VERIFY_NONE : SCRIPT_VERIFY_DISCOURAGE_ANYPREVOUT); + const unsigned int scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS | + DepDiscourageFlags(m_active_chainstate.m_chain.Tip(), m_active_chainstate.m_chainman, { + { Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY, SCRIPT_VERIFY_DISCOURAGE_CHECK_TEMPLATE_VERIFY_HASH }, + { Consensus::DEPLOYMENT_ANYPREVOUT, SCRIPT_VERIFY_DISCOURAGE_ANYPREVOUT }, + { Consensus::DEPLOYMENT_OP_CAT, SCRIPT_VERIFY_DISCOURAGE_OP_CAT }, + }); // Check input scripts and signatures. // This is done last to help prevent CPU exhaustion denial-of-service attacks. @@ -2017,6 +2029,11 @@ unsigned int GetBlockScriptFlags(const CBlockIndex& block_index, const Chainstat flags |= SCRIPT_VERIFY_ANYPREVOUT; } + // Enforce OP_CAT + if (DeploymentActiveAt(block_index, chainman, Consensus::DEPLOYMENT_OP_CAT)) { + flags |= SCRIPT_VERIFY_OP_CAT; + } + return flags; } diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index e7c223e8b1fc7..47cae42422b69 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -193,11 +193,10 @@ def _test_getblockchaininfo(self): def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash): assert height >= 144 and height <= 287 - assert_equal(gdi_result, { "hash": blockhash, "height": height, - "script_flags": "ANYPREVOUT,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,DEFAULT_CHECK_TEMPLATE_VERIFY_HASH,DERSIG,NULLDUMMY,P2SH,TAPROOT,WITNESS", + "script_flags": "ANYPREVOUT,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,DEFAULT_CHECK_TEMPLATE_VERIFY_HASH,DERSIG,NULLDUMMY,OP_CAT,P2SH,TAPROOT,WITNESS", "deployments": { 'bip34': {'type': 'buried', 'active': True, 'height': 2}, 'bip66': {'type': 'buried', 'active': True, 'height': 3}, @@ -248,6 +247,20 @@ def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash): 'active': True, 'height': 0, }, + 'opcat': { + 'type': 'heretical', + 'heretical': { + 'binana-id': "BIN-2024-0001-000", + 'start_time': -1, + 'timeout': 9223372036854775807, + 'period': 144, + 'status': 'active', + 'since': 0, + 'status_next': 'active' + }, + 'height': 0, + 'active': True, + }, } }) diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 2650b4d37912e..e599f7acfe866 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -935,4 +935,7 @@ def taproot_construct(pubkey, scripts=None, *, keyver=None, treat_internal_as_in return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked, keyver) def is_op_success(o): + # assume OP_CAT is activated in tests + if o == OP_CAT: + 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 e51aa648403450d7fb7d58abee9c8b8cb54129d3 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 9 Apr 2024 21:25:41 -0400 Subject: [PATCH 4/4] functional tests for OP_CAT --- test/functional/feature_opcat.py | 342 +++++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 343 insertions(+) create mode 100755 test/functional/feature_opcat.py diff --git a/test/functional/feature_opcat.py b/test/functional/feature_opcat.py new file mode 100755 index 0000000000000..beda51eb81ae0 --- /dev/null +++ b/test/functional/feature_opcat.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2024 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test (OP_CAT) +""" + +from test_framework.blocktools import ( + create_coinbase, + create_block, + add_witness_commitment, +) +from test_framework.messages import ( + CTransaction, + CTxOut, + CTxIn, + CTxInWitness, + COutPoint, + COIN, + sha256, +) +from test_framework.p2p import P2PInterface +from test_framework.script import ( + CScript, + OP_CAT, + OP_EQUAL, + OP_2, + OP_DUP, + taproot_construct, +) +from test_framework.script_util import script_to_p2sh_script +from test_framework.key import ECKey, compute_xonly_pubkey +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.wallet import MiniWallet, MiniWalletMode +from decimal import Decimal +import random +from io import BytesIO +from test_framework.address import script_to_p2sh + +DISCOURAGED_ERROR = ( + "non-mandatory-script-verify-flag (NOPx reserved for soft-fork upgrades)" +) +STACK_TOO_SHORT_ERROR = ( + "non-mandatory-script-verify-flag (Operation not valid with the current stack size)" +) +DISABLED_OP_CODE = ( + "non-mandatory-script-verify-flag (Attempted to use a disabled opcode)" +) +MAX_PUSH_ERROR = ( + "non-mandatory-script-verify-flag (Push value size limit exceeded)" +) + +def random_bytes(n): + return bytes(random.getrandbits(8) for i in range(n)) + + +def random_p2sh(): + return script_to_p2sh_script(random_bytes(20)) + + +def create_transaction_to_script(node, wallet, txid, script, *, amount_sats): + """Return signed transaction spending the first output of the + input txid. Note that the node must be able to sign for the + output that is being spent, and the node must not be running + multiple wallets. + """ + random_address = script_to_p2sh(CScript()) + output = wallet.get_utxo(txid=txid) + rawtx = node.createrawtransaction( + inputs=[{"txid": output["txid"], "vout": output["vout"]}], + outputs={random_address: Decimal(amount_sats) / COIN}, + ) + tx = CTransaction() + tx.deserialize(BytesIO(bytes.fromhex(rawtx))) + # Replace with our script + tx.vout[0].scriptPubKey = script + # Sign + wallet.sign_tx(tx) + return tx + + +class CatTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [ + ["-par=1"] + ] # Use only one script thread to get the exact reject reason for testing + self.setup_clean_chain = True + self.rpc_timeout = 120 + + def get_block(self, txs): + self.tip = self.nodes[0].getbestblockhash() + self.height = self.nodes[0].getblockcount() + self.log.debug(self.height) + block = create_block( + int(self.tip, 16), create_coinbase(self.height + 1)) + block.vtx.extend(txs) + add_witness_commitment(block) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + return block.serialize(True).hex(), block.hash + + def add_block(self, txs): + block, h = self.get_block(txs) + reason = self.nodes[0].submitblock(block) + if reason: + self.log.debug("Reject Reason: [%s]", reason) + assert_equal(self.nodes[0].getbestblockhash(), h) + return h + + def run_test(self): + # The goal is to test a number of circumstances and combinations of parameters. Roughly: + # + # - Taproot OP_CAT + # - CAT should fail when stack limit is hit + # - CAT should fail if there is insuffecient number of element on the stack + # - CAT should be able to concatenate two 8 byte payloads and check the resulting 16 byte payload + # - Segwit v0 OP_CAT + # - Spend should fail due to using disabled opcodes + + wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK) + self.nodes[0].add_p2p_connection(P2PInterface()) + + BLOCKS = 200 + self.log.info("Mining %d blocks for mature coinbases", BLOCKS) + # Drop the last 100 as they're unspendable! + coinbase_txids = [ + self.nodes[0].getblock(b)["tx"][0] + for b in self.generate(wallet, BLOCKS)[:-100] + ] + def get_coinbase(): return coinbase_txids.pop() + self.log.info("Creating setup transactions") + + outputs = [CTxOut(i * 1000, random_p2sh()) for i in range(1, 11)] + # Add some fee satoshis + amount_sats = sum(out.nValue for out in outputs) + 200 * 500 + + self.log.info( + "Creating funding txn for 10 random outputs as a Taproot script") + private_key = ECKey() + # use simple deterministic private key (k=1) + private_key.set((1).to_bytes(32, "big"), False) + assert private_key.is_valid + public_key, _ = compute_xonly_pubkey(private_key.get_bytes()) + + self.log.info( + "Creating CAT tx with not enough values on the stack") + not_enough_stack_elements_script = CScript([OP_CAT, OP_EQUAL, OP_2]) + taproot_not_enough_stack_elements = taproot_construct( + public_key, [("only-path", not_enough_stack_elements_script, 0xC0)]) + taproot_not_enough_stack_elements_funding_tx = create_transaction_to_script( + self.nodes[0], + wallet, + get_coinbase(), + taproot_not_enough_stack_elements.scriptPubKey, + amount_sats=amount_sats, + ) + + self.log.info( + "Creating CAT tx that exceeds the stack element limit size") + # Convert hex value to bytes + hex_bytes = bytes.fromhex(('00' * 8)) + stack_limit_script = CScript( + [ + hex_bytes, + OP_DUP, + OP_CAT, + # 16 bytes on the stack + OP_DUP, + OP_CAT, + # 32 bytes on the stack + OP_DUP, + OP_CAT, + # 64 bytes on the stack + OP_DUP, + OP_CAT, + # 128 bytes on the stack + OP_DUP, + OP_CAT, + # 256 bytes on the stack + OP_DUP, + OP_CAT, + # 512 bytes on the stack + OP_DUP, + OP_CAT, + ]) + + taproot_stack_limit = taproot_construct( + public_key, [("only-path", stack_limit_script, 0xC0)]) + taproot_stack_limit_funding_tx = create_transaction_to_script( + self.nodes[0], + wallet, + get_coinbase(), + taproot_stack_limit.scriptPubKey, + amount_sats=amount_sats, + ) + self.log.info( + "Creating CAT tx that concatenates to values and verifies") + hex_value_verify = bytes.fromhex('00' * 16) + op_cat_verify_script = CScript([ + hex_bytes, + OP_DUP, + OP_CAT, + hex_value_verify, + OP_EQUAL, + ]) + + taproot_op_cat = taproot_construct( + public_key, [("only-path", op_cat_verify_script, 0xC0)]) + taproot_op_cat_funding_tx = create_transaction_to_script( + self.nodes[0], + wallet, + get_coinbase(), + taproot_op_cat.scriptPubKey, + amount_sats=amount_sats, + ) + + self.log.info("Creating a CAT segwit funding tx") + segwit_cat_funding_tx = create_transaction_to_script( + self.nodes[0], + wallet, + get_coinbase(), + CScript([0, sha256(op_cat_verify_script)]), + amount_sats=amount_sats, + ) + + funding_txs = [ + taproot_not_enough_stack_elements_funding_tx, + taproot_stack_limit_funding_tx, + taproot_op_cat_funding_tx, + segwit_cat_funding_tx, + ] + self.log.info("Obtaining TXIDs") + ( + taproot_not_enough_stack_elements_outpoint, + taproot_stack_limit_outpoint, + taproot_op_cat_outpoint, + segwit_op_cat_outpoint, + ) = [COutPoint(int(tx.rehash(), 16), 0) for tx in funding_txs] + + self.log.info("Funding all outputs") + self.add_block(funding_txs) + + self.log.info("Testing Taproot not enough stack elements OP_CAT spend") + # Test sendrawtransaction + taproot_op_cat_not_enough_stack_elements_spend = CTransaction() + taproot_op_cat_not_enough_stack_elements_spend.nVersion = 2 + taproot_op_cat_not_enough_stack_elements_spend.vin = [ + CTxIn(taproot_not_enough_stack_elements_outpoint)] + taproot_op_cat_not_enough_stack_elements_spend.vout = outputs + taproot_op_cat_not_enough_stack_elements_spend.wit.vtxinwit += [ + CTxInWitness()] + taproot_op_cat_not_enough_stack_elements_spend.wit.vtxinwit[0].scriptWitness.stack = [ + not_enough_stack_elements_script, + bytes([0xC0 + taproot_not_enough_stack_elements.negflag]) + + taproot_not_enough_stack_elements.internal_pubkey, + ] + + assert_raises_rpc_error( + -26, + STACK_TOO_SHORT_ERROR, + self.nodes[0].sendrawtransaction, + taproot_op_cat_not_enough_stack_elements_spend.serialize().hex(), + ) + self.log.info( + "OP_CAT with wrong size stack rejected by sendrawtransaction as discouraged" + ) + + self.log.info("Testing Taproot tx with stack element size limit") + taproot_op_cat_stack_limit_spend = CTransaction() + taproot_op_cat_stack_limit_spend.nVersion = 2 + taproot_op_cat_stack_limit_spend.vin = [ + CTxIn(taproot_stack_limit_outpoint)] + taproot_op_cat_stack_limit_spend.vout = outputs + taproot_op_cat_stack_limit_spend.wit.vtxinwit += [ + CTxInWitness()] + taproot_op_cat_stack_limit_spend.wit.vtxinwit[0].scriptWitness.stack = [ + stack_limit_script, + bytes([0xC0 + taproot_stack_limit.negflag]) + + taproot_stack_limit.internal_pubkey, + ] + + assert_raises_rpc_error( + -26, + MAX_PUSH_ERROR, + self.nodes[0].sendrawtransaction, + taproot_op_cat_stack_limit_spend.serialize().hex(), + ) + self.log.info( + "OP_CAT with stack size limit rejected by sendrawtransaction as discouraged" + ) + + self.log.info("Testing Taproot OP_CAT usage") + taproot_op_cat_transaction = CTransaction() + taproot_op_cat_transaction.nVersion = 2 + taproot_op_cat_transaction.vin = [ + CTxIn(taproot_op_cat_outpoint)] + taproot_op_cat_transaction.vout = outputs + taproot_op_cat_transaction.wit.vtxinwit += [ + CTxInWitness()] + taproot_op_cat_transaction.wit.vtxinwit[0].scriptWitness.stack = [ + op_cat_verify_script, + bytes([0xC0 + taproot_op_cat.negflag]) + + taproot_op_cat.internal_pubkey, + ] + + assert_equal( + self.nodes[0].sendrawtransaction( + taproot_op_cat_transaction.serialize().hex()), + taproot_op_cat_transaction.rehash(), + ) + self.log.info( + "Taproot OP_CAT verify spend accepted by sendrawtransaction" + ) + self.add_block([taproot_op_cat_transaction]) + + self.log.info("Testing Segwitv0 CAT usage") + segwitv0_op_cat_transaction = CTransaction() + segwitv0_op_cat_transaction.nVersion = 2 + segwitv0_op_cat_transaction.vin = [ + CTxIn(segwit_op_cat_outpoint)] + segwitv0_op_cat_transaction.vout = outputs + segwitv0_op_cat_transaction.wit.vtxinwit += [ + CTxInWitness()] + segwitv0_op_cat_transaction.wit.vtxinwit[0].scriptWitness.stack = [ + op_cat_verify_script, + ] + + assert_raises_rpc_error( + -26, + DISABLED_OP_CODE, + self.nodes[0].sendrawtransaction, + segwitv0_op_cat_transaction.serialize().hex(), + ) + self.log.info( + "allowed by consensus, disallowed by relay policy" + ) + + +if __name__ == "__main__": + CatTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 052650a754521..af34fec5c1ca0 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -266,6 +266,7 @@ 'p2p_initial_headers_sync.py', 'feature_nulldummy.py', 'feature_checktemplateverify.py', + 'feature_opcat.py', 'mempool_accept.py', 'mempool_expiry.py', 'wallet_import_with_label.py --legacy-wallet',