diff --git a/src/test/app/RippleStateSfDust_test.cpp b/src/test/app/RippleStateSfDust_test.cpp new file mode 100644 index 0000000000..643d0f1294 --- /dev/null +++ b/src/test/app/RippleStateSfDust_test.cpp @@ -0,0 +1,139 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: keep +#include + +#include +#include +#include +#include + +// Tests for the sfDust field defined in this PR: read-side default value +// on an untouched trust line, and the SoeDefault byte-encoding contract +// that keeps pre-amendment and post-amendment ledgers hash-compatible for +// any RippleState that has never carried non-zero dust. +// +// Kept intentionally free of DustSplit and vault_dust:: references — the +// writer path lives in follow-up PRs. + +namespace xrpl::test { + +class RippleStateSfDust_test : public beast::unit_test::suite +{ + FeatureBitset const all_{jtx::testableAmendments()}; + + void + testAbsentReadsZero() + { + testcase("sfDust absent on an untouched trust line reads as zero"); + + using namespace jtx; + Env env{*this, all_}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(1'000), alice, bob); + env.close(); + PrettyAsset const asset = alice["USD"]; + env(trust(bob, asset(1'000))); + env.close(); + + auto const line = + env.le(keylet::trustLine(alice.id(), bob.id(), asset.raw().get().currency)); + if (!BEAST_EXPECT(line)) + return; + BEAST_EXPECT(!line->isFieldPresent(sfDust)); + BEAST_EXPECT(Number{line->at(sfDust)} == beast::kZero); + } + + // Golden-byte compatibility contract for sfDust: + // + // • sfDust is declared SoeDefault(0) in ledger_entries.macro. + // • SoeDefault semantics: absent field == field set to default + // value, both encode to the identical byte string. This is the + // property that lets a pre-amendment ledger and a post-amendment + // ledger co-exist for RippleState entries that have never carried + // non-zero dust. + // + // If a future refactor accidentally strips SoeDefault, or a codec + // change reserialises absent-and-zero differently, this test fires + // immediately with a byte-diff, preserving ledger hashability of + // existing state. + void + testGoldenByteCompat() + { + testcase("sfDust SoeDefault byte-encoding contract"); + + using namespace jtx; + Env const env{*this}; // amendment status is irrelevant for this test + + Account const alice{"alice_g"}; + Account const issuer{"issuer_g"}; + + AccountID const aliceId = alice.id(); + AccountID const issuerId = issuer.id(); + auto const [low, high] = std::minmax(aliceId, issuerId); + Issue const usd{toCurrency("USD"), high}; + Keylet const kl = keylet::trustLine(low, high, usd.currency); + + auto const makeSle = [&](std::optional dustValue) { + auto sle = std::make_shared(kl); + sle->setFieldAmount(sfBalance, STAmount{usd, 0}); + sle->setFieldAmount(sfLowLimit, STAmount{Issue{usd.currency, low}, 1000}); + sle->setFieldAmount(sfHighLimit, STAmount{Issue{usd.currency, high}, 1000}); + sle->setFieldU32(sfPreviousTxnLgrSeq, 42u); + sle->setFieldH256(sfPreviousTxnID, uint256{7u}); + if (dustValue) + sle->at(sfDust) = *dustValue; + return sle; + }; + + auto const bytesOf = [](std::shared_ptr const& sle) { + return strHex(sle->getSerializer().peekData()); + }; + + // Case A: sfDust absent (SoeDefault path — never set on the SLE). + auto const bytesAbsent = bytesOf(makeSle(std::nullopt)); + // Case B: sfDust explicitly assigned zero (should be collapsed + // to absent by SoeDefault). + auto const bytesZero = bytesOf(makeSle(Number{0})); + // Case C: sfDust explicitly non-zero (post-dust-op shape). + auto const bytesNonzero = bytesOf(makeSle(Number{7, -12})); + + BEAST_EXPECTS( + bytesAbsent == bytesZero, + "SoeDefault contract broken: absent-sfDust encoding (" + bytesAbsent + + ") differs from explicit-zero encoding (" + bytesZero + ")"); + BEAST_EXPECT(bytesAbsent != bytesNonzero); + BEAST_EXPECT(bytesZero != bytesNonzero); + } + +public: + void + run() override + { + testAbsentReadsZero(); + testGoldenByteCompat(); + } +}; + +BEAST_DEFINE_TESTSUITE(RippleStateSfDust, app, xrpl); + +} // namespace xrpl::test