@@ -327,4 +327,236 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup)
327
327
BOOST_CHECK (m_node.mempool ->exists (GenTxid::Txid (tx_child->GetHash ())));
328
328
}
329
329
}
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
+ }
330
562
BOOST_AUTO_TEST_SUITE_END ()
0 commit comments