From 230259641be7c8a9195f9a959e92f408b32b396d Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Mon, 11 Sep 2023 09:53:37 -0400 Subject: [PATCH] Add package evaluation fuzzer --- src/Makefile.test.include | 1 + src/test/fuzz/package_eval.cpp | 339 +++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 src/test/fuzz/package_eval.cpp diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 5dc20d4fab9fa0..d66f5bf53a439a 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -300,6 +300,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/netbase_dns_lookup.cpp \ test/fuzz/node_eviction.cpp \ test/fuzz/p2p_transport_serialization.cpp \ + test/fuzz/package_eval.cpp \ test/fuzz/parse_hd_keypath.cpp \ test/fuzz/parse_numbers.cpp \ test/fuzz/parse_script.cpp \ diff --git a/src/test/fuzz/package_eval.cpp b/src/test/fuzz/package_eval.cpp new file mode 100644 index 00000000000000..7f7f8001996055 --- /dev/null +++ b/src/test/fuzz/package_eval.cpp @@ -0,0 +1,339 @@ +outpoints_supply// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using node::BlockAssembler; +using node::NodeContext; + +namespace { + +const TestingSetup* g_setup; +std::vector g_outpoints_coinbase_init_mature; +std::vector g_outpoints_coinbase_init_immature; + +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(); + g_setup = testing_setup.get(); + + for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) { + COutPoint prevout{MineBlock(g_setup->m_node, P2WSH_OP_TRUE)}; + // Remember the txids to avoid expensive disk access later on + auto& outpoints = i < COINBASE_MATURITY ? + g_outpoints_coinbase_init_mature : + g_outpoints_coinbase_init_immature; + outpoints.push_back(prevout); + } + SyncWithValidationInterfaceQueue(); +} + +struct TransactionsDelta final : public CValidationInterface { + std::set& m_removed; + std::set& m_added; + + explicit TransactionsDelta(std::set& r, std::set& a) + : m_removed{r}, m_added{a} {} + + void TransactionAddedToMempool(const CTransactionRef& tx, uint64_t /* mempool_sequence */) override + { + // Transactions may be entered and booted any number of times + m_added.insert(tx); + } + + void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t /* mempool_sequence */) override + { + // Transactions may be entered and booted any number of times + m_removed.insert(tx); + } +}; + +void Finish(FuzzedDataProvider& fuzzed_data_provider, MockedTxPool& tx_pool, Chainstate& chainstate) +{ + WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1)); + { + BlockAssembler::Options options; + options.nBlockMaxWeight = fuzzed_data_provider.ConsumeIntegralInRange(0U, MAX_BLOCK_WEIGHT); + options.blockMinFeeRate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)}; + auto assembler = BlockAssembler{chainstate, &tx_pool, options}; + auto block_template = assembler.CreateNewBlock(CScript{} << OP_TRUE); + Assert(block_template->block.vtx.size() >= 1); + } + const auto info_all = tx_pool.infoAll(); + if (!info_all.empty()) { + const auto& tx_to_remove = *PickValue(fuzzed_data_provider, info_all).tx; + WITH_LOCK(tx_pool.cs, tx_pool.removeRecursive(tx_to_remove, MemPoolRemovalReason::BLOCK /* dummy */)); + std::vector all_txids; + tx_pool.queryHashes(all_txids); + assert(all_txids.size() < info_all.size()); + WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1)); + } + SyncWithValidationInterfaceQueue(); +} + +void MockTime(FuzzedDataProvider& fuzzed_data_provider, const Chainstate& chainstate) +{ + const auto time = ConsumeTime(fuzzed_data_provider, + chainstate.m_chain.Tip()->GetMedianTimePast() + 1, + std::numeric_limitsnTime)>::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(0, 50); + mempool_opts.limits.ancestor_size_vbytes = fuzzed_data_provider.ConsumeIntegralInRange(0, 202) * 1'000; + mempool_opts.limits.descendant_count = fuzzed_data_provider.ConsumeIntegralInRange(0, 50); + mempool_opts.limits.descendant_size_vbytes = fuzzed_data_provider.ConsumeIntegralInRange(0, 202) * 1'000; + mempool_opts.max_size_bytes = fuzzed_data_provider.ConsumeIntegralInRange(0, 200) * 1'000'000; + mempool_opts.expiry = std::chrono::hours{fuzzed_data_provider.ConsumeIntegralInRange(0, 999)}; + nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange(1, 999); + + mempool_opts.estimator = nullptr; + 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(node.chainman->ActiveChainstate())}; + + MockTime(fuzzed_data_provider, chainstate); + + // All RBF-spendable outpoints outside of the immediate package + std::set outpoints_rbf; + // All outpoints counting toward the total supply (subset of outpoints_rbf) + std::set outpoints_supply; + std::map outpoints_value; + for (const auto& outpoint : g_outpoints_coinbase_init_mature) { + Assert(outpoints_supply.insert(outpoint).second); + outpoints_value[outpoint] = 50 * COIN; + } + + // Seeded by coinbase outputs first + outpoints_rbf = outpoints_supply; + + CTxMemPool tx_pool_{MakeMempool(fuzzed_data_provider, node)}; + MockedTxPool& tx_pool = *static_cast(&tx_pool_); + + chainstate.SetMempool(&tx_pool); + + const CCoinsViewMemPool amount_view{WITH_LOCK(::cs_main, return &chainstate.CoinsTip()), tx_pool}; + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300) + { + Assert(!outpoints_supply.empty()); + + std::vector txs; + std::map wtxid_to_tx; + + + // Make packages of 1-to-26 transactions + const auto num_txs = (size_t) fuzzed_data_provider.ConsumeIntegralInRange(1, 26); + std::set 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 unconfirmed 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(); + // Last tx will sweep all outpoints in package + const auto num_in = last_tx ? package_outpoints.size() : fuzzed_data_provider.ConsumeIntegralInRange(1, outpoints_rbf.size()); + const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange(1, outpoints_rbf.size() * 2); + + auto& outpoints = last_tx ? package_outpoints : outpoints_rbf; + + 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(0, outpoints.size() - 1)); + const auto outpoint = *pop; + outpoints.erase(pop); + 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 = std::vector>{WITNESS_STACK_ELEM_OP_TRUE}; + CTxIn in; + in.prevout = outpoint; + in.nSequence = sequence; + in.scriptSig = script_sig; + in.scriptWitness.stack = script_wit_stack; + + tx_mut.vin.push_back(in); + } + const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange(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_OP_TRUE); + } + // 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) { + Assert(outpoints.insert(in.prevout).second); + } + } + // Cache the in-package outpoints being made + for (size_t i = 0; i < tx->vout.size(); ++i) { + package_outpoints.emplace(tx->GetHash(), i); + outpoints_value[COutPoint(tx->GetHash(), i)] = tx->vout[i].nValue; + } + return tx; + }(); + txs.push_back(tx); + wtxid_to_tx[tx->GetWitnessHash()] = 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, outpoints_rbf).hash; + const auto delta = fuzzed_data_provider.ConsumeIntegralInRange(-50 * COIN, +50 * COIN); + tx_pool.PrioritiseTransaction(txid, delta); + } + + // Remember all removed and added transactions + std::set removed; + std::set added; + auto txr = std::make_shared(removed, added); + RegisterSharedValidationInterface(txr); + const bool bypass_limits = fuzzed_data_provider.ConsumeBool(); + + // Single-tx packages should be rejected, so do that sometimes, and sometimes send it via single submission + // to allow it into the mempool by itself to make more interesting mempool packages + auto single_submit = txs.size() == 1 && fuzzed_data_provider.ConsumeBool(); + auto package_submit = !single_submit; + + const auto result_package = WITH_LOCK(::cs_main, + return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/!package_submit)); + // If something went wrong due to a package-specific policy, it might not return a + // validation result for the transaction. + if (result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) { + auto it = result_package.m_tx_results.find(txs.back()->GetWitnessHash()); + Assert(it != result_package.m_tx_results.end()); + Assert(it->second.m_result_type == MempoolAcceptResult::ResultType::VALID || + it->second.m_result_type == MempoolAcceptResult::ResultType::INVALID || + it->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY); + } + + 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); + + if (single_submit) { + Assert(accepted != added.empty()); + Assert(accepted == res.m_state.IsValid()); + Assert(accepted != res.m_state.IsInvalid()); + if (accepted) { + Assert(added.size() == 1); + Assert(txs.back() == *added.begin()); + } else { + // Do not consider rejected transaction removed + removed.erase(txs.back()); + } + } 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()); + if (result_package.m_state.GetResult() == PackageValidationResult::PCKG_POLICY) { + for (const auto& tx : txs) { + removed.erase(tx); + } + } else { + for (const auto& [k, v] : result_package.m_tx_results) { + if (v.m_result_type == MempoolAcceptResult::ResultType::INVALID) { + removed.erase(wtxid_to_tx[k]); + } + } + } + } + + // Helper to insert spent and created outpoints of a tx into collections + using Sets = std::vector>>; + const auto insert_tx = [](Sets created_by_tx, Sets consumed_by_tx, const auto& tx) { + for (size_t i{0}; i < tx.vout.size(); ++i) { + for (auto& set : created_by_tx) { + set.get().emplace(tx.GetHash(), i); + } + } + for (const auto& in : tx.vin) { + for (auto& set : consumed_by_tx) { + set.get().insert(in.prevout); + } + } + }; + + // Add created outpoints, remove spent outpoints + { + // Outpoints that no longer exist at all + std::set consumed_erased; + // Outpoints that no longer count toward the total supply + std::set consumed_supply; + for (const auto& removed_tx : removed) { + insert_tx(/*created_by_tx=*/{consumed_erased}, /*consumed_by_tx=*/{outpoints_supply}, /*tx=*/*removed_tx); + } + for (const auto& added_tx : added) { + insert_tx(/*created_by_tx=*/{outpoints_supply, outpoints_rbf}, /*consumed_by_tx=*/{consumed_supply}, /*tx=*/*added_tx); + } + for (const auto& p : consumed_erased) { + outpoints_supply.erase(p); + outpoints_rbf.erase(p); + } + for (const auto& p : consumed_supply) { + outpoints_supply.erase(p); + } + } + } + Finish(fuzzed_data_provider, tx_pool, chainstate); +} +} // namespace