forked from digibyte/digibyte
-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathdigidollar_txbuilder_validate.cpp
More file actions
334 lines (302 loc) · 13.5 KB
/
Copy pathdigidollar_txbuilder_validate.cpp
File metadata and controls
334 lines (302 loc) · 13.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
// 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.
//
// Wave 22 Agent B — Fuzz Harness Completeness.
//
// DD-FA-TEST-040 — strengthen direct coverage of the txbuilder validation
// surface that the wallet/Qt/RPC mint and redeem flows depend on:
//
// 1. `MintTxBuilder::ValidateMintParams` — exercised through every
// reject branch (zero/negative/over-cap dd amount, non-canonical
// lock duration, mismatched lock tier byte, invalid owner key,
// out-of-range fee rate, empty UTXOs, dd > MAX_DIGIDOLLAR). The
// existing `dd_txbuilder_mint` harness only calls
// `BuildMintTransaction`, which masks `ValidateMintParams` early
// returns behind a single boolean — this target asserts the
// validator is the one rejecting and that all canonical lock tier
// values (0..9) are accepted with consistent semantics.
//
// 2. `RedeemTxBuilder::BuildRedemptionTransaction` end-to-end against
// `DigiDollar::ValidateDigiDollarTransaction`. The output of the
// builder must classify as a DD redeem on the consensus side OR
// the builder must fail; never produce a redemption transaction
// that consensus does not recognise. This guards a class of
// builder/consensus drift that would silently mint UTXOs that
// block validation later refuses to spend.
//
// 3. `DigiDollar::ValidateDigiDollarTransaction` end-to-end determinism
// (same input twice → identical accept/reject outcome) for the txn
// shapes the builders produce.
//
// Together with the existing `dd_validate_*` and `dd_txbuilder_*`
// harnesses, this covers the three "real DD entry points" listed in the
// Wave 22 brief. Failures here are reachable from RPC `mintdigidollar`
// and `redeemdigidollar`, so any crash/assert here is a release blocker.
#include <chainparams.h>
#include <consensus/amount.h>
#include <consensus/digidollar.h>
#include <digidollar/digidollar.h>
#include <digidollar/scripts.h>
#include <digidollar/txbuilder.h>
#include <digidollar/validation.h>
#include <key.h>
#include <pubkey.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <uint256.h>
#include <util/chaintype.h>
#include <cassert>
#include <cstdint>
#include <vector>
namespace {
void initialize_dd_txbuilder_validate()
{
ECC_Start();
SelectParams(ChainType::REGTEST);
}
CKey MakeFuzzKey(FuzzedDataProvider& fdp)
{
CKey key;
auto bytes = fdp.ConsumeBytes<uint8_t>(32);
if (bytes.size() == 32) {
key.Set(bytes.begin(), bytes.end(), true);
}
if (!key.IsValid()) {
// Fall back to a deterministic-but-guaranteed-valid key so the
// owner-key reject branch only fires when fuzzer asks for it.
std::array<unsigned char, 32> seed{};
seed.fill(0x42);
key.Set(seed.begin(), seed.end(), true);
}
return key;
}
COutPoint MakeFuzzOutpoint(FuzzedDataProvider& fdp)
{
uint256 hash;
auto hash_bytes = fdp.ConsumeBytes<uint8_t>(32);
if (hash_bytes.size() == 32) {
std::memcpy(hash.begin(), hash_bytes.data(), 32);
}
uint32_t n = fdp.ConsumeIntegralInRange<uint32_t>(0, 16);
return COutPoint(hash, n);
}
/** Return all canonical lock periods (in days) that ValidateMintParams must
* accept on regtest. Mirrors the chainparams `collateralRatios` order. */
std::vector<int> CanonicalRegtestLockDays()
{
// Tiers 0..9: 1h, 30d, 90d, 180d, 1y, 2y, 3y, 5y, 7y, 10y.
// 1h on regtest is canonical-tier-0; LockDaysToBlocks rounds days to
// blocks via (days * 86400 / BLOCK_TIME). Use the canonical block
// counts from the consensus tier table to avoid day/block drift, by
// reversing the chainparams map.
return {0, 30, 90, 180, 365, 730, 1095, 1825, 2555, 3650};
}
} // namespace
// ============================================================================
// Target 1: dd_txbuilder_validate_mint_params
//
// Direct fuzz of `MintTxBuilder::ValidateMintParams` reject branches.
// ============================================================================
FUZZ_TARGET(dd_txbuilder_validate_mint_params, .init = initialize_dd_txbuilder_validate)
{
FuzzedDataProvider fdp(buffer.data(), buffer.size());
const auto& chainParams = Params();
const auto& ddParams = chainParams.GetDigiDollarParams();
int height = fdp.ConsumeIntegralInRange<int>(0, 10'000'000);
CAmount oraclePrice = fdp.ConsumeIntegralInRange<CAmount>(1, 100'000'000); // micro-USD
DigiDollar::MintTxBuilder builder(chainParams, height, oraclePrice);
// Build a "good" baseline params object from a chosen canonical tier.
DigiDollar::TxBuilderMintParams good;
auto canonical = CanonicalRegtestLockDays();
uint32_t tier = fdp.ConsumeIntegralInRange<uint32_t>(0, canonical.size() - 1);
good.lockDays = canonical[tier];
good.lockTier = tier;
good.ddAmount = fdp.ConsumeIntegralInRange<CAmount>(ddParams.minMintAmount, ddParams.maxMintAmount);
good.feeRate = fdp.ConsumeIntegralInRange<CAmount>(100000, 100000000); // 100k..100M sat/kB
good.ownerKey = MakeFuzzKey(fdp);
good.utxos.push_back(MakeFuzzOutpoint(fdp));
// Sanity: the baseline must validate. If it does not, the fuzzer chose
// an out-of-range canonical tier — log and fall through.
const bool baseline_ok = builder.ValidateMintParams(good);
assert(baseline_ok);
// Pick a single mutation to apply per iteration so reject branches are
// attributable to a specific corruption.
enum Mutation : uint8_t {
ZERO_AMOUNT,
NEGATIVE_AMOUNT,
BELOW_MIN_AMOUNT,
ABOVE_MAX_AMOUNT,
ABOVE_MAX_DIGIDOLLAR,
NON_CANONICAL_LOCK_DAYS,
WRONG_TIER_BYTE,
OUT_OF_RANGE_TIER_BYTE,
INVALID_OWNER_KEY,
FEE_RATE_TOO_LOW,
FEE_RATE_TOO_HIGH,
EMPTY_UTXOS,
IDENTITY,
N_MUTATIONS
};
const auto mutation = static_cast<Mutation>(fdp.ConsumeIntegralInRange<uint8_t>(0, static_cast<uint8_t>(N_MUTATIONS) - 1));
DigiDollar::TxBuilderMintParams test = good;
bool expect_reject = true;
switch (mutation) {
case ZERO_AMOUNT:
test.ddAmount = 0;
break;
case NEGATIVE_AMOUNT:
test.ddAmount = -fdp.ConsumeIntegralInRange<CAmount>(1, 1'000'000);
break;
case BELOW_MIN_AMOUNT:
// Only meaningful when min > 1; otherwise IsValidMintAmount can't
// reject "below min" without also being zero/negative.
if (ddParams.minMintAmount <= 1) {
// Fall back to ZERO_AMOUNT semantics for this network.
test.ddAmount = 0;
} else {
test.ddAmount = ddParams.minMintAmount - 1;
}
break;
case ABOVE_MAX_AMOUNT:
test.ddAmount = ddParams.maxMintAmount + 1;
break;
case ABOVE_MAX_DIGIDOLLAR:
// CAmount is int64_t; MAX_DIGIDOLLAR + delta could overflow into
// negative. Both branches are reject cases (negative -> ddAmount<=0
// guard; >MAX_DIGIDOLLAR -> dedicated guard) so either outcome is
// a valid reject.
test.ddAmount = MAX_DIGIDOLLAR + fdp.ConsumeIntegralInRange<CAmount>(1, 1'000'000);
break;
case NON_CANONICAL_LOCK_DAYS: {
// Pick a day count that is provably NOT in the canonical set by
// jumping to a value strictly between two canonical entries.
// canonical = {0, 30, 90, 180, 365, 730, 1095, 1825, 2555, 3650}.
const std::vector<int> nonCanonicalChoices{
1, 7, 14, 29, 31, 60, 89, 91, 100, 200, 364, 366, 500, 731,
1000, 1094, 1100, 1500, 1826, 2000, 2554, 2556, 3000, 3651, 50000};
test.lockDays = nonCanonicalChoices[fdp.ConsumeIntegralInRange<size_t>(0, nonCanonicalChoices.size() - 1)];
break;
}
case WRONG_TIER_BYTE: {
// Keep canonical lockDays but corrupt lockTier to a different
// canonical tier — must reject because it disagrees with the
// computed tier.
uint32_t wrong = (tier + 1 + fdp.ConsumeIntegralInRange<uint32_t>(0, canonical.size() - 2)) % canonical.size();
if (wrong == tier) wrong = (wrong + 1) % canonical.size();
test.lockTier = wrong;
break;
}
case OUT_OF_RANGE_TIER_BYTE:
test.lockTier = fdp.ConsumeIntegralInRange<uint32_t>(canonical.size(), 100);
break;
case INVALID_OWNER_KEY:
test.ownerKey = CKey{}; // invalid (default-constructed)
break;
case FEE_RATE_TOO_LOW:
test.feeRate = fdp.ConsumeIntegralInRange<CAmount>(0, 99999); // < 100k threshold
break;
case FEE_RATE_TOO_HIGH:
test.feeRate = fdp.ConsumeIntegralInRange<CAmount>(100000001, 1'000'000'000);
break;
case EMPTY_UTXOS:
test.utxos.clear();
break;
case IDENTITY:
// No mutation — must accept (mirror of baseline_ok).
expect_reject = false;
break;
default:
expect_reject = false;
break;
}
const bool actual = builder.ValidateMintParams(test);
if (expect_reject) {
assert(!actual);
} else {
assert(actual);
}
// Determinism: validating twice must yield the same boolean.
const bool actual_again = builder.ValidateMintParams(test);
assert(actual == actual_again);
// BuildMintTransaction must NEVER succeed when ValidateMintParams
// refused — guards against silent bypass paths in the build layer.
if (!actual) {
DigiDollar::TxBuilderResult result = builder.BuildMintTransaction(test);
assert(!result.success);
assert(!result.error.empty());
}
}
// ============================================================================
// Target 2: dd_txbuilder_redeem_consensus_round_trip
//
// Drive `RedeemTxBuilder::BuildRedemptionTransaction`, then if it
// succeeds run the result through `DigiDollar::ValidateDigiDollarTransaction`.
// Either the build refuses, or the consensus validator must classify the
// transaction as a redemption (it can still legitimately reject for
// structural reasons in the absence of a real coins view, but it must
// never crash and must remain deterministic).
// ============================================================================
FUZZ_TARGET(dd_txbuilder_redeem_consensus_round_trip, .init = initialize_dd_txbuilder_validate)
{
FuzzedDataProvider fdp(buffer.data(), buffer.size());
const auto& chainParams = Params();
int height = fdp.ConsumeIntegralInRange<int>(0, 10'000'000);
CAmount oraclePrice = fdp.ConsumeIntegralInRange<CAmount>(1, 100'000'000);
int systemHealth = fdp.ConsumeIntegralInRange<int>(0, 30000);
DigiDollar::RedeemTxBuilder builder(chainParams, height, oraclePrice);
DigiDollar::TxBuilderRedeemParams params;
params.collateralOutpoint = MakeFuzzOutpoint(fdp);
params.ddToRedeem = fdp.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY / 4);
params.path = fdp.ConsumeBool() ? DigiDollar::RedemptionPath::NORMAL
: DigiDollar::RedemptionPath::ERR;
params.ownerKey = MakeFuzzKey(fdp);
params.feeRate = fdp.ConsumeIntegralInRange<CAmount>(100000, 100000000);
params.collateralAmount = fdp.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY);
params.ddMinted = fdp.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY / 2);
params.unlockHeight = fdp.ConsumeIntegralInRange<uint32_t>(0, 10'000'000);
int numDD = fdp.ConsumeIntegralInRange<int>(0, 4);
for (int i = 0; i < numDD; ++i) {
params.ddUtxos.push_back(MakeFuzzOutpoint(fdp));
params.ddAmounts.push_back(fdp.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY / 10));
}
int numFee = fdp.ConsumeIntegralInRange<int>(0, 3);
for (int i = 0; i < numFee; ++i) {
params.feeUtxos.push_back(MakeFuzzOutpoint(fdp));
params.feeAmounts.push_back(fdp.ConsumeIntegralInRange<CAmount>(0, 100 * COIN));
}
DigiDollar::TxBuilderResult result = builder.BuildRedemptionTransaction(params);
if (!result.success) {
// Builder rejected — must produce an error string and not a tx.
assert(!result.error.empty());
return;
}
assert(result.error.empty());
assert(!result.tx.vin.empty());
assert(!result.tx.vout.empty());
// Consensus-side determinism on the produced tx. The validator may
// reject because the harness lacks a real coins view (oracle, prev
// outputs) but the call must not crash and must be deterministic.
CTransaction ctx_tx(result.tx);
DigiDollar::ValidationContext ctx(height, oraclePrice, systemHealth, chainParams,
/*coins=*/nullptr, /*skip_oracle=*/true);
TxValidationState state1, state2;
bool r1 = DigiDollar::ValidateDigiDollarTransaction(ctx_tx, ctx, state1);
bool r2 = DigiDollar::ValidateDigiDollarTransaction(ctx_tx, ctx, state2);
assert(r1 == r2);
// Either the validator agreed it is a DD redeem (returned true), or
// it refused — but the rejection reason must not be the generic
// "non-DD" classification, because the builder explicitly stamps
// DD_TX_REDEEM into nVersion.
if (!r1) {
// The builder always sets the redeem type marker in nVersion.
// Consensus rejection is fine, but the rejection text must
// indicate a DD-specific failure rather than "missing marker".
const std::string reason = state1.GetRejectReason();
// Empty reason means consensus didn't even recognise the tx as
// DD — a builder/consensus drift that should fail this assertion.
assert(!reason.empty());
}
}