bitcoin/src/test/fuzz/package_eval.cpp
Andrew Chow a97a89244e
Merge bitcoin/bitcoin#28368: Fee Estimator updates from Validation Interface/CScheduler thread
91504cbe0de2b74ef1aa2709761aaf0597ec66a2 rpc: `SyncWithValidationInterfaceQueue` on fee estimation RPC's (ismaelsadeeq)
714523918ba2b853fc69bee6b04a33ba0c828bf5 tx fees, policy: CBlockPolicyEstimator update from `CValidationInterface` notifications (ismaelsadeeq)
dff5ad3b9944cbb56126ba37a8da180d1327ba39 CValidationInterface: modify the parameter of `TransactionAddedToMempool` (ismaelsadeeq)
91532bd38223d7d04166e05de11d0d0b55e60f13 tx fees, policy: update `CBlockPolicyEstimator::processBlock` parameter (ismaelsadeeq)
bfcd401368fc0dc43827a8969a37b7e038d5ca79 CValidationInterface, mempool: add new callback to `CValidationInterface` (ismaelsadeeq)
0889e07987294d4ef2814abfca16d8e2a0c5f541 tx fees, policy: cast with static_cast instead of C-Style cast (ismaelsadeeq)
a0e3eb7549d2ba4dd3af12b9ce65e29158f59078 tx fees, policy: bugfix: move `removeTx` into reason != `BLOCK` condition (ismaelsadeeq)

Pull request description:

  This is an attempt to  #11775

  This Pr will enable fee estimator to listen to ValidationInterface notifications to process new transactions added and removed from the mempool.

  This PR includes the following changes:

  - Added a new callback to the Validation Interface `MempoolTransactionsRemovedForConnectedBlock`, which notifies listeners about the transactions that have been removed due to a new block being connected, along with the height at which the transactions were removed.
  - Modified the `TransactionAddedToMempool` callback parameter to include additional information about the transaction needed for fee estimation.
  - Updated `CBlockPolicyEstimator` to process transactions using` CTransactionRef` instead of `CTxMempoolEntry.`
  - Implemented the `CValidationInterface` interface in `CBlockPolicyEstimater` and overridden the `TransactionAddedToMempool`, `TransactionRemovedFromMempool`, and `MempoolTransactionsRemovedForConnectedBlock` methods to receive updates from their notifications.

  Prior to this PR, the fee estimator updates from the mempool, i.e whenever a new block is connected all transactions in the block that are in our mempool are going to be removed using the `removeForBlock` function in `txmempool.cpp`.

  This removal triggered updates to the fee estimator. As a result, the fee estimator would block mempool's `cs` until it finished updating every time a new block was connected.
  Instead of being blocked only on mempool tx removal, we were blocking on both tx removal and fee estimator updating.
  If we want to further improve fee estimation, or add heavy-calulation steps to it, it is currently not viable as we would be slowing down block relay in the process

  This PR is smaller in terms of the changes made compared to #11775, as it focuses solely on enabling fee estimator updates from the validationInterface/cscheduler thread notifications.

  I have not split the validation interface because, as I understand it, the rationale behind the split in #11775 was to have `MempoolInterface` signals come from the mempool and `CValidationInterface` events come from validation. I believe this separation can be achieved in a separate refactoring PR when the need arises.

  Also left out some commits from #11775
  - Some refactoring which are no longer needed.
  - Handle reorgs much better in fee estimator.
  - Track witness hash malleation in fee estimator

  I believe they are a separate change that can come in a follow-up after this.

ACKs for top commit:
  achow101:
    ACK 91504cbe0de2b74ef1aa2709761aaf0597ec66a2
  TheCharlatan:
    Re-ACK 91504cbe0de2b74ef1aa2709761aaf0597ec66a2
  willcl-ark:
    ACK 91504cbe0de2b74ef1aa2709761aaf0597ec66a2

Tree-SHA512: 846dfb9da57a8a42458827b8975722d153907fe6302ad65748d74f311e1925557ad951c3d95fe71fb90ddcc8a3710c45abb343ab86b88780871cb9c38c72c7b1
2023-12-01 15:07:23 -05:00

305 lines
13 KiB
C++

