From a7b52f819182b4693c21bc520b17405f3252df14 Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Fri, 1 Dec 2023 23:02:29 +0000 Subject: [PATCH] Add the RPC `fillmempool` (regtest only) --- src/rpc/blockchain.cpp | 192 ++++++++++++++++++++++++ src/rpc/client.cpp | 1 + src/test/mempool_tests.cpp | 16 ++ src/txmempool.h | 4 + test/functional/bchn-rpc-fillmempool.py | 50 ++++++ test/lint/lint-circular-dependencies.sh | 1 + 6 files changed, 264 insertions(+) create mode 100644 test/functional/bchn-rpc-fillmempool.py diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 8721ad4c5c..a0126272ad 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -2692,6 +2695,194 @@ static UniValue scantxoutset(const Config &config, throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid command"); } +static UniValue fillmempool(const Config &config, const JSONRPCRequest &request) { + if (request.fHelp || request.params.size() != 1) { + throw std::runtime_error( + RPCHelpMan{"fillmempool", + "\nFills the mempool with the specified number of megabytes worth of anyone-can-spend txns.\n", + { + {"megabytes", RPCArg::Type::NUM, /* opt */ false, /* default_val */ "", + "The number of megabytes worth of txns to fill the mempool with.", "", {"", "numeric"}}, + }} + .ToString() + + "\nExamples:\n" + + HelpExampleCli("fillmempool","10") + + HelpExampleRpc("fillmempool","320") + ); + } + + // Ensure we are on regtest + const auto &consensusParams = config.GetChainParams().GetConsensus(); + if ( ! consensusParams.fPowNoRetargeting) { + throw JSONRPCError(RPC_METHOD_DISABLED, + "fillmempool is not supported on this chain. Switch to regtest to use fillmempool."); + } + + // Check not already running in another thread + static std::mutex one_at_a_time_mut; + std::unique_lock one_at_a_time_guard(one_at_a_time_mut, std::try_to_lock); + if ( ! one_at_a_time_guard.owns_lock()) { + throw JSONRPCError(RPC_INVALID_REQUEST, "fillmempool is already running in another RPC thread"); + } + + // Temporarily disable the regtest mempool sanity checking since it will slow the below operation down + const auto orig_check_freq = g_mempool.getSanityCheck(); + Defer restore_sanity_check([&orig_check_freq]{ + // restore the original setting on scope end + g_mempool.setSanityCheck(orig_check_freq); + }); + g_mempool.setSanityCheck(0.0); + + Tic t0; + const size_t target_size = ONE_MEGABYTE * [&request]{ + if (const int arg = request.params[0].get_int(); arg <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "megabytes argument must be greater than 0"); + } else { + return arg; + } + }(); + if (target_size > config.GetMaxMemPoolSize()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Max mempool size is %i which is less than the requested %i", + config.GetMaxMemPoolSize(), target_size)); + } + const auto redeem_script = CScript() << OP_DROP << OP_TRUE; + const CTxDestination destination(ScriptID{redeem_script, /* is32 = */ false}); + const auto destination_spk = GetScriptForDestination(destination); + using UTXO = std::pair; + using UTXOList = std::list; + UTXOList utxos; + + // Mine over 100 blocks to get `nCB` valid coinbases we can spend using our "anyone can spend" p2sh + { + const auto reward = GetBlockSubsidy(WITH_LOCK(cs_main, return ::ChainActive().Height() + 1), consensusParams); + assert( reward > Amount::zero()); + const size_t nCB = std::max(1, (50 * COIN) / reward); // scale nCB to block reward size + auto reserve_script = std::make_shared(); + reserve_script->reserveScript = destination_spk; + const auto nBlocks = COINBASE_MATURITY + nCB; + LogPrint(BCLog::MEMPOOL, "fillmempool: Generating %i blocks, of which %i coinbases will be used ...\n", + nBlocks, nCB); + const auto blockhashes = generateBlocks(config, reserve_script, nBlocks, ~uint64_t{}, false); + for (size_t i = 0; i < nCB; ++i) { + const BlockHash bh{ParseHashV(blockhashes.at(i), "blockhash")}; + LOCK(cs_main); + const CBlockIndex *pindex = LookupBlockIndex(bh); + CBlock block; + if (!pindex || !::ChainActive().Contains(pindex) || !ReadBlockFromDisk(block, pindex, consensusParams)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to find mined block #%i", i)); + } + const auto &ptx = block.vtx.at(0); + const auto &txid = ptx->GetId(); + const auto &out = ptx->vout.at(0); + utxos.emplace_back(COutPoint{txid, 0}, out.nValue); + } + } + + const size_t op_return_size = std::max(3u, ::nMaxDatacarrierBytes) - 3; + const CTxOut op_return(Amount::zero(), CScript() << OP_RETURN << std::vector(op_return_size)); + + CFeeRate last_fee_rate; + size_t max_size_seen = 0u, min_size_seen = 0xffffffffu; + + auto SpendToMempool = [&] + (const size_t tx_num, const UTXO &txoIn, const size_t fanoutSize) -> UTXOList { + UTXOList ret; + assert(fanoutSize > 0); + CMutableTransaction tx; + const CScript script_sig = CScript() << std::vector(GetRandInt(MAX_SCRIPT_ELEMENT_SIZE)) // pad txn + << std::vector(redeem_script.begin(), redeem_script.end()); + tx.vin.emplace_back(txoIn.first, script_sig); + const auto &amt_in = txoIn.second; + while (tx.vout.size() < fanoutSize) { + tx.vout.emplace_back(int64_t((amt_in / SATOSHI) / fanoutSize) * SATOSHI, destination_spk); + } + // Now, add a full OP_RETURN to pad the txn + const size_t n_op_returns = 1; + tx.vout.push_back(op_return); + + tx.SortBip69(); + + auto IsUnspendable = [](const CTxOut &out) { + return out.nValue == Amount::zero() || out.scriptPubKey.IsUnspendable(); + }; + + // Adjust for fees + const auto tx_size = ::GetSerializeSize(tx, PROTOCOL_VERSION); + const auto mp_max_size = config.GetMaxMemPoolSize(); + const auto fee_rate = std::max(WITH_LOCK(cs_main, return ::minRelayTxFee), g_mempool.GetMinFee(mp_max_size)); + const auto fee = fee_rate.GetFee(tx_size) / SATOSHI; + const Amount fee_per_output = int64_t(std::ceil(fee / double(tx.vout.size() - n_op_returns))) * SATOSHI; + for (auto &out : tx.vout) { + if (IsUnspendable(out)) { + // skip op_return + continue; + } + out.nValue -= fee_per_output; + if (!MoneyRange(out.nValue) || IsDust(out, ::dustRelayFee)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Bad amount for txout: %s", out.nValue.ToString())); + } + } + + // Submit the txn + const CTransactionRef rtx = MakeTransactionRef(tx); + const Amount tx_fee = amt_in - rtx->GetValueOut(); + if (0 == tx_num % 1000 || last_fee_rate != fee_rate || tx_size > max_size_seen || tx_size < min_size_seen) { + // log what's happening every 1000th time, or if the fee rate changes, or if we hit a new hi/low tx size + last_fee_rate = fee_rate; + max_size_seen = std::max(tx_size, max_size_seen); + min_size_seen = std::min(tx_size, min_size_seen); + LogPrint(BCLog::MEMPOOL, "fillmempool: tx_num: %i, size: %i, fee: %i, fee_rate: %s\n", + tx_num, tx_size, tx_fee / SATOSHI, fee_rate.ToString()); + } + const auto &txId = rtx->GetId(); + unsigned outN = 0; + { + LOCK(cs_main); + CValidationState vstate; + bool missingInputs{}; + const bool ok = AcceptToMemoryPool(config, g_mempool, vstate, rtx, &missingInputs, false, Amount::zero()); + if (!ok || !vstate.IsValid()) { + throw JSONRPCError(RPC_INTERNAL_ERROR, + strprintf("Unable to accept txn to mempool: %s", + missingInputs ? "missing inputs" : vstate.GetRejectReason())); + } + } + + // Remember utxos + for (const auto &out : rtx->vout) { + if ( ! IsUnspendable(out)) { + ret.emplace_back(COutPoint{txId, outN}, out.nValue); + } + ++outN; + } + return ret; + }; + + // Generate txns to fill the mempool to the required size. + // Note that this is a bit fuzzy in that it may be +/- by as + // much as ~1.5KB dynamic size (or +/- ~500 B serialized size). + size_t ngen = 0, mp_dynusage = 0; + while ((mp_dynusage = g_mempool.DynamicMemoryUsage()) + 500 < target_size) { + assert(!utxos.empty()); + const UTXO utxo = utxos.front(); + utxos.pop_front(); + auto new_utxos = SpendToMempool(ngen + 1, utxo, 2); + utxos.splice(utxos.end(), std::move(new_utxos)); + ++ngen; + } + + UniValue::Object ret; + ret.reserve(7); + ret.emplace_back("txns_generated", ngen); + ret.emplace_back("mempool_txns", g_mempool.size()); + ret.emplace_back("mempool_bytes", g_mempool.GetTotalTxSize()); + ret.emplace_back("mempool_dynamic_usage", mp_dynusage); + ret.emplace_back("elapsed_msec", t0.msec()); + ret.emplace_back("address", EncodeDestination(destination, config)); + ret.emplace_back("redeemscript_hex", HexStr(redeem_script)); + return ret; +} + // clang-format off static const ContextFreeRPCCommand commands[] = { // category name actor (function) argNames @@ -2726,6 +2917,7 @@ static const ContextFreeRPCCommand commands[] = { { "blockchain", "verifychain", verifychain, {"checklevel","nblocks"} }, /* Not shown in help */ + { "hidden", "fillmempool", fillmempool, {"megabytes"} }, { "hidden", "syncwithvalidationinterfacequeue", syncwithvalidationinterfacequeue, {} }, { "hidden", "waitforblock", waitforblock, {"blockhash","timeout"} }, { "hidden", "waitforblockheight", waitforblockheight, {"height","timeout"} }, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 5a6fbe29b9..3486820240 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -172,6 +172,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { {"getnodeaddresses", 0, "count"}, { "addpeeraddress", 1, "port"}, {"stop", 0, "wait"}, + {"fillmempool", 0, "megabytes"}, }; class CRPCConvertTable { diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp index 9e1747b3b3..f77a93f866 100644 --- a/src/test/mempool_tests.cpp +++ b/src/test/mempool_tests.cpp @@ -824,4 +824,20 @@ BOOST_AUTO_TEST_CASE(CompareTxMemPoolEntryByModifiedFeeRateTest) { BOOST_CHECK(After(entryA, entryB)); } +BOOST_AUTO_TEST_CASE(SanityCheckGetterAndSetter) { + // Basic unit test that ensures the [gs]etSanityCheck() getter/setter behave as expected + CTxMemPool pool; + + const double increment = 65535.0/4294967295.0; // use this value to match resolution of CTxMemPool::nCheckFrequency + for (double d = 0.0; d <= 1.0; d += increment) { + pool.setSanityCheck(d); + // since comparing doubles is problematic, use 0.001 resolution for the equality check + BOOST_CHECK_EQUAL(int(d * 1000.0), int(pool.getSanityCheck() * 1000.0)); + } + + // check saturated value + pool.setSanityCheck(1.0); + BOOST_CHECK_EQUAL(int(pool.getSanityCheck() * 1000.0), 1000); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/txmempool.h b/src/txmempool.h index b75d928e8e..eccefd27ab 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -520,6 +520,10 @@ class CTxMemPool { LOCK(cs); nCheckFrequency = static_cast(dFrequency * 4294967295.0); } + double getSanityCheck() const { + LOCK(cs); + return static_cast(nCheckFrequency) / 4294967295.0; + } // addUnchecked must update state for all parents of a given transaction, // updating child links as necessary. diff --git a/test/functional/bchn-rpc-fillmempool.py b/test/functional/bchn-rpc-fillmempool.py new file mode 100644 index 0000000000..7031f6bde2 --- /dev/null +++ b/test/functional/bchn-rpc-fillmempool.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Tests the `fillmempool` RPC.""" + +from test_framework.cdefs import DEFAULT_EXCESSIVE_BLOCK_SIZE, ONE_MEGABYTE +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_greater_than_or_equal, + assert_raises_rpc_error, +) + +DEFAULT_MAX_MEMPOOL_SIZE_PER_MB = 10 +MAX_MEMPOOL_MB = DEFAULT_MAX_MEMPOOL_SIZE_PER_MB * DEFAULT_EXCESSIVE_BLOCK_SIZE // ONE_MEGABYTE + + +class FillMempoolTest(BitcoinTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.base_extra_args = ['-percentblockmaxsize=100'] + self.extra_args = [self.base_extra_args] * self.num_nodes + # We need a long timeout for this test because the sanitizer-undefined CI job is very slow for `fillmempool` + self.rpc_timeout = 600 + + def run_test(self): + node = self.nodes[0] + mpi = node.getmempoolinfo() + assert_equal(mpi['size'], 0) + + # Check error conditions + assert_raises_rpc_error(-8, "megabytes argument must be greater than 0", node.fillmempool, 0) + assert_raises_rpc_error(-8, "megabytes argument must be greater than 0", node.fillmempool, -1) + assert_raises_rpc_error(-8, "Max mempool size is", node.fillmempool, MAX_MEMPOOL_MB + 1) + + assert MAX_MEMPOOL_MB > 100 # Required by below loop + + fuzziness = 500 # fuzziness about how well fillmempool can meet the requirement -> +/- 500 bytes + for size_mb in [1, 10, 64, 100]: + res = node.fillmempool(size_mb) + assert_greater_than_or_equal(res['mempool_dynamic_usage'] + fuzziness, size_mb * ONE_MEGABYTE) + mpi = node.getmempoolinfo() + assert_greater_than_or_equal(mpi['usage'] + fuzziness, size_mb * ONE_MEGABYTE) + + +if __name__ == '__main__': + FillMempoolTest().main() diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh index 2a20949e4f..c8eb516bf1 100755 --- a/test/lint/lint-circular-dependencies.sh +++ b/test/lint/lint-circular-dependencies.sh @@ -42,6 +42,7 @@ EXPECTED_CIRCULAR_DEPENDENCIES=( "config -> policy/policy -> validation -> config" "config -> policy/policy -> validation -> protocol -> config" "psbt -> script/script_execution_context -> psbt" + "rpc/blockchain -> rpc/mining -> rpc/blockchain" ) EXIT_CODE=0