From 4582579b43af64e52504070ac47c019bb693e610 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 20:49:11 -0500 Subject: [PATCH 01/14] policy: add push-only witness filters and per-input v1 witness limit; expose -datacarrierwitnesslimit and -v1perinputwitnesslimit --- src/init.cpp | 5 +++++ src/kernel/mempool_options.h | 3 +++ src/node/mempool_args.cpp | 4 ++++ src/policy/policy.cpp | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/init.cpp b/src/init.cpp index 454f126298c54..8801d51413520 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -856,6 +856,11 @@ void InitParameterInteraction(ArgsManager& args) args.SoftSetArg("-datacarriercost", "0.25"); args.SoftSetArg("-datacarrierfullcount", "0"); args.SoftSetArg("-datacarriersize", "83"); + } + + { + argsman.AddArg("-datacarrierwitnesslimit", "Set maximum bytes allowed in any single push-only witness element for policy (default: 80)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-v1perinputwitnesslimit", "Set maximum total witness bytes per segwit v1 input for policy (default: 1024)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); args.SoftSetArg("-maxtxlegacysigops", strprintf("%s", std::numeric_limits::max())); args.SoftSetArg("-maxscriptsize", strprintf("%s", std::numeric_limits::max())); args.SoftSetArg("-mempooltruc", "enforce"); diff --git a/src/kernel/mempool_options.h b/src/kernel/mempool_options.h index f6a37e958c0b9..1e4f68eeb2f07 100644 --- a/src/kernel/mempool_options.h +++ b/src/kernel/mempool_options.h @@ -95,6 +95,9 @@ struct MemPoolOptions { bool permitephemeral_send{DEFAULT_PERMITEPHEMERAL_SEND}; bool permitephemeral_dust{DEFAULT_PERMITEPHEMERAL_DUST}; bool persist_v1_dat{DEFAULT_PERSIST_V1_DAT}; + // Policy to limit inscription-like data in witnesses/tapscripts + unsigned int policy_max_pushonly_witness_elem{80}; + unsigned int policy_max_v1_perinput_witness{1024}; MemPoolLimits limits{}; ValidationSignals* signals{nullptr}; diff --git a/src/node/mempool_args.cpp b/src/node/mempool_args.cpp index 1d82d8f4426fb..399000c9824ed 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -307,6 +307,10 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP mempool_opts.persist_v1_dat = argsman.GetBoolArg("-persistmempoolv1", mempool_opts.persist_v1_dat); + // Policy flags to constrain push-only witness elements and per-input size for segwit v1 + mempool_opts.policy_max_pushonly_witness_elem = argsman.GetIntArg("-datacarrierwitnesslimit", mempool_opts.policy_max_pushonly_witness_elem); + mempool_opts.policy_max_v1_perinput_witness = argsman.GetIntArg("-v1perinputwitnesslimit", mempool_opts.policy_max_v1_perinput_witness); + ApplyArgsManOptions(argsman, mempool_opts.limits); return {}; diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index f1a4bc5bc13c3..9c332c8661f41 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -422,6 +422,12 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, // - No annexes if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) { // Taproot spend (non-P2SH-wrapped, version 1, witness program size 32; see BIP 341) + // Policy: per-input total witness size budget to prevent data-splitting across elements + size_t per_input_bytes_total{0}; + for (const auto& elem : tx.vin[i].scriptWitness.stack) per_input_bytes_total += elem.size(); + if (per_input_bytes_total > 1024) { + MaybeReject("taproot-perinput-witness"); + } Span stack{tx.vin[i].scriptWitness.stack}; if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) { // Annexes are nonstandard as long as no semantics are defined for them. @@ -432,7 +438,7 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, if (stack.size() >= 2) { // Script path spend (2 or more stack elements after removing optional annex) const auto& control_block = SpanPopBack(stack); - SpanPopBack(stack); // Ignore script + const auto& script_bytes = SpanPopBack(stack); // revealed leaf script if (control_block.empty()) { // Empty control block is invalid out_reason = reason_prefix + "taproot-control-missing"; @@ -448,6 +454,32 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, } } } + // Policy: reject large push-only witness elements (any version), checked here for v1 path for efficiency + for (const auto& item : stack) { + if (item.size() > 0) { + // Attempt to parse as push-only element + size_t j = 0, n = item.size(); + uint64_t pushed_sum = 0; + bool push_only = true; + while (j < n) { + uint8_t op = item[j++]; + size_t push_len = 0; + if (op >= 0x01 && op <= 0x4b) { push_len = op; } + else if (op == OP_PUSHDATA1) { if (j >= n) { push_only = false; break; } push_len = item[j++]; } + else if (op == OP_PUSHDATA2) { if (j + 1 >= n) { push_only = false; break; } push_len = item[j] | (item[j+1] << 8); j += 2; } + else if (op == OP_PUSHDATA4) { if (j + 3 >= n) { push_only = false; break; } push_len = item[j] | (item[j+1] << 8) | (item[j+2] << 16) | (item[j+3] << 24); j += 4; } + else { push_only = false; break; } + if (j + push_len > n) { push_only = false; break; } + pushed_sum += push_len; + j += push_len; + } + if (push_only) { + if (pushed_sum > MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE) { // 80 bytes + MaybeReject("taproot-pushonly-witness-elem"); + } + } + } + } } } else if (stack.size() == 1) { // Key path spend (1 stack element after removing optional annex) From 4ab41d552d664396cb20a765648251eb819cb6e6 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 20:55:14 -0500 Subject: [PATCH 02/14] init: register -datacarrierwitnesslimit and -v1perinputwitnesslimit in SetupServerArgs; remove stray scoped block in InitParameterInteraction --- src/init.cpp | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 8801d51413520..a06ec4a6dc574 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -717,6 +717,8 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) strprintf("Maximum size of data in data carrier transactions we relay and mine, in bytes (default: %u)", MAX_OP_RETURN_RELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-datacarrierwitnesslimit", "Set maximum bytes allowed in any single push-only witness element for policy (default: 80)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-v1perinputwitnesslimit", "Set maximum total witness bytes per segwit v1 input for policy (default: 1024)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-maxscriptsize", strprintf("Maximum size of scripts (including the entire witness stack) we relay and mine, in bytes (default: %s)", DEFAULT_SCRIPT_SIZE_POLICY_LIMIT), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-maxtxlegacysigops", strprintf("Maximum number of legacy sigops allowed in transactions we relay and mine, as measured by BIP54 (default: %s)", @@ -858,18 +860,7 @@ void InitParameterInteraction(ArgsManager& args) args.SoftSetArg("-datacarriersize", "83"); } - { - argsman.AddArg("-datacarrierwitnesslimit", "Set maximum bytes allowed in any single push-only witness element for policy (default: 80)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); - argsman.AddArg("-v1perinputwitnesslimit", "Set maximum total witness bytes per segwit v1 input for policy (default: 1024)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); - args.SoftSetArg("-maxtxlegacysigops", strprintf("%s", std::numeric_limits::max())); - args.SoftSetArg("-maxscriptsize", strprintf("%s", std::numeric_limits::max())); - args.SoftSetArg("-mempooltruc", "enforce"); - args.SoftSetArg("-permitephemeral", "anchor,send,dust"); - args.SoftSetArg("-spkreuse", "allow"); - args.SoftSetArg("-blockprioritysize", "0"); - args.SoftSetArg("-blockmaxsize", "4000000"); - args.SoftSetArg("-blockmaxweight", "4000000"); - } + // when specifying an explicit binding address, you want to listen on it // even when -connect or -proxy is specified From 65e4bd071610958919b174bb506b28e71006f4fb Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 21:07:23 -0500 Subject: [PATCH 03/14] policy: wire witness standardness to mempool options; implement tapscript push-run and IF-branch push-only guards; drop redundant element check --- src/policy/policy.cpp | 113 +++++++++++++++++++++++++++++++++--------- src/policy/policy.h | 2 +- src/validation.cpp | 2 +- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 9c332c8661f41..ff2b936f9f385 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -348,7 +348,7 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, return true; } -bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects) +bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const kernel::MemPoolOptions& opts, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects) { if (tx.IsCoinBase()) return true; // Coinbases are skipped @@ -425,7 +425,7 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, // Policy: per-input total witness size budget to prevent data-splitting across elements size_t per_input_bytes_total{0}; for (const auto& elem : tx.vin[i].scriptWitness.stack) per_input_bytes_total += elem.size(); - if (per_input_bytes_total > 1024) { + if (per_input_bytes_total > opts.policy_max_v1_perinput_witness) { MaybeReject("taproot-perinput-witness"); } Span stack{tx.vin[i].scriptWitness.stack}; @@ -454,30 +454,95 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, } } } - // Policy: reject large push-only witness elements (any version), checked here for v1 path for efficiency - for (const auto& item : stack) { - if (item.size() > 0) { - // Attempt to parse as push-only element - size_t j = 0, n = item.size(); - uint64_t pushed_sum = 0; - bool push_only = true; - while (j < n) { - uint8_t op = item[j++]; - size_t push_len = 0; - if (op >= 0x01 && op <= 0x4b) { push_len = op; } - else if (op == OP_PUSHDATA1) { if (j >= n) { push_only = false; break; } push_len = item[j++]; } - else if (op == OP_PUSHDATA2) { if (j + 1 >= n) { push_only = false; break; } push_len = item[j] | (item[j+1] << 8); j += 2; } - else if (op == OP_PUSHDATA4) { if (j + 3 >= n) { push_only = false; break; } push_len = item[j] | (item[j+1] << 8) | (item[j+2] << 16) | (item[j+3] << 24); j += 4; } - else { push_only = false; break; } - if (j + push_len > n) { push_only = false; break; } - pushed_sum += push_len; - j += push_len; + // Policy: tapscript-level analysis + // 1) Reject push-only IF/NOTIF branch bodies exceeding 80 bytes total pushed + // 2) Reject large contiguous push-only runs exceeding 256 bytes total pushed + const std::vector leaf(script_bytes.begin(), script_bytes.end()); + // Helper lambdas to scan + auto parse_push = [&](const std::vector& s, size_t& j, uint64_t& sum)->bool{ + uint8_t op = s[j++]; + size_t push_len = 0; + if (op >= 0x01 && op <= 0x4b) { push_len = op; } + else if (op == OP_PUSHDATA1) { if (j >= s.size()) return false; push_len = s[j++]; } + else if (op == OP_PUSHDATA2) { if (j + 1 >= s.size()) return false; push_len = s[j] | (s[j+1] << 8); j += 2; } + else if (op == OP_PUSHDATA4) { if (j + 3 >= s.size()) return false; push_len = s[j] | (s[j+1] << 8) | (s[j+2] << 16) | (s[j+3] << 24); j += 4; } + else { return false; } + if (j + push_len > s.size()) return false; + sum += push_len; + j += push_len; + return true; + }; + // Max push-only run + uint64_t max_run_sum{0}; + for (size_t k = 0; k < leaf.size();) { + size_t j = k; + uint64_t sum = 0; + while (j < leaf.size()) { + size_t before = j; + if (!parse_push(leaf, j, sum)) { j = before; break; } + } + if (sum > max_run_sum) max_run_sum = sum; + // advance by one opcode (skip any push data if present) + if (j == k) { + if (j >= leaf.size()) break; + uint8_t op = leaf[j++]; + if (op >= 0x01 && op <= 0x4b) { if (j + op > leaf.size()) break; j += op; } + else if (op == OP_PUSHDATA1) { if (j >= leaf.size()) break; size_t l = leaf[j++]; if (j + l > leaf.size()) break; j += l; } + else if (op == OP_PUSHDATA2) { if (j + 1 >= leaf.size()) break; size_t l = leaf[j] | (leaf[j+1] << 8); j += 2; if (j + l > leaf.size()) break; j += l; } + else if (op == OP_PUSHDATA4) { if (j + 3 >= leaf.size()) break; size_t l = leaf[j] | (leaf[j+1] << 8) | (leaf[j+2] << 16) | (leaf[j+3] << 24); j += 4; if (j + l > leaf.size()) break; j += l; } + } + k = j; + } + if (max_run_sum > 256) { + MaybeReject("taproot-pushrun"); + } + // IF/NOTIF branch bodies scan (simple linear scan with stack for OP_IF/OP_NOTIF ... OP_ENDIF) + struct Branch { size_t start; size_t end; }; + std::vector branches; + std::vector if_stack; + for (size_t j = 0; j < leaf.size();) { + uint8_t op = leaf[j++]; + if (op == OP_IF || op == OP_NOTIF) { + if_stack.push_back(j); + continue; + } + if (op == OP_ELSE) { + if (!if_stack.empty()) { + size_t body_start = if_stack.back(); + size_t body_end = j - 1; // before OP_ELSE + branches.push_back({body_start, body_end}); + if_stack.back() = j; // else-body starts now } - if (push_only) { - if (pushed_sum > MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE) { // 80 bytes - MaybeReject("taproot-pushonly-witness-elem"); - } + continue; + } + if (op == OP_ENDIF) { + if (!if_stack.empty()) { + size_t body_start = if_stack.back(); + size_t body_end = j - 1; // before ENDIF + branches.push_back({body_start, body_end}); + if_stack.pop_back(); } + continue; + } + // skip pushdata payloads + size_t push_len = 0; + if (op >= 0x01 && op <= 0x4b) { push_len = op; } + else if (op == OP_PUSHDATA1) { if (j >= leaf.size()) break; push_len = leaf[j++]; } + else if (op == OP_PUSHDATA2) { if (j + 1 >= leaf.size()) break; push_len = leaf[j] | (leaf[j+1] << 8); j += 2; } + else if (op == OP_PUSHDATA4) { if (j + 3 >= leaf.size()) break; push_len = leaf[j] | (leaf[j+1] << 8) | (leaf[j+2] << 16) | (leaf[j+3] << 24); j += 4; } + if (push_len) { if (j + push_len > leaf.size()) break; j += push_len; } + } + for (const auto& b : branches) { + uint64_t sum = 0; + bool push_only = true; + for (size_t j = b.start; j < b.end;) { + size_t before = j; + if (!parse_push(leaf, j, sum)) { push_only = false; break; } + if (j == before) break; + } + if (push_only && sum > 80) { + MaybeReject("taproot-if-pushonly"); + break; } } } diff --git a/src/policy/policy.h b/src/policy/policy.h index a9c557279e06a..f1e9deda5786b 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -214,7 +214,7 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, * * Also enforce a maximum stack item size limit and no annexes for tapscript spends. */ -bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects=empty_ignore_rejects); +bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const kernel::MemPoolOptions& opts, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects=empty_ignore_rejects); /** Compute the virtual transaction size (weight reinterpreted as bytes). */ int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop); diff --git a/src/validation.cpp b/src/validation.cpp index 7034edde4949a..9abe0f99f1a76 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1036,7 +1036,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } // Check for non-standard witnesses. - if (tx.HasWitness() && m_pool.m_opts.require_standard && !IsWitnessStandard(tx, m_view, "bad-witness-", reason, ignore_rejects)) { + if (tx.HasWitness() && m_pool.m_opts.require_standard && !IsWitnessStandard(tx, m_view, m_pool.m_opts, "bad-witness-", reason, ignore_rejects)) { return state.Invalid(TxValidationResult::TX_WITNESS_MUTATED, reason); } From ef9ad505a2f472f98dbf611646e24ff2d39c5f7b Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 21:10:45 -0500 Subject: [PATCH 04/14] policy: finalize witness standardness wiring; remove unused -datacarrierwitnesslimit and field --- src/init.cpp | 9 ++++++++- src/kernel/mempool_options.h | 1 - src/node/mempool_args.cpp | 1 - src/test/fuzz/coins_view.cpp | 6 +++++- src/test/fuzz/transaction.cpp | 6 +++++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index a06ec4a6dc574..129b2f58d4134 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -717,7 +717,6 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) strprintf("Maximum size of data in data carrier transactions we relay and mine, in bytes (default: %u)", MAX_OP_RETURN_RELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); - argsman.AddArg("-datacarrierwitnesslimit", "Set maximum bytes allowed in any single push-only witness element for policy (default: 80)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-v1perinputwitnesslimit", "Set maximum total witness bytes per segwit v1 input for policy (default: 1024)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-maxscriptsize", strprintf("Maximum size of scripts (including the entire witness stack) we relay and mine, in bytes (default: %s)", DEFAULT_SCRIPT_SIZE_POLICY_LIMIT), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-maxtxlegacysigops", @@ -858,6 +857,14 @@ void InitParameterInteraction(ArgsManager& args) args.SoftSetArg("-datacarriercost", "0.25"); args.SoftSetArg("-datacarrierfullcount", "0"); args.SoftSetArg("-datacarriersize", "83"); + args.SoftSetArg("-maxtxlegacysigops", strprintf("%s", std::numeric_limits::max())); + args.SoftSetArg("-maxscriptsize", strprintf("%s", std::numeric_limits::max())); + args.SoftSetArg("-mempooltruc", "enforce"); + args.SoftSetArg("-permitephemeral", "anchor,send,dust"); + args.SoftSetArg("-spkreuse", "allow"); + args.SoftSetArg("-blockprioritysize", "0"); + args.SoftSetArg("-blockmaxsize", "4000000"); + args.SoftSetArg("-blockmaxweight", "4000000"); } diff --git a/src/kernel/mempool_options.h b/src/kernel/mempool_options.h index 1e4f68eeb2f07..1089f67eff26d 100644 --- a/src/kernel/mempool_options.h +++ b/src/kernel/mempool_options.h @@ -96,7 +96,6 @@ struct MemPoolOptions { bool permitephemeral_dust{DEFAULT_PERMITEPHEMERAL_DUST}; bool persist_v1_dat{DEFAULT_PERSIST_V1_DAT}; // Policy to limit inscription-like data in witnesses/tapscripts - unsigned int policy_max_pushonly_witness_elem{80}; unsigned int policy_max_v1_perinput_witness{1024}; MemPoolLimits limits{}; diff --git a/src/node/mempool_args.cpp b/src/node/mempool_args.cpp index 399000c9824ed..72c97c14788ab 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -308,7 +308,6 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP mempool_opts.persist_v1_dat = argsman.GetBoolArg("-persistmempoolv1", mempool_opts.persist_v1_dat); // Policy flags to constrain push-only witness elements and per-input size for segwit v1 - mempool_opts.policy_max_pushonly_witness_elem = argsman.GetIntArg("-datacarrierwitnesslimit", mempool_opts.policy_max_pushonly_witness_elem); mempool_opts.policy_max_v1_perinput_witness = argsman.GetIntArg("-v1perinputwitnesslimit", mempool_opts.policy_max_v1_perinput_witness); ApplyArgsManOptions(argsman, mempool_opts.limits); diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index 06145e0323b18..c7c80cd0188df 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -286,7 +286,11 @@ FUZZ_TARGET(coins_view, .init = initialize_coins_view) }, [&] { std::string reason; - (void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache, "bad-witness-", reason); + { + #include + kernel::MemPoolOptions opts; + (void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache, opts, "bad-witness-", reason); + } }); } } diff --git a/src/test/fuzz/transaction.cpp b/src/test/fuzz/transaction.cpp index f430834340cf7..07945cfeade19 100644 --- a/src/test/fuzz/transaction.cpp +++ b/src/test/fuzz/transaction.cpp @@ -92,7 +92,11 @@ FUZZ_TARGET(transaction, .init = initialize_transaction) const CCoinsViewCache coins_view_cache(&coins_view); (void)AreInputsStandard(tx, coins_view_cache); std::string reject_reason; - (void)IsWitnessStandard(tx, coins_view_cache, "fuzz", reject_reason); + { + #include + kernel::MemPoolOptions opts; + (void)IsWitnessStandard(tx, coins_view_cache, opts, "fuzz", reject_reason); + } if (tx.GetTotalSize() < 250'000) { // Avoid high memory usage (with msan) due to json encoding { From 9369191accd8fcafa808acb1ea49e4a6592c70c2 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 21:13:44 -0500 Subject: [PATCH 05/14] tests: fix include style and usage for IsWitnessStandard(opts) in fuzz tests --- src/test/fuzz/coins_view.cpp | 1 - src/test/fuzz/transaction.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index c7c80cd0188df..18cc093e0025e 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -287,7 +287,6 @@ FUZZ_TARGET(coins_view, .init = initialize_coins_view) [&] { std::string reason; { - #include kernel::MemPoolOptions opts; (void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache, opts, "bad-witness-", reason); } diff --git a/src/test/fuzz/transaction.cpp b/src/test/fuzz/transaction.cpp index 07945cfeade19..ecbd30ae5eb26 100644 --- a/src/test/fuzz/transaction.cpp +++ b/src/test/fuzz/transaction.cpp @@ -93,7 +93,6 @@ FUZZ_TARGET(transaction, .init = initialize_transaction) (void)AreInputsStandard(tx, coins_view_cache); std::string reject_reason; { - #include kernel::MemPoolOptions opts; (void)IsWitnessStandard(tx, coins_view_cache, opts, "fuzz", reject_reason); } From 312f14d060f83f60b13d5d40117b959762bdd26d Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 21:34:01 -0500 Subject: [PATCH 06/14] consensus: add BIP8 deployment stub for BIP-0444 (taproot_pushonly_limits) with TBD params and versionbits wiring --- src/consensus/params.h | 1 + src/deploymentinfo.cpp | 4 ++++ src/kernel/chainparams.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/src/consensus/params.h b/src/consensus/params.h index dd29b9408e232..9bc40d36b2357 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -32,6 +32,7 @@ constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_ enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, DEPLOYMENT_TAPROOT, // Deployment of Schnorr/Taproot (BIPs 340-342) + DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS, // BIP-0444: Taproot Push-Only Limits // 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 185a7dcb54ce7..505083726beca 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 =*/ "taproot_pushonly_limits", + /*.gbt_force =*/ true, + }, }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 861850b8e5c70..f4ce8fd8bc69b 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -117,6 +117,12 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1628640000; // August 11th, 2021 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 709632; // Approximately November 12th, 2021 + // Deployment of BIP-0444 Taproot Push-Only Limits (parameters TBD) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD: select final bit + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // set via -vbparams or when finalized + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; // set via -vbparams or when finalized + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; // set to Timeout+period when finalized + consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000dee8e2a309ad8a9820433c68"}; consensus.defaultAssumeValid = uint256{"00000000000000000000611fd22f2df7c8fbd0688745c3a6c3bb5109cc2a12cb"}; // 912683 @@ -280,6 +286,12 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1628640000; // August 11th, 2021 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay + // BIP-0444 Taproot Push-Only Limits (parameters TBD) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; + consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000015f5e0c9f13455b0eb17"}; consensus.defaultAssumeValid = uint256{"00000000000003fc7967410ba2d0a8a8d50daedc318d43e8baf1a9782c236a57"}; // 3974606 @@ -379,6 +391,12 @@ class CTestNet4Params : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay + // BIP-0444 Taproot Push-Only Limits (parameters TBD) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; // allow testing + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; + consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000001d6dce8651b6094e4c1"}; consensus.defaultAssumeValid = uint256{"0000000000003ed4f08dbdf6f7d6b271a6bcffce25675cb40aa9fa43179a89f3"}; // 72600 @@ -517,6 +535,12 @@ class SigNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay + // BIP-0444 Taproot Push-Only Limits (parameters overridable via -vbparams) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; + // message start is defined as the first 4 bytes of the sha256d of the block script HashWriter h{}; h << consensus.signet_challenge; From 46e9debf9e5feafea2e5b79794b69026968c836f Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 21:41:27 -0500 Subject: [PATCH 07/14] policy: tighten defaults and filters: spk size<=34 (non-NULL_DATA), pushlen<=256, disallow OP_IF in tapscript policy; default -acceptunknownwitness=false --- src/kernel/mempool_options.h | 2 +- src/policy/policy.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/kernel/mempool_options.h b/src/kernel/mempool_options.h index 1089f67eff26d..d31179bacc328 100644 --- a/src/kernel/mempool_options.h +++ b/src/kernel/mempool_options.h @@ -41,7 +41,7 @@ static constexpr bool DEFAULT_ACCEPT_NON_STD_DATACARRIER{false}; /** Default for -acceptnonstdtxn */ static constexpr bool DEFAULT_ACCEPT_NON_STD_TXN{false}; /** Default for -acceptunknownwitness */ -static constexpr bool DEFAULT_ACCEPTUNKNOWNWITNESS{true}; +static constexpr bool DEFAULT_ACCEPTUNKNOWNWITNESS{false}; namespace kernel { /** diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index ff2b936f9f385..f8402939ec690 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -174,10 +174,32 @@ bool IsStandardTx(const CTransaction& tx, const kernel::MemPoolOptions& opts, st MaybeReject("scriptpubkey-size"); } + // New policy: limit new scriptPubKeys to 34 bytes (except NULL_DATA) + if (whichType != TxoutType::NULL_DATA && txout.scriptPubKey.size() > 34) { + MaybeReject("scriptpubkey-size-34"); + } + if (!::IsStandard(txout.scriptPubKey, opts.max_datacarrier_bytes, whichType)) { MaybeReject("scriptpubkey"); } + // Enforce max push length 256 bytes in scriptPubKey (policy), excluding NULL_DATA which already has its own size limit. + if (whichType != TxoutType::NULL_DATA) { + const std::vector& s = txout.scriptPubKey; + for (size_t p = 0; p < s.size();) { + uint8_t op = s[p++]; + size_t push_len = 0; + if (op >= 0x01 && op <= 0x4b) { push_len = op; } + else if (op == OP_PUSHDATA1) { if (p >= s.size()) break; push_len = s[p++]; } + else if (op == OP_PUSHDATA2) { if (p + 1 >= s.size()) break; push_len = s[p] | (s[p+1] << 8); p += 2; } + else if (op == OP_PUSHDATA4) { if (p + 3 >= s.size()) break; push_len = s[p] | (s[p+1] << 8) | (s[p+2] << 16) | (s[p+3] << 24); p += 4; } + if (push_len) { + if (push_len > 256) { MaybeReject("scriptpubkey-pushlen"); } + if (p + push_len > s.size()) break; p += push_len; + } + } + } + if (whichType == TxoutType::WITNESS_UNKNOWN && !opts.acceptunknownwitness) { MaybeReject("scriptpubkey-unknown-witnessversion"); } @@ -457,6 +479,7 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, // Policy: tapscript-level analysis // 1) Reject push-only IF/NOTIF branch bodies exceeding 80 bytes total pushed // 2) Reject large contiguous push-only runs exceeding 256 bytes total pushed + // 3) Disallow OP_IF/OP_NOTIF entirely inside Tapscript (temporarily, per policy) const std::vector leaf(script_bytes.begin(), script_bytes.end()); // Helper lambdas to scan auto parse_push = [&](const std::vector& s, size_t& j, uint64_t& sum)->bool{ @@ -479,6 +502,11 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, uint64_t sum = 0; while (j < leaf.size()) { size_t before = j; + uint8_t op = leaf[j]; + if (op == OP_IF || op == OP_NOTIF) { + MaybeReject("taproot-if-disallowed"); + break; + } if (!parse_push(leaf, j, sum)) { j = before; break; } } if (sum > max_run_sum) max_run_sum = sum; From 9b891c14a1fcfaf172069f1c43af6a116e2896e5 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 21:45:50 -0500 Subject: [PATCH 08/14] policy: cap Taproot control block size to 257 bytes --- src/policy/policy.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index f8402939ec690..71c06606f674e 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -460,6 +460,10 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, if (stack.size() >= 2) { // Script path spend (2 or more stack elements after removing optional annex) const auto& control_block = SpanPopBack(stack); + // Policy: limit Taproot control block size to 257 bytes + if (control_block.size() > 257) { + MaybeReject("taproot-controlblock-size"); + } const auto& script_bytes = SpanPopBack(stack); // revealed leaf script if (control_block.empty()) { // Empty control block is invalid From 6de2837246c181b05d631396a88cbc70b075d2ec Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Tue, 14 Oct 2025 22:33:22 -0500 Subject: [PATCH 09/14] harmonize comments and names with expanded scope (script limits) --- src/consensus/params.h | 2 +- src/deploymentinfo.cpp | 2 +- src/kernel/chainparams.cpp | 40 +++++++++++++++++++------------------- src/node/mempool_args.cpp | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/consensus/params.h b/src/consensus/params.h index 9bc40d36b2357..507150ba187bf 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -32,7 +32,7 @@ constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_ enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, DEPLOYMENT_TAPROOT, // Deployment of Schnorr/Taproot (BIPs 340-342) - DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS, // BIP-0444: Taproot Push-Only Limits + DEPLOYMENT_TAPROOT_SCRIPT_LIMITS, // BIP-0444: Taproot and Script Limits // 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 505083726beca..72e4c7ae57d3e 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -18,7 +18,7 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B /*.gbt_force =*/ true, }, { - /*.name =*/ "taproot_pushonly_limits", + /*.name =*/ "taproot_script_limits", /*.gbt_force =*/ true, }, }; diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index f4ce8fd8bc69b..a795c779e8b5e 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -117,11 +117,11 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1628640000; // August 11th, 2021 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 709632; // Approximately November 12th, 2021 - // Deployment of BIP-0444 Taproot Push-Only Limits (parameters TBD) - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD: select final bit - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // set via -vbparams or when finalized - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; // set via -vbparams or when finalized - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; // set to Timeout+period when finalized + // Deployment of BIP-0444 Taproot and Script Limits (parameters TBD) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].bit = 3; // TBD: select final bit + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // set via -vbparams or when finalized + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; // set via -vbparams or when finalized + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].min_activation_height = 0; // set to Timeout+period when finalized consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000dee8e2a309ad8a9820433c68"}; consensus.defaultAssumeValid = uint256{"00000000000000000000611fd22f2df7c8fbd0688745c3a6c3bb5109cc2a12cb"}; // 912683 @@ -286,11 +286,11 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1628640000; // August 11th, 2021 consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay - // BIP-0444 Taproot Push-Only Limits (parameters TBD) - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; + // BIP-0444 Taproot and Script Limits (parameters TBD) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].bit = 3; // TBD + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].min_activation_height = 0; consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000015f5e0c9f13455b0eb17"}; consensus.defaultAssumeValid = uint256{"00000000000003fc7967410ba2d0a8a8d50daedc318d43e8baf1a9782c236a57"}; // 3974606 @@ -391,11 +391,11 @@ class CTestNet4Params : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay - // BIP-0444 Taproot Push-Only Limits (parameters TBD) - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; // allow testing - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; + // BIP-0444 Taproot and Script Limits (parameters TBD) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].bit = 3; // TBD + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; // allow testing + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].min_activation_height = 0; consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000001d6dce8651b6094e4c1"}; consensus.defaultAssumeValid = uint256{"0000000000003ed4f08dbdf6f7d6b271a6bcffce25675cb40aa9fa43179a89f3"}; // 72600 @@ -535,11 +535,11 @@ class SigNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay - // BIP-0444 Taproot Push-Only Limits (parameters overridable via -vbparams) - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].bit = 3; // TBD - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; - consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_PUSHONLY_LIMITS].min_activation_height = 0; + // BIP-0444 Taproot and Script Limits (parameters overridable via -vbparams) + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].bit = 3; // TBD + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT_SCRIPT_LIMITS].min_activation_height = 0; // message start is defined as the first 4 bytes of the sha256d of the block script HashWriter h{}; diff --git a/src/node/mempool_args.cpp b/src/node/mempool_args.cpp index 72c97c14788ab..9ecd100217508 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -307,7 +307,7 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP mempool_opts.persist_v1_dat = argsman.GetBoolArg("-persistmempoolv1", mempool_opts.persist_v1_dat); - // Policy flags to constrain push-only witness elements and per-input size for segwit v1 + // Policy flags to constrain per-input witness size for segwit v1 mempool_opts.policy_max_v1_perinput_witness = argsman.GetIntArg("-v1perinputwitnesslimit", mempool_opts.policy_max_v1_perinput_witness); ApplyArgsManOptions(argsman, mempool_opts.limits); From b960a5728309a0cdc5deddbdbd0a0d36ddf84779 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Wed, 15 Oct 2025 10:18:27 -0500 Subject: [PATCH 10/14] policy: refactor script parsing to helpers; single-pass tapscript scan; constants for limits; bounds for -v1perinputwitnesslimit; fix whichType use order; add comments with BIP-0444 rationale --- src/init.cpp | 2 +- src/node/mempool_args.cpp | 8 +- src/policy/policy.cpp | 260 +++++++++++++++++++++++--------------- 3 files changed, 168 insertions(+), 102 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 129b2f58d4134..ef605c3108582 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -717,7 +717,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) strprintf("Maximum size of data in data carrier transactions we relay and mine, in bytes (default: %u)", MAX_OP_RETURN_RELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); - argsman.AddArg("-v1perinputwitnesslimit", "Set maximum total witness bytes per segwit v1 input for policy (default: 1024)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-v1perinputwitnesslimit", "Set maximum total witness bytes per segwit v1 input for policy (default: 1024; min 128, max 8192)", ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-maxscriptsize", strprintf("Maximum size of scripts (including the entire witness stack) we relay and mine, in bytes (default: %s)", DEFAULT_SCRIPT_SIZE_POLICY_LIMIT), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-maxtxlegacysigops", strprintf("Maximum number of legacy sigops allowed in transactions we relay and mine, as measured by BIP54 (default: %s)", diff --git a/src/node/mempool_args.cpp b/src/node/mempool_args.cpp index 9ecd100217508..645064c8637ad 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -308,7 +308,13 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP mempool_opts.persist_v1_dat = argsman.GetBoolArg("-persistmempoolv1", mempool_opts.persist_v1_dat); // Policy flags to constrain per-input witness size for segwit v1 - mempool_opts.policy_max_v1_perinput_witness = argsman.GetIntArg("-v1perinputwitnesslimit", mempool_opts.policy_max_v1_perinput_witness); + if (auto lim = argsman.GetIntArg("-v1perinputwitnesslimit")) { + // Enforce reasonable bounds to avoid footguns + unsigned val = *lim; + if (val < 128) val = 128; + if (val > 8192) val = 8192; + mempool_opts.policy_max_v1_perinput_witness = val; + } ApplyArgsManOptions(argsman, mempool_opts.limits); diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 71c06606f674e..cb97daf41122c 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -28,6 +28,137 @@ unsigned int g_script_size_policy_limit{DEFAULT_SCRIPT_SIZE_POLICY_LIMIT}; +// BIP-0444 policy limits (documented in BIP-0444): +// - 34 bytes max for non-NULL_DATA scriptPubKey (keeps UTXOs small) +// - 256 bytes max for any single push payload in scriptPubKey +// - 257 bytes max Taproot control block size (33-byte base + up to 7 * 32-byte merkle path) +// - 256 bytes max contiguous push-only run in Tapscript leaves +// - 80 bytes max push-only IF/NOTIF branch body in Tapscript leaves +static constexpr unsigned int POLICY_MAX_SPK_SIZE_NON_NULLDATA{34}; +static constexpr unsigned int POLICY_MAX_SCRIPT_PUSH_LEN{256}; +static constexpr unsigned int POLICY_MAX_TAPROOT_CONTROL_BLOCK{257}; +static constexpr unsigned int POLICY_MAX_TAPSCRIPT_PUSH_RUN{256}; +static constexpr unsigned int POLICY_MAX_TAPSCRIPT_IF_BODY{80}; + +// Helpers for script parsing to avoid duplication and bugs +namespace { + +// Return true if op is a small integer push (OP_0, OP_1NEGATE, OP_1..OP_16) +inline bool IsSmallIntPush(uint8_t op) { + return op == OP_0 || op == OP_1NEGATE || (op >= OP_1 && op <= OP_16); +} + +// Parse a data push starting at s[pos]. Supports OP_0/OP_1NEGATE/OP_1..OP_16 as pushes of length 0. +// On success, advances pos to the byte after the payload and sets pushed_len to the payload size (0 for small-int pushes). +// Returns false on malformed/incomplete encoding or if a non-push opcode is encountered. +inline bool ParsePush(const std::vector& s, size_t& pos, uint64_t& pushed_len) { + if (pos >= s.size()) return false; + uint8_t op = s[pos++]; + pushed_len = 0; + if (IsSmallIntPush(op)) { + // Small integer push has no explicit payload + return true; + } + size_t need = 0; + if (op >= 0x01 && op <= 0x4b) { + pushed_len = op; + } else if (op == OP_PUSHDATA1) { + if (pos >= s.size()) return false; + pushed_len = s[pos++]; + } else if (op == OP_PUSHDATA2) { + if (pos + 1 >= s.size()) return false; + pushed_len = s[pos] | (uint32_t(s[pos+1]) << 8); + pos += 2; + } else if (op == OP_PUSHDATA4) { + if (pos + 3 >= s.size()) return false; + pushed_len = s[pos] | (uint32_t(s[pos+1]) << 8) | (uint32_t(s[pos+2]) << 16) | (uint32_t(s[pos+3]) << 24); + pos += 4; + } else { + return false; // non-push opcode + } + if (pushed_len > 0) { + if (pos + pushed_len > s.size()) return false; + pos += pushed_len; + } + return true; +} + +// Advance across exactly one opcode (including its payload if a push), without interpreting it. +inline bool AdvanceOneOp(const std::vector& s, size_t& pos) { + if (pos >= s.size()) return false; + uint8_t op = s[pos++]; + if (IsSmallIntPush(op)) return true; + size_t payload = 0; + if (op >= 0x01 && op <= 0x4b) payload = op; + else if (op == OP_PUSHDATA1) { if (pos >= s.size()) return false; payload = s[pos++]; } + else if (op == OP_PUSHDATA2) { if (pos + 1 >= s.size()) return false; payload = s[pos] | (uint32_t(s[pos+1]) << 8); pos += 2; } + else if (op == OP_PUSHDATA4) { if (pos + 3 >= s.size()) return false; payload = s[pos] | (uint32_t(s[pos+1]) << 8) | (uint32_t(s[pos+2]) << 16) | (uint32_t(s[pos+3]) << 24); pos += 4; } + // Non-push opcode: nothing to skip + if (payload) { + if (pos + payload > s.size()) return false; + pos += payload; + } + return true; +} + +struct Branch { size_t start; size_t end; }; + +// Single-pass scan over a Tapscript leaf to compute: +// - max contiguous push-only run total payload (max_run) +// - IF/NOTIF branch bodies (branches) with [start,end) byte ranges +// - presence of any OP_IF/OP_NOTIF opcodes (if_found) +inline bool ScanTapscriptLeaf(const std::vector& leaf, uint64_t& max_run, std::vector& branches, bool& if_found) { + max_run = 0; + if_found = false; + branches.clear(); + std::vector if_stack; + for (size_t k = 0; k < leaf.size();) { + // Count a push-only run starting at k + size_t j = k; + uint64_t sum = 0; + while (j < leaf.size()) { + size_t before = j; + uint8_t op = leaf[j]; + if (op == OP_IF || op == OP_NOTIF) { if_found = true; break; } + uint64_t pushed = 0; + if (!ParsePush(leaf, j, pushed)) { j = before; break; } + sum += pushed; + } + if (sum > max_run) max_run = sum; + // If no progress, consume one opcode and update IF/ELSE/ENDIF bookkeeping + if (j == k) { + if (j >= leaf.size()) break; + uint8_t op = leaf[j++]; + if (op == OP_IF || op == OP_NOTIF) { + if_found = true; + if_stack.push_back(j); + } else if (op == OP_ELSE) { + if (!if_stack.empty()) { + size_t body_start = if_stack.back(); + size_t body_end = j - 1; + if (body_end > body_start) branches.push_back({body_start, body_end}); + if_stack.back() = j; // else-branch starts now + } + } else if (op == OP_ENDIF) { + if (!if_stack.empty()) { + size_t body_start = if_stack.back(); + size_t body_end = j - 1; + if (body_end > body_start) branches.push_back({body_start, body_end}); + if_stack.pop_back(); + } + } else { + // Non-push non-IF opcode: if it has data payload, skip it + j--; // step back to opcode position for AdvanceOneOp + if (!AdvanceOneOp(leaf, j)) return false; + } + } + k = j; + } + return true; +} + +} // namespace + CAmount GetDustThreshold(const CTxOut& txout, const CFeeRate& dustRelayFeeIn) { // "Dust" is defined in terms of dustRelayFee, @@ -166,7 +297,6 @@ bool IsStandardTx(const CTransaction& tx, const kernel::MemPoolOptions& opts, st unsigned int nDataOut = 0; unsigned int n_dust{0}; unsigned int n_monetary{0}; - TxoutType whichType; for (size_t i{tx.vout.size()}; i; ) { const CTxOut& txout = tx.vout[--i]; @@ -174,29 +304,24 @@ bool IsStandardTx(const CTransaction& tx, const kernel::MemPoolOptions& opts, st MaybeReject("scriptpubkey-size"); } - // New policy: limit new scriptPubKeys to 34 bytes (except NULL_DATA) - if (whichType != TxoutType::NULL_DATA && txout.scriptPubKey.size() > 34) { - MaybeReject("scriptpubkey-size-34"); - } - + TxoutType whichType; if (!::IsStandard(txout.scriptPubKey, opts.max_datacarrier_bytes, whichType)) { MaybeReject("scriptpubkey"); } - // Enforce max push length 256 bytes in scriptPubKey (policy), excluding NULL_DATA which already has its own size limit. + // New policy: limit new scriptPubKeys to POLICY_MAX_SPK_SIZE_NON_NULLDATA bytes (except NULL_DATA) + if (whichType != TxoutType::NULL_DATA && txout.scriptPubKey.size() > POLICY_MAX_SPK_SIZE_NON_NULLDATA) { + MaybeReject("scriptpubkey-size-34"); + } + + // Enforce max push length in scriptPubKey (policy), excluding NULL_DATA which already has its own size limit. if (whichType != TxoutType::NULL_DATA) { const std::vector& s = txout.scriptPubKey; for (size_t p = 0; p < s.size();) { - uint8_t op = s[p++]; - size_t push_len = 0; - if (op >= 0x01 && op <= 0x4b) { push_len = op; } - else if (op == OP_PUSHDATA1) { if (p >= s.size()) break; push_len = s[p++]; } - else if (op == OP_PUSHDATA2) { if (p + 1 >= s.size()) break; push_len = s[p] | (s[p+1] << 8); p += 2; } - else if (op == OP_PUSHDATA4) { if (p + 3 >= s.size()) break; push_len = s[p] | (s[p+1] << 8) | (s[p+2] << 16) | (s[p+3] << 24); p += 4; } - if (push_len) { - if (push_len > 256) { MaybeReject("scriptpubkey-pushlen"); } - if (p + push_len > s.size()) break; p += push_len; - } + size_t before = p; + uint64_t pushed = 0; + if (!ParsePush(s, p, pushed)) { p = before; if (!AdvanceOneOp(s, p)) break; continue; } + if (pushed > POLICY_MAX_SCRIPT_PUSH_LEN) { MaybeReject("scriptpubkey-pushlen"); } } } @@ -460,8 +585,8 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, if (stack.size() >= 2) { // Script path spend (2 or more stack elements after removing optional annex) const auto& control_block = SpanPopBack(stack); - // Policy: limit Taproot control block size to 257 bytes - if (control_block.size() > 257) { + // Policy: limit Taproot control block size + if (control_block.size() > POLICY_MAX_TAPROOT_CONTROL_BLOCK) { MaybeReject("taproot-controlblock-size"); } const auto& script_bytes = SpanPopBack(stack); // revealed leaf script @@ -480,99 +605,34 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, } } } - // Policy: tapscript-level analysis - // 1) Reject push-only IF/NOTIF branch bodies exceeding 80 bytes total pushed - // 2) Reject large contiguous push-only runs exceeding 256 bytes total pushed - // 3) Disallow OP_IF/OP_NOTIF entirely inside Tapscript (temporarily, per policy) + // Policy: tapscript-level analysis (single pass) + // - Disallow OP_IF/OP_NOTIF entirely (temporary policy) + // - Cap contiguous push-only run total payload + // - Cap push-only IF/NOTIF branch body total payload const std::vector leaf(script_bytes.begin(), script_bytes.end()); - // Helper lambdas to scan - auto parse_push = [&](const std::vector& s, size_t& j, uint64_t& sum)->bool{ - uint8_t op = s[j++]; - size_t push_len = 0; - if (op >= 0x01 && op <= 0x4b) { push_len = op; } - else if (op == OP_PUSHDATA1) { if (j >= s.size()) return false; push_len = s[j++]; } - else if (op == OP_PUSHDATA2) { if (j + 1 >= s.size()) return false; push_len = s[j] | (s[j+1] << 8); j += 2; } - else if (op == OP_PUSHDATA4) { if (j + 3 >= s.size()) return false; push_len = s[j] | (s[j+1] << 8) | (s[j+2] << 16) | (s[j+3] << 24); j += 4; } - else { return false; } - if (j + push_len > s.size()) return false; - sum += push_len; - j += push_len; - return true; - }; - // Max push-only run uint64_t max_run_sum{0}; - for (size_t k = 0; k < leaf.size();) { - size_t j = k; - uint64_t sum = 0; - while (j < leaf.size()) { - size_t before = j; - uint8_t op = leaf[j]; - if (op == OP_IF || op == OP_NOTIF) { - MaybeReject("taproot-if-disallowed"); - break; - } - if (!parse_push(leaf, j, sum)) { j = before; break; } - } - if (sum > max_run_sum) max_run_sum = sum; - // advance by one opcode (skip any push data if present) - if (j == k) { - if (j >= leaf.size()) break; - uint8_t op = leaf[j++]; - if (op >= 0x01 && op <= 0x4b) { if (j + op > leaf.size()) break; j += op; } - else if (op == OP_PUSHDATA1) { if (j >= leaf.size()) break; size_t l = leaf[j++]; if (j + l > leaf.size()) break; j += l; } - else if (op == OP_PUSHDATA2) { if (j + 1 >= leaf.size()) break; size_t l = leaf[j] | (leaf[j+1] << 8); j += 2; if (j + l > leaf.size()) break; j += l; } - else if (op == OP_PUSHDATA4) { if (j + 3 >= leaf.size()) break; size_t l = leaf[j] | (leaf[j+1] << 8) | (leaf[j+2] << 16) | (leaf[j+3] << 24); j += 4; if (j + l > leaf.size()) break; j += l; } - } - k = j; + std::vector branches; + bool if_found{false}; + if (!ScanTapscriptLeaf(leaf, max_run_sum, branches, if_found)) { + MaybeReject("taproot-malformed"); } - if (max_run_sum > 256) { - MaybeReject("taproot-pushrun"); + if (if_found) { + MaybeReject("taproot-if-disallowed"); } - // IF/NOTIF branch bodies scan (simple linear scan with stack for OP_IF/OP_NOTIF ... OP_ENDIF) - struct Branch { size_t start; size_t end; }; - std::vector branches; - std::vector if_stack; - for (size_t j = 0; j < leaf.size();) { - uint8_t op = leaf[j++]; - if (op == OP_IF || op == OP_NOTIF) { - if_stack.push_back(j); - continue; - } - if (op == OP_ELSE) { - if (!if_stack.empty()) { - size_t body_start = if_stack.back(); - size_t body_end = j - 1; // before OP_ELSE - branches.push_back({body_start, body_end}); - if_stack.back() = j; // else-body starts now - } - continue; - } - if (op == OP_ENDIF) { - if (!if_stack.empty()) { - size_t body_start = if_stack.back(); - size_t body_end = j - 1; // before ENDIF - branches.push_back({body_start, body_end}); - if_stack.pop_back(); - } - continue; - } - // skip pushdata payloads - size_t push_len = 0; - if (op >= 0x01 && op <= 0x4b) { push_len = op; } - else if (op == OP_PUSHDATA1) { if (j >= leaf.size()) break; push_len = leaf[j++]; } - else if (op == OP_PUSHDATA2) { if (j + 1 >= leaf.size()) break; push_len = leaf[j] | (leaf[j+1] << 8); j += 2; } - else if (op == OP_PUSHDATA4) { if (j + 3 >= leaf.size()) break; push_len = leaf[j] | (leaf[j+1] << 8) | (leaf[j+2] << 16) | (leaf[j+3] << 24); j += 4; } - if (push_len) { if (j + push_len > leaf.size()) break; j += push_len; } + if (max_run_sum > POLICY_MAX_TAPSCRIPT_PUSH_RUN) { + MaybeReject("taproot-pushrun"); } for (const auto& b : branches) { uint64_t sum = 0; bool push_only = true; for (size_t j = b.start; j < b.end;) { size_t before = j; - if (!parse_push(leaf, j, sum)) { push_only = false; break; } + uint64_t pushed = 0; + if (!ParsePush(leaf, j, pushed)) { push_only = false; break; } + sum += pushed; if (j == before) break; } - if (push_only && sum > 80) { + if (push_only && sum > POLICY_MAX_TAPSCRIPT_IF_BODY) { MaybeReject("taproot-if-pushonly"); break; } From 45599f5d346007ccd2e043e9f66cc7367461ecee Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Wed, 15 Oct 2025 10:22:18 -0500 Subject: [PATCH 11/14] tests: add policy tests for scriptPubKey size/push caps and unknown witness default; note tapscript tests as follow-up --- src/test/transaction_tests.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index 5d3d999f55fbe..b4d82eb3f147b 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -1135,6 +1135,34 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) CheckIsNotStandard(t, "dust-nonanchor"); g_mempool_opts.permitephemeral_send = true; CheckIsStandard(t); + + // Policy: non-NULL_DATA scriptPubKey size cap (34) + t.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160 << std::vector(20, 0) << OP_EQUALVERIFY << OP_CHECKSIG; + CheckIsStandard(t); + // Pad scriptPubKey beyond 34 bytes (append a small push) + CScript spk_pad = t.vout[0].scriptPubKey; + spk_pad << std::vector(3, 0); + t.vout[0].scriptPubKey = spk_pad; + CheckIsNotStandard(t, "scriptpubkey-size-34"); + + // Policy: scriptPubKey push length cap (256) + CScript spk_push; + std::vector payload_256(256, 0); + std::vector payload_257(257, 0); + spk_push << OP_RETURN; // NULL_DATA is excluded; use non-NULL_DATA to test cap + t.vout[0].scriptPubKey = CScript() << payload_256 << OP_DROP; + CheckIsStandard(t); + t.vout[0].scriptPubKey = CScript() << payload_257 << OP_DROP; + CheckIsNotStandard(t, "scriptpubkey-pushlen"); + + // Policy: unknown witness versions default rejected (scriptPubKey witness unknown) + // OP_16 with 32-byte program is WITNESS_UNKNOWN; ensure default rejects when value is dust (exercise path) + t.vout[0].scriptPubKey = CScript() << OP_16 << std::vector(32, 0); + CheckIsNotStandard(t, "scriptpubkey-unknown-witnessversion"); + + // Placeholders for tapscript policy checks would be added in dedicated tests + // (taproot IF-ban, push-run cap, IF-body cap, control block cap, per-input witness cap) + // using a test harness that constructs valid taproot spends. } BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops) From 1870ad6165a60fa333131f9e3fa2bfbf49ac0093 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Wed, 15 Oct 2025 10:37:19 -0500 Subject: [PATCH 12/14] tests: add BIP-0444 policy unit tests for taproot control block cap, per-input witness cap, IF ban, push-run cap, and IF-body cap --- src/test/bip444_tests.cpp | 244 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/test/bip444_tests.cpp diff --git a/src/test/bip444_tests.cpp b/src/test/bip444_tests.cpp new file mode 100644 index 0000000000000..97af12ea0080c --- /dev/null +++ b/src/test/bip444_tests.cpp @@ -0,0 +1,244 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include