diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -157,6 +157,7 @@ 'abc-p2p-activation.py', 'abc-p2p-fullblocktest.py', 'abc-rpc.py', + 'mempool-accept-txn.py', ] if ENABLE_ZMQ: testScripts.append('zmq_test.py') diff --git a/qa/rpc-tests/mempool-accept-txn.py b/qa/rpc-tests/mempool-accept-txn.py new file mode 100755 --- /dev/null +++ b/qa/rpc-tests/mempool-accept-txn.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Copyright (c) 2017 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +""" +This test checks acceptance of transactions by the mempool +It is derived from the much more complex p2p-fullblocktest. +""" + +from test_framework.test_framework import ComparisonTestFramework +from test_framework.util import * +from test_framework.comptool import TestManager, TestInstance +from test_framework.blocktools import * +import time +from test_framework.key import CECKey +from test_framework.script import * +import struct +from test_framework.cdefs import LEGACY_MAX_BLOCK_SIZE, MAX_STANDARD_TX_SIGOPS + +# Error for too many sigops in one TX +TXNS_TOO_MANY_SIGOPS_ERROR = b'bad-txns-too-many-sigops' +RPC_TXNS_TOO_MANY_SIGOPS_ERROR = "64: " + TXNS_TOO_MANY_SIGOPS_ERROR.decode("utf-8") + +class PreviousSpendableOutput(object): + def __init__(self, tx = CTransaction(), n = -1): + self.tx = tx + self.n = n # the output we're spending + +class FullBlockTest(ComparisonTestFramework): + + # Can either run this test as 1 node with expected answers, or two and compare them. + # Change the "outcome" variable from each TestInstance object to only do the comparison. + def __init__(self): + super().__init__() + self.num_nodes = 1 + self.block_heights = {} + self.coinbase_key = CECKey() + self.coinbase_key.set_secretbytes(b"horsebattery") + self.coinbase_pubkey = self.coinbase_key.get_pubkey() + self.tip = None + self.blocks = {} + + def setup_network(self): + self.extra_args = [['-norelaypriority']] + self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, + self.extra_args, + binary=[self.options.testbinary]) + + def add_options(self, parser): + super().add_options(parser) + parser.add_option("--runbarelyexpensive", dest="runbarelyexpensive", default=True) + + def run_test(self): + self.test = TestManager(self, self.options.tmpdir) + self.test.add_all_connections(self.nodes) + # Start up network handling in another thread + NetworkThread().start() + # Set the blocksize to legacy cap (1MB) as initial condition + self.nodes[0].setexcessiveblock(LEGACY_MAX_BLOCK_SIZE) + self.test.run() + + def add_transactions_to_block(self, block, tx_list): + [ tx.rehash() for tx in tx_list ] + block.vtx.extend(tx_list) + + # this is a little handier to use than the version in blocktools.py + def create_tx(self, spend_tx, n, value, script=CScript([OP_TRUE])): + tx = create_transaction(spend_tx, n, b"", value, script) + return tx + + # sign a transaction, using the key we know about + # this signs input 0 in tx, which is assumed to be spending output n in spend_tx + def sign_tx(self, tx, spend_tx, n): + scriptPubKey = bytearray(spend_tx.vout[n].scriptPubKey) + if (scriptPubKey[0] == OP_TRUE): # an anyone-can-spend + tx.vin[0].scriptSig = CScript() + return + (sighash, err) = SignatureHash(spend_tx.vout[n].scriptPubKey, tx, 0, SIGHASH_ALL) + tx.vin[0].scriptSig = CScript([self.coinbase_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL]))]) + + def create_and_sign_transaction(self, spend_tx, n, value, script=CScript([OP_TRUE])): + tx = self.create_tx(spend_tx, n, value, script) + self.sign_tx(tx, spend_tx, n) + tx.rehash() + return tx + + def next_block(self, number, spend=None, additional_coinbase_value=0, script=CScript([OP_TRUE]), solve=True): + if self.tip == None: + base_block_hash = self.genesis_hash + block_time = int(time.time())+1 + else: + base_block_hash = self.tip.sha256 + block_time = self.tip.nTime + 1 + # First create the coinbase + height = self.block_heights[base_block_hash] + 1 + coinbase = create_coinbase(height, self.coinbase_pubkey) + coinbase.vout[0].nValue += additional_coinbase_value + coinbase.rehash() + if spend == None: + block = create_block(base_block_hash, coinbase, block_time) + else: + coinbase.vout[0].nValue += spend.tx.vout[spend.n].nValue - 1 # all but one satoshi to fees + coinbase.rehash() + block = create_block(base_block_hash, coinbase, block_time) + tx = create_transaction(spend.tx, spend.n, b"", 1, script) # spend 1 satoshi + self.sign_tx(tx, spend.tx, spend.n) + self.add_transactions_to_block(block, [tx]) + block.hashMerkleRoot = block.calc_merkle_root() + if solve: + block.solve() + self.tip = block + self.block_heights[block.sha256] = height + assert number not in self.blocks + self.blocks[number] = block + return block + + def get_tests(self): + self.genesis_hash = int(self.nodes[0].getbestblockhash(), 16) + self.block_heights[self.genesis_hash] = 0 + spendable_outputs = [] + + # save the current tip so it can be spent by a later block + def save_spendable_output(): + spendable_outputs.append(self.tip) + + # get an output that we previously marked as spendable + def get_spendable_output(): + return PreviousSpendableOutput(spendable_outputs.pop(0).vtx[0], 0) + + # returns a test case that asserts that the current tip was accepted + def accepted(): + return TestInstance([[self.tip, True]]) + + # returns a test case that asserts that the current tip was rejected + def rejected(reject = None): + if reject is None: + return TestInstance([[self.tip, False]]) + else: + return TestInstance([[self.tip, reject]]) + + # move the tip back to a previous block + def tip(number): + self.tip = self.blocks[number] + + # adds transactions to the block and updates state + def update_block(block_number, new_transactions): + block = self.blocks[block_number] + self.add_transactions_to_block(block, new_transactions) + old_sha256 = block.sha256 + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + # Update the internal state just like in next_block + self.tip = block + if block.sha256 != old_sha256: + self.block_heights[block.sha256] = self.block_heights[old_sha256] + del self.block_heights[old_sha256] + self.blocks[block_number] = block + return block + + # shorthand for functions + block = self.next_block + create_tx = self.create_tx + + # shorthand for variables + node = self.nodes[0] + + # Create a new block + block(0) + save_spendable_output() + yield accepted() + + # Now we need that block to mature so we can spend the coinbase. + test = TestInstance(sync_every_block=False) + for i in range(99): + block(5000 + i) + test.blocks_and_transactions.append([self.tip, True]) + save_spendable_output() + yield test + + # Collect spendable outputs now to avoid cluttering the code later on + out = [] + for i in range(33): + out.append(get_spendable_output()) + + # P2SH + # Build the redeem script, hash it, use hash to create the p2sh script + redeem_script = CScript([self.coinbase_pubkey] + [OP_2DUP, OP_CHECKSIGVERIFY]*5 + [OP_CHECKSIG]) + redeem_script_hash = hash160(redeem_script) + p2sh_script = CScript([OP_HASH160, redeem_script_hash, OP_EQUAL]) + + # Creates a new transaction using a p2sh transaction as input + def spend_p2sh_tx (p2sh_tx_to_spend, output_script=CScript([OP_TRUE])): + # Create the transaction + spent_p2sh_tx = CTransaction() + spent_p2sh_tx.vin.append(CTxIn(COutPoint(p2sh_tx_to_spend.sha256, 0), b'')) + spent_p2sh_tx.vout.append(CTxOut(1, output_script)) + # Sign the transaction using the redeem script + (sighash, err) = SignatureHash(redeem_script, spent_p2sh_tx, 0, SIGHASH_ALL) + sig = self.coinbase_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL])) + spent_p2sh_tx.vin[0].scriptSig = CScript([sig, redeem_script]) + spent_p2sh_tx.rehash() + return spent_p2sh_tx + + # P2SH tests + # Create a p2sh transaction + p2sh_tx = self.create_and_sign_transaction(out[0].tx, out[0].n, 1, p2sh_script) + + # Add the transaction to the block + block(1) + update_block(1, [p2sh_tx]) + yield accepted() + + # Sigops p2sh limit for the mempool test + p2sh_sigops_limit_mempool = MAX_STANDARD_TX_SIGOPS - redeem_script.GetSigOpCount(True) + # Too many sigops in one p2sh script + too_many_p2sh_sigops_mempool = CScript([OP_CHECKSIG] * (p2sh_sigops_limit_mempool + 1)) + + # A transaction with this output script can't get into the mempool + try: + node.sendrawtransaction(ToHex(spend_p2sh_tx(p2sh_tx, too_many_p2sh_sigops_mempool))) + except JSONRPCException as exp: + assert_equal(exp.error["message"], RPC_TXNS_TOO_MANY_SIGOPS_ERROR) + else: + assert(False) + + # The transaction is rejected, so the mempool should still be empty + assert_equal(set(node.getrawmempool()), set()) + + # Max sigops in one p2sh txn + max_p2sh_sigops_mempool = CScript([OP_CHECKSIG] * (p2sh_sigops_limit_mempool)) + + # A transaction with this output script can get into the mempool + max_p2sh_sigops_txn = spend_p2sh_tx(p2sh_tx, max_p2sh_sigops_mempool) + max_p2sh_sigops_txn_id = node.sendrawtransaction(ToHex(max_p2sh_sigops_txn)) + assert_equal(set(node.getrawmempool()), {max_p2sh_sigops_txn_id}) + + # Mine the transaction + block(2, spend=out[1]) + update_block(2, [max_p2sh_sigops_txn]) + yield accepted() + + # The transaction has been mined, it's not in the mempool anymore + assert_equal(set(node.getrawmempool()), set()) + +if __name__ == '__main__': + FullBlockTest().main() diff --git a/qa/rpc-tests/test_framework/cdefs.py b/qa/rpc-tests/test_framework/cdefs.py --- a/qa/rpc-tests/test_framework/cdefs.py +++ b/qa/rpc-tests/test_framework/cdefs.py @@ -41,6 +41,10 @@ # (network rule) MAX_TX_SIGOPS_COUNT = 20000 +# The maximum number of sigops we're willing to relay/mine in a single tx +# (policy.h constant) +MAX_STANDARD_TX_SIGOPS = MAX_TX_SIGOPS_COUNT // 5 + # Coinbase transaction outputs can only be spent after this number of new # blocks (network rule) COINBASE_MATURITY = 100