historical/moneyrocket.git/test/functional/feature_rbf.py
2024-01-16 11:20:27 -06:00

731 lines
28 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2014-2022 The Moneyrocket Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the RBF code."""
from decimal import Decimal
from test_framework.messages import (
MAX_BIP125_RBF_SEQUENCE,
COIN,
SEQUENCE_FINAL,
)
from test_framework.test_framework import MoneyrocketTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)
from test_framework.wallet import MiniWallet
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE
MAX_REPLACEMENT_LIMIT = 100
class ReplaceByFeeTest(MoneyrocketTestFramework):
def add_options(self, parser):
self.add_wallet_options(parser)
def set_test_params(self):
self.num_nodes = 2
self.extra_args = [
[
"-maxorphantx=1000",
"-limitancestorcount=50",
"-limitancestorsize=101",
"-limitdescendantcount=200",
"-limitdescendantsize=101",
],
# second node has default mempool parameters
[
],
]
self.supports_cli = False
def run_test(self):
self.wallet = MiniWallet(self.nodes[0])
self.log.info("Running test simple doublespend...")
self.test_simple_doublespend()
self.log.info("Running test doublespend chain...")
self.test_doublespend_chain()
self.log.info("Running test doublespend tree...")
self.test_doublespend_tree()
self.log.info("Running test replacement feeperkb...")
self.test_replacement_feeperkb()
self.log.info("Running test spends of conflicting outputs...")
self.test_spends_of_conflicting_outputs()
self.log.info("Running test new unconfirmed inputs...")
self.test_new_unconfirmed_inputs()
self.log.info("Running test too many replacements...")
self.test_too_many_replacements()
self.log.info("Running test too many replacements using default mempool params...")
self.test_too_many_replacements_with_default_mempool_params()
self.log.info("Running test opt-in...")
self.test_opt_in()
self.log.info("Running test RPC...")
self.test_rpc()
self.log.info("Running test prioritised transactions...")
self.test_prioritised_transactions()
self.log.info("Running test no inherited signaling...")
self.test_no_inherited_signaling()
self.log.info("Running test replacement relay fee...")
self.test_replacement_relay_fee()
self.log.info("Running test full replace by fee...")
self.test_fullrbf()
self.log.info("Passed")
def make_utxo(self, node, amount, *, confirmed=True, scriptPubKey=None):
"""Create a txout with a given amount and scriptPubKey
confirmed - txout created will be confirmed in the blockchain;
unconfirmed otherwise.
"""
txid, n = self.wallet.send_to(from_node=node, scriptPubKey=scriptPubKey or self.wallet.get_scriptPubKey(), amount=amount)
if confirmed:
mempool_size = len(node.getrawmempool())
while mempool_size > 0:
self.generate(node, 1)
new_size = len(node.getrawmempool())
# Error out if we have something stuck in the mempool, as this
# would likely be a bug.
assert new_size < mempool_size
mempool_size = new_size
return self.wallet.get_utxo(txid=txid, vout=n)
def test_simple_doublespend(self):
"""Simple doublespend"""
# we use MiniWallet to create a transaction template with inputs correctly set,
# and modify the output (amount, scriptPubKey) according to our needs
tx = self.wallet.create_self_transfer()["tx"]
tx1a_txid = self.nodes[0].sendrawtransaction(tx.serialize().hex())
# Should fail because we haven't changed the fee
tx.vout[0].scriptPubKey[-1] ^= 1
# This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0)
# Extra 0.1 RKT fee
tx.vout[0].nValue -= int(0.1 * COIN)
tx1b_hex = tx.serialize().hex()
# Works when enabled
tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0)
mempool = self.nodes[0].getrawmempool()
assert tx1a_txid not in mempool
assert tx1b_txid in mempool
assert_equal(tx1b_hex, self.nodes[0].getrawtransaction(tx1b_txid))
def test_doublespend_chain(self):
"""Doublespend of a long chain"""
initial_nValue = 5 * COIN
tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue)
prevout = tx0_outpoint
remaining_value = initial_nValue
chain_txids = []
while remaining_value > 1 * COIN:
remaining_value -= int(0.1 * COIN)
prevout = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=prevout,
sequence=0,
fee=Decimal("0.1"),
)["new_utxo"]
chain_txids.append(prevout["txid"])
# Whether the double-spend is allowed is evaluated by including all
# child fees - 4 RKT - so this attempt is rejected.
dbl_tx = self.wallet.create_self_transfer(
utxo_to_spend=tx0_outpoint,
sequence=0,
fee=Decimal("3"),
)["tx"]
dbl_tx_hex = dbl_tx.serialize().hex()
# This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)
# Accepted with sufficient fee
dbl_tx.vout[0].nValue = int(0.1 * COIN)
dbl_tx_hex = dbl_tx.serialize().hex()
self.nodes[0].sendrawtransaction(dbl_tx_hex, 0)
mempool = self.nodes[0].getrawmempool()
for doublespent_txid in chain_txids:
assert doublespent_txid not in mempool
def test_doublespend_tree(self):
"""Doublespend of a big tree of transactions"""
initial_nValue = 5 * COIN
tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue)
def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.00001 * COIN, _total_txs=None):
if _total_txs is None:
_total_txs = [0]
if _total_txs[0] >= max_txs:
return
txout_value = (initial_value - fee) // tree_width
if txout_value < fee:
return
tx = self.wallet.send_self_transfer_multi(
utxos_to_spend=[prevout],
from_node=self.nodes[0],
sequence=0,
num_outputs=tree_width,
amount_per_output=txout_value,
)
yield tx["txid"]
_total_txs[0] += 1
for utxo in tx["new_utxos"]:
for x in branch(utxo, txout_value,
max_txs,
tree_width=tree_width, fee=fee,
_total_txs=_total_txs):
yield x
fee = int(0.00001 * COIN)
n = MAX_REPLACEMENT_LIMIT
tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee))
assert_equal(len(tree_txs), n)
# Attempt double-spend, will fail because too little fee paid
dbl_tx_hex = self.wallet.create_self_transfer(
utxo_to_spend=tx0_outpoint,
sequence=0,
fee=(Decimal(fee) / COIN) * n,
)["hex"]
# This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)
# 0.1 RKT fee is enough
dbl_tx_hex = self.wallet.create_self_transfer(
utxo_to_spend=tx0_outpoint,
sequence=0,
fee=(Decimal(fee) / COIN) * n + Decimal("0.1"),
)["hex"]
self.nodes[0].sendrawtransaction(dbl_tx_hex, 0)
mempool = self.nodes[0].getrawmempool()
for txid in tree_txs:
assert txid not in mempool
# Try again, but with more total transactions than the "max txs
# double-spent at once" anti-DoS limit.
for n in (MAX_REPLACEMENT_LIMIT + 1, MAX_REPLACEMENT_LIMIT * 2):
fee = int(0.00001 * COIN)
tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue)
tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee))
assert_equal(len(tree_txs), n)
dbl_tx_hex = self.wallet.create_self_transfer(
utxo_to_spend=tx0_outpoint,
sequence=0,
fee=2 * (Decimal(fee) / COIN) * n,
)["hex"]
# This will raise an exception
assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)
for txid in tree_txs:
self.nodes[0].getrawtransaction(txid)
def test_replacement_feeperkb(self):
"""Replacement requires fee-per-KB to be higher"""
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx0_outpoint,
sequence=0,
fee=Decimal("0.1"),
)
# Higher fee, but the fee per KB is much lower, so the replacement is
# rejected.
tx1b_hex = self.wallet.create_self_transfer_multi(
utxos_to_spend=[tx0_outpoint],
sequence=0,
num_outputs=100,
amount_per_output=1000,
)["hex"]
# This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
def test_spends_of_conflicting_outputs(self):
"""Replacements that spend conflicting tx outputs are rejected"""
utxo1 = self.make_utxo(self.nodes[0], int(1.2 * COIN))
utxo2 = self.make_utxo(self.nodes[0], 3 * COIN)
tx1a_utxo = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=utxo1,
sequence=0,
fee=Decimal("0.1"),
)["new_utxo"]
# Direct spend an output of the transaction we're replacing.
tx2_hex = self.wallet.create_self_transfer_multi(
utxos_to_spend=[utxo1, utxo2, tx1a_utxo],
sequence=0,
amount_per_output=int(COIN * tx1a_utxo["value"]),
)["hex"]
# This will raise an exception
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0)
# Spend tx1a's output to test the indirect case.
tx1b_utxo = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx1a_utxo,
sequence=0,
fee=Decimal("0.1"),
)["new_utxo"]
tx2_hex = self.wallet.create_self_transfer_multi(
utxos_to_spend=[utxo1, utxo2, tx1b_utxo],
sequence=0,
amount_per_output=int(COIN * tx1a_utxo["value"]),
)["hex"]
# This will raise an exception
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0)
def test_new_unconfirmed_inputs(self):
"""Replacements that add new unconfirmed inputs are rejected"""
confirmed_utxo = self.make_utxo(self.nodes[0], int(1.1 * COIN))
unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.1 * COIN), confirmed=False)
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=confirmed_utxo,
sequence=0,
fee=Decimal("0.1"),
)
tx2_hex = self.wallet.create_self_transfer_multi(
utxos_to_spend=[confirmed_utxo, unconfirmed_utxo],
sequence=0,
amount_per_output=1 * COIN,
)["hex"]
# This will raise an exception
assert_raises_rpc_error(-26, "replacement-adds-unconfirmed", self.nodes[0].sendrawtransaction, tx2_hex, 0)
def test_too_many_replacements(self):
"""Replacements that evict too many transactions are rejected"""
# Try directly replacing more than MAX_REPLACEMENT_LIMIT
# transactions
# Start by creating a single transaction with many outputs
initial_nValue = 10 * COIN
utxo = self.make_utxo(self.nodes[0], initial_nValue)
fee = int(0.0001 * COIN)
split_value = int((initial_nValue - fee) / (MAX_REPLACEMENT_LIMIT + 1))
splitting_tx_utxos = self.wallet.send_self_transfer_multi(
from_node=self.nodes[0],
utxos_to_spend=[utxo],
sequence=0,
num_outputs=MAX_REPLACEMENT_LIMIT + 1,
amount_per_output=split_value,
)["new_utxos"]
# Now spend each of those outputs individually
for utxo in splitting_tx_utxos:
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=utxo,
sequence=0,
fee=Decimal(fee) / COIN,
)
# Now create doublespend of the whole lot; should fail.
# Need a big enough fee to cover all spending transactions and have
# a higher fee rate
double_spend_value = (split_value - 100 * fee) * (MAX_REPLACEMENT_LIMIT + 1)
double_tx = self.wallet.create_self_transfer_multi(
utxos_to_spend=splitting_tx_utxos,
sequence=0,
amount_per_output=double_spend_value,
)["tx"]
double_tx_hex = double_tx.serialize().hex()
# This will raise an exception
assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, double_tx_hex, 0)
# If we remove an input, it should pass
double_tx.vin.pop()
double_tx_hex = double_tx.serialize().hex()
self.nodes[0].sendrawtransaction(double_tx_hex, 0)
def test_too_many_replacements_with_default_mempool_params(self):
"""
Test rule 5 (do not allow replacements that cause more than 100
evictions) without having to rely on non-default mempool parameters.
In order to do this, create a number of "root" UTXOs, and then hang
enough transactions off of each root UTXO to exceed the MAX_REPLACEMENT_LIMIT.
Then create a conflicting RBF replacement transaction.
"""
# Clear mempools to avoid cross-node sync failure.
for node in self.nodes:
self.generate(node, 1)
normal_node = self.nodes[1]
wallet = MiniWallet(normal_node)
# This has to be chosen so that the total number of transactions can exceed
# MAX_REPLACEMENT_LIMIT without having any one tx graph run into the descendant
# limit; 10 works.
num_tx_graphs = 10
# (Number of transactions per graph, rule 5 failure expected)
cases = [
# Test the base case of evicting fewer than MAX_REPLACEMENT_LIMIT
# transactions.
((MAX_REPLACEMENT_LIMIT // num_tx_graphs) - 1, False),
# Test hitting the rule 5 eviction limit.
(MAX_REPLACEMENT_LIMIT // num_tx_graphs, True),
]
for (txs_per_graph, failure_expected) in cases:
self.log.debug(f"txs_per_graph: {txs_per_graph}, failure: {failure_expected}")
# "Root" utxos of each txn graph that we will attempt to double-spend with
# an RBF replacement.
root_utxos = []
# For each root UTXO, create a package that contains the spend of that
# UTXO and `txs_per_graph` children tx.
for graph_num in range(num_tx_graphs):
root_utxos.append(wallet.get_utxo())
optin_parent_tx = wallet.send_self_transfer_multi(
from_node=normal_node,
sequence=MAX_BIP125_RBF_SEQUENCE,
utxos_to_spend=[root_utxos[graph_num]],
num_outputs=txs_per_graph,
)
assert_equal(True, normal_node.getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable'])
new_utxos = optin_parent_tx['new_utxos']
for utxo in new_utxos:
# Create spends for each output from the "root" of this graph.
child_tx = wallet.send_self_transfer(
from_node=normal_node,
utxo_to_spend=utxo,
)
assert normal_node.getmempoolentry(child_tx['txid'])
num_txs_invalidated = len(root_utxos) + (num_tx_graphs * txs_per_graph)
if failure_expected:
assert num_txs_invalidated > MAX_REPLACEMENT_LIMIT
else:
assert num_txs_invalidated <= MAX_REPLACEMENT_LIMIT
# Now attempt to submit a tx that double-spends all the root tx inputs, which
# would invalidate `num_txs_invalidated` transactions.
tx_hex = wallet.create_self_transfer_multi(
utxos_to_spend=root_utxos,
fee_per_output=10_000_000, # absurdly high feerate
)["hex"]
if failure_expected:
assert_raises_rpc_error(
-26, "too many potential replacements", normal_node.sendrawtransaction, tx_hex, 0)
else:
txid = normal_node.sendrawtransaction(tx_hex, 0)
assert normal_node.getmempoolentry(txid)
# Clear the mempool once finished, and rescan the other nodes' wallet
# to account for the spends we've made on `normal_node`.
self.generate(normal_node, 1)
self.wallet.rescan_utxos()
def test_opt_in(self):
"""Replacing should only work if orig tx opted in"""
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
# Create a non-opting in transaction
tx1a_utxo = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx0_outpoint,
sequence=SEQUENCE_FINAL,
fee=Decimal("0.1"),
)["new_utxo"]
# This transaction isn't shown as replaceable
assert_equal(self.nodes[0].getmempoolentry(tx1a_utxo["txid"])['bip125-replaceable'], False)
# Shouldn't be able to double-spend
tx1b_hex = self.wallet.create_self_transfer(
utxo_to_spend=tx0_outpoint,
sequence=0,
fee=Decimal("0.2"),
)["hex"]
# This will raise an exception
assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
# Create a different non-opting in transaction
tx2a_utxo = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx1_outpoint,
sequence=0xfffffffe,
fee=Decimal("0.1"),
)["new_utxo"]
# Still shouldn't be able to double-spend
tx2b_hex = self.wallet.create_self_transfer(
utxo_to_spend=tx1_outpoint,
sequence=0,
fee=Decimal("0.2"),
)["hex"]
# This will raise an exception
assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx2b_hex, 0)
# Now create a new transaction that spends from tx1a and tx2a
# opt-in on one of the inputs
# Transaction should be replaceable on either input
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),
)["txid"]
# This transaction is shown as replaceable
assert_equal(self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], True)
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx1a_utxo,
sequence=0,
fee=Decimal("0.4"),
)
# If tx3b was accepted, tx3c won't look like a replacement,
# but make sure it is accepted anyway
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx2a_utxo,
sequence=0,
fee=Decimal("0.4"),
)
def test_prioritised_transactions(self):
# Ensure that fee deltas used via prioritisetransaction are
# correctly used by replacement logic
# 1. Check that feeperkb uses modified fees
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
tx1a_txid = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx0_outpoint,
sequence=0,
fee=Decimal("0.1"),
)["txid"]
# Higher fee, but the actual fee per KB is much lower.
tx1b_hex = self.wallet.create_self_transfer_multi(
utxos_to_spend=[tx0_outpoint],
sequence=0,
num_outputs=100,
amount_per_output=int(0.00001 * COIN),
)["hex"]
# Verify tx1b cannot replace tx1a.
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
# Use prioritisetransaction to set tx1a's fee to 0.
self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1 * COIN))
# Now tx1b should be able to replace tx1a
tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0)
assert tx1b_txid in self.nodes[0].getrawmempool()
# 2. Check that absolute fee checks use modified fee.
tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
# tx2a
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=tx1_outpoint,
sequence=0,
fee=Decimal("0.1"),
)
# Lower fee, but we'll prioritise it
tx2b = self.wallet.create_self_transfer(
utxo_to_spend=tx1_outpoint,
sequence=0,
fee=Decimal("0.09"),
)
# Verify tx2b cannot replace tx2a.
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx2b["hex"], 0)
# Now prioritise tx2b to have a higher modified fee
self.nodes[0].prioritisetransaction(txid=tx2b["txid"], fee_delta=int(0.1 * COIN))
# tx2b should now be accepted
tx2b_txid = self.nodes[0].sendrawtransaction(tx2b["hex"], 0)
assert tx2b_txid in self.nodes[0].getrawmempool()
def test_rpc(self):
us0 = self.wallet.get_utxo()
ins = [us0]
outs = {ADDRESS_BCRT1_UNSPENDABLE: Decimal(1.0000000)}
rawtx0 = self.nodes[0].createrawtransaction(ins, outs, 0, True)
rawtx1 = self.nodes[0].createrawtransaction(ins, outs, 0, False)
json0 = self.nodes[0].decoderawtransaction(rawtx0)
json1 = self.nodes[0].decoderawtransaction(rawtx1)
assert_equal(json0["vin"][0]["sequence"], 4294967293)
assert_equal(json1["vin"][0]["sequence"], 4294967295)
if self.is_specified_wallet_compiled():
self.init_wallet(node=0)
rawtx2 = self.nodes[0].createrawtransaction([], outs)
frawtx2a = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": True})
frawtx2b = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": False})
json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex'])
json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex'])
assert_equal(json0["vin"][0]["sequence"], 4294967293)
assert_equal(json1["vin"][0]["sequence"], 4294967294)
def test_no_inherited_signaling(self):
confirmed_utxo = self.wallet.get_utxo()
# Create an explicitly opt-in parent transaction
optin_parent_tx = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=confirmed_utxo,
sequence=MAX_BIP125_RBF_SEQUENCE,
fee_rate=Decimal('0.01'),
)
assert_equal(True, self.nodes[0].getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable'])
replacement_parent_tx = self.wallet.create_self_transfer(
utxo_to_spend=confirmed_utxo,
sequence=MAX_BIP125_RBF_SEQUENCE,
fee_rate=Decimal('0.02'),
)
# Test if parent tx can be replaced.
res = self.nodes[0].testmempoolaccept(rawtxs=[replacement_parent_tx['hex']])[0]
# Parent can be replaced.
assert_equal(res['allowed'], True)
# Create an opt-out child tx spending the opt-in parent
parent_utxo = self.wallet.get_utxo(txid=optin_parent_tx['txid'])
optout_child_tx = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=parent_utxo,
sequence=SEQUENCE_FINAL,
fee_rate=Decimal('0.01'),
)
# Reports true due to inheritance
assert_equal(True, self.nodes[0].getmempoolentry(optout_child_tx['txid'])['bip125-replaceable'])
replacement_child_tx = self.wallet.create_self_transfer(
utxo_to_spend=parent_utxo,
sequence=SEQUENCE_FINAL,
fee_rate=Decimal('0.02'),
)
# Broadcast replacement child tx
# BIP 125 :
# 1. The original transactions signal replaceability explicitly or through inheritance as described in the above
# Summary section.
# The original transaction (`optout_child_tx`) doesn't signal RBF but its parent (`optin_parent_tx`) does.
# The replacement transaction (`replacement_child_tx`) should be able to replace the original transaction.
# See CVE-2021-31876 for further explanations.
assert_equal(True, self.nodes[0].getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable'])
assert_raises_rpc_error(-26, 'txn-mempool-conflict', self.nodes[0].sendrawtransaction, replacement_child_tx["hex"], 0)
self.log.info('Check that the child tx can still be replaced (via a tx that also replaces the parent)')
replacement_parent_tx = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=confirmed_utxo,
sequence=SEQUENCE_FINAL,
fee_rate=Decimal('0.03'),
)
# Check that child is removed and update wallet utxo state
assert_raises_rpc_error(-5, 'Transaction not in mempool', self.nodes[0].getmempoolentry, optout_child_tx['txid'])
self.wallet.get_utxo(txid=optout_child_tx['txid'])
def test_replacement_relay_fee(self):
tx = self.wallet.send_self_transfer(from_node=self.nodes[0])['tx']
# Higher fee, higher feerate, different txid, but the replacement does not provide a relay
# fee conforming to node's `incrementalrelayfee` policy of 1000 sat per KB.
assert_equal(self.nodes[0].getmempoolinfo()["incrementalrelayfee"], Decimal("0.00001"))
tx.vout[0].nValue -= 1
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex())
def test_fullrbf(self):
confirmed_utxo = self.make_utxo(self.nodes[0], int(2 * COIN))
self.restart_node(0, extra_args=["-mempoolfullrbf=1"])
assert self.nodes[0].getmempoolinfo()["fullrbf"]
# Create an explicitly opt-out transaction
optout_tx = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=confirmed_utxo,
sequence=MAX_BIP125_RBF_SEQUENCE + 1,
fee_rate=Decimal('0.01'),
)
assert_equal(False, self.nodes[0].getmempoolentry(optout_tx['txid'])['bip125-replaceable'])
conflicting_tx = self.wallet.create_self_transfer(
utxo_to_spend=confirmed_utxo,
sequence=SEQUENCE_FINAL,
fee_rate=Decimal('0.02'),
)
# Send the replacement transaction, conflicting with the optout_tx.
self.nodes[0].sendrawtransaction(conflicting_tx['hex'], 0)
# Optout_tx is not anymore in the mempool.
assert optout_tx['txid'] not in self.nodes[0].getrawmempool()
assert conflicting_tx['txid'] in self.nodes[0].getrawmempool()
if __name__ == '__main__':
ReplaceByFeeTest().main()