Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13711470
D12126.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Subscribers
None
D12126.id.diff
View Options
diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py
new file mode 100755
--- /dev/null
+++ b/test/functional/rpc_packages.py
@@ -0,0 +1,377 @@
+#!/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."""
+
+import random
+from decimal import Decimal
+
+from test_framework.address import (
+ ADDRESS_ECREG_P2SH_OP_TRUE,
+ SCRIPTSIG_OP_TRUE,
+)
+from test_framework.messages import CTransaction, FromHex, ToHex
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.txtools import pad_tx
+from test_framework.util import assert_equal
+
+
+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_indices = list(range(len(package_hex)))
+ random.shuffle(shuffled_indices)
+ shuffled_package = [package_hex[i] for i in shuffled_indices]
+ shuffled_testres = [testres_expected[i] for i in shuffled_indices]
+ 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(300, 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("100.00")})
+ 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"], } for res in self.independent_txns_testres]
+
+ self.test_independent()
+ self.test_chain()
+ self.test_multiple_children()
+ self.test_multiple_parents()
+ self.test_conflicting()
+
+ 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("100.00")
+ 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 = FromHex(CTransaction(), signedtx["hex"])
+ assert signedtx["complete"]
+ 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_000_000})
+ tx = FromHex(CTransaction(), garbage_tx)
+ pad_tx(tx)
+ garbage_tx = ToHex(tx)
+
+ # This particular test differs from Core, because we do not test the
+ # missing inputs separately from the signature verification for a given
+ # transaction. Both are done in validation as part of PreChecks.
+ # See https://reviews.bitcoinabc.org/D8203
+ testres_bad = node.testmempoolaccept(
+ self.independent_txns_hex + [garbage_tx])
+ assert_equal(
+ testres_bad,
+ self.independent_txns_testres + [
+ {"txid": tx.get_id(), "allowed": False,
+ "reject-reason": "missing-inputs"}])
+
+ 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("100.00")})
+ tx_bad_sig = FromHex(CTransaction(), tx_bad_sig_hex)
+ pad_tx(tx_bad_sig)
+ tx_bad_sig_hex = ToHex(tx_bad_sig)
+ 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.get_id(), "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("999_000")})
+ tx_high_fee_signed = node.signrawtransactionwithkey(
+ hexstring=tx_high_fee_raw, privkeys=self.privkeys)
+ assert tx_high_fee_signed["complete"]
+ tx_high_fee = FromHex(CTransaction(), tx_high_fee_signed["hex"])
+ testres_high_fee = node.testmempoolaccept([tx_high_fee_signed["hex"]])
+ assert_equal(
+ testres_high_fee,
+ [{"txid": tx_high_fee.get_id(), "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 50 transactions
+ parent_locking_script = None
+ txid = first_coin["txid"]
+ chain_hex = []
+ chain_txns = []
+ value = first_coin["amount"]
+
+ for _ in range(50):
+ (tx, txhex, value, parent_locking_script) = self.chain_transaction(
+ txid, value, 0, parent_locking_script)
+ txid = tx.get_id()
+ chain_hex.append(txhex)
+ chain_txns.append(tx)
+
+ self.log.info(
+ "Check that testmempoolaccept requires packages to be sorted by dependency")
+ testres_multiple_unsorted = node.testmempoolaccept(
+ rawtxs=chain_hex[::-1])
+ assert_equal(testres_multiple_unsorted,
+ [{"txid": chain_txns[-1].get_id(), "allowed": False,
+ "reject-reason": "missing-inputs"}]
+ + [{"txid": tx.get_id(), } for tx in chain_txns[::-1]][1:])
+
+ self.log.info("Testmempoolaccept a chain of 50 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()
+ # Deduct reasonable fee and make 2 outputs
+ value = (first_coin["amount"] - Decimal("200.00")) / 2
+ inputs = [{"txid": first_coin["txid"], "vout": 0}]
+ outputs = [{self.address: value}, {ADDRESS_ECREG_P2SH_OP_TRUE: value}]
+ rawtx = node.createrawtransaction(inputs, outputs)
+
+ parent_signed = node.signrawtransactionwithkey(
+ hexstring=rawtx, privkeys=self.privkeys)
+ assert parent_signed["complete"]
+ parent_tx = FromHex(CTransaction(), parent_signed["hex"])
+ parent_txid = parent_tx.get_id()
+ assert node.testmempoolaccept([parent_signed["hex"]])[0]["allowed"]
+
+ parent_locking_script_a = parent_tx.vout[0].scriptPubKey.hex()
+ child_value = value - Decimal("100.00")
+
+ # Child A
+ (_, tx_child_a_hex, _, _) = self.chain_transaction(
+ parent_txid, 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 = FromHex(CTransaction(), rawtx_b)
+ tx_child_b.vin[0].scriptSig = SCRIPTSIG_OP_TRUE
+ pad_tx(tx_child_b)
+ tx_child_b_hex = ToHex(tx_child_b)
+ 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.get_id(), "vout": 0} for tx in parents_tx]
+ outputs = {self.address: total_value - num_parents * Decimal("100.00")}
+ rawtx_child = self.nodes[0].createrawtransaction(inputs, outputs)
+ prevtxs = []
+ for i in range(num_parents):
+ prevtxs.append(
+ {"txid": parents_tx[i].get_id(), "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, 49]:
+ # 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_000_000 -
+ 1250}
+ output2 = {ADDRESS_ECREG_P2SH_OP_TRUE: 50_000_000 - 1250}
+
+ # 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 = FromHex(CTransaction(), signedtx1["hex"])
+ tx2 = FromHex(CTransaction(), 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.get_id(), "package-error": "conflict-in-package"},
+ {"txid": tx1.get_id(), "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.get_id(), "package-error": "conflict-in-package"},
+ {"txid": tx2.get_id(), "package-error": "conflict-in-package"}
+ ])
+
+
+if __name__ == "__main__":
+ RPCPackagesTest().main()
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 26, 12:08 (2 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573517
Default Alt Text
D12126.id.diff (17 KB)
Attached To
D12126: [test] functional test for packages in RPCs
Event Timeline
Log In to Comment