// Copyright (c) 2023 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <consensus/validation.h>
#include <node/context.h>
#include <node/mempool_args.h>
#include <node/miner.h>
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h>
#include <test/fuzz/util/mempool.h>
#include <test/util/mining.h>
#include <test/util/script.h>
#include <test/util/setup_common.h>
#include <test/util/txmempool.h>
#include <util/rbf.h>
#include <validation.h>
#include <validationinterface.h>
using node::NodeContext;
namespace {
const TestingSetup* g_setup;
std::vector<COutPoint> g_outpoints_coinbase_init_mature;
struct MockedTxPool : public CTxMemPool {
void RollingFeeUpdate() EXCLUSIVE_LOCKS_REQUIRED(!cs)
{
LOCK(cs);
lastRollingFeeUpdate = GetTime();
blockSinceLastRollingFeeBump = true;
}
};
void initialize_tx_pool()
{
static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
g_setup = testing_setup.get();
for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) {
COutPoint prevout{MineBlock(g_setup->m_node, P2WSH_EMPTY)};
if (i < COINBASE_MATURITY) {
// Remember the txids to avoid expensive disk access later on
g_outpoints_coinbase_init_mature.push_back(prevout);
}
}
SyncWithValidationInterfaceQueue();
}
struct OutpointsUpdater final : public CValidationInterface {
std::set<COutPoint>& m_mempool_outpoints;
explicit OutpointsUpdater(std::set<COutPoint>& r)
: m_mempool_outpoints{r} {}
void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /* mempool_sequence */) override
{
// for coins spent we always want to be able to rbf so they're not removed
// outputs from this tx can now be spent
for (uint32_t index{0}; index < tx.info.m_tx->vout.size(); ++index) {
m_mempool_outpoints.insert(COutPoint{tx.info.m_tx->GetHash(), index});
}
}
void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t /* mempool_sequence */) override
{
// outpoints spent by this tx are now available
for (const auto& input : tx->vin) {
// Could already exist if this was a replacement
m_mempool_outpoints.insert(input.prevout);
}
// outpoints created by this tx no longer exist
for (uint32_t index{0}; index < tx->vout.size(); ++index) {
m_mempool_outpoints.erase(COutPoint{tx->GetHash(), index});
}
}
};
struct TransactionsDelta final : public CValidationInterface {
std::set<CTransactionRef>& m_added;
explicit TransactionsDelta(std::set<CTransactionRef>& a)
: m_added{a} {}
void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /* mempool_sequence */) override
{
// Transactions may be entered and booted any number of times
m_added.insert(tx.info.m_tx);
}
void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t /* mempool_sequence */) override
{
// Transactions may be entered and booted any number of times
m_added.erase(tx);
}
};
void MockTime(FuzzedDataProvider& fuzzed_data_provider, const Chainstate& chainstate)
{
const auto time = ConsumeTime(fuzzed_data_provider,
chainstate.m_chain.Tip()->GetMedianTimePast() + 1,
std::numeric_limits<decltype(chainstate.m_chain.Tip()->nTime)>::max());
SetMockTime(time);
}
CTxMemPool MakeMempool(FuzzedDataProvider& fuzzed_data_provider, const NodeContext& node)
{
// Take the default options for tests...
CTxMemPool::Options mempool_opts{MemPoolOptionsForTest(node)};
// ...override specific options for this specific fuzz suite
mempool_opts.limits.ancestor_count = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 50);
mempool_opts.limits.ancestor_size_vbytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 202) * 1'000;
mempool_opts.limits.descendant_count = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 50);
mempool_opts.limits.descendant_size_vbytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 202) * 1'000;
mempool_opts.max_size_bytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 200) * 1'000'000;
mempool_opts.expiry = std::chrono::hours{fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 999)};
nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(1, 999);
mempool_opts.check_ratio = 1;
mempool_opts.require_standard = fuzzed_data_provider.ConsumeBool();
// ...and construct a CTxMemPool from it
return CTxMemPool{mempool_opts};
}
FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
{
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
const auto& node = g_setup->m_node;
auto& chainstate{static_cast<DummyChainState&>(node.chainman->ActiveChainstate())};
MockTime(fuzzed_data_provider, chainstate);
// All RBF-spendable outpoints outside of the unsubmitted package
std::set<COutPoint> mempool_outpoints;
std::map<COutPoint, CAmount> outpoints_value;
for (const auto& outpoint : g_outpoints_coinbase_init_mature) {
Assert(mempool_outpoints.insert(outpoint).second);
outpoints_value[outpoint] = 50 * COIN;
}
auto outpoints_updater = std::make_shared<OutpointsUpdater>(mempool_outpoints);
RegisterSharedValidationInterface(outpoints_updater);
CTxMemPool tx_pool_{MakeMempool(fuzzed_data_provider, node)};
MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(&tx_pool_);
chainstate.SetMempool(&tx_pool);
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300)
{
Assert(!mempool_outpoints.empty());
std::vector<CTransactionRef> txs;
// Make packages of 1-to-26 transactions
const auto num_txs = (size_t) fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 26);
std::set<COutPoint> package_outpoints;
while (txs.size() < num_txs) {
// Last transaction in a package needs to be a child of parents to get further in validation
// so the last transaction to be generated(in a >1 package) must spend all package-made outputs
// Note that this test currently only spends package outputs in last transaction.
bool last_tx = num_txs > 1 && txs.size() == num_txs - 1;
// Create transaction to add to the mempool
const CTransactionRef tx = [&] {
CMutableTransaction tx_mut;
tx_mut.nVersion = CTransaction::CURRENT_VERSION;
tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
// Last tx will sweep all outpoints in package
const auto num_in = last_tx ? package_outpoints.size() : fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size());
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2);
auto& outpoints = last_tx ? package_outpoints : mempool_outpoints;
Assert(!outpoints.empty());
CAmount amount_in{0};
for (size_t i = 0; i < num_in; ++i) {
// Pop random outpoint
auto pop = outpoints.begin();
std::advance(pop, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, outpoints.size() - 1));
const auto outpoint = *pop;
outpoints.erase(pop);
// no need to update or erase from outpoints_value
amount_in += outpoints_value.at(outpoint);
// Create input
const auto sequence = ConsumeSequence(fuzzed_data_provider);
const auto script_sig = CScript{};
const auto script_wit_stack = fuzzed_data_provider.ConsumeBool() ? P2WSH_EMPTY_TRUE_STACK : P2WSH_EMPTY_TWO_STACK;
CTxIn in;
in.prevout = outpoint;
in.nSequence = sequence;
in.scriptSig = script_sig;
in.scriptWitness.stack = script_wit_stack;
tx_mut.vin.push_back(in);
}
// Duplicate an input
bool dup_input = fuzzed_data_provider.ConsumeBool();
if (dup_input) {
tx_mut.vin.push_back(tx_mut.vin.back());
}
// Refer to a non-existant input
if (fuzzed_data_provider.ConsumeBool()) {
tx_mut.vin.emplace_back();
}
const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, amount_in);
const auto amount_out = (amount_in - amount_fee) / num_out;
for (int i = 0; i < num_out; ++i) {
tx_mut.vout.emplace_back(amount_out, P2WSH_EMPTY);
}
// TODO vary transaction sizes to catch size-related issues
auto tx = MakeTransactionRef(tx_mut);
// Restore previously removed outpoints, except in-package outpoints
if (!last_tx) {
for (const auto& in : tx->vin) {
// It's a fake input, or a new input, or a duplicate
Assert(in == CTxIn() || outpoints.insert(in.prevout).second || dup_input);
}
// Cache the in-package outpoints being made
for (size_t i = 0; i < tx->vout.size(); ++i) {
package_outpoints.emplace(tx->GetHash(), i);
}
}
// We need newly-created values for the duration of this run
for (size_t i = 0; i < tx->vout.size(); ++i) {
outpoints_value[COutPoint(tx->GetHash(), i)] = tx->vout[i].nValue;
}
return tx;
}();
txs.push_back(tx);
}
if (fuzzed_data_provider.ConsumeBool()) {
MockTime(fuzzed_data_provider, chainstate);
}
if (fuzzed_data_provider.ConsumeBool()) {
tx_pool.RollingFeeUpdate();
}
if (fuzzed_data_provider.ConsumeBool()) {
const auto& txid = fuzzed_data_provider.ConsumeBool() ?
txs.back()->GetHash() :
PickValue(fuzzed_data_provider, mempool_outpoints).hash;
const auto delta = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-50 * COIN, +50 * COIN);
tx_pool.PrioritiseTransaction(txid.ToUint256(), delta);
}
// Remember all added transactions
std::set<CTransactionRef> added;
auto txr = std::make_shared<TransactionsDelta>(added);
RegisterSharedValidationInterface(txr);
const bool bypass_limits = fuzzed_data_provider.ConsumeBool();
// When there are multiple transactions in the package, we call ProcessNewPackage(txs, test_accept=false)
// and AcceptToMemoryPool(txs.back(), test_accept=true). When there is only 1 transaction, we might flip it
// (the package is a test accept and ATMP is a submission).
auto single_submit = txs.size() == 1 && fuzzed_data_provider.ConsumeBool();
const auto result_package = WITH_LOCK(::cs_main,
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit));
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(), bypass_limits, /*test_accept=*/!single_submit));
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
SyncWithValidationInterfaceQueue();
UnregisterSharedValidationInterface(txr);
// There is only 1 transaction in the package. We did a test-package-accept and a ATMP
if (single_submit) {
Assert(accepted != added.empty());
Assert(accepted == res.m_state.IsValid());
if (accepted) {
Assert(added.size() == 1);
Assert(txs.back() == *added.begin());
}
} else if (result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) {
// We don't know anything about the validity since transactions were randomly generated, so
// just use result_package.m_state here. This makes the expect_valid check meaningless, but
// we can still verify that the contents of m_tx_results are consistent with m_state.
const bool expect_valid{result_package.m_state.IsValid()};
Assert(!CheckPackageMempoolAcceptResult(txs, result_package, expect_valid, nullptr));
} else {
// This is empty if it fails early checks, or "full" if transactions are looked at deeper
Assert(result_package.m_tx_results.size() == txs.size() || result_package.m_tx_results.empty());
}
}
UnregisterSharedValidationInterface(outpoints_updater);
WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1));
}
} // namespace