Skip to content

Commit bdb33ec

Browse files
committed
Merge bitcoin/bitcoin#29735: AcceptMultipleTransactions: Fix workspace not being set as client_maxfeerate failure
4ba1d0b fuzz: Add coverage for client_maxfeerate (Greg Sanders) 91d7d8f AcceptMultipleTransactions: Fix workspace client_maxfeerate (Greg Sanders) f3aa5bd fill_mempool: assertions and docsctring update (Greg Sanders) a3da63e Move fill_mempool to util function (Greg Sanders) 73b68bd fill_mempool: remove subtest-specific comment (Greg Sanders) Pull request description: Bug causes an `Assume()` failure due to the expectation that the individual result should be invalid when done over `submitpackage` via rpc. Bug introduced by bitcoin/bitcoin#28950 , and I discovered it rebasing bitcoin/bitcoin#28984 since it's easier to hit in that test scenario. Tests in place were only checking `AcceptSingleTransaction`-level checks due to package evaluation only triggering when minfee is too high for the parent transaction. Added test along with fix, moving the fill_mempool utility into a common area for re-use. ACKs for top commit: glozow: reACK 4ba1d0b theStack: ACK 4ba1d0b ismaelsadeeq: re-ACK bitcoin/bitcoin@4ba1d0b via [diff](https://github.com/bitcoin/bitcoin/compare/4fe7d150eb3c85a6597d8fc910fe1490358197ad..4ba1d0b55339c3ea90e2bcd64662a06f0f90dd46) Tree-SHA512: 3729bdf7f25d04e232f173ccee04ddbb2afdaafa3d04292a01cecf58fb11b3b2bc133e8490277f1a67622b62d17929c242dc980f9bb647896beea4332ee35306
2 parents 3f6a6da + 4ba1d0b commit bdb33ec

File tree

5 files changed

+122
-60
lines changed

5 files changed

+122
-60
lines changed

src/test/fuzz/package_eval.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,14 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
276276
// (the package is a test accept and ATMP is a submission).
277277
auto single_submit = txs.size() == 1 && fuzzed_data_provider.ConsumeBool();
278278

279+
// Exercise client_maxfeerate logic
280+
std::optional<CFeeRate> client_maxfeerate{};
281+
if (fuzzed_data_provider.ConsumeBool()) {
282+
client_maxfeerate = CFeeRate(fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-1, 50 * COIN), 100);
283+
}
284+
279285
const auto result_package = WITH_LOCK(::cs_main,
280-
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, /*client_maxfeerate=*/{}));
286+
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, client_maxfeerate));
281287

282288
// Always set bypass_limits to false because it is not supported in ProcessNewPackage and
283289
// can be a source of divergence.

src/validation.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1366,7 +1366,9 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
13661366
// Individual modified feerate exceeded caller-defined max; abort
13671367
// N.B. this doesn't take into account CPFPs. Chunk-aware validation may be more robust.
13681368
if (args.m_client_maxfeerate && CFeeRate(ws.m_modified_fees, ws.m_vsize) > args.m_client_maxfeerate.value()) {
1369-
package_state.Invalid(PackageValidationResult::PCKG_TX, "max feerate exceeded");
1369+
// Need to set failure here both individually and at package level
1370+
ws.m_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "max feerate exceeded", "");
1371+
package_state.Invalid(PackageValidationResult::PCKG_TX, "transaction failed");
13701372
// Exit early to avoid doing pointless work. Update the failed tx result; the rest are unfinished.
13711373
results.emplace(ws.m_ptx->GetWitnessHash(), MempoolAcceptResult::Failure(ws.m_state));
13721374
return PackageMempoolAcceptResult(package_state, std::move(results));

test/functional/mempool_limit.py

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66

77
from decimal import Decimal
88

9-
from test_framework.blocktools import COINBASE_MATURITY
109
from test_framework.p2p import P2PTxInvStore
1110
from test_framework.test_framework import BitcoinTestFramework
1211
from test_framework.util import (
1312
assert_equal,
1413
assert_fee_amount,
1514
assert_greater_than,
1615
assert_raises_rpc_error,
17-
create_lots_of_big_transactions,
18-
gen_return_txouts,
16+
fill_mempool,
1917
)
2018
from test_framework.wallet import (
2119
COIN,
@@ -34,50 +32,6 @@ def set_test_params(self):
3432
]]
3533
self.supports_cli = False
3634

