From b61a4d9c838ec38ccd6df87bb66e07edf3ee0c49 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 18 Oct 2023 16:01:12 -0400 Subject: [PATCH] `DID`: Decentralized identifiers (DIDs) (XLS-40): (#4636) Implement native support for W3C DIDs. Add a new ledger object: `DID`. Add two new transactions: 1. `DIDSet`: create or update the `DID` object. 2. `DIDDelete`: delete the `DID` object. This meets the requirements specified in the DID v1.0 specification currently recommended by the W3C Credentials Community Group. The DID format for the XRP Ledger conforms to W3C DID standards. The objects can be created and owned by any XRPL account holder. The transactions can be integrated by any service, wallet, or application. --- Builds/CMake/RippledCore.cmake | 3 + src/ripple/app/tx/impl/DID.cpp | 226 ++++++++++++ src/ripple/app/tx/impl/DID.h | 73 ++++ src/ripple/app/tx/impl/DeleteAccount.cpp | 15 + src/ripple/app/tx/impl/InvariantCheck.cpp | 1 + src/ripple/app/tx/impl/applySteps.cpp | 5 + src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/Indexes.h | 3 + src/ripple/protocol/LedgerFormats.h | 6 + src/ripple/protocol/Protocol.h | 9 + src/ripple/protocol/SField.h | 2 + src/ripple/protocol/TER.h | 3 + src/ripple/protocol/TxFormats.h | 6 + src/ripple/protocol/impl/Feature.cpp | 3 +- src/ripple/protocol/impl/Indexes.cpp | 7 + src/ripple/protocol/impl/LedgerFormats.cpp | 13 + src/ripple/protocol/impl/SField.cpp | 2 + src/ripple/protocol/impl/TER.cpp | 2 + src/ripple/protocol/impl/TxFormats.cpp | 11 + src/ripple/protocol/jss.h | 4 + src/ripple/rpc/handlers/LedgerEntry.cpp | 10 + src/ripple/rpc/impl/RPCHelpers.cpp | 5 +- src/test/app/AccountDelete_test.cpp | 3 +- src/test/app/DID_test.cpp | 406 +++++++++++++++++++++ src/test/app/NFToken_test.cpp | 2 +- src/test/jtx.h | 1 + src/test/jtx/did.h | 104 ++++++ src/test/jtx/impl/did.cpp | 67 ++++ src/test/rpc/AccountObjects_test.cpp | 23 +- src/test/rpc/LedgerRPC_test.cpp | 52 +++ 30 files changed, 1063 insertions(+), 7 deletions(-) create mode 100644 src/ripple/app/tx/impl/DID.cpp create mode 100644 src/ripple/app/tx/impl/DID.h create mode 100644 src/test/app/DID_test.cpp create mode 100644 src/test/jtx/did.h create mode 100644 src/test/jtx/impl/did.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 8d2ff6cbaef..ef6f925ec6d 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -519,6 +519,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/CreateTicket.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/InvariantCheck.cpp src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -785,6 +786,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 @@ -937,6 +939,7 @@ if (tests) src/test/jtx/impl/check.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/src/ripple/app/tx/impl/DID.cpp b/src/ripple/app/tx/impl/DID.cpp new file mode 100644 index 00000000000..c92162c8306 --- /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 00000000000..13d5a261542 --- /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 67545723a5f..49b645e31d9 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 @@ -133,6 +134,18 @@ removeNFTokenOfferFromLedger( 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 @@ -151,6 +164,8 @@ nonObligationDeleter(LedgerEntryType t) return removeDepositPreauthFromLedger; case ltNFTOKEN_OFFER: return removeNFTokenOfferFromLedger; + 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 7b1ac7d08df..c717777f88f 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -391,6 +391,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 4c882f3fb8a..10e2b0c4524 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -154,6 +155,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 17aca813f71..6377ce3ac62 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 = 63; +static constexpr std::size_t numFeatures = 64; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -350,6 +350,7 @@ extern uint256 const fixReducedOffersV1; extern uint256 const featureClawback; 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 0c83f7765a4..9a330b6b4f0 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -280,6 +280,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 e907e299f52..db64942790a 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -186,6 +186,12 @@ enum LedgerEntryType : std::uint16_t */ ltAMM = 0x0079, + /** The ledger object which tracks the DID. + + \sa keylet::did + */ + ltDID = 0x0049, + //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. diff --git a/src/ripple/protocol/Protocol.h b/src/ripple/protocol/Protocol.h index 6e4879cd746..49642efc4cf 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 7c802a64e1d..fe045d001d5 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -524,6 +524,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 d6ca7ce7e4f..f832fe24bce 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -132,6 +132,8 @@ enum TEMcodes : TERUnderlyingType { temXCHAIN_BRIDGE_NONDOOR_OWNER, temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT, temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, + + temEMPTY_DID, }; //------------------------------------------------------------------------------ @@ -328,6 +330,7 @@ enum TECcodes : TERUnderlyingType { tecXCHAIN_SELF_COMMIT = 184, tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 185, tecXCHAIN_CREATE_ACCOUNT_DISABLED = 186, + tecEMPTY_DID = 187 }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index d8785f3ea1d..b12547b0a67 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -184,6 +184,12 @@ enum TxType : std::uint16_t /** This transactions creates a sidechain */ ttXCHAIN_CREATE_BRIDGE = 48, + /** This transaction type creates or updates a DID */ + ttDID_SET = 49, + + /** This transaction type deletes a DID */ + ttDID_DELETE = 50, + /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 6a3430f4f50..77a0a9284ac 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -456,7 +456,8 @@ REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::De REGISTER_FEATURE(Clawback, 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 3fef856b365..74f6b6492de 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -71,6 +71,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. @@ -437,6 +438,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 e5313a8c1f9..729ddc1c7bc 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -326,6 +326,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 517971dbf07..027c8ffb9c5 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -298,6 +298,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookStateData, "HookStateData", VL, CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL, 23); CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24); CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25); +CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 26); +CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 87dae362598..f48bef5232d 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -113,6 +113,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."), @@ -179,6 +180,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 755401bda92..7be8ca741e2 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -472,6 +472,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 e31a1cb3bf8..e2800dc80a0 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -69,6 +69,9 @@ JSS(CheckCash); // transaction type. JSS(CheckCreate); // transaction type. JSS(Clawback); // transaction type. JSS(ClearFlag); // field. +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. @@ -278,6 +281,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 7f40d3ee3be..baff721cc1f 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -588,6 +588,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/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index f082f8913ca..a9cc0f9fffe 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -988,7 +988,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 19> + static constexpr std::array, 20> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -1009,7 +1009,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 2ec0b876a64..fbd631f444a 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -148,13 +148,14 @@ class AccountDelete_test : public beast::unit_test::suite 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 00000000000..3aa27978bfe --- /dev/null +++ b/src/test/app/DID_test.cpp @@ -0,0 +1,406 @@ +//------------------------------------------------------------------------------ +/* + 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. +std::uint32_t +ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct) +{ + std::uint32_t ret{0}; + if (auto const sleAcct = env.le(acct)) + ret = sleAcct->at(sfOwnerCount); + return ret; +} + +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 0539d2abd93..2f029c5db1f 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -581,7 +581,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 dd987472cc8..03bbf154e63 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -35,6 +35,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 00000000000..0cffb60e527 --- /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 00000000000..94dfcc32754 --- /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 e38c7c029b7..17217f2c880 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -599,6 +599,7 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::state), 0)); BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::ticket), 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)}; @@ -854,6 +855,26 @@ class AccountObjects_test : public beast::unit_test::suite 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(); @@ -881,7 +902,7 @@ class AccountObjects_test : public beast::unit_test::suite 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() == 13); + BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 14); } { // See how "deletion_blockers_only" handles gw's directory. diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 960ac3a86ee..2b4d8527a64 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -1530,6 +1530,57 @@ class LedgerRPC_test : public beast::unit_test::suite } } + 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) { @@ -2252,6 +2303,7 @@ class LedgerRPC_test : public beast::unit_test::suite testNoQueue(); testQueue(); testLedgerAccountsOption(); + testLedgerEntryDID(); test::jtx::forAllApiVersions(std::bind_front( &LedgerRPC_test::testLedgerEntryInvalidParams, this));