Skip to content

Commit e643ea7

Browse files
glozowinstagibbs
andcommitted
[fuzz] v3 transactions and sigop-adjusted vsize
Ensure we are checking sigop-adjusted virtual size by creating setups and packages where sigop cost is larger than bip141 vsize. Co-authored-by: Gregory Sanders <gsanders87@gmail.com>
1 parent 1fd16b5 commit e643ea7

File tree

4 files changed

+66
-12
lines changed

4 files changed

+66
-12
lines changed

src/test/fuzz/package_eval.cpp

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <node/context.h>
77
#include <node/mempool_args.h>
88
#include <node/miner.h>
9+
#include <policy/v3_policy.h>
910
#include <test/fuzz/FuzzedDataProvider.h>
1011
#include <test/fuzz/fuzz.h>
1112
#include <test/fuzz/util.h>
@@ -119,7 +120,8 @@ CTxMemPool MakeMempool(FuzzedDataProvider& fuzzed_data_provider, const NodeConte
119120
mempool_opts.limits.descendant_size_vbytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 202) * 1'000;
120121
mempool_opts.max_size_bytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 200) * 1'000'000;
121122
mempool_opts.expiry = std::chrono::hours{fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 999)};
122-
nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(1, 999);
123+
// Only interested in 2 cases: sigop cost 0 or when single legacy sigop cost is >> 1KvB
124+
nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 1) * 10'000;
123125

124126
mempool_opts.check_ratio = 1;
125127
mempool_opts.require_standard = fuzzed_data_provider.ConsumeBool();
@@ -171,11 +173,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
171173
// Create transaction to add to the mempool
172174
const CTransactionRef tx = [&] {
173175
CMutableTransaction tx_mut;
174-
tx_mut.nVersion = CTransaction::CURRENT_VERSION;
176+
tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : CTransaction::CURRENT_VERSION;
175177
tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
176178
// Last tx will sweep all outpoints in package
177179
const auto num_in = last_tx ? package_outpoints.size() : fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size());
178-
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2);
180+
auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2);
179181

180182
auto& outpoints = last_tx ? package_outpoints : mempool_outpoints;
181183

@@ -211,17 +213,24 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
211213
tx_mut.vin.push_back(tx_mut.vin.back());
212214
}
213215

214-
// Refer to a non-existant input
216+
// Refer to a non-existent input
215217
if (fuzzed_data_provider.ConsumeBool()) {
216218
tx_mut.vin.emplace_back();
217219
}
218220

221+
// Make a p2pk output to make sigops adjusted vsize to violate v3, potentially, which is never spent
222+
if (last_tx && amount_in > 1000 && fuzzed_data_provider.ConsumeBool()) {
223+
tx_mut.vout.emplace_back(1000, CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG);
224+
// Don't add any other outputs.
225+
num_out = 1;
226+
amount_in -= 1000;
227+
}
228+
219229
const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, amount_in);
220230
const auto amount_out = (amount_in - amount_fee) / num_out;
221231
for (int i = 0; i < num_out; ++i) {
222232
tx_mut.vout.emplace_back(amount_out, P2WSH_EMPTY);
223233
}
224-
// TODO vary transaction sizes to catch size-related issues
225234
auto tx = MakeTransactionRef(tx_mut);
226235
// Restore previously removed outpoints, except in-package outpoints
227236
if (!last_tx) {
@@ -261,7 +270,6 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
261270
std::set<CTransactionRef> added;
262271
auto txr = std::make_shared<TransactionsDelta>(added);
263272
RegisterSharedValidationInterface(txr);
264-
const bool bypass_limits = fuzzed_data_provider.ConsumeBool();
265273

266274
// When there are multiple transactions in the package, we call ProcessNewPackage(txs, test_accept=false)
267275
// 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)
271279
const auto result_package = WITH_LOCK(::cs_main,
272280
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit));
273281

274-
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(), bypass_limits, /*test_accept=*/!single_submit));
275-
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
282+
// Always set bypass_limits to false because it is not supported in ProcessNewPackage and
283+
// can be a source of divergence.
284+
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(),
285+
/*bypass_limits=*/false, /*test_accept=*/!single_submit));
286+
const bool passed = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
276287

277288
SyncWithValidationInterfaceQueue();
278289
UnregisterSharedValidationInterface(txr);
279290

