diff options
Diffstat (limited to 'test')
-rwxr-xr-x | test/functional/feature_cltv.py | 7 | ||||
-rwxr-xr-x | test/functional/feature_csv_activation.py | 7 | ||||
-rwxr-xr-x | test/functional/feature_dersig.py | 26 | ||||
-rwxr-xr-x | test/functional/mempool_accept.py | 3 | ||||
-rwxr-xr-x | test/functional/rpc_net.py | 25 | ||||
-rwxr-xr-x | test/functional/rpc_packages.py | 362 | ||||
-rw-r--r-- | test/functional/test_framework/wallet.py | 33 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 1 | ||||
-rwxr-xr-x | test/functional/wallet_avoidreuse.py | 46 | ||||
-rwxr-xr-x | test/lint/lint-circular-dependencies.sh | 4 |
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 |