Merge bitcoin/bitcoin#25522: test: Remove -acceptnonstdtxn=1 from feature_rbf.py

fad690ba0a test: Remove -acceptnonstdtxn=1 from feature_rbf.py (MacroFake)
fa5059b7df test: Make the scriptPubKey of MiniWallet created txs mutable (MacroFake)
fa29245827 test: Allow setting sequence per input in MiniWallet create_self_transfer_multi (MacroFake)
fac3800d2c test: Allow amount_per_output in MiniWallet create_self_transfer_multi (MacroFake)
2222842ae7 test: Allow absolute fee in MiniWallet create_self_transfer (MacroFake)

Pull request description:

  On the main network, nonstandard transactions are hardly relayed, so it seems odd that one of our policy test requires a policy setting opposite of the norm.

  Surely it is also important to test that nonstandard transactions can be replaced. However, rbf code should not care about the standardness at all. Moreover, I think testing nonstandardness rbf is of lower priority than testing the stuff that actually happens in production.

ACKs for top commit:
  theStack:
    re-ACK fad690ba0a
  jamesob:
    ACK fad690ba0a ([`jamesob/ackr/25522.1.MarcoFalke.test_remove_acceptnonstd`](https://github.com/jamesob/bitcoin/tree/ackr/25522.1.MarcoFalke.test_remove_acceptnonstd))

Tree-SHA512: e0a0c808bccdddf738fb6a84e5e5672d7c341bffd941c4f0c232112bfc68265fa81a2e42ddcab107d586358ffdb3dccc46bb5533d46999fd9ab024169dac0f78
This commit is contained in:
MacroFake committed 2022-07-07 16:12:12 +02:00
commit 8ef096d4f8
2 files changed
+202 -210

No files matched your search

+186 -202
View File
@@ -4,28 +4,18 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the RBF code.""" """Test the RBF code."""
from copy import deepcopy
from decimal import Decimal from decimal import Decimal
from test_framework.messages import ( from test_framework.messages import (
BIP125_SEQUENCE_NUMBER, BIP125_SEQUENCE_NUMBER,
COIN, COIN,
COutPoint,
CTransaction,
CTxIn,
CTxOut,
SEQUENCE_FINAL, SEQUENCE_FINAL,
) )
from test_framework.script import CScript, OP_DROP
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_equal, assert_equal,
assert_raises_rpc_error, assert_raises_rpc_error,
) )
from test_framework.script_util import (
DUMMY_P2WPKH_SCRIPT,
DUMMY_2_P2WPKH_SCRIPT,
)
from test_framework.wallet import MiniWallet from test_framework.wallet import MiniWallet
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE
@@ -35,7 +25,6 @@ class ReplaceByFeeTest(BitcoinTestFramework):
self.num_nodes = 2 self.num_nodes = 2
self.extra_args = [ self.extra_args = [
[ [
"-acceptnonstdtxn=1",
"-maxorphantx=1000", "-maxorphantx=1000",
"-limitancestorcount=50", "-limitancestorcount=50",
"-limitancestorsize=101", "-limitancestorsize=101",
@@ -96,15 +85,14 @@ class ReplaceByFeeTest(BitcoinTestFramework):
self.log.info("Passed") self.log.info("Passed")
def make_utxo(self, node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): def make_utxo(self, node, amount, *, confirmed=True, scriptPubKey=None):
"""Create a txout with a given amount and scriptPubKey """Create a txout with a given amount and scriptPubKey
confirmed - txouts created will be confirmed in the blockchain; confirmed - txout created will be confirmed in the blockchain;
unconfirmed otherwise. unconfirmed otherwise.
""" """
txid, n = self.wallet.send_to(from_node=node, scriptPubKey=scriptPubKey, amount=amount) txid, n = self.wallet.send_to(from_node=node, scriptPubKey=scriptPubKey or self.wallet.get_scriptPubKey(), amount=amount)
# If requested, ensure txouts are confirmed.
if confirmed: if confirmed:
mempool_size = len(node.getrawmempool()) mempool_size = len(node.getrawmempool())
while mempool_size > 0: while mempool_size > 0:
@@ -115,30 +103,24 @@ class ReplaceByFeeTest(BitcoinTestFramework):
assert new_size < mempool_size assert new_size < mempool_size
mempool_size = new_size mempool_size = new_size
return COutPoint(int(txid, 16), n) return self.wallet.get_utxo(txid=txid, vout=n)
def test_simple_doublespend(self): def test_simple_doublespend(self):
"""Simple doublespend""" """Simple doublespend"""
# we use MiniWallet to create a transaction template with inputs correctly set, # we use MiniWallet to create a transaction template with inputs correctly set,
# and modify the output (amount, scriptPubKey) according to our needs # and modify the output (amount, scriptPubKey) according to our needs
tx_template = self.wallet.create_self_transfer()['tx'] tx = self.wallet.create_self_transfer()["tx"]
tx1a_txid = self.nodes[0].sendrawtransaction(tx.serialize().hex())
tx1a = deepcopy(tx_template)
tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)]
tx1a_hex = tx1a.serialize().hex()
tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0)
# Should fail because we haven't changed the fee # Should fail because we haven't changed the fee
tx1b = deepcopy(tx_template) tx.vout[0].scriptPubKey[-1] ^= 1
tx1b.vout = [CTxOut(1 * COIN, DUMMY_2_P2WPKH_SCRIPT)]
tx1b_hex = tx1b.serialize().hex()
# This will raise an exception due to insufficient fee # This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0)
# Extra 0.1 BTC fee # Extra 0.1 BTC fee
tx1b.vout[0].nValue -= int(0.1 * COIN) tx.vout[0].nValue -= int(0.1 * COIN)
tx1b_hex = tx1b.serialize().hex() tx1b_hex = tx.serialize().hex()
# Works when enabled # Works when enabled
tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0)
@@ -160,28 +142,28 @@ class ReplaceByFeeTest(BitcoinTestFramework):
chain_txids = [] chain_txids = []
while remaining_value > 1 * COIN: while remaining_value > 1 * COIN:
remaining_value -= int(0.1 * COIN) remaining_value -= int(0.1 * COIN)
tx = CTransaction() prevout = self.wallet.send_self_transfer(
tx.vin = [CTxIn(prevout, nSequence=0)] from_node=self.nodes[0],
tx.vout = [CTxOut(remaining_value, CScript([1, OP_DROP] * 15 + [1]))] utxo_to_spend=prevout,
tx_hex = tx.serialize().hex() sequence=0,
txid = self.nodes[0].sendrawtransaction(tx_hex, 0) fee=Decimal("0.1"),
chain_txids.append(txid) )["new_utxo"]
prevout = COutPoint(int(txid, 16), 0) chain_txids.append(prevout["txid"])
# Whether the double-spend is allowed is evaluated by including all # Whether the double-spend is allowed is evaluated by including all
# child fees - 4 BTC - so this attempt is rejected. # child fees - 4 BTC - so this attempt is rejected.
dbl_tx = CTransaction() dbl_tx = self.wallet.create_self_transfer(
dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] utxo_to_spend=tx0_outpoint,
dbl_tx.vout = [CTxOut(initial_nValue - 3 * COIN, DUMMY_P2WPKH_SCRIPT)] sequence=0,
fee=Decimal("3"),
)["tx"]
dbl_tx_hex = dbl_tx.serialize().hex() dbl_tx_hex = dbl_tx.serialize().hex()
# This will raise an exception due to insufficient fee # This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)
# Accepted with sufficient fee # Accepted with sufficient fee
dbl_tx = CTransaction() dbl_tx.vout[0].nValue = int(0.1 * COIN)
dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)]
dbl_tx.vout = [CTxOut(int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT)]
dbl_tx_hex = dbl_tx.serialize().hex() dbl_tx_hex = dbl_tx.serialize().hex()
self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) self.nodes[0].sendrawtransaction(dbl_tx_hex, 0)
@@ -205,22 +187,19 @@ class ReplaceByFeeTest(BitcoinTestFramework):
if txout_value < fee: if txout_value < fee:
return return
vout = [CTxOut(txout_value, CScript([i+1])) tx = self.wallet.send_self_transfer_multi(
for i in range(tree_width)] utxos_to_spend=[prevout],
tx = CTransaction() from_node=self.nodes[0],
tx.vin = [CTxIn(prevout, nSequence=0)] sequence=0,
tx.vout = vout num_outputs=tree_width,
tx_hex = tx.serialize().hex() amount_per_output=txout_value,
)
assert len(tx.serialize()) < 100000 yield tx["txid"]
txid = self.nodes[0].sendrawtransaction(tx_hex, 0)
yield tx
_total_txs[0] += 1 _total_txs[0] += 1
txid = int(txid, 16) for utxo in tx["new_utxos"]:
for x in branch(utxo, txout_value,
for i, txout in enumerate(tx.vout):
for x in branch(COutPoint(txid, i), txout_value,
max_txs, max_txs,
tree_width=tree_width, fee=fee, tree_width=tree_width, fee=fee,
_total_txs=_total_txs): _total_txs=_total_txs):
@@ -232,25 +211,26 @@ class ReplaceByFeeTest(BitcoinTestFramework):
assert_equal(len(tree_txs), n) assert_equal(len(tree_txs), n)
# Attempt double-spend, will fail because too little fee paid # Attempt double-spend, will fail because too little fee paid
dbl_tx = CTransaction() dbl_tx_hex = self.wallet.create_self_transfer(
dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] utxo_to_spend=tx0_outpoint,
dbl_tx.vout = [CTxOut(initial_nValue - fee * n, DUMMY_P2WPKH_SCRIPT)] sequence=0,
dbl_tx_hex = dbl_tx.serialize().hex() fee=(Decimal(fee) / COIN) * n,
)["hex"]
# This will raise an exception due to insufficient fee # This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)
# 0.1 BTC fee is enough # 0.1 BTC fee is enough
dbl_tx = CTransaction() dbl_tx_hex = self.wallet.create_self_transfer(
dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] utxo_to_spend=tx0_outpoint,
dbl_tx.vout = [CTxOut(initial_nValue - fee * n - int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT)] sequence=0,
dbl_tx_hex = dbl_tx.serialize().hex() fee=(Decimal(fee) / COIN) * n + Decimal("0.1"),
)["hex"]
self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) self.nodes[0].sendrawtransaction(dbl_tx_hex, 0)
mempool = self.nodes[0].getrawmempool() mempool = self.nodes[0].getrawmempool()
for tx in tree_txs: for txid in tree_txs:
tx.rehash() assert txid not in mempool
assert tx.hash not in mempool
# Try again, but with more total transactions than the "max txs # Try again, but with more total transactions than the "max txs
# double-spent at once" anti-DoS limit. # double-spent at once" anti-DoS limit.
@@ -260,33 +240,36 @@ class ReplaceByFeeTest(BitcoinTestFramework):
tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee))
assert_equal(len(tree_txs), n) assert_equal(len(tree_txs), n)
dbl_tx = CTransaction() dbl_tx_hex = self.wallet.create_self_transfer(
dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] utxo_to_spend=tx0_outpoint,
dbl_tx.vout = [CTxOut(initial_nValue - 2 * fee * n, DUMMY_P2WPKH_SCRIPT)] sequence=0,
dbl_tx_hex = dbl_tx.serialize().hex() fee=2 * (Decimal(fee) / COIN) * n,
)["hex"]
# This will raise an exception # This will raise an exception
assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)
for tx in tree_txs: for txid in tree_txs:
tx.rehash() self.nodes[0].getrawtransaction(txid)
self.nodes[0].getrawtransaction(tx.hash)
def test_replacement_feeperkb(self): def test_replacement_feeperkb(self):
"""Replacement requires fee-per-KB to be higher""" """Replacement requires fee-per-KB to be higher"""
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
tx1a = CTransaction() self.wallet.send_self_transfer(
tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] from_node=self.nodes[0],
tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=tx0_outpoint,
tx1a_hex = tx1a.serialize().hex() sequence=0,
self.nodes[0].sendrawtransaction(tx1a_hex, 0) fee=Decimal("0.1"),
)
# Higher fee, but the fee per KB is much lower, so the replacement is # Higher fee, but the fee per KB is much lower, so the replacement is
# rejected. # rejected.
tx1b = CTransaction() tx1b_hex = self.wallet.create_self_transfer_multi(
tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] utxos_to_spend=[tx0_outpoint],
tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 999000]))] sequence=0,
tx1b_hex = tx1b.serialize().hex() num_outputs=100,
amount_per_output=1000,
)["hex"]
# This will raise an exception due to insufficient fee # This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
@@ -296,37 +279,36 @@ class ReplaceByFeeTest(BitcoinTestFramework):
utxo1 = self.make_utxo(self.nodes[0], int(1.2 * COIN)) utxo1 = self.make_utxo(self.nodes[0], int(1.2 * COIN))
utxo2 = self.make_utxo(self.nodes[0], 3 * COIN) utxo2 = self.make_utxo(self.nodes[0], 3 * COIN)
tx1a = CTransaction() tx1a_utxo = self.wallet.send_self_transfer(
tx1a.vin = [CTxIn(utxo1, nSequence=0)] from_node=self.nodes[0],
tx1a.vout = [CTxOut(int(1.1 * COIN), DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=utxo1,
tx1a_hex = tx1a.serialize().hex() sequence=0,
tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) fee=Decimal("0.1"),
)["new_utxo"]
tx1a_txid = int(tx1a_txid, 16)
# Direct spend an output of the transaction we're replacing. # Direct spend an output of the transaction we're replacing.
tx2 = CTransaction() tx2_hex = self.wallet.create_self_transfer_multi(
tx2.vin = [CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0)] utxos_to_spend=[utxo1, utxo2, tx1a_utxo],
tx2.vin.append(CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)) sequence=0,
tx2.vout = tx1a.vout amount_per_output=int(COIN * tx1a_utxo["value"]),
tx2_hex = tx2.serialize().hex() )["hex"]
# This will raise an exception # This will raise an exception
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) 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. # Spend tx1a's output to test the indirect case.
tx1b = CTransaction() tx1b_utxo = self.wallet.send_self_transfer(
tx1b.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)] from_node=self.nodes[0],
tx1b.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=tx1a_utxo,
tx1b_hex = tx1b.serialize().hex() sequence=0,
tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) fee=Decimal("0.1"),
tx1b_txid = int(tx1b_txid, 16) )["new_utxo"]
tx2 = CTransaction() tx2_hex = self.wallet.create_self_transfer_multi(
tx2.vin = [CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0), utxos_to_spend=[utxo1, utxo2, tx1b_utxo],
CTxIn(COutPoint(tx1b_txid, 0))] sequence=0,
tx2.vout = tx1a.vout amount_per_output=int(COIN * tx1a_utxo["value"]),
tx2_hex = tx2.serialize().hex() )["hex"]
# This will raise an exception # This will raise an exception
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0) assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0)
@@ -334,18 +316,20 @@ class ReplaceByFeeTest(BitcoinTestFramework):
def test_new_unconfirmed_inputs(self): def test_new_unconfirmed_inputs(self):
"""Replacements that add new unconfirmed inputs are rejected""" """Replacements that add new unconfirmed inputs are rejected"""
confirmed_utxo = self.make_utxo(self.nodes[0], int(1.1 * COIN)) confirmed_utxo = self.make_utxo(self.nodes[0], int(1.1 * COIN))
unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.1 * COIN), False) unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.1 * COIN), confirmed=False)
tx1 = CTransaction() self.wallet.send_self_transfer(
tx1.vin = [CTxIn(confirmed_utxo)] from_node=self.nodes[0],
tx1.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=confirmed_utxo,
tx1_hex = tx1.serialize().hex() sequence=0,
self.nodes[0].sendrawtransaction(tx1_hex, 0) fee=Decimal("0.1"),
)
tx2 = CTransaction() tx2_hex = self.wallet.create_self_transfer_multi(
tx2.vin = [CTxIn(confirmed_utxo), CTxIn(unconfirmed_utxo)] utxos_to_spend=[confirmed_utxo, unconfirmed_utxo],
tx2.vout = tx1.vout sequence=0,
tx2_hex = tx2.serialize().hex() amount_per_output=1 * COIN,
)["hex"]
# This will raise an exception # This will raise an exception
assert_raises_rpc_error(-26, "replacement-adds-unconfirmed", self.nodes[0].sendrawtransaction, tx2_hex, 0) assert_raises_rpc_error(-26, "replacement-adds-unconfirmed", self.nodes[0].sendrawtransaction, tx2_hex, 0)
@@ -361,45 +345,39 @@ class ReplaceByFeeTest(BitcoinTestFramework):
fee = int(0.0001 * COIN) fee = int(0.0001 * COIN)
split_value = int((initial_nValue - fee) / (MAX_REPLACEMENT_LIMIT + 1)) split_value = int((initial_nValue - fee) / (MAX_REPLACEMENT_LIMIT + 1))
outputs = [] splitting_tx_utxos = self.wallet.send_self_transfer_multi(
for _ in range(MAX_REPLACEMENT_LIMIT + 1): from_node=self.nodes[0],
outputs.append(CTxOut(split_value, CScript([1]))) utxos_to_spend=[utxo],
sequence=0,
splitting_tx = CTransaction() num_outputs=MAX_REPLACEMENT_LIMIT + 1,
splitting_tx.vin = [CTxIn(utxo, nSequence=0)] amount_per_output=split_value,
splitting_tx.vout = outputs )["new_utxos"]
splitting_tx_hex = splitting_tx.serialize().hex()
txid = self.nodes[0].sendrawtransaction(splitting_tx_hex, 0)
txid = int(txid, 16)
# Now spend each of those outputs individually # Now spend each of those outputs individually
for i in range(MAX_REPLACEMENT_LIMIT + 1): for utxo in splitting_tx_utxos:
tx_i = CTransaction() self.wallet.send_self_transfer(
tx_i.vin = [CTxIn(COutPoint(txid, i), nSequence=0)] from_node=self.nodes[0],
tx_i.vout = [CTxOut(split_value - fee, DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=utxo,
tx_i_hex = tx_i.serialize().hex() sequence=0,
self.nodes[0].sendrawtransaction(tx_i_hex, 0) fee=Decimal(fee) / COIN,
)
# Now create doublespend of the whole lot; should fail. # Now create doublespend of the whole lot; should fail.
# Need a big enough fee to cover all spending transactions and have # Need a big enough fee to cover all spending transactions and have
# a higher fee rate # a higher fee rate
double_spend_value = (split_value - 100 * fee) * (MAX_REPLACEMENT_LIMIT + 1) double_spend_value = (split_value - 100 * fee) * (MAX_REPLACEMENT_LIMIT + 1)
inputs = [] double_tx = self.wallet.create_self_transfer_multi(
for i in range(MAX_REPLACEMENT_LIMIT + 1): utxos_to_spend=splitting_tx_utxos,
inputs.append(CTxIn(COutPoint(txid, i), nSequence=0)) sequence=0,
double_tx = CTransaction() amount_per_output=double_spend_value,
double_tx.vin = inputs )["tx"]
double_tx.vout = [CTxOut(double_spend_value, CScript([b'a']))]
double_tx_hex = double_tx.serialize().hex() double_tx_hex = double_tx.serialize().hex()
# This will raise an exception # This will raise an exception
assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, double_tx_hex, 0) 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 # If we remove an input, it should pass
double_tx = CTransaction() double_tx.vin.pop()
double_tx.vin = inputs[0:-1]
double_tx.vout = [CTxOut(double_spend_value, CScript([b'a']))]
double_tx_hex = double_tx.serialize().hex() double_tx_hex = double_tx.serialize().hex()
self.nodes[0].sendrawtransaction(double_tx_hex, 0) self.nodes[0].sendrawtransaction(double_tx_hex, 0)
@@ -494,20 +472,22 @@ class ReplaceByFeeTest(BitcoinTestFramework):
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
# Create a non-opting in transaction # Create a non-opting in transaction
tx1a = CTransaction() tx1a_utxo = self.wallet.send_self_transfer(
tx1a.vin = [CTxIn(tx0_outpoint, nSequence=SEQUENCE_FINAL)] from_node=self.nodes[0],
tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=tx0_outpoint,
tx1a_hex = tx1a.serialize().hex() sequence=SEQUENCE_FINAL,
tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) fee=Decimal("0.1"),
)["new_utxo"]
# This transaction isn't shown as replaceable # This transaction isn't shown as replaceable
assert_equal(self.nodes[0].getmempoolentry(tx1a_txid)['bip125-replaceable'], False) assert_equal(self.nodes[0].getmempoolentry(tx1a_utxo["txid"])['bip125-replaceable'], False)
# Shouldn't be able to double-spend # Shouldn't be able to double-spend
tx1b = CTransaction() tx1b_hex = self.wallet.create_self_transfer(
tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] utxo_to_spend=tx0_outpoint,
tx1b.vout = [CTxOut(int(0.9 * COIN), DUMMY_P2WPKH_SCRIPT)] sequence=0,
tx1b_hex = tx1b.serialize().hex() fee=Decimal("0.2"),
)["hex"]
# This will raise an exception # This will raise an exception
assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx1b_hex, 0) assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
@@ -515,17 +495,19 @@ class ReplaceByFeeTest(BitcoinTestFramework):
tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
# Create a different non-opting in transaction # Create a different non-opting in transaction
tx2a = CTransaction() tx2a_utxo = self.wallet.send_self_transfer(
tx2a.vin = [CTxIn(tx1_outpoint, nSequence=0xfffffffe)] from_node=self.nodes[0],
tx2a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=tx1_outpoint,
tx2a_hex = tx2a.serialize().hex() sequence=0xfffffffe,
tx2a_txid = self.nodes[0].sendrawtransaction(tx2a_hex, 0) fee=Decimal("0.1"),
)["new_utxo"]
# Still shouldn't be able to double-spend # Still shouldn't be able to double-spend
tx2b = CTransaction() tx2b_hex = self.wallet.create_self_transfer(
tx2b.vin = [CTxIn(tx1_outpoint, nSequence=0)] utxo_to_spend=tx1_outpoint,
tx2b.vout = [CTxOut(int(0.9 * COIN), DUMMY_P2WPKH_SCRIPT)] sequence=0,
tx2b_hex = tx2b.serialize().hex() fee=Decimal("0.2"),
)["hex"]
# This will raise an exception # This will raise an exception
assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx2b_hex, 0) assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx2b_hex, 0)
@@ -534,34 +516,31 @@ class ReplaceByFeeTest(BitcoinTestFramework):
# opt-in on one of the inputs # opt-in on one of the inputs
# Transaction should be replaceable on either input # Transaction should be replaceable on either input
tx1a_txid = int(tx1a_txid, 16) tx3a_txid = self.wallet.send_self_transfer_multi(
tx2a_txid = int(tx2a_txid, 16) from_node=self.nodes[0],
utxos_to_spend=[tx1a_utxo, tx2a_utxo],
tx3a = CTransaction() sequence=[SEQUENCE_FINAL, 0xfffffffd],
tx3a.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=SEQUENCE_FINAL), fee_per_output=int(0.1 * COIN),
CTxIn(COutPoint(tx2a_txid, 0), nSequence=0xfffffffd)] )["txid"]
tx3a.vout = [CTxOut(int(0.9 * COIN), CScript([b'c'])), CTxOut(int(0.9 * COIN), CScript([b'd']))]
tx3a_hex = tx3a.serialize().hex()
tx3a_txid = self.nodes[0].sendrawtransaction(tx3a_hex, 0)
# This transaction is shown as replaceable # This transaction is shown as replaceable
assert_equal(self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], True) assert_equal(self.nodes[0].getmempoolentry(tx3a_txid)['bip125-replaceable'], True)
tx3b = CTransaction() self.wallet.send_self_transfer(
tx3b.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)] from_node=self.nodes[0],
tx3b.vout = [CTxOut(int(0.5 * COIN), DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=tx1a_utxo,
tx3b_hex = tx3b.serialize().hex() sequence=0,
fee=Decimal("0.4"),
)
tx3c = CTransaction()
tx3c.vin = [CTxIn(COutPoint(tx2a_txid, 0), nSequence=0)]
tx3c.vout = [CTxOut(int(0.5 * COIN), DUMMY_P2WPKH_SCRIPT)]
tx3c_hex = tx3c.serialize().hex()
self.nodes[0].sendrawtransaction(tx3b_hex, 0)
# If tx3b was accepted, tx3c won't look like a replacement, # If tx3b was accepted, tx3c won't look like a replacement,
# but make sure it is accepted anyway # but make sure it is accepted anyway
self.nodes[0].sendrawtransaction(tx3c_hex, 0) 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): def test_prioritised_transactions(self):
# Ensure that fee deltas used via prioritisetransaction are # Ensure that fee deltas used via prioritisetransaction are
@@ -570,17 +549,20 @@ class ReplaceByFeeTest(BitcoinTestFramework):
# 1. Check that feeperkb uses modified fees # 1. Check that feeperkb uses modified fees
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
tx1a = CTransaction() tx1a_txid = self.wallet.send_self_transfer(
tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] from_node=self.nodes[0],
tx1a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] utxo_to_spend=tx0_outpoint,
tx1a_hex = tx1a.serialize().hex() sequence=0,
tx1a_txid = self.nodes[0].sendrawtransaction(tx1a_hex, 0) fee=Decimal("0.1"),
)["txid"]
# Higher fee, but the actual fee per KB is much lower. # Higher fee, but the actual fee per KB is much lower.
tx1b = CTransaction() tx1b_hex = self.wallet.create_self_transfer_multi(
tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] utxos_to_spend=[tx0_outpoint],
tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 740000]))] sequence=0,
tx1b_hex = tx1b.serialize().hex() num_outputs=100,
amount_per_output=int(0.00001 * COIN),
)["hex"]
# Verify tx1b cannot replace tx1a. # Verify tx1b cannot replace tx1a.
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
@@ -596,27 +578,29 @@ class ReplaceByFeeTest(BitcoinTestFramework):
# 2. Check that absolute fee checks use modified fee. # 2. Check that absolute fee checks use modified fee.
tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx1_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))
tx2a = CTransaction() # tx2a
tx2a.vin = [CTxIn(tx1_outpoint, nSequence=0)] self.wallet.send_self_transfer(
tx2a.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] from_node=self.nodes[0],
tx2a_hex = tx2a.serialize().hex() utxo_to_spend=tx1_outpoint,
self.nodes[0].sendrawtransaction(tx2a_hex, 0) sequence=0,
fee=Decimal("0.1"),
)
# Lower fee, but we'll prioritise it # Lower fee, but we'll prioritise it
tx2b = CTransaction() tx2b = self.wallet.create_self_transfer(
tx2b.vin = [CTxIn(tx1_outpoint, nSequence=0)] utxo_to_spend=tx1_outpoint,
tx2b.vout = [CTxOut(int(1.01 * COIN), DUMMY_P2WPKH_SCRIPT)] sequence=0,
tx2b.rehash() fee=Decimal("0.09"),
tx2b_hex = tx2b.serialize().hex() )
# Verify tx2b cannot replace tx2a. # Verify tx2b cannot replace tx2a.
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx2b_hex, 0) assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx2b["hex"], 0)
# Now prioritise tx2b to have a higher modified fee # Now prioritise tx2b to have a higher modified fee
self.nodes[0].prioritisetransaction(txid=tx2b.hash, fee_delta=int(0.1 * COIN)) self.nodes[0].prioritisetransaction(txid=tx2b["txid"], fee_delta=int(0.1 * COIN))
# tx2b should now be accepted # tx2b should now be accepted
tx2b_txid = self.nodes[0].sendrawtransaction(tx2b_hex, 0) tx2b_txid = self.nodes[0].sendrawtransaction(tx2b["hex"], 0)
assert tx2b_txid in self.nodes[0].getrawmempool() assert tx2b_txid in self.nodes[0].getrawmempool()
+16 -8
View File
@@ -197,7 +197,7 @@ class MiniWallet:
return utxos return utxos
def send_self_transfer(self, *, from_node, **kwargs): def send_self_transfer(self, *, from_node, **kwargs):
"""Create and send a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" """Call create_self_transfer and send the transaction."""
tx = self.create_self_transfer(**kwargs) tx = self.create_self_transfer(**kwargs)
self.sendrawtransaction(from_node=from_node, tx_hex=tx['hex']) self.sendrawtransaction(from_node=from_node, tx_hex=tx['hex'])
return tx return tx
@@ -232,21 +232,27 @@ class MiniWallet:
*, *,
utxos_to_spend: Optional[List[dict]] = None, utxos_to_spend: Optional[List[dict]] = None,
num_outputs=1, num_outputs=1,
amount_per_output=0,
sequence=0, sequence=0,
fee_per_output=1000, fee_per_output=1000,
): ):
""" """
Create and return a transaction that spends the given UTXOs and creates a Create and return a transaction that spends the given UTXOs and creates a
certain number of outputs with equal amounts. certain number of outputs with equal amounts. The output amounts can be
set by amount_per_output or automatically calculated with a fee_per_output.
""" """
utxos_to_spend = utxos_to_spend or [self.get_utxo()] utxos_to_spend = utxos_to_spend or [self.get_utxo()]
sequence = [sequence] * len(utxos_to_spend) if type(sequence) is int else sequence
assert_equal(len(utxos_to_spend), len(sequence))
# create simple tx template (1 input, 1 output) # create simple tx template (1 input, 1 output)
tx = self.create_self_transfer( tx = self.create_self_transfer(
fee_rate=0, fee_rate=0,
utxo_to_spend=utxos_to_spend[0], sequence=sequence)["tx"] utxo_to_spend=utxos_to_spend[0])["tx"]
# duplicate inputs, witnesses and outputs # duplicate inputs, witnesses and outputs
tx.vin = [deepcopy(tx.vin[0]) for _ in range(len(utxos_to_spend))] tx.vin = [deepcopy(tx.vin[0]) for _ in range(len(utxos_to_spend))]
for txin, seq in zip(tx.vin, sequence):
txin.nSequence = seq
tx.wit.vtxinwit = [deepcopy(tx.wit.vtxinwit[0]) for _ in range(len(utxos_to_spend))] tx.wit.vtxinwit = [deepcopy(tx.wit.vtxinwit[0]) for _ in range(len(utxos_to_spend))]
tx.vout = [deepcopy(tx.vout[0]) for _ in range(num_outputs)] tx.vout = [deepcopy(tx.vout[0]) for _ in range(num_outputs)]
@@ -258,7 +264,7 @@ class MiniWallet:
inputs_value_total = sum([int(COIN * utxo['value']) for utxo in utxos_to_spend]) inputs_value_total = sum([int(COIN * utxo['value']) for utxo in utxos_to_spend])
outputs_value_total = inputs_value_total - fee_per_output * num_outputs outputs_value_total = inputs_value_total - fee_per_output * num_outputs
for o in tx.vout: for o in tx.vout:
o.nValue = outputs_value_total // num_outputs o.nValue = amount_per_output or (outputs_value_total // num_outputs)
txid = tx.rehash() txid = tx.rehash()
return { return {
"new_utxos": [self._create_utxo( "new_utxos": [self._create_utxo(
@@ -272,21 +278,23 @@ class MiniWallet:
"tx": tx, "tx": tx,
} }
def create_self_transfer(self, *, fee_rate=Decimal("0.003"), utxo_to_spend=None, locktime=0, sequence=0): def create_self_transfer(self, *, fee_rate=Decimal("0.003"), fee=Decimal("0"), utxo_to_spend=None, locktime=0, sequence=0):
"""Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" """Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed."""
utxo_to_spend = utxo_to_spend or self.get_utxo() utxo_to_spend = utxo_to_spend or self.get_utxo()
assert fee_rate >= 0
assert fee >= 0
if self._mode in (MiniWalletMode.RAW_OP_TRUE, MiniWalletMode.ADDRESS_OP_TRUE): if self._mode in (MiniWalletMode.RAW_OP_TRUE, MiniWalletMode.ADDRESS_OP_TRUE):
vsize = Decimal(104) # anyone-can-spend vsize = Decimal(104) # anyone-can-spend
elif self._mode == MiniWalletMode.RAW_P2PK: elif self._mode == MiniWalletMode.RAW_P2PK:
vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other) vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other)
else: else:
assert False assert False
send_value = utxo_to_spend["value"] - (fee_rate * vsize / 1000) send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000))
assert send_value > 0 assert send_value > 0
tx = CTransaction() tx = CTransaction()
tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)] tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)]
tx.vout = [CTxOut(int(COIN * send_value), self._scriptPubKey)] tx.vout = [CTxOut(int(COIN * send_value), bytearray(self._scriptPubKey))]
tx.nLockTime = locktime tx.nLockTime = locktime
if self._mode == MiniWalletMode.RAW_P2PK: if self._mode == MiniWalletMode.RAW_P2PK:
self.sign_tx(tx) self.sign_tx(tx)