From f04d065099cfdb58da64d9f60a55cc7f96bfd833 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 bb763e9935..2dedb010b1 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 f166473d7f..a30262ec01 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 ec05804253..4b5ed84af3 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -230,6 +230,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) // 32-bit signed (common) 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()); } }