37-
def fill_mempool(self):
38-
"""Fill mempool until eviction."""
39-
self.log.info("Fill the mempool until eviction is triggered and the mempoolminfee rises")
40-
txouts = gen_return_txouts()
41-
node = self.nodes[0]
42-
miniwallet = self.wallet
43-
relayfee = node.getnetworkinfo()['relayfee']
44-
45-
tx_batch_size = 1
46-
num_of_batches = 75
47-
# Generate UTXOs to flood the mempool
48-
# 1 to create a tx initially that will be evicted from the mempool later
49-
# 75 transactions each with a fee rate higher than the previous one
50-
# And 1 more to verify that this tx does not get added to the mempool with a fee rate less than the mempoolminfee
51-
# And 2 more for the package cpfp test
52-
self.generate(miniwallet, 1 + (num_of_batches * tx_batch_size))
53-
54-
# Mine 99 blocks so that the UTXOs are allowed to be spent
55-
self.generate(node, COINBASE_MATURITY - 1)
56-
57-
self.log.debug("Create a mempool tx that will be evicted")
58-
tx_to_be_evicted_id = miniwallet.send_self_transfer(from_node=node, fee_rate=relayfee)["txid"]
59-
60-
# Increase the tx fee rate to give the subsequent transactions a higher priority in the mempool
61-
# The tx has an approx. vsize of 65k, i.e. multiplying the previous fee rate (in sats/kvB)
62-
# by 130 should result in a fee that corresponds to 2x of that fee rate
63-
base_fee = relayfee * 130
64-
65-
self.log.debug("Fill up the mempool with txs with higher fee rate")
66-
with node.assert_debug_log(["rolling minimum fee bumped"]):
67-
for batch_of_txid in range(num_of_batches):
68-
fee = (batch_of_txid + 1) * base_fee
69-
create_lots_of_big_transactions(miniwallet, node, fee, tx_batch_size, txouts)
70-
71-
self.log.debug("The tx should be evicted by now")
72-
# The number of transactions created should be greater than the ones present in the mempool
73-
assert_greater_than(tx_batch_size * num_of_batches, len(node.getrawmempool()))
74-
# Initial tx created should not be present in the mempool anymore as it had a lower fee rate
75-
assert tx_to_be_evicted_id not in node.getrawmempool()
76-
77-
self.log.debug("Check that mempoolminfee is larger than minrelaytxfee")
78-
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
79-
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
80-
8135
def test_rbf_carveout_disallowed(self):
8236
node = self.nodes[0]
8337

@@ -139,7 +93,7 @@ def test_mid_package_eviction(self):
13993
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
14094
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
14195

142-
self.fill_mempool()
96+
fill_mempool(self, node, self.wallet)
14397
current_info = node.getmempoolinfo()
14498
mempoolmin_feerate = current_info["mempoolminfee"]
14599

@@ -229,7 +183,7 @@ def test_mid_package_replacement(self):
229183
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
230184
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
231185

232-
self.fill_mempool()
186+
fill_mempool(self, node, self.wallet)
233187
current_info = node.getmempoolinfo()
234188
mempoolmin_feerate = current_info["mempoolminfee"]
235189

@@ -303,7 +257,7 @@ def run_test(self):
303257
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
304258
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
305259

306-
self.fill_mempool()
260+
fill_mempool(self, node, self.wallet)
307261

308262
# Deliberately try to create a tx with a fee less than the minimum mempool fee to assert that it does not get added to the mempool
309263
self.log.info('Create a mempool tx that will not pass mempoolminfee')

