aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rwxr-xr-xtest/functional/feature_cltv.py7
-rwxr-xr-xtest/functional/feature_csv_activation.py7
-rwxr-xr-xtest/functional/feature_dersig.py26
-rwxr-xr-xtest/functional/mempool_accept.py3
-rwxr-xr-xtest/functional/rpc_net.py25
-rwxr-xr-xtest/functional/rpc_packages.py362
-rw-r--r--test/functional/test_framework/wallet.py33
-rwxr-xr-xtest/functional/test_runner.py1
-rwxr-xr-xtest/functional/wallet_avoidreuse.py46
-rwxr-xr-xtest/lint/lint-circular-dependencies.sh4
10 files changed, 472 insertions, 42 deletions
diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py
index 6c51944d81..d25aaa070d 100755
--- a/test/functional/feature_cltv.py
+++ b/test/functional/feature_cltv.py
@@ -26,7 +26,10 @@ from test_framework.script import (
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
-from test_framework.wallet import MiniWallet
+from test_framework.wallet import (
+ MiniWallet,
+ MiniWalletMode,
+)
CLTV_HEIGHT = 1351
@@ -97,7 +100,7 @@ class BIP65Test(BitcoinTestFramework):
def run_test(self):
peer = self.nodes[0].add_p2p_connection(P2PInterface())
- wallet = MiniWallet(self.nodes[0], raw_script=True)
+ wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_OP_TRUE)
self.test_cltv_info(is_active=False)
diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py
index d815ad83b1..5081867319 100755
--- a/test/functional/feature_csv_activation.py
+++ b/test/functional/feature_csv_activation.py
@@ -55,7 +55,10 @@ from test_framework.util import (
assert_equal,
softfork_active,
)
-from test_framework.wallet import MiniWallet
+from test_framework.wallet import (
+ MiniWallet,
+ MiniWalletMode,
+)
TESTING_TX_COUNT = 83 # Number of testing transactions: 1 BIP113 tx, 16 BIP68 txs, 66 BIP112 txs (see comments above)
COINBASE_BLOCK_COUNT = TESTING_TX_COUNT # Number of coinbase blocks we need to generate as inputs for our txs
@@ -181,7 +184,7 @@ class BIP68_112_113Test(BitcoinTestFramework):
def run_test(self):
self.helper_peer = self.nodes[0].add_p2p_connection(P2PDataStore())
- self.miniwallet = MiniWallet(self.nodes[0], use_p2pk=True)
+ self.miniwallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
self.log.info("Generate blocks in the past for coinbase outputs.")
long_past_time = int(time.time()) - 600 * 1000 # enough to build up to 1000 blocks 10 minutes apart without worrying about getting into the future
diff --git a/test/functional/feature_dersig.py b/test/functional/feature_dersig.py
index 3b430139b1..eb027c554a 100755
--- a/test/functional/feature_dersig.py
+++ b/test/functional/feature_dersig.py
@@ -7,7 +7,10 @@
Test that the DERSIG soft-fork activates at (regtest) height 1251.
"""
-from test_framework.blocktools import create_coinbase, create_block, create_transaction
+from test_framework.blocktools import (
+ create_block,
+ create_coinbase,
+)
from test_framework.messages import msg_block
from test_framework.p2p import P2PInterface
from test_framework.script import CScript
@@ -15,6 +18,10 @@ from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
)
+from test_framework.wallet import (
+ MiniWallet,
+ MiniWalletMode,
+)
DERSIG_HEIGHT = 1251
@@ -46,8 +53,9 @@ class BIP66Test(BitcoinTestFramework):
self.setup_clean_chain = True
self.rpc_timeout = 240
- def skip_test_if_missing_module(self):
- self.skip_if_no_wallet()
+ def create_tx(self, input_txid):
+ utxo_to_spend = self.miniwallet.get_utxo(txid=input_txid, mark_as_spent=False)
+ return self.miniwallet.create_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_to_spend)['tx']
def test_dersig_info(self, *, is_active):
assert_equal(self.nodes[0].getblockchaininfo()['softforks']['bip66'],
@@ -60,17 +68,16 @@ class BIP66Test(BitcoinTestFramework):
def run_test(self):
peer = self.nodes[0].add_p2p_connection(P2PInterface())
+ self.miniwallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
self.test_dersig_info(is_active=False)
self.log.info("Mining %d blocks", DERSIG_HEIGHT - 2)
- self.coinbase_txids = [self.nodes[0].getblock(b)['tx'][0] for b in self.nodes[0].generate(DERSIG_HEIGHT - 2)]
- self.nodeaddress = self.nodes[0].getnewaddress()
+ self.coinbase_txids = [self.nodes[0].getblock(b)['tx'][0] for b in self.miniwallet.generate(DERSIG_HEIGHT - 2)]
self.log.info("Test that a transaction with non-DER signature can still appear in a block")
- spendtx = create_transaction(self.nodes[0], self.coinbase_txids[0],
- self.nodeaddress, amount=1.0)
+ spendtx = self.create_tx(self.coinbase_txids[0])
unDERify(spendtx)
spendtx.rehash()
@@ -104,8 +111,7 @@ class BIP66Test(BitcoinTestFramework):
self.log.info("Test that transactions with non-DER signatures cannot appear in a block")
block.nVersion = 3
- spendtx = create_transaction(self.nodes[0], self.coinbase_txids[1],
- self.nodeaddress, amount=1.0)
+ spendtx = self.create_tx(self.coinbase_txids[1])
unDERify(spendtx)
spendtx.rehash()
@@ -133,7 +139,7 @@ class BIP66Test(BitcoinTestFramework):
peer.sync_with_ping()
self.log.info("Test that a version 3 block with a DERSIG-compliant transaction is accepted")
- block.vtx[1] = create_transaction(self.nodes[0], self.coinbase_txids[1], self.nodeaddress, amount=1.0)
+ block.vtx[1] = self.create_tx(self.coinbase_txids[1])
block.hashMerkleRoot = block.calc_merkle_root()
block.rehash()
block.solve()
diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py
index c4002f524a..12aac3ab65 100755
--- a/test/functional/mempool_accept.py
+++ b/test/functional/mempool_accept.py
@@ -67,7 +67,8 @@ class MempoolAcceptanceTest(BitcoinTestFramework):
self.log.info('Should not accept garbage to testmempoolaccept')
assert_raises_rpc_error(-3, 'Expected type array, got string', lambda: node.testmempoolaccept(rawtxs='ff00baar'))
- assert_raises_rpc_error(-8, 'Array must contain exactly one raw transaction for now', lambda: node.testmempoolaccept(rawtxs=['ff00baar', 'ff22']))
+ assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=['ff22']*26))
+ assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=[]))
assert_raises_rpc_error(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar']))
self.log.info('A transaction already in the blockchain')
diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py
index 2a58f8b3f7..9a2817a6ee 100755
--- a/test/functional/rpc_net.py
+++ b/test/functional/rpc_net.py
@@ -69,6 +69,7 @@ class NetTest(BitcoinTestFramework):
self.test_getaddednodeinfo()
self.test_service_flags()
self.test_getnodeaddresses()
+ self.test_addpeeraddress()
def test_connection_count(self):
self.log.info("Test getconnectioncount")
@@ -236,6 +237,30 @@ class NetTest(BitcoinTestFramework):
assert_raises_rpc_error(-8, "Address count out of range", self.nodes[0].getnodeaddresses, -1)
assert_raises_rpc_error(-8, "Network not recognized: Foo", self.nodes[0].getnodeaddresses, 1, "Foo")
+ def test_addpeeraddress(self):
+ self.log.info("Test addpeeraddress")
+ node = self.nodes[1]
+
+ self.log.debug("Test that addpeerinfo is a hidden RPC")
+ # It is hidden from general help, but its detailed help may be called directly.
+ assert "addpeerinfo" not in node.help()
+ assert "addpeerinfo" in node.help("addpeerinfo")
+
+ self.log.debug("Test that adding an empty address fails")
+ assert_equal(node.addpeeraddress(address="", port=8333), {"success": False})
+ assert_equal(node.getnodeaddresses(count=0), [])
+
+ self.log.debug("Test that adding a valid address succeeds")
+ assert_equal(node.addpeeraddress(address="1.2.3.4", port=8333), {"success": True})
+ addrs = node.getnodeaddresses(count=0)
+ assert_equal(len(addrs), 1)
+ assert_equal(addrs[0]["address"], "1.2.3.4")
+ assert_equal(addrs[0]["port"], 8333)
+
+ self.log.debug("Test that adding the same address again when already present fails")
+ assert_equal(node.addpeeraddress(address="1.2.3.4", port=8333), {"success": False})
+ assert_equal(len(node.getnodeaddresses(count=0)), 1)
+
if __name__ == '__main__':
NetTest().main()
diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py
new file mode 100755
index 0000000000..3d8d81d6b8
--- /dev/null
+++ b/test/functional/rpc_packages.py
@@ -0,0 +1,362 @@
+#!/usr/bin/env python3
+# Copyright (c) 2021 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""RPCs that handle raw transaction packages."""
+
+from decimal import Decimal
+from io import BytesIO
+import random
+
+from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.messages import (
+ BIP125_SEQUENCE_NUMBER,
+ COIN,
+ CTransaction,
+ CTxInWitness,
+)
+from test_framework.script import (
+ CScript,
+ OP_TRUE,
+)
+from test_framework.util import (
+ assert_equal,
+ hex_str_to_bytes,
+)
+
+class RPCPackagesTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.setup_clean_chain = True
+
+ def assert_testres_equal(self, package_hex, testres_expected):
+ """Shuffle package_hex and assert that the testmempoolaccept result matches testres_expected. This should only
+ be used to test packages where the order does not matter. The ordering of transactions in package_hex and
+ testres_expected must match.
+ """
+ shuffled_indeces = list(range(len(package_hex)))
+ random.shuffle(shuffled_indeces)
+ shuffled_package = [package_hex[i] for i in shuffled_indeces]
+ shuffled_testres = [testres_expected[i] for i in shuffled_indeces]
+ assert_equal(shuffled_testres, self.nodes[0].testmempoolaccept(shuffled_package))
+
+ def run_test(self):
+ self.log.info("Generate blocks to create UTXOs")
+ node = self.nodes[0]
+ self.privkeys = [node.get_deterministic_priv_key().key]
+ self.address = node.get_deterministic_priv_key().address
+ self.coins = []
+ # The last 100 coinbase transactions are premature
+ for b in node.generatetoaddress(200, self.address)[:100]:
+ coinbase = node.getblock(blockhash=b, verbosity=2)["tx"][0]
+ self.coins.append({
+ "txid": coinbase["txid"],
+ "amount": coinbase["vout"][0]["value"],
+ "scriptPubKey": coinbase["vout"][0]["scriptPubKey"],
+ })
+
+ # Create some transactions that can be reused throughout the test. Never submit these to mempool.
+ self.independent_txns_hex = []
+ self.independent_txns_testres = []
+ for _ in range(3):
+ coin = self.coins.pop()
+ rawtx = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}],
+ {self.address : coin["amount"] - Decimal("0.0001")})
+ signedtx = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys)
+ assert signedtx["complete"]
+ testres = node.testmempoolaccept([signedtx["hex"]])
+ assert testres[0]["allowed"]
+ self.independent_txns_hex.append(signedtx["hex"])
+ # testmempoolaccept returns a list of length one, avoid creating a 2D list
+ self.independent_txns_testres.append(testres[0])
+ self.independent_txns_testres_blank = [{
+ "txid": res["txid"], "wtxid": res["wtxid"]} for res in self.independent_txns_testres]
+
+ self.test_independent()
+ self.test_chain()
+ self.test_multiple_children()
+ self.test_multiple_parents()
+ self.test_conflicting()
+ self.test_rbf()
+
+ def chain_transaction(self, parent_txid, parent_value, n=0, parent_locking_script=None):
+ """Build a transaction that spends parent_txid.vout[n] and produces one output with
+ amount = parent_value with a fee deducted.
+ Return tuple (CTransaction object, raw hex, nValue, scriptPubKey of the output created).
+ """
+ node = self.nodes[0]
+ inputs = [{"txid": parent_txid, "vout": n}]
+ my_value = parent_value - Decimal("0.0001")
+ outputs = {self.address : my_value}
+ rawtx = node.createrawtransaction(inputs, outputs)
+ prevtxs = [{
+ "txid": parent_txid,
+ "vout": n,
+ "scriptPubKey": parent_locking_script,
+ "amount": parent_value,
+ }] if parent_locking_script else None
+ signedtx = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys, prevtxs=prevtxs)
+ tx = CTransaction()
+ assert signedtx["complete"]
+ tx.deserialize(BytesIO(hex_str_to_bytes(signedtx["hex"])))
+ return (tx, signedtx["hex"], my_value, tx.vout[0].scriptPubKey.hex())
+
+ def test_independent(self):
+ self.log.info("Test multiple independent transactions in a package")
+ node = self.nodes[0]
+ # For independent transactions, order doesn't matter.
+ self.assert_testres_equal(self.independent_txns_hex, self.independent_txns_testres)
+
+ self.log.info("Test an otherwise valid package with an extra garbage tx appended")
+ garbage_tx = node.createrawtransaction([{"txid": "00" * 32, "vout": 5}], {self.address: 1})
+ tx = CTransaction()
+ tx.deserialize(BytesIO(hex_str_to_bytes(garbage_tx)))
+ # Only the txid and wtxids are returned because validation is incomplete for the independent txns.
+ # Package validation is atomic: if the node cannot find a UTXO for any single tx in the package,
+ # it terminates immediately to avoid unnecessary, expensive signature verification.
+ package_bad = self.independent_txns_hex + [garbage_tx]
+ testres_bad = self.independent_txns_testres_blank + [{"txid": tx.rehash(), "wtxid": tx.getwtxid(), "allowed": False, "reject-reason": "missing-inputs"}]
+ self.assert_testres_equal(package_bad, testres_bad)
+
+ self.log.info("Check testmempoolaccept tells us when some transactions completed validation successfully")
+ coin = self.coins.pop()
+ tx_bad_sig_hex = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}],
+ {self.address : coin["amount"] - Decimal("0.0001")})
+ tx_bad_sig = CTransaction()
+ tx_bad_sig.deserialize(BytesIO(hex_str_to_bytes(tx_bad_sig_hex)))
+ testres_bad_sig = node.testmempoolaccept(self.independent_txns_hex + [tx_bad_sig_hex])
+ # By the time the signature for the last transaction is checked, all the other transactions
+ # have been fully validated, which is why the node returns full validation results for all
+ # transactions here but empty results in other cases.
+ assert_equal(testres_bad_sig, self.independent_txns_testres + [{
+ "txid": tx_bad_sig.rehash(),
+ "wtxid": tx_bad_sig.getwtxid(), "allowed": False,
+ "reject-reason": "mandatory-script-verify-flag-failed (Operation not valid with the current stack size)"
+ }])
+
+ self.log.info("Check testmempoolaccept reports txns in packages that exceed max feerate")
+ coin = self.coins.pop()
+ tx_high_fee_raw = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}],
+ {self.address : coin["amount"] - Decimal("0.999")})
+ tx_high_fee_signed = node.signrawtransactionwithkey(hexstring=tx_high_fee_raw, privkeys=self.privkeys)
+ assert tx_high_fee_signed["complete"]
+ tx_high_fee = CTransaction()
+ tx_high_fee.deserialize(BytesIO(hex_str_to_bytes(tx_high_fee_signed["hex"])))
+ testres_high_fee = node.testmempoolaccept([tx_high_fee_signed["hex"]])
+ assert_equal(testres_high_fee, [
+ {"txid": tx_high_fee.rehash(), "wtxid": tx_high_fee.getwtxid(), "allowed": False, "reject-reason": "max-fee-exceeded"}
+ ])
+ package_high_fee = [tx_high_fee_signed["hex"]] + self.independent_txns_hex
+ testres_package_high_fee = node.testmempoolaccept(package_high_fee)
+ assert_equal(testres_package_high_fee, testres_high_fee + self.independent_txns_testres_blank)
+
+ def test_chain(self):
+ node = self.nodes[0]
+ first_coin = self.coins.pop()
+
+ # Chain of 25 transactions
+ parent_locking_script = None
+ txid = first_coin["txid"]
+ chain_hex = []
+ chain_txns = []
+ value = first_coin["amount"]
+
+ for _ in range(25):
+ (tx, txhex, value, parent_locking_script) = self.chain_transaction(txid, value, 0, parent_locking_script)
+ txid = tx.rehash()
+ chain_hex.append(txhex)
+ chain_txns.append(tx)
+
+ self.log.info("Check that testmempoolaccept requires packages to be sorted by dependency")
+ assert_equal(node.testmempoolaccept(rawtxs=chain_hex[::-1]),
+ [{"txid": tx.rehash(), "wtxid": tx.getwtxid(), "package-error": "package-not-sorted"} for tx in chain_txns[::-1]])
+
+ self.log.info("Testmempoolaccept a chain of 25 transactions")
+ testres_multiple = node.testmempoolaccept(rawtxs=chain_hex)
+
+ testres_single = []
+ # Test accept and then submit each one individually, which should be identical to package test accept
+ for rawtx in chain_hex:
+ testres = node.testmempoolaccept([rawtx])
+ testres_single.append(testres[0])
+ # Submit the transaction now so its child should have no problem validating
+ node.sendrawtransaction(rawtx)
+ assert_equal(testres_single, testres_multiple)
+
+ # Clean up by clearing the mempool
+ node.generate(1)
+
+ def test_multiple_children(self):
+ node = self.nodes[0]
+
+ self.log.info("Testmempoolaccept a package in which a transaction has two children within the package")
+ first_coin = self.coins.pop()
+ value = (first_coin["amount"] - Decimal("0.0002")) / 2 # Deduct reasonable fee and make 2 outputs
+ inputs = [{"txid": first_coin["txid"], "vout": 0}]
+ outputs = [{self.address : value}, {ADDRESS_BCRT1_P2WSH_OP_TRUE : value}]
+ rawtx = node.createrawtransaction(inputs, outputs)
+
+ parent_signed = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys)
+ parent_tx = CTransaction()
+ assert parent_signed["complete"]
+ parent_tx.deserialize(BytesIO(hex_str_to_bytes(parent_signed["hex"])))
+ parent_txid = parent_tx.rehash()
+ assert node.testmempoolaccept([parent_signed["hex"]])[0]["allowed"]
+
+ parent_locking_script_a = parent_tx.vout[0].scriptPubKey.hex()
+ child_value = value - Decimal("0.0001")
+
+ # Child A
+ (_, tx_child_a_hex, _, _) = self.chain_transaction(parent_txid, child_value, 0, parent_locking_script_a)
+ assert not node.testmempoolaccept([tx_child_a_hex])[0]["allowed"]
+
+ # Child B
+ rawtx_b = node.createrawtransaction([{"txid": parent_txid, "vout": 1}], {self.address : child_value})
+ tx_child_b = CTransaction()
+ tx_child_b.deserialize(BytesIO(hex_str_to_bytes(rawtx_b)))
+ tx_child_b.wit.vtxinwit = [CTxInWitness()]
+ tx_child_b.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])]
+ tx_child_b_hex = tx_child_b.serialize().hex()
+ assert not node.testmempoolaccept([tx_child_b_hex])[0]["allowed"]
+
+ self.log.info("Testmempoolaccept with entire package, should work with children in either order")
+ testres_multiple_ab = node.testmempoolaccept(rawtxs=[parent_signed["hex"], tx_child_a_hex, tx_child_b_hex])
+ testres_multiple_ba = node.testmempoolaccept(rawtxs=[parent_signed["hex"], tx_child_b_hex, tx_child_a_hex])
+ assert all([testres["allowed"] for testres in testres_multiple_ab + testres_multiple_ba])
+
+ testres_single = []
+ # Test accept and then submit each one individually, which should be identical to package testaccept
+ for rawtx in [parent_signed["hex"], tx_child_a_hex, tx_child_b_hex]:
+ testres = node.testmempoolaccept([rawtx])
+ testres_single.append(testres[0])
+ # Submit the transaction now so its child should have no problem validating
+ node.sendrawtransaction(rawtx)
+ assert_equal(testres_single, testres_multiple_ab)
+
+ def create_child_with_parents(self, parents_tx, values, locking_scripts):
+ """Creates a transaction that spends the first output of each parent in parents_tx."""
+ num_parents = len(parents_tx)
+ total_value = sum(values)
+ inputs = [{"txid": tx.rehash(), "vout": 0} for tx in parents_tx]
+ outputs = {self.address : total_value - num_parents * Decimal("0.0001")}
+ rawtx_child = self.nodes[0].createrawtransaction(inputs, outputs)
+ prevtxs = []
+ for i in range(num_parents):
+ prevtxs.append({"txid": parents_tx[i].rehash(), "vout": 0, "scriptPubKey": locking_scripts[i], "amount": values[i]})
+ signedtx_child = self.nodes[0].signrawtransactionwithkey(hexstring=rawtx_child, privkeys=self.privkeys, prevtxs=prevtxs)
+ assert signedtx_child["complete"]
+ return signedtx_child["hex"]
+
+ def test_multiple_parents(self):
+ node = self.nodes[0]
+
+ self.log.info("Testmempoolaccept a package in which a transaction has multiple parents within the package")
+ for num_parents in [2, 10, 24]:
+ # Test a package with num_parents parents and 1 child transaction.
+ package_hex = []
+ parents_tx = []
+ values = []
+ parent_locking_scripts = []
+ for _ in range(num_parents):
+ parent_coin = self.coins.pop()
+ value = parent_coin["amount"]
+ (tx, txhex, value, parent_locking_script) = self.chain_transaction(parent_coin["txid"], value)
+ package_hex.append(txhex)
+ parents_tx.append(tx)
+ values.append(value)
+ parent_locking_scripts.append(parent_locking_script)
+ child_hex = self.create_child_with_parents(parents_tx, values, parent_locking_scripts)
+ # Package accept should work with the parents in any order (as long as parents come before child)
+ for _ in range(10):
+ random.shuffle(package_hex)
+ testres_multiple = node.testmempoolaccept(rawtxs=package_hex + [child_hex])
+ assert all([testres["allowed"] for testres in testres_multiple])
+
+ testres_single = []
+ # Test accept and then submit each one individually, which should be identical to package testaccept
+ for rawtx in package_hex + [child_hex]:
+ testres_single.append(node.testmempoolaccept([rawtx])[0])
+ # Submit the transaction now so its child should have no problem validating
+ node.sendrawtransaction(rawtx)
+ assert_equal(testres_single, testres_multiple)
+
+ def test_conflicting(self):
+ node = self.nodes[0]
+ prevtx = self.coins.pop()
+ inputs = [{"txid": prevtx["txid"], "vout": 0}]
+ output1 = {node.get_deterministic_priv_key().address: 50 - 0.00125}
+ output2 = {ADDRESS_BCRT1_P2WSH_OP_TRUE: 50 - 0.00125}
+
+ # tx1 and tx2 share the same inputs
+ rawtx1 = node.createrawtransaction(inputs, output1)
+ rawtx2 = node.createrawtransaction(inputs, output2)
+ signedtx1 = node.signrawtransactionwithkey(hexstring=rawtx1, privkeys=self.privkeys)
+ signedtx2 = node.signrawtransactionwithkey(hexstring=rawtx2, privkeys=self.privkeys)
+ tx1 = CTransaction()
+ tx1.deserialize(BytesIO(hex_str_to_bytes(signedtx1["hex"])))
+ tx2 = CTransaction()
+ tx2.deserialize(BytesIO(hex_str_to_bytes(signedtx2["hex"])))
+ assert signedtx1["complete"]
+ assert signedtx2["complete"]
+
+ # Ensure tx1 and tx2 are valid by themselves
+ assert node.testmempoolaccept([signedtx1["hex"]])[0]["allowed"]
+ assert node.testmempoolaccept([signedtx2["hex"]])[0]["allowed"]
+
+ self.log.info("Test duplicate transactions in the same package")
+ testres = node.testmempoolaccept([signedtx1["hex"], signedtx1["hex"]])
+ assert_equal(testres, [
+ {"txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package"},
+ {"txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package"}
+ ])
+
+ self.log.info("Test conflicting transactions in the same package")
+ testres = node.testmempoolaccept([signedtx1["hex"], signedtx2["hex"]])
+ assert_equal(testres, [
+ {"txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package"},
+ {"txid": tx2.rehash(), "wtxid": tx2.getwtxid(), "package-error": "conflict-in-package"}
+ ])
+
+ def test_rbf(self):
+ node = self.nodes[0]
+ coin = self.coins.pop()
+ inputs = [{"txid": coin["txid"], "vout": 0, "sequence": BIP125_SEQUENCE_NUMBER}]
+ fee = Decimal('0.00125000')
+ output = {node.get_deterministic_priv_key().address: 50 - fee}
+ raw_replaceable_tx = node.createrawtransaction(inputs, output)
+ signed_replaceable_tx = node.signrawtransactionwithkey(hexstring=raw_replaceable_tx, privkeys=self.privkeys)
+ testres_replaceable = node.testmempoolaccept([signed_replaceable_tx["hex"]])
+ replaceable_tx = CTransaction()
+ replaceable_tx.deserialize(BytesIO(hex_str_to_bytes(signed_replaceable_tx["hex"])))
+ assert_equal(testres_replaceable, [
+ {"txid": replaceable_tx.rehash(), "wtxid": replaceable_tx.getwtxid(),
+ "allowed": True, "vsize": replaceable_tx.get_vsize(), "fees": { "base": fee }}
+ ])
+
+ # Replacement transaction is identical except has double the fee
+ replacement_tx = CTransaction()
+ replacement_tx.deserialize(BytesIO(hex_str_to_bytes(signed_replaceable_tx["hex"])))
+ replacement_tx.vout[0].nValue -= int(fee * COIN) # Doubled fee
+ signed_replacement_tx = node.signrawtransactionwithkey(replacement_tx.serialize().hex(), self.privkeys)
+ replacement_tx.deserialize(BytesIO(hex_str_to_bytes(signed_replacement_tx["hex"])))
+
+ self.log.info("Test that transactions within a package cannot replace each other")
+ testres_rbf_conflicting = node.testmempoolaccept([signed_replaceable_tx["hex"], signed_replacement_tx["hex"]])
+ assert_equal(testres_rbf_conflicting, [
+ {"txid": replaceable_tx.rehash(), "wtxid": replaceable_tx.getwtxid(), "package-error": "conflict-in-package"},
+ {"txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "package-error": "conflict-in-package"}
+ ])
+
+ self.log.info("Test that packages cannot conflict with mempool transactions, even if a valid BIP125 RBF")
+ node.sendrawtransaction(signed_replaceable_tx["hex"])
+ testres_rbf_single = node.testmempoolaccept([signed_replacement_tx["hex"]])
+ # This transaction is a valid BIP125 replace-by-fee
+ assert testres_rbf_single[0]["allowed"]
+ testres_rbf_package = self.independent_txns_testres_blank + [{
+ "txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "allowed": False, "reject-reason": "txn-mempool-conflict"
+ }]
+ self.assert_testres_equal(self.independent_txns_hex + [signed_replacement_tx["hex"]], testres_rbf_package)
+
+if __name__ == "__main__":
+ RPCPackagesTest().main()
diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py
index 18822fc610..05bda5d899 100644
--- a/test/functional/test_framework/wallet.py
+++ b/test/functional/test_framework/wallet.py
@@ -5,6 +5,7 @@
"""A limited-functionality wallet, which may replace a real wallet in tests"""
from decimal import Decimal
+from enum import Enum
from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE
from test_framework.key import ECKey
from test_framework.messages import (
@@ -30,22 +31,46 @@ from test_framework.util import (
)
+class MiniWalletMode(Enum):
+ """Determines the transaction type the MiniWallet is creating and spending.
+
+ For most purposes, the default mode ADDRESS_OP_TRUE should be sufficient;
+ it simply uses a fixed bech32 P2WSH address whose coins are spent with a
+ witness stack of OP_TRUE, i.e. following an anyone-can-spend policy.
+ However, if the transactions need to be modified by the user (e.g. prepending
+ scriptSig for testing opcodes that are activated by a soft-fork), or the txs
+ should contain an actual signature, the raw modes RAW_OP_TRUE and RAW_P2PK
+ can be useful. Summary of modes:
+
+ | output | | tx is | can modify | needs
+ mode | description | address | standard | scriptSig | signing
+ ----------------+-------------------+-----------+----------+------------+----------
+ ADDRESS_OP_TRUE | anyone-can-spend | bech32 | yes | no | no
+ RAW_OP_TRUE | anyone-can-spend | - (raw) | no | yes | no
+ RAW_P2PK | pay-to-public-key | - (raw) | yes | yes | yes
+ """
+ ADDRESS_OP_TRUE = 1
+ RAW_OP_TRUE = 2
+ RAW_P2PK = 3
+
+
class MiniWallet:
- def __init__(self, test_node, *, raw_script=False, use_p2pk=False):
+ def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE):
self._test_node = test_node
self._utxos = []
self._priv_key = None
self._address = None
- if raw_script:
+ assert isinstance(mode, MiniWalletMode)
+ if mode == MiniWalletMode.RAW_OP_TRUE:
self._scriptPubKey = bytes(CScript([OP_TRUE]))
- elif use_p2pk:
+ elif mode == MiniWalletMode.RAW_P2PK:
# use simple deterministic private key (k=1)
self._priv_key = ECKey()
self._priv_key.set((1).to_bytes(32, 'big'), True)
pub_key = self._priv_key.get_pubkey()
self._scriptPubKey = bytes(CScript([pub_key.get_bytes(), OP_CHECKSIG]))
- else:
+ elif mode == MiniWalletMode.ADDRESS_OP_TRUE:
self._address = ADDRESS_BCRT1_P2WSH_OP_TRUE
self._scriptPubKey = hex_str_to_bytes(self._test_node.validateaddress(self._address)['scriptPubKey'])
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 00527e78f1..49f269f8b4 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -211,6 +211,7 @@ BASE_SCRIPTS = [
'mempool_package_onemore.py',
'rpc_createmultisig.py --legacy-wallet',
'rpc_createmultisig.py --descriptors',
+ 'rpc_packages.py',
'feature_versionbits_warning.py',
'rpc_preciousblock.py',
'wallet_importprunedfunds.py --legacy-wallet',
diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py
index 1d3736d9b1..c13d8de4b5 100755
--- a/test/functional/wallet_avoidreuse.py
+++ b/test/functional/wallet_avoidreuse.py
@@ -42,25 +42,25 @@ def count_unspent(node):
r["reused"]["supported"] = supports_reused
return r
-def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None):
+def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None, margin=0.001):
'''Make assertions about a node's unspent output statistics'''
stats = count_unspent(node)
if total_count is not None:
assert_equal(stats["total"]["count"], total_count)
if total_sum is not None:
- assert_approx(stats["total"]["sum"], total_sum, 0.001)
+ assert_approx(stats["total"]["sum"], total_sum, margin)
if reused_supported is not None:
assert_equal(stats["reused"]["supported"], reused_supported)
if reused_count is not None:
assert_equal(stats["reused"]["count"], reused_count)
if reused_sum is not None:
- assert_approx(stats["reused"]["sum"], reused_sum, 0.001)
+ assert_approx(stats["reused"]["sum"], reused_sum, margin)
-def assert_balances(node, mine):
+def assert_balances(node, mine, margin=0.001):
'''Make assertions about a node's getbalances output'''
got = node.getbalances()["mine"]
for k,v in mine.items():
- assert_approx(got[k], v, 0.001)
+ assert_approx(got[k], v, margin)
class AvoidReuseTest(BitcoinTestFramework):
@@ -299,7 +299,7 @@ class AvoidReuseTest(BitcoinTestFramework):
ret_addr = self.nodes[0].getnewaddress()
# send multiple transactions, reusing one address
- for _ in range(11):
+ for _ in range(101):
self.nodes[0].sendtoaddress(new_addr, 1)
self.nodes[0].generate(1)
@@ -311,14 +311,14 @@ class AvoidReuseTest(BitcoinTestFramework):
# getbalances and listunspent should show the remaining outputs
# in the reused address as used/reused
- assert_unspent(self.nodes[1], total_count=2, total_sum=6, reused_count=1, reused_sum=1)
- assert_balances(self.nodes[1], mine={"used": 1, "trusted": 5})
+ assert_unspent(self.nodes[1], total_count=2, total_sum=96, reused_count=1, reused_sum=1, margin=0.01)
+ assert_balances(self.nodes[1], mine={"used": 1, "trusted": 95}, margin=0.01)
def test_full_destination_group_is_preferred(self):
'''
- Test the case where [1] only has 11 outputs of 1 BTC in the same reused
+ Test the case where [1] only has 101 outputs of 1 BTC in the same reused
address and tries to send a small payment of 0.5 BTC. The wallet
- should use 10 outputs from the reused address as inputs and not a
+ should use 100 outputs from the reused address as inputs and not a
single 1 BTC input, in order to join several outputs from the reused
address.
'''
@@ -330,8 +330,8 @@ class AvoidReuseTest(BitcoinTestFramework):
new_addr = self.nodes[1].getnewaddress()
ret_addr = self.nodes[0].getnewaddress()
- # Send 11 outputs of 1 BTC to the same, reused address in the wallet
- for _ in range(11):
+ # Send 101 outputs of 1 BTC to the same, reused address in the wallet
+ for _ in range(101):
self.nodes[0].sendtoaddress(new_addr, 1)
self.nodes[0].generate(1)
@@ -342,14 +342,14 @@ class AvoidReuseTest(BitcoinTestFramework):
txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=0.5)
inputs = self.nodes[1].getrawtransaction(txid, 1)["vin"]
- # The transaction should use 10 inputs exactly
- assert_equal(len(inputs), 10)
+ # The transaction should use 100 inputs exactly
+ assert_equal(len(inputs), 100)
def test_all_destination_groups_are_used(self):
'''
- Test the case where [1] only has 22 outputs of 1 BTC in the same reused
- address and tries to send a payment of 20.5 BTC. The wallet
- should use all 22 outputs from the reused address as inputs.
+ Test the case where [1] only has 202 outputs of 1 BTC in the same reused
+ address and tries to send a payment of 200.5 BTC. The wallet
+ should use all 202 outputs from the reused address as inputs.
'''
self.log.info("Test that all destination groups are used")
@@ -359,20 +359,20 @@ class AvoidReuseTest(BitcoinTestFramework):
new_addr = self.nodes[1].getnewaddress()
ret_addr = self.nodes[0].getnewaddress()
- # Send 22 outputs of 1 BTC to the same, reused address in the wallet
- for _ in range(22):
+ # Send 202 outputs of 1 BTC to the same, reused address in the wallet
+ for _ in range(202):
self.nodes[0].sendtoaddress(new_addr, 1)
self.nodes[0].generate(1)
self.sync_all()
# Sending a transaction that needs to use the full groups
- # of 10 inputs but also the incomplete group of 2 inputs.
- txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=20.5)
+ # of 100 inputs but also the incomplete group of 2 inputs.
+ txid = self.nodes[1].sendtoaddress(address=ret_addr, amount=200.5)
inputs = self.nodes[1].getrawtransaction(txid, 1)["vin"]
- # The transaction should use 22 inputs exactly
- assert_equal(len(inputs), 22)
+ # The transaction should use 202 inputs exactly
+ assert_equal(len(inputs), 202)
if __name__ == '__main__':
diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh
index 354e14f361..07d613f62d 100755
--- a/test/lint/lint-circular-dependencies.sh
+++ b/test/lint/lint-circular-dependencies.sh
@@ -25,6 +25,10 @@ EXPECTED_CIRCULAR_DEPENDENCIES=(
"wallet/fees -> wallet/wallet -> wallet/fees"
"wallet/wallet -> wallet/walletdb -> wallet/wallet"
"node/coinstats -> validation -> node/coinstats"
+ # Temporary circular dependencies that allow wallet.h/wallet.cpp to be
+ # split up in a MOVEONLY commit. These are removed in #21206.
+ "wallet/receive -> wallet/wallet -> wallet/receive"
+ "wallet/spend -> wallet/wallet -> wallet/spend"
)
EXIT_CODE=0