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
This commit is contained in:
commit
18815b4bfb
10 changed files with 399 additions and 14 deletions
|
@ -103,6 +103,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||||
{ "signrawtransactionwithkey", 2, "prevtxs" },
|
{ "signrawtransactionwithkey", 2, "prevtxs" },
|
||||||
{ "signrawtransactionwithwallet", 1, "prevtxs" },
|
{ "signrawtransactionwithwallet", 1, "prevtxs" },
|
||||||
{ "sendrawtransaction", 1, "allowhighfees" },
|
{ "sendrawtransaction", 1, "allowhighfees" },
|
||||||
|
{ "testmempoolaccept", 0, "rawtxs" },
|
||||||
|
{ "testmempoolaccept", 1, "allowhighfees" },
|
||||||
{ "combinerawtransaction", 0, "txs" },
|
{ "combinerawtransaction", 0, "txs" },
|
||||||
{ "fundrawtransaction", 1, "options" },
|
{ "fundrawtransaction", 1, "options" },
|
||||||
{ "fundrawtransaction", 2, "iswitness" },
|
{ "fundrawtransaction", 2, "iswitness" },
|
||||||
|
|
|
@ -1134,6 +1134,87 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
|
||||||
return hashTx.GetHex();
|
return hashTx.GetHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UniValue testmempoolaccept(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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObserveSafeMode();
|
||||||
|
|
||||||
|
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& tx_hash = tx->GetHash();
|
||||||
|
|
||||||
|
CAmount max_raw_tx_fee = ::maxTxFee;
|
||||||
|
if (!request.params[1].isNull() && request.params[1].get_bool()) {
|
||||||
|
max_raw_tx_fee = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
UniValue result(UniValue::VARR);
|
||||||
|
UniValue result_0(UniValue::VOBJ);
|
||||||
|
result_0.pushKV("txid", tx_hash.GetHex());
|
||||||
|
|
||||||
|
CValidationState state;
|
||||||
|
bool missing_inputs;
|
||||||
|
bool test_accept_res;
|
||||||
|
{
|
||||||
|
LOCK(cs_main);
|
||||||
|
test_accept_res = AcceptToMemoryPool(mempool, state, std::move(tx), &missing_inputs,
|
||||||
|
nullptr /* plTxnReplaced */, false /* bypass_limits */, max_raw_tx_fee, /* test_accpet */ 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;
|
||||||
|
}
|
||||||
|
|
||||||
static const CRPCCommand commands[] =
|
static const CRPCCommand commands[] =
|
||||||
{ // category name actor (function) argNames
|
{ // category name actor (function) argNames
|
||||||
// --------------------- ------------------------ ----------------------- ----------
|
// --------------------- ------------------------ ----------------------- ----------
|
||||||
|
@ -1145,6 +1226,7 @@ static const CRPCCommand commands[] =
|
||||||
{ "rawtransactions", "combinerawtransaction", &combinerawtransaction, {"txs"} },
|
{ "rawtransactions", "combinerawtransaction", &combinerawtransaction, {"txs"} },
|
||||||
{ "rawtransactions", "signrawtransaction", &signrawtransaction, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */
|
{ "rawtransactions", "signrawtransaction", &signrawtransaction, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */
|
||||||
{ "rawtransactions", "signrawtransactionwithkey", &signrawtransactionwithkey, {"hexstring","privkeys","prevtxs","sighashtype"} },
|
{ "rawtransactions", "signrawtransactionwithkey", &signrawtransactionwithkey, {"hexstring","privkeys","prevtxs","sighashtype"} },
|
||||||
|
{ "rawtransactions", "testmempoolaccept", &testmempoolaccept, {"rawtxs","allowhighfees"} },
|
||||||
|
|
||||||
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
|
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
|
||||||
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
|
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
|
||||||
|
|
|
@ -447,7 +447,7 @@ void CTxMemPool::removeUnchecked(txiter it, MemPoolRemovalReason reason)
|
||||||
// Also assumes that if an entry is in setDescendants already, then all
|
// Also assumes that if an entry is in setDescendants already, then all
|
||||||
// in-mempool descendants of it are already in setDescendants as well, so that we
|
// in-mempool descendants of it are already in setDescendants as well, so that we
|
||||||
// can save time by not iterating over those entries.
|
// can save time by not iterating over those entries.
|
||||||
void CTxMemPool::CalculateDescendants(txiter entryit, setEntries &setDescendants)
|
void CTxMemPool::CalculateDescendants(txiter entryit, setEntries& setDescendants) const
|
||||||
{
|
{
|
||||||
setEntries stage;
|
setEntries stage;
|
||||||
if (setDescendants.count(entryit) == 0) {
|
if (setDescendants.count(entryit) == 0) {
|
||||||
|
|
|
@ -600,7 +600,7 @@ public:
|
||||||
/** Populate setDescendants with all in-mempool descendants of hash.
|
/** Populate setDescendants with all in-mempool descendants of hash.
|
||||||
* Assumes that setDescendants includes all in-mempool descendants of anything
|
* Assumes that setDescendants includes all in-mempool descendants of anything
|
||||||
* already in it. */
|
* already in it. */
|
||||||
void CalculateDescendants(txiter it, setEntries &setDescendants);
|
void CalculateDescendants(txiter it, setEntries& setDescendants) const;
|
||||||
|
|
||||||
/** The minimum fee to get into the mempool, which may itself not be enough
|
/** The minimum fee to get into the mempool, which may itself not be enough
|
||||||
* for larger-sized transactions.
|
* for larger-sized transactions.
|
||||||
|
|
|
@ -543,7 +543,7 @@ static bool CheckInputsFromMempoolAndCache(const CTransaction& tx, CValidationSt
|
||||||
|
|
||||||
static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool& pool, CValidationState& state, const CTransactionRef& ptx,
|
static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool& pool, CValidationState& state, const CTransactionRef& ptx,
|
||||||
bool* pfMissingInputs, int64_t nAcceptTime, std::list<CTransactionRef>* plTxnReplaced,
|
bool* pfMissingInputs, int64_t nAcceptTime, std::list<CTransactionRef>* plTxnReplaced,
|
||||||
bool bypass_limits, const CAmount& nAbsurdFee, std::vector<COutPoint>& coins_to_uncache)
|
bool bypass_limits, const CAmount& nAbsurdFee, std::vector<COutPoint>& coins_to_uncache, bool test_accept)
|
||||||
{
|
{
|
||||||
const CTransaction& tx = *ptx;
|
const CTransaction& tx = *ptx;
|
||||||
const uint256 hash = tx.GetHash();
|
const uint256 hash = tx.GetHash();
|
||||||
|
@ -935,6 +935,11 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (test_accept) {
|
||||||
|
// Tx was accepted, but not added
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove conflicting transactions from the mempool
|
// Remove conflicting transactions from the mempool
|
||||||
for (const CTxMemPool::txiter it : allConflicting)
|
for (const CTxMemPool::txiter it : allConflicting)
|
||||||
{
|
{
|
||||||
|
@ -974,10 +979,10 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool
|
||||||
/** (try to) add transaction to memory pool with a specified acceptance time **/
|
/** (try to) add transaction to memory pool with a specified acceptance time **/
|
||||||
static bool AcceptToMemoryPoolWithTime(const CChainParams& chainparams, CTxMemPool& pool, CValidationState &state, const CTransactionRef &tx,
|
static bool AcceptToMemoryPoolWithTime(const CChainParams& chainparams, CTxMemPool& pool, CValidationState &state, const CTransactionRef &tx,
|
||||||
bool* pfMissingInputs, int64_t nAcceptTime, std::list<CTransactionRef>* plTxnReplaced,
|
bool* pfMissingInputs, int64_t nAcceptTime, std::list<CTransactionRef>* plTxnReplaced,
|
||||||
bool bypass_limits, const CAmount nAbsurdFee)
|
bool bypass_limits, const CAmount nAbsurdFee, bool test_accept)
|
||||||
{
|
{
|
||||||
std::vector<COutPoint> coins_to_uncache;
|
std::vector<COutPoint> coins_to_uncache;
|
||||||
bool res = AcceptToMemoryPoolWorker(chainparams, pool, state, tx, pfMissingInputs, nAcceptTime, plTxnReplaced, bypass_limits, nAbsurdFee, coins_to_uncache);
|
bool res = AcceptToMemoryPoolWorker(chainparams, pool, state, tx, pfMissingInputs, nAcceptTime, plTxnReplaced, bypass_limits, nAbsurdFee, coins_to_uncache, test_accept);
|
||||||
if (!res) {
|
if (!res) {
|
||||||
for (const COutPoint& hashTx : coins_to_uncache)
|
for (const COutPoint& hashTx : coins_to_uncache)
|
||||||
pcoinsTip->Uncache(hashTx);
|
pcoinsTip->Uncache(hashTx);
|
||||||
|
@ -990,10 +995,10 @@ static bool AcceptToMemoryPoolWithTime(const CChainParams& chainparams, CTxMemPo
|
||||||
|
|
||||||
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransactionRef &tx,
|
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransactionRef &tx,
|
||||||
bool* pfMissingInputs, std::list<CTransactionRef>* plTxnReplaced,
|
bool* pfMissingInputs, std::list<CTransactionRef>* plTxnReplaced,
|
||||||
bool bypass_limits, const CAmount nAbsurdFee)
|
bool bypass_limits, const CAmount nAbsurdFee, bool test_accept)
|
||||||
{
|
{
|
||||||
const CChainParams& chainparams = Params();
|
const CChainParams& chainparams = Params();
|
||||||
return AcceptToMemoryPoolWithTime(chainparams, pool, state, tx, pfMissingInputs, GetTime(), plTxnReplaced, bypass_limits, nAbsurdFee);
|
return AcceptToMemoryPoolWithTime(chainparams, pool, state, tx, pfMissingInputs, GetTime(), plTxnReplaced, bypass_limits, nAbsurdFee, test_accept);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4640,7 +4645,8 @@ bool LoadMempool(void)
|
||||||
if (nTime + nExpiryTimeout > nNow) {
|
if (nTime + nExpiryTimeout > nNow) {
|
||||||
LOCK(cs_main);
|
LOCK(cs_main);
|
||||||
AcceptToMemoryPoolWithTime(chainparams, mempool, state, tx, nullptr /* pfMissingInputs */, nTime,
|
AcceptToMemoryPoolWithTime(chainparams, mempool, state, tx, nullptr /* pfMissingInputs */, nTime,
|
||||||
nullptr /* plTxnReplaced */, false /* bypass_limits */, 0 /* nAbsurdFee */);
|
nullptr /* plTxnReplaced */, false /* bypass_limits */, 0 /* nAbsurdFee */,
|
||||||
|
false /* test_accept */);
|
||||||
if (state.IsValid()) {
|
if (state.IsValid()) {
|
||||||
++count;
|
++count;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -308,7 +308,7 @@ void PruneBlockFilesManual(int nManualPruneHeight);
|
||||||
* plTxnReplaced will be appended to with all transactions replaced from mempool **/
|
* plTxnReplaced will be appended to with all transactions replaced from mempool **/
|
||||||
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransactionRef &tx,
|
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransactionRef &tx,
|
||||||
bool* pfMissingInputs, std::list<CTransactionRef>* plTxnReplaced,
|
bool* pfMissingInputs, std::list<CTransactionRef>* plTxnReplaced,
|
||||||
bool bypass_limits, const CAmount nAbsurdFee);
|
bool bypass_limits, const CAmount nAbsurdFee, bool test_accept=false);
|
||||||
|
|
||||||
/** Convert CValidationState to a human-readable message for logging */
|
/** Convert CValidationState to a human-readable message for logging */
|
||||||
std::string FormatStateMessage(const CValidationState &state);
|
std::string FormatStateMessage(const CValidationState &state);
|
||||||
|
|
293
test/functional/mempool_accept.py
Executable file
293
test/functional/mempool_accept.py
Executable file
|
@ -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()
|
|
@ -36,6 +36,8 @@ 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_NETWORK = (1 << 0)
|
||||||
# NODE_GETUTXO = (1 << 1)
|
# NODE_GETUTXO = (1 << 1)
|
||||||
NODE_BLOOM = (1 << 2)
|
NODE_BLOOM = (1 << 2)
|
||||||
|
@ -470,6 +472,7 @@ class CTransaction():
|
||||||
def rehash(self):
|
def rehash(self):
|
||||||
self.sha256 = None
|
self.sha256 = None
|
||||||
self.calc_sha256()
|
self.calc_sha256()
|
||||||
|
return self.hash
|
||||||
|
|
||||||
# We will only cache the serialization without witness in
|
# We will only cache the serialization without witness in
|
||||||
# self.sha256 and self.hash -- those are expected to be the txid.
|
# self.sha256 and self.hash -- those are expected to be the txid.
|
||||||
|
|
|
@ -55,7 +55,7 @@ TEST_EXIT_SKIPPED = 77
|
||||||
# 20 minutes represented in seconds
|
# 20 minutes represented in seconds
|
||||||
TRAVIS_TIMEOUT_DURATION = 20 * 60
|
TRAVIS_TIMEOUT_DURATION = 20 * 60
|
||||||
|
|
||||||
BASE_SCRIPTS= [
|
BASE_SCRIPTS = [
|
||||||
# Scripts that are run by the travis build process.
|
# Scripts that are run by the travis build process.
|
||||||
# Longest test should go first, to favor running tests in parallel
|
# Longest test should go first, to favor running tests in parallel
|
||||||
'wallet_hd.py',
|
'wallet_hd.py',
|
||||||
|
@ -118,6 +118,7 @@ BASE_SCRIPTS= [
|
||||||
'wallet_importprunedfunds.py',
|
'wallet_importprunedfunds.py',
|
||||||
'rpc_signmessage.py',
|
'rpc_signmessage.py',
|
||||||
'feature_nulldummy.py',
|
'feature_nulldummy.py',
|
||||||
|
'mempool_accept.py',
|
||||||
'wallet_import_rescan.py',
|
'wallet_import_rescan.py',
|
||||||
'mining_basic.py',
|
'mining_basic.py',
|
||||||
'wallet_bumpfee.py',
|
'wallet_bumpfee.py',
|
||||||
|
|
|
@ -17,14 +17,12 @@ make assumptions about execution order.
|
||||||
from test_framework.blocktools import send_to_witness
|
from test_framework.blocktools import send_to_witness
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
from test_framework import blocktools
|
from test_framework import blocktools
|
||||||
|
from test_framework.messages import BIP125_SEQUENCE_NUMBER
|
||||||
from test_framework.mininode import CTransaction
|
from test_framework.mininode import CTransaction
|
||||||
from test_framework.util import *
|
from test_framework.util import *
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
# Sequence number that is BIP 125 opt-in and BIP 68-compliant
|
|
||||||
BIP125_SEQUENCE_NUMBER = 0xfffffffd
|
|
||||||
|
|
||||||
WALLET_PASSPHRASE = "test"
|
WALLET_PASSPHRASE = "test"
|
||||||
WALLET_PASSPHRASE_TIMEOUT = 3600
|
WALLET_PASSPHRASE_TIMEOUT = 3600
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue