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_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
|
||||||
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], 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):
|
def run_test(self):
|
||||||
node = self.nodes[0]
|
node = self.nodes[0]
|
||||||
self.wallet = MiniWallet(node)
|
self.wallet = MiniWallet(node)
|
||||||
miniwallet = self.wallet
|
miniwallet = self.wallet
|
||||||
|
|
||||||
# Generate coins needed to create transactions in the subtests (excluding coins used in fill_mempool).
|
# 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']
|
relayfee = node.getnetworkinfo()['relayfee']
|
||||||
self.log.info('Check that mempoolminfee is minrelaytxfee')
|
self.log.info('Check that mempoolminfee is minrelaytxfee')
|
||||||
|
@ -163,6 +322,9 @@ class MempoolLimitTest(BitcoinTestFramework):
|
||||||
self.stop_node(0)
|
self.stop_node(0)
|
||||||
self.nodes[0].assert_start_raises_init_error(["-maxmempool=4"], "Error: -maxmempool must be at least 5 MB")
|
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__':
|
if __name__ == '__main__':
|
||||||
MempoolLimitTest().main()
|
MempoolLimitTest().main()
|
||||||
|
|
Loading…
Reference in a new issue