diff --git a/src/init.cpp b/src/init.cpp index 84a8bc000e..e4b4518870 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -662,6 +662,7 @@ void SetupServerArgs(ArgsManager& argsman) ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-mempoolfullrbf", strprintf("Accept transaction replace-by-fee without requiring replaceability signaling (default: %u)", (DEFAULT_MEMPOOL_RBF_POLICY == RBFPolicy::Always)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-mempoolreplacement", strprintf("Set to 0 to disable RBF entirely, \"fee,optin\" to honour RBF opt-out signal, or \"fee,-optin\" to always RBF aka full RBF (default: %s)", "fee,optin"), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-mempooltruc", strprintf("Behaviour for transactions requesting TRUC limits: \"reject\" the transactions entirely, \"accept\" them just like any other, or \"enforce\" to impose their requested restrictions (default: %s)", "reject"), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-permitbaremultisig", strprintf("Relay non-P2SH multisig (default: %u)", DEFAULT_PERMIT_BAREMULTISIG), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-minrelaytxfee=", strprintf("Fees (in %s/kvB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)", @@ -760,6 +761,10 @@ static bool AppInitServers(NodeContext& node) // Parameter interaction based on rules void InitParameterInteraction(ArgsManager& args) { + if (args.GetBoolArg("-acceptnonstdtxn", DEFAULT_ACCEPT_NON_STD_TXN) && (!args.IsArgSet("-mempooltruc")) && DEFAULT_MEMPOOL_TRUC_POLICY == TRUCPolicy::Reject) { + args.SoftSetArg("-mempooltruc", "enforce"); + } + // when specifying an explicit binding address, you want to listen on it // even when -connect or -proxy is specified if (args.IsArgSet("-bind")) { diff --git a/src/kernel/mempool_options.h b/src/kernel/mempool_options.h index 176e5814c3..b1f6d30ef3 100644 --- a/src/kernel/mempool_options.h +++ b/src/kernel/mempool_options.h @@ -14,6 +14,7 @@ #include enum class RBFPolicy { Never, OptIn, Always }; +enum class TRUCPolicy { Reject, Accept, Enforce }; /** Default for -maxmempool, maximum megabytes of mempool memory usage */ static constexpr unsigned int DEFAULT_MAX_MEMPOOL_SIZE_MB{300}; @@ -23,6 +24,8 @@ static constexpr unsigned int DEFAULT_BLOCKSONLY_MAX_MEMPOOL_SIZE_MB{5}; static constexpr unsigned int DEFAULT_MEMPOOL_EXPIRY_HOURS{336}; /** Default for -mempoolreplacement; must update docs in init.cpp manually */ static constexpr RBFPolicy DEFAULT_MEMPOOL_RBF_POLICY{RBFPolicy::OptIn}; +/** Default for -mempooltruc; must update docs in init.cpp manually */ +static constexpr TRUCPolicy DEFAULT_MEMPOOL_TRUC_POLICY{TRUCPolicy::Reject}; /** Whether to fall back to legacy V1 serialization when writing mempool.dat */ static constexpr bool DEFAULT_PERSIST_V1_DAT{false}; /** Default for -acceptnonstdtxn */ @@ -56,6 +59,7 @@ struct MemPoolOptions { bool permit_bare_multisig{DEFAULT_PERMIT_BAREMULTISIG}; bool require_standard{true}; RBFPolicy rbf_policy{DEFAULT_MEMPOOL_RBF_POLICY}; + TRUCPolicy truc_policy{DEFAULT_MEMPOOL_TRUC_POLICY}; bool persist_v1_dat{DEFAULT_PERSIST_V1_DAT}; MemPoolLimits limits{}; }; diff --git a/src/node/mempool_args.cpp b/src/node/mempool_args.cpp index f42655f693..48cdb18d33 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -143,6 +143,34 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP } } + if (argsman.IsArgSet("-mempooltruc")) { + std::optional accept_flag, enforce_flag; + if (argsman.GetBoolArg("-mempooltruc", false)) { + enforce_flag = true; + } + for (auto& opt : SplitString(argsman.GetArg("-mempooltruc", ""), ",+")) { + if (opt == "optin" || opt == "enforce") { + enforce_flag = true; + } else if (opt == "-optin" || opt == "-enforce") { + enforce_flag = false; + } else if (opt == "accept") { + accept_flag = true; + } else if (opt == "reject" || opt == "0") { + accept_flag = false; + } + } + + if (accept_flag && !*accept_flag) { // reject + mempool_opts.truc_policy = TRUCPolicy::Reject; + } else if (enforce_flag && *enforce_flag) { // enforce + mempool_opts.truc_policy = TRUCPolicy::Enforce; + } else if ((!accept_flag) && !enforce_flag) { + // nothing specified, leave at default + } else { // accept or -enforce + mempool_opts.truc_policy = TRUCPolicy::Accept; + } + } + mempool_opts.persist_v1_dat = argsman.GetBoolArg("-persistmempoolv1", mempool_opts.persist_v1_dat); ApplyArgsManOptions(argsman, mempool_opts.limits); diff --git a/src/policy/policy.h b/src/policy/policy.h index 11493df1c8..c95fea0e0d 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -141,7 +141,7 @@ bool IsStandard(const CScript& scriptPubKey, const std::optional& max_ // Changing the default transaction version requires a two step process: first // adapting relay policy by bumping TX_MAX_STANDARD_VERSION, and then later // allowing the new transaction version in the wallet/RPC. -static constexpr decltype(CTransaction::nVersion) TX_MAX_STANDARD_VERSION{2}; +static constexpr decltype(CTransaction::nVersion) TX_MAX_STANDARD_VERSION{3}; /** * Check for standard transaction types diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index ccb00247d2..ebd7d8efa1 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -891,6 +891,11 @@ UniValue MempoolInfoToJSON(const CTxMemPool& pool, const std::optional", "Fee rate group named by its lower bound (in " + CURRENCY_ATOM + "/vB), identical to the \"from_feerate\" field below", diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 9e5869579c..bef0fda588 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -406,6 +406,7 @@ CTxMemPool::CTxMemPool(const Options& opts) m_max_datacarrier_bytes{opts.max_datacarrier_bytes}, m_require_standard{opts.require_standard}, m_rbf_policy{opts.rbf_policy}, + m_truc_policy{opts.truc_policy}, m_persist_v1_dat{opts.persist_v1_dat}, m_limits{opts.limits} { diff --git a/src/txmempool.h b/src/txmempool.h index ed122a5a5d..5db353a478 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -448,6 +448,7 @@ public: const std::optional m_max_datacarrier_bytes; const bool m_require_standard; const RBFPolicy m_rbf_policy; + const TRUCPolicy m_truc_policy; const bool m_persist_v1_dat; const Limits m_limits; diff --git a/src/validation.cpp b/src/validation.cpp index 2b3ec44780..1466f399a4 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -738,6 +738,10 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) if (tx.IsCoinBase()) return state.Invalid(TxValidationResult::TX_CONSENSUS, "coinbase"); + if (tx.nVersion == 3 && m_pool.m_truc_policy == TRUCPolicy::Reject) { + return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "version"); + } + // Rather not work on nonstandard transactions (unless -testnet/-regtest) std::string reason; if (m_pool.m_require_standard && !IsStandardTx(tx, m_pool.m_max_datacarrier_bytes, m_pool.m_permit_bare_multisig, m_pool.m_dust_relay_feerate, reason, ignore_rejects)) { @@ -906,7 +910,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) // method of ensuring the tx remains bumped. For example, the fee-bumping child could disappear // due to a replacement. // The only exception is v3 transactions. - if (ws.m_ptx->nVersion != 3 && ws.m_modified_fees < m_pool.m_min_relay_feerate.GetFee(ws.m_vsize) && !args.m_ignore_rejects.count(rejectmsg_mempoolfull)) { + if ((ws.m_ptx->nVersion != 3 || m_pool.m_truc_policy != TRUCPolicy::Enforce) && ws.m_modified_fees < m_pool.m_min_relay_feerate.GetFee(ws.m_vsize) && !args.m_ignore_rejects.count(rejectmsg_mempoolfull)) { // Even though this is a fee-related failure, this result is TX_MEMPOOL_POLICY, not // TX_RECONSIDERABLE, because it cannot be bypassed using package validation. return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met", @@ -986,7 +990,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) .descendant_size_vbytes = maybe_rbf_limits.descendant_size_vbytes + EXTRA_DESCENDANT_TX_SIZE_LIMIT, }; const auto error_message{util::ErrorString(ancestors).original}; - if (ws.m_vsize > EXTRA_DESCENDANT_TX_SIZE_LIMIT || ws.m_ptx->nVersion == 3) { + if (ws.m_vsize > EXTRA_DESCENDANT_TX_SIZE_LIMIT || (ws.m_ptx->nVersion == 3 && m_pool.m_truc_policy == TRUCPolicy::Enforce)) { return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too-long-mempool-chain", error_message); } ancestors = m_pool.CalculateMemPoolAncestors(*entry, cpfp_carve_out_limits); @@ -997,6 +1001,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) // Even though just checking direct mempool parents for inheritance would be sufficient, we // check using the full ancestor set here because it's more convenient to use what we have // already calculated. + if (m_pool.m_truc_policy == TRUCPolicy::Enforce) { if (const auto err{SingleV3Checks(ws.m_ptx, "truc-", reason, ignore_rejects, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) { // Disabled within package validation. if (err->second != nullptr && args.m_allow_replacement) { @@ -1014,7 +1019,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } else { return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, reason, err->first); } - } + }} // A transaction that spends outputs that would be replaced by it is invalid. Now // that we have the set of all ancestors we can detect this @@ -1391,13 +1396,14 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std:: // At this point we have all in-mempool ancestors, and we know every transaction's vsize. // Run the v3 checks on the package. + if (m_pool.m_truc_policy == TRUCPolicy::Enforce) { std::string reason; for (Workspace& ws : workspaces) { if (auto err{PackageV3Checks(ws.m_ptx, ws.m_vsize, "truc-", reason, args.m_ignore_rejects, txns, ws.m_ancestors)}) { package_state.Invalid(PackageValidationResult::PCKG_POLICY, reason, err.value()); return PackageMempoolAcceptResult(package_state, {}); } - } + }} // Transactions must meet two minimum feerates: the mempool minimum fee and min relay fee. // For transactions consisting of exactly one child and its parents, it suffices to use the diff --git a/test/functional/feature_rbf.py b/test/functional/feature_rbf.py index 7cf3df43a2..01ad2b99cc 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -33,6 +33,8 @@ class ReplaceByFeeTest(BitcoinTestFramework): "-limitancestorsize=101", "-limitdescendantcount=200", "-limitdescendantsize=101", + "-mempooltruc=accept", + "-paytxfee=0.00001", # this test confuses the fee estimator into nearly 1 BTC fees ], # second node has default mempool parameters [ @@ -97,8 +99,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): self.log.info("Running test opt-in...") self.test_opt_in(fullrbf=False) + self.test_opt_in(fullrbf=False, use_truc=True) self.nodes[0], self.nodes[-1] = self.nodes[-1], self.nodes[0] self.test_opt_in(fullrbf=True) + self.test_opt_in(fullrbf=True, use_truc=True) self.nodes[0], self.nodes[-1] = self.nodes[-1], self.nodes[0] self.log.info("Running test RPC...") @@ -511,7 +515,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): self.generate(normal_node, 1) self.wallet.rescan_utxos() - def test_opt_in(self, fullrbf): + def test_opt_in(self, fullrbf, use_truc=False): """Replacing should only work if orig tx opted in""" tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) @@ -570,15 +574,24 @@ class ReplaceByFeeTest(BitcoinTestFramework): # opt-in on one of the inputs # Transaction should be replaceable on either input + self.generate(self.nodes[0], 1) # clean mempool so parent txs don't trigger BIP125 + if use_truc: + kwargs = {'sequence': SEQUENCE_FINAL, 'version': 3} + else: + kwargs = {'sequence': [SEQUENCE_FINAL, 0xfffffffd]} + tx3a_txid = self.wallet.send_self_transfer_multi( from_node=self.nodes[0], utxos_to_spend=[tx1a_utxo, tx2a_utxo], - sequence=[SEQUENCE_FINAL, 0xfffffffd], fee_per_output=int(0.1 * COIN), + **kwargs )["txid"] # This transaction is shown as replaceable - assert_equal(self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], True) + if use_truc: + assert_equal(self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], False) + else: + assert_equal(self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], True) self.wallet.send_self_transfer( from_node=self.nodes[0],