diff --git a/doc/release-notes-bip444.md b/doc/release-notes-bip444.md new file mode 100644 index 0000000000000..8c1b12cbb06ab --- /dev/null +++ b/doc/release-notes-bip444.md @@ -0,0 +1,52 @@ +# BIP-0444 Policy Changes (Taproot and Script Limits) + +## Summary + +This release introduces policy-level enforcement of BIP-0444 to prevent large arbitrary-data inscriptions and reduce UTXO/script bloat. These changes are active immediately as relay/mempool policy defaults; consensus enforcement will follow after BIP8 activation (parameters TBD). + +## Policy Changes (Active Now) + +### scriptPubKey Limits +- **Non-NULL_DATA scriptPubKey size cap**: Transactions with non-NULL_DATA outputs exceeding 34 bytes are rejected (`scriptpubkey-size-34`). +- **scriptPubKey push length cap**: Any single push operation in a scriptPubKey exceeding 256 bytes is rejected (`scriptpubkey-pushlen`). + +### Taproot/Tapscript Limits +- **Control block size cap**: Taproot control blocks exceeding 257 bytes (33-byte base + 7 merkle path nodes) are rejected (`taproot-controlblock-size`). +- **Per-input witness size cap**: Segwit v1 inputs with total witness data exceeding 1024 bytes are rejected (`taproot-perinput-witness`). Configurable via `-v1perinputwitnesslimit` (min 128, max 8192). +- **Tapscript IF ban**: OP_IF and OP_NOTIF opcodes are disallowed in Tapscript leaves (`taproot-if-disallowed`). +- **Tapscript push-only run cap**: Contiguous push-only regions in Tapscript leaves exceeding 256 bytes total payload are rejected (`taproot-pushrun`). +- **Tapscript IF-body cap**: Push-only IF/NOTIF branch bodies exceeding 80 bytes total payload are rejected (`taproot-if-pushonly`). + +### Unknown Witness Versions +- **Default reject unknown witness**: The default for `-acceptunknownwitness` is now `false`. Transactions sending to undefined witness program versions are rejected by default unless explicitly allowed via `-acceptunknownwitness=1`. + +## Configuration Options + +- `-v1perinputwitnesslimit=`: Set maximum total witness bytes per segwit v1 input (default: 1024; min 128, max 8192). +- `-acceptunknownwitness=`: Allow relay of transactions to unknown/future witness versions (default: 0). + +## Rationale + +These policy defaults target the common inscription vectors (Taproot witness/script abuse) while preserving legitimate usage: +- Standard P2TR/P2WSH outputs remain valid (≤34 bytes). +- Normal signatures, keys, and small scripts are unaffected. +- Taproot key-path spends are unaffected. +- The limits are conservative and tunable for advanced use cases. + +## Deployment + +BIP-0444 includes a soft-fork component with BIP8 activation (parameters TBD). Policy enforcement is active immediately; consensus rules will activate after the signaling period and delayed activation height. + +For testing on regtest/testnet/signet, use `-vbparams=taproot_script_limits:start:end[:min_activation_height]` to override deployment parameters. + +## Compatibility + +- **Wallets**: Avoid creating transactions with oversized scriptPubKeys, large push payloads, or Tapscript leaves using OP_IF/NOTIF. +- **Contracts**: Multi-party protocols with large witness data (e.g., complex lightning channels, vaults) may need to tune `-v1perinputwitnesslimit` or split across multiple inputs. +- **Unknown witness versions**: If your application relies on future witness versions, set `-acceptunknownwitness=1`. + +## References + +- BIP-0444: https://github.com/bitcoin/bips/blob/master/bip-0444.mediawiki (pending merge) +- Discussion: https://gnusha.org/pi/bitcoindev/CALeFGL0PDjtRt2rfbY4gTkoc+5oNQ0mn_obraE7PrtHuNYFpQw@mail.gmail.com/T/#mb71350c5dfb119efeb92c5ee738b6c8225bf15b6 + diff --git a/src/consensus/params.h b/src/consensus/params.h index dd29b9408e232..507150ba187bf 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_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 185a7dcb54ce7..72e4c7ae57d3e 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_script_limits", + /*.gbt_force =*/ true, + }, }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/init.cpp b/src/init.cpp index 454f126298c54..ef605c3108582 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -717,6 +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; 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)", @@ -866,6 +867,8 @@ void InitParameterInteraction(ArgsManager& args) args.SoftSetArg("-blockmaxweight", "4000000"); } + + // when specifying an explicit binding address, you want to listen on it // even when -connect or -proxy is specified if (!args.GetArgs("-bind").empty()) { diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 861850b8e5c70..e6f9f72cf1967 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 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 @@ -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 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 @@ -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 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; // ALWAYS_ACTIVE for easy signet 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 @@ -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 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{}; h << consensus.signet_challenge; diff --git a/src/kernel/mempool_options.h b/src/kernel/mempool_options.h index f6a37e958c0b9..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 { /** @@ -95,6 +95,8 @@ 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_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..645064c8637ad 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -307,6 +307,15 @@ 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 + 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); return {}; diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index f1a4bc5bc13c3..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,10 +304,27 @@ bool IsStandardTx(const CTransaction& tx, const kernel::MemPoolOptions& opts, st MaybeReject("scriptpubkey-size"); } + TxoutType whichType; if (!::IsStandard(txout.scriptPubKey, opts.max_datacarrier_bytes, whichType)) { MaybeReject("scriptpubkey"); } + // 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();) { + 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"); } + } + } + if (whichType == TxoutType::WITNESS_UNKNOWN && !opts.acceptunknownwitness) { MaybeReject("scriptpubkey-unknown-witnessversion"); } @@ -348,7 +495,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 @@ -422,6 +569,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 > opts.policy_max_v1_perinput_witness) { + 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 +585,11 @@ 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 + // 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 if (control_block.empty()) { // Empty control block is invalid out_reason = reason_prefix + "taproot-control-missing"; @@ -448,6 +605,38 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, } } } + // 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()); + uint64_t max_run_sum{0}; + std::vector branches; + bool if_found{false}; + if (!ScanTapscriptLeaf(leaf, max_run_sum, branches, if_found)) { + MaybeReject("taproot-malformed"); + } + if (if_found) { + MaybeReject("taproot-if-disallowed"); + } + 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; + uint64_t pushed = 0; + if (!ParsePush(leaf, j, pushed)) { push_only = false; break; } + sum += pushed; + if (j == before) break; + } + if (push_only && sum > POLICY_MAX_TAPSCRIPT_IF_BODY) { + MaybeReject("taproot-if-pushonly"); + break; + } + } } } else if (stack.size() == 1) { // Key path spend (1 stack element after removing optional annex) 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/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