test/functional/rpc_packages.py

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
assert_equal,
1919
assert_fee_amount,
2020
assert_raises_rpc_error,
21+
fill_mempool,
2122
)
2223
from test_framework.wallet import (
2324
DEFAULT_FEE,
@@ -82,7 +83,8 @@ def run_test(self):
8283
self.test_conflicting()
8384
self.test_rbf()
8485
self.test_submitpackage()
85-
self.test_maxfeerate_maxburn_submitpackage()
86+
self.test_maxfeerate_submitpackage()
87+
self.test_maxburn_submitpackage()
8688

8789
def test_independent(self, coin):
8890
self.log.info("Test multiple independent transactions in a package")
@@ -358,7 +360,7 @@ def test_submitpackage(self):
358360
assert_equal(res["tx-results"][sec_wtxid]["error"], "version")
359361
peer.wait_for_broadcast([first_wtxid])
360362

361-
def test_maxfeerate_maxburn_submitpackage(self):
363+
def test_maxfeerate_submitpackage(self):
362364
node = self.nodes[0]
363365
# clear mempool
364366
deterministic_address = node.get_deterministic_priv_key().address
@@ -369,23 +371,72 @@ def test_maxfeerate_maxburn_submitpackage(self):
369371
minrate_btc_kvb = min([chained_txn["fee"] / chained_txn["tx"].get_vsize() * 1000 for chained_txn in chained_txns])
370372
chain_hex = [t["hex"] for t in chained_txns]
371373
pkg_result = node.submitpackage(chain_hex, maxfeerate=minrate_btc_kvb - Decimal("0.00000001"))
374+
375+
# First tx failed in single transaction evaluation, so package message is generic
376+
assert_equal(pkg_result["package_msg"], "transaction failed")
372377
assert_equal(pkg_result["tx-results"][chained_txns[0]["wtxid"]]["error"], "max feerate exceeded")
373378
assert_equal(pkg_result["tx-results"][chained_txns[1]["wtxid"]]["error"], "bad-txns-inputs-missingorspent")
374379
assert_equal(node.getrawmempool(), [])
375380

381+
# Make chain of two transactions where parent doesn't make minfee threshold
382+
# but child is too high fee
383+
# Lower mempool limit to make it easier to fill_mempool
384+
self.restart_node(0, extra_args=[
385+
"-datacarriersize=100000",
386+
"-maxmempool=5",
387+
"-persistmempool=0",
388+
])
389+
390+
fill_mempool(self, node, self.wallet)
391+
392+
minrelay = node.getmempoolinfo()["minrelaytxfee"]
393+
parent = self.wallet.create_self_transfer(
394+
fee_rate=minrelay,
395+
)
396+
397+
child = self.wallet.create_self_transfer(
398+
fee_rate=DEFAULT_FEE,
399+
utxo_to_spend=parent["new_utxo"],
400+
)
401+
402+
pkg_result = node.submitpackage([parent["hex"], child["hex"]], maxfeerate=DEFAULT_FEE - Decimal("0.00000001"))
403+
404+
# Child is connected even though parent is invalid and still reports fee exceeded
405+
# this implies sub-package evaluation of both entries together.
406+
assert_equal(pkg_result["package_msg"], "transaction failed")
407+
assert "mempool min fee not met" in pkg_result["tx-results"][parent["wtxid"]]["error"]
408+
assert_equal(pkg_result["tx-results"][child["wtxid"]]["error"], "max feerate exceeded")
409+
assert parent["txid"] not in node.getrawmempool()
410+
assert child["txid"] not in node.getrawmempool()
411+
412+
# Reset maxmempool, datacarriersize, reset dynamic mempool minimum feerate, and empty mempool.
413+
self.restart_node(0)
414+
415+
assert_equal(node.getrawmempool(), [])
416+
417+
def test_maxburn_submitpackage(self):
418+
node = self.nodes[0]
419+
420+
assert_equal(node.getrawmempool(), [])
421+
376422
self.log.info("Submitpackage maxburnamount arg testing")
377-
tx = tx_from_hex(chain_hex[1])
423+
chained_txns_burn = self.wallet.create_self_transfer_chain(chain_length=2)
424+
chained_burn_hex = [t["hex"] for t in chained_txns_burn]
425+
426+
tx = tx_from_hex(chained_burn_hex[1])
378427
tx.vout[-1].scriptPubKey = b'a' * 10001 # scriptPubKey bigger than 10k IsUnspendable
379-
chain_hex = [chain_hex[0], tx.serialize().hex()]
428+
chained_burn_hex = [chained_burn_hex[0], tx.serialize().hex()]
380429
# burn test is run before any package evaluation; nothing makes it in and we get broader exception
381-
assert_raises_rpc_error(-25, "Unspendable output exceeds maximum configured by user", node.submitpackage, chain_hex, 0, chained_txns[1]["new_utxo"]["value"] - Decimal("0.00000001"))
430+
assert_raises_rpc_error(-25, "Unspendable output exceeds maximum configured by user", node.submitpackage, chained_burn_hex, 0, chained_txns_burn[1]["new_utxo"]["value"] - Decimal("0.00000001"))
382431
assert_equal(node.getrawmempool(), [])
383432

433+
minrate_btc_kvb_burn = min([chained_txn_burn["fee"] / chained_txn_burn["tx"].get_vsize() * 1000 for chained_txn_burn in chained_txns_burn])
434+
384435
# Relax the restrictions for both and send it; parent gets through as own subpackage
385-
pkg_result = node.submitpackage(chain_hex, maxfeerate=minrate_btc_kvb, maxburnamount=chained_txns[1]["new_utxo"]["value"])
386-
assert "error" not in pkg_result["tx-results"][chained_txns[0]["wtxid"]]
436+
pkg_result = node.submitpackage(chained_burn_hex, maxfeerate=minrate_btc_kvb_burn, maxburnamount=chained_txns_burn[1]["new_utxo"]["value"])
437+
assert "error" not in pkg_result["tx-results"][chained_txns_burn[0]["wtxid"]]
387438
assert_equal(pkg_result["tx-results"][tx.getwtxid()]["error"], "scriptpubkey")
388-
assert_equal(node.getrawmempool(), [chained_txns[0]["txid"]])
439+
assert_equal(node.getrawmempool(), [chained_txns_burn[0]["txid"]])
389440

390441
if __name__ == "__main__":
391442
RPCPackagesTest().main()

test/functional/test_framework/util.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,55 @@ def check_node_connections(*, node, num_in, num_out):
496496
assert_equal(info["connections_in"], num_in)
497497
assert_equal(info["connections_out"], num_out)
498498

499+
def fill_mempool(test_framework, node, miniwallet):
500+
"""Fill mempool until eviction.
501+
502+
Allows for simpler testing of scenarios with floating mempoolminfee > minrelay
503+
Requires -datacarriersize=100000 and
504+
-maxmempool=5.
505+
It will not ensure mempools become synced as it
506+
is based on a single node and assumes -minrelaytxfee
507+
is 1 sat/vbyte.
508+
"""
509+
test_framework.log.info("Fill the mempool until eviction is triggered and the mempoolminfee rises")
510+
txouts = gen_return_txouts()
511+
relayfee = node.getnetworkinfo()['relayfee']
512+
513+
assert_equal(relayfee, Decimal('0.00001000'))
514+
515+
tx_batch_size = 1
516+
num_of_batches = 75
517+
# Generate UTXOs to flood the mempool
518+
# 1 to create a tx initially that will be evicted from the mempool later
519+
# 75 transactions each with a fee rate higher than the previous one
520+
test_framework.generate(miniwallet, 1 + (num_of_batches * tx_batch_size))
521+
522+
# Mine COINBASE_MATURITY - 1 blocks so that the UTXOs are allowed to be spent
523+
test_framework.generate(node, 100 - 1)
524+
525+
test_framework.log.debug("Create a mempool tx that will be evicted")
526+
tx_to_be_evicted_id = miniwallet.send_self_transfer(from_node=node, fee_rate=relayfee)["txid"]
527+
528+
# Increase the tx fee rate to give the subsequent transactions a higher priority in the mempool
529+
# The tx has an approx. vsize of 65k, i.e. multiplying the previous fee rate (in sats/kvB)
530+
# by 130 should result in a fee that corresponds to 2x of that fee rate
531+
base_fee = relayfee * 130
532+
533+
test_framework.log.debug("Fill up the mempool with txs with higher fee rate")
534+
with node.assert_debug_log(["rolling minimum fee bumped"]):
535+
for batch_of_txid in range(num_of_batches):
536+
fee = (batch_of_txid + 1) * base_fee
537+
create_lots_of_big_transactions(miniwallet, node, fee, tx_batch_size, txouts)
538+
539+
test_framework.log.debug("The tx should be evicted by now")
540+
# The number of transactions created should be greater than the ones present in the mempool
541+
assert_greater_than(tx_batch_size * num_of_batches, len(node.getrawmempool()))
542+
# Initial tx created should not be present in the mempool anymore as it had a lower fee rate
543+
assert tx_to_be_evicted_id not in node.getrawmempool()
544+
545+
test_framework.log.debug("Check that mempoolminfee is larger than minrelaytxfee")
546+
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
547+
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
499548

500549
# Transaction/Block functions
501550
#############################

0 commit comments

Comments
 (0)