280291
// There is only 1 transaction in the package. We did a test-package-accept and a ATMP
281292
if (single_submit) {
282-
Assert(accepted != added.empty());
283-
Assert(accepted == res.m_state.IsValid());
284-
if (accepted) {
293+
Assert(passed != added.empty());
294+
Assert(passed == res.m_state.IsValid());
295+
if (passed) {
285296
Assert(added.size() == 1);
286297
Assert(txs.back() == *added.begin());
287298
}
@@ -295,6 +306,8 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
295306
// This is empty if it fails early checks, or "full" if transactions are looked at deeper
296307
Assert(result_package.m_tx_results.size() == txs.size() || result_package.m_tx_results.empty());
297308
}
309+
310+
CheckMempoolV3Invariants(tx_pool);
298311
}
299312

300313
UnregisterSharedValidationInterface(outpoints_updater);

src/test/fuzz/tx_pool.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <node/context.h>
77
#include <node/mempool_args.h>
88
#include <node/miner.h>
9+
#include <policy/v3_policy.h>
910
#include <test/fuzz/FuzzedDataProvider.h>
1011
#include <test/fuzz/fuzz.h>
1112
#include <test/fuzz/util.h>
@@ -229,7 +230,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
229230
// Create transaction to add to the mempool
230231
const CTransactionRef tx = [&] {
231232
CMutableTransaction tx_mut;
232-
tx_mut.nVersion = CTransaction::CURRENT_VERSION;
233+
tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : CTransaction::CURRENT_VERSION;
233234
tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
234235
const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size());
235236
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size() * 2);
@@ -315,6 +316,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
315316
if (accepted) {
316317
Assert(added.size() == 1); // For now, no package acceptance
317318
Assert(tx == *added.begin());
319+
CheckMempoolV3Invariants(tx_pool);
318320
} else {
319321
// Do not consider rejected transaction removed
320322
removed.erase(tx);
@@ -407,6 +409,9 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool)
407409
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
408410
if (accepted) {
409411
txids.push_back(tx->GetHash());
412+
// Only check fees if accepted and not bypass_limits, otherwise it's not guaranteed that
413+
// trimming has happened for this tx and previous iterations.
414+
CheckMempoolV3Invariants(tx_pool);
410415
}
411416
}
412417
Finish(fuzzed_data_provider, tx_pool, chainstate);

src/test/util/txmempool.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <chainparams.h>
88
#include <node/context.h>
99
#include <node/mempool_args.h>
10+
#include <policy/v3_policy.h>
1011
#include <txmempool.h>
1112
#include <util/check.h>
1213
#include <util/time.h>
@@ -116,3 +117,28 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
116117
}
117118
return std::nullopt;
118119
}
120+
121+
void CheckMempoolV3Invariants(const CTxMemPool& tx_pool)
122+
{
123+
LOCK(tx_pool.cs);
124+
for (const auto& tx_info : tx_pool.infoAll()) {
125+
const auto& entry = *Assert(tx_pool.GetEntry(tx_info.tx->GetHash()));
126+
if (tx_info.tx->nVersion == 3) {
127+
// Check that special v3 ancestor/descendant limits and rules are always respected
128+
Assert(entry.GetCountWithDescendants() <= V3_DESCENDANT_LIMIT);
129+
Assert(entry.GetCountWithAncestors() <= V3_ANCESTOR_LIMIT);
130+
// If this transaction has at least 1 ancestor, it's a "child" and has restricted weight.
131+
if (entry.GetCountWithAncestors() > 1) {
132+
Assert(entry.GetTxSize() <= V3_CHILD_MAX_VSIZE);
133+
// All v3 transactions must only have v3 unconfirmed parents.
134+
const auto& parents = entry.GetMemPoolParentsConst();
135+
Assert(parents.begin()->get().GetSharedTx()->nVersion == 3);
136+
}
137+
} else if (entry.GetCountWithAncestors() > 1) {
138+
// All non-v3 transactions must only have non-v3 unconfirmed parents.
139+
for (const auto& parent : entry.GetMemPoolParentsConst()) {
140+
Assert(parent.get().GetSharedTx()->nVersion != 3);
141+
}
142+
}
143+
}
144+
}

src/test/util/txmempool.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,14 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
4646
const PackageMempoolAcceptResult& result,
4747
bool expect_valid,
4848
const CTxMemPool* mempool);
49+
50+
/** For every transaction in tx_pool, check v3 invariants:
51+
* - a v3 tx's ancestor count must be within V3_ANCESTOR_LIMIT
52+
* - a v3 tx's descendant count must be within V3_DESCENDANT_LIMIT
53+
* - if a v3 tx has ancestors, its sigop-adjusted vsize must be within V3_CHILD_MAX_VSIZE
54+
* - any non-v3 tx must only have non-v3 parents
55+
* - any v3 tx must only have v3 parents
56+
* */
57+
void CheckMempoolV3Invariants(const CTxMemPool& tx_pool);
58+
4959
#endif // BITCOIN_TEST_UTIL_TXMEMPOOL_H

0 commit comments

Comments
 (0)