diff --git a/src/txmempool.h b/src/txmempool.h --- a/src/txmempool.h +++ b/src/txmempool.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -598,14 +599,18 @@ std::set m_unbroadcast_txids GUARDED_BY(cs); /** - * Helper function to populate setAncestors with all the ancestors of entry - * and apply ancestor and descendant limits. + * Helper function to calculate all in-mempool ancestors of staged_ancestors + * and apply ancestor and descendant limits (including staged_ancestors + * themselves, entry_size and entry_count). + * param@[in] entry_size Virtual size to include in the limits. + * param@[in] entry_count How many entries to include in the + * limits. + * param@[in] staged_ancestors Should contain entries in the mempool. * param@[out] setAncestors Will be populated with all mempool - * ancestors of entry. - * param@[in] staged_ancestors Should contain mempool parents of entry. + * ancestors. */ bool CalculateAncestorsAndCheckLimits( - const CTxMemPoolEntry &entry, setEntries &setAncestors, + size_t entry_size, size_t entry_count, setEntries &setAncestors, CTxMemPoolEntry::Parents &staged_ancestors, uint64_t limitAncestorCount, uint64_t limitAncestorSize, uint64_t limitDescendantCount, uint64_t limitDescendantSize, std::string &errString) const @@ -736,6 +741,33 @@ std::string &errString, bool fSearchForParents = true) const EXCLUSIVE_LOCKS_REQUIRED(cs); + /** + * Calculate all in-mempool ancestors of a set of transactions not already + * in the mempool and check ancestor and descendant limits. Heuristics are + * used to estimate the ancestor and descendant count of all entries if the + * package were to be added to the mempool. The limits are applied to the + * union of all package transactions. For example, if the package has 3 + * transactions and limitAncestorCount = 50, the union of all 3 sets of + * ancestors (including the transactions themselves) must be <= 47. + * @param[in] package Transaction package being + * evaluated for acceptance to mempool. The transactions need not be + * direct ancestors/descendants of each other. + * @param[in] limitAncestorCount Max number of txns including + * ancestors. + * @param[in] limitAncestorSize Max size including ancestors. + * @param[in] limitDescendantCount Max number of txns including + * descendants. + * @param[in] limitDescendantSize Max size including descendants. + * @param[out] errString Populated with error reason if + * a limit is hit. + */ + bool CheckPackageLimits(const Package &package, uint64_t limitAncestorCount, + uint64_t limitAncestorSize, + uint64_t limitDescendantCount, + uint64_t limitDescendantSize, + std::string &errString) const + EXCLUSIVE_LOCKS_REQUIRED(cs); + /** * Populate setDescendants with all in-mempool descendants of hash. * Assumes that setDescendants includes all in-mempool descendants of diff --git a/src/txmempool.cpp b/src/txmempool.cpp --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -177,11 +177,11 @@ } bool CTxMemPool::CalculateAncestorsAndCheckLimits( - const CTxMemPoolEntry &entry, setEntries &setAncestors, + size_t entry_size, size_t entry_count, setEntries &setAncestors, CTxMemPoolEntry::Parents &staged_ancestors, uint64_t limitAncestorCount, uint64_t limitAncestorSize, uint64_t limitDescendantCount, uint64_t limitDescendantSize, std::string &errString) const { - size_t totalSizeWithAncestors = entry.GetTxSize(); + size_t totalSizeWithAncestors = entry_size; while (!staged_ancestors.empty()) { const CTxMemPoolEntry &stage = staged_ancestors.begin()->get(); @@ -191,7 +191,7 @@ staged_ancestors.erase(stage); totalSizeWithAncestors += stageit->GetTxSize(); - if (stageit->GetSizeWithDescendants() + entry.GetTxSize() > + if (stageit->GetSizeWithDescendants() + entry_size > limitDescendantSize) { errString = strprintf( "exceeds descendant size limit for tx %s [limit: %u]", @@ -199,7 +199,8 @@ return false; } - if (stageit->GetCountWithDescendants() + 1 > limitDescendantCount) { + if (stageit->GetCountWithDescendants() + entry_count > + limitDescendantCount) { errString = strprintf("too many descendants for tx %s [limit: %u]", stageit->GetTx().GetId().ToString(), limitDescendantCount); @@ -221,7 +222,7 @@ if (setAncestors.count(parent_it) == 0) { staged_ancestors.insert(parent); } - if (staged_ancestors.size() + setAncestors.size() + 1 > + if (staged_ancestors.size() + setAncestors.size() + entry_count > limitAncestorCount) { errString = strprintf("too many unconfirmed ancestors [limit: %u]", @@ -234,6 +235,46 @@ return true; } +bool CTxMemPool::CheckPackageLimits(const Package &package, + uint64_t limitAncestorCount, + uint64_t limitAncestorSize, + uint64_t limitDescendantCount, + uint64_t limitDescendantSize, + std::string &errString) const { + CTxMemPoolEntry::Parents staged_ancestors; + size_t total_size = 0; + for (const auto &tx : package) { + total_size += GetVirtualTransactionSize(*tx); + for (const auto &input : tx->vin) { + std::optional piter = GetIter(input.prevout.GetTxId()); + if (piter) { + staged_ancestors.insert(**piter); + if (staged_ancestors.size() + package.size() > + limitAncestorCount) { + errString = + strprintf("too many unconfirmed parents [limit: %u]", + limitAncestorCount); + return false; + } + } + } + } + // When multiple transactions are passed in, the ancestors and descendants + // of all transactions considered together must be within limits even if + // they are not interdependent. This may be stricter than the limits for + // each individual transaction. + setEntries setAncestors; + const auto ret = CalculateAncestorsAndCheckLimits( + total_size, package.size(), setAncestors, staged_ancestors, + limitAncestorCount, limitAncestorSize, limitDescendantCount, + limitDescendantSize, errString); + // It's possible to overestimate the ancestor/descendant totals. + if (!ret) { + errString.insert(0, "possibly "); + } + return ret; +} + bool CTxMemPool::CalculateMemPoolAncestors( const CTxMemPoolEntry &entry, setEntries &setAncestors, uint64_t limitAncestorCount, uint64_t limitAncestorSize, @@ -267,9 +308,9 @@ } return CalculateAncestorsAndCheckLimits( - entry, setAncestors, staged_ancestors, limitAncestorCount, - limitAncestorSize, limitDescendantCount, limitDescendantSize, - errString); + entry.GetTxSize(), /* entry_count */ 1, setAncestors, staged_ancestors, + limitAncestorCount, limitAncestorSize, limitDescendantCount, + limitDescendantSize, errString); } void CTxMemPool::UpdateAncestorsOf(bool add, txiter it, diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -771,7 +771,6 @@ // Make the coins created by this transaction available for subsequent // transactions in the package to spend. m_viewmempool.PackageAddTransaction(ws.m_ptx); - if (args.m_test_accept) { // When test_accept=true, transactions that pass PreChecks // are valid because there are no further mempool checks (passing @@ -781,6 +780,23 @@ } } + // Apply package mempool ancestor/descendant limits. Skip if there is only + // one transaction, because it's unnecessary. Also, CPFP carve out can + // increase the limit for individual transactions, but this exemption is + // not extended to packages in CheckPackageLimits(). + std::string err_string; + if (txns.size() > 1 && + !m_pool.CheckPackageLimits(txns, m_limit_ancestors, + m_limit_ancestor_size, m_limit_descendants, + m_limit_descendant_size, err_string)) { + // All transactions must have individually passed mempool ancestor and + // descendant limits inside of PreChecks(), so this is separate from an + // individual transaction error. + package_state.Invalid(PackageValidationResult::PCKG_POLICY, + "package-mempool-limits", err_string); + return PackageMempoolAcceptResult(package_state, std::move(results)); + } + return PackageMempoolAcceptResult(package_state, std::move(results)); } diff --git a/test/functional/mempool_package_limits.py b/test/functional/mempool_package_limits.py new file mode 100755 --- /dev/null +++ b/test/functional/mempool_package_limits.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test logic for limiting mempool and package ancestors/descendants.""" + +from decimal import Decimal + +from test_framework.address import ( + ADDRESS_ECREG_P2SH_OP_TRUE, + SCRIPTSIG_OP_TRUE, +) +from test_framework.messages import XEC, CTransaction, FromHex, ToHex +from test_framework.test_framework import BitcoinTestFramework +from test_framework.txtools import pad_tx +from test_framework.util import assert_equal +from test_framework.wallet import ( + bulk_transaction, + create_child_with_parents, + make_chain, +) + + +class MempoolPackageLimitsTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def run_test(self): + self.log.info("Generate blocks to create UTXOs") + node = self.nodes[0] + self.privkeys = [node.get_deterministic_priv_key().key] + self.address = node.get_deterministic_priv_key().address + self.coins = [] + # The last 100 coinbase transactions are premature + for b in node.generatetoaddress(200, self.address)[:100]: + coinbase = node.getblock(blockhash=b, verbosity=2)["tx"][0] + self.coins.append({ + "txid": coinbase["txid"], + "amount": coinbase["vout"][0]["value"], + "scriptPubKey": coinbase["vout"][0]["scriptPubKey"], + }) + + self.test_chain_limits() + self.test_desc_count_limits() + self.test_anc_count_limits() + self.test_anc_count_limits_2() + self.test_anc_count_limits_bushy() + + # The node will accept our (nonstandard) extra large OP_RETURN outputs + self.restart_node(0, extra_args=["-acceptnonstdtxn=1"]) + self.test_anc_size_limits() + self.test_desc_size_limits() + + def test_chain_limits_helper(self, mempool_count, package_count): + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + first_coin = self.coins.pop() + spk = None + txid = first_coin["txid"] + chain_hex = [] + chain_txns = [] + value = first_coin["amount"] + + for i in range(mempool_count + package_count): + (tx, txhex, value, spk) = make_chain( + node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.get_id() + if i < mempool_count: + node.sendrawtransaction(txhex) + assert_equal( + node.getrawmempool(verbose=True)[txid]["ancestorcount"], + i + 1) + else: + chain_hex.append(txhex) + chain_txns.append(tx) + testres_too_long = node.testmempoolaccept(rawtxs=chain_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] is True + for res in node.testmempoolaccept(rawtxs=chain_hex)]) + + def test_chain_limits(self): + """Create chains from mempool and package transactions that are longer than 50, + but only if both in-mempool and in-package transactions are considered together. + This checks that both mempool and in-package transactions are taken into account + when calculating ancestors/descendant limits. + """ + self.log.info( + "Check that in-package ancestors count for mempool ancestor limits") + + self.test_chain_limits_helper(mempool_count=49, package_count=2) + self.test_chain_limits_helper(mempool_count=2, package_count=49) + self.test_chain_limits_helper(mempool_count=26, package_count=26) + + def test_desc_count_limits(self): + """Create an 'A' shaped package with 49 transactions in the mempool and 2 in the package: + M1 + ^ ^ + M2a M2b + . . + . . + M25a M25b + ^ ^ + Pa Pb + The top ancestor in the package exceeds descendant limits but only if the in-mempool and in-package + descendants are all considered together (49 including in-mempool descendants and 51 including both + package transactions). + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + self.log.info( + "Check that in-mempool and in-package descendants are calculated properly in packages") + # Top parent in mempool, M1 + first_coin = self.coins.pop() + # Deduct reasonable fee and make 2 outputs + parent_value = (first_coin["amount"] - Decimal("200.00")) / 2 + inputs = [{"txid": first_coin["txid"], "vout": 0}] + outputs = [{self.address: parent_value}, + {ADDRESS_ECREG_P2SH_OP_TRUE: parent_value}] + rawtx = node.createrawtransaction(inputs, outputs) + + parent_signed = node.signrawtransactionwithkey( + hexstring=rawtx, privkeys=self.privkeys) + assert parent_signed["complete"] + parent_tx = FromHex(CTransaction(), parent_signed["hex"]) + parent_txid = parent_tx.rehash() + node.sendrawtransaction(parent_signed["hex"]) + + package_hex = [] + + # Chain A + spk = parent_tx.vout[0].scriptPubKey.hex() + value = parent_value + txid = parent_txid + for i in range(25): + (tx, txhex, value, spk) = make_chain( + node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.get_id() + if i < 24: + # M2a... M25a + node.sendrawtransaction(txhex) + else: + # Pa + package_hex.append(txhex) + + # Chain B + value = parent_value - Decimal("100.00") + rawtx_b = node.createrawtransaction( + [{"txid": parent_txid, "vout": 1}], {self.address: value}) + # M2b + tx_child_b = FromHex(CTransaction(), rawtx_b) + tx_child_b.vin[0].scriptSig = SCRIPTSIG_OP_TRUE + pad_tx(tx_child_b) + tx_child_b_hex = ToHex(tx_child_b) + node.sendrawtransaction(tx_child_b_hex) + spk = tx_child_b.vout[0].scriptPubKey.hex() + txid = tx_child_b.rehash() + for i in range(24): + (tx, txhex, value, spk) = make_chain( + node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.get_id() + if i < 23: + # M3b... M25b + node.sendrawtransaction(txhex) + else: + # Pb + package_hex.append(txhex) + + assert_equal(49, node.getmempoolinfo()["size"]) + assert_equal(2, len(package_hex)) + testres_too_long = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] is True + for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_count_limits(self): + """Create a 'V' shaped chain with 49 transactions in the mempool and 3 in the package: + M1a + ^ M1b + M2a ^ + . M2b + . . + . . + M25a M24b + ^ ^ + Pa Pb + ^ ^ + Pc + The lowest descendant, Pc, exceeds ancestor limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + package_hex = [] + parents_tx = [] + values = [] + scripts = [] + + self.log.info( + "Check that in-mempool and in-package ancestors are calculated " + "properly in packages") + + # Two chains of 26 & 25 transactions + for chain_length in [26, 25]: + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + for i in range(chain_length): + (tx, txhex, value, spk) = make_chain( + node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.get_id() + if i < chain_length - 1: + node.sendrawtransaction(txhex) + else: + # Save the last transaction for the package + package_hex.append(txhex) + parents_tx.append(tx) + scripts.append(spk) + values.append(value) + + # Child Pc + child_hex = create_child_with_parents( + node, self.address, self.privkeys, parents_tx, values, scripts) + package_hex.append(child_hex) + + assert_equal(49, node.getmempoolinfo()["size"]) + assert_equal(3, len(package_hex)) + testres_too_long = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] is True + for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_count_limits_2(self): + """Create a 'Y' shaped chain with 49 transactions in the mempool and 2 in the package: + M1a + ^ M1b + M2a ^ + . M2b + . . + . . + M25a M24b + ^ ^ + Pc + ^ + Pd + The lowest descendant, Pc, exceeds ancestor limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + parents_tx = [] + values = [] + scripts = [] + + self.log.info( + "Check that in-mempool and in-package ancestors are calculated properly in packages") + # Two chains of 25 & 24 transactions + for chain_length in [25, 24]: + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + for i in range(chain_length): + (tx, txhex, value, spk) = make_chain( + node, self.address, self.privkeys, txid, value, 0, spk) + txid = tx.get_id() + node.sendrawtransaction(txhex) + if i == chain_length - 1: + # last 2 transactions will be the parents of Pc + parents_tx.append(tx) + values.append(value) + scripts.append(spk) + + # Child Pc + pc_hex = create_child_with_parents( + node, self.address, self.privkeys, parents_tx, values, scripts) + pc_tx = FromHex(CTransaction(), pc_hex) + pc_value = sum(values) - Decimal("100.00") + pc_spk = pc_tx.vout[0].scriptPubKey.hex() + + # Child Pd + (_, pd_hex, _, _) = make_chain( + node, self.address, self.privkeys, pc_tx.get_id(), pc_value, 0, pc_spk) + + assert_equal(49, node.getmempoolinfo()["size"]) + testres_too_long = node.testmempoolaccept(rawtxs=[pc_hex, pd_hex]) + for txres in testres_too_long: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] is True + for res in node.testmempoolaccept(rawtxs=[pc_hex, pd_hex])]) + + def test_anc_count_limits_bushy(self): + """Create a tree with 45 transactions in the mempool and 6 in the package: + M1...M9 M10...M18 M19...M27 M28...M36 M37...M45 + ^ ^ ^ ^ ^ (each with 9 parents) + P0 P1 P2 P3 P4 + ^ ^ ^ ^ ^ (5 parents) + PC + Where M(9i+1)...M+(9i+9) are the parents of Pi and P0, P1, P2, P3, and P4 are the parents of PC. + P0... P4 individually only have 9 parents each, and PC has no in-mempool parents. But + combined, PC has 50 in-mempool and in-package parents. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + package_hex = [] + parent_txns = [] + parent_values = [] + scripts = [] + # Make package transactions P0 ... P4 + for _ in range(5): + gp_tx = [] + gp_values = [] + gp_scripts = [] + # Make mempool transactions M(9i+1)...M(9i+9) + for _ in range(9): + parent_coin = self.coins.pop() + value = parent_coin["amount"] + txid = parent_coin["txid"] + (tx, txhex, value, spk) = make_chain( + node, self.address, self.privkeys, txid, value) + gp_tx.append(tx) + gp_values.append(value) + gp_scripts.append(spk) + node.sendrawtransaction(txhex) + # Package transaction Pi + pi_hex = create_child_with_parents( + node, self.address, self.privkeys, gp_tx, gp_values, gp_scripts) + package_hex.append(pi_hex) + pi_tx = FromHex(CTransaction(), pi_hex) + parent_txns.append(pi_tx) + parent_values.append(Decimal(pi_tx.vout[0].nValue) / XEC) + scripts.append(pi_tx.vout[0].scriptPubKey.hex()) + # Package transaction PC + package_hex.append( + create_child_with_parents(node, self.address, self.privkeys, + parent_txns, parent_values, scripts)) + + assert_equal(45, node.getmempoolinfo()["size"]) + assert_equal(6, len(package_hex)) + testres = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] is True + for res in node.testmempoolaccept(rawtxs=package_hex)]) + + def test_anc_size_limits(self): + """Test Case with 2 independent transactions in the mempool and a parent + child in the + package, where the package parent is the child of both mempool transactions (30KB each): + A B + ^ ^ + C + ^ + D + The lowest descendant, D, exceeds ancestor size limits, but only if the in-mempool + and in-package ancestors are all considered together. + """ + node = self.nodes[0] + + assert_equal(0, node.getmempoolinfo()["size"]) + parents_tx = [] + values = [] + scripts = [] + target_size = 30_000 + # 10 sats/B + high_fee = Decimal("3000.00") + self.log.info( + "Check that in-mempool and in-package ancestor size limits are calculated properly in packages") + # Mempool transactions A and B + for _ in range(2): + spk = None + top_coin = self.coins.pop() + txid = top_coin["txid"] + value = top_coin["amount"] + (tx, _, _, _) = make_chain( + node, self.address, self.privkeys, txid, value, 0, spk, high_fee) + bulked_tx = bulk_transaction(tx, node, target_size, self.privkeys) + node.sendrawtransaction(ToHex(bulked_tx)) + parents_tx.append(bulked_tx) + values.append(Decimal(bulked_tx.vout[0].nValue) / XEC) + scripts.append(bulked_tx.vout[0].scriptPubKey.hex()) + + # Package transaction C + small_pc_hex = create_child_with_parents( + node, self.address, self.privkeys, parents_tx, values, scripts, high_fee) + pc_tx = bulk_transaction( + FromHex(CTransaction(), small_pc_hex), node, target_size, self.privkeys) + pc_value = Decimal(pc_tx.vout[0].nValue) / XEC + pc_spk = pc_tx.vout[0].scriptPubKey.hex() + pc_hex = ToHex(pc_tx) + + # Package transaction D + (small_pd, _, val, spk) = make_chain( + node, self.address, self.privkeys, pc_tx.rehash(), pc_value, 0, pc_spk, high_fee) + prevtxs = [{ + "txid": pc_tx.get_id(), + "vout": 0, + "scriptPubKey": spk, + "amount": pc_value, + }] + pd_tx = bulk_transaction( + small_pd, node, target_size, self.privkeys, prevtxs) + pd_hex = ToHex(pd_tx) + + assert_equal(2, node.getmempoolinfo()["size"]) + testres_too_heavy = node.testmempoolaccept(rawtxs=[pc_hex, pd_hex]) + print(testres_too_heavy) + for txres in testres_too_heavy: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] is True + for res in node.testmempoolaccept(rawtxs=[pc_hex, pd_hex])]) + + def test_desc_size_limits(self): + """Create 3 mempool transactions and 2 package transactions (25KB each): + Ma + ^ ^ + Mb Mc + ^ ^ + Pd Pe + The top ancestor in the package exceeds descendant size limits but only if the in-mempool + and in-package descendants are all considered together. + """ + node = self.nodes[0] + assert_equal(0, node.getmempoolinfo()["size"]) + target_size = 21_000 + # 10 sats/vB + high_fee = Decimal("2100.00") + self.log.info( + "Check that in-mempool and in-package descendant sizes are calculated properly in packages") + # Top parent in mempool, Ma + first_coin = self.coins.pop() + # Deduct fee and make 2 outputs + parent_value = (first_coin["amount"] - high_fee) / 2 + inputs = [{"txid": first_coin["txid"], "vout": 0}] + outputs = [{self.address: parent_value}, + {ADDRESS_ECREG_P2SH_OP_TRUE: parent_value}] + rawtx = node.createrawtransaction(inputs, outputs) + parent_tx = bulk_transaction( + FromHex(CTransaction(), rawtx), node, target_size, self.privkeys) + node.sendrawtransaction(ToHex(parent_tx)) + + package_hex = [] + # Two legs (left and right) + for j in range(2): + # Mempool transaction (Mb and Mc) + spk = parent_tx.vout[j].scriptPubKey.hex() + value = Decimal(parent_tx.vout[j].nValue) / XEC + txid = parent_tx.get_id() + prevtxs = [{ + "txid": txid, + "vout": j, + "scriptPubKey": spk, + "amount": value, + }] + if j == 0: + # normal key + (tx_small, _, _, _) = make_chain( + node, self.address, self.privkeys, txid, value, j, spk, high_fee) + mempool_tx = bulk_transaction( + tx_small, node, target_size, self.privkeys, prevtxs) + + else: + # OP_TRUE + inputs = [{"txid": txid, "vout": 1}] + outputs = {self.address: value - high_fee} + small_tx = FromHex( + CTransaction(), node.createrawtransaction(inputs, outputs)) + mempool_tx = bulk_transaction( + small_tx, node, target_size, None, prevtxs) + node.sendrawtransaction(ToHex(mempool_tx)) + + # Package transaction (Pd and Pe) + spk = mempool_tx.vout[0].scriptPubKey.hex() + value = Decimal(mempool_tx.vout[0].nValue) / XEC + txid = mempool_tx.get_id() + (tx_small, _, _, _) = make_chain( + node, self.address, self.privkeys, txid, value, 0, spk, high_fee) + prevtxs = [{ + "txid": txid, + "vout": 0, + "scriptPubKey": spk, + "amount": value, + }] + package_tx = bulk_transaction( + tx_small, node, target_size, self.privkeys, prevtxs) + package_hex.append(ToHex(package_tx)) + + assert_equal(3, node.getmempoolinfo()["size"]) + assert_equal(2, len(package_hex)) + testres_too_heavy = node.testmempoolaccept(rawtxs=package_hex) + for txres in testres_too_heavy: + assert_equal(txres["package-error"], "package-mempool-limits") + + # Clear mempool and check that the package passes now + node.generate(1) + assert all([res["allowed"] is True + for res in node.testmempoolaccept(rawtxs=package_hex)]) + + +if __name__ == "__main__": + MempoolPackageLimitsTest().main() diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -4,6 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """A limited-functionality wallet, which may replace a real wallet in tests""" +from copy import deepcopy from decimal import Decimal from typing import Optional @@ -18,9 +19,16 @@ CTxIn, CTxOut, FromHex, + ToHex, ) from test_framework.txtools import pad_tx -from test_framework.util import assert_equal, satoshi_round +from test_framework.util import ( + assert_equal, + assert_greater_than_or_equal, + satoshi_round, +) + +DEFAULT_FEE = Decimal("100.00") class MiniWallet: @@ -91,14 +99,14 @@ def make_chain(node, address, privkeys, parent_txid, parent_value, n=0, - parent_locking_script=None): + parent_locking_script=None, fee=DEFAULT_FEE): """Build a transaction that spends parent_txid.vout[n] and produces one output with amount = parent_value with a fee deducted. Return tuple (CTransaction object, raw hex, nValue, scriptPubKey of the output created). """ inputs = [{"txid": parent_txid, "vout": n}] - my_value = parent_value - Decimal("100.00") + my_value = parent_value - fee outputs = {address: my_value} rawtx = node.createrawtransaction(inputs, outputs) prevtxs = [{ @@ -115,12 +123,12 @@ def create_child_with_parents(node, address, privkeys, parents_tx, values, - locking_scripts): + locking_scripts, fee=DEFAULT_FEE): """Creates a transaction that spends the first output of each parent in parents_tx.""" num_parents = len(parents_tx) total_value = sum(values) inputs = [{"txid": tx.get_id(), "vout": 0} for tx in parents_tx] - outputs = {address: total_value - num_parents * Decimal("100.00")} + outputs = {address: total_value - fee} rawtx_child = node.createrawtransaction(inputs, outputs) prevtxs = [] for i in range(num_parents): @@ -152,3 +160,23 @@ chain_txns.append(tx) return (chain_hex, chain_txns) + + +def bulk_transaction( + tx: CTransaction, node, target_size: int, privkeys=None, prevtxs=None +) -> CTransaction: + """Return a padded and signed transaction. The original transaction is left + unaltered. + If privkeys is not specified, it is assumed that the transaction has an + anyone-can-spend output as unique output. + """ + tx_heavy = deepcopy(tx) + pad_tx(tx_heavy, target_size) + assert_greater_than_or_equal(tx_heavy.billable_size(), target_size) + if privkeys is not None: + signed_tx = node.signrawtransactionwithkey( + ToHex(tx_heavy), privkeys, prevtxs) + return FromHex(CTransaction(), signed_tx["hex"]) + # OP_TRUE + tx_heavy.vin[0].scriptSig = SCRIPTSIG_OP_TRUE + return tx_heavy