From c0bc5f2fec6695463007f74302940108cf3a0d5c Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 25 Mar 2026 15:10:28 +0200 Subject: [PATCH 1/2] simplicity: allow exact annex padding in policy --- src/policy/policy.cpp | 9 +++++++++ src/policy/policy.h | 3 ++- src/script/interpreter.cpp | 26 +++++++++++++++++++++++--- src/script/interpreter.h | 8 ++++++-- src/script/script_error.cpp | 2 ++ src/script/script_error.h | 1 + 6 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index b88cb53180e..60c65a86543 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -274,6 +274,7 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) // Check policy limits for Taproot spends: // - MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE limit for stack item size // - No annexes + // ELEMENTS: allow annexes for simplicity transactions if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) { // Missing witness; invalid by consensus rules if (i >= tx.witness.vtxinwit.size()) { @@ -282,8 +283,16 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) // Taproot spend (non-P2SH-wrapped, version 1, witness program size 32; see BIP 341) Span stack{tx.witness.vtxinwit[i].scriptWitness.stack}; if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) { + SpanPopBack(stack); // drop the annex + const auto& control_block = SpanPopBack(stack); + if ((control_block[0] & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSIMPLICITY) { + // Annexes are allowed for Simplicity spends + // checks for zero padding and exact size are done in CheckSimplicity + return true; + } else { // Annexes are nonstandard as long as no semantics are defined for them. return false; + } } if (stack.size() >= 2) { // Script path spend (2 or more stack elements after removing optional annex) diff --git a/src/policy/policy.h b/src/policy/policy.h index 14390dcf540..1eedb093d18 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -81,7 +81,8 @@ static constexpr unsigned int STANDARD_SCRIPT_VERIFY_FLAGS = MANDATORY_SCRIPT_VE SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION | SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS | SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE | - SCRIPT_VERIFY_SIMPLICITY; + SCRIPT_VERIFY_SIMPLICITY | + SCRIPT_VERIFY_ANNEX_PADDING; /** For convenience, standard but not mandatory verify flags. */ diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 498343432e3..dbd9ad1d1f5 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -3119,14 +3119,14 @@ uint32_t GenericTransactionSignatureChecker::GetnIn() const } template -bool GenericTransactionSignatureChecker::CheckSimplicity(const valtype& program, const valtype& witness, const rawElementsTapEnv& simplicityRawTap, int64_t budget, ScriptError* serror) const +bool GenericTransactionSignatureChecker::CheckSimplicity(const valtype& program, const valtype& witness, const rawElementsTapEnv& simplicityRawTap, int64_t minCost, int64_t budget, ScriptError* serror) const { simplicity_err error; elementsTapEnv* simplicityTapEnv = simplicity_elements_mallocTapEnv(&simplicityRawTap); assert(txdata->m_simplicity_tx_data); assert(simplicityTapEnv); - if (!simplicity_elements_execSimplicity(&error, 0, txdata->m_simplicity_tx_data.get(), nIn, simplicityTapEnv, txdata->m_hash_genesis_block.data(), 0, budget, 0, program.data(), program.size(), witness.data(), witness.size())) { + if (!simplicity_elements_execSimplicity(&error, 0, txdata->m_simplicity_tx_data.get(), nIn, simplicityTapEnv, txdata->m_hash_genesis_block.data(), minCost, budget, 0, program.data(), program.size(), witness.data(), witness.size())) { assert(!"simplicity_elements_execSimplicity internal error"); } simplicity_elements_freeTapEnv(simplicityTapEnv); @@ -3278,11 +3278,15 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, // BIP341 Taproot: 32-byte non-P2SH witness v1 program (which encodes a P2C-tweaked pubkey) if (!(flags & SCRIPT_VERIFY_TAPROOT)) return set_success(serror); if (stack.size() == 0) return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WITNESS_EMPTY); + valtype padding; if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) { // Drop annex (this is non-standard; see IsWitnessStandard) const valtype& annex = SpanPopBack(stack); execdata.m_annex_hash = (CHashWriter(SER_GETHASH, 0) << annex).GetSHA256(); execdata.m_annex_present = true; + if ((stack.back()[0] & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSIMPLICITY) { + padding = annex; + } } else { execdata.m_annex_present = false; } @@ -3322,7 +3326,23 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, simplicityRawTap.controlBlock = control.data(); simplicityRawTap.pathLen = (control.size() - TAPROOT_CONTROL_BASE_SIZE) / TAPROOT_CONTROL_NODE_SIZE; simplicityRawTap.scriptCMR = script_bytes.data(); - return checker.CheckSimplicity(simplicity_program, simplicity_witness, simplicityRawTap, budget, serror); + int64_t minCost = 0; + if ((flags & SCRIPT_VERIFY_ANNEX_PADDING) && padding.size() > 0) { + valtype zero_padding(padding.size(), 0); + zero_padding[0] = ANNEX_TAG; + if (padding != zero_padding) { + return set_error(serror, SCRIPT_ERR_SIMPLICITY_PADDING_NONZERO); + } + // Compute what the budget would have been without the padding. + // budget includes the padding cost, so subtracting this stack item won't underflow. + minCost = budget - ::GetSerializeSize(padding); + if (!zero_padding.empty()) { + // Set the minCost to what the budget would have been if the padding were one byte smaller. + zero_padding.pop_back(); + minCost += ::GetSerializeSize(zero_padding); + } + } + return checker.CheckSimplicity(simplicity_program, simplicity_witness, simplicityRawTap, minCost, budget, serror); } if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION) { return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION); diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 9bfab8650c1..386ce32f833 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -159,6 +159,10 @@ enum : uint32_t { // SCRIPT_VERIFY_SIMPLICITY = (1U << 23), + // Check exact annex padding policy for simplicity spends + // + SCRIPT_VERIFY_ANNEX_PADDING = (1U << 24), + // Constants to point to the highest flag in use. Add new flags above this line. // SCRIPT_VERIFY_END_MARKER @@ -343,7 +347,7 @@ class BaseSignatureChecker return std::numeric_limits::max(); } - virtual bool CheckSimplicity(const std::vector& witness, const std::vector& program, const rawElementsTapEnv& simplicityRawTap, int64_t budget, ScriptError* serror) const + virtual bool CheckSimplicity(const std::vector& witness, const std::vector& program, const rawElementsTapEnv& simplicityRawTap, int64_t minCost, int64_t budget, ScriptError* serror) const { return false; } @@ -391,7 +395,7 @@ class GenericTransactionSignatureChecker : public BaseSignatureChecker const PrecomputedTransactionData* GetPrecomputedTransactionData() const override; uint32_t GetnIn() const override; - bool CheckSimplicity(const std::vector& program, const std::vector& witness, const rawElementsTapEnv& simplicityRawTap, int64_t budget, ScriptError* serror) const override; + bool CheckSimplicity(const std::vector& program, const std::vector& witness, const rawElementsTapEnv& simplicityRawTap, int64_t minCost, int64_t budget, ScriptError* serror) const override; }; using TransactionSignatureChecker = GenericTransactionSignatureChecker; diff --git a/src/script/script_error.cpp b/src/script/script_error.cpp index b79c9e7b347..0b61d4e429a 100644 --- a/src/script/script_error.cpp +++ b/src/script/script_error.cpp @@ -134,6 +134,8 @@ std::string ScriptErrorString(const ScriptError serror) return "EC scalar mult verify fail"; case SCRIPT_ERR_SIMPLICITY_WRONG_LENGTH: return "Simplicity witness has incorrect length"; + case SCRIPT_ERR_SIMPLICITY_PADDING_NONZERO: + return "Simplicity annex padding must be all zeros"; case SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_RANGE: return SIMPLICITY_ERR_MSG(SIMPLICITY_ERR_DATA_OUT_OF_RANGE); case SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_ORDER: diff --git a/src/script/script_error.h b/src/script/script_error.h index 0285255e1e7..f87dbf612fe 100644 --- a/src/script/script_error.h +++ b/src/script/script_error.h @@ -98,6 +98,7 @@ typedef enum ScriptError_t /* Elements: Simplicity related errors */ SCRIPT_ERR_SIMPLICITY_WRONG_LENGTH, + SCRIPT_ERR_SIMPLICITY_PADDING_NONZERO, SCRIPT_ERR_SIMPLICITY_NOT_YET_IMPLEMENTED, SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_RANGE, SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_ORDER, From 1918260a0a841eb6ce164cea0a4f36ea1339f67a Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 25 Mar 2026 15:09:35 +0200 Subject: [PATCH 2/2] test: added exact annex padding test for simplicity spends --- src/test/transaction_tests.cpp | 1 + test/functional/mempool_annex_padding.py | 145 +++++++++++++++++++++++ test/functional/test_runner.py | 1 + 3 files changed, 147 insertions(+) create mode 100755 test/functional/mempool_annex_padding.py diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index 68e0048dc63..60cae1e49b5 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -65,6 +65,7 @@ static std::map mapFlagNames = { {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("SIMPLICITY"), (unsigned int)SCRIPT_VERIFY_SIMPLICITY}, + {std::string("ANNEX_PADDING"), (unsigned int)SCRIPT_VERIFY_ANNEX_PADDING}, }; unsigned int ParseScriptFlags(std::string strFlags) diff --git a/test/functional/mempool_annex_padding.py b/test/functional/mempool_annex_padding.py new file mode 100755 index 00000000000..288e71482b4 --- /dev/null +++ b/test/functional/mempool_annex_padding.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +from decimal import Decimal +from io import BytesIO + +from test_framework.messages import CTransaction, CTxInWitness +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class AnnexPaddingTest(BitcoinTestFramework): + """Test exact annex padding for Simplicity spends.""" + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [["-evbparams=simplicity:-1:::"]] * self.num_nodes + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self.log.info("Check that Simplicity is active") + simplicity = self.nodes[0].getdeploymentinfo()["deployments"]["simplicity"] + assert simplicity["active"] + self.generate(self.nodes[0], 101) + + utxo = self.nodes[0].listunspent()[0] + assert_equal(utxo["amount"], Decimal("50.00000000")) + fee = Decimal("0.00001000") + amount = utxo["amount"] - fee + + # this elementsregtest address is generated from the "hash loop" SimplicityHL template + addr = "ert1pzp4xccn92zvhh44z9qwh3ap3jnv677ympuaafmyv4urgfrp2lafsdap5ha" + # --- + # fn hash_counter_8(ctx: Ctx8, unused: (), byte: u8) -> Either { + # let new_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(ctx, byte); + # match jet::all_8(byte) { + # true => Left(jet::sha_256_ctx_8_finalize(new_ctx)), + # false => Right(new_ctx), + # } + # } + # fn main() { + # // Hash bytes 0x00 to 0xff + # let ctx: Ctx8 = jet::sha_256_ctx_8_init(); + # let out: Either = for_while::(ctx, ()); + # let expected: u256 = 0x40aff2e9d2d8922e47afd4648e6967497158785fbd1da870e7110266bf944880; + # assert!(jet::eq_256(expected, unwrap_left::(out))); + # } + # --- + + self.log.info("Fund the contract address") + raw = self.nodes[0].createrawtransaction([{"txid": utxo["txid"], "vout": utxo["vout"]}], [{addr: amount}, {"fee": fee}]) + signed = self.nodes[0].signrawtransactionwithwallet(raw) + assert signed["complete"] + txid = self.nodes[0].sendrawtransaction(signed["hex"]) + self.generate(self.nodes[0], 1) + + in_witness = CTxInWitness() + simplicity_witness = "" + simplicity_program = "e8144eac81081420c48a0f9128a0590da107248150b21b79b8118720e30e3e070a85a02d8370c41c920542c86e2341c920542c86e2a36e30e3f0b30e3f0e38542cc2d6371b1b8e4c39071871f038542d016c1b906839240a8590dc8a41c920542c86e489b71871f90461c7e429c2a16616b1b93a839240a8590dca441c920542c86e559b71871f93861c7e4f9c2a16616b1b96e6e3430e3f204c38fc8438542cc2d6373066e618c39071871f038542d016c1b99041c70b06aa0420507cb3650420506e678e2b5620a203801a00dc0708038980e33039001390ac5f8bdd59a0d0ed8d3bb22cb0ef50f71e3a577040de5bfe99608095e7d53356765e430b9101722c0661c40cc0e4804e4a9792a2e4b85c9a01907681901c9f03958139625e588b966172d80641e0c064072ec273005e6005cc105cc280c83c380c80e6280e6600e694273545e6a85cd605cd780c83c5006407368139b92f3722e6ec2e6f80641e30032039c3039d109ceb179d6173b0173b60320f1c81901cf004e790bcf38b9e60b9ea01907902064073d840a136940aff2e9d2d8922e47afd4648e6967497158785fbd1da870e7110266bf944880042050831061c9160366ce8867390b3cffedf1a67f35f4e6e69c39210d09fddb9189a14c225a77e6c262e1806006616b66dd008c0212283f4060201c180e180740780" + cmr = "988c6d7a1c50012028523debc8ec575ce96920c46a45f663051aa3309f6fc539" + control_block = "be50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + + self.log.info("Try to spend without no annex") + addr = self.nodes[0].getnewaddress(address_type="bech32") + fee = Decimal("0.00003000") + raw = self.nodes[0].createrawtransaction([{"txid": txid, "vout": 0}], [{addr: amount - fee}, {"fee": fee}]) + ctx = CTransaction() + ctx.deserialize(BytesIO(bytes.fromhex(raw))) + + in_witness.scriptWitness.stack = [ + bytes.fromhex(simplicity_witness), + bytes.fromhex(simplicity_program), + bytes.fromhex(cmr), + bytes.fromhex(control_block), + ] + ctx.wit.vtxinwit.append(in_witness) + raw = ctx.serialize().hex() + assert_raises_rpc_error(-26, "Program's execution cost could exceed budget", self.nodes[0].sendrawtransaction, raw) + + EXACT_PADDING = 7423 + # FIXME: investigate why rust-simplicity `Cost::get_padding` gives exact padding as 7426 + # https://github.com/BlockstreamResearch/rust-simplicity/blob/d28440bc0c6be333aa84fa441844541c14dbb563/src/analysis.rs#L148 + + self.log.info("Try to spend with non-zero padded annex") + annex = [0x50] + [0x01] * EXACT_PADDING + in_witness.scriptWitness.stack = [ + bytes.fromhex(simplicity_witness), + bytes.fromhex(simplicity_program), + bytes.fromhex(cmr), + bytes.fromhex(control_block), + bytes(annex), + ] + ctx.wit.vtxinwit[0] = in_witness + raw = ctx.serialize().hex() + assert_raises_rpc_error(-26, "Simplicity annex padding must be all zeros", self.nodes[0].sendrawtransaction, raw) + + self.log.info("Try to spend with under-padded annex") + annex = [0x50] + [0x00] * (EXACT_PADDING - 1) + in_witness.scriptWitness.stack = [ + bytes.fromhex(simplicity_witness), + bytes.fromhex(simplicity_program), + bytes.fromhex(cmr), + bytes.fromhex(control_block), + bytes(annex), + ] + ctx.wit.vtxinwit[0] = in_witness + raw = ctx.serialize().hex() + assert_raises_rpc_error(-26, "Program's execution cost could exceed budget", self.nodes[0].sendrawtransaction, raw) + + self.log.info("Try to spend with over-padded annex") + annex = [0x50] + [0x00] * (EXACT_PADDING + 1) + in_witness.scriptWitness.stack = [ + bytes.fromhex(simplicity_witness), + bytes.fromhex(simplicity_program), + bytes.fromhex(cmr), + bytes.fromhex(control_block), + bytes(annex), + ] + ctx.wit.vtxinwit[0] = in_witness + raw = ctx.serialize().hex() + assert_raises_rpc_error(-26, "Program's budget is too large", self.nodes[0].sendrawtransaction, raw) + + self.log.info("Spend with exact padded annex") + annex = [0x50] + [0x00] * EXACT_PADDING + in_witness.scriptWitness.stack = [ + bytes.fromhex(simplicity_witness), + bytes.fromhex(simplicity_program), + bytes.fromhex(cmr), + bytes.fromhex(control_block), + bytes(annex), + ] + ctx.wit.vtxinwit[0] = in_witness + raw = ctx.serialize().hex() + txid = self.nodes[0].sendrawtransaction(raw) + self.generate(self.nodes[0], 1) + tx = self.nodes[0].gettransaction(txid) + assert_equal(tx["confirmations"], 1) + + +if __name__ == "__main__": + AnnexPaddingTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 9c769bc9bb6..c49ef4976f3 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -373,6 +373,7 @@ 'feature_help.py', 'feature_shutdown.py', 'p2p_ibd_txrelay.py', + 'mempool_annex_padding.py', 'feature_blockfilterindex_prune.py' # Don't append tests at the end to avoid merge conflicts # Put them in a random line within the section that fits their approximate run-time