forked from digibyte/digibyte
-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathdigidollar_rh33_mempool_relay_tests.cpp
More file actions
890 lines (789 loc) · 40.5 KB
/
Copy pathdigidollar_rh33_mempool_relay_tests.cpp
File metadata and controls
890 lines (789 loc) · 40.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
// Copyright (c) 2025 The DigiByte Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
/**
* RH-33: TRANSACTION RELAY & MEMPOOL ATTACKS — DEEP DIVE
*
* Final-round security audit focusing on novel attack vectors against
* DigiDollar transaction relay, mempool acceptance, eviction, and P2P sync.
*
* Attack vectors:
* 1. Mempool flooding with crafted DD txs to evict legitimate ones
* 2. RBF replacement of valid DD mints with invalid replacements
* 3. DD tx priority inversion via fee/weight manipulation
* 4. Relay bandwidth exhaustion via DD tx size characteristics
* 5. P2P mempool desync via DD state divergence
* 6. DD validation cost amplification (CPU DoS via ATMP)
* 7. DD version mask abuse for eviction priority gaming
* 8. Ancestor/descendant chain limits with DD txs
* 9. DD tx expiry race conditions
* 10. Fee sniping with DD transactions
*/
#include <consensus/amount.h>
#include <consensus/digidollar.h>
#include <digidollar/digidollar.h>
#include <digidollar/scripts.h>
#include <digidollar/validation.h>
#include <kernel/chainparams.h>
#include <policy/feerate.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <test/util/setup_common.h>
#include <util/rbf.h>
#include <util/strencodings.h>
#include <util/time.h>
#include <boost/test/unit_test.hpp>
#include <chrono>
#include <vector>
BOOST_FIXTURE_TEST_SUITE(digidollar_rh33_mempool_relay_tests, BasicTestingSetup)
// =============================================================================
// Helpers
// =============================================================================
/** Build a DD-versioned transaction with a specific tx type encoded */
static CMutableTransaction MakeDDTx(DigiDollar::DigiDollarTxType txType, int numInputs = 1, int numOutputs = 2)
{
CMutableTransaction tx;
// DD version: lower 16 bits = 0x0770, bits 24-31 = txType
tx.nVersion = static_cast<int32_t>((static_cast<uint32_t>(txType) << 24) | 0x00000770);
tx.vin.resize(numInputs);
for (int i = 0; i < numInputs; i++) {
tx.vin[i].prevout = COutPoint(uint256::ONE, i);
tx.vin[i].scriptSig = CScript() << std::vector<unsigned char>(72, 0x30);
tx.vin[i].nSequence = CTxIn::SEQUENCE_FINAL - 2; // Signal RBF opt-in
}
tx.vout.resize(numOutputs);
for (int i = 0; i < numOutputs; i++) {
tx.vout[i].nValue = 100000;
tx.vout[i].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0x01) << OP_EQUALVERIFY << OP_CHECKSIG;
}
return tx;
}
/** Build a regular (non-DD) transaction */
static CMutableTransaction MakeRegularTx(int numInputs = 1, int numOutputs = 1)
{
CMutableTransaction tx;
tx.nVersion = 2;
tx.vin.resize(numInputs);
for (int i = 0; i < numInputs; i++) {
tx.vin[i].prevout = COutPoint(uint256::ONE, i);
tx.vin[i].scriptSig = CScript() << std::vector<unsigned char>(72, 0x30);
tx.vin[i].nSequence = CTxIn::SEQUENCE_FINAL - 2;
}
tx.vout.resize(numOutputs);
for (int i = 0; i < numOutputs; i++) {
tx.vout[i].nValue = 100000;
tx.vout[i].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0x01) << OP_EQUALVERIFY << OP_CHECKSIG;
}
return tx;
}
/** Create a DD tx that passes IsStandardTx (version mask match) */
static CMutableTransaction MakeDDStandardTx(int32_t customVersion = 0) __attribute__((unused));
static CMutableTransaction MakeDDStandardTx(int32_t customVersion)
{
CMutableTransaction tx;
tx.nVersion = customVersion ? customVersion : 0x0D1D0770;
tx.vin.resize(1);
tx.vin[0].prevout = COutPoint(uint256::ONE, 0);
tx.vin[0].scriptSig = CScript() << std::vector<unsigned char>(72, 0x30);
tx.vout.resize(1);
tx.vout[0].nValue = 100000;
tx.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0x01) << OP_EQUALVERIFY << OP_CHECKSIG;
return tx;
}
// =============================================================================
// RH-33-01: MEMPOOL FLOODING — DD TX FEERATE EXPLOITATION
//
// ATTACK: DD transactions bypass dust checks, allowing 0-value outputs.
// An attacker creates many DD-versioned txs with 0-value outputs. These
// have artificially low fee-per-byte because the outputs contribute no
// value but the tx weight stays the same. In TrimToSize(), these txs
// are evicted FIRST (lowest descendant feerate), but the attacker can
// create them faster than they're evicted, potentially displacing
// legitimate DD txs that have similar fee characteristics.
//
// KEY INSIGHT: DD txs inherently have lower effective feerate because
// they carry 0-value token outputs + OP_RETURN metadata, increasing
// weight without increasing fee-relevant value. This makes ALL DD txs
// systematically disadvantaged in feerate-based eviction.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_01_mempool_flooding_feerate_exploitation)
{
// Demonstrate that DD txs have worse fee-to-weight ratio than regular txs
// of equivalent economic value, making them eviction targets
// Regular tx: 1 input, 1 output, minimal weight
CMutableTransaction regularTx = MakeRegularTx(1, 1);
regularTx.vout[0].nValue = 1000000; // 0.01 DGB
unsigned int regularWeight = GetTransactionWeight(CTransaction(regularTx));
// DD mint tx: 1 input, 3 outputs (collateral + DD token + OP_RETURN)
// The DD token output has nValue=0, OP_RETURN has nValue=0
CMutableTransaction ddMintTx = MakeDDTx(DigiDollar::DD_TX_MINT, 1, 3);
ddMintTx.vout[0].nValue = 1000000; // Collateral lock
ddMintTx.vout[1].nValue = 0; // DD token output (0 DGB)
ddMintTx.vout[1].scriptPubKey = CScript() << OP_1 << std::vector<unsigned char>(32, 0xDD);
ddMintTx.vout[2].nValue = 0; // OP_RETURN metadata
ddMintTx.vout[2].scriptPubKey = CScript() << OP_RETURN << std::vector<unsigned char>(40, 0xAA);
unsigned int ddWeight = GetTransactionWeight(CTransaction(ddMintTx));
// DD tx is heavier due to extra outputs
BOOST_CHECK_GT(ddWeight, regularWeight);
// For the same fee, DD tx has worse fee-per-weight
// This means DD txs are systematically evicted before regular txs
// in TrimToSize(), even though they carry the same economic value.
//
// FINDING: DD transactions are inherently disadvantaged in mempool
// priority. An attacker flooding with regular txs at the same fee
// will cause DD txs to be evicted first.
double regularFeePerWeight = 10000.0 / regularWeight; // Assume 10000 sat fee
double ddFeePerWeight = 10000.0 / ddWeight;
BOOST_CHECK_GT(regularFeePerWeight, ddFeePerWeight);
// Quantify the disadvantage
double disadvantageRatio = regularFeePerWeight / ddFeePerWeight;
// DD txs need to pay this much MORE in fees to achieve same priority
BOOST_CHECK_GT(disadvantageRatio, 1.0);
// SECURITY NOTE: This is an inherent design characteristic, not a bug.
// However, it means:
// 1. During high fee periods, DD txs get evicted disproportionately
// 2. An attacker can specifically target DD tx displacement
// 3. DD users must overpay fees relative to regular tx users
//
// RECOMMENDATION: Consider DD-aware eviction policy that accounts
// for the inherent weight overhead of DD metadata outputs.
}
// =============================================================================
// RH-33-02: RBF REPLACEMENT — DD MINT CANCELED BY NON-DD REPLACEMENT
//
// ATTACK: User creates a DD mint tx that signals RBF (nSequence < MAX-1).
// Attacker (or user themselves) creates a non-DD replacement spending the
// same input with higher fee. The DD mint is replaced, but the DD system
// has no record of the "canceled" mint. If any system tracked the pending
// mint (e.g., UI showing "minting in progress"), this creates confusion.
//
// DEEPER: What if the DD mint is in a CPFP chain? The child tx (spending
// DD change) gets evicted when the parent is replaced. If the child was
// a DD transfer, the entire DD operation chain is disrupted.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_02_rbf_dd_mint_replacement_by_nondd)
{
// DD mint signals RBF
CMutableTransaction ddMint = MakeDDTx(DigiDollar::DD_TX_MINT, 1, 3);
ddMint.vin[0].nSequence = CTxIn::SEQUENCE_FINAL - 2; // RBF signal
CTransaction ddMintTx(ddMint);
// Verify it signals RBF
BOOST_CHECK(SignalsOptInRBF(ddMintTx));
// Verify it has DD marker
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(ddMintTx));
// Non-DD replacement spending same input
CMutableTransaction replacement = MakeRegularTx(1, 1);
replacement.vin[0].prevout = ddMint.vin[0].prevout; // Same input!
replacement.vout[0].nValue = ddMint.vout[0].nValue - 1000; // Higher fee (less output)
CTransaction replacementTx(replacement);
// Replacement is NOT a DD tx
BOOST_CHECK(!DigiDollar::HasDigiDollarMarker(replacementTx));
// FINDING: RBF system has zero DD awareness. A valid DD mint can be
// replaced by any non-DD tx. The BIP125 rules only check:
// - Conflicting input exists
// - Replacement pays higher fee
// - Replacement pays for its own bandwidth
//
// There is NO check that says "you can't replace a DD tx with a non-DD tx"
// or vice versa. This is by design (RBF is input-conflict-based), but:
//
// RISK: Attacker who controls the same UTXO as a DD minter can cancel
// any pending DD mint by broadcasting a higher-fee non-DD replacement.
// In practice, this requires controlling the private key, so it's
// self-griefing — but it could be exploited in shared wallet scenarios
// or if a key is partially compromised.
//
// RECOMMENDATION: DD wallet software should use nSequence=MAX (no RBF)
// for DD mints to prevent replacement. Document this as best practice.
// Verify DD mint could use non-RBF sequence
CMutableTransaction ddMintNoRbf = MakeDDTx(DigiDollar::DD_TX_MINT);
ddMintNoRbf.vin[0].nSequence = CTxIn::SEQUENCE_FINAL; // No RBF
CTransaction ddMintNoRbfTx(ddMintNoRbf);
BOOST_CHECK(!SignalsOptInRBF(ddMintNoRbfTx));
// This is the safe approach — DD mints should NOT signal RBF
}
// =============================================================================
// RH-33-03: PRIORITY INVERSION — DD TX TYPE MASQUERADING
//
// ATTACK: DD transactions have different types (MINT, TRANSFER, REDEEM).
// The mempool treats all txs equally by feerate. But DD semantics imply
// different urgencies:
// - REDEEM during ERR should be high-priority (system health depends on it)
// - MINT during normal operations is routine
// - TRANSFER is user-to-user, variable urgency
//
// An attacker can flood the mempool with low-fee DD MINTs, pushing out
// high-urgency DD REDEEMs that happen to have similar fees.
//
// DEEPER: During ERR (Emergency Redemption Ratio), the system needs
// redemptions to process to restore health. If an attacker prevents
// redemptions from entering the mempool, the system stays unhealthy.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_03_priority_inversion_dd_tx_types)
{
// Create a DD REDEEM (should be high priority during ERR)
CMutableTransaction ddRedeem = MakeDDTx(DigiDollar::DD_TX_REDEEM, 1, 2);
CTransaction ddRedeemTx(ddRedeem);
BOOST_CHECK_EQUAL(DigiDollar::GetDigiDollarTxType(ddRedeemTx),
DigiDollar::DD_TX_REDEEM);
// Create a DD MINT (routine operation)
CMutableTransaction ddMint = MakeDDTx(DigiDollar::DD_TX_MINT, 1, 3);
CTransaction ddMintTx(ddMint);
BOOST_CHECK_EQUAL(DigiDollar::GetDigiDollarTxType(ddMintTx),
DigiDollar::DD_TX_MINT);
// Both have the same weight class
unsigned int redeemWeight [[maybe_unused]] = GetTransactionWeight(ddRedeemTx);
unsigned int mintWeight [[maybe_unused]] = GetTransactionWeight(ddMintTx);
// For same fee, they have similar priority in mempool
// There is NO DD-type-aware priority boost for REDEEMs
//
// FINDING: During ERR, the system should be processing redemptions
// to restore collateral health. But the mempool doesn't prioritize
// DD REDEEMs over DD MINTs. Worse, ValidateMintTransaction already
// blocks mints during ERR (ShouldBlockMintingDuringERR), but the
// validation cost is still paid before rejection.
//
// ATTACK SCENARIO:
// 1. System enters ERR (collateral < 100%)
// 2. Minting is blocked by consensus
// 3. Attacker floods with DD MINT txs that will ALL fail validation
// 4. Each mint triggers expensive ATMP validation (oracle lookup, etc.)
// 5. Legitimate REDEEMs compete for validation CPU time
// 6. System health recovery is delayed
//
// RECOMMENDATION: Add early rejection of DD MINTs in PreChecks when
// system is in ERR state, before expensive DD validation.
// Verify the version encoding distinguishes types
BOOST_CHECK_NE(ddRedeemTx.nVersion, ddMintTx.nVersion);
// But the mempool has no mechanism to prefer one over the other
// based on DD type. This is a design gap.
}
// =============================================================================
// RH-33-04: RELAY BANDWIDTH EXHAUSTION — DD TX OVERHEAD
//
// ATTACK: DD transactions are structurally larger than regular txs due to:
// - OP_RETURN metadata output (up to 80 bytes of data)
// - DD token output (P2TR + 32-byte commitment)
// - Larger version field interpretation
//
// An attacker crafts DD txs that maximize size while minimizing fee,
// exploiting the DD dust exemption to include 0-value outputs that
// inflate tx weight without paying proportional fees.
//
// Each DD tx relayed to N peers multiplies bandwidth. If DD txs are
// systematically larger, the relay cost per tx is higher.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_04_relay_bandwidth_dd_tx_overhead)
{
// Measure overhead of DD tx structure vs regular tx
// Minimal regular tx
CMutableTransaction minTx = MakeRegularTx(1, 1);
minTx.vout[0].nValue = 100000;
unsigned int minWeight = GetTransactionWeight(CTransaction(minTx));
// DD tx with token output + OP_RETURN (typical mint structure)
CMutableTransaction ddTx = MakeDDTx(DigiDollar::DD_TX_MINT, 1, 3);
ddTx.vout[0].nValue = 100000; // Collateral
ddTx.vout[1].nValue = 0; // DD token (0 value, P2TR-like)
ddTx.vout[1].scriptPubKey = CScript() << OP_1 << std::vector<unsigned char>(32, 0xDD);
ddTx.vout[2].nValue = 0; // OP_RETURN with max standard data
ddTx.vout[2].scriptPubKey = CScript() << OP_RETURN
<< std::vector<unsigned char>(80, 0xBB); // Max OP_RETURN data
unsigned int ddWeight = GetTransactionWeight(CTransaction(ddTx));
// Calculate overhead percentage
double overheadPercent = ((double)(ddWeight - minWeight) / minWeight) * 100.0;
// DD tx should be significantly heavier
BOOST_CHECK_GT(ddWeight, minWeight);
// Document the overhead for security assessment
// Typical overhead is 30-50% for DD mint vs regular P2PKH
BOOST_CHECK_GT(overheadPercent, 20.0); // At least 20% heavier
// Now test: DD tx with maximum OP_RETURN still passes standardness
std::string reason;
bool isStandard [[maybe_unused]] = IsStandardTx(CTransaction(ddTx),
MAX_OP_RETURN_RELAY, true, CFeeRate(DUST_RELAY_TX_FEE), reason);
// DD tx with 0-value outputs should pass if DD token script check passes
// (may fail if IsDDTokenScript returns false for our test script)
// The point is: the DD dust exemption allows 0-value outputs through
// standardness, increasing relay bandwidth per tx.
// FINDING: DD transactions impose ~30-50% more relay bandwidth than
// equivalent regular txs. Over thousands of txs, this significantly
// increases network bandwidth costs. Combined with the feerate
// disadvantage from RH-33-01, DD txs are doubly penalized:
// more expensive to relay AND easier to evict.
//
// ATTACK: Send 1000 DD-version txs (not actually valid DD, just
// version mask match) with 0-value outputs to waste relay bandwidth.
// Each peer validates, relays, then eventually rejects at consensus.
// Verify MAX_STANDARD_TX_WEIGHT still limits DD txs
CMutableTransaction hugeDDTx = MakeDDTx(DigiDollar::DD_TX_MINT, 1, 100);
for (int i = 0; i < 100; i++) {
hugeDDTx.vout[i].nValue = 0;
hugeDDTx.vout[i].scriptPubKey = CScript() << OP_1
<< std::vector<unsigned char>(32, 0xDD);
}
unsigned int hugeWeight = GetTransactionWeight(CTransaction(hugeDDTx));
// Even DD txs are bounded by MAX_STANDARD_TX_WEIGHT
// This IS correctly enforced (checked in RH-17-10)
if (hugeWeight > MAX_STANDARD_TX_WEIGHT) {
std::string hugeReason;
BOOST_CHECK(!IsStandardTx(CTransaction(hugeDDTx),
MAX_OP_RETURN_RELAY, true, CFeeRate(DUST_RELAY_TX_FEE), hugeReason));
BOOST_CHECK_EQUAL(hugeReason, "tx-size");
}
}
// =============================================================================
// RH-33-05: P2P MEMPOOL DESYNC — DD STATE DIVERGENCE
//
// ATTACK: Different nodes may have different oracle prices cached at any
// moment. DD validation in ATMP uses GetOraclePriceForTransaction() which
// reads from local oracle cache. If Node A has price X and Node B has
// price Y (both valid within priceValidBlocks window), then:
// - A DD mint requiring collateral ratio C might be valid on A but not B
// - Node A accepts and relays; Node B rejects
// - Mempool contents diverge
//
// This creates a situation where:
// 1. Block producers mining Node A's mempool include the DD tx
// 2. Block producers mining Node B's mempool don't
// 3. When Node A mines a block with the DD tx, Node B MUST accept it
// (consensus validation uses block-extracted oracle price, not local cache)
// 4. But until that block, nodes disagree on mempool contents
//
// This is expected behavior for policy vs consensus divergence, BUT:
// An attacker can exploit it to create DD txs that are accepted by some
// peers but not others, fragmenting the relay network.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_05_p2p_mempool_desync_oracle_price)
{
// Demonstrate oracle price sensitivity in DD validation
const DigiDollar::ConsensusParams ddParams;
// Two different oracle prices (both hypothetically valid)
CAmount priceA = 6310; // $0.00631 per DGB
CAmount priceB = 6000; // $0.00600 per DGB
// For the same lock time, different prices yield different collateral requirements
int64_t lockBlocks = DigiDollar::LockDaysToBlocks(30); // 30 days
int baseRatio = DigiDollar::GetCollateralRatioForLockTime(lockBlocks, ddParams);
// At price A: required DGB for $100 DD
// $100 DD = 10000 cents
// Required collateral = ddAmount * ratio * (1e8 / priceMicroUSD)
// At 500% ratio, $100 DD, price $0.00631:
// = 10000 * 500/100 * (100000000 / 6310) = 10000 * 5 * 15847 = 792,350,000 sat
// At price $0.00600:
// = 10000 * 500/100 * (100000000 / 6000) = 10000 * 5 * 16666 = 833,300,000 sat
// The difference in required collateral between price A and B
// A tx with collateral between these two values would be:
// - Valid at price A (enough collateral)
// - Invalid at price B (insufficient collateral)
// FINDING: A ~5% oracle price difference creates a window where
// a DD tx is valid on some nodes but not others. This is inherent
// to any system with decentralized price feeds, but the size of
// the window depends on priceValidBlocks (20 blocks = 5 minutes).
//
// ATTACK SCENARIO:
// 1. Monitor oracle price feed
// 2. When price drops 3-5%, quickly broadcast DD mints with collateral
// calibrated to the OLD (higher) price
// 3. Nodes with stale price accept; nodes with fresh price reject
// 4. Creates network fragmentation
//
// MITIGATION: The priceValidBlocks window of 20 blocks (5 min) limits
// exposure. But during volatile periods, this could be exploited.
//
// RECOMMENDATION: Consider using the MINIMUM price seen in the
// validity window for mempool acceptance (more conservative), while
// keeping exact block price for consensus validation.
BOOST_CHECK_GT(baseRatio, 100); // Sanity: ratio > 100%
BOOST_CHECK_GT(priceA, priceB); // Price A > Price B
// Collateral required at price B > collateral required at price A
// (lower price = need more DGB for same USD value)
}
// =============================================================================
// RH-33-06: DD VALIDATION COST AMPLIFICATION (CPU DoS)
//
// ATTACK: DD tx validation in ATMP is significantly more expensive than
// regular tx validation because it involves:
// 1. HasDigiDollarMarker() — cheap, version field check
// 2. IsDigiDollarEnabled() — BIP9 deployment check
// 3. GetOraclePriceForTransaction() — oracle price lookup
// 4. Block database reads (txLookup lambda for DD amount extraction)
// 5. ValidateDigiDollarTransaction() — full DD consensus validation
//
// An attacker creates txs that PASS the cheap checks (version marker)
// but require maximum work before failing. The worst case is a tx that
// passes all checks up to step 4 (block DB read) before failing in step 5.
//
// Cost asymmetry: attacker pays minimal fee, defender (node) does
// significant I/O + CPU work before rejecting.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_06_dd_validation_cost_amplification)
{
// Create a tx that triggers DD validation path
CMutableTransaction costlyTx = MakeDDTx(DigiDollar::DD_TX_MINT, 1, 3);
CTransaction ctx(costlyTx);
// Verify it triggers DD path
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(ctx));
BOOST_CHECK_EQUAL(DigiDollar::GetDigiDollarTxType(ctx), DigiDollar::DD_TX_MINT);
// Now create a tx that has DD marker but INVALID type
CMutableTransaction badTypeTx;
// Use type 0xFF (> DD_TX_MAX), which passes HasDigiDollarMarker but
// GetDigiDollarTxType returns DD_TX_NONE
badTypeTx.nVersion = static_cast<int32_t>(0xFF1D0770);
badTypeTx.vin.resize(1);
badTypeTx.vin[0].prevout = COutPoint(uint256::ONE, 0);
badTypeTx.vin[0].scriptSig = CScript() << std::vector<unsigned char>(72, 0x30);
badTypeTx.vout.resize(1);
badTypeTx.vout[0].nValue = 100000;
badTypeTx.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0x01) << OP_EQUALVERIFY << OP_CHECKSIG;
CTransaction badCtx(badTypeTx);
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(badCtx));
// Type extraction should return NONE for invalid type byte
BOOST_CHECK_EQUAL(DigiDollar::GetDigiDollarTxType(badCtx), DigiDollar::DD_TX_NONE);
// FINDING: A tx with DD marker but invalid type byte (>= DD_TX_MAX)
// still enters the DD validation path in ATMP. The validation path:
// 1. HasDigiDollarMarker → true (lower 16 bits match)
// 2. IsDigiDollarEnabled check → passes if DD is active
// 3. Oracle price lookup → DONE (wasted work!)
// 4. Block DB tx lookup lambda created → DONE (wasted work!)
// 5. ValidateDigiDollarTransaction → should reject early for invalid type
//
// RECOMMENDATION: Add early type validation BEFORE oracle price lookup.
// In ATMP, after HasDigiDollarMarker, immediately check:
// auto txType = GetDigiDollarTxType(tx);
// if (txType == DD_TX_NONE) return state.Invalid(...);
// This avoids oracle + block DB work for trivially invalid DD txs.
// Test the range of invalid type bytes
for (uint32_t typeVal = DigiDollar::DD_TX_MAX; typeVal <= 0xFF; typeVal += 0x10) {
CMutableTransaction t;
t.nVersion = static_cast<int32_t>((typeVal << 24) | 0x001D0770);
t.vin.resize(1);
t.vin[0].prevout = COutPoint(uint256::ONE, 0);
t.vin[0].scriptSig = CScript() << std::vector<unsigned char>(72, 0x30);
t.vout.resize(1);
t.vout[0].nValue = 100000;
t.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0x01) << OP_EQUALVERIFY << OP_CHECKSIG;
CTransaction ct(t);
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(ct));
BOOST_CHECK_EQUAL(DigiDollar::GetDigiDollarTxType(ct), DigiDollar::DD_TX_NONE);
// All of these enter DD validation path and waste oracle/DB work
}
}
// =============================================================================
// RH-33-07: VERSION MASK EVICTION GAMING
//
// ATTACK: The version mask 0x0000FFFF means bits 16-23 are "flags" that
// don't affect DD detection but DO affect the version field value.
// An attacker can create 256 different "DD" version values per tx type
// by varying bits 16-23. Combined with the dust exemption, this allows
// creating many 0-value-output txs that look like DD txs to the policy
// layer but are all different versions.
//
// While this alone isn't dangerous (they still get rejected at consensus),
// it means the attacker can craft txs that fingerprint which nodes
// have strict vs lenient DD version checking.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_07_version_mask_flag_bits_fingerprinting)
{
const int32_t DD_MARKER = 0x0770;
// Generate all possible flag byte values (bits 16-23)
int matchCount = 0;
for (uint32_t flags = 0; flags <= 0xFF; flags++) {
int32_t version = static_cast<int32_t>((flags << 16) | DD_MARKER);
CMutableTransaction tx;
tx.nVersion = version;
tx.vin.resize(1);
tx.vin[0].prevout = COutPoint(uint256::ONE, 0);
tx.vin[0].scriptSig = CScript() << std::vector<unsigned char>(72, 0x30);
tx.vout.resize(1);
tx.vout[0].nValue = 100000;
tx.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0x01) << OP_EQUALVERIFY << OP_CHECKSIG;
CTransaction ctx(tx);
// ALL of these match the DD version mask
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(ctx));
matchCount++;
}
// 256 distinct version values all trigger DD code path
BOOST_CHECK_EQUAL(matchCount, 256);
// Combined with type byte (bits 24-31), there are 256 * 256 = 65536
// distinct version values that all trigger DD detection.
// Of those, only types 1-3 are valid DD types.
// So 65536 - 768 = 64768 version values trigger DD validation path
// but are ultimately invalid → wasted work.
//
// FINDING: The version mask is too permissive. Only bits 16-23
// should be checked as "flags" if they have defined meanings.
// Currently they're ignored by HasDigiDollarMarker but still
// cause policy-layer DD treatment.
//
// RECOMMENDATION: Tighten the mask. If bits 16-23 have no defined
// use, reject DD txs where those bits are set. This reduces the
// attack surface from 65536 to 768 version values (3 valid types
// × 256 flag combinations, or even fewer if flags are constrained).
}
// =============================================================================
// RH-33-08: ANCESTOR/DESCENDANT CHAIN LIMITS WITH DD TXS
//
// ATTACK: Bitcoin Core limits ancestor/descendant chains to 25 txs.
// A DD MINT creates a collateral UTXO and a DD token UTXO. Both can
// be spent in child txs:
// - Collateral UTXO: spent in REDEEM
// - DD token UTXO: spent in TRANSFER
//
// An attacker creates a chain: MINT → TRANSFER → TRANSFER → ... → TRANSFER
// Each TRANSFER spends the DD token from the previous one. After 24
// transfers, the chain hits the ancestor limit. The 25th TRANSFER
// (or the REDEEM) is rejected.
//
// This means an attacker can "lock" a DD token in a long unconfirmed
// chain, preventing the owner from redeeming their collateral because
// the REDEEM would reference a UTXO deep in the chain.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_08_ancestor_chain_dd_locking)
{
// Demonstrate chain building with DD txs
const int MAX_CHAIN = 25; // DEFAULT_ANCESTOR_LIMIT
// Build a chain of DD TRANSFER txs
std::vector<CMutableTransaction> chain;
uint256 prevTxid = uint256::ONE;
uint32_t prevVout = 0;
for (int i = 0; i < MAX_CHAIN; i++) {
CMutableTransaction tx = MakeDDTx(DigiDollar::DD_TX_TRANSFER, 1, 2);
tx.vin[0].prevout = COutPoint(prevTxid, prevVout);
// DD token output
tx.vout[0].nValue = 0;
tx.vout[0].scriptPubKey = CScript() << OP_1
<< std::vector<unsigned char>(32, static_cast<unsigned char>(i));
// Change output
tx.vout[1].nValue = 100000 - (i * 1000); // Decreasing for fees
CTransaction ctx(tx);
prevTxid = ctx.GetHash();
prevVout = 0; // DD token is output 0
chain.push_back(tx);
}
BOOST_CHECK_EQUAL(chain.size(), (size_t)MAX_CHAIN);
// The 25th tx in the chain would be rejected by ancestor limits
// If this chain is all DD TRANSFERs, the DD token is effectively
// "stuck" until a block confirms some of the chain.
//
// ATTACK SCENARIO:
// 1. Alice mints DD tokens
// 2. Attacker intercepts and creates rapid TRANSFER chain
// (requires controlling Alice's key — or Alice does it herself
// to grief the system)
// 3. The collateral UTXO is NOT spent in this chain (it's timelocked)
// 4. But the DD token is bounced through 24 transfers
// 5. Any further operation on this DD token is blocked by chain limits
//
// MITIGATION: This is self-griefing (requires own key), but could
// affect shared wallet scenarios. Standard ancestor limits apply.
//
// NOTE: This is the same attack as with any Bitcoin tx chain.
// DD doesn't make it worse, but DD's collateral lockup makes the
// CONSEQUENCE worse: stuck DD tokens mean stuck collateral.
}
// =============================================================================
// RH-33-09: DD TX EXPIRY RACE — MEMPOOL TIME-BASED EVICTION
//
// ATTACK: CTxMemPool::Expire() removes txs older than mempoolExpiry
// (default 336 hours = 14 days). A DD MINT that sits in the mempool
// for 14 days gets evicted. But the timelocked collateral UTXO is
// still unspent in the UTXO set. The user's funds aren't lost, but
// they may think the mint "happened" if they only check mempool.
//
// DEEPER: If the user's wallet tracks the pending mint and the tx is
// evicted, the wallet state becomes inconsistent. The user needs to
// re-broadcast or create a new mint.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_09_dd_tx_mempool_expiry)
{
// This is a documentation/awareness test rather than code test
// since we can't easily simulate mempool expiry in BasicTestingSetup
// Default mempool expiry: 336 hours (14 days)
const int DEFAULT_MEMPOOL_EXPIRY_HOURS = 336;
// DD lock times range from 1 hour (240 blocks) to 10 years
// A DD mint with 1-hour lock that sits in mempool for 14 days
// means the intended lock period has LONG expired before the
// mint even confirms!
int64_t oneHourLock = 240; // blocks
int64_t fourteenDaysBlocks = 14 * DigiDollar::BLOCKS_PER_DAY;
// The lock time is relative to CONFIRMATION, not broadcast
// So a 1-hour lock mint that takes 14 days to confirm still
// locks for 1 hour FROM confirmation — this is correct.
// But if the mint is evicted after 14 days, the user loses
// their fee and must re-create the transaction.
BOOST_CHECK_GT(fourteenDaysBlocks, oneHourLock);
// FINDING: No special concern here beyond standard mempool expiry.
// DD mints are not uniquely affected. However, DD wallet software
// should monitor mempool status of pending mints and alert users
// if a mint is about to expire from mempool.
//
// RECOMMENDATION: DD wallet should auto-rebroadcast pending mints
// with fee bumping if they've been in mempool > 24 hours.
BOOST_CHECK_EQUAL(DEFAULT_MEMPOOL_EXPIRY_HOURS, 336);
}
// =============================================================================
// RH-33-10: MARKER-ONLY VALIDATION BYPASS — POLICY/CONSENSUS GAP
//
// ATTACK: In policy (IsStandardTx), DD detection uses:
// (tx.nVersion & 0x0000FFFF) == (0x0D1D0770 & 0x0000FFFF) = 0x0770
//
// In consensus (HasDigiDollarMarker in digidollar.cpp), DD detection uses:
// (tx.nVersion & 0x0000FFFF) == (0x0D1D0770 & 0x0000FFFF) = 0x0770
//
// SAME MASK! Good — no policy/consensus gap in version detection.
// BUT: The policy layer gives dust exemptions based on DD marker alone.
// The consensus layer validates the actual DD content (outputs, amounts).
// So a tx can pass policy (IsStandard) with dust outputs due to DD marker,
// enter the mempool, but fail consensus in ConnectBlock.
//
// This creates a mempool poisoning vector: the tx takes up space in the
// mempool, gets relayed to peers, wastes bandwidth, and is never mined.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_10_policy_consensus_dust_gap)
{
// Create a tx with DD version marker but NO valid DD content
CMutableTransaction fakeDDTx;
fakeDDTx.nVersion = 0x011D0770; // DD marker, type=1 (MINT)
fakeDDTx.vin.resize(1);
fakeDDTx.vin[0].prevout = COutPoint(uint256::ONE, 0);
fakeDDTx.vin[0].scriptSig = CScript() << std::vector<unsigned char>(72, 0x30);
// Regular P2PKH output with dust value
fakeDDTx.vout.resize(1);
fakeDDTx.vout[0].nValue = 100000; // Non-dust, standard value
CTransaction fakeTx(fakeDDTx);
// This tx has DD marker
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(fakeTx));
// With canonical version 0x0D1D0770, type byte is 0x0D (13), which is >= DD_TX_MAX(4)
// So GetDigiDollarTxType returns DD_TX_NONE — but HasDigiDollarMarker is true!
// This means policy treats it as DD (dust bypass, version bypass) but
// consensus type extraction says "not a valid DD type"
// Document: canonical DD version type extraction
auto fakeTxType = DigiDollar::GetDigiDollarTxType(fakeTx);
BOOST_TEST_MESSAGE("DD version 0x0D1D0770 type: " << static_cast<int>(fakeTxType));
// Check standardness
std::string reason;
bool standard = IsStandardTx(fakeTx, MAX_OP_RETURN_RELAY, true, CFeeRate(DUST_RELAY_TX_FEE), reason);
BOOST_TEST_MESSAGE("IsStandardTx: " << standard << " reason: " << reason);
// But it would fail DD consensus validation (wrong outputs, no collateral,
// no DD token output, no OP_RETURN metadata)
//
// FINDING: A tx with DD version marker and type byte but completely
// wrong structure passes policy, enters mempool, and is relayed.
// It fails consensus validation when a miner tries to include it,
// and it fails ATMP DD validation... BUT only if DD is activated.
//
// CRITICAL: If DD is NOT yet activated (BIP9 not signaled), then
// ATMP rejects with "digidollar-not-active". But the tx STILL passes
// IsStandardTx and consumes policy-layer resources.
//
// Before DD activation: These txs pass standardness but fail ATMP.
// After DD activation: These txs pass standardness, enter DD validation
// in ATMP, consume oracle/DB resources, then fail.
//
// RECOMMENDATION: IsStandardTx should validate minimal DD structure
// (correct output count for type) in addition to version marker.
// This filters obviously invalid DD txs BEFORE ATMP.
}
// =============================================================================
// RH-33-11: DD TRANSFER CONSERVATION LAW — MEMPOOL DOUBLE-SPEND
//
// ATTACK: DD TRANSFERs must conserve DD value (inputs = outputs).
// In mempool, if two conflicting TRANSFER txs spend the same DD token
// UTXO but create different output distributions, both might be
// independently valid. The mempool accepts the first seen; a miner
// might include either.
//
// This isn't a bug (it's normal double-spend behavior), but with DD:
// - Transfer A sends DD to Alice
// - Transfer B sends same DD to Bob
// - Only one confirms, but both are "valid" DD txs
//
// If a DD-aware system tracks "pending transfers" and shows both
// as pending, users see inconsistent state.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_11_dd_transfer_double_spend_confusion)
{
// Two DD TRANSFERs spending same DD token input
CMutableTransaction transferA = MakeDDTx(DigiDollar::DD_TX_TRANSFER, 1, 2);
CMutableTransaction transferB = MakeDDTx(DigiDollar::DD_TX_TRANSFER, 1, 2);
// Same input (same DD token UTXO)
transferA.vin[0].prevout = COutPoint(uint256::ONE, 0);
transferB.vin[0].prevout = COutPoint(uint256::ONE, 0);
// Different recipients
transferA.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0xAA) << OP_EQUALVERIFY << OP_CHECKSIG;
transferB.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160
<< std::vector<unsigned char>(20, 0xBB) << OP_EQUALVERIFY << OP_CHECKSIG;
CTransaction txA(transferA);
CTransaction txB(transferB);
// Both are valid DD transfers (structurally)
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(txA));
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(txB));
BOOST_CHECK_EQUAL(DigiDollar::GetDigiDollarTxType(txA), DigiDollar::DD_TX_TRANSFER);
BOOST_CHECK_EQUAL(DigiDollar::GetDigiDollarTxType(txB), DigiDollar::DD_TX_TRANSFER);
// They conflict (same input)
BOOST_CHECK(txA.vin[0].prevout.hash == txB.vin[0].prevout.hash);
BOOST_CHECK_EQUAL(txA.vin[0].prevout.n, txB.vin[0].prevout.n);
// Standard double-spend rules apply — first seen wins
// No DD-specific handling needed here; this is by design.
//
// FINDING: Standard behavior, no vulnerability. But DD wallets
// should NOT show pending transfers as "confirmed" until in a block.
}
// =============================================================================
// RH-33-12: NOVEL — MULTI-INPUT DD TX WITH MIXED DD/NON-DD INPUTS
//
// ATTACK: Can an attacker create a DD TRANSFER tx that spends both
// DD token inputs AND regular DGB inputs? If so:
// - The DD conservation law should only count DD inputs/outputs
// - The DGB inputs provide fee payment
// - But what if the attacker misattributes a DGB input as a DD input?
//
// This tests the boundary between DD and non-DD UTXOs in validation.
// =============================================================================
BOOST_AUTO_TEST_CASE(rh33_12_mixed_input_dd_transfer)
{
// DD TRANSFER with 2 inputs: 1 DD token + 1 regular DGB (for fees)
CMutableTransaction mixedTx = MakeDDTx(DigiDollar::DD_TX_TRANSFER, 2, 3);
// Input 0: DD token UTXO (created by previous MINT)
mixedTx.vin[0].prevout = COutPoint(uint256::ONE, 1); // DD token output from mint
// Input 1: Regular DGB UTXO (for fee payment)
mixedTx.vin[1].prevout = COutPoint(uint256::ZERO, 0); // Regular DGB
// Output 0: DD token to recipient (0 DGB value)
mixedTx.vout[0].nValue = 0;
mixedTx.vout[0].scriptPubKey = CScript() << OP_1
<< std::vector<unsigned char>(32, 0xDD);
// Output 1: Change from DGB fee input
mixedTx.vout[1].nValue = 90000; // Fee input minus fee
// Output 2: OP_RETURN with DD metadata
mixedTx.vout[2].nValue = 0;
mixedTx.vout[2].scriptPubKey = CScript() << OP_RETURN
<< std::vector<unsigned char>(20, 0xAA);
CTransaction ctx(mixedTx);
// Has DD marker
BOOST_CHECK(DigiDollar::HasDigiDollarMarker(ctx));
// FINDING: The DD validation must correctly distinguish which inputs
// are DD token inputs and which are regular DGB fee inputs. If it
// doesn't, an attacker could:
// 1. Claim a regular DGB input is a DD token input
// 2. Create DD outputs with value "from" the DGB input
// 3. Effectively create DD tokens from nothing (inflation bug)
//
// ValidateTransferTransaction must check each input against the UTXO
// set to determine if it's a DD token UTXO (by checking if the
// previous output's script is IsDDTokenScript).
//
// CRITICAL: If this check is missing or bypassable, it's an
// infinite DD creation vulnerability.
//
// Current implementation (from validation.cpp): The coins view lookup
// in ATMP provides the previous outputs. ValidateTransferTransaction
// must use IsDDTokenScript on each input's previous output to identify
// DD vs non-DD inputs. This is enforced by the validation context
// having the coins view.
}
BOOST_AUTO_TEST_SUITE_END()