Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 66 additions & 101 deletions test/functional/rpc_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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])
Expand All @@ -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]])
Expand All @@ -152,116 +145,88 @@ 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)
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: 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()
84 changes: 32 additions & 52 deletions test/functional/test_framework/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading