diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index 740a8c02750f..d4915b2296a6 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -7,24 +7,19 @@ from decimal import Decimal import random -from test_framework.address import ADDRESS_BCRT1_P2SH_OP_TRUE -from test_framework.test_framework import BitcoinTestFramework +from test_framework.blocktools import COINBASE_MATURITY from test_framework.messages import ( tx_from_hex, ) -from test_framework.script import ( - CScript, - OP_TRUE, -) +from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, ) from test_framework.wallet import ( - create_child_with_parents, - create_raw_chain, - make_chain, + MiniWallet, ) + class RPCPackagesTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -42,52 +37,53 @@ def assert_testres_equal(self, package_hex, testres_expected): 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 self.generatetoaddress(node, 200, self.address)[:100]: - coinbase = node.getblock(blockhash=b, verbosity=2)["tx"][0] - self.coins.append({ + + # get an UTXO that requires signature to be spent + deterministic_address = node.get_deterministic_priv_key().address + blockhash = self.generatetoaddress(node, 1, deterministic_address)[0] + coinbase = node.getblock(blockhash=blockhash, verbosity=2)["tx"][0] + coin = { "txid": coinbase["txid"], "amount": coinbase["vout"][0]["value"], "scriptPubKey": coinbase["vout"][0]["scriptPubKey"], - }) + "vout": 0, + "height": 0 + } + + self.wallet = MiniWallet(self.nodes[0]) + self.generate(self.wallet, COINBASE_MATURITY + 100) # blocks generated for inputs + self.log.info("Create some transactions") # 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"]]) + tx_hex = self.wallet.create_self_transfer(fee_rate=Decimal("0.0001"))["hex"] + testres = self.nodes[0].testmempoolaccept([tx_hex]) assert testres[0]["allowed"] - self.independent_txns_hex.append(signedtx["hex"]) + self.independent_txns_hex.append(tx_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"]} for res in self.independent_txns_testres] - self.test_independent() + self.test_independent(coin) self.test_chain() self.test_multiple_children() self.test_multiple_parents() self.test_conflicting() - def test_independent(self): + def test_independent(self, coin): 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}) + address = node.get_deterministic_priv_key().address + garbage_tx = node.createrawtransaction([{"txid": "00" * 32, "vout": 5}], {address: 1}) tx = tx_from_hex(garbage_tx) # Only the txid is 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, @@ -97,9 +93,8 @@ def test_independent(self): 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")}) + {address : coin["amount"] - Decimal("0.0001")}) tx_bad_sig = tx_from_hex(tx_bad_sig_hex) tx_bad_sig_hex = tx_bad_sig.serialize().hex() testres_bad_sig = node.testmempoolaccept(self.independent_txns_hex + [tx_bad_sig_hex]) @@ -113,24 +108,22 @@ def test_independent(self): }]) 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 = tx_from_hex(tx_high_fee_signed["hex"]) - testres_high_fee = node.testmempoolaccept([tx_high_fee_signed["hex"]]) + tx_high_fee = self.wallet.create_self_transfer(fee=Decimal("0.999")) + testres_high_fee = node.testmempoolaccept([tx_high_fee["hex"]]) assert_equal(testres_high_fee, [ - {"txid": tx_high_fee.rehash(), "allowed": False, "reject-reason": "max-fee-exceeded"} + {"txid": tx_high_fee["txid"], "allowed": False, "reject-reason": "max-fee-exceeded"} ]) - package_high_fee = [tx_high_fee_signed["hex"]] + self.independent_txns_hex + package_high_fee = [tx_high_fee["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_hex, chain_txns) = create_raw_chain(node, first_coin, self.address, self.privkeys) + + chain = self.wallet.create_self_transfer_chain(chain_length=25) + chain_hex = chain["chain_hex"] + chain_txns = chain["chain_txns"] + self.log.info("Check that testmempoolaccept requires packages to be sorted by dependency") assert_equal(node.testmempoolaccept(rawtxs=chain_hex[::-1]), [{"txid": tx.rehash(), "package-error": "package-not-sorted"} for tx in chain_txns[::-1]]) @@ -152,77 +145,57 @@ def test_chain(self): 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_P2SH_OP_TRUE : value}] - rawtx = node.createrawtransaction(inputs, outputs) - - parent_signed = node.signrawtransactionwithkey(hexstring=rawtx, privkeys=self.privkeys) - parent_tx = tx_from_hex(parent_signed["hex"]) - assert parent_signed["complete"] - 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") + parent_tx = self.wallet.create_self_transfer_multi(num_outputs=2) + assert node.testmempoolaccept([parent_tx["hex"]])[0]["allowed"] # Child A - (_, tx_child_a_hex, _, _) = make_chain(node, self.address, self.privkeys, parent_txid, child_value, 0, parent_locking_script_a) - assert not node.testmempoolaccept([tx_child_a_hex])[0]["allowed"] + child_a_tx = self.wallet.create_self_transfer(utxo_to_spend=parent_tx["new_utxos"][0]) + assert not node.testmempoolaccept([child_a_tx["hex"]])[0]["allowed"] # Child B - rawtx_b = node.createrawtransaction([{"txid": parent_txid, "vout": 1}], {self.address : child_value}) - tx_child_b = tx_from_hex(rawtx_b) - tx_child_b.vin[0].scriptSig = CScript([CScript([OP_TRUE])]) - tx_child_b_hex = tx_child_b.serialize().hex() - assert not node.testmempoolaccept([tx_child_b_hex])[0]["allowed"] + child_b_tx = self.wallet.create_self_transfer(utxo_to_spend=parent_tx["new_utxos"][1]) + assert not node.testmempoolaccept([child_b_tx["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]) + testres_multiple_ab = node.testmempoolaccept(rawtxs=[parent_tx["hex"], child_a_tx["hex"], child_b_tx["hex"]]) + testres_multiple_ba = node.testmempoolaccept(rawtxs=[parent_tx["hex"], child_b_tx["hex"], child_a_tx["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]: + for rawtx in [parent_tx["hex"], child_a_tx["hex"], child_b_tx["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 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. + parent_coins = [] 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) = make_chain(node, self.address, self.privkeys, parent_coin["txid"], value) - package_hex.append(txhex) - parents_tx.append(tx) - values.append(value) - parent_locking_scripts.append(parent_locking_script) - child_hex = create_child_with_parents(node, self.address, self.privkeys, parents_tx, values, parent_locking_scripts) - # Package accept should work with the parents in any order (as long as parents come before child) + # Package accept should work with the parents in any order (as long as parents come before child) + parent_tx = self.wallet.create_self_transfer() + parent_coins.append(parent_tx["new_utxo"]) + package_hex.append(parent_tx["hex"]) + + child_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins, fee_per_output=2000) for _ in range(10): random.shuffle(package_hex) - testres_multiple = node.testmempoolaccept(rawtxs=package_hex + [child_hex]) + testres_multiple = node.testmempoolaccept(rawtxs=package_hex + [child_tx['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]: + for rawtx in package_hex + [child_tx["hex"]]: testres_single.append(node.testmempoolaccept([rawtx])[0]) # Submit the transaction now so its child should have no problem validating node.sendrawtransaction(rawtx) @@ -230,38 +203,30 @@ def test_multiple_parents(self): 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: 500 - 0.00125} - output2 = {ADDRESS_BCRT1_P2SH_OP_TRUE: 500 - 0.00125} + coin = self.wallet.get_utxo() # 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 = tx_from_hex(signedtx1["hex"]) - tx2 = tx_from_hex(signedtx2["hex"]) - assert signedtx1["complete"] - assert signedtx2["complete"] + tx1 = self.wallet.create_self_transfer(utxo_to_spend=coin) + tx2 = self.wallet.create_self_transfer(utxo_to_spend=coin) # Ensure tx1 and tx2 are valid by themselves - assert node.testmempoolaccept([signedtx1["hex"]])[0]["allowed"] - assert node.testmempoolaccept([signedtx2["hex"]])[0]["allowed"] + assert node.testmempoolaccept([tx1["hex"]])[0]["allowed"] + assert node.testmempoolaccept([tx2["hex"]])[0]["allowed"] self.log.info("Test duplicate transactions in the same package") - testres = node.testmempoolaccept([signedtx1["hex"], signedtx1["hex"]]) + testres = node.testmempoolaccept([tx1["hex"], tx1["hex"]]) assert_equal(testres, [ - {"txid": tx1.rehash(), "package-error": "conflict-in-package"}, - {"txid": tx1.rehash(), "package-error": "conflict-in-package"} + {"txid": tx1["txid"], "package-error": "conflict-in-package"}, + {"txid": tx1["txid"], "package-error": "conflict-in-package"} ]) self.log.info("Test conflicting transactions in the same package") - testres = node.testmempoolaccept([signedtx1["hex"], signedtx2["hex"]]) + testres = node.testmempoolaccept([tx1["hex"], tx2["hex"]]) assert_equal(testres, [ - {"txid": tx1.rehash(), "package-error": "conflict-in-package"}, - {"txid": tx2.rehash(), "package-error": "conflict-in-package"} + {"txid": tx1["txid"], "package-error": "conflict-in-package"}, + {"txid": tx2["txid"], "package-error": "conflict-in-package"} ]) + if __name__ == "__main__": RPCPackagesTest().main() diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index e350614b20cb..825a6c990055 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -319,6 +319,38 @@ def sendrawtransaction(self, *, from_node, tx_hex, maxfeerate=0, **kwargs): self.scan_tx(from_node.decoderawtransaction(tx_hex)) return txid + def create_self_transfer_chain(self, *, chain_length): + """ + Create a "chain" of chain_length transactions. The nth transaction in + the chain is a child of the n-1th transaction and parent of the n+1th transaction. + + Returns a dic {"chain_hex": chain_hex, "chain_txns" : chain_txns} + + "chain_hex" is a list representing the chain's transactions in hexadecimal. + "chain_txns" is a list representing the chain's transactions in the CTransaction object. + """ + chaintip_utxo = self.get_utxo() + chain_hex = [] + chain_txns = [] + + for _ in range(chain_length): + tx = self.create_self_transfer(utxo_to_spend=chaintip_utxo) + chaintip_utxo = tx["new_utxo"] + chain_hex.append(tx["hex"]) + chain_txns.append(tx["tx"]) + + return {"chain_hex": chain_hex, "chain_txns" : chain_txns} + + def send_self_transfer_chain(self, *, from_node, chain_length, utxo_to_spend=None): + """Create and send a "chain" of chain_length transactions. The nth transaction in + the chain is a child of the n-1th transaction and parent of the n+1th transaction. + + Returns the txid of the last transaction.""" + chaintip_utxo = utxo_to_spend or self.get_utxo() + for _ in range(chain_length): + chaintip_utxo = self.send_self_transfer(utxo_to_spend=chaintip_utxo, from_node=from_node)["new_utxo"] + return chaintip_utxo["txid"] + def getnewdestination(address_type='legacy'): """Generate a random destination of the specified type and return the @@ -349,58 +381,6 @@ def address_to_scriptpubkey(address): assert False -def make_chain(node, address, privkeys, parent_txid, parent_value, n=0, parent_locking_script=None, fee=DEFAULT_FEE): - """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). - """ - inputs = [{"txid": parent_txid, "vout": n}] - my_value = parent_value - fee - outputs = {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=privkeys, prevtxs=prevtxs) - assert signedtx["complete"] - tx = tx_from_hex(signedtx["hex"]) - return (tx, signedtx["hex"], my_value, tx.vout[0].scriptPubKey.hex()) - -def create_child_with_parents(node, address, privkeys, parents_tx, values, locking_scripts, fee=DEFAULT_FEE): - """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 = {address : total_value - fee} - rawtx_child = node.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 = node.signrawtransactionwithkey(hexstring=rawtx_child, privkeys=privkeys, prevtxs=prevtxs) - assert signedtx_child["complete"] - return signedtx_child["hex"] - -def create_raw_chain(node, first_coin, address, privkeys, chain_length=25): - """Helper function: create a "chain" of chain_length transactions. The nth transaction in the - chain is a child of the n-1th transaction and parent of the n+1th transaction. - """ - parent_locking_script = None - txid = first_coin["txid"] - chain_hex = [] - chain_txns = [] - value = first_coin["amount"] - - for _ in range(chain_length): - (tx, txhex, value, parent_locking_script) = make_chain(node, address, privkeys, txid, value, 0, parent_locking_script) - txid = tx.rehash() - chain_hex.append(txhex) - chain_txns.append(tx) - - return (chain_hex, chain_txns) - def bulk_transaction(tx, node, target_weight, privkeys, prevtxs=None): """Pad a transaction with extra outputs until it reaches a target weight (or higher). returns CTransaction object