From 92c84471fd447ce7b4e280af1bbe51709c1b28ef Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 19 Feb 2026 18:52:59 +0900 Subject: [PATCH] `fixEmptyDID`: fix amendment to handle empty DID edge case: (#4950) This amendment fixes an edge case where an empty DID object can be created. It adds an additional check to ensure that DIDs are non-empty when created, and returns a `tecEMPTY_DID` error if the DID would be empty. --- src/ripple/app/tx/impl/DID.cpp | 7 +++++++ src/ripple/protocol/Feature.h | 3 ++- src/ripple/protocol/impl/Feature.cpp | 3 ++- src/test/app/DID_test.cpp | 28 ++++++++++++++++++++++------ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/ripple/app/tx/impl/DID.cpp b/src/ripple/app/tx/impl/DID.cpp index c92162c83..1cad8992a 100644 --- a/src/ripple/app/tx/impl/DID.cpp +++ b/src/ripple/app/tx/impl/DID.cpp @@ -161,6 +161,13 @@ DIDSet::doApply() set(sfURI); set(sfDIDDocument); set(sfData); + if (ctx_.view().rules().enabled(fixEmptyDID) && + !sleDID->isFieldPresent(sfURI) && + !sleDID->isFieldPresent(sfDIDDocument) && + !sleDID->isFieldPresent(sfData)) + { + return tecEMPTY_DID; + } return addSLE(ctx_, sleDID, account_); } diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index fb30f23cb..3fbc88e65 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 102; +static constexpr std::size_t numFeatures = 103; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -390,6 +390,7 @@ extern uint256 const fixNFTokenReserve; extern uint256 const fixInnerObjTemplate; extern uint256 const fixAMMOverflowOffer; extern uint256 const featurePriceOracle; +extern uint256 const fixEmptyDID; } // namespace ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 2c8b0fcfc..283a622fe 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -491,11 +491,12 @@ REGISTER_FEATURE(AMM, Supported::yes, VoteBehavior::De REGISTER_FEATURE(XChainBridge, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::DefaultNo); -REGISTER_FIX(fixFillOrKill, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FIX (fixFillOrKill, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixNFTokenReserve, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixInnerObjTemplate, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixAMMOverflowOffer, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FEATURE(PriceOracle, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FIX (fixEmptyDID, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/test/app/DID_test.cpp b/src/test/app/DID_test.cpp index ea79d90b6..43435b6ff 100644 --- a/src/test/app/DID_test.cpp +++ b/src/test/app/DID_test.cpp @@ -122,7 +122,7 @@ struct DID_test : public beast::unit_test::suite using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; Account const alice{"alice"}; env.fund(XRP(5000), alice); env.close(); @@ -169,6 +169,16 @@ struct DID_test : public beast::unit_test::suite env.close(); BEAST_EXPECT(ownerCount(env, alice) == 0); + // some empty fields, some optional fields + // pre-fix amendment + auto const fixEnabled = env.current()->rules().enabled(fixEmptyDID); + env(did::set(alice), + did::uri(""), + fixEnabled ? ter(tecEMPTY_DID) : ter(tesSUCCESS)); + env.close(); + auto const expectedOwnerReserve = fixEnabled ? 0 : 1; + BEAST_EXPECT(ownerCount(env, alice) == expectedOwnerReserve); + // Modifying a DID to become empty is checked in testSetModify } @@ -180,7 +190,7 @@ struct DID_test : public beast::unit_test::suite using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; Account const alice{"alice"}; env.fund(XRP(5000), alice); env.close(); @@ -211,7 +221,7 @@ struct DID_test : public beast::unit_test::suite using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; Account const alice{"alice"}; Account const bob{"bob"}; Account const charlie{"charlie"}; @@ -265,7 +275,7 @@ struct DID_test : public beast::unit_test::suite using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; Account const alice{"alice"}; env.fund(XRP(5000), alice); env.close(); @@ -382,13 +392,19 @@ struct DID_test : public beast::unit_test::suite run() override { using namespace test::jtx; - FeatureBitset const all{ - supported_amendments() | FeatureBitset{featureDID}}; + FeatureBitset const all{supported_amendments()}; + FeatureBitset const emptyDID{fixEmptyDID}; testEnabled(all); testAccountReserve(all); testSetInvalid(all); testDeleteInvalid(all); testSetModify(all); + + testEnabled(all - emptyDID); + testAccountReserve(all - emptyDID); + testSetInvalid(all - emptyDID); + testDeleteInvalid(all - emptyDID); + testSetModify(all - emptyDID); } };