diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index a00d6b85c1b..041ffe3fcee 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,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 = 79; +static constexpr std::size_t numFeatures = 80; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -372,6 +372,7 @@ extern uint256 const fixEnforceNFTokenTrustline; extern uint256 const fixInnerObjTemplate2; extern uint256 const featureInvariantsV1_1; extern uint256 const fixNFTokenPageLinks; +extern uint256 const featureCredentials; } // namespace ripple diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index bc9c23d1910..38fa2efe9e9 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -84,6 +84,9 @@ enum class HashPrefix : std::uint32_t { /** Payment Channel Claim */ paymentChannelClaim = detail::make_hash_prefix('C', 'L', 'M'), + + /** Credentials signature */ + credential = detail::make_hash_prefix('C', 'R', 'S'), }; template diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index f179bbacfab..aaffe122476 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -30,6 +30,7 @@ #include #include #include + #include namespace ripple { @@ -189,6 +190,9 @@ check(uint256 const& key) noexcept Keylet depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept; +Keylet +depositPreauth(AccountID const& owner, STArray const& authCreds) noexcept; + inline Keylet depositPreauth(uint256 const& key) noexcept { @@ -287,6 +291,18 @@ did(AccountID const& account) noexcept; Keylet oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; +Keylet +credential( + AccountID const& subject, + AccountID const& issuer, + Slice const& credType) noexcept; + +inline Keylet +credential(uint256 const& key) noexcept +{ + return {ltCREDENTIAL, key}; +} + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 0ee6c992d8d..58310bf29b7 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -197,6 +197,12 @@ enum LedgerEntryType : std::uint16_t */ ltORACLE = 0x0080, + /** A ledger object which describes a Verifiable Credentials for DID. + + \sa keylet::credential + */ + ltCREDENTIAL = 0x0081, + //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. @@ -308,6 +314,9 @@ enum LedgerSpecificFlags { // ltNFTOKEN_OFFER lsfSellNFToken = 0x00000001, + + // ltCREDENTIAL + lsfAccepted = 0x00010000, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 8d8a71dfef8..0d8787d76df 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -48,6 +48,9 @@ std::size_t constexpr unfundedOfferRemoveLimit = 1000; /** The maximum number of expired offers to delete at once */ std::size_t constexpr expiredOfferRemoveLimit = 256; +/** The maximum number of credentials can be passed in array */ +std::size_t constexpr credentialsArrayMaxSize = 8; + /** The maximum number of metadata entries allowed in one transaction */ std::size_t constexpr oversizeMetaDataCap = 5200; @@ -95,6 +98,9 @@ std::size_t constexpr maxDIDAttestationLength = 256; /** The maximum length of a domain */ std::size_t constexpr maxDomainLength = 256; +/** The maximum length of a URI inside a Credential */ +std::size_t constexpr maxCredentialURILength = 256; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 7f54201a4b8..793321c70b0 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -576,6 +576,7 @@ extern SF_VL const sfHookStateData; extern SF_VL const sfHookReturnString; extern SF_VL const sfHookParameterName; extern SF_VL const sfHookParameterValue; +extern SF_VL const sfCredentialType; // account extern SF_ACCOUNT const sfAccount; @@ -596,6 +597,7 @@ extern SF_ACCOUNT const sfAttestationSignerAccount; extern SF_ACCOUNT const sfAttestationRewardAccount; extern SF_ACCOUNT const sfLockingChainDoor; extern SF_ACCOUNT const sfIssuingChainDoor; +extern SF_ACCOUNT const sfSubject; // path set extern SField const sfPaths; @@ -618,6 +620,7 @@ extern SF_VECTOR256 const sfIndexes; extern SF_VECTOR256 const sfHashes; extern SF_VECTOR256 const sfAmendments; extern SF_VECTOR256 const sfNFTokenOffers; +extern SF_VECTOR256 const sfCredentialIDs; // inner object // OBJECT/1 is reserved for end of object @@ -651,6 +654,7 @@ extern SField const sfXChainClaimProofSig; extern SField const sfXChainCreateAccountProofSig; extern SField const sfXChainClaimAttestationCollectionElement; extern SField const sfXChainCreateAccountAttestationCollectionElement; +extern SField const sfCredential; // array of objects (common) // ARRAY/1 is reserved for end of array @@ -676,6 +680,8 @@ extern SField const sfHookParameters; extern SField const sfHookGrants; extern SField const sfXChainClaimAttestations; extern SField const sfXChainCreateAccountAttestations; +extern SField const sfAuthorizeCredentials; +extern SField const sfUnauthorizeCredentials; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index aae3c7107bd..8556cd5d4e2 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -139,6 +139,8 @@ enum TEMcodes : TERUnderlyingType { temARRAY_EMPTY, temARRAY_TOO_LARGE, + + temBAD_CREDENTIALS, }; //------------------------------------------------------------------------------ @@ -339,7 +341,7 @@ enum TECcodes : TERUnderlyingType { tecINVALID_UPDATE_TIME = 188, tecTOKEN_PAIR_NOT_FOUND = 189, tecARRAY_EMPTY = 190, - tecARRAY_TOO_LARGE = 191 + tecARRAY_TOO_LARGE = 191, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index a3f5cca108c..c149b93425a 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -200,6 +200,15 @@ enum TxType : std::uint16_t ttLEDGER_STATE_FIX = 53, + /** This transaction type creates an Credential instance */ + ttCREDENTIAL_CREATE = 54, + + /** This transaction type accepts an Credential instance */ + ttCREDENTIAL_ACCEPT = 55, + + /** This transaction type deletes an Credential instance */ + ttCREDENTIAL_DELETE = 56, + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index e3eda80b44f..087a6e549d9 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -72,6 +72,10 @@ JSS(CheckCash); // transaction type. JSS(CheckCreate); // transaction type. JSS(Clawback); // transaction type. JSS(ClearFlag); // field. +JSS(Credential); // ledger type. +JSS(CredentialAccept); // transaction type. +JSS(CredentialCreate); // transaction type. +JSS(CredentialDelete); // transaction type. JSS(DID); // ledger type. JSS(DIDDelete); // transaction type. JSS(DIDSet); // transaction type. @@ -137,6 +141,7 @@ JSS(SendMax); // in: TransactionSign JSS(Sequence); // in/out: TransactionSign; field. JSS(SetFlag); // field. JSS(SetRegularKey); // transaction type. +JSS(Signature); // in: Credential transactions JSS(SignerList); // ledger type. JSS(SignerListSet); // transaction type. JSS(SigningPubKey); // field. @@ -208,6 +213,7 @@ JSS(attestations); // JSS(attestation_reward_account); // JSS(auction_slot); // out: amm_info JSS(authorized); // out: AccountLines +JSS(authorize_credentials); // in: ledger_entry DepositPreauth JSS(auth_accounts); // out: amm_info JSS(auth_change); // out: AccountInfo JSS(auth_change_queued); // out: AccountInfo @@ -266,6 +272,9 @@ JSS(converge_time_s); // out: NetworkOPs JSS(cookie); // out: NetworkOPs JSS(count); // in: AccountTx*, ValidatorList JSS(counters); // in/out: retrieve counters +JSS(credential); // in: LedgerEntry Credential +JSS(credentials); // in: deposit_authorized +JSS(credential_type); // in: LedgerEntry DepositPreauth JSS(ctid); // in/out: Tx RPC JSS(currency_a); // out: BookChanges JSS(currency_b); // out: BookChanges @@ -651,6 +660,7 @@ JSS(streams); // in: Subscribe, Unsubscribe JSS(strict); // in: AccountCurrencies, AccountInfo JSS(sub_index); // in: LedgerEntry JSS(subcommand); // in: PathFind +JSS(subject); // in: LedgerEntry Credential JSS(success); // rpc JSS(supported); // out: AmendmentTableImpl JSS(sync_mode); // in: Submit diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 078369bf20c..0da774c728a 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -498,6 +498,8 @@ REGISTER_FIX (fixReducedOffersV2, Supported::yes, VoteBehavior::De REGISTER_FIX (fixEnforceNFTokenTrustline, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixInnerObjTemplate2, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixNFTokenPageLinks, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo); + // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. REGISTER_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo); diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 30d97416cfa..c45de108007 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,7 @@ enum class LedgerNameSpace : std::uint16_t { XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', DID = 'I', ORACLE = 'R', + CREDENTIAL = 'D', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -301,6 +303,27 @@ depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept indexHash(LedgerNameSpace::DEPOSIT_PREAUTH, owner, preauthorized)}; } +Keylet +depositPreauth(AccountID const& owner, STArray const& authCreds) noexcept +{ + std::string s; + std::vector hashes; + hashes.reserve(authCreds.size()); + for (auto const& o : authCreds) + { + hashes.push_back(sha512Half( + o.getAccountID(sfIssuer), o.getFieldVL(sfCredentialType))); + } + + // eleminate duplicates + std::sort(hashes.begin(), hashes.end()); + hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end()); + + return { + ltDEPOSIT_PREAUTH, + indexHash(LedgerNameSpace::DEPOSIT_PREAUTH, owner, hashes)}; +} + //------------------------------------------------------------------------------ Keylet @@ -451,6 +474,17 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept return {ltORACLE, indexHash(LedgerNameSpace::ORACLE, account, documentID)}; } +Keylet +credential( + AccountID const& subject, + AccountID const& issuer, + Slice const& credType) noexcept +{ + return { + ltCREDENTIAL, + indexHash(LedgerNameSpace::CREDENTIAL, subject, issuer, credType)}; +} + } // namespace keylet } // namespace ripple diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 6d7b855d199..87c42a8085f 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -147,6 +147,13 @@ InnerObjectFormats::InnerObjectFormats() {sfAssetPrice, soeOPTIONAL}, {sfScale, soeDEFAULT}, }); + + add(sfCredential.jsonName.c_str(), + sfCredential.getCode(), + { + {sfIssuer, soeREQUIRED}, + {sfCredentialType, soeREQUIRED}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 9401c00278b..581cf626fea 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -19,7 +19,6 @@ #include #include -#include namespace ripple { @@ -233,10 +232,11 @@ LedgerFormats::LedgerFormats() ltDEPOSIT_PREAUTH, { {sfAccount, soeREQUIRED}, - {sfAuthorize, soeREQUIRED}, + {sfAuthorize, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfAuthorizeCredentials, soeOPTIONAL}, }, commonFields); @@ -365,6 +365,20 @@ LedgerFormats::LedgerFormats() }, commonFields); + add(jss::Credential, + ltCREDENTIAL, + { + {sfSubject, soeREQUIRED}, + {sfIssuer, soeREQUIRED}, + {sfCredentialType, soeREQUIRED}, + {sfExpiration, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + }, + commonFields); + // clang-format on } diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index f8eb2d6f877..64a54c64ea1 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -307,6 +307,7 @@ CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27); CONSTRUCT_TYPED_SFIELD(sfAssetClass, "AssetClass", VL, 28); CONSTRUCT_TYPED_SFIELD(sfProvider, "Provider", VL, 29); +CONSTRUCT_TYPED_SFIELD(sfCredentialType, "CredentialType", VL, 30); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); @@ -328,12 +329,14 @@ CONSTRUCT_TYPED_SFIELD(sfAttestationSignerAccount, "AttestationSignerAccount", A CONSTRUCT_TYPED_SFIELD(sfAttestationRewardAccount, "AttestationRewardAccount", ACCOUNT, 21); CONSTRUCT_TYPED_SFIELD(sfLockingChainDoor, "LockingChainDoor", ACCOUNT, 22); CONSTRUCT_TYPED_SFIELD(sfIssuingChainDoor, "IssuingChainDoor", ACCOUNT, 23); +CONSTRUCT_TYPED_SFIELD(sfSubject, "Subject", ACCOUNT, 24); // vector of 256-bit CONSTRUCT_TYPED_SFIELD(sfIndexes, "Indexes", VECTOR256, 1, SField::sMD_Never); CONSTRUCT_TYPED_SFIELD(sfHashes, "Hashes", VECTOR256, 2); CONSTRUCT_TYPED_SFIELD(sfAmendments, "Amendments", VECTOR256, 3); CONSTRUCT_TYPED_SFIELD(sfNFTokenOffers, "NFTokenOffers", VECTOR256, 4); +CONSTRUCT_TYPED_SFIELD(sfCredentialIDs, "CredentialIDs", VECTOR256, 5); // path set CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1); @@ -391,6 +394,8 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, "XChainCreateAccountAttestationCollectionElement", OBJECT, 31); CONSTRUCT_UNTYPED_SFIELD(sfPriceData, "PriceData", OBJECT, 32); +CONSTRUCT_UNTYPED_SFIELD(sfCredential, "Credential", OBJECT, 33); + // array of objects // ARRAY/1 is reserved for end of array @@ -421,6 +426,8 @@ CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestations, // 23 is unused and available for use CONSTRUCT_UNTYPED_SFIELD(sfPriceDataSeries, "PriceDataSeries", ARRAY, 24); CONSTRUCT_UNTYPED_SFIELD(sfAuthAccounts, "AuthAccounts", ARRAY, 25); +CONSTRUCT_UNTYPED_SFIELD(sfAuthorizeCredentials, "AuthorizeCredentials", ARRAY, 26); +CONSTRUCT_UNTYPED_SFIELD(sfUnauthorizeCredentials, "UnauthorizeCredentials", ARRAY, 27); // clang-format on diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 917bbf26a9f..9dd36efe45b 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -205,6 +205,7 @@ transResults() MAKE_ERROR(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, "Malformed: Bad reward amount."), MAKE_ERROR(temARRAY_EMPTY, "Malformed: Array is empty."), MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."), + MAKE_ERROR(temBAD_CREDENTIALS, "Malformed: Provided credentials aren't authorised by destination."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 8a93232604e..c64b53f4ca7 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -168,6 +168,7 @@ TxFormats::TxFormats() {sfInvoiceID, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, }, commonFields); @@ -190,6 +191,7 @@ TxFormats::TxFormats() {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, }, commonFields); @@ -280,6 +282,7 @@ TxFormats::TxFormats() {sfBalance, soeOPTIONAL}, {sfSignature, soeOPTIONAL}, {sfPublicKey, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, }, commonFields); @@ -315,6 +318,7 @@ TxFormats::TxFormats() { {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, }, commonFields); @@ -323,6 +327,8 @@ TxFormats::TxFormats() { {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, + {sfAuthorizeCredentials, soeOPTIONAL}, + {sfUnauthorizeCredentials, soeOPTIONAL}, }, commonFields); @@ -513,6 +519,35 @@ TxFormats::TxFormats() {sfOwner, soeOPTIONAL}, }, commonFields); + + add(jss::CredentialCreate, + ttCREDENTIAL_CREATE, + { + {sfSubject, soeOPTIONAL}, + {sfPublicKey, soeOPTIONAL}, + {sfCredentialType, soeREQUIRED}, + {sfSignature, soeOPTIONAL}, + {sfExpiration, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, + }, + commonFields); + + add(jss::CredentialAccept, + ttCREDENTIAL_ACCEPT, + { + {sfIssuer, soeREQUIRED}, + {sfCredentialType, soeREQUIRED}, + }, + commonFields); + + add(jss::CredentialDelete, + ttCREDENTIAL_DELETE, + { + {sfSubject, soeOPTIONAL}, + {sfIssuer, soeOPTIONAL}, + {sfCredentialType, soeREQUIRED}, + }, + commonFields); } TxFormats const& diff --git a/src/test/jtx/acctdelete.h b/src/test/jtx/acctdelete.h index 98a23c6de29..912719ae7c6 100644 --- a/src/test/jtx/acctdelete.h +++ b/src/test/jtx/acctdelete.h @@ -29,7 +29,10 @@ namespace jtx { /** Delete account. If successful transfer remaining XRP to dest. */ Json::Value -acctdelete(Account const& account, Account const& dest); +acctdelete( + Account const& account, + Account const& dest, + std::vector const& credentialIDs = {}); } // namespace jtx diff --git a/src/test/jtx/impl/acctdelete.cpp b/src/test/jtx/impl/acctdelete.cpp index d7f8f10e04d..ac0ce77a618 100644 --- a/src/test/jtx/impl/acctdelete.cpp +++ b/src/test/jtx/impl/acctdelete.cpp @@ -26,12 +26,21 @@ namespace jtx { // Delete account. If successful transfer remaining XRP to dest. Json::Value -acctdelete(jtx::Account const& account, jtx::Account const& dest) +acctdelete( + jtx::Account const& account, + jtx::Account const& dest, + std::vector const& credentialIDs) { Json::Value jv; jv[sfAccount.jsonName] = account.human(); jv[sfDestination.jsonName] = dest.human(); jv[sfTransactionType.jsonName] = jss::AccountDelete; + if (!credentialIDs.empty()) + { + auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue); + for (auto const& o : credentialIDs) + arr.append(o); + } return jv; } diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index 799911f63dd..0ea1a481a9a 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -143,7 +143,7 @@ printHelp(const po::options_description& desc) " connect []\n" " consensus_info\n" " deposit_authorized " - "[]\n" + "[] []\n" " feature [ [accept|reject]]\n" " fetch_info [clear]\n" " gateway_balances [] [ [ " diff --git a/src/xrpld/app/tx/detail/Credentials.cpp b/src/xrpld/app/tx/detail/Credentials.cpp new file mode 100644 index 00000000000..7390dd41b19 --- /dev/null +++ b/src/xrpld/app/tx/detail/Credentials.cpp @@ -0,0 +1,446 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 +#include + +#include + +namespace ripple { + +/* + Credentials + ====== + + A verifiable credentials (VC + https://en.wikipedia.org/wiki/Verifiable_credentials), as defined by the W3C + specification (https://www.w3.org/TR/vc-data-model-2.0/), is a + secure and tamper-evident way to represent information about a subject, such + as an individual, organization, or even an IoT device. These credentials are + issued by a trusted entity and can be verified by third parties without + directly involving the issuer at all. +*/ + +//------------------------------------------------------------------------------ + +bool +credentialCheckExpired( + std::shared_ptr const& sle, + NetClock::time_point const& closed) +{ + std::uint32_t const exp = + sle->isFieldPresent(sfExpiration) ? sle->getFieldU32(sfExpiration) : 0; + std::uint32_t const now = closed.time_since_epoch().count(); + return static_cast(exp) && (now > exp); +} + +// special check for deletion +static bool +credentialCheckNotExpired( + std::shared_ptr const& sle, + NetClock::time_point const& closed) +{ + if (!sle->isFieldPresent(sfExpiration)) + return false; + + std::uint32_t const exp = sle->getFieldU32(sfExpiration); + std::uint32_t const now = closed.time_since_epoch().count(); + + return now <= exp; +} + +Blob +signCredential( + PublicKey const& issuerPK, + SecretKey const& issuerSK, + AccountID const& subject, + std::string_view credType) +{ + AccountID const issuer(calcAccountID(issuerPK)); + Slice sct(credType.data(), credType.size()); + auto const kCred = keylet::credential(subject, issuer, sct); + + Serializer msg; + msg.add32(HashPrefix::credential); + msg.addBitString(kCred.key); + + auto const b = sign(issuerPK, issuerSK, msg.slice()); + return Blob(b.cbegin(), b.cend()); +} + +// ------- CREATE -------------------------- + +NotTEC +CredentialCreate::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCredentials)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const& tx = ctx.tx; + auto& j = ctx.j; + + auto const subject = tx[~sfSubject]; + auto const pubKey = tx[~sfPublicKey]; + auto const signature = tx[~sfSignature]; + + if ((subject.has_value() == pubKey.has_value()) || + (pubKey.has_value() != signature.has_value())) + { + // Either both fields are present or neither field is present. In + // either case the transaction is malformed. + JLOG(j.trace()) + << "Malformed transaction: " + "Invalid Subject, Issuer and Signature fields combination."; + return temMALFORMED; + } + + if (subject && subject->isZero()) + { + JLOG(j.trace()) << "Malformed transaction: Subject field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + auto const optUri = tx[~sfURI]; + if (optUri && (optUri->size() > maxCredentialURILength)) + { + JLOG(j.trace()) << "Malformed transaction: " + "URI too long."; + return temMALFORMED; + } + + if (signature) + { + PublicKey const pk(*pubKey); + AccountID const issuer(calcAccountID(pk)); + auto const kCred = + keylet::credential(tx[sfAccount], issuer, ctx.tx[sfCredentialType]); + + Serializer msg; + msg.add32(HashPrefix::credential); + msg.addBitString(kCred.key); + + if (!verify(pk, msg.slice(), *signature, /*canonical*/ true)) + return temBAD_SIGNATURE; + } + + return preflight2(ctx); +} + +TER +CredentialCreate::preclaim(PreclaimContext const& ctx) +{ + auto const credType(ctx.tx[sfCredentialType]); + AccountID const account(ctx.tx[sfAccount]); + auto const subject = ctx.tx[~sfSubject].value_or(account); + auto const issuer = ctx.tx.isFieldPresent(sfPublicKey) + ? calcAccountID(PublicKey(ctx.tx[sfPublicKey])) + : account; + + if (ctx.tx.isFieldPresent(sfSubject) && + !ctx.view.exists(keylet::account(subject))) + return tecNO_TARGET; + + if (ctx.view.exists(keylet::credential(subject, issuer, credType))) + return tecDUPLICATE; + + return tesSUCCESS; +} + +TER +CredentialCreate::doApply() +{ + auto const sleOwner = view().peek(keylet::account(account_)); + + { + STAmount const reserve{view().fees().accountReserve( + sleOwner->getFieldU32(sfOwnerCount) + 1)}; + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + auto const subject = ctx_.tx[~sfSubject].value_or(account_); + auto const issuer = ctx_.tx.isFieldPresent(sfPublicKey) + ? calcAccountID(PublicKey(ctx_.tx[sfPublicKey])) + : account_; + auto const credType(ctx_.tx[sfCredentialType]); + Keylet const kCred = keylet::credential(subject, issuer, credType); + auto const sleCred = std::make_shared(kCred); + + auto const optExp = ctx_.tx[~sfExpiration]; + if (optExp) + { + std::uint32_t const closeTime = + ctx_.view().info().parentCloseTime.time_since_epoch().count(); + + if (closeTime > *optExp) + { + JLOG(j_.trace()) << "Malformed transaction: " + "Expiration time is in the past."; + return tecEXPIRED; + } + + sleCred->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration)); + } + + sleCred->setAccountID(sfSubject, subject); + sleCred->setAccountID(sfIssuer, issuer); + sleCred->setFieldVL(sfCredentialType, credType); + + if (ctx_.tx.isFieldPresent(sfSignature)) + { + } + + if (ctx_.tx.isFieldPresent(sfURI)) + sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI)); + + if (!ctx_.tx.isFieldPresent(sfSubject)) + sleCred->setFieldU32(sfFlags, lsfAccepted); + + view().insert(sleCred); + auto const page = view().dirInsert( + keylet::ownerDir(account_), kCred, describeOwnerDir(account_)); + + JLOG(j_.trace()) << "Adding Credential to owner directory " + << to_string(kCred.key) << ": " + << (page ? "success" : "failure"); + + if (!page) + return tecDIR_FULL; + + sleCred->setFieldU64(sfOwnerNode, *page); + + adjustOwnerCount(view(), sleOwner, 1, j_); + view().update(sleOwner); + + return tesSUCCESS; +} + +// ------- DELETE -------------------------- +NotTEC +CredentialDelete::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCredentials)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const optSubj = ctx.tx[~sfSubject]; + auto const optIss = ctx.tx[~sfIssuer]; + + if (!optSubj && !optIss) + { + // Neither field is present, the transaction is malformed. + JLOG(ctx.j.trace()) << "Malformed transaction: " + "Invalid Subject and Issuer fields combination."; + return temMALFORMED; + } + + // Make sure that the passed account is valid. + if ((optSubj && !AccountID(*optSubj)) || (optIss && !AccountID(*optIss))) + { + JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer " + "field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + return preflight2(ctx); +} + +TER +CredentialDelete::preclaim(PreclaimContext const& ctx) +{ + AccountID const account{ctx.tx[sfAccount]}; + auto const subject = ctx.tx[~sfSubject].value_or(account); + auto const issuer = ctx.tx[~sfIssuer].value_or(account); + auto const credType(ctx.tx[sfCredentialType]); + + if (!ctx.view.exists(keylet::credential(subject, issuer, credType))) + return tecNO_ENTRY; + + return tesSUCCESS; +} + +TER +CredentialDelete::deleteSLE( + ApplyView& view, + std::shared_ptr const& sle, + beast::Journal j) +{ + if (!sle) + return tecNO_ENTRY; + + AccountID const owner = sle->getAccountID( + (sle->getFlags() & lsfAccepted) ? sfSubject : sfIssuer); + + auto const sleOwner = view.peek(keylet::account(owner)); + if (!sleOwner) + return tecINTERNAL; + + // Remove object from owner directory + std::uint64_t const page = sle->getFieldU64(sfOwnerNode); + if (!view.dirRemove(keylet::ownerDir(owner), page, sle->key(), false)) + { + JLOG(j.fatal()) << "Unable to delete Credential from owner."; + return tefBAD_LEDGER; + } + + adjustOwnerCount(view, sleOwner, -1, j); + view.update(sleOwner); + + // Remove object from ledger + view.erase(sle); + + return tesSUCCESS; +} + +TER +CredentialDelete::doApply() +{ + auto const subject = ctx_.tx[~sfSubject].value_or(account_); + auto const issuer = ctx_.tx[~sfIssuer].value_or(account_); + + auto const credType(ctx_.tx[sfCredentialType]); + auto const sleCred = + view().peek(keylet::credential(subject, issuer, credType)); + + if ((subject != account_) && (issuer != account_)) + { + if (credentialCheckNotExpired( + sleCred, ctx_.view().info().parentCloseTime)) + return tecNO_PERMISSION; + } + + return deleteSLE(view(), sleCred, j_); +} + +// ------- APPLY -------------------------- + +NotTEC +CredentialAccept::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCredentials)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + AccountID const issuer = ctx.tx[sfIssuer]; + if (!issuer) + { + JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + return preflight2(ctx); +} + +TER +CredentialAccept::preclaim(PreclaimContext const& ctx) +{ + AccountID const subject = ctx.tx[sfAccount]; + AccountID const issuer = ctx.tx[sfIssuer]; + auto const credType(ctx.tx[sfCredentialType]); + + if (!ctx.view.exists(keylet::account(issuer))) + return tecNO_TARGET; + + auto const sleCred = + ctx.view.read(keylet::credential(subject, issuer, credType)); + if (!sleCred) + return tecNO_ENTRY; + + if (sleCred->getFieldU32(sfFlags) & lsfAccepted) + return tecDUPLICATE; + + return tesSUCCESS; +} + +TER +CredentialAccept::doApply() +{ + AccountID const subject{account_}; + AccountID const issuer{ctx_.tx[sfIssuer]}; + + auto const sleSubj = view().peek(keylet::account(subject)); + auto const sleIss = view().peek(keylet::account(issuer)); + + { + STAmount const reserve{view().fees().accountReserve( + sleSubj->getFieldU32(sfOwnerCount) + 1)}; + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + auto const credType(ctx_.tx[sfCredentialType]); + Keylet const kCred = keylet::credential(subject, issuer, credType); + auto const sleCred = ctx_.view().peek(kCred); // Checked in preclaim() + + if (credentialCheckExpired(sleCred, ctx_.view().info().parentCloseTime)) + { + JLOG(j_.trace()) << "Credentials are expired. Cred: " + << sleCred->getText(); + // delete expired credentials even if the transaction failed + CredentialDelete::deleteSLE(ctx_.view(), sleCred, j_); + return tecEXPIRED; + } + + // Change ownership from issuer to subject + std::uint64_t const page = sleCred->getFieldU64(sfOwnerNode); + if (!view().dirRemove(keylet::ownerDir(issuer), page, kCred, false)) + { + JLOG(j_.fatal()) + << "CredentialAccept: Unable to delete Credential from owner."; + return tefBAD_LEDGER; + } + adjustOwnerCount(view(), sleIss, -1, j_); + view().update(sleIss); + + auto const pageIns = view().dirInsert( + keylet::ownerDir(subject), kCred, describeOwnerDir(subject)); + + JLOG(j_.trace()) << "Moving Credential to owner directory " + << to_string(kCred.key) << ": " + << (pageIns ? "success" : "failure"); + + if (!pageIns) + return tecDIR_FULL; + + sleCred->setFieldU32(sfFlags, lsfAccepted); + sleCred->setFieldU64(sfOwnerNode, *pageIns); + + adjustOwnerCount(view(), sleSubj, 1, j_); + view().update(sleCred); + view().update(sleSubj); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Credentials.h b/src/xrpld/app/tx/detail/Credentials.h new file mode 100644 index 00000000000..a14af5027d5 --- /dev/null +++ b/src/xrpld/app/tx/detail/Credentials.h @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2024 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. +*/ +//============================================================================== + +#pragma once + +#include + +namespace ripple { + +class CredentialCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +class CredentialDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + static TER + deleteSLE( + ApplyView& view, + std::shared_ptr const& sle, + beast::Journal j); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +class CredentialAccept : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +bool +credentialCheckExpired( + std::shared_ptr const& sle, + NetClock::time_point const& closed); + +Blob +signCredential( + PublicKey const& issuerPK, + SecretKey const& issuerSK, + AccountID const& subject, + std::string_view credType); + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index fb2f3fc507f..1d3c544edf0 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -51,6 +52,10 @@ DeleteAccount::preflight(PreflightContext const& ctx) // An account cannot be deleted and give itself the resulting XRP. return temDST_IS_SRC; + auto const err = DepositPreauth::preauthPreflightCheck(ctx, ctx.j); + if (!isTesSuccess(err)) + return err; + return preflight2(ctx); } @@ -110,14 +115,14 @@ removeTicketFromLedger( TER removeDepositPreauthFromLedger( - Application& app, + Application&, ApplyView& view, - AccountID const& account, + AccountID const&, uint256 const& delIndex, - std::shared_ptr const& sleDel, + std::shared_ptr const&, beast::Journal j) { - return DepositPreauth::removeFromLedger(app, view, delIndex, j); + return DepositPreauth::removeFromLedger(view, delIndex, j); } TER @@ -159,6 +164,18 @@ removeOracleFromLedger( return DeleteOracle::deleteOracle(view, sleDel, account, j); } +TER +removeCredentialFromLedger( + Application&, + ApplyView& view, + AccountID const&, + uint256 const&, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return CredentialDelete::deleteSLE(view, sleDel, 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 @@ -181,6 +198,8 @@ nonObligationDeleter(LedgerEntryType t) return removeDIDFromLedger; case ltORACLE: return removeOracleFromLedger; + case ltCREDENTIAL: + return removeCredentialFromLedger; default: return nullptr; } @@ -203,12 +222,10 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) return tecDST_TAG_NEEDED; // Check whether the destination account requires deposit authorization. - if (ctx.view.rules().enabled(featureDepositAuth) && - (sleDst->getFlags() & lsfDepositAuth)) - { - if (!ctx.view.exists(keylet::depositPreauth(dst, account))) - return tecNO_PERMISSION; - } + auto const err = DepositPreauth::preauthPreclaimCheck( + ctx.view, ctx.tx, account, dst, sleDst, ctx.j); + if (!isTesSuccess(err)) + return err; auto sleAccount = ctx.view.read(keylet::account(account)); assert(sleAccount); @@ -322,6 +339,16 @@ DeleteAccount::doApply() if (!src || !dst) return tefBAD_LEDGER; + auto const err = DepositPreauth::preauthApplyCheck( + view(), + ctx_.tx, + account_, + ctx_.tx.getAccountID(sfDestination), + dst, + j_); + if (!isTesSuccess(err)) + return err; + Keylet const ownerDirKeylet{keylet::ownerDir(account_)}; auto const ter = cleanupOnAccountDelete( view(), diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index b60fd3e0eae..3f673b5f1cb 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -17,16 +17,217 @@ */ //============================================================================== +#include #include #include #include #include #include #include +#include #include +#include +#include + namespace ripple { +struct HashUint256 +{ + std::size_t + operator()(uint256 const& x) const noexcept + { + std::size_t u = 0; + std::size_t const* const b = + reinterpret_cast(x.data()); + std::size_t const* const e = b + 256 / 8 / sizeof(std::size_t); + + for (std::size_t const* c = b; c < e; ++c) + boost::hash_combine(u, *c); + + return u; + } +}; + +bool +DepositPreauth::credentialIDsRemoveExpired( + ApplyView& view, + STTx const& tx, + beast::Journal const j) +{ + auto const closeTime = view.info().parentCloseTime; + bool foundExpired = false; + + STVector256 const& arr(tx.getFieldV256(sfCredentialIDs)); + for (auto const& h : arr) + { + // Credentials already checked in preclaim. Look only for expired here. + auto const k = keylet::credential(h); + auto const sleCred = view.peek(k); + + if (credentialCheckExpired(sleCred, closeTime)) + { + JLOG(j.trace()) + << "Credentials are expired. Cred: " << sleCred->getText(); + // delete expired credentials even if the transaction failed + CredentialDelete::deleteSLE(view, sleCred, j); + foundExpired = true; + } + } + + return foundExpired; +} + +NotTEC +DepositPreauth::preauthPreflightCheck( + PreflightContext const& ctx, + beast::Journal const j) +{ + if (ctx.tx.isFieldPresent(sfCredentialIDs)) + { + if (!ctx.rules.enabled(featureCredentials) || + !ctx.rules.enabled(featureDepositPreauth) || + !ctx.rules.enabled(featureDepositAuth)) + { + JLOG(j.trace()) << "Credentials rule is disabled."; + return temDISABLED; + } + + STVector256 const& arr(ctx.tx.getFieldV256(sfCredentialIDs)); + if (arr.empty() || (arr.size() > credentialsArrayMaxSize)) + { + JLOG(j.trace()) + << "Malformed transaction: Credentials array is empty."; + return temMALFORMED; + } + } + return tesSUCCESS; +} + +TER +DepositPreauth::preauthPreclaimCredentialsCheck( + ReadView const& view, + AccountID const& src, + AccountID const& dst, + STVector256 const& credentials, + beast::Journal const j) +{ + STArray authCreds; + + for (auto const& h : credentials) + { + auto const sleCred = view.read(keylet::credential(h)); + if (!sleCred) + { + JLOG(j.trace()) << "Credential doesn't exists. Cred: " << h; + return temBAD_CREDENTIALS; + } + + if (sleCred->getAccountID(sfSubject) != src) + { + JLOG(j.trace()) + << "Credential not for current account. Cred: " << h; + return temBAD_CREDENTIALS; + } + + if (!(sleCred->getFlags() & lsfAccepted)) + { + JLOG(j.trace()) << "Credential not accepted. Cred: " << h; + return temBAD_CREDENTIALS; + } + + auto o = STObject::makeInnerObject(sfCredential); + o.setAccountID(sfIssuer, sleCred->getAccountID(sfIssuer)); + o.setFieldVL(sfCredentialType, sleCred->getFieldVL(sfCredentialType)); + authCreds.push_back(std::move(o)); + } + + if (!view.exists(keylet::depositPreauth(dst, authCreds))) + { + JLOG(j.trace()) << "DepositPreauth doesn't exists"; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +// Not used in Payment tx +TER +DepositPreauth::preauthPreclaimCheck( + ReadView const& view, + STTx const& tx, + AccountID const& src, + AccountID const& dst, + std::shared_ptr const& sleDst, + beast::Journal const j) +{ + bool const ruleAuth = view.rules().enabled(featureDepositAuth); + bool const rulePreAuth = view.rules().enabled(featureDepositPreauth); + bool const reqAuth = + sleDst && (sleDst->getFlags() & lsfDepositAuth) && ruleAuth; + bool const credentialsPresent = tx.isFieldPresent(sfCredentialIDs); + + if (reqAuth && !rulePreAuth) + { + JLOG(j.trace()) << "Preauth rule is disabled."; + return tecNO_PERMISSION; + } + + if (credentialsPresent && (!reqAuth || (src == dst))) + { + JLOG(j.trace()) << "Destination doesn't not require authorization."; + return tecNO_PERMISSION; + } + + if (!reqAuth || (src == dst)) + return tesSUCCESS; + + if (credentialsPresent) + { + auto const err = preauthPreclaimCredentialsCheck( + view, src, dst, tx.getFieldV256(sfCredentialIDs), j); + if (!isTesSuccess(err)) + return err; + } + else if (!view.exists(keylet::depositPreauth(dst, src))) + { + JLOG(j.trace()) << "DepositPreauth doesn't exist"; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +// Not used in Payment tx +TER +DepositPreauth::preauthApplyCheck( + ApplyView& view, + STTx const& tx, + AccountID const& src, + AccountID const& dst, + std::shared_ptr const& sleDst, + beast::Journal const j) +{ + // If depositPreauth is enabled, then an account that requires + // authorization has 2 ways to get an XRP Payment in: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. This already + // checked in preauthPreclaimCheck + + bool const reqAuth = sleDst && (sleDst->getFlags() & lsfDepositAuth); + + if (!reqAuth || (src == dst)) + return tesSUCCESS; + + if (tx.isFieldPresent(sfCredentialIDs)) + { + if (credentialIDsRemoveExpired(view, tx, j)) + return tecEXPIRED; + } + + return tesSUCCESS; +} + NotTEC DepositPreauth::preflight(PreflightContext const& ctx) { @@ -36,7 +237,7 @@ DepositPreauth::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - auto& tx = ctx.tx; + auto const& tx = ctx.tx; auto& j = ctx.j; if (tx.getFlags() & tfUniversalMask) @@ -45,9 +246,17 @@ DepositPreauth::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - auto const optAuth = ctx.tx[~sfAuthorize]; - auto const optUnauth = ctx.tx[~sfUnauthorize]; - if (static_cast(optAuth) == static_cast(optUnauth)) + auto const optAuth = tx[~sfAuthorize]; + auto const optUnauth = tx[~sfUnauthorize]; + int const authPresent = static_cast(optAuth.has_value()) + + static_cast(optUnauth.has_value()); + + bool const authArrPresent = tx.isFieldPresent(sfAuthorizeCredentials); + bool const unauthArrPresent = tx.isFieldPresent(sfUnauthorizeCredentials); + int const authCredPresent = + static_cast(authArrPresent) + static_cast(unauthArrPresent); + + if (authPresent + authCredPresent != 1) { // Either both fields are present or neither field is present. In // either case the transaction is malformed. @@ -58,20 +267,72 @@ DepositPreauth::preflight(PreflightContext const& ctx) } // Make sure that the passed account is valid. - AccountID const target{optAuth ? *optAuth : *optUnauth}; - if (target == beast::zero) + AccountID const account(tx[sfAccount]); + if (authPresent) { - JLOG(j.trace()) << "Malformed transaction: Authorized or Unauthorized " - "field zeroed."; - return temINVALID_ACCOUNT_ID; - } + AccountID const& target(optAuth ? *optAuth : *optUnauth); + if (!target) + { + JLOG(j.trace()) + << "Malformed transaction: Authorized or Unauthorized " + "field zeroed."; + return temINVALID_ACCOUNT_ID; + } - // An account may not preauthorize itself. - if (optAuth && (target == ctx.tx[sfAccount])) + // An account may not preauthorize itself. + if (target == account) + { + JLOG(j.trace()) + << "Malformed transaction: Attempting to DepositPreauth self."; + return temCANNOT_PREAUTH_SELF; + } + } + else { - JLOG(j.trace()) - << "Malformed transaction: Attempting to DepositPreauth self."; - return temCANNOT_PREAUTH_SELF; + if (!ctx.rules.enabled(featureCredentials)) + { + JLOG(j.trace()) << "Credentials rule is disabled."; + return temDISABLED; + } + + STArray const& arr(tx.getFieldArray( + authArrPresent ? sfAuthorizeCredentials + : sfUnauthorizeCredentials)); + if (arr.empty() || (arr.size() > credentialsArrayMaxSize)) + { + JLOG(j.trace()) << "Malformed transaction: " + "Invalid AuthorizeCredentials size: " + << arr.size(); + return temMALFORMED; + } + + for (auto const& o : arr) + { + AccountID const target(o[sfIssuer]); + if (!target) + { + JLOG(j.trace()) + << "Malformed transaction: " + "AuthorizeCredentials Issuer account is invalid."; + return temINVALID_ACCOUNT_ID; + } + + // An account may not preauthorize itself. + if (target == account) + { + JLOG(j.trace()) << "Malformed transaction: Attempting to " + "DepositPreauth self."; + return temCANNOT_PREAUTH_SELF; + } + + auto const ct = o[sfCredentialType]; + if (ct.empty()) + { + JLOG(j.trace()) + << "Malformed transaction: empty credential type."; + return temMALFORMED; + } + } } return preflight2(ctx); @@ -80,6 +341,8 @@ DepositPreauth::preflight(PreflightContext const& ctx) TER DepositPreauth::preclaim(PreclaimContext const& ctx) { + AccountID const account(ctx.tx[sfAccount]); + // Determine which operation we're performing: authorizing or unauthorizing. if (ctx.tx.isFieldPresent(sfAuthorize)) { @@ -90,14 +353,30 @@ DepositPreauth::preclaim(PreclaimContext const& ctx) // Verify that the Preauth entry they asked to add is not already // in the ledger. - if (ctx.view.exists(keylet::depositPreauth(ctx.tx[sfAccount], auth))) + if (ctx.view.exists(keylet::depositPreauth(account, auth))) return tecDUPLICATE; } - else + else if (ctx.tx.isFieldPresent(sfUnauthorize)) { // Verify that the Preauth entry they asked to remove is in the ledger. AccountID const unauth{ctx.tx[sfUnauthorize]}; - if (!ctx.view.exists(keylet::depositPreauth(ctx.tx[sfAccount], unauth))) + if (!ctx.view.exists(keylet::depositPreauth(account, unauth))) + return tecNO_ENTRY; + } + else if (ctx.tx.isFieldPresent(sfAuthorizeCredentials)) + { + STArray const& authCred(ctx.tx.getFieldArray(sfAuthorizeCredentials)); + // Verify that the Preauth entry they asked to add is not already + // in the ledger. + if (ctx.view.exists(keylet::depositPreauth(account, authCred))) + return tecDUPLICATE; + } + else if (ctx.tx.isFieldPresent(sfUnauthorizeCredentials)) + { + auto const& unauthCred = ctx.tx.getFieldArray(sfUnauthorizeCredentials); + + // Verify that the Preauth entry is in the ledger. + if (!ctx.view.exists(keylet::depositPreauth(account, unauthCred))) return tecNO_ENTRY; } return tesSUCCESS; @@ -133,7 +412,6 @@ DepositPreauth::doApply() slePreauth->setAccountID(sfAuthorize, auth); view().insert(slePreauth); - auto viewJ = ctx_.app.journal("View"); auto const page = view().dirInsert( keylet::ownerDir(account_), preauthKeylet, @@ -149,35 +427,97 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, 1, viewJ); + adjustOwnerCount(view(), sleOwner, 1, j_); } - else + else if (ctx_.tx.isFieldPresent(sfUnauthorize)) { auto const preauth = keylet::depositPreauth(account_, ctx_.tx[sfUnauthorize]); - return DepositPreauth::removeFromLedger( - ctx_.app, view(), preauth.key, j_); + return DepositPreauth::removeFromLedger(view(), preauth.key, j_); + } + else if (ctx_.tx.isFieldPresent(sfAuthorizeCredentials)) + { + auto const sleOwner = view().peek(keylet::account(account_)); + if (!sleOwner) + return tefINTERNAL; + + // A preauth counts against the reserve of the issuing account, but we + // check the starting balance because we want to allow dipping into the + // reserve to pay fees. + { + STAmount const reserve{view().fees().accountReserve( + sleOwner->getFieldU32(sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + // Preclaim already verified that the Preauth entry does not yet exist. + // Create and populate the Preauth entry. + + STArray const& authCred(ctx_.tx.getFieldArray(sfAuthorizeCredentials)); + Keylet const kPreauth = keylet::depositPreauth(account_, authCred); + auto slePreauth = std::make_shared(kPreauth); + slePreauth->setAccountID(sfAccount, account_); + auto* pArr = slePreauth->makeFieldPresent(sfAuthorizeCredentials); + if (!pArr) + return tefINTERNAL; + STArray& arr = *static_cast(pArr); + arr.reserve(authCred.size()); + + // eleminate duplicates + std::unordered_set hashes; + for (auto const& o : authCred) + { + auto [it, ins] = hashes.insert(sha512Half( + o.getAccountID(sfIssuer), o.getFieldVL(sfCredentialType))); + if (ins) + { + arr.push_back(o); + } + else + { + JLOG(j_.trace()) + << "DepositPreauth duplicate removed: " << o.getText(); + } + } + + view().insert(slePreauth); + + auto const page = view().dirInsert( + keylet::ownerDir(account_), kPreauth, describeOwnerDir(account_)); + + JLOG(j_.trace()) << "Adding DepositPreauth to owner directory " + << to_string(kPreauth.key) << ": " + << (page ? "success" : "failure"); + + if (!page) + return tecDIR_FULL; + + slePreauth->setFieldU64(sfOwnerNode, *page); + + // If we succeeded, the new entry counts against the creator's reserve. + adjustOwnerCount(view(), sleOwner, 1, j_); } + else if (ctx_.tx.isFieldPresent(sfUnauthorizeCredentials)) + { + auto const kPreauth = keylet::depositPreauth( + account_, ctx_.tx.getFieldArray(sfUnauthorizeCredentials)); + return DepositPreauth::removeFromLedger(view(), kPreauth.key, j_); + } + return tesSUCCESS; } TER DepositPreauth::removeFromLedger( - Application& app, ApplyView& view, uint256 const& preauthIndex, beast::Journal j) { - // Verify that the Preauth entry they asked to remove is - // in the ledger. - std::shared_ptr const slePreauth{ - view.peek(keylet::depositPreauth(preauthIndex))}; - if (!slePreauth) - { - JLOG(j.warn()) << "Selected DepositPreauth does not exist."; - return tecNO_ENTRY; - } + // Existence already checked in preclaim and DeleteAccount + auto const slePreauth{view.peek(keylet::depositPreauth(preauthIndex))}; AccountID const account{(*slePreauth)[sfAccount]}; std::uint64_t const page{(*slePreauth)[sfOwnerNode]}; @@ -192,7 +532,7 @@ DepositPreauth::removeFromLedger( if (!sleOwner) return tefINTERNAL; - adjustOwnerCount(view, sleOwner, -1, app.journal("View")); + adjustOwnerCount(view, sleOwner, -1, j); // Remove DepositPreauth from ledger. view.erase(slePreauth); diff --git a/src/xrpld/app/tx/detail/DepositPreauth.h b/src/xrpld/app/tx/detail/DepositPreauth.h index 5edcee104d0..bdbc983c46a 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.h +++ b/src/xrpld/app/tx/detail/DepositPreauth.h @@ -45,10 +45,47 @@ class DepositPreauth : public Transactor // Interface used by DeleteAccount static TER removeFromLedger( - Application& app, ApplyView& view, uint256 const& delIndex, beast::Journal j); + + // Next 3 are used by transactions that check pre-authorization (move funds + // transactions) + static NotTEC + preauthPreflightCheck(PreflightContext const& ctx, beast::Journal const j); + + static TER + preauthPreclaimCheck( + ReadView const& view, + STTx const& tx, + AccountID const& src, + AccountID const& dst, + std::shared_ptr const& sleDest, + beast::Journal const j); + + static TER + preauthPreclaimCredentialsCheck( + ReadView const& view, + AccountID const& src, + AccountID const& dst, + STVector256 const& credentials, + beast::Journal const j); + + static TER + preauthApplyCheck( + ApplyView& view, + STTx const& tx, + AccountID const& src, + AccountID const& dst, + std::shared_ptr const& sleDest, + beast::Journal const j); + + // return true if at least 1 expired credentials was found(and deleted) + static bool + credentialIDsRemoveExpired( + ApplyView& view, + STTx const& tx, + beast::Journal const j); }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index e34b675998d..f0d60164434 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -347,6 +348,10 @@ EscrowFinish::preflight(PreflightContext const& ctx) } } + auto const err = DepositPreauth::preauthPreflightCheck(ctx, ctx.j); + if (!isTesSuccess(err)) + return err; + return tesSUCCESS; } @@ -454,22 +459,19 @@ EscrowFinish::doApply() if (!sled) return tecNO_DST; - if (ctx_.view().rules().enabled(featureDepositAuth)) - { - // Is EscrowFinished authorized? - if (sled->getFlags() & lsfDepositAuth) - { - // A destination account that requires authorization has two - // ways to get an EscrowFinished into the account: - // 1. If Account == Destination, or - // 2. If Account is deposit preauthorized by destination. - if (account_ != destID) - { - if (!view().exists(keylet::depositPreauth(destID, account_))) - return tecNO_PERMISSION; - } - } - } + // A destination account that requires authorization has two + // ways to get an EscrowFinished into the account: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. + + auto const e1 = DepositPreauth::preauthPreclaimCheck( + view(), ctx_.tx, account_, destID, sled, j_); + if (!isTesSuccess(e1)) + return e1; + auto const e2 = DepositPreauth::preauthApplyCheck( + view(), ctx_.tx, account_, destID, sled, j_); + if (!isTesSuccess(e2)) + return e2; AccountID const account = (*slep)[sfAccount]; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index f855ad8578c..1ea3ad292db 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -478,6 +478,7 @@ LedgerEntryTypesMatch::visitEntry( case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: case ltDID: case ltORACLE: + case ltCREDENTIAL: break; default: invalidTypeAdded_ = true; diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index d17736c4738..3579a58b34e 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -453,6 +454,10 @@ PayChanClaim::preflight(PreflightContext const& ctx) return temBAD_SIGNATURE; } + auto const err = DepositPreauth::preauthPreflightCheck(ctx, ctx.j); + if (!isTesSuccess(err)) + return err; + return preflight2(ctx); } @@ -516,19 +521,18 @@ PayChanClaim::doApply() (txAccount == src && (sled->getFlags() & lsfDisallowXRP))) return tecNO_TARGET; - // Check whether the destination account requires deposit authorization. - if (depositAuth && (sled->getFlags() & lsfDepositAuth)) - { - // A destination account that requires authorization has two - // ways to get a Payment Channel Claim into the account: - // 1. If Account == Destination, or - // 2. If Account is deposit preauthorized by destination. - if (txAccount != dst) - { - if (!view().exists(keylet::depositPreauth(dst, txAccount))) - return tecNO_PERMISSION; - } - } + // A destination account that requires authorization has two + // ways to get a Payment Channel Claim into the account: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. + auto const e1 = DepositPreauth::preauthPreclaimCheck( + view(), ctx_.tx, txAccount, dst, sled, j_); + if (!isTesSuccess(e1)) + return e1; + auto const e2 = DepositPreauth::preauthApplyCheck( + view(), ctx_.tx, txAccount, dst, sled, j_); + if (!isTesSuccess(e2)) + return e2; (*slep)[sfBalance] = ctx_.tx[sfBalance]; XRPAmount const reqDelta = reqBalance - chanBalance; diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 309e9d4a498..1c3a210e0e5 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -200,6 +201,10 @@ Payment::preflight(PreflightContext const& ctx) } } + auto const err = DepositPreauth::preauthPreflightCheck(ctx, j); + if (!isTesSuccess(err)) + return err; + return preflight2(ctx); } @@ -215,6 +220,8 @@ Payment::preclaim(PreclaimContext const& ctx) AccountID const uDstAccountID(ctx.tx[sfDestination]); STAmount const saDstAmount(ctx.tx[sfAmount]); + bool const bRipple = paths || sendMax || !saDstAmount.native(); + auto const k = keylet::account(uDstAccountID); auto const sleDst = ctx.view.read(k); @@ -271,7 +278,7 @@ Payment::preclaim(PreclaimContext const& ctx) } // Payment with at least one intermediate step and uses transitive balances. - if ((paths || sendMax || !saDstAmount.native()) && ctx.view.open()) + if (bRipple && ctx.view.open()) { STPathSet const& paths = ctx.tx.getFieldPathSet(sfPaths); @@ -284,6 +291,26 @@ Payment::preclaim(PreclaimContext const& ctx) } } + if (ctx.tx.isFieldPresent(sfCredentialIDs)) + { + // Don't check for minimal balance with credentials provided. + // The rules have already been checked in preauthPreflightCheck() + + bool const reqAuth = sleDst && (sleDst->getFlags() & lsfDepositAuth); + if (!reqAuth) + return tecNO_PERMISSION; + + auto const err = DepositPreauth::preauthPreclaimCredentialsCheck( + ctx.view, + ctx.tx[sfAccount], + uDstAccountID, + ctx.tx.getFieldV256(sfCredentialIDs), + ctx.j); + + if (!isTesSuccess(err)) + return err; + } + return tesSUCCESS; } @@ -355,6 +382,8 @@ Payment::doApply() if (!depositPreauth && bRipple && reqDepositAuth) return tecNO_PERMISSION; + bool const credentialsPresent = ctx_.tx.isFieldPresent(sfCredentialIDs); + if (bRipple) { // Ripple payment with at least one intermediate step and uses @@ -366,7 +395,14 @@ Payment::doApply() // authorization has two ways to get an IOU Payment in: // 1. If Account == Destination, or // 2. If Account is deposit preauthorized by destination. - if (uDstAccountID != account_) + + if (credentialsPresent) + { + if (DepositPreauth::credentialIDsRemoveExpired( + view(), ctx_.tx, j_)) + return tecEXPIRED; + } + else if (uDstAccountID != account_) { if (!view().exists( keylet::depositPreauth(uDstAccountID, account_))) @@ -480,7 +516,13 @@ Payment::doApply() // We choose the base reserve as our bound because it is // a small number that seldom changes but is always sufficient // to get the account un-wedged. - if (uDstAccountID != account_) + + if (credentialsPresent) + { + if (DepositPreauth::credentialIDsRemoveExpired(view(), ctx_.tx, j_)) + return tecEXPIRED; + } + else if (uDstAccountID != account_) { if (!view().exists(keylet::depositPreauth(uDstAccountID, account_))) { diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 6ae8be8a67f..6fe3127913b 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -760,6 +761,19 @@ removeExpiredNFTokenOffers( } } +static void +removeExpiredCredentials( + ApplyView& view, + std::vector const& creds, + beast::Journal viewJ) +{ + for (auto const& index : creds) + { + if (auto const sle = view.peek(keylet::credential(index))) + CredentialDelete::deleteSLE(view, sle, viewJ); + } +} + static void removeDeletedTrustLines( ApplyView& view, @@ -907,19 +921,23 @@ Transactor::operator()() std::vector removedOffers; std::vector removedTrustLines; std::vector expiredNFTokenOffers; + std::vector expiredCredentials; bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED)); bool const doLines = (result == tecINCOMPLETE); bool const doNFTokenOffers = (result == tecEXPIRED); - if (doOffers || doLines || doNFTokenOffers) + bool const doCredentials = (result == tecEXPIRED); + if (doOffers || doLines || doNFTokenOffers || doCredentials) { - ctx_.visit([&doOffers, + ctx_.visit([doOffers, &removedOffers, - &doLines, + doLines, &removedTrustLines, - &doNFTokenOffers, - &expiredNFTokenOffers]( + doNFTokenOffers, + &expiredNFTokenOffers, + doCredentials, + &expiredCredentials]( uint256 const& index, bool isDelete, std::shared_ptr const& before, @@ -946,6 +964,10 @@ Transactor::operator()() if (doNFTokenOffers && before && after && (before->getType() == ltNFTOKEN_OFFER)) expiredNFTokenOffers.push_back(index); + + if (doCredentials && before && after && + (before->getType() == ltCREDENTIAL)) + expiredCredentials.push_back(index); } }); } @@ -972,6 +994,10 @@ Transactor::operator()() removeDeletedTrustLines( view(), removedTrustLines, ctx_.app.journal("View")); + if (result == tecEXPIRED) + removeExpiredCredentials( + view(), expiredCredentials, ctx_.app.journal("View")); + applied = isTecClaim(result); } diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index cbeabb6fc9c..107fe8d7fdf 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -168,6 +169,12 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttORACLE_DELETE: return f.template operator()(); + case ttCREDENTIAL_CREATE: + return f.template operator()(); + case ttCREDENTIAL_DELETE: + return f.template operator()(); + case ttCREDENTIAL_ACCEPT: + return f.template operator()(); default: throw UnknownTxnType(txnType); } diff --git a/src/xrpld/net/detail/RPCCall.cpp b/src/xrpld/net/detail/RPCCall.cpp index 997d6463f23..3e7349f71fd 100644 --- a/src/xrpld/net/detail/RPCCall.cpp +++ b/src/xrpld/net/detail/RPCCall.cpp @@ -417,6 +417,7 @@ class RPCParser } // deposit_authorized [] + // [, ...] Json::Value parseDepositAuthorized(Json::Value const& jvParams) { @@ -424,9 +425,17 @@ class RPCParser jvRequest[jss::source_account] = jvParams[0u].asString(); jvRequest[jss::destination_account] = jvParams[1u].asString(); - if (jvParams.size() == 3) + if (jvParams.size() >= 3) jvParseLedger(jvRequest, jvParams[2u].asString()); + // 8 credentials max + if ((jvParams.size() >= 4) && (jvParams.size() <= 11)) + { + jvRequest[jss::credentials] = Json::Value(Json::arrayValue); + for (uint32_t i = 3; i < jvParams.size(); ++i) + jvRequest[jss::credentials].append(jvParams[i].asString()); + } + return jvRequest; } @@ -1161,7 +1170,7 @@ class RPCParser {"channel_verify", &RPCParser::parseChannelVerify, 4, 4}, {"connect", &RPCParser::parseConnect, 1, 2}, {"consensus_info", &RPCParser::parseAsIs, 0, 0}, - {"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3}, + {"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 11}, {"feature", &RPCParser::parseFeature, 0, 2}, {"fetch_info", &RPCParser::parseFetchInfo, 0, 1}, {"gateway_balances", &RPCParser::parseGatewayBalances, 1, -1}, diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 71513ddcd5c..a56531570cf 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -30,11 +30,17 @@ #include #include #include +#include #include +#include #include #include + #include + #include +#include +#include namespace ripple { namespace RPC { @@ -934,31 +940,32 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 22> + static const std::unordered_map types{ - {{jss::account, ltACCOUNT_ROOT}, - {jss::amendments, ltAMENDMENTS}, - {jss::amm, ltAMM}, - {jss::bridge, ltBRIDGE}, - {jss::check, ltCHECK}, - {jss::deposit_preauth, ltDEPOSIT_PREAUTH}, - {jss::did, ltDID}, - {jss::directory, ltDIR_NODE}, - {jss::escrow, ltESCROW}, - {jss::fee, ltFEE_SETTINGS}, - {jss::hashes, ltLEDGER_HASHES}, - {jss::nunl, ltNEGATIVE_UNL}, - {jss::oracle, ltORACLE}, - {jss::nft_offer, ltNFTOKEN_OFFER}, - {jss::nft_page, ltNFTOKEN_PAGE}, - {jss::offer, ltOFFER}, - {jss::payment_channel, ltPAYCHAN}, - {jss::signer_list, ltSIGNER_LIST}, - {jss::state, ltRIPPLE_STATE}, - {jss::ticket, ltTICKET}, - {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, - {jss::xchain_owned_create_account_claim_id, - ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}}}; + {jss::account.c_str(), ltACCOUNT_ROOT}, + {jss::amendments.c_str(), ltAMENDMENTS}, + {jss::amm.c_str(), ltAMM}, + {jss::bridge.c_str(), ltBRIDGE}, + {jss::check.c_str(), ltCHECK}, + {jss::deposit_preauth.c_str(), ltDEPOSIT_PREAUTH}, + {jss::did.c_str(), ltDID}, + {jss::directory.c_str(), ltDIR_NODE}, + {jss::escrow.c_str(), ltESCROW}, + {jss::fee.c_str(), ltFEE_SETTINGS}, + {jss::hashes.c_str(), ltLEDGER_HASHES}, + {jss::nunl.c_str(), ltNEGATIVE_UNL}, + {jss::oracle.c_str(), ltORACLE}, + {jss::nft_offer.c_str(), ltNFTOKEN_OFFER}, + {jss::nft_page.c_str(), ltNFTOKEN_PAGE}, + {jss::offer.c_str(), ltOFFER}, + {jss::payment_channel.c_str(), ltPAYCHAN}, + {jss::signer_list.c_str(), ltSIGNER_LIST}, + {jss::state.c_str(), ltRIPPLE_STATE}, + {jss::ticket.c_str(), ltTICKET}, + {jss::xchain_owned_claim_id.c_str(), ltXCHAIN_OWNED_CLAIM_ID}, + {jss::xchain_owned_create_account_claim_id.c_str(), + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, + {jss::credential.c_str(), ltCREDENTIAL}}; auto const& p = params[jss::type]; if (!p.isString()) @@ -970,10 +977,7 @@ chooseLedgerEntryType(Json::Value const& params) } auto const filter = p.asString(); - auto iter = std::find_if( - types.begin(), types.end(), [&filter](decltype(types.front())& t) { - return t.first == filter; - }); + auto const iter = types.find(filter); if (iter == types.end()) { result.first = diff --git a/src/xrpld/rpc/handlers/DepositAuthorized.cpp b/src/xrpld/rpc/handlers/DepositAuthorized.cpp index 0efa584625b..1631a28f37c 100644 --- a/src/xrpld/rpc/handlers/DepositAuthorized.cpp +++ b/src/xrpld/rpc/handlers/DepositAuthorized.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -32,6 +33,7 @@ namespace ripple { // destination_account : // ledger_hash : // ledger_index : +// credentials : [,...] // } Json::Value @@ -88,20 +90,84 @@ doDepositAuthorized(RPC::JsonContext& context) return result; } - // If the two accounts are the same, then the deposit should be fine. - bool depositAuthorized{true}; - if (srcAcct != dstAcct) + bool const reqAuth = sleDest->getFlags() & lsfDepositAuth; + bool const credentialsPresent = params.isMember(jss::credentials); + + bool invalidCredentials = false; + STArray authCreds; + if (credentialsPresent && reqAuth) { - // Check destination for the DepositAuth flag. If that flag is - // not set then a deposit should be just fine. - if (sleDest->getFlags() & lsfDepositAuth) + auto const& creds(params[jss::credentials]); + if (!creds.isArray() || !creds) { - // See if a preauthorization entry is in the ledger. - auto const sleDepositAuth = - ledger->read(keylet::depositPreauth(dstAcct, srcAcct)); - depositAuthorized = static_cast(sleDepositAuth); + return RPC::make_error( + rpcINVALID_PARAMS, + RPC::expected_field_message( + jss::credentials, "an array of CredentialID(hash256)")); + } + + for (auto const& jo : creds) + { + if (!jo.isString()) + { + return RPC::make_error( + rpcINVALID_PARAMS, + RPC::expected_field_message( + jss::credentials, "an array of CredentialID(hash256)")); + } + + uint256 credH; + auto const credS = jo.asString(); + if (!credH.parseHex(credS)) + { + return RPC::make_error( + rpcINVALID_PARAMS, + RPC::expected_field_message( + jss::credentials, "an array of CredentialID(hash256)")); + } + + std::shared_ptr sleCred = + ledger->read(keylet::credential(credH)); + if (!sleCred || (sleCred->getType() != ltCREDENTIAL)) + { + invalidCredentials = true; + break; + } + + AccountID const subj = sleCred->getAccountID(sfSubject); + AccountID const iss = sleCred->getAccountID(sfIssuer); + Blob const credType = sleCred->getFieldVL(sfCredentialType); + + if ((subj != srcID) || !(sleCred->getFlags() & lsfAccepted) || + credentialCheckExpired(sleCred, context.app.timeKeeper().now())) + { + invalidCredentials = true; + break; + } + + auto o = STObject::makeInnerObject(sfCredential); + o.setAccountID(sfIssuer, iss); + o.setFieldVL(sfCredentialType, credType); + authCreds.push_back(std::move(o)); } } + + // If the two accounts are the same OR if that flag is + // not set, then the deposit should be fine. + bool depositAuthorized = true; + + if (credentialsPresent && !reqAuth) + depositAuthorized = false; + else if ((srcAcct != dstAcct) && reqAuth) + { + if (credentialsPresent) + depositAuthorized = !invalidCredentials && + ledger->exists(keylet::depositPreauth(dstAcct, authCreds)); + else + depositAuthorized = + ledger->exists(keylet::depositPreauth(dstAcct, srcAcct)); + } + result[jss::source_account] = params[jss::source_account].asString(); result[jss::destination_account] = params[jss::destination_account].asString(); diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index f461cd3100b..f2a2e570521 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -34,6 +34,30 @@ namespace ripple { +static STArray +parseAuthorizeCredentials(Json::Value const& jv) +{ + STArray arr; + for (auto const& jo : jv) + { + auto const issuer = parseBase58(jo[jss::issuer].asString()); + if (!issuer || !*issuer) + return {}; + + auto const credentialType = + strUnHex(jo[jss::credential_type].asString()); + if (!credentialType || credentialType->empty()) + return {}; + + auto o = STObject::makeInnerObject(sfCredential); + o.setAccountID(sfIssuer, *issuer); + o.setFieldVL(sfCredentialType, *credentialType); + arr.push_back(std::move(o)); + } + + return arr; +} + // { // ledger_hash : // ledger_index : @@ -84,44 +108,55 @@ doLedgerEntry(RPC::JsonContext& context) else if (context.params.isMember(jss::deposit_preauth)) { expectedType = ltDEPOSIT_PREAUTH; + auto const& dp = context.params[jss::deposit_preauth]; - if (!context.params[jss::deposit_preauth].isObject()) + if (!dp.isObject()) { - if (!context.params[jss::deposit_preauth].isString() || - !uNodeIndex.parseHex( - context.params[jss::deposit_preauth].asString())) + if (!dp.isString() || !uNodeIndex.parseHex(dp.asString())) { uNodeIndex = beast::zero; jvResult[jss::error] = "malformedRequest"; } } + // clang-format off else if ( - !context.params[jss::deposit_preauth].isMember(jss::owner) || - !context.params[jss::deposit_preauth][jss::owner].isString() || - !context.params[jss::deposit_preauth].isMember( - jss::authorized) || - !context.params[jss::deposit_preauth][jss::authorized] - .isString()) + (!dp.isMember(jss::owner) || !dp[jss::owner].isString()) || + (!dp.isMember(jss::authorized) && !dp.isMember(jss::authorize_credentials)) || + (dp.isMember(jss::authorized) && !dp[jss::authorized].isString()) || + (dp.isMember(jss::authorize_credentials) && !dp[jss::authorize_credentials].isArray()) + ) + // clang-format on { jvResult[jss::error] = "malformedRequest"; } else { - auto const owner = parseBase58( - context.params[jss::deposit_preauth][jss::owner] - .asString()); - - auto const authorized = parseBase58( - context.params[jss::deposit_preauth][jss::authorized] - .asString()); - + auto const owner = + parseBase58(dp[jss::owner].asString()); if (!owner) + { jvResult[jss::error] = "malformedOwner"; - else if (!authorized) - jvResult[jss::error] = "malformedAuthorized"; + } + else if (dp.isMember(jss::authorized)) + { + auto const authorized = + parseBase58(dp[jss::authorized].asString()); + if (!authorized) + jvResult[jss::error] = "malformedAuthorized"; + else + uNodeIndex = + keylet::depositPreauth(*owner, *authorized).key; + } else - uNodeIndex = - keylet::depositPreauth(*owner, *authorized).key; + { + auto const& ac(dp[jss::authorize_credentials]); + STArray const arr = parseAuthorizeCredentials(ac); + + if (arr.empty() || (arr.size() > credentialsArrayMaxSize)) + jvResult[jss::error] = "malformedAuthorizeCredentials"; + else + uNodeIndex = keylet::depositPreauth(*owner, arr).key; + } } } else if (context.params.isMember(jss::directory)) @@ -644,6 +679,43 @@ doLedgerEntry(RPC::JsonContext& context) uNodeIndex = keylet::oracle(*account, *documentID).key; } } + else if (context.params.isMember(jss::credential)) + { + expectedType = ltCREDENTIAL; + auto const& cred = context.params[jss::credential]; + + if ((!cred.isMember(jss::subject) || + !cred[jss::subject].isString()) || + (!cred.isMember(jss::issuer) || + !cred[jss::issuer].isString()) || + (!cred.isMember(jss::credential_type) || + !cred[jss::credential_type].isString())) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + auto const subj = + parseBase58(cred[jss::subject].asString()); + auto const iss = + parseBase58(cred[jss::issuer].asString()); + auto const credType = + strUnHex(cred[jss::credential_type].asString()); + if (!subj || subj->isZero() || !iss || iss->isZero() || + !credType || credType->empty()) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + uNodeIndex = keylet::credential( + *subj, + *iss, + Slice(credType->data(), credType->size())) + .key; + } + } + } else { if (context.params.isMember("params") &&