@@ -137,6 +137,219 @@ std::unique_ptr<CTxMemPool> MakeMempool(FuzzedDataProvider& fuzzed_data_provider
137137 return mempool;
138138}
139139
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+
140353FUZZ_TARGET (tx_package_eval, .init = initialize_tx_pool)
141354{
142355 FuzzedDataProvider fuzzed_data_provider (buffer.data (), buffer.size ());
@@ -321,6 +534,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
321534 }
322535
323536 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+ }
324542 }
325543
326544 node.validation_signals ->UnregisterSharedValidationInterface (outpoints_updater);
0 commit comments