@@ -327,4 +327,236 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
327327 BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (tx_child->GetHash ())));
328328 }
329329}
330+
331+ // Tests for packages containing transactions that have same-txid-different-witness equivalents in
332+ // the mempool.
333+ BOOST_FIXTURE_TEST_CASE (package_witness_swap_tests, TestChain100Setup)
334+ {
335+ // Mine blocks to mature coinbases.
336+ mineBlocks (5 );
337+ LOCK (cs_main);
338+
339+ // Transactions with a same-txid-different-witness transaction in the mempool should be ignored,
340+ // and the mempool entry's wtxid returned.
341+ CScript witnessScript = CScript () << OP_DROP << OP_TRUE;
342+ CScript scriptPubKey = GetScriptForDestination (WitnessV0ScriptHash (witnessScript));
343+ auto mtx_parent = CreateValidMempoolTransaction (/* input_transaction=*/ m_coinbase_txns[0 ], /* vout=*/ 0 ,
344+ /* input_height=*/ 0 , /* input_signing_key=*/ coinbaseKey,
345+ /* output_destination=*/ scriptPubKey,
346+ /* output_amount=*/ CAmount (49 * COIN), /* submit=*/ false );
347+ CTransactionRef ptx_parent = MakeTransactionRef (mtx_parent);
348+
349+ // Make two children with the same txid but different witnesses.
350+ CScriptWitness witness1;
351+ witness1.stack .push_back (std::vector<unsigned char >(1 ));
352+ witness1.stack .push_back (std::vector<unsigned char >(witnessScript.begin (), witnessScript.end ()));
353+
354+ CScriptWitness witness2 (witness1);
355+ witness2.stack .push_back (std::vector<unsigned char >(2 ));
356+ witness2.stack .push_back (std::vector<unsigned char >(witnessScript.begin (), witnessScript.end ()));
357+
358+ CKey child_key;
359+ child_key.MakeNewKey (true );
360+ CScript child_locking_script = GetScriptForDestination (WitnessV0KeyHash (child_key.GetPubKey ()));
361+ CMutableTransaction mtx_child1;
362+ mtx_child1.nVersion = 1 ;
363+ mtx_child1.vin .resize (1 );
364+ mtx_child1.vin [0 ].prevout .hash = ptx_parent->GetHash ();
365+ mtx_child1.vin [0 ].prevout .n = 0 ;
366+ mtx_child1.vin [0 ].scriptSig = CScript ();
367+ mtx_child1.vin [0 ].scriptWitness = witness1;
368+ mtx_child1.vout .resize (1 );
369+ mtx_child1.vout [0 ].nValue = CAmount (48 * COIN);
370+ mtx_child1.vout [0 ].scriptPubKey = child_locking_script;
371+
372+ CMutableTransaction mtx_child2{mtx_child1};
373+ mtx_child2.vin [0 ].scriptWitness = witness2;
374+
375+ CTransactionRef ptx_child1 = MakeTransactionRef (mtx_child1);
376+ CTransactionRef ptx_child2 = MakeTransactionRef (mtx_child2);
377+
378+ // child1 and child2 have the same txid
379+ BOOST_CHECK_EQUAL (ptx_child1->GetHash (), ptx_child2->GetHash ());
380+ // child1 and child2 have different wtxids
381+ BOOST_CHECK (ptx_child1->GetWitnessHash () != ptx_child2->GetWitnessHash ());
382+
383+ // Try submitting Package1{parent, child1} and Package2{parent, child2} where the children are
384+ // same-txid-different-witness.
385+ {
386+ const auto submit_witness1 = ProcessNewPackage (m_node.chainman ->ActiveChainstate (), *m_node.mempool ,
387+ {ptx_parent, ptx_child1}, /* test_accept=*/ false );
388+ BOOST_CHECK_MESSAGE (submit_witness1.m_state .IsValid (),
389+ " Package validation unexpectedly failed: " << submit_witness1.m_state .GetRejectReason ());
390+ auto it_parent1 = submit_witness1.m_tx_results .find (ptx_parent->GetWitnessHash ());
391+ auto it_child1 = submit_witness1.m_tx_results .find (ptx_child1->GetWitnessHash ());
392+ BOOST_CHECK (it_parent1 != submit_witness1.m_tx_results .end ());
393+ BOOST_CHECK_MESSAGE (it_parent1->second .m_state .IsValid (),
394+ " Transaction unexpectedly failed: " << it_parent1->second .m_state .GetRejectReason ());
395+ BOOST_CHECK (it_child1 != submit_witness1.m_tx_results .end ());
396+ BOOST_CHECK_MESSAGE (it_child1->second .m_state .IsValid (),
397+ " Transaction unexpectedly failed: " << it_child1->second .m_state .GetRejectReason ());
398+
399+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_parent->GetHash ())));
400+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_child1->GetHash ())));
401+
402+ const auto submit_witness2 = ProcessNewPackage (m_node.chainman ->ActiveChainstate (), *m_node.mempool ,
403+ {ptx_parent, ptx_child2}, /* test_accept=*/ false );
404+ BOOST_CHECK_MESSAGE (submit_witness2.m_state .IsValid (),
405+ " Package validation unexpectedly failed: " << submit_witness2.m_state .GetRejectReason ());
406+ auto it_parent2_deduped = submit_witness2.m_tx_results .find (ptx_parent->GetWitnessHash ());
407+ auto it_child2 = submit_witness2.m_tx_results .find (ptx_child2->GetWitnessHash ());
408+ BOOST_CHECK (it_parent2_deduped != submit_witness2.m_tx_results .end ());
409+ BOOST_CHECK (it_parent2_deduped->second .m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
410+ BOOST_CHECK (it_child2 != submit_witness2.m_tx_results .end ());
411+ BOOST_CHECK (it_child2->second .m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS);
412+ BOOST_CHECK_EQUAL (ptx_child1->GetWitnessHash (), it_child2->second .m_other_wtxid .value ());
413+
414+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_child2->GetHash ())));
415+ BOOST_CHECK (!m_node.mempool ->exists (GenTxid::Wtxid (ptx_child2->GetWitnessHash ())));
416+ }
417+
418+ // Try submitting Package1{child2, grandchild} where child2 is same-txid-different-witness as
419+ // the in-mempool transaction, child1. Since child1 exists in the mempool and its outputs are
420+ // available, child2 should be ignored and grandchild should be accepted.
421+ //
422+ // This tests a potential censorship vector in which an attacker broadcasts a competing package
423+ // where a parent's witness is mutated. The honest package should be accepted despite the fact
424+ // that we don't allow witness replacement.
425+ CKey grandchild_key;
426+ grandchild_key.MakeNewKey (true );
427+ CScript grandchild_locking_script = GetScriptForDestination (WitnessV0KeyHash (grandchild_key.GetPubKey ()));
428+ auto mtx_grandchild = CreateValidMempoolTransaction (/* input_transaction=*/ ptx_child2, /* vout=*/ 0 ,
429+ /* input_height=*/ 0 , /* input_signing_key=*/ child_key,
430+ /* output_destination=*/ grandchild_locking_script,
431+ /* output_amount=*/ CAmount (47 * COIN), /* submit=*/ false );
432+ CTransactionRef ptx_grandchild = MakeTransactionRef (mtx_grandchild);
433+
434+ // We already submitted child1 above.
435+ {
436+ const auto submit_spend_ignored = ProcessNewPackage (m_node.chainman ->ActiveChainstate (), *m_node.mempool ,
437+ {ptx_child2, ptx_grandchild}, /* test_accept=*/ false );
438+ BOOST_CHECK_MESSAGE (submit_spend_ignored.m_state .IsValid (),
439+ " Package validation unexpectedly failed: " << submit_spend_ignored.m_state .GetRejectReason ());
440+ auto it_child2_ignored = submit_spend_ignored.m_tx_results .find (ptx_child2->GetWitnessHash ());
441+ auto it_grandchild = submit_spend_ignored.m_tx_results .find (ptx_grandchild->GetWitnessHash ());
442+ BOOST_CHECK (it_child2_ignored != submit_spend_ignored.m_tx_results .end ());
443+ BOOST_CHECK (it_child2_ignored->second .m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS);
444+ BOOST_CHECK (it_grandchild != submit_spend_ignored.m_tx_results .end ());
445+ BOOST_CHECK (it_grandchild->second .m_result_type == MempoolAcceptResult::ResultType::VALID);
446+
447+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_child2->GetHash ())));
448+ BOOST_CHECK (!m_node.mempool ->exists (GenTxid::Wtxid (ptx_child2->GetWitnessHash ())));
449+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Wtxid (ptx_grandchild->GetWitnessHash ())));
450+ }
451+
452+ // A package Package{parent1, parent2, parent3, child} where the parents are a mixture of
453+ // identical-tx-in-mempool, same-txid-different-witness-in-mempool, and new transactions.
454+ Package package_mixed;
455+
456+ // Give all the parents anyone-can-spend scripts so we don't have to deal with signing the child.
457+ CScript acs_script = CScript () << OP_TRUE;
458+ CScript acs_spk = GetScriptForDestination (WitnessV0ScriptHash (acs_script));
459+ CScriptWitness acs_witness;
460+ acs_witness.stack .push_back (std::vector<unsigned char >(acs_script.begin (), acs_script.end ()));
461+
462+ // parent1 will already be in the mempool
463+ auto mtx_parent1 = CreateValidMempoolTransaction (/* input_transaction=*/ m_coinbase_txns[1 ], /* vout=*/ 0 ,
464+ /* input_height=*/ 0 , /* input_signing_key=*/ coinbaseKey,
465+ /* output_destination=*/ acs_spk,
466+ /* output_amount=*/ CAmount (49 * COIN), /* submit=*/ true );
467+ CTransactionRef ptx_parent1 = MakeTransactionRef (mtx_parent1);
468+ package_mixed.push_back (ptx_parent1);
469+
470+ // parent2 will have a same-txid-different-witness tx already in the mempool
471+ CScript grandparent2_script = CScript () << OP_DROP << OP_TRUE;
472+ CScript grandparent2_spk = GetScriptForDestination (WitnessV0ScriptHash (grandparent2_script));
473+ CScriptWitness parent2_witness1;
474+ parent2_witness1.stack .push_back (std::vector<unsigned char >(1 ));
475+ parent2_witness1.stack .push_back (std::vector<unsigned char >(grandparent2_script.begin (), grandparent2_script.end ()));
476+ CScriptWitness parent2_witness2;
477+ parent2_witness2.stack .push_back (std::vector<unsigned char >(2 ));
478+ parent2_witness2.stack .push_back (std::vector<unsigned char >(grandparent2_script.begin (), grandparent2_script.end ()));
479+
480+ // Create grandparent2 creating an output with multiple spending paths. Submit to mempool.
481+ auto mtx_grandparent2 = CreateValidMempoolTransaction (/* input_transaction=*/ m_coinbase_txns[2 ], /* vout=*/ 0 ,
482+ /* input_height=*/ 0 , /* input_signing_key=*/ coinbaseKey,
483+ /* output_destination=*/ grandparent2_spk,
484+ /* output_amount=*/ CAmount (49 * COIN), /* submit=*/ true );
485+ CTransactionRef ptx_grandparent2 = MakeTransactionRef (mtx_grandparent2);
486+
487+ CMutableTransaction mtx_parent2_v1;
488+ mtx_parent2_v1.nVersion = 1 ;
489+ mtx_parent2_v1.vin .resize (1 );
490+ mtx_parent2_v1.vin [0 ].prevout .hash = ptx_grandparent2->GetHash ();
491+ mtx_parent2_v1.vin [0 ].prevout .n = 0 ;
492+ mtx_parent2_v1.vin [0 ].scriptSig = CScript ();
493+ mtx_parent2_v1.vin [0 ].scriptWitness = parent2_witness1;
494+ mtx_parent2_v1.vout .resize (1 );
495+ mtx_parent2_v1.vout [0 ].nValue = CAmount (48 * COIN);
496+ mtx_parent2_v1.vout [0 ].scriptPubKey = acs_spk;
497+
498+ CMutableTransaction mtx_parent2_v2{mtx_parent2_v1};
499+ mtx_parent2_v2.vin [0 ].scriptWitness = parent2_witness2;
500+
501+ CTransactionRef ptx_parent2_v1 = MakeTransactionRef (mtx_parent2_v1);
502+ CTransactionRef ptx_parent2_v2 = MakeTransactionRef (mtx_parent2_v2);
503+ // Put parent2_v1 in the package, submit parent2_v2 to the mempool.
504+ const MempoolAcceptResult parent2_v2_result = m_node.chainman ->ProcessTransaction (ptx_parent2_v2);
505+ BOOST_CHECK (parent2_v2_result.m_result_type == MempoolAcceptResult::ResultType::VALID);
506+ package_mixed.push_back (ptx_parent2_v1);
507+
508+ // parent3 will be a new transaction
509+ auto mtx_parent3 = CreateValidMempoolTransaction (/* input_transaction=*/ m_coinbase_txns[3 ], /* vout=*/ 0 ,
510+ /* input_height=*/ 0 , /* input_signing_key=*/ coinbaseKey,
511+ /* output_destination=*/ acs_spk,
512+ /* output_amount=*/ CAmount (49 * COIN), /* submit=*/ false );
513+ CTransactionRef ptx_parent3 = MakeTransactionRef (mtx_parent3);
514+ package_mixed.push_back (ptx_parent3);
515+
516+ // child spends parent1, parent2, and parent3
517+ CKey mixed_grandchild_key;
518+ mixed_grandchild_key.MakeNewKey (true );
519+ CScript mixed_child_spk = GetScriptForDestination (WitnessV0KeyHash (mixed_grandchild_key.GetPubKey ()));
520+
521+ CMutableTransaction mtx_mixed_child;
522+ mtx_mixed_child.vin .push_back (CTxIn (COutPoint (ptx_parent1->GetHash (), 0 )));
523+ mtx_mixed_child.vin .push_back (CTxIn (COutPoint (ptx_parent2_v1->GetHash (), 0 )));
524+ mtx_mixed_child.vin .push_back (CTxIn (COutPoint (ptx_parent3->GetHash (), 0 )));
525+ mtx_mixed_child.vin [0 ].scriptWitness = acs_witness;
526+ mtx_mixed_child.vin [1 ].scriptWitness = acs_witness;
527+ mtx_mixed_child.vin [2 ].scriptWitness = acs_witness;
528+ mtx_mixed_child.vout .push_back (CTxOut (145 * COIN, mixed_child_spk));
529+ CTransactionRef ptx_mixed_child = MakeTransactionRef (mtx_mixed_child);
530+ package_mixed.push_back (ptx_mixed_child);
531+
532+ // Submit package:
533+ // parent1 should be ignored
534+ // parent2_v1 should be ignored (and v2 wtxid returned)
535+ // parent3 should be accepted
536+ // child should be accepted
537+ {
538+ const auto mixed_result = ProcessNewPackage (m_node.chainman ->ActiveChainstate (), *m_node.mempool , package_mixed, false );
539+ BOOST_CHECK_MESSAGE (mixed_result.m_state .IsValid (), mixed_result.m_state .GetRejectReason ());
540+ auto it_parent1 = mixed_result.m_tx_results .find (ptx_parent1->GetWitnessHash ());
541+ auto it_parent2 = mixed_result.m_tx_results .find (ptx_parent2_v1->GetWitnessHash ());
542+ auto it_parent3 = mixed_result.m_tx_results .find (ptx_parent3->GetWitnessHash ());
543+ auto it_child = mixed_result.m_tx_results .find (ptx_mixed_child->GetWitnessHash ());
544+ BOOST_CHECK (it_parent1 != mixed_result.m_tx_results .end ());
545+ BOOST_CHECK (it_parent2 != mixed_result.m_tx_results .end ());
546+ BOOST_CHECK (it_parent3 != mixed_result.m_tx_results .end ());
547+ BOOST_CHECK (it_child != mixed_result.m_tx_results .end ());
548+
549+ BOOST_CHECK (it_parent1->second .m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
550+ BOOST_CHECK (it_parent2->second .m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS);
551+ BOOST_CHECK (it_parent3->second .m_result_type == MempoolAcceptResult::ResultType::VALID);
552+ BOOST_CHECK (it_child->second .m_result_type == MempoolAcceptResult::ResultType::VALID);
553+ BOOST_CHECK_EQUAL (ptx_parent2_v2->GetWitnessHash (), it_parent2->second .m_other_wtxid .value ());
554+
555+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_parent1->GetHash ())));
556+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_parent2_v1->GetHash ())));
557+ BOOST_CHECK (!m_node.mempool ->exists (GenTxid::Wtxid (ptx_parent2_v1->GetWitnessHash ())));
558+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_parent3->GetHash ())));
559+ BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (ptx_mixed_child->GetHash ())));
560+ }
561+ }
330562BOOST_AUTO_TEST_SUITE_END ()
0 commit comments