From b421945e717a29e81b029adea576b24cd2068051 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 18 Oct 2023 16:01:12 -0400 Subject: [PATCH 1/4] `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)); From 078bd606c73e73fc242c59b67866143deb19df04 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 18 Oct 2023 19:16:40 -0400 Subject: [PATCH 2/4] feat(rpc): add `server_definitions` method (#4703) Add a new RPC / WS call for `server_definitions`, which returns an SDK-compatible `definitions.json` (binary enum definitions) generated by the server. This enables clients/libraries to dynamically work with new fields and features, such as ones that may become available on side chains. Clients query `server_definitions` on a node from the network they want to work with, and immediately know how to speak that node's binary "language", even if new features are added to it in the future (as long as there are no new serialized types that the software doesn't know how to serialize/deserialize). Example: ```js > {"command": "server_definitions"} < { "result": { "FIELDS": [ [ "Generic", { "isSerialized": false, "isSigningField": false, "isVLEncoded": false, "nth": 0, "type": "Unknown" } ], [ "Invalid", { "isSerialized": false, "isSigningField": false, "isVLEncoded": false, "nth": -1, "type": "Unknown" } ], [ "ObjectEndMarker", { "isSerialized": false, "isSigningField": true, "isVLEncoded": false, "nth": 1, "type": "STObject" } ], ... ``` Close #3657 --------- Co-authored-by: Richard Holland --- API-CHANGELOG.md | 51 +++-- src/ripple/app/main/Main.cpp | 1 + src/ripple/net/impl/RPCCall.cpp | 23 +- src/ripple/protocol/SField.h | 102 +++++---- src/ripple/protocol/TER.h | 6 + src/ripple/protocol/impl/TER.cpp | 11 +- src/ripple/protocol/jss.h | 19 +- src/ripple/rpc/handlers/Handlers.h | 2 + src/ripple/rpc/handlers/ServerInfo.cpp | 291 ++++++++++++++++++++++++- src/ripple/rpc/impl/Handler.cpp | 12 +- src/test/rpc/ServerInfo_test.cpp | 96 ++++++++ 11 files changed, 538 insertions(+), 76 deletions(-) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index 5115eee0855..a3c06399cf5 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -26,6 +26,14 @@ In `api_version: 2`, the `signer_lists` field [will be moved](#modifications-to- The `network_id` field was added in the `server_info` response in version 1.5.0 (2019), but it was not returned in [reporting mode](https://xrpl.org/rippled-server-modes.html#reporting-mode). +## Unreleased + +### Additions + +Additions are intended to be non-breaking (because they are purely additive). + +- `server_definitions`: A new RPC that generates a `definitions.json`-like output that can be used in XRPL libraries. + ## XRP Ledger version 1.12.0 [Version 1.12.0](https://github.com/XRPLF/rippled/releases/tag/1.12.0) was released on Sep 6, 2023. @@ -46,27 +54,27 @@ Additions are intended to be non-breaking (because they are purely additive). - `Account`: The issuer of the asset being clawed back. Must also be the sender of the transaction. - `Amount`: The amount being clawed back, with the `Amount.issuer` being the token holder's address. - Adds [AMM](https://github.com/XRPLF/XRPL-Standards/discussions/78) ([#4294](https://github.com/XRPLF/rippled/pull/4294), [#4626](https://github.com/XRPLF/rippled/pull/4626)) feature: - - Adds `amm_info` API to retrieve AMM information for a given tokens pair. - - Adds `AMMCreate` transaction type to create `AMM` instance. - - Adds `AMMDeposit` transaction type to deposit funds into `AMM` instance. - - Adds `AMMWithdraw` transaction type to withdraw funds from `AMM` instance. - - Adds `AMMVote` transaction type to vote for the trading fee of `AMM` instance. - - Adds `AMMBid` transaction type to bid for the Auction Slot of `AMM` instance. - - Adds `AMMDelete` transaction type to delete `AMM` instance. - - Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`. - - Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account. - - Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`, - `tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination - for `AMMDeposit` and `AMMWithdraw` transactions. - - Adds new transaction result codes: - - tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision. - - tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances. - - tecAMM_FAILED: AMM transaction failed. Fails due to a processing failure. - - tecAMM_INVALID_TOKENS: AMM invalid LP tokens. Invalid input values, format, or calculated values. - - tecAMM_EMPTY: AMM is in empty state. Transaction expects AMM in non-empty state (LP tokens > 0). - - tecAMM_NOT_EMPTY: AMM is not in empty state. Transaction expects AMM in empty state (LP tokens == 0). - - tecAMM_ACCOUNT: AMM account. Clawback of AMM account. - - tecINCOMPLETE: Some work was completed, but more submissions required to finish. AMMDelete partially deletes the trustlines. + - Adds `amm_info` API to retrieve AMM information for a given tokens pair. + - Adds `AMMCreate` transaction type to create `AMM` instance. + - Adds `AMMDeposit` transaction type to deposit funds into `AMM` instance. + - Adds `AMMWithdraw` transaction type to withdraw funds from `AMM` instance. + - Adds `AMMVote` transaction type to vote for the trading fee of `AMM` instance. + - Adds `AMMBid` transaction type to bid for the Auction Slot of `AMM` instance. + - Adds `AMMDelete` transaction type to delete `AMM` instance. + - Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`. + - Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account. + - Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`, + `tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination + for `AMMDeposit` and `AMMWithdraw` transactions. + - Adds new transaction result codes: + - tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision. + - tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances. + - tecAMM_FAILED: AMM transaction failed. Fails due to a processing failure. + - tecAMM_INVALID_TOKENS: AMM invalid LP tokens. Invalid input values, format, or calculated values. + - tecAMM_EMPTY: AMM is in empty state. Transaction expects AMM in non-empty state (LP tokens > 0). + - tecAMM_NOT_EMPTY: AMM is not in empty state. Transaction expects AMM in empty state (LP tokens == 0). + - tecAMM_ACCOUNT: AMM account. Clawback of AMM account. + - tecINCOMPLETE: Some work was completed, but more submissions required to finish. AMMDelete partially deletes the trustlines. ## XRP Ledger version 1.11.0 @@ -118,6 +126,7 @@ Changes below this point are in development. At the time of writing, this version is expected to be introduced in `rippled` version 2.0. Currently (prior to the release of 2.0), it is available as a "beta" version, meaning it can be enabled with a config setting in `rippled.cfg`: + ``` [beta_rpc_api] 1 diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index a998640c01c..2ddb771b1e1 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -169,6 +169,7 @@ printHelp(const po::options_description& desc) " peer_reservations_list\n" " ripple ...\n" " ripple_path_find []\n" + " server_definitions []\n" " server_info [counters]\n" " server_state [counters]\n" " sign [offline]\n" diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 1242fdc2a68..b52545960cc 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -1194,6 +1194,20 @@ class RPCParser return jvRequest; } + // server_definitions [hash] + Json::Value + parseServerDefinitions(Json::Value const& jvParams) + { + Json::Value jvRequest{Json::objectValue}; + + if (jvParams.size() == 1) + { + jvRequest[jss::hash] = jvParams[0u].asString(); + } + + return jvRequest; + } + // server_info [counters] Json::Value parseServerInfo(Json::Value const& jvParams) @@ -1255,6 +1269,7 @@ class RPCParser {"channel_verify", &RPCParser::parseChannelVerify, 4, 4}, {"connect", &RPCParser::parseConnect, 1, 2}, {"consensus_info", &RPCParser::parseAsIs, 0, 0}, + {"crawl_shards", &RPCParser::parseAsIs, 0, 2}, {"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3}, {"download_shard", &RPCParser::parseDownloadShard, 2, -1}, {"feature", &RPCParser::parseFeature, 0, 2}, @@ -1292,14 +1307,14 @@ class RPCParser 1}, {"peer_reservations_list", &RPCParser::parseAsIs, 0, 0}, {"ripple_path_find", &RPCParser::parseRipplePathFind, 1, 2}, + {"server_definitions", &RPCParser::parseServerDefinitions, 0, 1}, + {"server_info", &RPCParser::parseServerInfo, 0, 1}, + {"server_state", &RPCParser::parseServerInfo, 0, 1}, {"sign", &RPCParser::parseSignSubmit, 2, 3}, {"sign_for", &RPCParser::parseSignFor, 3, 4}, + {"stop", &RPCParser::parseAsIs, 0, 0}, {"submit", &RPCParser::parseSignSubmit, 1, 3}, {"submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1}, - {"server_info", &RPCParser::parseServerInfo, 0, 1}, - {"server_state", &RPCParser::parseServerInfo, 0, 1}, - {"crawl_shards", &RPCParser::parseAsIs, 0, 2}, - {"stop", &RPCParser::parseAsIs, 0, 0}, {"transaction_entry", &RPCParser::parseTransactionEntry, 2, 2}, {"tx", &RPCParser::parseTx, 1, 4}, {"tx_account", &RPCParser::parseTxAccount, 1, 7}, diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index fe045d001d5..5d7acb12383 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -52,43 +52,65 @@ class STInteger; class STXChainBridge; class STVector256; -enum SerializedTypeID { - // special types - STI_UNKNOWN = -2, - STI_NOTPRESENT = 0, - - // // types (common) - STI_UINT16 = 1, - STI_UINT32 = 2, - STI_UINT64 = 3, - STI_UINT128 = 4, - STI_UINT256 = 5, - STI_AMOUNT = 6, - STI_VL = 7, - STI_ACCOUNT = 8, - // 9-13 are reserved - STI_OBJECT = 14, - STI_ARRAY = 15, - - // types (uncommon) - STI_UINT8 = 16, - STI_UINT160 = 17, - STI_PATHSET = 18, - STI_VECTOR256 = 19, - STI_UINT96 = 20, - STI_UINT192 = 21, - STI_UINT384 = 22, - STI_UINT512 = 23, - STI_ISSUE = 24, - STI_XCHAIN_BRIDGE = 25, - - // high level types - // cannot be serialized inside other types - STI_TRANSACTION = 10001, - STI_LEDGERENTRY = 10002, - STI_VALIDATION = 10003, - STI_METADATA = 10004, -}; +#pragma push_macro("XMACRO") +#undef XMACRO + +#define XMACRO(STYPE) \ + /* special types */ \ + STYPE(STI_UNKNOWN, -2) \ + STYPE(STI_NOTPRESENT, 0) \ + STYPE(STI_UINT16, 1) \ + \ + /* types (common) */ \ + STYPE(STI_UINT32, 2) \ + STYPE(STI_UINT64, 3) \ + STYPE(STI_UINT128, 4) \ + STYPE(STI_UINT256, 5) \ + STYPE(STI_AMOUNT, 6) \ + STYPE(STI_VL, 7) \ + STYPE(STI_ACCOUNT, 8) \ + \ + /* 9-13 are reserved */ \ + STYPE(STI_OBJECT, 14) \ + STYPE(STI_ARRAY, 15) \ + \ + /* types (uncommon) */ \ + STYPE(STI_UINT8, 16) \ + STYPE(STI_UINT160, 17) \ + STYPE(STI_PATHSET, 18) \ + STYPE(STI_VECTOR256, 19) \ + STYPE(STI_UINT96, 20) \ + STYPE(STI_UINT192, 21) \ + STYPE(STI_UINT384, 22) \ + STYPE(STI_UINT512, 23) \ + STYPE(STI_ISSUE, 24) \ + STYPE(STI_XCHAIN_BRIDGE, 25) \ + \ + /* high-level types */ \ + /* cannot be serialized inside other types */ \ + STYPE(STI_TRANSACTION, 10001) \ + STYPE(STI_LEDGERENTRY, 10002) \ + STYPE(STI_VALIDATION, 10003) \ + STYPE(STI_METADATA, 10004) + +#pragma push_macro("TO_ENUM") +#undef TO_ENUM +#pragma push_macro("TO_MAP") +#undef TO_MAP + +#define TO_ENUM(name, value) name = value, +#define TO_MAP(name, value) {#name, value}, + +enum SerializedTypeID { XMACRO(TO_ENUM) }; + +static std::map const sTypeMap = {XMACRO(TO_MAP)}; + +#undef XMACRO +#undef TO_ENUM + +#pragma pop_macro("XMACRO") +#pragma pop_macro("TO_ENUM") +#pragma pop_macro("TO_MAP") // constexpr inline int @@ -266,6 +288,12 @@ class SField static int compare(const SField& f1, const SField& f2); + static std::map const& + getKnownCodeToField() + { + return knownCodeToField; + } + private: static int num; static std::map knownCodeToField; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index f832fe24bce..23d4fb3ef00 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace ripple { @@ -644,6 +645,11 @@ isTecClaim(TER x) return ((x) >= tecCLAIM); } +std::unordered_map< + TERUnderlyingType, + std::pair> const& +transResults(); + bool transResultInfo(TER code, std::string& token, std::string& text); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index f48bef5232d..1c2db3feb3b 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -20,13 +20,10 @@ #include #include #include -#include namespace ripple { -namespace detail { - -static std::unordered_map< +std::unordered_map< TERUnderlyingType, std::pair> const& transResults() @@ -225,12 +222,10 @@ transResults() return results; } -} // namespace detail - bool transResultInfo(TER code, std::string& token, std::string& text) { - auto& results = detail::transResults(); + auto& results = transResults(); auto const r = results.find(TERtoInt(code)); @@ -264,7 +259,7 @@ std::optional transCode(std::string const& token) { static auto const results = [] { - auto& byTer = detail::transResults(); + auto& byTer = transResults(); auto range = boost::make_iterator_range(byTer.begin(), byTer.end()); auto tRange = boost::adaptors::transform(range, [](auto const& r) { return std::make_pair(r.second.first, r.first); diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index e2800dc80a0..4a8f80b8c7b 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -321,6 +321,8 @@ JSS(fee_level); // out: AccountInfo JSS(fee_mult_max); // in: TransactionSign JSS(fee_ref); // out: NetworkOPs, DEPRECATED JSS(fetch_pack); // out: NetworkOPs +JSS(FIELDS); // out: RPC server_definitions + // matches definitions.json format JSS(first); // out: rpc/Version JSS(firstSequence); // out: NodeToShardStatus JSS(firstShardIndex); // out: NodeToShardStatus @@ -365,6 +367,12 @@ JSS(invalid_API_version); // out: Many, when a request has an invalid JSS(io_latency_ms); // out: NetworkOPs JSS(ip); // in: Connect, out: OverlayImpl JSS(is_burned); // out: nft_info (clio) +JSS(isSerialized); // out: RPC server_definitions + // matches definitions.json format +JSS(isSigningField); // out: RPC server_definitions + // matches definitions.json format +JSS(isVLEncoded); // out: RPC server_definitions + // matches definitions.json format JSS(issuer); // in: RipplePathFind, Subscribe, // Unsubscribe, BookOffers // out: STPathSet, STAmount @@ -404,6 +412,8 @@ JSS(ledger_index_min); // in, out: AccountTx* JSS(ledger_max); // in, out: AccountTx* JSS(ledger_min); // in, out: AccountTx* JSS(ledger_time); // out: NetworkOPs +JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions + // matches definitions.json format JSS(levels); // LogLevels JSS(limit); // in/out: AccountTx*, AccountOffers, // AccountLines, AccountObjects @@ -490,6 +500,7 @@ JSS(node_written_bytes); // out: GetCounts JSS(node_writes_duration_us); // out: GetCounts JSS(node_write_retries); // out: GetCounts JSS(node_writes_delayed); // out::GetCounts +JSS(nth); // out: RPC server_definitions JSS(obligations); // out: GatewayBalances JSS(offer); // in: LedgerEntry JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe @@ -649,6 +660,12 @@ JSS(transaction); // in: Tx JSS(transaction_hash); // out: RCLCxPeerPos, LedgerToJson JSS(transactions); // out: LedgerToJson, // in: AccountTx*, Unsubscribe +JSS(TRANSACTION_RESULTS); // out: RPC server_definitions + // matches definitions.json format +JSS(TRANSACTION_TYPES); // out: RPC server_definitions + // matches definitions.json format +JSS(TYPES); // out: RPC server_definitions + // matches definitions.json format JSS(transfer_rate); // out: nft_info (clio) JSS(transitions); // out: NetworkOPs JSS(treenode_cache_size); // out: GetCounts @@ -680,7 +697,7 @@ JSS(txr_not_enabled_cnt); // out: peers with tx reduce-relay disabled count JSS(txr_missing_tx_freq); // out: missing tx frequency average JSS(txs); // out: TxHistory JSS(type); // in: AccountObjects - // out: NetworkOPs + // out: NetworkOPs, RPC server_definitions // OverlayImpl, Logic JSS(type_hex); // out: STPathSet JSS(unl); // out: UnlList diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 367e715ce1f..ba93be54513 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -127,6 +127,8 @@ doPeerReservationsList(RPC::JsonContext&); Json::Value doRipplePathFind(RPC::JsonContext&); Json::Value +doServerDefinitions(RPC::JsonContext&); +Json::Value doServerInfo(RPC::JsonContext&); // for humans Json::Value doServerState(RPC::JsonContext&); // for machines diff --git a/src/ripple/rpc/handlers/ServerInfo.cpp b/src/ripple/rpc/handlers/ServerInfo.cpp index aad3d9988e3..2637c3d51be 100644 --- a/src/ripple/rpc/handlers/ServerInfo.cpp +++ b/src/ripple/rpc/handlers/ServerInfo.cpp @@ -22,13 +22,302 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include -#include + +#include + +#include namespace ripple { +namespace detail { + +class ServerDefinitions +{ +private: + std::string + // translate e.g. STI_LEDGERENTRY to LedgerEntry + translate(std::string const& inp); + + uint256 defsHash_; + Json::Value defs_; + +public: + ServerDefinitions(); + + bool + hashMatches(uint256 hash) const + { + return defsHash_ == hash; + } + + Json::Value const& + get() const + { + return defs_; + } +}; + +std::string +ServerDefinitions::translate(std::string const& inp) +{ + auto replace = [&](char const* oldStr, char const* newStr) -> std::string { + std::string out = inp; + boost::replace_all(out, oldStr, newStr); + return out; + }; + + auto contains = [&](char const* s) -> bool { + return inp.find(s) != std::string::npos; + }; + + if (contains("UINT")) + { + if (contains("256") || contains("160") || contains("128")) + return replace("UINT", "Hash"); + else + return replace("UINT", "UInt"); + } + + std::unordered_map replacements{ + {"OBJECT", "STObject"}, + {"ARRAY", "STArray"}, + {"ACCOUNT", "AccountID"}, + {"LEDGERENTRY", "LedgerEntry"}, + {"NOTPRESENT", "NotPresent"}, + {"PATHSET", "PathSet"}, + {"VL", "Blob"}, + {"XCHAIN_BRIDGE", "XChainBridge"}, + }; + + if (auto const& it = replacements.find(inp); it != replacements.end()) + { + return it->second; + } + + std::string out; + size_t pos = 0; + std::string inpToProcess = inp; + + // convert snake_case to CamelCase + for (;;) + { + pos = inpToProcess.find("_"); + if (pos == std::string::npos) + pos = inpToProcess.size(); + std::string token = inpToProcess.substr(0, pos); + if (token.size() > 1) + { + boost::algorithm::to_lower(token); + token.data()[0] -= ('a' - 'A'); + out += token; + } + else + out += token; + if (pos == inpToProcess.size()) + break; + inpToProcess = inpToProcess.substr(pos + 1); + } + return out; +}; + +ServerDefinitions::ServerDefinitions() : defs_{Json::objectValue} +{ + // populate SerializedTypeID names and values + defs_[jss::TYPES] = Json::objectValue; + + defs_[jss::TYPES]["Done"] = -1; + std::map typeMap{{-1, "Done"}}; + for (auto const& [rawName, typeValue] : sTypeMap) + { + std::string typeName = + translate(std::string(rawName).substr(4) /* remove STI_ */); + defs_[jss::TYPES][typeName] = typeValue; + typeMap[typeValue] = typeName; + } + + // populate LedgerEntryType names and values + defs_[jss::LEDGER_ENTRY_TYPES] = Json::objectValue; + defs_[jss::LEDGER_ENTRY_TYPES][jss::Invalid] = -1; + + for (auto const& f : LedgerFormats::getInstance()) + { + defs_[jss::LEDGER_ENTRY_TYPES][f.getName()] = f.getType(); + } + + // populate SField serialization data + defs_[jss::FIELDS] = Json::arrayValue; + + uint32_t i = 0; + { + Json::Value a = Json::arrayValue; + a[0U] = "Generic"; + Json::Value v = Json::objectValue; + v[jss::nth] = 0; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Unknown"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "Invalid"; + Json::Value v = Json::objectValue; + v[jss::nth] = -1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Unknown"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "ObjectEndMarker"; + Json::Value v = Json::objectValue; + v[jss::nth] = 1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = true; + v[jss::isSigningField] = true; + v[jss::type] = "STObject"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "ArrayEndMarker"; + Json::Value v = Json::objectValue; + v[jss::nth] = 1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = true; + v[jss::isSigningField] = true; + v[jss::type] = "STArray"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "taker_gets_funded"; + Json::Value v = Json::objectValue; + v[jss::nth] = 258; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Amount"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "taker_pays_funded"; + Json::Value v = Json::objectValue; + v[jss::nth] = 259; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Amount"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + for (auto const& [code, f] : ripple::SField::getKnownCodeToField()) + { + if (f->fieldName == "") + continue; + + Json::Value innerObj = Json::objectValue; + + uint32_t type = f->fieldType; + + innerObj[jss::nth] = f->fieldValue; + + // whether the field is variable-length encoded + // this means that the length is included before the content + innerObj[jss::isVLEncoded] = + (type == 7U /* Blob */ || type == 8U /* AccountID */ || + type == 19U /* Vector256 */); + + // whether the field is included in serialization + innerObj[jss::isSerialized] = + (type < 10000 && f->fieldName != "hash" && + f->fieldName != "index"); /* hash, index, TRANSACTION, + LEDGER_ENTRY, VALIDATION, METADATA */ + + // whether the field is included in serialization when signing + innerObj[jss::isSigningField] = f->shouldInclude(false); + + innerObj[jss::type] = typeMap[type]; + + Json::Value innerArray = Json::arrayValue; + innerArray[0U] = f->fieldName; + innerArray[1U] = innerObj; + + defs_[jss::FIELDS][i++] = innerArray; + } + + // populate TER code names and values + defs_[jss::TRANSACTION_RESULTS] = Json::objectValue; + + for (auto const& [code, terInfo] : transResults()) + { + defs_[jss::TRANSACTION_RESULTS][terInfo.first] = code; + } + + // populate TxType names and values + defs_[jss::TRANSACTION_TYPES] = Json::objectValue; + defs_[jss::TRANSACTION_TYPES][jss::Invalid] = -1; + for (auto const& f : TxFormats::getInstance()) + { + defs_[jss::TRANSACTION_TYPES][f.getName()] = f.getType(); + } + + // generate hash + { + const std::string out = Json::FastWriter().write(defs_); + defsHash_ = ripple::sha512Half(ripple::Slice{out.data(), out.size()}); + defs_[jss::hash] = to_string(defsHash_); + } +} + +} // namespace detail + +Json::Value +doServerDefinitions(RPC::JsonContext& context) +{ + auto& params = context.params; + + uint256 hash; + if (params.isMember(jss::hash)) + { + if (!params[jss::hash].isString() || + !hash.parseHex(params[jss::hash].asString())) + return RPC::invalid_field_error(jss::hash); + } + + static const detail::ServerDefinitions defs{}; + if (defs.hashMatches(hash)) + { + Json::Value jv = Json::objectValue; + jv[jss::hash] = to_string(hash); + return jv; + } + return defs.get(); +} + Json::Value doServerInfo(RPC::JsonContext& context) { diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index dd898ee8722..b69d2608b0e 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -80,6 +80,7 @@ Handler const handlerArray[]{ {"channel_verify", byRef(&doChannelVerify), Role::USER, NO_CONDITION}, {"connect", byRef(&doConnect), Role::ADMIN, NO_CONDITION}, {"consensus_info", byRef(&doConsensusInfo), Role::ADMIN, NO_CONDITION}, + {"crawl_shards", byRef(&doCrawlShards), Role::ADMIN, NO_CONDITION}, {"deposit_authorized", byRef(&doDepositAuthorized), Role::USER, @@ -139,17 +140,20 @@ Handler const handlerArray[]{ Role::ADMIN, NO_CONDITION}, {"ripple_path_find", byRef(&doRipplePathFind), Role::USER, NO_CONDITION}, + {"server_definitions", + byRef(&doServerDefinitions), + Role::USER, + NO_CONDITION}, + {"server_info", byRef(&doServerInfo), Role::USER, NO_CONDITION}, + {"server_state", byRef(&doServerState), Role::USER, NO_CONDITION}, {"sign", byRef(&doSign), Role::USER, NO_CONDITION}, {"sign_for", byRef(&doSignFor), Role::USER, NO_CONDITION}, + {"stop", byRef(&doStop), Role::ADMIN, NO_CONDITION}, {"submit", byRef(&doSubmit), Role::USER, NEEDS_CURRENT_LEDGER}, {"submit_multisigned", byRef(&doSubmitMultiSigned), Role::USER, NEEDS_CURRENT_LEDGER}, - {"server_info", byRef(&doServerInfo), Role::USER, NO_CONDITION}, - {"server_state", byRef(&doServerState), Role::USER, NO_CONDITION}, - {"crawl_shards", byRef(&doCrawlShards), Role::ADMIN, NO_CONDITION}, - {"stop", byRef(&doStop), Role::ADMIN, NO_CONDITION}, {"transaction_entry", byRef(&doTransactionEntry), Role::USER, NO_CONDITION}, {"tx", byRef(&doTxJson), Role::USER, NEEDS_NETWORK_CONNECTION}, {"tx_history", byRef(&doTxHistory), Role::USER, NO_CONDITION}, diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp index a69483cb130..1d78f1cc36b 100644 --- a/src/test/rpc/ServerInfo_test.cpp +++ b/src/test/rpc/ServerInfo_test.cpp @@ -79,6 +79,8 @@ admin = 127.0.0.1 void testServerInfo() { + testcase("server_info"); + using namespace test::jtx; { @@ -148,10 +150,104 @@ admin = 127.0.0.1 } } + void + testServerDefinitions() + { + testcase("server_definitions"); + + using namespace test::jtx; + + { + Env env(*this); + auto const result = env.rpc("server_definitions"); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + + // test a random element of each result + // (testing the whole output would be difficult to maintain) + + { + auto const firstField = result[jss::result][jss::FIELDS][0u]; + BEAST_EXPECT(firstField[0u].asString() == "Generic"); + BEAST_EXPECT( + firstField[1][jss::isSerialized].asBool() == false); + BEAST_EXPECT( + firstField[1][jss::isSigningField].asBool() == false); + BEAST_EXPECT(firstField[1][jss::isVLEncoded].asBool() == false); + BEAST_EXPECT(firstField[1][jss::nth].asUInt() == 0); + BEAST_EXPECT(firstField[1][jss::type].asString() == "Unknown"); + } + + BEAST_EXPECT( + result[jss::result][jss::LEDGER_ENTRY_TYPES]["AccountRoot"] + .asUInt() == 97); + BEAST_EXPECT( + result[jss::result][jss::TRANSACTION_RESULTS]["tecDIR_FULL"] + .asUInt() == 121); + BEAST_EXPECT( + result[jss::result][jss::TRANSACTION_TYPES]["Payment"] + .asUInt() == 0); + BEAST_EXPECT( + result[jss::result][jss::TYPES]["AccountID"].asUInt() == 8); + } + + // test providing the same hash + { + Env env(*this); + auto const firstResult = env.rpc("server_definitions"); + auto const hash = firstResult[jss::result][jss::hash].asString(); + auto const hashParam = + std::string("{ ") + "\"hash\": \"" + hash + "\"}"; + + auto const result = + env.rpc("json", "server_definitions", hashParam); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(!result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + + // test providing a different hash + { + Env env(*this); + std::string const hash = + "54296160385A27154BFA70A239DD8E8FD4CC2DB7BA32D970BA3A5B132CF749" + "D1"; + auto const hashParam = + std::string("{ ") + "\"hash\": \"" + hash + "\"}"; + + auto const result = + env.rpc("json", "server_definitions", hashParam); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + } + void run() override { testServerInfo(); + testServerDefinitions(); } }; From 8d86c5e17d14b013ad9139e79743e6674e58744a Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Thu, 19 Oct 2023 19:00:49 +0200 Subject: [PATCH 3/4] fix: allow pseudo-transactions to omit NetworkID (#4737) The Network ID logic should not be applied to pseudo-transactions. This allows amendments to enable on a network with an ID > 1024. Context: - NetworkID: https://github.com/XRPLF/rippled/pull/4370 - Pseudo-transactions: https://xrpl.org/pseudo-transaction-types.html Fix #4736 --------- Co-authored-by: RichardAH --- src/ripple/app/tx/impl/Transactor.cpp | 37 +++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 449392531b8..1affb6fcc56 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -40,25 +40,28 @@ namespace ripple { NotTEC preflight0(PreflightContext const& ctx) { - uint32_t const nodeNID = ctx.app.config().NETWORK_ID; - std::optional const txNID = ctx.tx[~sfNetworkID]; - - if (nodeNID <= 1024) + if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID)) { - // legacy networks have IDs 1024 and below. These networks cannot - // specify NetworkID in txn - if (txNID) - return telNETWORK_ID_MAKES_TX_NON_CANONICAL; - } - else - { - // new networks both require the field to be present and require it to - // match - if (!txNID) - return telREQUIRES_NETWORK_ID; + uint32_t nodeNID = ctx.app.config().NETWORK_ID; + std::optional txNID = ctx.tx[~sfNetworkID]; + + if (nodeNID <= 1024) + { + // legacy networks have ids less than 1024, these networks cannot + // specify NetworkID in txn + if (txNID) + return telNETWORK_ID_MAKES_TX_NON_CANONICAL; + } + else + { + // new networks both require the field to be present and require it + // to match + if (!txNID) + return telREQUIRES_NETWORK_ID; - if (*txNID != nodeNID) - return telWRONG_NETWORK; + if (*txNID != nodeNID) + return telWRONG_NETWORK; + } } auto const txID = ctx.tx.getTransactionID(); From 5af9dc5abd5ba4dc955a6eb96701dea390c636e8 Mon Sep 17 00:00:00 2001 From: shichengsg002 <147461171+shichengsg002@users.noreply.github.com> Date: Fri, 20 Oct 2023 01:53:38 +0800 Subject: [PATCH 4/4] fix(CI): set permission for doxygen workflow (#4756) --- .github/workflows/doxygen.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 65515ea737f..6607ae59d42 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -11,6 +11,8 @@ concurrency: jobs: job: runs-on: ubuntu-latest + permissions: + contents: write container: image: docker://rippleci/rippled-ci-builder:2944b78d22db steps: