@@ -137,6 +137,219 @@ std::unique_ptr<CTxMemPool> MakeMempool(FuzzedDataProvider& fuzzed_data_provider
137
137
return mempool;
138
138
}
139
139
140
+ std::unique_ptr<CTxMemPool> MakeEphemeralMempool (const NodeContext& node)
141
+ {
142
+ // Take the default options for tests...
143
+ CTxMemPool::Options mempool_opts{MemPoolOptionsForTest (node)};
144
+
145
+ mempool_opts.check_ratio = 1 ;
146
+
147
+ // Require standardness rules otherwise ephemeral dust is no-op
148
+ mempool_opts.require_standard = true ;
149
+
150
+ // And set minrelay to 0 to allow ephemeral parent tx even with non-TRUC
151
+ mempool_opts.min_relay_feerate = CFeeRate (0 );
152
+
153
+ bilingual_str error;
154
+ // ...and construct a CTxMemPool from it
155
+ auto mempool{std::make_unique<CTxMemPool>(std::move (mempool_opts), error)};
156
+ Assert (error.empty ());
157
+ return mempool;
158
+ }
159
+
160
+ // Scan mempool for a tx that has spent dust and return a
161
+ // prevout of the child that isn't the dusty parent itself.
162
+ // This is used to double-spend the child out of the mempool,
163
+ // leaving the parent childless.
164
+ // This assumes CheckMempoolEphemeralInvariants has passed for tx_pool.
165
+ std::optional<COutPoint> GetChildEvictingPrevout (const CTxMemPool& tx_pool)
166
+ {
167
+ LOCK (tx_pool.cs );
168
+ for (const auto & tx_info : tx_pool.infoAll ()) {
169
+ const auto & entry = *Assert (tx_pool.GetEntry (tx_info.tx ->GetHash ()));
170
+ std::vector<uint32_t > dust_indexes{GetDustIndexes (tx_info.tx , tx_pool.m_opts .dust_relay_feerate )};
171
+ if (!dust_indexes.empty ()) {
172
+ const auto & children = entry.GetMemPoolChildrenConst ();
173
+ if (!children.empty ()) {
174
+ Assert (children.size () == 1 );
175
+ // Find an input that doesn't spend from parent's txid
176
+ const auto & only_child = children.begin ()->get ().GetTx ();
177
+ for (const auto & tx_input : only_child.vin ) {
178
+ if (tx_input.prevout .hash != tx_info.tx ->GetHash ()) {
179
+ return tx_input.prevout ;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ return std::nullopt;
187
+ }
188
+
189
+ FUZZ_TARGET (ephemeral_package_eval, .init = initialize_tx_pool)
190
+ {
191
+ FuzzedDataProvider fuzzed_data_provider (buffer.data (), buffer.size ());
192
+ const auto & node = g_setup->m_node ;
193
+ auto & chainstate{static_cast <DummyChainState&>(node.chainman ->ActiveChainstate ())};
194
+
195
+ MockTime (fuzzed_data_provider, chainstate);
196
+
197
+ // All RBF-spendable outpoints outside of the unsubmitted package
198
+ std::set<COutPoint> mempool_outpoints;
199
+ std::map<COutPoint, CAmount> outpoints_value;
200
+ for (const auto & outpoint : g_outpoints_coinbase_init_mature) {
201
+ Assert (mempool_outpoints.insert (outpoint).second );
202
+ outpoints_value[outpoint] = 50 * COIN;
203
+ }
204
+
205
+ auto outpoints_updater = std::make_shared<OutpointsUpdater>(mempool_outpoints);
206
+ node.validation_signals ->RegisterSharedValidationInterface (outpoints_updater);
207
+
208
+ auto tx_pool_{MakeEphemeralMempool (node)};
209
+ MockedTxPool& tx_pool = *static_cast <MockedTxPool*>(tx_pool_.get ());
210
+
211
+ chainstate.SetMempool (&tx_pool);
212
+
213
+ LIMITED_WHILE (fuzzed_data_provider.ConsumeBool (), 300 )
214
+ {
215
+ Assert (!mempool_outpoints.empty ());
216
+
217
+ std::vector<CTransactionRef> txs;
218
+
219
+ // Find something we may want to double-spend with two input single tx
220
+ std::optional<COutPoint> outpoint_to_rbf{GetChildEvictingPrevout (tx_pool)};
221
+ bool should_rbf_eph_spend = outpoint_to_rbf && fuzzed_data_provider.ConsumeBool ();
222
+
223
+ // Make small packages
224
+ const auto num_txs = should_rbf_eph_spend ? 1 : (size_t ) fuzzed_data_provider.ConsumeIntegralInRange <int >(1 , 4 );
225
+
226
+ std::set<COutPoint> package_outpoints;
227
+ while (txs.size () < num_txs) {
228
+
229
+ // Last transaction in a package needs to be a child of parents to get further in validation
230
+ // so the last transaction to be generated(in a >1 package) must spend all package-made outputs
231
+ // Note that this test currently only spends package outputs in last transaction.
232
+ bool last_tx = num_txs > 1 && txs.size () == num_txs - 1 ;
233
+
234
+ // Create transaction to add to the mempool
235
+ const CTransactionRef tx = [&] {
236
+ CMutableTransaction tx_mut;
237
+ tx_mut.version = CTransaction::CURRENT_VERSION;
238
+ tx_mut.nLockTime = 0 ;
239
+ // Last tx will sweep half or more of all outpoints from package
240
+ const auto num_in = should_rbf_eph_spend ? 2 :
241
+ last_tx ? fuzzed_data_provider.ConsumeIntegralInRange <int >(package_outpoints.size ()/2 + 1 , package_outpoints.size ()) :
242
+ fuzzed_data_provider.ConsumeIntegralInRange <int >(1 , 4 );
243
+ auto num_out = should_rbf_eph_spend ? 1 : fuzzed_data_provider.ConsumeIntegralInRange <int >(1 , 4 );
244
+
245
+ auto & outpoints = last_tx ? package_outpoints : mempool_outpoints;
246
+
247
+ Assert ((int )outpoints.size () >= num_in && num_in > 0 );
248
+
249
+ CAmount amount_in{0 };
250
+ for (int i = 0 ; i < num_in; ++i) {
251
+ // Pop random outpoint
252
+ auto pop = outpoints.begin ();
253
+ std::advance (pop, fuzzed_data_provider.ConsumeIntegralInRange <size_t >(0 , outpoints.size () - 1 ));
254
+ auto outpoint = *pop;
255
+
256
+ if (i == 0 && should_rbf_eph_spend) {
257
+ outpoint = *outpoint_to_rbf;
258
+ outpoints.erase (outpoint);
259
+ } else {
260
+ outpoints.erase (pop);
261
+ }
262
+ // no need to update or erase from outpoints_value
263
+ amount_in += outpoints_value.at (outpoint);
264
+
265
+ // Create input
266
+ CTxIn in;
267
+ in.prevout = outpoint;
268
+ in.scriptWitness .stack = P2WSH_EMPTY_TRUE_STACK;
269
+
270
+ tx_mut.vin .push_back (in);
271
+ }
272
+
273
+ const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange <CAmount>(0 , amount_in);
274
+ const auto amount_out = (amount_in - amount_fee) / num_out;
275
+ for (int i = 0 ; i < num_out; ++i) {
276
+ tx_mut.vout .emplace_back (amount_out, P2WSH_EMPTY);
277
+ }
278
+
279
+ // Note output amounts can naturally drop to dust on their own.
280
+ if (!should_rbf_eph_spend && fuzzed_data_provider.ConsumeBool ()) {
281
+ uint32_t dust_index = fuzzed_data_provider.ConsumeIntegralInRange <uint32_t >(0 , num_out);
282
+ tx_mut.vout .insert (tx_mut.vout .begin () + dust_index, CTxOut (0 , P2WSH_EMPTY));
283
+ }
284
+
285
+ auto tx = MakeTransactionRef (tx_mut);
286
+ // Restore previously removed outpoints, except in-package outpoints (to allow RBF)
287
+ if (!last_tx) {
288
+ for (const auto & in : tx->vin ) {
289
+ Assert (outpoints.insert (in.prevout ).second );
290
+ }
291
+ // Cache the in-package outpoints being made
292
+ for (size_t i = 0 ; i < tx->vout .size (); ++i) {
293
+ package_outpoints.emplace (tx->GetHash (), i);
294
+ }
295
+ }
296
+ // We need newly-created values for the duration of this run
297
+ for (size_t i = 0 ; i < tx->vout .size (); ++i) {
298
+ outpoints_value[COutPoint (tx->GetHash (), i)] = tx->vout [i].nValue ;
299
+ }
300
+ return tx;
301
+ }();
302
+ txs.push_back (tx);
303
+ }
304
+
305
+ if (fuzzed_data_provider.ConsumeBool ()) {
306
+ const auto & txid = fuzzed_data_provider.ConsumeBool () ?
307
+ txs.back ()->GetHash () :
308
+ PickValue (fuzzed_data_provider, mempool_outpoints).hash ;
309
+ const auto delta = fuzzed_data_provider.ConsumeIntegralInRange <CAmount>(-50 * COIN, +50 * COIN);
310
+ // We only prioritise out of mempool transactions since PrioritiseTransaction doesn't
311
+ // filter for ephemeral dust GetEntry
312
+ if (tx_pool.exists (GenTxid::Txid (txid))) {
313
+ const auto tx_info{tx_pool.info (GenTxid::Txid (txid))};
314
+ if (GetDustIndexes (tx_info.tx , tx_pool.m_opts .dust_relay_feerate ).empty ()) {
315
+ tx_pool.PrioritiseTransaction (txid.ToUint256 (), delta);
316
+ }
317
+ }
318
+ }
319
+
320
+ // Remember all added transactions
321
+ std::set<CTransactionRef> added;
322
+ auto txr = std::make_shared<TransactionsDelta>(added);
323
+ node.validation_signals ->RegisterSharedValidationInterface (txr);
324
+
325
+ auto single_submit = txs.size () == 1 ;
326
+
327
+ const auto result_package = WITH_LOCK (::cs_main,
328
+ return ProcessNewPackage (chainstate, tx_pool, txs, /* test_accept=*/ single_submit, /* client_maxfeerate=*/ {}));
329
+
330
+ const auto res = WITH_LOCK (::cs_main, return AcceptToMemoryPool (chainstate, txs.back (), GetTime (),
331
+ /* bypass_limits=*/ fuzzed_data_provider.ConsumeBool (), /* test_accept=*/ !single_submit));
332
+
333
+ if (!single_submit && result_package.m_state .GetResult () != PackageValidationResult::PCKG_POLICY) {
334
+ // We don't know anything about the validity since transactions were randomly generated, so
335
+ // just use result_package.m_state here. This makes the expect_valid check meaningless, but
336
+ // we can still verify that the contents of m_tx_results are consistent with m_state.
337
+ const bool expect_valid{result_package.m_state .IsValid ()};
338
+ Assert (!CheckPackageMempoolAcceptResult (txs, result_package, expect_valid, &tx_pool));
339
+ }
340
+
341
+ node.validation_signals ->SyncWithValidationInterfaceQueue ();
342
+ node.validation_signals ->UnregisterSharedValidationInterface (txr);
343
+
344
+ CheckMempoolEphemeralInvariants (tx_pool);
345
+ }
346
+
347
+ node.validation_signals ->UnregisterSharedValidationInterface (outpoints_updater);
348
+
349
+ WITH_LOCK (::cs_main, tx_pool.check (chainstate.CoinsTip (), chainstate.m_chain .Height () + 1 ));
350
+ }
351
+
352
+
140
353
FUZZ_TARGET (tx_package_eval, .init = initialize_tx_pool)
141
354
{
142
355
FuzzedDataProvider fuzzed_data_provider (buffer.data (), buffer.size ());
@@ -321,6 +534,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
321
534
}
322
535
323
536
CheckMempoolTRUCInvariants (tx_pool);
537
+
538
+ // Dust checks only make sense when dust is enforced
539
+ if (tx_pool.m_opts .require_standard ) {
540
+ CheckMempoolEphemeralInvariants (tx_pool);
541
+ }
324
542
}
325
543
326
544
node.validation_signals ->UnregisterSharedValidationInterface (outpoints_updater);
0 commit comments