mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-07-13 01:27:00 +02:00
Merge bitcoin/bitcoin#28948: v3 transaction policy for anti-pinning
29029df5c7
[doc] v3 signaling in mempool-replacements.md (glozow)e643ea795e
[fuzz] v3 transactions and sigop-adjusted vsize (glozow)1fd16b5c62
[functional test] v3 transaction submission (glozow)27c8786ba9
test framework: Add and use option for tx-version in MiniWallet methods (MarcoFalke)9a1fea55b2
[policy/validation] allow v3 transactions with certain restrictions (glozow)eb8d5a2e7d
[policy] add v3 policy rules (glozow)9a29d470fb
[rpc] return full string for package_msg and package-error (glozow)158623b8e0
[refactor] change Workspace::m_conflicts and adjacent funcs/structs to use Txid (glozow) Pull request description: See #27463 for overall package relay tracking. Delving Bitcoin discussion thread: https://delvingbitcoin.org/t/v3-transaction-policy-for-anti-pinning/340 Delving Bitcoin discussion for LN usage: https://delvingbitcoin.org/t/lightning-transactions-with-v3-and-ephemeral-anchors/418 Rationale: - There are various pinning problems with RBF and our general ancestor/descendant limits. These policies help mitigate many pinning attacks and make package RBF feasible (see #28984 which implements package RBF on top of this). I would focus the most here on Rule 3 pinning. [1][2] - Switching to a cluster-based mempool (see #27677 and #28676) requires the removal of CPFP carve out, which applications depend on. V3 + package RBF + ephemeral anchors + 1-parent-1-child package relay provides an intermediate solution. V3 policy is for "Priority Transactions." [3][4] It allows users to opt in to more restrictive topological limits for shared transactions, in exchange for the more robust fee-bumping abilities that offers. Even though we don't have cluster limits, we are able to treat these transactions as having as having a maximum cluster size of 2. Immediate benefits: - You can presign a transaction with 0 fees (not just 1sat/vB!) and add a fee-bump later. - Rule 3 pinning is reduced by a significant amount, since the attacker can only attach a maximum of 1000vB to your shared transaction. This also enables some other cool things (again see #27463 for overall roadmap): - Ephemeral Anchors - Package RBF for these 1-parent-1-child packages. That means e.g. a commitment tx + child can replace another commitment tx using the child's fees. - We can transition to a "single anchor" universe without worrying about package limit pinning. So current users of CPFP carve out would have something else to use. - We can switch to a cluster-based mempool [5] (#27677 #28676), which removes CPFP carve out [6]. [1]: Original mailing list post and discussion about RBF pinning problems https://gist.github.com/glozow/25d9662c52453bd08b4b4b1d3783b9ff, https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-January/019817.html [2]: A FAQ is "we need this for cluster mempool, but is this still necessary afterwards?" There are some pinning issues that are fixed here and not fully fixed in cluster mempool, so we will still want this or something similar afterward. [3]: Mailing list post for v3 https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-September/020937.html [4]: Original PR #25038 also contains a lot of the discussion [5]: https://delvingbitcoin.org/t/an-overview-of-the-cluster-mempool-proposal/393/7 [6]: https://delvingbitcoin.org/t/an-overview-of-the-cluster-mempool-proposal/393#the-cpfp-carveout-rule-can-no-longer-be-supported-12 ACKs for top commit: sdaftuar: ACK29029df5c7
achow101: ACK29029df5c7
instagibbs: ACK29029df5c7
modulo that Tree-SHA512: 9664b078890cfdca2a146439f8835c9d9ab483f43b30af8c7cd6962f09aa557fb1ce7689d5e130a2ec142235dbc8f21213881baa75241c5881660f9008d68450
This commit is contained in:
@ -6,6 +6,7 @@
|
||||
#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>
|
||||
@ -119,7 +120,8 @@ CTxMemPool MakeMempool(FuzzedDataProvider& fuzzed_data_provider, const NodeConte
|
||||
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);
|
||||
// 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();
|
||||
@ -171,11 +173,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
||||
// Create transaction to add to the mempool
|
||||
const CTransactionRef tx = [&] {
|
||||
CMutableTransaction tx_mut;
|
||||
tx_mut.nVersion = CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : 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 num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2);
|
||||
|
||||
auto& outpoints = last_tx ? package_outpoints : mempool_outpoints;
|
||||
|
||||
@ -211,17 +213,24 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
||||
tx_mut.vin.push_back(tx_mut.vin.back());
|
||||
}
|
||||
|
||||
// Refer to a non-existant input
|
||||
// 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);
|
||||
}
|
||||
// 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) {
|
||||
@ -261,7 +270,6 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
||||
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
|
||||
@ -271,17 +279,20 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
||||
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;
|
||||
// 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;
|
||||
|
||||
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(passed != added.empty());
|
||||
Assert(passed == res.m_state.IsValid());
|
||||
if (passed) {
|
||||
Assert(added.size() == 1);
|
||||
Assert(txs.back() == *added.begin());
|
||||
}
|
||||
@ -295,6 +306,8 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
||||
// 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);
|
||||
}
|
||||
|
||||
UnregisterSharedValidationInterface(outpoints_updater);
|
||||
|
@ -6,6 +6,7 @@
|
||||
#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>
|
||||
@ -227,7 +228,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
|
||||
// Create transaction to add to the mempool
|
||||
const CTransactionRef tx = [&] {
|
||||
CMutableTransaction tx_mut;
|
||||
tx_mut.nVersion = CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
|
||||
const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size());
|
||||
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size() * 2);
|
||||
@ -313,6 +314,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
|
||||
if (accepted) {
|
||||
Assert(added.size() == 1); // For now, no package acceptance
|
||||
Assert(tx == *added.begin());
|
||||
CheckMempoolV3Invariants(tx_pool);
|
||||
} else {
|
||||
// Do not consider rejected transaction removed
|
||||
removed.erase(tx);
|
||||
@ -405,6 +407,9 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool)
|
||||
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
|
||||
if (accepted) {
|
||||
txids.push_back(tx->GetHash());
|
||||
// Only check fees if accepted and not bypass_limits, otherwise it's not guaranteed that
|
||||
// trimming has happened for this tx and previous iterations.
|
||||
CheckMempoolV3Invariants(tx_pool);
|
||||
}
|
||||
}
|
||||
Finish(fuzzed_data_provider, tx_pool, chainstate);
|
||||
|
Reference in New Issue
Block a user