mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-10 03:47:29 -03:00
[test] mempool coins disappearing mid-package evaluation
Test for scenario(s) outlined in PR 28251. Test what happens when a package transaction spends a mempool coin which is fetched and then disappears mid-package evaluation due to eviction or replacement.
This commit is contained in:
parent
a67f460c3f
commit
32c1dd1ad6
1 changed files with 163 additions and 1 deletions
|
@ -78,13 +78,172 @@ class MempoolLimitTest(BitcoinTestFramework):
|
|||
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
|
||||
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
|
||||
|
||||
def test_mid_package_eviction(self):
|
||||
node = self.nodes[0]
|
||||
self.log.info("Check a package where each parent passes the current mempoolminfee but would cause eviction before package submission terminates")
|
||||
|
||||
self.restart_node(0, extra_args=self.extra_args[0])
|
||||
|
||||
# Restarting the node resets mempool minimum feerate
|
||||
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
|
||||
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
|
||||
|
||||
self.fill_mempool()
|
||||
current_info = node.getmempoolinfo()
|
||||
mempoolmin_feerate = current_info["mempoolminfee"]
|
||||
|
||||
package_hex = []
|
||||
# UTXOs to be spent by the ultimate child transaction
|
||||
parent_utxos = []
|
||||
|
||||
evicted_weight = 8000
|
||||
# Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
|
||||
# mempool overflows and evicts by descendant score. It's important that the eviction doesn't
|
||||
# happen in the middle of package evaluation, as it can invalidate the coins cache.
|
||||
mempool_evicted_tx = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee=(mempoolmin_feerate / 1000) * (evicted_weight // 4) + Decimal('0.000001'),
|
||||
target_weight=evicted_weight,
|
||||
confirmed_only=True
|
||||
)
|
||||
# Already in mempool when package is submitted.
|
||||
assert mempool_evicted_tx["txid"] in node.getrawmempool()
|
||||
|
||||
# This parent spends the above mempool transaction that exists when its inputs are first
|
||||
# looked up, but disappears later. It is rejected for being too low fee (but eligible for
|
||||
# reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
|
||||
# coin is no longer available, but the cache could still contains the tx.
|
||||
cpfp_parent = self.wallet.create_self_transfer(
|
||||
utxo_to_spend=mempool_evicted_tx["new_utxo"],
|
||||
fee_rate=mempoolmin_feerate - Decimal('0.00001'),
|
||||
confirmed_only=True)
|
||||
package_hex.append(cpfp_parent["hex"])
|
||||
parent_utxos.append(cpfp_parent["new_utxo"])
|
||||
assert_equal(node.testmempoolaccept([cpfp_parent["hex"]])[0]["reject-reason"], "mempool min fee not met")
|
||||
|
||||
self.wallet.rescan_utxos()
|
||||
|
||||
# Series of parents that don't need CPFP and are submitted individually. Each one is large and
|
||||
# high feerate, which means they should trigger eviction but not be evicted.
|
||||
parent_weight = 100000
|
||||
num_big_parents = 3
|
||||
assert_greater_than(parent_weight * num_big_parents, current_info["maxmempool"] - current_info["bytes"])
|
||||
parent_fee = (100 * mempoolmin_feerate / 1000) * (parent_weight // 4)
|
||||
|
||||
big_parent_txids = []
|
||||
for i in range(num_big_parents):
|
||||
parent = self.wallet.create_self_transfer(fee=parent_fee, target_weight=parent_weight, confirmed_only=True)
|
||||
parent_utxos.append(parent["new_utxo"])
|
||||
package_hex.append(parent["hex"])
|
||||
big_parent_txids.append(parent["txid"])
|
||||
# There is room for each of these transactions independently
|
||||
assert node.testmempoolaccept([parent["hex"]])[0]["allowed"]
|
||||
|
||||
# Create a child spending everything, bumping cpfp_parent just above mempool minimum
|
||||
# feerate. It's important not to bump too much as otherwise mempool_evicted_tx would not be
|
||||
# evicted, making this test much less meaningful.
|
||||
approx_child_vsize = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos)["tx"].get_vsize()
|
||||
cpfp_fee = (mempoolmin_feerate / 1000) * (cpfp_parent["tx"].get_vsize() + approx_child_vsize) - cpfp_parent["fee"]
|
||||
# Specific number of satoshis to fit within a small window. The parent_cpfp + child package needs to be
|
||||
# - When there is mid-package eviction, high enough feerate to meet the new mempoolminfee
|
||||
# - When there is no mid-package eviction, low enough feerate to be evicted immediately after submission.
|
||||
magic_satoshis = 1200
|
||||
cpfp_satoshis = int(cpfp_fee * COIN) + magic_satoshis
|
||||
|
||||
child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=cpfp_satoshis)
|
||||
package_hex.append(child["hex"])
|
||||
|
||||
# Package should be submitted, temporarily exceeding maxmempool, and then evicted.
|
||||
with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]):
|
||||
assert_raises_rpc_error(-26, "mempool full", node.submitpackage, package_hex)
|
||||
|
||||
# Maximum size must never be exceeded.
|
||||
assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
|
||||
|
||||
# Evicted transaction and its descendants must not be in mempool.
|
||||
resulting_mempool_txids = node.getrawmempool()
|
||||
assert mempool_evicted_tx["txid"] not in resulting_mempool_txids
|
||||
assert cpfp_parent["txid"] not in resulting_mempool_txids
|
||||
assert child["txid"] not in resulting_mempool_txids
|
||||
for txid in big_parent_txids:
|
||||
assert txid in resulting_mempool_txids
|
||||
|
||||
def test_mid_package_replacement(self):
|
||||
node = self.nodes[0]
|
||||
self.log.info("Check a package where an early tx depends on a later-replaced mempool tx")
|
||||
|
||||
self.restart_node(0, extra_args=self.extra_args[0])
|
||||
|
||||
# Restarting the node resets mempool minimum feerate
|
||||
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
|
||||
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
|
||||
|
||||
self.fill_mempool()
|
||||
current_info = node.getmempoolinfo()
|
||||
mempoolmin_feerate = current_info["mempoolminfee"]
|
||||
|
||||
# Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
|
||||
# mempool overflows and evicts by descendant score. It's important that the eviction doesn't
|
||||
# happen in the middle of package evaluation, as it can invalidate the coins cache.
|
||||
double_spent_utxo = self.wallet.get_utxo(confirmed_only=True)
|
||||
replaced_tx = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
utxo_to_spend=double_spent_utxo,
|
||||
fee_rate=mempoolmin_feerate,
|
||||
confirmed_only=True
|
||||
)
|
||||
# Already in mempool when package is submitted.
|
||||
assert replaced_tx["txid"] in node.getrawmempool()
|
||||
|
||||
# This parent spends the above mempool transaction that exists when its inputs are first
|
||||
# looked up, but disappears later. It is rejected for being too low fee (but eligible for
|
||||
# reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
|
||||
# coin is no longer available, but the cache could still contain the tx.
|
||||
cpfp_parent = self.wallet.create_self_transfer(
|
||||
utxo_to_spend=replaced_tx["new_utxo"],
|
||||
fee_rate=mempoolmin_feerate - Decimal('0.00001'),
|
||||
confirmed_only=True)
|
||||
|
||||
self.wallet.rescan_utxos()
|
||||
|
||||
# Parent that replaces the parent of cpfp_parent.
|
||||
replacement_tx = self.wallet.create_self_transfer(
|
||||
utxo_to_spend=double_spent_utxo,
|
||||
fee_rate=10*mempoolmin_feerate,
|
||||
confirmed_only=True
|
||||
)
|
||||
parent_utxos = [cpfp_parent["new_utxo"], replacement_tx["new_utxo"]]
|
||||
|
||||
# Create a child spending everything, CPFPing the low-feerate parent.
|
||||
approx_child_vsize = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos)["tx"].get_vsize()
|
||||
cpfp_fee = (2 * mempoolmin_feerate / 1000) * (cpfp_parent["tx"].get_vsize() + approx_child_vsize) - cpfp_parent["fee"]
|
||||
child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=int(cpfp_fee * COIN))
|
||||
# It's very important that the cpfp_parent is before replacement_tx so that its input (from
|
||||
# replaced_tx) is first looked up *before* replacement_tx is submitted.
|
||||
package_hex = [cpfp_parent["hex"], replacement_tx["hex"], child["hex"]]
|
||||
|
||||
# Package should be submitted, temporarily exceeding maxmempool, and then evicted.
|
||||
assert_raises_rpc_error(-26, "bad-txns-inputs-missingorspent", node.submitpackage, package_hex)
|
||||
|
||||
# Maximum size must never be exceeded.
|
||||
assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
|
||||
|
||||
resulting_mempool_txids = node.getrawmempool()
|
||||
# The replacement should be successful.
|
||||
assert replacement_tx["txid"] in resulting_mempool_txids
|
||||
# The replaced tx and all of its descendants must not be in mempool.
|
||||
assert replaced_tx["txid"] not in resulting_mempool_txids
|
||||
assert cpfp_parent["txid"] not in resulting_mempool_txids
|
||||
assert child["txid"] not in resulting_mempool_txids
|
||||
|
||||
|
||||
def run_test(self):
|
||||
node = self.nodes[0]
|
||||
self.wallet = MiniWallet(node)
|
||||
miniwallet = self.wallet
|
||||
|
||||
# Generate coins needed to create transactions in the subtests (excluding coins used in fill_mempool).
|
||||
self.generate(miniwallet, 10)
|
||||
self.generate(miniwallet, 20)
|
||||
|
||||
relayfee = node.getnetworkinfo()['relayfee']
|
||||
self.log.info('Check that mempoolminfee is minrelaytxfee')
|
||||
|
@ -163,6 +322,9 @@ class MempoolLimitTest(BitcoinTestFramework):
|
|||
self.stop_node(0)
|
||||
self.nodes[0].assert_start_raises_init_error(["-maxmempool=4"], "Error: -maxmempool must be at least 5 MB")
|
||||
|
||||
self.test_mid_package_replacement()
|
||||
self.test_mid_package_eviction()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
MempoolLimitTest().main()
|
||||
|
|
Loading…
Reference in a new issue