From f012e3d305952e1adfe30cccbda71688778398cb Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:33:27 +0200 Subject: [PATCH] feat: Add sfDust field on RippleState Introduces the sfDust NUMBER field (id 18) on ltRIPPLE_STATE, a sub-quantum residual for the trust-line credit path. Together with sfBalance it forms the trust line's extended balance. kSmdNeedsAsset is deliberately omitted: that flag rounds the field to the asset's representable precision via associateAsset(), which is the opposite of sfDust's purpose (it exists to hold value finer than that precision). No writer or consumer of the field yet; that lands in follow-ups. Additive-only: - New sfields.macro entry with rationale - ltRIPPLE_STATE gains an SoeDefault sfDust slot - Regenerated RippleState.h getters/setters + hasDust - LedgerEntry.h.mako gains a conditional STNumber.h include so any NUMBER-bearing ledger entry pulls STNumber directly (avoids transitively relying on STLedgerEntry.h) - RippleStateTests round-trip and nullopt coverage for sfDust - .cspell.config.yaml picks up 'recognise*'/'renormalise*' used in follow-up docs and code --- .cspell.config.yaml | 7 ++++ .../codegen/templates/LedgerEntry.h.mako | 3 ++ .../xrpl/protocol/detail/ledger_entries.macro | 1 + include/xrpl/protocol/detail/sfields.macro | 10 ++++++ .../ledger_entries/RippleState.h | 36 +++++++++++++++++++ .../ledger_entries/RippleStateTests.cpp | 27 ++++++++++++++ 6 files changed, 84 insertions(+) diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 21b0145f43..5389f99be9 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -248,6 +248,13 @@ words: - queuable - Raphson - rcflags + - recognise + - recognises + - recognised + - renormalise + - renormalises + - renormalised + - renormalisation - replayer - rerandomize - rerandomization diff --git a/cmake/scripts/codegen/templates/LedgerEntry.h.mako b/cmake/scripts/codegen/templates/LedgerEntry.h.mako index c799903b21..4e328110da 100644 --- a/cmake/scripts/codegen/templates/LedgerEntry.h.mako +++ b/cmake/scripts/codegen/templates/LedgerEntry.h.mako @@ -2,6 +2,9 @@ #pragma once #include +% if any(field.get('stiSuffix') == 'NUMBER' for field in fields): +#include // IWYU pragma: keep +% endif #include #include #include diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index ffcd025f01..64fca8cd3a 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -291,6 +291,7 @@ LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({ {sfHighQualityOut, SoeOptional}, {sfHighSponsor, SoeOptional}, {sfLowSponsor, SoeOptional}, + {sfDust, SoeDefault}, })) /** The ledger object which lists the network's fee settings. diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index cff075e738..d89a3a6cbf 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -236,6 +236,16 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault) TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault) +// sfDust is a sub-quantum residual on a trust line: the fraction of a credit +// that could not be represented in sfBalance at the caller-chosen accounting +// scale, parked here so the extended quantity (sfBalance + sfDust) preserves +// its exact value across successive rounded credits. Deliberately WITHOUT +// kSmdNeedsAsset: that flag rounds the field to the asset's representable +// precision via associateAsset(), which is the opposite of this field's +// purpose (it exists to hold value finer than that precision). Do not add +// kSmdNeedsAsset to this entry — see the "Why sfDust" section in +// docs/dust-mechanism.md. +TYPED_SFIELD(sfDust, NUMBER, 18, SField::kSmdDefault) // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) diff --git a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h index dda1b78e66..a3b2ec1327 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h @@ -2,6 +2,7 @@ #pragma once #include +#include // IWYU pragma: keep #include #include #include @@ -291,6 +292,30 @@ public: { return this->sle_->isFieldPresent(sfLowSponsor); } + + /** + * @brief Get sfDust (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDust() const + { + if (hasDust()) + return this->sle_->at(sfDust); + return std::nullopt; + } + + /** + * @brief Check if sfDust is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDust() const + { + return this->sle_->isFieldPresent(sfDust); + } }; /** @@ -482,6 +507,17 @@ public: return *this; } + /** + * @brief Set sfDust (SoeDefault) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setDust(std::decay_t const& value) + { + object_[sfDust] = value; + return *this; + } + /** * @brief Build and return the completed RippleState wrapper. * @param index The ledger entry index. diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp index de0769793b..0f791eb0e5 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp @@ -33,6 +33,7 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) auto const highQualityOutValue = canonical_UINT32(); auto const highSponsorValue = canonical_ACCOUNT(); auto const lowSponsorValue = canonical_ACCOUNT(); + auto const dustValue = canonical_NUMBER(); RippleStateBuilder builder{ balanceValue, @@ -50,6 +51,7 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) builder.setHighQualityOut(highQualityOutValue); builder.setHighSponsor(highSponsorValue); builder.setLowSponsor(lowSponsorValue); + builder.setDust(dustValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -154,6 +156,14 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasLowSponsor()); } + { + auto const& expected = dustValue; + auto const actualOpt = entry.getDust(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfDust"); + EXPECT_TRUE(entry.hasDust()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -180,6 +190,7 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) auto const highQualityOutValue = canonical_UINT32(); auto const highSponsorValue = canonical_ACCOUNT(); auto const lowSponsorValue = canonical_ACCOUNT(); + auto const dustValue = canonical_NUMBER(); auto sle = std::make_shared(RippleState::entryType, index); @@ -196,6 +207,7 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) sle->at(sfHighQualityOut) = highQualityOutValue; sle->at(sfHighSponsor) = highSponsorValue; sle->at(sfLowSponsor) = lowSponsorValue; + sle->at(sfDust) = dustValue; RippleStateBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -360,6 +372,19 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfLowSponsor"); } + { + auto const& expected = dustValue; + + auto const fromSleOpt = entryFromSle.getDust(); + auto const fromBuilderOpt = entryFromBuilder.getDust(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfDust"); + expectEqualField(expected, *fromBuilderOpt, "sfDust"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -438,5 +463,7 @@ TEST(RippleStateTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getHighSponsor().has_value()); EXPECT_FALSE(entry.hasLowSponsor()); EXPECT_FALSE(entry.getLowSponsor().has_value()); + EXPECT_FALSE(entry.hasDust()); + EXPECT_FALSE(entry.getDust().has_value()); } }