mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-02-04 20:51:14 +00:00
94ed4fbf8e1a396c650b5134d396d6c0be35ce10 Add release note for size 2 package rbf (Greg Sanders)
afd52d8e63ed323a159ea49fd1f10542abeacb97 doc: update package RBF comment (Greg Sanders)
6e3c4394cfadf32c06c8c4732d136ca10c316721 mempool: Improve logging of replaced transactions (Greg Sanders)
d3466e4cc5051c314873dd14ec8f7a88494c0780 CheckPackageMempoolAcceptResult: Check package rbf invariants (Greg Sanders)
316d7b63c97144ba3e21201315c784852210f8ff Fuzz: pass mempool to CheckPackageMempoolAcceptResult (Greg Sanders)
4d15bcf448eb3c4451b63e8f78cc61f3f9f9b639 [test] package rbf (glozow)
dc21f61c72e5a97d974ca2c5cb70b8328f4fab2a [policy] package rbf (Suhas Daftuar)
5da396781589177d4ceb3b4b59c9f309a5e4d029 PackageV3Checks: Relax assumptions (Greg Sanders)
Pull request description:
Allows any 2 transaction package with no in-mempool ancestors to do package RBF when directly conflicting with other mempool clusters of size two or less.
Proposed validation steps:
1) If the transaction package is of size 1, legacy rbf rules apply.
2) Otherwise the transaction package consists of a (parent, child) pair with no other in-mempool ancestors (or descendants, obviously), so it is also going to create a cluster of size 2. If larger, fail.
3) The package rbf may not evict more than 100 transactions from the mempool(bip125 rule 5)
4) The package is a single chunk
5) Every directly conflicted mempool transaction is connected to at most 1 other in-mempool transaction (ie the cluster size of the conflict is at most 2).
6) Diagram check: We ensure that the replacement is strictly superior, improving the mempool
7) The total fee of the package, minus the total fee of what is being evicted, is at least the minrelayfee * size of the package (equivalent to bip125 rule 3 and 4)
Post-cluster mempool this will likely be expanded to general package rbf, but this is what we can safely support today.
ACKs for top commit:
achow101:
ACK 94ed4fbf8e1a396c650b5134d396d6c0be35ce10
glozow:
reACK 94ed4fbf8e via range-diff
ismaelsadeeq:
re-ACK 94ed4fbf8e1a396c650b5134d396d6c0be35ce10
theStack:
Code-review ACK 94ed4fbf8e1a396c650b5134d396d6c0be35ce10
murchandamus:
utACK 94ed4fbf8e1a396c650b5134d396d6c0be35ce10
Tree-SHA512: 9bd383e695964f362f147482bbf73b1e77c4d792bda2e91d7f30d74b3540a09146a5528baf86854a113005581e8c75f04737302517b7d5124296bd7a151e3992
331 lines
15 KiB
C++
331 lines
15 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 <policy/v3_policy.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/check.h>
|
|
#include <util/rbf.h>
|
|
#include <util/translation.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);
|
|
}
|
|
}
|
|
g_setup->m_node.validation_signals->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);
|
|
}
|
|
|
|
std::unique_ptr<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)};
|
|
// Only interested in 2 cases: sigop cost 0 or when single legacy sigop cost is >> 1KvB
|
|
nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 1) * 10'000;
|
|
|
|
mempool_opts.check_ratio = 1;
|
|
mempool_opts.require_standard = fuzzed_data_provider.ConsumeBool();
|
|
|
|
bilingual_str error;
|
|
// ...and construct a CTxMemPool from it
|
|
auto mempool{std::make_unique<CTxMemPool>(std::move(mempool_opts), error)};
|
|
// ... ignore the error since it might be beneficial to fuzz even when the
|
|
// mempool size is unreasonably small
|
|
Assert(error.empty() || error.original.starts_with("-maxmempool must be at least "));
|
|
return mempool;
|
|
}
|
|
|
|
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);
|
|
node.validation_signals->RegisterSharedValidationInterface(outpoints_updater);
|
|
|
|
auto tx_pool_{MakeMempool(fuzzed_data_provider, node)};
|
|
MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(tx_pool_.get());
|
|
|
|
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.version = fuzzed_data_provider.ConsumeBool() ? TRUC_VERSION : 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());
|
|
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-existent input
|
|
if (fuzzed_data_provider.ConsumeBool()) {
|
|
tx_mut.vin.emplace_back();
|
|
}
|
|
|
|
// Make a p2pk output to make sigops adjusted vsize to violate v3, potentially, which is never spent
|
|
if (last_tx && amount_in > 1000 && fuzzed_data_provider.ConsumeBool()) {
|
|
tx_mut.vout.emplace_back(1000, CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG);
|
|
// Don't add any other outputs.
|
|
num_out = 1;
|
|
amount_in -= 1000;
|
|
}
|
|
|
|
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);
|
|
}
|
|
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);
|
|
node.validation_signals->RegisterSharedValidationInterface(txr);
|
|
|
|
// 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();
|
|
|
|
// Exercise client_maxfeerate logic
|
|
std::optional<CFeeRate> client_maxfeerate{};
|
|
if (fuzzed_data_provider.ConsumeBool()) {
|
|
client_maxfeerate = CFeeRate(fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-1, 50 * COIN), 100);
|
|
}
|
|
|
|
const auto result_package = WITH_LOCK(::cs_main,
|
|
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, client_maxfeerate));
|
|
|
|
// Always set bypass_limits to false because it is not supported in ProcessNewPackage and
|
|
// can be a source of divergence.
|
|
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(),
|
|
/*bypass_limits=*/false, /*test_accept=*/!single_submit));
|
|
const bool passed = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
|
|
|
|
node.validation_signals->SyncWithValidationInterfaceQueue();
|
|
node.validation_signals->UnregisterSharedValidationInterface(txr);
|
|
|
|
// There is only 1 transaction in the package. We did a test-package-accept and a ATMP
|
|
if (single_submit) {
|
|
Assert(passed != added.empty());
|
|
Assert(passed == res.m_state.IsValid());
|
|
if (passed) {
|
|
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, &tx_pool));
|
|
} 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());
|
|
}
|
|
|
|
CheckMempoolV3Invariants(tx_pool);
|
|
}
|
|
|
|
node.validation_signals->UnregisterSharedValidationInterface(outpoints_updater);
|
|
|
|
WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1));
|
|
}
|
|
} // namespace
|