diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 7470b8c64..d3f297cfe 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -546,6 +546,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/CronSet.cpp src/ripple/app/tx/impl/DeleteAccount.cpp src/ripple/app/tx/impl/DepositPreauth.cpp + src/ripple/app/tx/impl/DID.cpp src/ripple/app/tx/impl/Escrow.cpp src/ripple/app/tx/impl/GenesisMint.cpp src/ripple/app/tx/impl/Import.cpp @@ -828,6 +829,7 @@ if (tests) src/test/app/DeliverMin_test.cpp src/test/app/DepositAuth_test.cpp src/test/app/Discrepancy_test.cpp + src/test/app/DID_test.cpp src/test/app/DNS_test.cpp src/test/app/Escrow_test.cpp src/test/app/FeeVote_test.cpp @@ -996,6 +998,7 @@ if (tests) src/test/jtx/impl/cron.cpp src/test/jtx/impl/delivermin.cpp src/test/jtx/impl/deposit.cpp + src/test/jtx/impl/did.cpp src/test/jtx/impl/envconfig.cpp src/test/jtx/impl/fee.cpp src/test/jtx/impl/flags.cpp diff --git a/hook/sfcodes.h b/hook/sfcodes.h index bc9ee8884..d0a138809 100644 --- a/hook/sfcodes.h +++ b/hook/sfcodes.h @@ -200,6 +200,8 @@ #define sfHookParameterName ((7U << 16U) + 24U) #define sfHookParameterValue ((7U << 16U) + 25U) #define sfBlob ((7U << 16U) + 26U) +#define sfDIDDocument ((7U << 16U) + 27U) +#define sfData ((7U << 16U) + 28U) #define sfRemarkValue ((7U << 16U) + 98U) #define sfRemarkName ((7U << 16U) + 99U) #define sfAccount ((8U << 16U) + 1U) diff --git a/hook/tts.h b/hook/tts.h index 4fd78bc03..8cf07a294 100644 --- a/hook/tts.h +++ b/hook/tts.h @@ -48,6 +48,8 @@ #define ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION 55 #define ttXCHAIN_MODIFY_BRIDGE 56 #define ttXCHAIN_CREATE_BRIDGE 57 +#define ttDID_SET 58 +#define ttDID_DELETE 59 #define ttCRON 92 #define ttCRON_SET 93 #define ttREMARKS_SET 94 diff --git a/src/ripple/app/tx/impl/DID.cpp b/src/ripple/app/tx/impl/DID.cpp new file mode 100644 index 000000000..c92162c83 --- /dev/null +++ b/src/ripple/app/tx/impl/DID.cpp @@ -0,0 +1,226 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +/* + DID + ====== + + Decentralized Identifiers (DIDs) are a new type of identifier that enable + verifiable, self-sovereign digital identity and are designed to be + compatible with any distributed ledger or network. This implementation + conforms to the requirements specified in the DID v1.0 specification + currently recommended by the W3C Credentials Community Group + (https://www.w3.org/TR/did-core/). +*/ + +//------------------------------------------------------------------------------ + +NotTEC +DIDSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureDID)) + return temDISABLED; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!ctx.tx.isFieldPresent(sfURI) && + !ctx.tx.isFieldPresent(sfDIDDocument) && !ctx.tx.isFieldPresent(sfData)) + return temEMPTY_DID; + + if (ctx.tx.isFieldPresent(sfURI) && ctx.tx[sfURI].empty() && + ctx.tx.isFieldPresent(sfDIDDocument) && ctx.tx[sfDIDDocument].empty() && + ctx.tx.isFieldPresent(sfData) && ctx.tx[sfData].empty()) + return temEMPTY_DID; + + auto isTooLong = [&](auto const& sField, std::size_t length) -> bool { + if (auto field = ctx.tx[~sField]) + return field->length() > length; + return false; + }; + + if (isTooLong(sfURI, maxDIDURILength) || + isTooLong(sfDIDDocument, maxDIDDocumentLength) || + isTooLong(sfData, maxDIDAttestationLength)) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +addSLE( + ApplyContext& ctx, + std::shared_ptr const& sle, + AccountID const& owner) +{ + auto const sleAccount = ctx.view().peek(keylet::account(owner)); + if (!sleAccount) + return tefINTERNAL; + + // Check reserve availability for new object creation + { + auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); + auto const reserve = + ctx.view().fees().accountReserve((*sleAccount)[sfOwnerCount] + 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + // Add ledger object to ledger + ctx.view().insert(sle); + + // Add ledger object to owner's page + { + auto page = ctx.view().dirInsert( + keylet::ownerDir(owner), sle->key(), describeOwnerDir(owner)); + if (!page) + return tecDIR_FULL; + (*sle)[sfOwnerNode] = *page; + } + adjustOwnerCount(ctx.view(), sleAccount, 1, ctx.journal); + ctx.view().update(sleAccount); + + return tesSUCCESS; +} + +TER +DIDSet::doApply() +{ + // Edit ledger object if it already exists + Keylet const didKeylet = keylet::did(account_); + if (auto const sleDID = ctx_.view().peek(didKeylet)) + { + auto update = [&](auto const& sField) { + if (auto const field = ctx_.tx[~sField]) + { + if (field->empty()) + { + sleDID->makeFieldAbsent(sField); + } + else + { + (*sleDID)[sField] = *field; + } + } + }; + update(sfURI); + update(sfDIDDocument); + update(sfData); + + if (!sleDID->isFieldPresent(sfURI) && + !sleDID->isFieldPresent(sfDIDDocument) && + !sleDID->isFieldPresent(sfData)) + { + return tecEMPTY_DID; + } + ctx_.view().update(sleDID); + return tesSUCCESS; + } + + // Create new ledger object otherwise + auto const sleDID = std::make_shared(didKeylet); + (*sleDID)[sfAccount] = account_; + + auto set = [&](auto const& sField) { + if (auto const field = ctx_.tx[~sField]; field && !field->empty()) + (*sleDID)[sField] = *field; + }; + + set(sfURI); + set(sfDIDDocument); + set(sfData); + + return addSLE(ctx_, sleDID, account_); +} + +NotTEC +DIDDelete::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureDID)) + return temDISABLED; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + return preflight2(ctx); +} + +TER +DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner) +{ + auto const sle = ctx.view().peek(sleKeylet); + if (!sle) + return tecNO_ENTRY; + + return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal); +} + +TER +DIDDelete::deleteSLE( + ApplyView& view, + std::shared_ptr sle, + AccountID const owner, + beast::Journal j) +{ + // Remove object from owner directory + if (!view.dirRemove( + keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true)) + { + JLOG(j.fatal()) << "Unable to delete DID Token from owner."; + return tefBAD_LEDGER; + } + + auto const sleOwner = view.peek(keylet::account(owner)); + if (!sleOwner) + return tecINTERNAL; + + adjustOwnerCount(view, sleOwner, -1, j); + view.update(sleOwner); + + // Remove object from ledger + view.erase(sle); + return tesSUCCESS; +} + +TER +DIDDelete::doApply() +{ + return deleteSLE(ctx_, keylet::did(account_), account_); +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/DID.h b/src/ripple/app/tx/impl/DID.h new file mode 100644 index 000000000..13d5a2615 --- /dev/null +++ b/src/ripple/app/tx/impl/DID.h @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_DID_H_INCLUDED +#define RIPPLE_TX_DID_H_INCLUDED + +#include + +namespace ripple { + +class DIDSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit DIDSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +class DIDDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit DIDDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner); + + static TER + deleteSLE( + ApplyView& view, + std::shared_ptr sle, + AccountID const owner, + beast::Journal j); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/DeleteAccount.cpp b/src/ripple/app/tx/impl/DeleteAccount.cpp index c2ba62262..a5e4b056a 100644 --- a/src/ripple/app/tx/impl/DeleteAccount.cpp +++ b/src/ripple/app/tx/impl/DeleteAccount.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -163,6 +164,18 @@ removeGeneric( return tesSUCCESS; } +TER +removeDIDFromLedger( + Application& app, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return DIDDelete::deleteSLE(view, sleDel, account, j); +} + // Return nullptr if the LedgerEntryType represents an obligation that can't // be deleted. Otherwise return the pointer to the function that can delete // the non-obligation @@ -183,6 +196,8 @@ nonObligationDeleter(LedgerEntryType t) return removeNFTokenOfferFromLedger; case ltURI_TOKEN: return removeGeneric; + case ltDID: + return removeDIDFromLedger; default: return nullptr; } diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 53339059b..a06078d8b 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -512,6 +512,7 @@ LedgerEntryTypesMatch::visitEntry( case ltBRIDGE: case ltXCHAIN_OWNED_CLAIM_ID: case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: + case ltDID: break; default: invalidTypeAdded_ = true; diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 62a55af2f..e75ab420b 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -191,6 +192,10 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttXCHAIN_ACCOUNT_CREATE_COMMIT: return f.template operator()(); + case ttDID_SET: + return f.template operator()(); + case ttDID_DELETE: + return f.template operator()(); default: throw UnknownTxnType(txnType); } diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 370b5d9dc..2299d87ef 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 = 96; +static constexpr std::size_t numFeatures = 97; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -384,6 +384,7 @@ extern uint256 const fixReducedOffersV1; extern uint256 const featureAMM; extern uint256 const featureXChainBridge; extern uint256 const fixDisallowIncomingV1; +extern uint256 const featureDID; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index e839cf325..c03734a8c 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -317,6 +317,9 @@ xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq); Keylet xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); +Keylet +did(AccountID const& account) noexcept; + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 6915b88d2..98167ed10 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -210,6 +210,12 @@ enum LedgerEntryType : std::uint16_t */ ltAMM = 0x0079, + /** The ledger object which tracks the DID. + + \sa keylet::did + */ + ltDID = 0x008D, + //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. diff --git a/src/ripple/protocol/Protocol.h b/src/ripple/protocol/Protocol.h index 055d096b1..81790c69a 100644 --- a/src/ripple/protocol/Protocol.h +++ b/src/ripple/protocol/Protocol.h @@ -83,6 +83,15 @@ std::uint16_t constexpr maxTransferFee = 50000; /** The maximum length of a URI inside an NFT */ std::size_t constexpr maxTokenURILength = 256; +/** The maximum length of a Data element inside a DID */ +std::size_t constexpr maxDIDDocumentLength = 256; + +/** The maximum length of a URI inside a DID */ +std::size_t constexpr maxDIDURILength = 256; + +/** The maximum length of an Attestation inside a DID */ +std::size_t constexpr maxDIDAttestationLength = 256; + /** The maximum length of a domain */ std::size_t constexpr maxDomainLength = 256; diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index ac70f8cf3..086578a82 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -553,6 +553,8 @@ extern SF_VL const sfCreateCode; extern SF_VL const sfMemoType; extern SF_VL const sfMemoData; extern SF_VL const sfMemoFormat; +extern SF_VL const sfDIDDocument; +extern SF_VL const sfData; // variable length (uncommon) extern SF_VL const sfFulfillment; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 847931888..520dee8f2 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -137,6 +137,7 @@ enum TEMcodes : TERUnderlyingType { temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, temHOOK_DATA_TOO_LARGE, + temEMPTY_DID, }; //------------------------------------------------------------------------------ @@ -346,6 +347,7 @@ enum TECcodes : TERUnderlyingType { tecHAS_HOOK_STATE = 190, tecINCOMPLETE = 191, // 192: tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR + tecEMPTY_DID = 193, tecLAST_POSSIBLE_ENTRY = 255, }; diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index c41e4a5bf..d8b79c96f 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -191,6 +191,12 @@ enum TxType : std::uint16_t /** This transactions creates a sidechain */ ttXCHAIN_CREATE_BRIDGE = 57, + /** This transaction type creates or updates a DID */ + ttDID_SET = 58, + + /** This transaction type deletes a DID */ + ttDID_DELETE = 59, + /* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */ ttCRON = 92, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 3c4e13353..de7d01d2e 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -489,7 +489,8 @@ REGISTER_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::De REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(XChainBridge, Supported::yes, VoteBehavior::DefaultNo); -REGISTER_FIX(fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FIX (fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index ba24c5783..4fb51a69f 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -81,6 +81,7 @@ enum class LedgerNameSpace : std::uint16_t { BRIDGE = 'H', XCHAIN_CLAIM_ID = 'Q', XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', + DID = 'I', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -563,6 +564,12 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq) seq)}; } +Keylet +did(AccountID const& account) noexcept +{ + return {ltDID, indexHash(LedgerNameSpace::DID, account)}; +} + } // namespace keylet } // namespace ripple diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 638078b91..b052f1cb1 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -441,6 +441,19 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + + add(jss::DID, + ltDID, + { + {sfAccount, soeREQUIRED}, + {sfDIDDocument, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, + {sfData, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); // clang-format on } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index df6aef7d7..ac72aae9a 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -326,6 +326,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL, CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24); CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25); CONSTRUCT_TYPED_SFIELD(sfBlob, "Blob", VL, 26); +CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 27); +CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 28); CONSTRUCT_TYPED_SFIELD(sfRemarkValue, "RemarkValue", VL, 98); CONSTRUCT_TYPED_SFIELD(sfRemarkName, "RemarkName", VL, 99); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 6e3b53d87..a59d90ed2 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -120,6 +120,7 @@ transResults() MAKE_ERROR(tecXCHAIN_SELF_COMMIT, "Account cannot commit funds to itself."), MAKE_ERROR(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR, "Bad public key account pair in an xchain transaction."), MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_DISABLED, "This bridge does not support account creation."), + MAKE_ERROR(tecEMPTY_DID, "The DID object did not have a URI or DIDDocument field."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -192,6 +193,7 @@ transResults() MAKE_ERROR(temBAD_WEIGHT, "Malformed: Weight must be a positive value."), MAKE_ERROR(temDST_IS_SRC, "Destination may not be source."), MAKE_ERROR(temDST_NEEDED, "Destination not specified."), + MAKE_ERROR(temEMPTY_DID, "Malformed: No DID data provided."), MAKE_ERROR(temINVALID, "The transaction is ill-formed."), MAKE_ERROR(temINVALID_FLAG, "The transaction has an invalid flag."), MAKE_ERROR(temREDUNDANT, "The transaction is redundant."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 0deff0a98..76f140bb7 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -616,6 +616,17 @@ TxFormats::TxFormats() {sfSignatureReward, soeREQUIRED}, }, commonFields); + + add(jss::DIDSet, + ttDID_SET, + { + {sfDIDDocument, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, + {sfData, soeOPTIONAL}, + }, + commonFields); + + add(jss::DIDDelete, ttDID_DELETE, {}, commonFields); } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index aa7ad80c1..e5038b1d2 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -75,6 +75,9 @@ JSS(ClearFlag); // field. JSS(CreateCode); // field. JSS(Cron); JSS(CronSet); +JSS(DID); // ledger type. +JSS(DIDDelete); // transaction type. +JSS(DIDSet); // transaction type. JSS(DeliverMin); // in: TransactionSign JSS(DepositPreauth); // transaction and ledger type. JSS(Destination); // in: TransactionSign; field. @@ -341,6 +344,7 @@ JSS(destination_currencies); // in: PathRequest, RipplePathFind JSS(destination_tag); // in: PathRequest // out: AccountChannels JSS(details); // out: Manifest, server_info +JSS(did); // in: LedgerEntry JSS(dir_entry); // out: DirectoryEntryIterator JSS(dir_index); // out: DirectoryEntryIterator JSS(dir_root); // out: DirectoryEntryIterator diff --git a/src/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index 66798f869..ba55941c2 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -784,6 +784,16 @@ doLedgerEntry(RPC::JsonContext& context) } } } + else if (context.params.isMember(jss::did)) + { + expectedType = ltDID; + auto const account = + parseBase58(context.params[jss::did].asString()); + if (!account || account->isZero()) + jvResult[jss::error] = "malformedAddress"; + else + uNodeIndex = keylet::did(*account).key; + } else { if (context.params.isMember("params") && diff --git a/src/ripple/rpc/handlers/ServerDefinitions.cpp b/src/ripple/rpc/handlers/ServerDefinitions.cpp index b8a911125..81559b7f5 100644 --- a/src/ripple/rpc/handlers/ServerDefinitions.cpp +++ b/src/ripple/rpc/handlers/ServerDefinitions.cpp @@ -159,7 +159,9 @@ private: {"ID", "ID"}, {"AMM", "AMM"}, {"URITOKEN", "URIToken"}, - {"URI", "URI"}}; + {"URI", "URI"}, + {"DID", "DID"}, + }; std::string out; size_t pos = 0; diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index 8c3bace94..6d61c8804 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -1113,7 +1113,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 27> + static constexpr std::array, 28> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -1142,7 +1142,8 @@ chooseLedgerEntryType(Json::Value const& params) {jss::bridge, ltBRIDGE}, {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, {jss::xchain_owned_create_account_claim_id, - ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}}}; + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, + {jss::did, ltDID}}}; auto const& p = params[jss::type]; if (!p.isString()) diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 27d7336ee..2a5f2c1cc 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -148,13 +148,14 @@ public: env.close(); // Give carol a deposit preauthorization, an offer, a ticket, - // and a signer list. Even with all that she's still deletable. + // a signer list, and a DID. Even with all that she's still deletable. env(deposit::auth(carol, becky)); std::uint32_t const carolOfferSeq{env.seq(carol)}; env(offer(carol, gw["USD"](51), XRP(51))); std::uint32_t const carolTicketSeq{env.seq(carol) + 1}; env(ticket::create(carol, 1)); env(signers(carol, 1, {{alice, 1}, {becky, 1}})); + env(did::setValid(carol)); // Deleting should fail with TOO_SOON, which is a relatively // cheap check compared to validating the contents of her directory. diff --git a/src/test/app/DID_test.cpp b/src/test/app/DID_test.cpp new file mode 100644 index 000000000..ea79d90b6 --- /dev/null +++ b/src/test/app/DID_test.cpp @@ -0,0 +1,398 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { + +// Helper function that returns the owner count of an account root. + +bool +checkVL(Slice const& result, std::string expected) +{ + Serializer s; + s.addRaw(result); + return s.getString() == expected; +} + +struct DID_test : public beast::unit_test::suite +{ + void + testEnabled(FeatureBitset features) + { + testcase("featureDID Enabled"); + + using namespace jtx; + // If the DID amendment is not enabled, you should not be able + // to set or delete DIDs. + Env env{*this, features - featureDID}; + Account const alice{"alice"}; + env.fund(XRP(5000), alice); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + env(did::setValid(alice), ter(temDISABLED)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + env(did::del(alice), ter(temDISABLED)); + env.close(); + } + + void + testAccountReserve(FeatureBitset features) + { + // Verify that the reserve behaves as expected for minting. + testcase("DID Account Reserve"); + + using namespace test::jtx; + + Env env{*this, features}; + Account const alice{"alice"}; + + // Fund alice enough to exist, but not enough to meet + // the reserve for creating a DID. + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + env.fund(acctReserve, alice); + env.close(); + BEAST_EXPECT(env.balance(alice) == acctReserve); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // alice does not have enough XRP to cover the reserve for a DID + env(did::setValid(alice), ter(tecINSUFFICIENT_RESERVE)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // Pay alice almost enough to make the reserve for a DID. + env(pay(env.master, alice, incReserve + drops(19))); + BEAST_EXPECT(env.balance(alice) == acctReserve + incReserve + drops(9)); + env.close(); + + // alice still does not have enough XRP for the reserve of a DID. + env(did::setValid(alice), ter(tecINSUFFICIENT_RESERVE)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // Pay alice enough to make the reserve for a DID. + env(pay(env.master, alice, drops(11))); + env.close(); + + // Now alice can create a DID. + env(did::setValid(alice)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // alice deletes her DID. + env(did::del(alice)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + env.close(); + } + + void + testSetInvalid(FeatureBitset features) + { + testcase("Invalid DIDSet"); + + using namespace jtx; + using namespace std::chrono; + + Env env(*this); + Account const alice{"alice"}; + env.fund(XRP(5000), alice); + env.close(); + + //---------------------------------------------------------------------- + // preflight + + // invalid flags + BEAST_EXPECT(ownerCount(env, alice) == 0); + env(did::setValid(alice), txflags(0x00010000), ter(temINVALID_FLAG)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // no fields + env(did::set(alice), ter(temEMPTY_DID)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // all empty fields + env(did::set(alice), + did::uri(""), + did::document(""), + did::data(""), + ter(temEMPTY_DID)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // uri is too long + const std::string longString(257, 'a'); + env(did::set(alice), did::uri(longString), ter(temMALFORMED)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // document is too long + env(did::set(alice), did::document(longString), ter(temMALFORMED)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // attestation is too long + env(did::set(alice), + did::document("data"), + did::data(longString), + ter(temMALFORMED)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // Modifying a DID to become empty is checked in testSetModify + } + + void + testDeleteInvalid(FeatureBitset features) + { + testcase("Invalid DIDDelete"); + + using namespace jtx; + using namespace std::chrono; + + Env env(*this); + Account const alice{"alice"}; + env.fund(XRP(5000), alice); + env.close(); + + //---------------------------------------------------------------------- + // preflight + + // invalid flags + BEAST_EXPECT(ownerCount(env, alice) == 0); + env(did::del(alice), txflags(0x00010000), ter(temINVALID_FLAG)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + //---------------------------------------------------------------------- + // doApply + + // DID doesn't exist + env(did::del(alice), ter(tecNO_ENTRY)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + } + + void + testSetValidInitial(FeatureBitset features) + { + testcase("Valid Initial DIDSet"); + + using namespace jtx; + using namespace std::chrono; + + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const charlie{"charlie"}; + Account const dave{"dave"}; + Account const edna{"edna"}; + Account const francis{"francis"}; + Account const george{"george"}; + env.fund(XRP(5000), alice, bob, charlie, dave, edna, francis); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, charlie) == 0); + + // only URI + env(did::set(alice), did::uri("uri")); + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // only DIDDocument + env(did::set(bob), did::document("data")); + BEAST_EXPECT(ownerCount(env, bob) == 1); + + // only Data + env(did::set(charlie), did::data("data")); + BEAST_EXPECT(ownerCount(env, charlie) == 1); + + // URI + Data + env(did::set(dave), did::uri("uri"), did::data("attest")); + BEAST_EXPECT(ownerCount(env, dave) == 1); + + // URI + DIDDocument + env(did::set(edna), did::uri("uri"), did::document("data")); + BEAST_EXPECT(ownerCount(env, edna) == 1); + + // DIDDocument + Data + env(did::set(francis), did::document("data"), did::data("attest")); + BEAST_EXPECT(ownerCount(env, francis) == 1); + + // URI + DIDDocument + Data + env(did::set(george), + did::uri("uri"), + did::document("data"), + did::data("attest")); + BEAST_EXPECT(ownerCount(env, george) == 1); + } + + void + testSetModify(FeatureBitset features) + { + testcase("Modify DID with DIDSet"); + + using namespace jtx; + using namespace std::chrono; + + Env env(*this); + Account const alice{"alice"}; + env.fund(XRP(5000), alice); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + auto const ar = env.le(alice); + + // Create DID + std::string const initialURI = "uri"; + { + env(did::set(alice), did::uri(initialURI)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(sleDID); + BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); + } + + // Try to delete URI, fails because no elements are set + { + env(did::set(alice), did::uri(""), ter(tecEMPTY_DID)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); + } + + // Set DIDDocument + std::string const initialDocument = "data"; + { + env(did::set(alice), did::document(initialDocument)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); + } + + // Set Data + std::string const initialData = "attest"; + { + env(did::set(alice), did::data(initialData)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData)); + } + + // Remove URI + { + env(did::set(alice), did::uri("")); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); + BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData)); + } + + // Remove Data + { + env(did::set(alice), did::data("")); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); + BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); + } + + // Remove Data + set URI + std::string const secondURI = "uri2"; + { + env(did::set(alice), did::uri(secondURI), did::document("")); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(checkVL((*sleDID)[sfURI], secondURI)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); + } + + // Remove URI + set DIDDocument + std::string const secondDocument = "data2"; + { + env(did::set(alice), did::uri(""), did::document(secondDocument)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); + BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], secondDocument)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); + } + + // Remove DIDDocument + set Data + std::string const secondData = "randomData"; + { + env(did::set(alice), did::document(""), did::data(secondData)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); + BEAST_EXPECT(checkVL((*sleDID)[sfData], secondData)); + } + + // Delete DID + { + env(did::del(alice)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + auto const sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(!sleDID); + } + } + + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{ + supported_amendments() | FeatureBitset{featureDID}}; + testEnabled(all); + testAccountReserve(all); + testSetInvalid(all); + testDeleteInvalid(all); + testSetModify(all); + } +}; + +BEAST_DEFINE_TESTSUITE(DID, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index a83c6ee7d..5b66b61f6 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -571,7 +571,7 @@ class NFToken0_test : public beast::unit_test::suite ter(temMALFORMED)); //---------------------------------------------------------------------- - // preflight + // preclaim // Non-existent issuer. env(token::mint(alice, 0u), diff --git a/src/test/jtx.h b/src/test/jtx.h index e4892baf2..f04c25b05 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/did.h b/src/test/jtx/did.h new file mode 100644 index 000000000..0cffb60e5 --- /dev/null +++ b/src/test/jtx/did.h @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_DID_H_INCLUDED +#define RIPPLE_TEST_JTX_DID_H_INCLUDED + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** DID operations. */ +namespace did { + +Json::Value +set(jtx::Account const& account); + +Json::Value +setValid(jtx::Account const& account); + +/** Sets the optional DIDDocument on a DIDSet. */ +class document +{ +private: + std::string document_; + +public: + explicit document(std::string const& u) : document_(strHex(u)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + jtx.jv[sfDIDDocument.jsonName] = document_; + } +}; + +/** Sets the optional URI on a DIDSet. */ +class uri +{ +private: + std::string uri_; + +public: + explicit uri(std::string const& u) : uri_(strHex(u)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + jtx.jv[sfURI.jsonName] = uri_; + } +}; + +/** Sets the optional Attestation on a DIDSet. */ +class data +{ +private: + std::string data_; + +public: + explicit data(std::string const& u) : data_(strHex(u)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + jtx.jv[sfData.jsonName] = data_; + } +}; + +Json::Value +del(jtx::Account const& account); + +} // namespace did + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/did.cpp b/src/test/jtx/impl/did.cpp new file mode 100644 index 000000000..94dfcc327 --- /dev/null +++ b/src/test/jtx/impl/did.cpp @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** DID operations. */ +namespace did { + +Json::Value +set(jtx::Account const& account) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::DIDSet; + jv[jss::Account] = to_string(account.id()); + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +setValid(jtx::Account const& account) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::DIDSet; + jv[jss::Account] = to_string(account.id()); + jv[jss::Flags] = tfUniversal; + jv[sfURI.jsonName] = strHex(std::string{"uri"}); + return jv; +} + +Json::Value +del(jtx::Account const& account) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::DIDDelete; + jv[jss::Account] = to_string(account.id()); + jv[jss::Flags] = tfUniversal; + return jv; +} + +} // namespace did + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index a4ddc0ee8..657d58d4f 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -599,6 +599,7 @@ public: BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::uri_token), 0)); BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::hook), 0)); BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::amm), 0)); + BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::did), 0)); // gw mints an NFT so we can find it. uint256 const nftID{token::getNextID(env, gw, 0u, tfTransferable)}; @@ -863,6 +864,26 @@ public: BEAST_EXPECT( payChan[sfSettleDelay.jsonName].asUInt() == 24 * 60 * 60); } + + { + // gw creates a DID that we can look for in the ledger. + Json::Value jvDID; + jvDID[jss::TransactionType] = jss::DIDSet; + jvDID[jss::Flags] = tfUniversal; + jvDID[jss::Account] = gw.human(); + jvDID[sfURI.jsonName] = strHex(std::string{"uri"}); + env(jvDID); + env.close(); + } + { + // Find the DID. + Json::Value const resp = acct_objs(gw, jss::did); + BEAST_EXPECT(acct_objs_is_size(resp, 1)); + + auto const& did = resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT(did[sfAccount.jsonName] == gw.human()); + BEAST_EXPECT(did[sfURI.jsonName] == strHex(std::string{"uri"})); + } // Make gw multisigning by adding a signerList. env(jtx::signers(gw, 6, {{alice, 7}})); env.close(); @@ -890,7 +911,7 @@ public: auto const& ticket = resp[jss::result][jss::account_objects][0u]; BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human()); BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket); - BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 10); + BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 11); } { // Create a uri token. diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index f4d5a530b..87f70599d 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -2170,6 +2170,7 @@ public: cron::repeat(200), fee(XRP(1)), ter(tesSUCCESS)); + env.close(); std::string const ledgerHash{to_string(env.closed()->info().hash)}; @@ -2232,6 +2233,57 @@ public: } } + void + testLedgerEntryDID() + { + testcase("ledger_entry Request DID"); + using namespace test::jtx; + using namespace std::literals::chrono_literals; + Env env{*this}; + Account const alice{"alice"}; + + env.fund(XRP(10000), alice); + env.close(); + + // Lambda to create a DID. + auto didCreate = [](test::jtx::Account const& account) { + Json::Value jv; + jv[jss::TransactionType] = jss::DIDSet; + jv[jss::Account] = account.human(); + jv[sfDIDDocument.jsonName] = strHex(std::string{"data"}); + jv[sfURI.jsonName] = strHex(std::string{"uri"}); + return jv; + }; + + env(didCreate(alice)); + env.close(); + + std::string const ledgerHash{to_string(env.closed()->info().hash)}; + + { + // Request the DID using its index. + Json::Value jvParams; + jvParams[jss::did] = alice.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfDIDDocument.jsonName] == + strHex(std::string{"data"})); + BEAST_EXPECT( + jrr[jss::node][sfURI.jsonName] == strHex(std::string{"uri"})); + } + { + // Request an index that is not a DID. + Json::Value jvParams; + jvParams[jss::did] = env.master.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + } + void testLedgerEntryInvalidParams(unsigned int apiVersion) { @@ -2983,6 +3035,7 @@ public: testNoQueue(); testQueue(); testLedgerAccountsOption(); + testLedgerEntryDID(); test::jtx::forAllApiVersions(std::bind_front( &LedgerRPC_test::testLedgerEntryInvalidParams, this));