diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -95,6 +95,8 @@ {"signrawtransactionwithkey", 2, "prevtxs"}, {"signrawtransactionwithwallet", 1, "prevtxs"}, {"sendrawtransaction", 1, "allowhighfees"}, + {"testmempoolaccept", 0, "rawtxs"}, + {"testmempoolaccept", 1, "allowhighfees"}, {"combinerawtransaction", 0, "txs"}, {"fundrawtransaction", 1, "options"}, {"gettxout", 1, "n"}, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1343,6 +1343,93 @@ return txid.GetHex(); } +UniValue testmempoolaccept(const Config &config, + const JSONRPCRequest &request) { + if (request.fHelp || request.params.size() < 1 || + request.params.size() > 2) { + throw std::runtime_error( + // clang-format off + "testmempoolaccept [\"rawtxs\"] ( allowhighfees )\n" + "\nReturns if raw transaction (serialized, hex-encoded) would be accepted by mempool.\n" + "\nThis checks if the transaction violates the consensus or policy rules.\n" + "\nSee sendrawtransaction call.\n" + "\nArguments:\n" + "1. [\"rawtxs\"] (array, required) An array of hex strings of raw transactions.\n" + " Length must be one for now.\n" + "2. allowhighfees (boolean, optional, default=false) Allow high fees\n" + "\nResult:\n" + "[ (array) The result of the mempool acceptance test for each raw transaction in the input array.\n" + " Length is exactly one for now.\n" + " {\n" + " \"txid\" (string) The transaction hash in hex\n" + " \"allowed\" (boolean) If the mempool allows this tx to be inserted\n" + " \"reject-reason\" (string) Rejection string (only present when 'allowed' is false)\n" + " }\n" + "]\n" + "\nExamples:\n" + "\nCreate a transaction\n" + + HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\" : \\\"mytxid\\\",\\\"vout\\\":0}]\" \"{\\\"myaddress\\\":0.01}\"") + + "Sign the transaction, and get back the hex\n" + + HelpExampleCli("signrawtransaction", "\"myhex\"") + + "\nTest acceptance of the transaction (signed hex)\n" + + HelpExampleCli("testmempoolaccept", "\"signedhex\"") + + "\nAs a json rpc call\n" + + HelpExampleRpc("testmempoolaccept", "[\"signedhex\"]") + // clang-format on + ); + } + + RPCTypeCheck(request.params, {UniValue::VARR, UniValue::VBOOL}); + if (request.params[0].get_array().size() != 1) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Array must contain exactly one raw transaction for now"); + } + + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, request.params[0].get_array()[0].get_str())) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + } + CTransactionRef tx(MakeTransactionRef(std::move(mtx))); + const uint256 &txid = tx->GetId(); + + bool fLimitFree = false; + Amount max_raw_tx_fee = maxTxFee; + if (!request.params[1].isNull() && request.params[1].get_bool()) { + max_raw_tx_fee = Amount::zero(); + } + + UniValue result(UniValue::VARR); + UniValue result_0(UniValue::VOBJ); + result_0.pushKV("txid", txid.GetHex()); + + CValidationState state; + bool missing_inputs; + bool test_accept_res; + { + LOCK(cs_main); + test_accept_res = AcceptToMemoryPool( + config, g_mempool, state, std::move(tx), fLimitFree, + &missing_inputs, /* bypass_limits */ false, max_raw_tx_fee, + /* test_accept */ true); + } + result_0.pushKV("allowed", test_accept_res); + if (!test_accept_res) { + if (state.IsInvalid()) { + result_0.pushKV("reject-reason", + strprintf("%i: %s", state.GetRejectCode(), + state.GetRejectReason())); + } else if (missing_inputs) { + result_0.pushKV("reject-reason", "missing-inputs"); + } else { + result_0.pushKV("reject-reason", state.GetRejectReason()); + } + } + + result.push_back(std::move(result_0)); + return result; +} + // clang-format off static const ContextFreeRPCCommand commands[] = { // category name actor (function) argNames @@ -1355,6 +1442,7 @@ { "rawtransactions", "combinerawtransaction", combinerawtransaction, {"txs"} }, { "rawtransactions", "signrawtransaction", signrawtransaction, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */ { "rawtransactions", "signrawtransactionwithkey", signrawtransactionwithkey, {"hexstring","privkeys","prevtxs","sighashtype"} }, + { "rawtransactions", "testmempoolaccept", testmempoolaccept, {"rawtxs","allowhighfees"} }, { "blockchain", "gettxoutproof", gettxoutproof, {"txids", "blockhash"} }, { "blockchain", "verifytxoutproof", verifytxoutproof, {"proof"} }, diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -459,7 +459,8 @@ CValidationState &state, const CTransactionRef &tx, bool fLimitFree, bool *pfMissingInputs, bool fOverrideMempoolLimit = false, - const Amount nAbsurdFee = Amount::zero()); + const Amount nAbsurdFee = Amount::zero(), + bool test_accept = false); /** Convert CValidationState to a human-readable message for logging */ std::string FormatStateMessage(const CValidationState &state); diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -467,7 +467,7 @@ const Config &config, CTxMemPool &pool, CValidationState &state, const CTransactionRef &ptx, bool fLimitFree, bool *pfMissingInputs, int64_t nAcceptTime, bool fOverrideMempoolLimit, const Amount nAbsurdFee, - std::vector &coins_to_uncache) { + std::vector &coins_to_uncache, bool test_accept) { AssertLockHeld(cs_main); const CTransaction &tx = *ptx; @@ -791,6 +791,11 @@ "otherwise cause instability!\n"); } + if (test_accept) { + // Tx was accepted, but not added + return true; + } + // Store transaction in memory. pool.addUnchecked(txid, entry, setAncestors); @@ -818,11 +823,11 @@ const Config &config, CTxMemPool &pool, CValidationState &state, const CTransactionRef &tx, bool fLimitFree, bool *pfMissingInputs, int64_t nAcceptTime, bool fOverrideMempoolLimit = false, - const Amount nAbsurdFee = Amount::zero()) { + const Amount nAbsurdFee = Amount::zero(), bool test_accept = false) { std::vector coins_to_uncache; bool res = AcceptToMemoryPoolWorker( config, pool, state, tx, fLimitFree, pfMissingInputs, nAcceptTime, - fOverrideMempoolLimit, nAbsurdFee, coins_to_uncache); + fOverrideMempoolLimit, nAbsurdFee, coins_to_uncache, test_accept); if (!res) { for (const COutPoint &outpoint : coins_to_uncache) { pcoinsTip->Uncache(outpoint); @@ -840,10 +845,11 @@ bool AcceptToMemoryPool(const Config &config, CTxMemPool &pool, CValidationState &state, const CTransactionRef &tx, bool fLimitFree, bool *pfMissingInputs, - bool fOverrideMempoolLimit, const Amount nAbsurdFee) { - return AcceptToMemoryPoolWithTime(config, pool, state, tx, fLimitFree, - pfMissingInputs, GetTime(), - fOverrideMempoolLimit, nAbsurdFee); + bool fOverrideMempoolLimit, const Amount nAbsurdFee, + bool test_accept) { + return AcceptToMemoryPoolWithTime( + config, pool, state, tx, fLimitFree, pfMissingInputs, GetTime(), + fOverrideMempoolLimit, nAbsurdFee, test_accept); } /** diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py new file mode 100755 --- /dev/null +++ b/test/functional/mempool_accept.py @@ -0,0 +1,325 @@ +#!/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 ( + 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) + # Must not change mempool state + assert_equal(self.nodes[0].getmempoolinfo()['size'], self.mempool_size) + + 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": 0xfffffffd}], + 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], + ) + + # Removed RBF test + # self.log.info('A transaction that replaces a mempool transaction') + # ... + + 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))) + # Set vout to 1, to spend the other outpoint (49 coins) of the in-chain-tx we want to double spend + tx.vin[0].prevout.n = 1 + 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]] * (1 + 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: bad-tx-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))) + # Some not-pushonly scriptSig + tx.vin[0].scriptSig = CScript([OP_HASH160]) + 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])) + # Use enough outputs to make the tx too large for our policy + num_scripts = 100000 // len(output_p2sh_burn.serialize()) + 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 + # Make output smaller, such that it is dust for our policy + tx.vout[0].nValue -= 1 + 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))) + # Should be non-max, so locktime is not ignored + tx.vin[0].nSequence -= 1 + tx.nLockTime = node.getblockcount() + 1 + self.check_mempool_result( + result_expected=[ + {'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: bad-txns-nonfinal'}], + 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))) + # We could include it in the second block mined from now, but not the very next one + tx.vin[0].nSequence = 2 + # 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 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -401,6 +401,7 @@ def rehash(self): self.sha256 = None self.calc_sha256() + return self.hash # self.sha256 and self.hash -- those are expected to be the txid. def calc_sha256(self):