xrpld
Loading...
Searching...
No Matches
VaultHelpers_test.cpp
1#include <test/jtx/Account.h>
2
3#include <xrpl/basics/Number.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/beast/unit_test/suite.h>
6#include <xrpl/ledger/helpers/VaultHelpers.h>
7#include <xrpl/protocol/Asset.h>
8#include <xrpl/protocol/Indexes.h>
9#include <xrpl/protocol/Issue.h>
10#include <xrpl/protocol/MPTIssue.h>
11#include <xrpl/protocol/SField.h>
12#include <xrpl/protocol/STAmount.h>
13#include <xrpl/protocol/STIssue.h>
14#include <xrpl/protocol/STLedgerEntry.h>
15#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
16#include <xrpl/protocol/STTakesAsset.h>
17#include <xrpl/protocol/TER.h>
18#include <xrpl/protocol/UintTypes.h>
19
20#include <algorithm>
21#include <array>
22#include <cstddef>
23#include <expected>
24#include <memory>
25#include <optional>
26#include <string>
27
28namespace xrpl {
29
30// True unit test of `clampToAssetsTotalScale`. The function under test only
31// reads sfAsset and sfAssetsTotal from the vault SLE and never touches a
32// ledger view or Rules, so a bare in-memory ltVAULT SLE is enough; there is
33// no jtx::Env and no transaction submitted anywhere in this file.
34//
35// Number regime: this suite relies on the default thread_local Number
36// mantissa range, which src/libxrpl/basics/Number.cpp initializes to
37// Large330 (19-digit mantissa, post-fixCleanup3_3_0 cusp-rounding behavior):
38//
39// thread_local std::reference_wrapper<MantissaRange const> Number::kRange =
40// MantissaRange::Access::mantissaRange(MantissaRange::MantissaScale::Large330);
41//
42// Unlike transaction processing, this test never constructs a ledger `Rules`
43// object, so `STAmount::operator=(Number const&)` always takes its
44// `!getCurrentTransactionRules()` branch and calls `fromNumber`, independent
45// of amendment state. testProbeLarge330Regime() below asserts directly on a
46// value that only round-trips exactly under Large330, pinning the regime
47// rather than merely asserting it by comment.
49{
50private:
51 // A single row of the clampToAssetsTotalScale table. `assetsTotal` and
52 // `delta` must already be genuine, on-grid STAmount values for `asset`.
53 struct Case
54 {
55 char const* name = nullptr;
58 std::optional<Number> expected; // nullopt means tecPRECISION_LOSS
59 };
60
61 // Builds a bare ltVAULT SLE with only sfAsset and sfAssetsTotal set,
62 // mirroring what a transactor does: set the STNumber field, then call
63 // associateAsset() so it is quantized to the asset's STAmount grid, the
64 // same way VaultDeposit::doApply does for a real vault (see
65 // src/libxrpl/tx/transactors/vault/VaultDeposit.cpp).
67 makeVault(Asset const& asset, Number const& assetsTotal)
68 {
70 vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
71 vault->at(sfAssetsTotal) = assetsTotal;
72 associateAsset(*vault, asset);
73 return vault;
74 }
75
76 // Runs every case in `cases` against `asset`, once per ambient rounding
77 // mode. The function must give the same answer under all four modes,
78 // and its answer must match the hand-derived `expected` value.
79 template <std::size_t N>
80 void
81 runCases(Asset const& asset, std::array<Case, N> const& cases)
82 {
88
89 for (auto const& c : cases)
90 {
91 testcase(c.name);
92
93 auto const vault = makeVault(asset, c.assetsTotal);
94 BEAST_EXPECTS(
95 Number(vault->at(sfAssetsTotal)) == c.assetsTotal,
96 std::string(c.name) +
97 ": assetsTotal is not a genuine on-grid STAmount value (associateAsset "
98 "changed it)");
99
100 STAmount const delta{asset, c.delta};
101 BEAST_EXPECTS(
102 Number(delta) == c.delta,
103 std::string(c.name) + ": delta is not a genuine on-grid STAmount value");
104
106 for (auto const mode : modes)
107 {
108 NumberRoundModeGuard const rg(mode);
109 auto const result = clampToAssetsTotalScale(vault, delta);
110
111 // The function must be insensitive to the caller's ambient
112 // rounding mode: every mode must agree with the first one
113 // tried.
114 if (!reference)
115 {
116 reference = result;
117 }
118 else
119 {
120 BEAST_EXPECTS(
121 result.has_value() == reference->has_value(),
122 std::string(c.name) + ": result depends on ambient rounding mode");
123 if (result.has_value() && reference->has_value())
124 {
125 BEAST_EXPECTS(
126 *result == **reference,
127 std::string(c.name) + ": value depends on ambient rounding mode");
128 }
129 else if (!result.has_value() && !reference->has_value())
130 {
131 BEAST_EXPECTS(
132 result.error() == reference->error(),
133 std::string(c.name) + ": error depends on ambient rounding mode");
134 }
135 }
136
137 if (!c.expected)
138 {
139 BEAST_EXPECTS(
140 !result.has_value(),
141 std::string(c.name) + ": expected tecPRECISION_LOSS, got success value " +
142 (result.has_value() ? result->getText() : std::string()));
143 if (!result.has_value())
144 {
145 BEAST_EXPECTS(
146 result.error() == tecPRECISION_LOSS,
147 std::string(c.name) + ": expected tecPRECISION_LOSS, got " +
148 transToken(result.error()));
149 }
150 continue;
151 }
152
153 STAmount const expected{asset, *c.expected};
154 if (!BEAST_EXPECTS(
155 result.has_value(),
156 std::string(c.name) + ": expected success (" + expected.getText() +
157 "), got " + transToken(result.error())))
158 {
159 continue;
160 }
161
162 BEAST_EXPECTS(
163 *result == expected,
164 std::string(c.name) + ": expected " + expected.getText() + ", got " +
165 result->getText());
166
167 // The result must always be positive...
168 BEAST_EXPECT(Number(*result) > Number{0});
169
170 // ...and never larger in magnitude than the requested delta.
171 BEAST_EXPECT(abs(Number(*result)) <= abs(c.delta));
172
173 // For IOU rows, re-flooring the result on the posterior grid
174 // must be a no-op: the result is already exactly
175 // representable at that scale.
176 //
177 // For debits this holds directly at postScale, because the
178 // result IS `roundToScale(magnitude, postScale, Downward)` by
179 // construction. For credits the result is
180 // `roundedPosterior - assetsTotal`, where roundedPosterior
181 // sits exactly on the postScale grid but assetsTotal sits on
182 // its own (possibly finer) natural grid; the difference of a
183 // multiple of 10^postScale and a multiple of 10^assetsScale
184 // is only guaranteed exact at the FINER of the two scales.
185 // Row 7 below ("overcredit fix across a scale boundary") is
186 // exactly this case: assetsTotal's own scale (-15) is finer
187 // than postScale (-14), so checking exactness at postScale
188 // alone fails even though the implementation is correct.
189 if (!asset.integral())
190 {
191 bool const isDebit = c.delta.mantissa() < 0;
192 Number const posterior =
193 isDebit ? c.assetsTotal - Number(*result) : c.assetsTotal + Number(*result);
194 int const postScale = scale(posterior, asset);
195 int const checkScale =
196 isDebit ? postScale : std::min(postScale, scale(c.assetsTotal, asset));
197 STAmount const reFloored =
198 roundToScale(*result, checkScale, Number::RoundingMode::Downward);
199 BEAST_EXPECTS(
200 reFloored == *result,
201 std::string(c.name) + ": result " + result->getText() +
202 " is not exact on the posterior grid (scale " +
203 std::to_string(checkScale) + ")");
204 }
205 }
206 }
207 }
208
209 // Pins the Number mantissa regime this suite relies on. Under Large330,
210 // a 19-digit mantissa (max 10^19-1) is exact where a legacy 16-digit
211 // ("Small", max 10^16-1) regime would have to round it down to 16
212 // significant digits, changing both mantissa and exponent.
213 void
215 {
216 testcase("probe: default Number regime is Large330 (19-digit mantissa)");
217
219
220 // std::numeric_limits<std::int64_t>::max(), 19 significant digits.
221 // This is already inside Large330's [10^18, 10^19-1] range, so
222 // constructing it is a no-op; under "Small" it would have to lose
223 // its low 3 digits.
224 Number const probe{9'223'372'036'854'775'807LL, 0};
225 BEAST_EXPECT(probe.mantissa() == 9'223'372'036'854'775'807LL);
226 BEAST_EXPECT(probe.exponent() == 0);
227 }
228
229 // -------------------------------------------------------------------
230 // IOU debits (delta negative).
231 // -------------------------------------------------------------------
232 void
234 {
235 std::array<Case, 5> const cases{
236 Case{
237 // T = 1000000.000000005, delta = -1e-9.
238 // Posterior = 1000000.000000004, still 16 significant
239 // digits at exponent -9 (no rounding, no decade change).
240 // postScale = -9. magnitude 1e-9 has its own exponent -24
241 // (finer than -9), so it must be actually floored: 1e-9 is
242 // exactly 1 ULP at scale -9, so flooring is a no-op.
243 .name = "IOU debit: on-grid, same decade",
244 .assetsTotal = Number{1'000'000'000'000'005LL, -9},
245 .delta = Number{-1, -9},
246 .expected = Number{1, -9},
247 },
248 Case{
249 // T = 1000000, delta = -7.3e-10.
250 // Posterior = 999999.99999999927 exactly (17 significant
251 // digits: 15 nines, then "27"). Rounding to 16 digits
252 // (ToNearest) rounds the trailing "...92.7" up to
253 // "...93", giving mantissa 9999999999999993 at exponent
254 // -10 -- postScale = -10, ONE DIGIT FINER than the naive
255 // "posterior stays in T's decade at -9" guess, because
256 // subtracting anything positive from an exact power-of-ten
257 // total necessarily drops into the next lower decade
258 // (1000000 has 7 integer digits, 999999.x has 6).
259 // At scale -10 the ULP is 1e-10, and floor(7.3) = 7, so
260 // the debit is NOT sub-ULP: it floors to 7e-10, not to
261 // zero. See discrepancy note in the report.
262 .name = "IOU debit: sub-ULP at the naive scale, but not at the true postScale",
263 .assetsTotal = Number{1'000'000, 0},
264 .delta = Number{-73, -11},
265 .expected = Number{7, -10},
266 },
267 Case{
268 // T = 1000000, delta = -5.3e-9.
269 // Posterior = 999999.9999999947 exactly -- this needs only
270 // 16 significant digits (14 nines, then "47"), so it is
271 // exactly representable with NO rounding at exponent -10.
272 // postScale = -10 (again one digit finer than T's own -9,
273 // for the same power-of-ten-boundary reason as the row
274 // above). At that grid 5.3e-9 is exactly 53 ULPs (integer),
275 // so it floors to itself, unchanged.
276 .name = "IOU debit: exact at the true (finer) postScale",
277 .assetsTotal = Number{1'000'000, 0},
278 .delta = Number{-53, -10},
279 .expected = Number{53, -10},
280 },
281 Case{
282 // T = 1.000000000000000, delta = -7.3e-16.
283 // Posterior = 0.99999999999999927 exactly (17 significant
284 // digits: 15 nines then "27"). Rounding to 16 digits
285 // (ToNearest) gives mantissa 9999999999999993 at exponent
286 // -16 -- postScale = -16. At that grid, 7.3e-16 is 7.3
287 // ULPs (not integral), so it floors to 7e-16, not to
288 // itself. See discrepancy note in the report.
289 .name = "IOU debit: decade-crossing debit, floored (not exact) at finer grid",
290 .assetsTotal = Number{1, 0},
291 .delta = Number{-73, -17},
292 .expected = Number{7, -16},
293 },
294 Case{
295 // T = 1000000, delta = -999999.9999999999 (9.999999999999999e5).
296 // Posterior = 0.0000000001 = 1e-10 exactly. postScale is
297 // the exponent of 1e-10 as a canonical STAmount, i.e. -25 --
298 // far finer than the magnitude's own exponent (-10).
299 // roundToScale short-circuits ("value.exponent() >= scale")
300 // and returns the magnitude unchanged.
301 .name = "IOU debit: near-total debit, unchanged (finer postScale than magnitude)",
302 .assetsTotal = Number{1'000'000, 0},
303 .delta = Number{-9'999'999'999'999'999LL, -10},
304 .expected = Number{9'999'999'999'999'999LL, -10},
305 },
306 };
307
308 runCases(iou, cases);
309 }
310
311 // -------------------------------------------------------------------
312 // IOU credits (delta positive).
313 // -------------------------------------------------------------------
314 void
316 {
317 std::array<Case, 6> const cases{
318 Case{
319 // T = 1000000, delta = +2e-9. Posterior = 1000000.000000002,
320 // exactly 16 significant digits at exponent -9
321 // (postScale = -9, unchanged from T -- addition never
322 // crosses below the 1e6 boundary the way subtraction does).
323 // magnitude is already exact at that scale, so it passes
324 // through unchanged.
325 .name = "IOU credit: on-grid",
326 .assetsTotal = Number{1'000'000, 0},
327 .delta = Number{2, -9},
328 .expected = Number{2, -9},
329 },
330 Case{
331 // T = 9.999999999999999, delta = +5.
332 // Exact posterior = 14.999999999999999 (17 significant
333 // digits: "14" then 15 nines). postScale is computed under
334 // ToNearest at the Number (19-digit) level: normalized
335 // mantissa 1499999999999999900 (exponent -17) divided by
336 // 1000 (to reach 16-digit IOU precision) gives
337 // 1499999999999999.9, which rounds UP to 1500000000000000
338 // -- i.e. exactly 15, at exponent -14. postScale = -14.
339 // Downward-guarded posterior (exact, no rounding needed
340 // since 17 digits < 19): 14.999999999999999. Flooring THAT
341 // to 16 digits at scale -14 (Downward) gives
342 // 1499999999999999 * 10^-14 = 14.99999999999999 (postScale
343 // already matches the STAmount's own exponent, so no
344 // further roundToScale is applied).
345 // actualDelta = 14.99999999999999 - 9.999999999999999
346 // = 4.999999999999991.
347 // This mirrors testBugVaultDepositOvercreditsAcrossScaleBoundary
348 // in VaultBugs_test.cpp (same seed/deposit values), which
349 // asserts post-fix `credited <= paid` rather than an exact
350 // number; this row pins the exact value.
351 .name = "IOU credit: overcredit fix across a scale boundary",
352 .assetsTotal = Number{9'999'999'999'999'999LL, -15},
353 .delta = Number{5, 0},
354 .expected = Number{4'999'999'999'999'991LL, -15},
355 },
356 Case{
357 // Finding-1 regression: T = 1000000, delta = +9.999999999999999e-10.
358 // The exact sum needs ~25 significant digits (1000000 at
359 // position 6, delta's last digit at position -25), far
360 // beyond Number's 19-digit mantissa.
361 //
362 // postScale (computed under ToNearest): the digits of delta
363 // that land within the 19-digit window (positions -10..-12,
364 // "999") plus an all-nines remainder below position -12
365 // round UP under ToNearest, carrying all the way through
366 // the intervening zeros: the sum rounds to exactly
367 // 1000000.000000001, i.e. postScale = -9.
368 //
369 // But the credit branch computes the *posterior* under a
370 // Downward guard, not ToNearest: positions -10..-12 stay
371 // "999" (no carry), giving posterior = 1000000.000000000999
372 // exactly. Flooring that (Downward) to scale -9 truncates
373 // the "999" entirely, landing back on exactly 1000000 --
374 // i.e. the same as T. actualDelta = 0 => tecPRECISION_LOSS.
375 // This is the ambient-rounding leak the Downward guard on
376 // the credit-side sum exists to close; this row is a
377 // regression test that the guard is doing its job.
378 .name = "IOU credit: Finding-1 regression, ToNearest sum would overcredit",
379 .assetsTotal = Number{1'000'000, 0},
380 .delta = Number{9'999'999'999'999'999LL, -25},
381 .expected = std::nullopt,
382 },
383 Case{
384 // Same shape as the row above, but delta = +9.995e-10 is a
385 // 19-digit half-even tie at the position-(-12) cusp: the
386 // remainder below the retained "999" digits is exactly
387 // 0.5 ULP, and ToNearest ties-to-even rounds the (odd) "9"
388 // up, carrying the same way. Downward-guarded posterior
389 // still truncates to "...000999" and floors back to T, so
390 // the outcome is identical: tecPRECISION_LOSS.
391 .name = "IOU credit: Finding-1 regression, 19-digit half-even tie",
392 .assetsTotal = Number{1'000'000, 0},
393 .delta = Number{9'995, -13},
394 .expected = std::nullopt,
395 },
396 Case{
397 // T = 0, delta = +3.7e-5. Posterior grid is delta's own
398 // scale (postScale = -20, the canonical exponent of
399 // 3.7e-5), so the magnitude is trivially unchanged.
400 .name = "IOU credit: zero-total vault",
401 .assetsTotal = Number{0},
402 .delta = Number{37, -6},
403 .expected = Number{37, -6},
404 },
405 Case{
406 // T = 1000000, delta = +4e-10. Exact sum needs 17
407 // significant digits (leading "1" at position 6, trailing
408 // "4" at position -10); rounding to 16 digits drops the "4"
409 // entirely (0.4 ULP at scale -9 rounds down under both
410 // ToNearest and Downward), so postScale = -9 and the
411 // Downward-guarded posterior floors straight back to T.
412 // actualDelta = 0 => tecPRECISION_LOSS.
413 .name = "IOU credit: sub-ULP credit",
414 .assetsTotal = Number{1'000'000, 0},
415 .delta = Number{4, -10},
416 .expected = std::nullopt,
417 },
418 };
419
420 runCases(iou, cases);
421 }
422
423 // -------------------------------------------------------------------
424 // Integral assets (XRP, MPT): rounding is a no-op, magnitude is
425 // returned unchanged and positive regardless of delta's sign. This is
426 // a regression test for a signed-return bug: the function must not
427 // hand back a negative delta for a debit.
428 // -------------------------------------------------------------------
429 void
430 testIntegralAssets(Asset const& mpt, Asset const& xrp)
431 {
432 std::array<Case, 2> const mptCases{
433 Case{
434 .name = "MPT debit: magnitude is positive, not the signed delta",
435 .assetsTotal = Number{1'000'000},
436 .delta = Number{-5},
437 .expected = Number{5},
438 },
439 Case{
440 .name = "MPT credit: unchanged",
441 .assetsTotal = Number{1'000'000},
442 .delta = Number{7},
443 .expected = Number{7},
444 },
445 };
446 runCases(mpt, mptCases);
447
448 std::array<Case, 2> const xrpCases{
449 Case{
450 .name = "XRP debit: magnitude is positive, not the signed delta",
451 .assetsTotal = Number{100'000},
452 .delta = Number{-3},
453 .expected = Number{3},
454 },
455 Case{
456 .name = "XRP credit: unchanged",
457 .assetsTotal = Number{100'000},
458 .delta = Number{10},
459 .expected = Number{10},
460 },
461 };
462 runCases(xrp, xrpCases);
463 }
464
465public:
466 void
467 run() override
468 {
470
471 test::jtx::Account const issuer{"issuer"};
472 Issue const iou{toCurrency("USD"), issuer.id()};
473 MPTIssue const mpt{makeMptID(1, issuer.id())};
474 Issue const xrp = xrpIssue();
475
476 testIouDebits(iou);
477 testIouCredits(iou);
478 testIntegralAssets(mpt, xrp);
479 }
480};
481
482BEAST_DEFINE_TESTSUITE(VaultHelpers, app, xrpl);
483
484} // namespace xrpl
A testsuite class.
Definition suite.h:52
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
bool integral() const
Definition Asset.h:133
A currency issued by an account.
Definition Issue.h:18
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
constexpr rep mantissa() const noexcept
Returns the mantissa of the external view of the Number.
Definition Number.h:692
static MantissaRange::MantissaScale getMantissaScale()
Returns which mantissa scale is currently in use for normalization.
constexpr int exponent() const noexcept
Returns the exponent of the external view of the Number.
Definition Number.h:714
std::string getText() const override
Definition STAmount.cpp:647
void testIntegralAssets(Asset const &mpt, Asset const &xrp)
static std::shared_ptr< SLE > makeVault(Asset const &asset, Number const &assetsTotal)
void testIouCredits(Asset const &iou)
void run() override
Runs the suite.
void testIouDebits(Asset const &iou)
void runCases(Asset const &asset, std::array< Case, N > const &cases)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
T make_shared(T... args)
T min(T... args)
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
std::expected< STAmount, TER > clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const &delta)
Adjusts a requested asset change (delta) to match the decimal scale of the updated total vault assets...
bool toCurrency(Currency &, std::string const &)
Tries to convert a string to a Currency, returns true on success.
Definition UintTypes.cpp:65
std::string transToken(TER code)
Definition TER.cpp:257
STAmount roundToScale(STAmount const &value, std::int32_t scale, Number::RoundingMode rounding=Number::getround())
Round an arbitrary precision Amount to the precision of an STAmount that has a given exponent.
BaseUInt< 256 > UInt256
Definition base_uint.h:580
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:206
constexpr Number abs(Number x) noexcept
Definition Number.h:876
@ tecPRECISION_LOSS
Definition TER.h:371
void associateAsset(STLedgerEntry &sle, Asset const &asset)
Associate an Asset with all sMD_NeedsAsset fields in a ledger entry.
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
std::optional< Number > expected
T to_string(T... args)