From 646b3aa269ca03a44a877c506c6531033e87897a Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 29 May 2026 10:50:44 -0400 Subject: [PATCH] do an audit of fields --- .../xrpl/protocol/detail/ledger_entries.macro | 8 +- src/test/app/Invariants_test.cpp | 73 ++++++++++++++++++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 9942699f0d..d9f1b24c98 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -101,7 +101,7 @@ LEDGER_ENTRY(ltNFTOKEN_PAGE, 0x0050, NFTokenPage, nft_page, ({ // All fields are SoeRequired because there is always a SignerEntries. // If there are no SignerEntries the node is deleted. LEDGER_ENTRY(ltSIGNER_LIST, 0x0053, SignerList, signer_list, ({ - {sfOwner, SoeOptional, SoeImmutable}, + {sfOwner, SoeOptional, SoeImmutableSetOnce}, {sfOwnerNode, SoeRequired, SoeImmutable}, {sfSignerQuorum, SoeRequired, SoeMutable}, {sfSignerEntries, SoeRequired, SoeMutable}, @@ -391,7 +391,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfSequence, SoeRequired, SoeImmutable}, {sfTransferFee, SoeDefault, SoeMutable}, {sfOwnerNode, SoeRequired, SoeImmutable}, - {sfAssetScale, SoeDefault, SoeMutable}, + {sfAssetScale, SoeDefault, SoeImmutable}, {sfMaximumAmount, SoeOptional, SoeImmutable}, {sfOutstandingAmount, SoeRequired, SoeMutable}, {sfLockedAmount, SoeOptional, SoeMutable}, @@ -421,10 +421,10 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ */ LEDGER_ENTRY(ltORACLE, 0x0080, Oracle, oracle, ({ {sfOwner, SoeRequired, SoeImmutable}, - {sfOracleDocumentID, SoeOptional, SoeImmutable}, + {sfOracleDocumentID, SoeOptional, SoeImmutableSetOnce}, {sfProvider, SoeRequired, SoeImmutable}, {sfPriceDataSeries, SoeRequired, SoeMutable}, - {sfAssetClass, SoeRequired, SoeMutable}, + {sfAssetClass, SoeRequired, SoeImmutable}, {sfLastUpdateTime, SoeRequired, SoeMutable}, {sfURI, SoeOptional, SoeMutable}, {sfOwnerNode, SoeRequired, SoeImmutable}, diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 900f9685e3..12e6beacaa 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -2320,7 +2321,7 @@ class Invariants_test : public beast::unit_test::Suite {tesSUCCESS, tesSUCCESS}); } - // Without fixConstantInvariant, old hardcoded path should + // Without fixImmutableInvariant, old hardcoded path should // still catch sfLedgerEntryType/sfLedgerIndex changes { auto const mods = std::to_array>({ @@ -2331,7 +2332,7 @@ class Invariants_test : public beast::unit_test::Suite for (auto const& mod : mods) { doInvariantCheck( - Env(*this, defaultAmendments() - fixConstantInvariant), + Env(*this, defaultAmendments() - fixImmutableInvariant), {{"changed an unchangeable field"}}, [&](Account const& a1, Account const&, ApplyContext& ac) { auto sle = ac.view().peek(keylet::account(a1.id())); @@ -2344,12 +2345,12 @@ class Invariants_test : public beast::unit_test::Suite } } - // Without fixConstantInvariant, modifying a SoeImmutable field + // Without fixImmutableInvariant, modifying a SoeImmutable field // that is NOT sfLedgerEntryType/sfLedgerIndex on a non-loan // type should NOT fail (old code doesn't check it) { doInvariantCheck( - Env(*this, defaultAmendments() - fixConstantInvariant), + Env(*this, defaultAmendments() - fixImmutableInvariant), {}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto sle = ac.view().peek(keylet::account(a1.id())); @@ -2363,6 +2364,70 @@ class Invariants_test : public beast::unit_test::Suite STTx{ttACCOUNT_SET, [](STObject&) {}}, {tesSUCCESS, tesSUCCESS}); } + + // Template-based checks: SoeImmutableSetOnce field on Oracle. + { + Keylet oracleKeylet = keylet::amendments(); + Preclose const createLegacyOracle = [this, &oracleKeylet]( + Account const& a, Account const&, Env& env) { + jtx::oracle::Oracle const oracle{env, {.owner = a.id()}}; + oracleKeylet = keylet::oracle(a.id(), oracle.documentID()); + + auto const sle = env.le(oracleKeylet); + return BEAST_EXPECT(sle && !sle->isFieldPresent(sfOracleDocumentID)); + }; + + doInvariantCheck( + Env(*this, defaultAmendments() - fixIncludeKeyletFields), + {}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(oracleKeylet); + if (!sle) + return false; + sle->at(sfOracleDocumentID) = std::uint32_t{1}; + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tesSUCCESS, tesSUCCESS}, + createLegacyOracle); + } + + { + Keylet oracleKeylet = keylet::amendments(); + Preclose const createOracle = [this, &oracleKeylet]( + Account const& a, Account const&, Env& env) { + jtx::oracle::Oracle const oracle{env, {.owner = a.id()}}; + oracleKeylet = keylet::oracle(a.id(), oracle.documentID()); + + auto const sle = env.le(oracleKeylet); + return BEAST_EXPECT(sle && sle->isFieldPresent(sfOracleDocumentID)); + }; + + auto const mods = std::to_array>({ + [](SLE::pointer& sle) { sle->at(sfOracleDocumentID) = std::uint32_t{2}; }, + [](SLE::pointer& sle) { sle->makeFieldAbsent(sfOracleDocumentID); }, + }); + + for (auto const& mod : mods) + { + doInvariantCheck( + {{"changed an unchangeable field"}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(oracleKeylet); + if (!sle) + return false; + mod(sle); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createOracle); + } + } } void