diff options
author | Wladimir J. van der Laan <laanwj@gmail.com> | 2018-04-02 16:00:57 +0200 |
---|---|---|
committer | Wladimir J. van der Laan <laanwj@gmail.com> | 2018-04-02 16:02:33 +0200 |
commit | 18815b4bfb20c9c1112e547217662529d81e4393 (patch) | |
tree | 65e21644eea8257d651d10dc8201df67221e3674 /test | |
parent | 821980c00feef143a6ecf6af65926e18cfd30396 (diff) | |
parent | b55555da3e25a47f1e7fced7f09d4f0bf8198624 (diff) |
Merge #11742: rpc: Add testmempoolaccept
b55555d rpc: Add testmempoolaccept (MarcoFalke)
Pull request description:
To check if a single raw transaction makes it into the current transaction pool, one had to call `sendrawtransaction`. However, on success, this adds the transaction to the mempool with no easy way to undo.
The call `testmempoolaccept` is introduced to provide a way to solely check the result without changing the mempool state.
Tree-SHA512: 5afd9311190135cee8fc1f229c7d39bf893f1028f29e28d34f70df820198ff97b4bf86b41cbbd6e6c36a5c30073cefa92d541c74a4939c7a2a6fa283dfd41b63
Diffstat (limited to 'test')
-rwxr-xr-x | test/functional/mempool_accept.py | 293 | ||||
-rwxr-xr-x | test/functional/test_framework/messages.py | 5 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 3 | ||||
-rwxr-xr-x | test/functional/wallet_bumpfee.py | 4 |
4 files changed, 300 insertions, 5 deletions
diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py new file mode 100755 index 0000000000..7cdb24c6a5 --- /dev/null +++ b/test/functional/mempool_accept.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test mempool acceptance of raw transactions.""" + +from io import BytesIO +from test_framework.test_framework import BitcoinTestFramework +from test_framework.messages import ( + BIP125_SEQUENCE_NUMBER, + COIN, + COutPoint, + CTransaction, + CTxOut, + MAX_BLOCK_BASE_SIZE, +) +from test_framework.script import ( + hash160, + CScript, + OP_0, + OP_EQUAL, + OP_HASH160, + OP_RETURN, +) +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + bytes_to_hex_str, + hex_str_to_bytes, + wait_until, +) + + +class MempoolAcceptanceTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [[ + '-checkmempool', + '-txindex', + '-reindex', # Need reindex for txindex + '-acceptnonstdtxn=0', # Try to mimic main-net + ]] * self.num_nodes + + def check_mempool_result(self, result_expected, *args, **kwargs): + """Wrapper to check result of testmempoolaccept on node_0's mempool""" + result_test = self.nodes[0].testmempoolaccept(*args, **kwargs) + assert_equal(result_expected, result_test) + assert_equal(self.nodes[0].getmempoolinfo()['size'], self.mempool_size) # Must not change mempool state + + def run_test(self): + node = self.nodes[0] + + self.log.info('Start with empty mempool, and 200 blocks') + self.mempool_size = 0 + wait_until(lambda: node.getblockcount() == 200) + assert_equal(node.getmempoolinfo()['size'], self.mempool_size) + + 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(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar'])) + + self.log.info('A transaction already in the blockchain') + coin = node.listunspent()[0] # Pick a random coin(base) to spend + raw_tx_in_block = node.signrawtransactionwithwallet(node.createrawtransaction( + inputs=[{'txid': coin['txid'], 'vout': coin['vout']}], + outputs=[{node.getnewaddress(): 0.3}, {node.getnewaddress(): 49}], + ))['hex'] + txid_in_block = node.sendrawtransaction(hexstring=raw_tx_in_block, allowhighfees=True) + node.generate(1) + self.check_mempool_result( + result_expected=[{'txid': txid_in_block, 'allowed': False, 'reject-reason': '18: txn-already-known'}], + rawtxs=[raw_tx_in_block], + ) + + self.log.info('A transaction not in the mempool') + fee = 0.00000700 + raw_tx_0 = node.signrawtransactionwithwallet(node.createrawtransaction( + inputs=[{"txid": txid_in_block, "vout": 0, "sequence": BIP125_SEQUENCE_NUMBER}], # RBF is used later + outputs=[{node.getnewaddress(): 0.3 - fee}], + ))['hex'] + tx = CTransaction() + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + txid_0 = tx.rehash() + self.check_mempool_result( + result_expected=[{'txid': txid_0, 'allowed': True}], + rawtxs=[raw_tx_0], + ) + + self.log.info('A transaction in the mempool') + node.sendrawtransaction(hexstring=raw_tx_0) + self.mempool_size = 1 + self.check_mempool_result( + result_expected=[{'txid': txid_0, 'allowed': False, 'reject-reason': '18: txn-already-in-mempool'}], + rawtxs=[raw_tx_0], + ) + + self.log.info('A transaction that replaces a mempool transaction') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx.vout[0].nValue -= int(fee * COIN) # Double the fee + tx.vin[0].nSequence = BIP125_SEQUENCE_NUMBER + 1 # Now, opt out of RBF + raw_tx_0 = node.signrawtransactionwithwallet(bytes_to_hex_str(tx.serialize()))['hex'] + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + txid_0 = tx.rehash() + self.check_mempool_result( + result_expected=[{'txid': txid_0, 'allowed': True}], + rawtxs=[raw_tx_0], + ) + + self.log.info('A transaction that conflicts with an unconfirmed tx') + # Send the transaction that replaces the mempool transaction and opts out of replaceability + node.sendrawtransaction(hexstring=bytes_to_hex_str(tx.serialize()), allowhighfees=True) + # take original raw_tx_0 + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx.vout[0].nValue -= int(4 * fee * COIN) # Set more fee + # skip re-signing the tx + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '18: txn-mempool-conflict'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + allowhighfees=True, + ) + + self.log.info('A transaction with missing inputs, that never existed') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx.vin[0].prevout = COutPoint(hash=int('ff' * 32, 16), n=14) + # skip re-signing the tx + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'missing-inputs'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A transaction with missing inputs, that existed once in the past') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0))) + tx.vin[0].prevout.n = 1 # Set vout to 1, to spend the other outpoint (49 coins) of the in-chain-tx we want to double spend + raw_tx_1 = node.signrawtransactionwithwallet(bytes_to_hex_str(tx.serialize()))['hex'] + txid_1 = node.sendrawtransaction(hexstring=raw_tx_1, allowhighfees=True) + # Now spend both to "clearly hide" the outputs, ie. remove the coins from the utxo set by spending them + raw_tx_spend_both = node.signrawtransactionwithwallet(node.createrawtransaction( + inputs=[ + {'txid': txid_0, 'vout': 0}, + {'txid': txid_1, 'vout': 0}, + ], + outputs=[{node.getnewaddress(): 0.1}] + ))['hex'] + txid_spend_both = node.sendrawtransaction(hexstring=raw_tx_spend_both, allowhighfees=True) + node.generate(1) + self.mempool_size = 0 + # Now see if we can add the coins back to the utxo set by sending the exact txs again + self.check_mempool_result( + result_expected=[{'txid': txid_0, 'allowed': False, 'reject-reason': 'missing-inputs'}], + rawtxs=[raw_tx_0], + ) + self.check_mempool_result( + result_expected=[{'txid': txid_1, 'allowed': False, 'reject-reason': 'missing-inputs'}], + rawtxs=[raw_tx_1], + ) + + self.log.info('Create a signed "reference" tx for later use') + raw_tx_reference = node.signrawtransactionwithwallet(node.createrawtransaction( + inputs=[{'txid': txid_spend_both, 'vout': 0}], + outputs=[{node.getnewaddress(): 0.05}], + ))['hex'] + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + # Reference tx should be valid on itself + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': True}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A transaction with no outputs') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vout = [] + # Skip re-signing the transaction for context independent checks from now on + # tx.deserialize(BytesIO(hex_str_to_bytes(node.signrawtransactionwithwallet(bytes_to_hex_str(tx.serialize()))['hex']))) + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: bad-txns-vout-empty'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A really large transaction') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vin = [tx.vin[0]] * (MAX_BLOCK_BASE_SIZE // len(tx.vin[0].serialize())) + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: bad-txns-oversize'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A transaction with negative output value') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vout[0].nValue *= -1 + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: bad-txns-vout-negative'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A transaction with too large output value') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vout[0].nValue = 21000000 * COIN + 1 + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: bad-txns-vout-toolarge'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A transaction with too large sum of output values') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vout = [tx.vout[0]] * 2 + tx.vout[0].nValue = 21000000 * COIN + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: bad-txns-txouttotal-toolarge'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A transaction with duplicate inputs') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vin = [tx.vin[0]] * 2 + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: bad-txns-inputs-duplicate'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A coinbase transaction') + # Pick the input of the first tx we signed, so it has to be a coinbase tx + raw_tx_coinbase_spent = node.getrawtransaction(txid=node.decoderawtransaction(hexstring=raw_tx_in_block)['vin'][0]['txid']) + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_coinbase_spent))) + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: coinbase'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('Some nonstandard transactions') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.nVersion = 3 # A version currently non-standard + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: version'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vout[0].scriptPubKey = CScript([OP_0]) # Some non-standard script + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: scriptpubkey'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vin[0].scriptSig = CScript([OP_HASH160]) # Some not-pushonly scriptSig + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: scriptsig-not-pushonly'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + output_p2sh_burn = CTxOut(nValue=540, scriptPubKey=CScript([OP_HASH160, hash160(b'burn'), OP_EQUAL])) + num_scripts = 100000 // len(output_p2sh_burn.serialize()) # Use enough outputs to make the tx too large for our policy + tx.vout = [output_p2sh_burn] * num_scripts + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: tx-size'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vout[0] = output_p2sh_burn + tx.vout[0].nValue -= 1 # Make output smaller, such that it is dust for our policy + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: dust'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'\xff']) + tx.vout = [tx.vout[0]] * 2 + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: multi-op-return'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A timelocked transaction') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vin[0].nSequence -= 1 # Should be non-max, so locktime is not ignored + tx.nLockTime = node.getblockcount() + 1 + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: non-final'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + ) + + self.log.info('A transaction that is locked by BIP68 sequence logic') + tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) + tx.vin[0].nSequence = 2 # We could include it in the second block mined from now, but not the very next one + # Can skip re-signing the tx because of early rejection + self.check_mempool_result( + result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: non-BIP68-final'}], + rawtxs=[bytes_to_hex_str(tx.serialize())], + allowhighfees=True, + ) + + +if __name__ == '__main__': + MempoolAcceptanceTest().main() diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index ee573e01cc..ca2e425bd6 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -34,7 +34,9 @@ MY_RELAY = 1 # from version 70001 onwards, fRelay should be appended to version MAX_INV_SZ = 50000 MAX_BLOCK_BASE_SIZE = 1000000 -COIN = 100000000 # 1 btc in satoshis +COIN = 100000000 # 1 btc in satoshis + +BIP125_SEQUENCE_NUMBER = 0xfffffffd # Sequence number that is BIP 125 opt-in and BIP 68-opt-out NODE_NETWORK = (1 << 0) # NODE_GETUTXO = (1 << 1) @@ -470,6 +472,7 @@ class CTransaction(): def rehash(self): self.sha256 = None self.calc_sha256() + return self.hash # We will only cache the serialization without witness in # self.sha256 and self.hash -- those are expected to be the txid. diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index a2e92dce3b..3cae4b1df3 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -55,7 +55,7 @@ TEST_EXIT_SKIPPED = 77 # 20 minutes represented in seconds TRAVIS_TIMEOUT_DURATION = 20 * 60 -BASE_SCRIPTS= [ +BASE_SCRIPTS = [ # Scripts that are run by the travis build process. # Longest test should go first, to favor running tests in parallel 'wallet_hd.py', @@ -118,6 +118,7 @@ BASE_SCRIPTS= [ 'wallet_importprunedfunds.py', 'rpc_signmessage.py', 'feature_nulldummy.py', + 'mempool_accept.py', 'wallet_import_rescan.py', 'mining_basic.py', 'wallet_bumpfee.py', diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 3e496248fd..5efc846b6e 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -17,14 +17,12 @@ make assumptions about execution order. from test_framework.blocktools import send_to_witness from test_framework.test_framework import BitcoinTestFramework from test_framework import blocktools +from test_framework.messages import BIP125_SEQUENCE_NUMBER from test_framework.mininode import CTransaction from test_framework.util import * import io -# Sequence number that is BIP 125 opt-in and BIP 68-compliant -BIP125_SEQUENCE_NUMBER = 0xfffffffd - WALLET_PASSPHRASE = "test" WALLET_PASSPHRASE_TIMEOUT = 3600 |