forked from digibyte/digibyte
-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathdigidollar_prune_blockdb.cpp
More file actions
483 lines (426 loc) · 21.5 KB
/
Copy pathdigidollar_prune_blockdb.cpp
File metadata and controls
483 lines (426 loc) · 21.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
// Copyright (c) 2026 The DigiByte Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Fuzz targets for the v9.26.4 DigiDollar-compatible pruning surface.
//
// Targets:
// dd_extract_amount_blockdb - DigiDollar::ExtractDDAmountFromBlockDb(), the
// txindex-free DD amount resolution path that reads the creating
// transaction out of the retained block via a TxLookupFn callback
// (src/digidollar/validation.cpp; wired up in production by
// MakeCachedBlockTxLookup in src/validation.cpp).
// dd_prune_activation_floor - property fuzz of the shared dd_floor helper
// DigiDollar::EarliestActivationFloor (src/consensus/digidollar.cpp),
// the single source of truth used by the "digidollar" prune lock in
// src/node/chainstate.cpp, the startup guards in
// src/digidollar/health.cpp and src/oracle/bundle_manager.cpp, and
// validation's EarliestDigiDollarActivationHeight — plus the inline
// prune-lock clamp arithmetic used by Chainstate::FlushStateToDisk in
// src/validation.cpp (still replicated below; update if it changes).
// (unit coverage: src/test/digidollar_txindex_tests.cpp and
// test/functional/feature_digidollar_pruning.py).
// dd_prune_coin_gating - SpendsDigiDollarCollateralVault() /
// RequiresDigiDollarValidation() on MAINNET params with coins created
// around the DigiDollar activation floor and a fuzzed block-db lookup.
// This drives the production EarliestDigiDollarActivationHeight
// pre-floor coin skip and the LookupPreviousTransaction txLookup
// fallback, i.e. the exact reason pruning below the floor is safe.
//
#include <chainparams.h>
#include <coins.h>
#include <consensus/amount.h>
#include <consensus/digidollar.h>
#include <consensus/params.h>
#include <digidollar/digidollar.h>
#include <digidollar/scripts.h>
#include <digidollar/validation.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h>
#include <uint256.h>
#include <util/chaintype.h>
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <limits>
#include <optional>
#include <vector>
namespace {
//! Mirrors MEMPOOL_HEIGHT from src/txmempool.h without pulling in the mempool.
constexpr uint32_t FUZZ_MEMPOOL_HEIGHT{0x7FFFFFFF};
//! Mirrors PRUNE_LOCK_BUFFER from src/validation.cpp.
constexpr int FUZZ_PRUNE_LOCK_BUFFER{10};
//! Buried "deployment disabled" sentinel (the old NEVER_ACTIVE equivalent).
constexpr int DISABLED_HEIGHT = std::numeric_limits<int>::max();
/** Build a DD-versioned mutable transaction with the given tx type byte. */
CMutableTransaction MakeDDTx(uint8_t txTypeByte)
{
CMutableTransaction mtx;
// DD version format: txType(8) | flags(8) | 0x0770 (lower 16)
mtx.nVersion = (static_cast<int32_t>(txTypeByte) << 24) | 0x0770;
return mtx;
}
/** Build a P2TR-shaped script (OP_1 + 32 fuzzed bytes). */
CScript MakeP2TR(FuzzedDataProvider& fdp)
{
std::vector<uint8_t> payload = fdp.ConsumeBytes<uint8_t>(32);
if (payload.size() < 32) payload.resize(32, 0xAB);
CScript s;
s << OP_1 << payload;
return s;
}
/** Build a DD-shaped "creating transaction" that a block-db lookup would return.
*
* shape 1: mint-shaped - OP_RETURN <DD> <1> <amount> <lockHeight> <lockTier>,
* one value>0 P2TR collateral output, one zero-value
* P2TR DD token output.
* shape 2: transfer-shaped - OP_RETURN <DD> <2> <amount>..., N zero-value P2TR outputs.
* shape 3: corrupted - fuzzed type byte / duplicate DD OP_RETURNs /
* optionally coinbase-shaped (T5-02 rejection path).
*/
CTransactionRef BuildDDShapedPrevTx(FuzzedDataProvider& fdp, uint8_t shape)
{
const std::vector<unsigned char> dd_marker = {'D', 'D'};
if (shape == 1) {
CMutableTransaction mtx = MakeDDTx(DigiDollar::DD_TX_MINT);
mtx.vin.emplace_back(COutPoint(uint256::ONE, 0));
// Collateral output (value > 0 P2TR)
mtx.vout.emplace_back(fdp.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY), MakeP2TR(fdp));
// DD token output (zero-value P2TR)
mtx.vout.emplace_back(0, MakeP2TR(fdp));
// Mint OP_RETURN: DD <type=1> <ddAmount> <lockHeight> <lockTier>
CScript opret;
opret << OP_RETURN << dd_marker << CScriptNum(DigiDollar::DD_TX_MINT);
opret << CScriptNum(fdp.ConsumeIntegral<int64_t>()); // ddAmount (any value)
opret << CScriptNum(fdp.ConsumeIntegralInRange<int64_t>(0, 30'000'000)); // lockHeight
opret << CScriptNum(fdp.ConsumeIntegralInRange<int64_t>(0, 9)); // lockTier
mtx.vout.emplace_back(0, opret);
return MakeTransactionRef(std::move(mtx));
}
if (shape == 2) {
CMutableTransaction mtx = MakeDDTx(DigiDollar::DD_TX_TRANSFER);
mtx.vin.emplace_back(COutPoint(uint256::ONE, 0));
const int splits = fdp.ConsumeIntegralInRange<int>(1, 4);
for (int i = 0; i < splits; i++) {
mtx.vout.emplace_back(0, MakeP2TR(fdp));
}
// Transfer OP_RETURN: DD <type=2> <amount1> ... <amountN>
CScript opret;
opret << OP_RETURN << dd_marker << CScriptNum(DigiDollar::DD_TX_TRANSFER);
for (int i = 0; i < splits; i++) {
opret << CScriptNum(fdp.ConsumeIntegral<int64_t>());
}
mtx.vout.emplace_back(0, opret);
return MakeTransactionRef(std::move(mtx));
}
// shape 3: corrupted / adversarial
CMutableTransaction mtx = MakeDDTx(fdp.ConsumeIntegral<uint8_t>());
if (fdp.ConsumeBool()) {
// Coinbase-shaped source tx (must be rejected as a DD source, T5-02)
mtx.vin.emplace_back(COutPoint(uint256(), std::numeric_limits<uint32_t>::max()));
} else {
mtx.vin.emplace_back(COutPoint(uint256::ONE, 0));
}
const int n_out = fdp.ConsumeIntegralInRange<int>(0, 5);
for (int i = 0; i < n_out; i++) {
const uint8_t kind = fdp.ConsumeIntegralInRange<uint8_t>(0, 2);
if (kind == 0) {
mtx.vout.emplace_back(fdp.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY), MakeP2TR(fdp));
} else if (kind == 1) {
// DD OP_RETURN with a fuzzed type byte and fuzzed pushes (may
// mismatch the version type, may appear more than once).
CScript opret;
opret << OP_RETURN << dd_marker << CScriptNum(fdp.ConsumeIntegral<uint8_t>());
const int pushes = fdp.ConsumeIntegralInRange<int>(0, 3);
for (int p = 0; p < pushes; p++) {
opret << CScriptNum(fdp.ConsumeIntegral<int64_t>());
}
mtx.vout.emplace_back(0, opret);
} else {
const auto raw = ConsumeRandomLengthByteVector(fdp, 48);
mtx.vout.emplace_back(fdp.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY),
CScript(raw.begin(), raw.end()));
}
}
return MakeTransactionRef(std::move(mtx));
}
/** Fuzzed coin creation height, biased towards activation/prune boundaries. */
uint32_t ConsumeCoinHeight(FuzzedDataProvider& fdp)
{
const uint32_t boundaries[] = {
0, 1,
599, 600, 601, // testnet DD activation floor
649, 650, 651, // regtest DD/oracle height gates
23'627'519, 23'627'520, 23'627'521, // mainnet DD activation floor
FUZZ_MEMPOOL_HEIGHT - 1, FUZZ_MEMPOOL_HEIGHT,
std::numeric_limits<uint32_t>::max(),
};
if (fdp.ConsumeBool()) return fdp.PickValueInArray(boundaries);
return fdp.ConsumeIntegral<uint32_t>();
}
/** Build one of the TxLookupFn flavors used by both lookup-driven targets.
*
* The production callback (MakeCachedBlockTxLookup in src/validation.cpp) only
* ever returns true with a non-null transaction found by txid in the retained
* block at coinHeight. Mode 3 mimics that contract exactly; mode 2 stresses a
* hash-mismatched return; modes 0/1 are the fail-closed paths (no callback /
* block or tx not found).
*/
DigiDollar::TxLookupFn MakeFuzzedLookup(uint8_t mode, const CTransactionRef& prev_tx)
{
switch (mode) {
case 0:
return nullptr;
case 1:
return [](const uint256&, uint32_t, CTransactionRef&) { return false; };
case 2:
return [prev_tx](const uint256&, uint32_t, CTransactionRef& tx_out) {
tx_out = prev_tx;
return true;
};
default:
return [prev_tx](const uint256& txid, uint32_t, CTransactionRef& tx_out) {
if (txid != prev_tx->GetHash()) return false;
tx_out = prev_tx;
return true;
};
}
}
/** Build a Consensus::Params carrying only the fields EarliestActivationFloor reads. */
Consensus::Params MakeFloorParams(int DigiDollarHeight, int nDDActivationHeight)
{
Consensus::Params cp{};
cp.DigiDollarHeight = DigiDollarHeight;
cp.nDDActivationHeight = nDDActivationHeight;
return cp;
}
} // namespace
void initialize_dd_prune_blockdb()
{
SelectParams(ChainType::REGTEST);
}
void initialize_dd_prune_gating_main()
{
// Mainnet params so the DigiDollar activation floor (23,627,520) is a real,
// positive boundary and the pre-floor coin skip is live.
SelectParams(ChainType::MAIN);
}
// ============================================================================
// Target 1: dd_extract_amount_blockdb
//
// Feed arbitrary/DD-shaped transactions through the block-db DD amount
// extraction used by pruned nodes (no txindex).
// ============================================================================
FUZZ_TARGET(dd_extract_amount_blockdb, .init = initialize_dd_prune_blockdb)
{
FuzzedDataProvider fdp(buffer.data(), buffer.size());
// The "creating transaction" the block-db lookup hands back.
CTransactionRef prev_tx;
const uint8_t shape = fdp.ConsumeIntegralInRange<uint8_t>(0, 3);
if (shape == 0) {
// Arbitrary bytes deserialized as a transaction.
const std::optional<CMutableTransaction> mtx = ConsumeDeserializable<CMutableTransaction>(fdp);
if (!mtx) return;
prev_tx = MakeTransactionRef(*mtx);
} else {
prev_tx = BuildDDShapedPrevTx(fdp, shape);
}
// Outpoint being resolved: usually the tx's own hash (contract-accurate),
// sometimes unrelated; output index both in and out of range.
COutPoint prevout;
prevout.hash = fdp.ConsumeBool() ? prev_tx->GetHash() : ConsumeUInt256(fdp);
prevout.n = fdp.ConsumeBool() ? fdp.ConsumeIntegralInRange<uint32_t>(0, 8)
: fdp.ConsumeIntegral<uint32_t>();
const uint32_t coin_height = ConsumeCoinHeight(fdp);
const uint8_t mode = fdp.ConsumeIntegralInRange<uint8_t>(0, 3);
const DigiDollar::TxLookupFn lookup = MakeFuzzedLookup(mode, prev_tx);
// Seed the out-param with garbage: the callee must fully define it.
CAmount amount{-99};
const bool ok = DigiDollar::ExtractDDAmountFromBlockDb(prevout, coin_height, lookup, amount);
// Fail-closed invariant: success iff a strictly positive DD amount was
// recovered. Failure must never leave a positive amount behind for a
// caller that (incorrectly) ignored the return value.
assert(ok == (amount > 0));
if (mode == 0 || mode == 1) {
// No callback / lookup miss: hard failure with a zeroed amount.
assert(!ok && amount == 0);
}
// Determinism: extraction is a pure function of (prevout, coinHeight,
// looked-up tx) - a pruned node and an archival node resolving the same
// coin must agree.
CAmount amount2{-77};
const bool ok2 = DigiDollar::ExtractDDAmountFromBlockDb(prevout, coin_height, lookup, amount2);
assert(ok2 == ok && amount2 == amount);
// Documented (not asserted): on success the amount is only guaranteed
// positive here. MAX_DIGIDOLLAR bounding is enforced at the consumption
// sites (AddDDAmount / conservation checks in src/digidollar/validation.cpp).
}
// ============================================================================
// Target 2: dd_prune_activation_floor
//
// Property fuzz of the shared activation-floor helper
// (DigiDollar::EarliestActivationFloor) and the prune-lock clamp arithmetic.
// ============================================================================
FUZZ_TARGET(dd_prune_activation_floor, .init = initialize_dd_prune_blockdb)
{
FuzzedDataProvider fdp(buffer.data(), buffer.size());
const int height_values[] = {
std::numeric_limits<int>::min(), -1, 0, 1,
600, 650, 23'627'520, 23'869'440,
DISABLED_HEIGHT - 1, DISABLED_HEIGHT,
};
const int dd_height = fdp.ConsumeBool() ? fdp.PickValueInArray(height_values) : fdp.ConsumeIntegral<int>();
const int dd_act = fdp.ConsumeBool() ? fdp.PickValueInArray(height_values) : fdp.ConsumeIntegral<int>();
const Consensus::Params cp = MakeFloorParams(dd_height, dd_act);
const int dd_floor = DigiDollar::EarliestActivationFloor(cp);
// Deterministic.
assert(dd_floor == DigiDollar::EarliestActivationFloor(cp));
if (dd_height == DISABLED_HEIGHT) {
// Deployment disabled (buried int-max sentinel): DD can never
// activate — no block retention needed, no prune lock. Matches the
// old NEVER_ACTIVE contract.
assert(dd_floor == 0);
} else {
// Spec check: the floor is the earliest DD-creating height, so the
// retained window [dd_floor, tip] covers every block a DD spend can
// ever need to read. A higher floor would let pruning delete a
// needed block. Buried formula: min(static gate, buried height).
assert(dd_floor == std::min(dd_act, dd_height));
// The floor never exceeds the buried activation height.
assert(dd_floor <= dd_height);
// Sane params (non-negative heights) yield a non-negative floor, and
// strictly positive mainnet/testnet-style params always register the
// lock on a pruned node (dd_floor > 0 gate in chainstate.cpp).
if (dd_height >= 0 && dd_act >= 0) assert(dd_floor >= 0);
if (dd_height > 0 && dd_act > 0) assert(dd_floor > 0);
}
// Live-params sanity for the selected chain (regtest here): the shared
// helper is what production consumes, so it must be deterministic and
// non-negative on real chainparams too.
{
const Consensus::Params& cp_live = Params().GetConsensus();
const int live_floor = DigiDollar::EarliestActivationFloor(cp_live);
assert(live_floor == DigiDollar::EarliestActivationFloor(cp_live));
assert(live_floor >= 0);
}
// Prune-lock registration predicate from chainstate.cpp: lock registered
// iff pruning is on and the floor is positive.
const bool prune_enabled = fdp.ConsumeBool();
const bool lock_registered = prune_enabled && dd_floor > 0;
if (lock_registered) {
// Clamp arithmetic from Chainstate::FlushStateToDisk
// (src/validation.cpp):
// lock_height = height_first - PRUNE_LOCK_BUFFER - 1
// last_prune = max(1, min(last_prune, lock_height))
// height_first == INT_MAX is the "no lock" sentinel and is skipped in
// production before this expression runs.
const int height_first = dd_floor;
if (height_first != std::numeric_limits<int>::max()) {
const int lock_height = height_first - FUZZ_PRUNE_LOCK_BUFFER - 1;
// No signed overflow: the lock only exists for height_first >= 1.
assert(static_cast<int64_t>(height_first) - FUZZ_PRUNE_LOCK_BUFFER - 1 == lock_height);
const int last_prune_in = fdp.ConsumeIntegralInRange<int>(-1, std::numeric_limits<int>::max());
const int clamped = std::max(1, std::min(last_prune_in, lock_height));
assert(clamped >= 1);
if (height_first >= FUZZ_PRUNE_LOCK_BUFFER + 2) {
// Binding property: nothing at or above (floor - buffer) can be
// pruned, no matter how high the requested prune height is.
assert(clamped <= height_first - FUZZ_PRUNE_LOCK_BUFFER - 1);
}
// For height_first in [1, 11] the max(1, ...) floor dominates; that
// is inherited upstream Bitcoin Core behavior (block file 0 is
// practically never prunable), documented rather than asserted.
}
}
}
// ============================================================================
// Target 3: dd_prune_coin_gating
//
// Drive the production pre-floor coin skip (EarliestDigiDollarActivationHeight)
// and the LookupPreviousTransaction block-db fallback through the public
// SpendsDigiDollarCollateralVault / RequiresDigiDollarValidation entry points,
// on mainnet params where the activation floor is a real positive boundary.
// ============================================================================
FUZZ_TARGET(dd_prune_coin_gating, .init = initialize_dd_prune_gating_main)
{
FuzzedDataProvider fdp(buffer.data(), buffer.size());
const CChainParams& chainparams = Params();
const Consensus::Params& cp = chainparams.GetConsensus();
// The production pre-floor skip boundary. On mainnet this is the
// DigiDollar activation floor used for the prune lock — still the static
// 23,627,520 gate post-burial: min(nDDActivationHeight 23,627,520,
// buried DigiDollarHeight 23,869,440).
const int dd_floor = DigiDollar::EarliestActivationFloor(cp);
assert(dd_floor == 23'627'520);
// "Creating transaction" served by the fuzzed block-db lookup.
const CTransactionRef prev_tx = BuildDDShapedPrevTx(fdp, fdp.ConsumeIntegralInRange<uint8_t>(1, 3));
// Spending transaction: sometimes DD-marked, sometimes arbitrary version.
CMutableTransaction spend;
spend.nVersion = fdp.ConsumeBool()
? ((static_cast<int32_t>(fdp.ConsumeIntegralInRange<uint8_t>(0, 4)) << 24) | 0x0770)
: fdp.ConsumeIntegral<int32_t>();
CCoinsView backend;
CCoinsViewCache coins{&backend};
bool all_coins_below_floor = true;
const int n_in = fdp.ConsumeIntegralInRange<int>(0, 4);
for (int i = 0; i < n_in; i++) {
COutPoint op;
op.hash = fdp.ConsumeBool() ? prev_tx->GetHash() : ConsumeUInt256(fdp);
op.n = fdp.ConsumeIntegralInRange<uint32_t>(0, 8);
spend.vin.emplace_back(op);
// Sometimes the coin is simply absent from the view.
if (!fdp.ConsumeBool()) continue;
// Coin height around the activation floor (Coin::nHeight is 31 bits).
const uint32_t heights[] = {
0u, 1u,
static_cast<uint32_t>(dd_floor - 1),
static_cast<uint32_t>(dd_floor),
static_cast<uint32_t>(dd_floor + 1),
FUZZ_MEMPOOL_HEIGHT,
};
const uint32_t h = fdp.ConsumeBool() ? fdp.PickValueInArray(heights)
: fdp.ConsumeIntegralInRange<uint32_t>(0, FUZZ_MEMPOOL_HEIGHT);
if (h >= static_cast<uint32_t>(dd_floor)) all_coins_below_floor = false;
CTxOut out;
out.nValue = fdp.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY);
const uint8_t script_kind = fdp.ConsumeIntegralInRange<uint8_t>(0, 2);
if (script_kind == 0) {
out.scriptPubKey = MakeP2TR(fdp);
} else if (script_kind == 1) {
const auto raw = ConsumeRandomLengthByteVector(fdp, 40);
out.scriptPubKey = CScript(raw.begin(), raw.end());
} else {
// Registered collateral vault script (process-local metadata map),
// exercising the IsRegisteredCollateralVaultScript branch.
out.scriptPubKey = MakeP2TR(fdp);
DigiDollar::RegisterScriptMetadata(out.scriptPubKey, DigiDollar::ScriptType::COLLATERAL_LOCK,
/*ddAmount=*/10000, /*lockHeight=*/1000);
}
coins.AddCoin(op, Coin(out, static_cast<int>(h), /*fCoinBaseIn=*/fdp.ConsumeBool()),
/*possible_overwrite=*/true);
}
const CTransaction tx(spend);
const DigiDollar::TxLookupFn lookup = MakeFuzzedLookup(fdp.ConsumeIntegralInRange<uint8_t>(0, 3), prev_tx);
const int tip_height = fdp.ConsumeIntegralInRange<int>(0, 40'000'000);
const CAmount price = fdp.ConsumeIntegralInRange<CAmount>(1, 100'000'000'000LL);
const DigiDollar::ValidationContext ctx(tip_height, price, /*collateral=*/15000, chainparams,
&coins, /*skip_oracle=*/true, lookup,
/*pool=*/nullptr, /*block_time=*/0);
const bool vault1 = DigiDollar::SpendsDigiDollarCollateralVault(tx, ctx);
const bool vault2 = DigiDollar::SpendsDigiDollarCollateralVault(tx, ctx);
assert(vault1 == vault2); // deterministic
// Pre-activation coins are plain DGB history: if every resolvable input
// coin was created below the activation floor, the tx must never classify
// as a DD collateral vault spend. This is exactly the property that makes
// pruning blocks below the floor safe.
if (all_coins_below_floor) assert(!vault1);
const bool requires_dd = DigiDollar::RequiresDigiDollarValidation(tx, ctx);
assert(requires_dd == (DigiDollar::HasDigiDollarMarker(tx) || vault1));
// Without a coins view the vault check fails closed to "not a vault spend".
const DigiDollar::ValidationContext ctx_nocoins(tip_height, price, 15000, chainparams,
nullptr, true, lookup, nullptr, 0);
assert(!DigiDollar::SpendsDigiDollarCollateralVault(tx, ctx_nocoins));
}