forked from bitcoin/bitcoin
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f1a9fd6
commit bf7436c
Showing
2 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,339 @@ | ||
// 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::BlockAssembler; | ||
using node::NodeContext; | ||
|
||
namespace { | ||
|
||
const TestingSetup* g_setup; | ||
std::vector<COutPoint> g_outpoints_coinbase_init_mature; | ||
std::vector<COutPoint> 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<const TestingSetup>(); | ||
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<CTransactionRef>& m_removed; | ||
std::set<CTransactionRef>& m_added; | ||
|
||
explicit TransactionsDelta(std::set<CTransactionRef>& r, std::set<CTransactionRef>& 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<uint256> 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_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.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<DummyChainState&>(node.chainman->ActiveChainstate())}; | ||
|
||
MockTime(fuzzed_data_provider, chainstate); | ||
|
||
// All RBF-spendable outpoints outside of the immediate package | ||
std::set<COutPoint> outpoints_rbf; | ||
// All outpoints counting toward the total supply (subset of outpoints_rbf) | ||
std::set<COutPoint> outpoints_supply; | ||
std::map<COutPoint, CAmount> 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<MockedTxPool*>(&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<CTransactionRef> txs; | ||
std::map<uint256, CTransactionRef> wtxid_to_tx; | ||
|
||
|
||
// 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 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<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, outpoints_rbf.size()); | ||
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(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<size_t>(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<std::vector<uint8_t>>{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<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_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<CAmount>(-50 * COIN, +50 * COIN); | ||
tx_pool.PrioritiseTransaction(txid, delta); | ||
} | ||
|
||
// Remember all removed and added transactions | ||
std::set<CTransactionRef> removed; | ||
std::set<CTransactionRef> added; | ||
auto txr = std::make_shared<TransactionsDelta>(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<std::reference_wrapper<std::set<COutPoint>>>; | ||
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<COutPoint> consumed_erased; | ||
// Outpoints that no longer count toward the total supply | ||
std::set<COutPoint> 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 |