From 90c380f9a4addec6ac0ee6c65784cf2593ee1eb6 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Fri, 13 Mar 2026 14:59:35 +0200 Subject: [PATCH 1/3] upstream: simplicity_elements_computeCostBound --- src/simplicity/elements-sources.mk | 2 + src/simplicity/elements/cost.c | 65 +++++++++++++++++++ .../include/simplicity/elements/cost.h | 22 +++++++ 3 files changed, 89 insertions(+) create mode 100644 src/simplicity/elements/cost.c create mode 100644 src/simplicity/include/simplicity/elements/cost.h diff --git a/src/simplicity/elements-sources.mk b/src/simplicity/elements-sources.mk index 1c0dc4b9a9b..a03c84cee06 100644 --- a/src/simplicity/elements-sources.mk +++ b/src/simplicity/elements-sources.mk @@ -7,6 +7,7 @@ ELEMENTS_SIMPLICITY_INCLUDE_DIR_INT = %reldir%/include ELEMENTS_SIMPLICITY_DIST_HEADERS_INT = ELEMENTS_SIMPLICITY_DIST_HEADERS_INT += %reldir%/include/simplicity/errorCodes.h ELEMENTS_SIMPLICITY_DIST_HEADERS_INT += %reldir%/include/simplicity/elements/cmr.h +ELEMENTS_SIMPLICITY_DIST_HEADERS_INT += %reldir%/include/simplicity/elements/cost.h ELEMENTS_SIMPLICITY_DIST_HEADERS_INT += %reldir%/include/simplicity/elements/env.h ELEMENTS_SIMPLICITY_DIST_HEADERS_INT += %reldir%/include/simplicity/elements/exec.h @@ -24,6 +25,7 @@ ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/type.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/typeInference.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/elements/cmr.c +ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/elements/cost.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/elements/env.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/elements/exec.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/elements/elementsJets.c diff --git a/src/simplicity/elements/cost.c b/src/simplicity/elements/cost.c new file mode 100644 index 00000000000..ea882464d16 --- /dev/null +++ b/src/simplicity/elements/cost.c @@ -0,0 +1,65 @@ +#include + +#include "../bounded.h" +#include "../deserialize.h" +#include "../eval.h" +#include "../limitations.h" +#include "../simplicity_alloc.h" +#include "../simplicity_assert.h" +#include "../typeInference.h" +#include "primitive.h" + +/* Deserialize a Simplicity 'program', perform type inference, and compute its cost bound. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned. + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If the operation completes successfully then '*error' is set to 'SIMPLICITY_NO_ERROR', + * and '*cost_bound' is set to the program's cost bound in milli weight units. + * + * Precondition: NULL != error; + * NULL != cost_bound; + * unsigned char program[program_len] + */ +bool simplicity_elements_computeCostBound(simplicity_err* error, ubounded* cost_bound , const unsigned char* program, size_t program_len) { + simplicity_assert(NULL != error); + simplicity_assert(NULL != cost_bound); + simplicity_assert(NULL != program || 0 == program_len); + + dag_node* dag = NULL; + type* type_dag = NULL; + combinator_counters census; + + bitstream stream = initializeBitstream(program, program_len); + int_fast32_t dag_len = simplicity_decodeMallocDag(&dag, simplicity_elements_decodeJet, &census, &stream); + if (dag_len <= 0) { + simplicity_assert(dag_len < 0); + *error = (simplicity_err)dag_len; + simplicity_free(dag); + return IS_PERMANENT(*error); + } + + simplicity_assert(NULL != dag); + simplicity_assert((uint_fast32_t)dag_len <= DAG_LEN_MAX); + *error = simplicity_closeBitstream(&stream); + if (!IS_OK(*error)) { + simplicity_free(dag); + return IS_PERMANENT(*error); + } + + *error = simplicity_mallocTypeInference(&type_dag, simplicity_elements_mallocBoundVars, dag, dag_len, &census); + if (!IS_OK(*error)) { + simplicity_free(type_dag); + simplicity_free(dag); + return IS_PERMANENT(*error); + } + + simplicity_assert(NULL != type_dag); + + ubounded cellsBound, UWORDBound, frameBound; + *error = simplicity_analyseBounds(&cellsBound, &UWORDBound, &frameBound, cost_bound, UBOUNDED_MAX, 0, UBOUNDED_MAX, dag, type_dag, dag_len); + + simplicity_free(type_dag); + simplicity_free(dag); + return IS_PERMANENT(*error); +} diff --git a/src/simplicity/include/simplicity/elements/cost.h b/src/simplicity/include/simplicity/elements/cost.h new file mode 100644 index 00000000000..30121961e11 --- /dev/null +++ b/src/simplicity/include/simplicity/elements/cost.h @@ -0,0 +1,22 @@ +#ifndef SIMPLICITY_ELEMENTS_COST_H +#define SIMPLICITY_ELEMENTS_COST_H + +#include +#include +#include +#include + +/* Deserialize a Simplicity 'program', perform type inference, and compute its cost bound. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned. + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If the operation completes successfully then '*error' is set to 'SIMPLICITY_NO_ERROR', + * and '*cost_bound' is set to the program's cost bound in milli weight units. + * + * Precondition: NULL != error; + * NULL != cost_bound; + * unsigned char program[program_len] + */ +extern bool simplicity_elements_computeCostBound(simplicity_err* error, uint_least32_t* cost_bound, const unsigned char* program, size_t program_len); +#endif From 5bbc40fafd96e59c52472ec3c9b58b5990480579 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Fri, 13 Mar 2026 14:59:59 +0200 Subject: [PATCH 2/3] test: added exact annex padding test for simplicity spends --- test/functional/mempool_annex_padding.py | 144 +++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 145 insertions(+) create mode 100755 test/functional/mempool_annex_padding.py diff --git a/test/functional/mempool_annex_padding.py b/test/functional/mempool_annex_padding.py new file mode 100755 index 00000000000..bc3f1cdbedf --- /dev/null +++ b/test/functional/mempool_annex_padding.py @@ -0,0 +1,144 @@ +#!/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 generation and spending of P2TR address outputs.""" + + 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 Cost::get_padding indicates exact padding is 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, "bad-annex-nonzero-padding", 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, "bad-annex-padding-size", 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, "bad-annex-padding-size", 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) + + +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 From 4a9b55e9dae8596590f92cde08354afa696b118f Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Fri, 13 Mar 2026 15:07:43 +0200 Subject: [PATCH 3/3] policy: add checks for exact annex padding of simplicity programs --- src/policy/policy.cpp | 104 ++++++++++++++++++++++++++++++++++++++++++ src/policy/policy.h | 7 +++ src/validation.cpp | 6 +++ 3 files changed, 117 insertions(+) diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index b88cb53180e..b83e7c4c78a 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -14,6 +14,12 @@ #include #include // Peg-out enforcement +extern "C" { +#include +#include +#include +} + // ELEMENTS: CAsset policyAsset; @@ -274,6 +280,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 +289,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); // Ignore 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 ExactAnnexPadding + 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) @@ -340,3 +355,92 @@ int64_t GetVirtualTransactionInputSize(const CTransaction& tx, const size_t nIn, { return GetVirtualTransactionSize(GetTransactionInputWeight(tx, nIn), nSigOpCost, bytes_per_sigop); } + +bool ExactAnnexPadding(const CTransaction& tx, const CCoinsViewCache& mapInputs, std::string& reason) +{ + if (tx.IsCoinBase()) + return true; + + for (unsigned int i = 0; i < tx.vin.size(); i++) + { + if (tx.witness.vtxinwit.size() <= i || tx.witness.vtxinwit[i].scriptWitness.IsNull()) + continue; + + const CTxOut &prev = tx.vin[i].m_is_pegin ? GetPeginOutputFromWitness(tx.witness.vtxinwit[i].m_pegin_witness) : mapInputs.AccessCoin(tx.vin[i].prevout).out; + + CScript prevScript = prev.scriptPubKey; + + // Skip P2SH + if (prevScript.IsPayToScriptHash()) + continue; + + int witnessversion = 0; + std::vector witnessprogram; + if (!prevScript.IsWitnessProgram(witnessversion, witnessprogram)) + continue; + + // Only check taproot v1 spends + if (witnessversion != 1 || witnessprogram.size() != WITNESS_V1_TAPROOT_SIZE) + continue; + + Span stack{tx.witness.vtxinwit[i].scriptWitness.stack}; + + // Check for annex + if (stack.size() < 2 || stack.back().empty() || stack.back()[0] != ANNEX_TAG) + continue; + + const auto& annex = SpanPopBack(stack); + + // Need at least 2 more elements (control block + script) for script path spend + if (stack.size() < 2) + continue; + + const auto& control_block = SpanPopBack(stack); + if (control_block.empty()) + continue; + + // Only check Simplicity spends (leaf version 0xbe) + if ((control_block[0] & TAPROOT_LEAF_MASK) != TAPROOT_LEAF_TAPSIMPLICITY) + continue; + + // All annex bytes after 0x50 tag must be zero + std::vector zero_padding(annex.size(), 0); + zero_padding[0] = ANNEX_TAG; + if (annex != zero_padding) { + reason = "bad-annex-nonzero-padding"; + return false; + } + + // Annex padding must provide exactly the right budget for the program's cost bound. + // The program is the second-to-last stack item (after popping CMR script and control block). + if (stack.size() < 1) + continue; + + SpanPopBack(stack); // drop the CMR + if (stack.size() < 1) + continue; + + const auto& simplicity_program = SpanPopBack(stack); + + // Compute the program's cost bound (in milli weight units) + simplicity_err error; + ubounded cost_bound; + if (!simplicity_elements_computeCostBound(&error, &cost_bound, simplicity_program.data(), simplicity_program.size())) { + reason = "bad-annex-compute-cost"; + return false; + } + + // Budget = serialized witness size + VALIDATION_WEIGHT_OFFSET + // cost_bound is in milliWU, budget is in WU + // For exact padding: budget must equal ceil(cost_bound / 1000) + int64_t required_budget = ((int64_t)cost_bound + 999) / 1000; + int64_t actual_budget = ::GetSerializeSize(tx.witness.vtxinwit[i].scriptWitness.stack, PROTOCOL_VERSION) + VALIDATION_WEIGHT_OFFSET; + + if (actual_budget != required_budget) { + reason = "bad-annex-padding-size"; + return false; + } + } + + return true; +} diff --git a/src/policy/policy.h b/src/policy/policy.h index 14390dcf540..897e93a08cf 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -134,6 +134,13 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) */ bool IsIssuanceInMoneyRange(const CTransaction& tx); +/** +* Check that Simplicity transactions have exact annex padding: +* - Annex bytes (after 0x50 tag) must all be zero +* - Annex must be exactly the right size for the program's cost bound +*/ +bool ExactAnnexPadding(const CTransaction& tx, const CCoinsViewCache& mapInputs, std::string& reason); + /** Compute the virtual transaction size (weight reinterpreted as bytes). */ int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop); int64_t GetVirtualTransactionSize(const CTransaction& tx, int64_t nSigOpCost, unsigned int bytes_per_sigop); diff --git a/src/validation.cpp b/src/validation.cpp index 1e0fa0d32a9..7397de5dd19 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1016,6 +1016,12 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) if (tx.HasWitness() && fRequireStandard && !IsWitnessStandard(tx, m_view)) return state.Invalid(TxValidationResult::TX_WITNESS_MUTATED, "bad-witness-nonstandard"); + // Check Simplicity transactions for exact annex padding + std::string annex_reason; + if (tx.HasWitness() && fRequireStandard && !ExactAnnexPadding(tx, m_view, annex_reason)) { + return state.Invalid(TxValidationResult::TX_NOT_STANDARD, annex_reason); + } + int64_t nSigOpsCost = GetTransactionSigOpCost(tx, m_view, STANDARD_SCRIPT_VERIFY_FLAGS); // We only consider policyAsset