From 8e827e32ace41a410a738ccb8ee19c064469b337 Mon Sep 17 00:00:00 2001 From: Olek <115580134+oleks-rip@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:05:03 -0500 Subject: [PATCH] Introduce Credentials support (XLS-70d): (#5103) Amendment: - Credentials New Transactions: - CredentialCreate - CredentialAccept - CredentialDelete Modified Transactions: - DepositPreauth - Payment - EscrowFinish - PaymentChannelClaim - AccountDelete New Object: - Credential Modified Object: - DepositPreauth API updates: - ledger_entry - account_objects - ledger_data - deposit_authorized Read full spec: https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0070d-credentials --- include/xrpl/protocol/ErrorCodes.h | 5 +- include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/HashPrefix.h | 3 + include/xrpl/protocol/Indexes.h | 18 + include/xrpl/protocol/LedgerFormats.h | 3 + include/xrpl/protocol/Protocol.h | 9 + include/xrpl/protocol/TER.h | 1 + include/xrpl/protocol/UintTypes.h | 6 + include/xrpl/protocol/detail/features.macro | 4 +- .../xrpl/protocol/detail/ledger_entries.macro | 18 +- include/xrpl/protocol/detail/sfields.macro | 8 + .../xrpl/protocol/detail/transactions.macro | 29 + include/xrpl/protocol/jss.h | 8 + src/libxrpl/protocol/ErrorCodes.cpp | 3 +- src/libxrpl/protocol/Indexes.cpp | 30 + src/libxrpl/protocol/InnerObjectFormats.cpp | 7 + src/libxrpl/protocol/TER.cpp | 1 + src/test/app/AccountDelete_test.cpp | 362 ++++++ src/test/app/Check_test.cpp | 10 - src/test/app/Clawback_test.cpp | 10 - src/test/app/Credentials_test.cpp | 1079 +++++++++++++++++ src/test/app/DID_test.cpp | 10 - src/test/app/DepositAuth_test.cpp | 829 +++++++++++++ src/test/app/Escrow_test.cpp | 149 +++ src/test/app/FixNFTokenPageLinks_test.cpp | 10 - src/test/app/MPToken_test.cpp | 152 +++ src/test/app/NFTokenBurn_test.cpp | 10 - src/test/app/NFToken_test.cpp | 12 +- src/test/app/Oracle_test.cpp | 10 - src/test/app/PayChan_test.cpp | 185 +++ src/test/jtx.h | 1 + src/test/jtx/TestHelpers.h | 4 + src/test/jtx/credentials.h | 104 ++ src/test/jtx/deposit.h | 35 + src/test/jtx/impl/TestHelpers.cpp | 11 +- src/test/jtx/impl/credentials.cpp | 110 ++ src/test/jtx/impl/deposit.cpp | 40 + src/test/jtx/impl/mpt.cpp | 13 +- src/test/jtx/mpt.h | 3 +- src/test/rpc/DepositAuthorized_test.cpp | 351 +++++- src/test/rpc/LedgerRPC_test.cpp | 496 ++++++++ src/test/rpc/RPCCall_test.cpp | 23 +- src/xrpld/app/main/Main.cpp | 2 +- src/xrpld/app/misc/CredentialHelpers.cpp | 262 ++++ src/xrpld/app/misc/CredentialHelpers.h | 77 ++ src/xrpld/app/tx/detail/Credentials.cpp | 382 ++++++ src/xrpld/app/tx/detail/Credentials.h | 87 ++ src/xrpld/app/tx/detail/DeleteAccount.cpp | 60 +- src/xrpld/app/tx/detail/DepositPreauth.cpp | 218 +++- src/xrpld/app/tx/detail/DepositPreauth.h | 1 - src/xrpld/app/tx/detail/Escrow.cpp | 37 +- src/xrpld/app/tx/detail/Escrow.h | 3 + src/xrpld/app/tx/detail/InvariantCheck.cpp | 1 + src/xrpld/app/tx/detail/PayChan.cpp | 36 +- src/xrpld/app/tx/detail/PayChan.h | 3 + src/xrpld/app/tx/detail/Payment.cpp | 57 +- src/xrpld/app/tx/detail/Transactor.cpp | 42 +- src/xrpld/app/tx/detail/applySteps.cpp | 1 + src/xrpld/net/detail/RPCCall.cpp | 15 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 5 +- src/xrpld/rpc/handlers/DepositAuthorized.cpp | 110 +- src/xrpld/rpc/handlers/LedgerEntry.cpp | 135 ++- 62 files changed, 5491 insertions(+), 217 deletions(-) create mode 100644 src/test/app/Credentials_test.cpp create mode 100644 src/test/jtx/credentials.h create mode 100644 src/test/jtx/impl/credentials.cpp create mode 100644 src/xrpld/app/misc/CredentialHelpers.cpp create mode 100644 src/xrpld/app/misc/CredentialHelpers.h create mode 100644 src/xrpld/app/tx/detail/Credentials.cpp create mode 100644 src/xrpld/app/tx/detail/Credentials.h diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index d8ec3052b7b..39cfa9369cd 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -148,7 +148,10 @@ enum error_code_i { // Oracle rpcORACLE_MALFORMED = 94, - rpcLAST = rpcORACLE_MALFORMED // rpcLAST should always equal the last code. + // deposit_authorized + credentials + rpcBAD_CREDENTIALS = 95, + + rpcLAST = rpcBAD_CREDENTIALS // rpcLAST should always equal the last code. }; /** Codes returned in the `warnings` array of certain RPC commands. diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index d8353f50a44..90a81c55ef4 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 = 82; +static constexpr std::size_t numFeatures = 83; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index bc9c23d1910..0b6ddda4921 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', 'D'), }; template diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 8249eabb43a..72cf0b527b1 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,11 @@ check(uint256 const& key) noexcept Keylet depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept; +Keylet +depositPreauth( + AccountID const& owner, + std::set> const& authCreds) noexcept; + inline Keylet depositPreauth(uint256 const& key) noexcept { @@ -287,6 +293,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}; +} + Keylet mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept; diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index b0374db1c29..4f3eef4919d 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -186,6 +186,9 @@ enum LedgerSpecificFlags { // ltMPTOKEN lsfMPTAuthorized = 0x00000002, + + // ltCREDENTIAL + lsfAccepted = 0x00010000, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index f706b6a3bbb..a9bd10a6fd1 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -95,6 +95,15 @@ 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; + +/** The maximum length of a CredentialType inside a Credential */ +std::size_t constexpr maxCredentialTypeLength = 64; + +/** The maximum number of credentials can be passed in array */ +std::size_t constexpr maxCredentialsArraySize = 8; + /** The maximum length of MPTokenMetadata */ std::size_t constexpr maxMPTokenMetadataLength = 1024; diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index cf297b0c37b..317e9c2c978 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -343,6 +343,7 @@ enum TECcodes : TERUnderlyingType { tecARRAY_EMPTY = 190, tecARRAY_TOO_LARGE = 191, tecLOCKED = 192, + tecBAD_CREDENTIALS = 193, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index cf676189bad..9a7284158e7 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -134,6 +134,12 @@ struct hash : ripple::Directory::hasher explicit hash() = default; }; +template <> +struct hash : ripple::uint256::hasher +{ + explicit hash() = default; +}; + } // namespace std #endif diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c0d2706cc1a..24c6e72ae34 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,6 +29,8 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_2, Supported::yes, VoteBehavior::DefaultNo) // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. @@ -96,7 +98,7 @@ XRPL_FIX (1513, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(FlowCross, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo) -XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultYes) + // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 3c23539593d..0cb1ec3416a 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -245,10 +245,11 @@ LEDGER_ENTRY(ltOFFER, 0x006f, Offer, ({ */ LEDGER_ENTRY(ltDEPOSIT_PREAUTH, 0x0070, DepositPreauth, ({ {sfAccount, soeREQUIRED}, - {sfAuthorize, soeREQUIRED}, + {sfAuthorize, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfAuthorizeCredentials, soeOPTIONAL}, })) /** A claim id for a cross chain transaction. @@ -420,3 +421,18 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, })) + +/** A ledger object which tracks Credential + \sa keylet::credential + */ +LEDGER_ENTRY(ltCREDENTIAL, 0x0081, Credential, ({ + {sfSubject, soeREQUIRED}, + {sfIssuer, soeREQUIRED}, + {sfCredentialType, soeREQUIRED}, + {sfExpiration, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, + {sfIssuerNode, soeREQUIRED}, + {sfSubjectNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, +})) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index e3a93fc7f46..ccf6350cbfc 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -140,6 +140,8 @@ TYPED_SFIELD(sfAssetPrice, UINT64, 23) TYPED_SFIELD(sfMaximumAmount, UINT64, 24, SField::sMD_BaseTen|SField::sMD_Default) TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SField::sMD_Default) TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default) +TYPED_SFIELD(sfIssuerNode, UINT64, 27) +TYPED_SFIELD(sfSubjectNode, UINT64, 28) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -258,6 +260,7 @@ TYPED_SFIELD(sfData, VL, 27) TYPED_SFIELD(sfAssetClass, VL, 28) TYPED_SFIELD(sfProvider, VL, 29) TYPED_SFIELD(sfMPTokenMetadata, VL, 30) +TYPED_SFIELD(sfCredentialType, VL, 31) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) @@ -280,12 +283,14 @@ TYPED_SFIELD(sfAttestationSignerAccount, ACCOUNT, 20) TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21) TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22) TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23) +TYPED_SFIELD(sfSubject, ACCOUNT, 24) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never) TYPED_SFIELD(sfHashes, VECTOR256, 2) TYPED_SFIELD(sfAmendments, VECTOR256, 3) TYPED_SFIELD(sfNFTokenOffers, VECTOR256, 4) +TYPED_SFIELD(sfCredentialIDs, VECTOR256, 5) // path set UNTYPED_SFIELD(sfPaths, PATHSET, 1) @@ -337,6 +342,7 @@ UNTYPED_SFIELD(sfXChainCreateAccountProofSig, OBJECT, 29) UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) +UNTYPED_SFIELD(sfCredential, OBJECT, 33) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -364,3 +370,5 @@ UNTYPED_SFIELD(sfXChainCreateAccountAttestations, ARRAY, 22) // 23 unused UNTYPED_SFIELD(sfPriceDataSeries, ARRAY, 24) UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25) +UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) +UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index a064abbc12b..4f4c8f12595 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -37,6 +37,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, ({ {sfInvoiceID, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, + {sfCredentialIDs, soeOPTIONAL}, })) /** This transaction type creates an escrow object. */ @@ -55,6 +56,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({ {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, })) @@ -139,6 +141,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({ {sfBalance, soeOPTIONAL}, {sfSignature, soeOPTIONAL}, {sfPublicKey, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, })) /** This transaction type creates a new check. */ @@ -166,6 +169,8 @@ TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, ({ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({ {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, + {sfAuthorizeCredentials, soeOPTIONAL}, + {sfUnauthorizeCredentials, soeOPTIONAL}, })) /** This transaction type modifies a trustline between two accounts. */ @@ -179,6 +184,7 @@ TRANSACTION(ttTRUST_SET, 20, TrustSet, ({ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, })) // 22 reserved @@ -420,6 +426,28 @@ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, ({ {sfHolder, soeOPTIONAL}, })) +/** This transaction type create an Credential instance */ +TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, ({ + {sfSubject, soeREQUIRED}, + {sfCredentialType, soeREQUIRED}, + {sfExpiration, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, +})) + +/** This transaction type accept an Credential object */ +TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, ({ + {sfIssuer, soeREQUIRED}, + {sfCredentialType, soeREQUIRED}, +})) + +/** This transaction type delete an Credential object */ +TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, ({ + {sfSubject, soeOPTIONAL}, + {sfIssuer, soeOPTIONAL}, + {sfCredentialType, soeREQUIRED}, +})) + + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html @@ -455,3 +483,4 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, ({ {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, })) + diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 90e5b1c6e47..f9e0db24949 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -63,6 +63,7 @@ JSS(BidMin); // in: AMM Bid JSS(Bridge); // ledger type. JSS(Check); // ledger type. JSS(ClearFlag); // field. +JSS(Credential); // ledger type. JSS(DID); // ledger type. JSS(DeliverMax); // out: alias to Amount JSS(DeliverMin); // in: TransactionSign @@ -75,6 +76,7 @@ JSS(FeeSettings); // ledger type. JSS(Flags); // in/out: TransactionSign; field. JSS(Holder); // field. JSS(Invalid); // +JSS(Issuer); // in: Credential transactions JSS(LastLedgerSequence); // in: TransactionSign; field JSS(LastUpdateTime); // field. JSS(LedgerHashes); // ledger type. @@ -107,6 +109,7 @@ JSS(Sequence); // in/out: TransactionSign; field. JSS(SetFlag); // field. JSS(SignerList); // ledger type. JSS(SigningPubKey); // field. +JSS(Subject); // in: Credential transactions JSS(TakerGets); // field. JSS(TakerPays); // field. JSS(Ticket); // ledger type. @@ -165,6 +168,7 @@ JSS(attestations); JSS(attestation_reward_account); JSS(auction_slot); // out: amm_info JSS(authorized); // out: AccountLines +JSS(authorized_credentials); // in: ledger_entry DepositPreauth JSS(auth_accounts); // out: amm_info JSS(auth_change); // out: AccountInfo JSS(auth_change_queued); // out: AccountInfo @@ -228,6 +232,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 @@ -614,6 +621,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/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index 4c934f4fd53..c157d9b296c 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -108,7 +108,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{ {rpcTOO_BUSY, "tooBusy", "The server is too busy to help you now.", 503}, {rpcTXN_NOT_FOUND, "txnNotFound", "Transaction not found.", 404}, {rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method.", 405}, - {rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}}; + {rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}, + {rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400}}; // clang-format on // Sort and validate unorderedErrorInfos at compile time. Should be diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 9a537eaaf2b..12142879ad5 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -63,6 +63,7 @@ enum class LedgerNameSpace : std::uint16_t { XRP_PAYMENT_CHANNEL = 'x', CHECK = 'C', DEPOSIT_PREAUTH = 'p', + DEPOSIT_PREAUTH_CREDENTIALS = 'P', NEGATIVE_UNL = 'N', NFTOKEN_OFFER = 'q', NFTOKEN_BUY_OFFERS = 'h', @@ -75,6 +76,7 @@ enum class LedgerNameSpace : std::uint16_t { ORACLE = 'R', MPTOKEN_ISSUANCE = '~', MPTOKEN = 't', + CREDENTIAL = 'D', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -313,6 +315,22 @@ depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept indexHash(LedgerNameSpace::DEPOSIT_PREAUTH, owner, preauthorized)}; } +// Credentials should be sorted here, use credentials::makeSorted +Keylet +depositPreauth( + AccountID const& owner, + std::set> const& authCreds) noexcept +{ + std::vector hashes; + hashes.reserve(authCreds.size()); + for (auto const& o : authCreds) + hashes.emplace_back(sha512Half(o.first, o.second)); + + return { + ltDEPOSIT_PREAUTH, + indexHash(LedgerNameSpace::DEPOSIT_PREAUTH_CREDENTIALS, owner, hashes)}; +} + //------------------------------------------------------------------------------ Keylet @@ -489,6 +507,18 @@ mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept return { ltMPTOKEN, indexHash(LedgerNameSpace::MPTOKEN, issuanceKey, holder)}; } + +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/TER.cpp b/src/libxrpl/protocol/TER.cpp index 90809b29981..815b27c0018 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -116,6 +116,7 @@ transResults() MAKE_ERROR(tecARRAY_EMPTY, "Array is empty."), MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), MAKE_ERROR(tecLOCKED, "Fund is locked."), + MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 5fbb0ba38b1..f8d3cf4692a 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -912,6 +912,366 @@ class AccountDelete_test : public beast::unit_test::suite env.close(); } + void + testDestinationDepositAuthCredentials() + { + { + testcase( + "Destination Constraints with DepositPreauth and Credentials"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const carol{"carol"}; + Account const daria{"daria"}; + + const char credType[] = "abcd"; + + Env env{*this}; + env.fund(XRP(100000), alice, becky, carol, daria); + env.close(); + + // carol issue credentials for becky + env(credentials::create(becky, carol, credType)); + env.close(); + + // get credentials index + auto const jv = + credentials::ledgerEntry(env, becky, carol, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // Close enough ledgers to be able to delete becky's account. + incLgrSeqForAccDel(env, becky); + + auto const acctDelFee{drops(env.current()->fees().increment)}; + + // becky use credentials but they aren't accepted + env(acctdelete(becky, alice), + credentials::ids({credIdx}), + fee(acctDelFee), + ter(tecBAD_CREDENTIALS)); + env.close(); + + { + // alice sets the lsfDepositAuth flag on her account. This + // should prevent becky from deleting her account while using + // alice as the destination. + env(fset(alice, asfDepositAuth)); + env.close(); + } + + // Fail, credentials still not accepted + env(acctdelete(becky, alice), + credentials::ids({credIdx}), + fee(acctDelFee), + ter(tecBAD_CREDENTIALS)); + env.close(); + + // becky accept the credentials + env(credentials::accept(becky, carol, credType)); + env.close(); + + // Fail, credentials doesn’t belong to carol + env(acctdelete(carol, alice), + credentials::ids({credIdx}), + fee(acctDelFee), + ter(tecBAD_CREDENTIALS)); + + // Fail, no depositPreauth for provided credentials + env(acctdelete(becky, alice), + credentials::ids({credIdx}), + fee(acctDelFee), + ter(tecNO_PERMISSION)); + env.close(); + + // alice create DepositPreauth Object + env(deposit::authCredentials(alice, {{carol, credType}})); + env.close(); + + // becky attempts to delete her account, but alice won't take her + // XRP, so the delete is blocked. + env(acctdelete(becky, alice), + fee(acctDelFee), + ter(tecNO_PERMISSION)); + + // becky use empty credentials and can't delete account + env(acctdelete(becky, alice), + fee(acctDelFee), + credentials::ids({}), + ter(temMALFORMED)); + + // becky use bad credentials and can't delete account + env(acctdelete(becky, alice), + credentials::ids( + {"48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6E" + "A288BE4"}), + fee(acctDelFee), + ter(tecBAD_CREDENTIALS)); + env.close(); + + // becky use credentials and can delete account + env(acctdelete(becky, alice), + credentials::ids({credIdx}), + fee(acctDelFee)); + env.close(); + + { + // check that credential object deleted too + auto const jNoCred = + credentials::ledgerEntry(env, becky, carol, credType); + BEAST_EXPECT( + jNoCred.isObject() && jNoCred.isMember(jss::result) && + jNoCred[jss::result].isMember(jss::error) && + jNoCred[jss::result][jss::error] == "entryNotFound"); + } + + testcase("Credentials that aren't required"); + { // carol issue credentials for daria + env(credentials::create(daria, carol, credType)); + env.close(); + env(credentials::accept(daria, carol, credType)); + env.close(); + std::string const credDaria = + credentials::ledgerEntry( + env, daria, carol, credType)[jss::result][jss::index] + .asString(); + + // daria use valid credentials, which aren't required and can + // delete her account + env(acctdelete(daria, carol), + credentials::ids({credDaria}), + fee(acctDelFee)); + env.close(); + + // check that credential object deleted too + auto const jNoCred = + credentials::ledgerEntry(env, daria, carol, credType); + + BEAST_EXPECT( + jNoCred.isObject() && jNoCred.isMember(jss::result) && + jNoCred[jss::result].isMember(jss::error) && + jNoCred[jss::result][jss::error] == "entryNotFound"); + } + + { + Account const eaton{"eaton"}; + Account const fred{"fred"}; + + env.fund(XRP(5000), eaton, fred); + + // carol issue credentials for eaton + env(credentials::create(eaton, carol, credType)); + env.close(); + env(credentials::accept(eaton, carol, credType)); + env.close(); + std::string const credEaton = + credentials::ledgerEntry( + env, eaton, carol, credType)[jss::result][jss::index] + .asString(); + + // fred make preauthorization through authorized account + env(fset(fred, asfDepositAuth)); + env.close(); + env(deposit::auth(fred, eaton)); + env.close(); + + // Close enough ledgers to be able to delete becky's account. + incLgrSeqForAccDel(env, eaton); + auto const acctDelFee{drops(env.current()->fees().increment)}; + + // eaton use valid credentials, but he already authorized + // through "Authorized" field. + env(acctdelete(eaton, fred), + credentials::ids({credEaton}), + fee(acctDelFee)); + env.close(); + + // check that credential object deleted too + auto const jNoCred = + credentials::ledgerEntry(env, eaton, carol, credType); + + BEAST_EXPECT( + jNoCred.isObject() && jNoCred.isMember(jss::result) && + jNoCred[jss::result].isMember(jss::error) && + jNoCred[jss::result][jss::error] == "entryNotFound"); + } + + testcase("Expired credentials"); + { + Account const john{"john"}; + + env.fund(XRP(10000), john); + env.close(); + + auto jv = credentials::create(john, carol, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 20; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + env(credentials::accept(john, carol, credType)); + env.close(); + jv = credentials::ledgerEntry(env, john, carol, credType); + std::string const credIdx = + jv[jss::result][jss::index].asString(); + + incLgrSeqForAccDel(env, john); + + // credentials are expired + // john use credentials but can't delete account + env(acctdelete(john, alice), + credentials::ids({credIdx}), + fee(acctDelFee), + ter(tecEXPIRED)); + env.close(); + + { + // check that expired credential object deleted + auto jv = + credentials::ledgerEntry(env, john, carol, credType); + BEAST_EXPECT( + jv.isObject() && jv.isMember(jss::result) && + jv[jss::result].isMember(jss::error) && + jv[jss::result][jss::error] == "entryNotFound"); + } + } + } + + { + testcase("Credentials feature disabled"); + using namespace test::jtx; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const carol{"carol"}; + + Env env{*this, supported_amendments() - featureCredentials}; + env.fund(XRP(100000), alice, becky, carol); + env.close(); + + // alice sets the lsfDepositAuth flag on her account. This should + // prevent becky from deleting her account while using alice as the + // destination. + env(fset(alice, asfDepositAuth)); + env.close(); + + // Close enough ledgers to be able to delete becky's account. + incLgrSeqForAccDel(env, becky); + + auto const acctDelFee{drops(env.current()->fees().increment)}; + + std::string const credIdx = + "098B7F1B146470A1C5084DC7832C04A72939E3EBC58E68AB8B579BA072B0CE" + "CB"; + + // and can't delete even with old DepositPreauth + env(deposit::auth(alice, becky)); + env.close(); + + env(acctdelete(becky, alice), + credentials::ids({credIdx}), + fee(acctDelFee), + ter(temDISABLED)); + env.close(); + } + } + + void + testDeleteCredentialsOwner() + { + { + testcase("Deleting Issuer deletes issued credentials"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const carol{"carol"}; + + const char credType[] = "abcd"; + + Env env{*this}; + env.fund(XRP(100000), alice, becky, carol); + env.close(); + + // carol issue credentials for becky + env(credentials::create(becky, carol, credType)); + env.close(); + env(credentials::accept(becky, carol, credType)); + env.close(); + + // get credentials index + auto const jv = + credentials::ledgerEntry(env, becky, carol, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // Close enough ledgers to be able to delete carol's account. + incLgrSeqForAccDel(env, carol); + + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(carol, alice), fee(acctDelFee)); + env.close(); + + { // check that credential object deleted too + BEAST_EXPECT(!env.le(credIdx)); + auto const jv = + credentials::ledgerEntry(env, becky, carol, credType); + BEAST_EXPECT( + jv.isObject() && jv.isMember(jss::result) && + jv[jss::result].isMember(jss::error) && + jv[jss::result][jss::error] == "entryNotFound"); + } + } + + { + testcase("Deleting Subject deletes issued credentials"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const carol{"carol"}; + + const char credType[] = "abcd"; + + Env env{*this}; + env.fund(XRP(100000), alice, becky, carol); + env.close(); + + // carol issue credentials for becky + env(credentials::create(becky, carol, credType)); + env.close(); + env(credentials::accept(becky, carol, credType)); + env.close(); + + // get credentials index + auto const jv = + credentials::ledgerEntry(env, becky, carol, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // Close enough ledgers to be able to delete carol's account. + incLgrSeqForAccDel(env, becky); + + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(becky, alice), fee(acctDelFee)); + env.close(); + + { // check that credential object deleted too + BEAST_EXPECT(!env.le(credIdx)); + auto const jv = + credentials::ledgerEntry(env, becky, carol, credType); + BEAST_EXPECT( + jv.isObject() && jv.isMember(jss::result) && + jv[jss::result].isMember(jss::error) && + jv[jss::result][jss::error] == "entryNotFound"); + } + } + } + void run() override { @@ -925,6 +1285,8 @@ class AccountDelete_test : public beast::unit_test::suite testBalanceTooSmallForFee(); testWithTickets(); testDest(); + testDestinationDepositAuthCredentials(); + testDeleteCredentialsOwner(); } }; diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 31b45abf43a..2c4f44ce79f 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -108,16 +108,6 @@ class Check_test : public beast::unit_test::suite return result; } - // Helper function that returns the owner count on an account. - static std::uint32_t - ownerCount(test::jtx::Env const& env, test::jtx::Account const& account) - { - std::uint32_t ret{0}; - if (auto const sleAccount = env.le(account)) - ret = sleAccount->getFieldU32(sfOwnerCount); - return ret; - } - // Helper function that verifies the expected DeliveredAmount is present. // // NOTE: the function _infers_ the transaction to operate on by calling diff --git a/src/test/app/Clawback_test.cpp b/src/test/app/Clawback_test.cpp index 8a42d4c38ef..c000433d2af 100644 --- a/src/test/app/Clawback_test.cpp +++ b/src/test/app/Clawback_test.cpp @@ -37,16 +37,6 @@ class Clawback_test : public beast::unit_test::suite return boost::lexical_cast(t); } - // Helper function that returns the owner count of an account root. - static 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; - } - // Helper function that returns the number of tickets held by an account. static std::uint32_t ticketCount(test::jtx::Env const& env, test::jtx::Account const& acct) diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp new file mode 100644 index 00000000000..e5d90d9766c --- /dev/null +++ b/src/test/app/Credentials_test.cpp @@ -0,0 +1,1079 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +namespace ripple { +namespace test { + +static inline bool +checkVL( + std::shared_ptr const& sle, + SField const& field, + std::string const& expected) +{ + return strHex(expected) == strHex(sle->getFieldVL(field)); +} + +static inline Keylet +credentialKeylet( + test::jtx::Account const& subject, + test::jtx::Account const& issuer, + std::string_view credType) +{ + return keylet::credential( + subject.id(), issuer.id(), Slice(credType.data(), credType.size())); +} + +struct Credentials_test : public beast::unit_test::suite +{ + void + testSuccessful(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + const char uri[] = "uri"; + + Account const issuer{"issuer"}; + Account const subject{"subject"}; + Account const other{"other"}; + + Env env{*this, features}; + + { + testcase("Create for subject."); + + auto const credKey = credentialKeylet(subject, issuer, credType); + + env.fund(XRP(5000), subject, issuer, other); + env.close(); + + // Test Create credentials + env(credentials::create(subject, issuer, credType), + credentials::uri(uri)); + env.close(); + { + auto const sleCred = env.le(credKey); + BEAST_EXPECT(static_cast(sleCred)); + if (!sleCred) + return; + + BEAST_EXPECT(sleCred->getAccountID(sfSubject) == subject.id()); + BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id()); + BEAST_EXPECT(!sleCred->getFieldU32(sfFlags)); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(!ownerCount(env, subject)); + BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + subject.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType))); + } + + env(credentials::accept(subject, issuer, credType)); + env.close(); + { + // check switching owner of the credentials from issuer to + // subject + auto const sleCred = env.le(credKey); + BEAST_EXPECT(static_cast(sleCred)); + if (!sleCred) + return; + + BEAST_EXPECT(sleCred->getAccountID(sfSubject) == subject.id()); + BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id()); + BEAST_EXPECT(!ownerCount(env, issuer)); + BEAST_EXPECT(ownerCount(env, subject) == 1); + BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + BEAST_EXPECT(sleCred->getFieldU32(sfFlags) == lsfAccepted); + } + + env(credentials::deleteCred(subject, subject, issuer, credType)); + env.close(); + { + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, issuer)); + BEAST_EXPECT(!ownerCount(env, subject)); + + // check no credential exists anymore + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + } + + { + testcase("Create for themself."); + + auto const credKey = credentialKeylet(issuer, issuer, credType); + + env(credentials::create(issuer, issuer, credType), + credentials::uri(uri)); + env.close(); + { + auto const sleCred = env.le(credKey); + BEAST_EXPECT(static_cast(sleCred)); + if (!sleCred) + return; + + BEAST_EXPECT(sleCred->getAccountID(sfSubject) == issuer.id()); + BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id()); + BEAST_EXPECT((sleCred->getFieldU32(sfFlags) & lsfAccepted)); + BEAST_EXPECT( + sleCred->getFieldU64(sfIssuerNode) == + sleCred->getFieldU64(sfSubjectNode)); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + auto const jle = + credentials::ledgerEntry(env, issuer, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + issuer.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType))); + } + + env(credentials::deleteCred(issuer, issuer, issuer, credType)); + env.close(); + { + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, issuer)); + + // check no credential exists anymore + auto const jle = + credentials::ledgerEntry(env, issuer, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + } + } + + void + testCredentialsDelete(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + + Account const issuer{"issuer"}; + Account const subject{"subject"}; + Account const other{"other"}; + + Env env{*this, features}; + + // fund subject and issuer + env.fund(XRP(5000), issuer, subject, other); + env.close(); + + { + testcase("Delete issuer before accept"); + + auto const credKey = credentialKeylet(subject, issuer, credType); + env(credentials::create(subject, issuer, credType)); + env.close(); + + // delete issuer + { + int const delta = env.seq(issuer) + 255; + for (int i = 0; i < delta; ++i) + env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(issuer, other), fee(acctDelFee)); + env.close(); + } + + // check credentials deleted too + { + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, subject)); + + // check no credential exists anymore + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + + // resurrection + env.fund(XRP(5000), issuer); + env.close(); + } + + { + testcase("Delete issuer after accept"); + + auto const credKey = credentialKeylet(subject, issuer, credType); + env(credentials::create(subject, issuer, credType)); + env.close(); + env(credentials::accept(subject, issuer, credType)); + env.close(); + + // delete issuer + { + int const delta = env.seq(issuer) + 255; + for (int i = 0; i < delta; ++i) + env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(issuer, other), fee(acctDelFee)); + env.close(); + } + + // check credentials deleted too + { + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, subject)); + + // check no credential exists anymore + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + + // resurrection + env.fund(XRP(5000), issuer); + env.close(); + } + + { + testcase("Delete subject before accept"); + + auto const credKey = credentialKeylet(subject, issuer, credType); + env(credentials::create(subject, issuer, credType)); + env.close(); + + // delete subject + { + int const delta = env.seq(subject) + 255; + for (int i = 0; i < delta; ++i) + env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(subject, other), fee(acctDelFee)); + env.close(); + } + + // check credentials deleted too + { + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, issuer)); + + // check no credential exists anymore + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + + // resurrection + env.fund(XRP(5000), subject); + env.close(); + } + + { + testcase("Delete subject after accept"); + + auto const credKey = credentialKeylet(subject, issuer, credType); + env(credentials::create(subject, issuer, credType)); + env.close(); + env(credentials::accept(subject, issuer, credType)); + env.close(); + + // delete subject + { + int const delta = env.seq(subject) + 255; + for (int i = 0; i < delta; ++i) + env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(subject, other), fee(acctDelFee)); + env.close(); + } + + // check credentials deleted too + { + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, issuer)); + + // check no credential exists anymore + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + + // resurrection + env.fund(XRP(5000), subject); + env.close(); + } + + { + testcase("Delete by other"); + + auto const credKey = credentialKeylet(subject, issuer, credType); + auto jv = credentials::create(subject, issuer, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + jv[sfExpiration.jsonName] = t + 20; + env(jv); + + // time advance + env.close(); + env.close(); + env.close(); + + // Other account delete credentials + env(credentials::deleteCred(other, subject, issuer, credType)); + env.close(); + + // check credentials object + { + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, issuer)); + BEAST_EXPECT(!ownerCount(env, subject)); + + // check no credential exists anymore + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + } + + { + testcase("Delete by subject"); + + env(credentials::create(subject, issuer, credType)); + env.close(); + + // Subject can delete + env(credentials::deleteCred(subject, subject, issuer, credType)); + env.close(); + { + auto const credKey = + credentialKeylet(subject, issuer, credType); + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, subject)); + BEAST_EXPECT(!ownerCount(env, issuer)); + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + } + + { + testcase("Delete by issuer"); + env(credentials::create(subject, issuer, credType)); + env.close(); + + env(credentials::deleteCred(issuer, subject, issuer, credType)); + env.close(); + { + auto const credKey = + credentialKeylet(subject, issuer, credType); + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(!ownerCount(env, subject)); + BEAST_EXPECT(!ownerCount(env, issuer)); + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error) && + jle[jss::result][jss::error] == "entryNotFound"); + } + } + } + + void + testCreateFailed(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + + Account const issuer{"issuer"}; + Account const subject{"subject"}; + + { + using namespace jtx; + Env env{*this, features}; + + env.fund(XRP(5000), subject, issuer); + env.close(); + + { + testcase("Credentials fail, no subject param."); + auto jv = credentials::create(subject, issuer, credType); + jv.removeMember(jss::Subject); + env(jv, ter(temMALFORMED)); + } + + { + auto jv = credentials::create(subject, issuer, credType); + jv[jss::Subject] = to_string(xrpAccount()); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail, no credentialType param."); + auto jv = credentials::create(subject, issuer, credType); + jv.removeMember(sfCredentialType.jsonName); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail, empty credentialType param."); + auto jv = credentials::create(subject, issuer, ""); + env(jv, ter(temMALFORMED)); + } + + { + testcase( + "Credentials fail, credentialType length > " + "maxCredentialTypeLength."); + constexpr std::string_view longCredType = + "abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]" + "asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p"; + static_assert(longCredType.size() > maxCredentialTypeLength); + auto jv = credentials::create(subject, issuer, longCredType); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail, URI length > 256."); + constexpr std::string_view longURI = + "abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]" + "asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p " + "9hfup;wDJFBVSD8f72 " + "pfhiusdovnbs;" + "djvbldafghwpEFHdjfaidfgio84763tfysgdvhjasbd " + "vujhgWQIE7F6WEUYFGWUKEYFVQW87FGWOEFWEFUYWVEF8723GFWEFB" + "WULE" + "fv28o37gfwEFB3872TFO8GSDSDVD"; + static_assert(longURI.size() > maxCredentialURILength); + env(credentials::create(subject, issuer, credType), + credentials::uri(longURI), + ter(temMALFORMED)); + } + + { + testcase("Credentials fail, URI empty."); + env(credentials::create(subject, issuer, credType), + credentials::uri(""), + ter(temMALFORMED)); + } + + { + testcase("Credentials fail, expiration in the past."); + auto jv = credentials::create(subject, issuer, credType); + // current time in ripple epoch - 1s + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() - + 1; + jv[sfExpiration.jsonName] = t; + env(jv, ter(tecEXPIRED)); + } + + { + testcase("Credentials fail, invalid fee."); + + auto jv = credentials::create(subject, issuer, credType); + jv[jss::Fee] = -1; + env(jv, ter(temBAD_FEE)); + } + + { + testcase("Credentials fail, duplicate."); + auto const jv = credentials::create(subject, issuer, credType); + env(jv); + env.close(); + env(jv, ter(tecDUPLICATE)); + env.close(); + + // check credential still present + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + subject.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType))); + } + } + + { + using namespace jtx; + Env env{*this, features}; + + env.fund(XRP(5000), issuer); + env.close(); + + { + testcase("Credentials fail, subject doesn't exist."); + auto const jv = credentials::create(subject, issuer, credType); + env(jv, ter(tecNO_TARGET)); + } + } + + { + using namespace jtx; + Env env{*this, features}; + + auto const reserve = drops(env.current()->fees().accountReserve(0)); + env.fund(reserve, subject, issuer); + env.close(); + + testcase("Credentials fail, not enough reserve."); + { + auto const jv = credentials::create(subject, issuer, credType); + env(jv, ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + } + } + + void + testAcceptFailed(FeatureBitset features) + { + using namespace jtx; + + const char credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const subject{"subject"}; + Account const other{"other"}; + + { + Env env{*this, features}; + + env.fund(XRP(5000), subject, issuer); + + { + testcase("CredentialsAccept fail, Credential doesn't exist."); + env(credentials::accept(subject, issuer, credType), + ter(tecNO_ENTRY)); + env.close(); + } + + { + testcase("CredentialsAccept fail, invalid Issuer account."); + auto jv = credentials::accept(subject, issuer, credType); + jv[jss::Issuer] = to_string(xrpAccount()); + env(jv, ter(temINVALID_ACCOUNT_ID)); + env.close(); + } + + { + testcase( + "CredentialsAccept fail, invalid credentialType param."); + auto jv = credentials::accept(subject, issuer, ""); + env(jv, ter(temMALFORMED)); + } + } + + { + Env env{*this, features}; + + env.fund(drops(env.current()->fees().accountReserve(1)), issuer); + env.fund(drops(env.current()->fees().accountReserve(0)), subject); + env.close(); + + { + testcase("CredentialsAccept fail, not enough reserve."); + env(credentials::create(subject, issuer, credType)); + env.close(); + + env(credentials::accept(subject, issuer, credType), + ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + // check credential still present + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + subject.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType))); + } + } + + { + using namespace jtx; + Env env{*this, features}; + + env.fund(XRP(5000), subject, issuer); + env.close(); + + { + env(credentials::create(subject, issuer, credType)); + env.close(); + + testcase("CredentialsAccept fail, invalid fee."); + auto jv = credentials::accept(subject, issuer, credType); + jv[jss::Fee] = -1; + env(jv, ter(temBAD_FEE)); + + testcase("CredentialsAccept fail, lsfAccepted already set."); + env(credentials::accept(subject, issuer, credType)); + env.close(); + env(credentials::accept(subject, issuer, credType), + ter(tecDUPLICATE)); + env.close(); + + // check credential still present + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + subject.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType))); + } + + { + const char credType2[] = "efghi"; + + testcase("CredentialsAccept fail, expired credentials."); + auto jv = credentials::create(subject, issuer, credType2); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // credentials are expired now + env(credentials::accept(subject, issuer, credType2), + ter(tecEXPIRED)); + env.close(); + + // check that expired credentials were deleted + auto const jDelCred = + credentials::ledgerEntry(env, subject, issuer, credType2); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error) && + jDelCred[jss::result][jss::error] == "entryNotFound"); + + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, subject) == 1); + } + } + + { + using namespace jtx; + Env env{*this, features}; + + env.fund(XRP(5000), issuer, subject, other); + env.close(); + + { + testcase("CredentialsAccept fail, issuer doesn't exist."); + auto jv = credentials::create(subject, issuer, credType); + env(jv); + env.close(); + + // delete issuer + int const delta = env.seq(issuer) + 255; + for (int i = 0; i < delta; ++i) + env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(issuer, other), fee(acctDelFee)); + + // can't accept - no issuer account + jv = credentials::accept(subject, issuer, credType); + env(jv, ter(tecNO_ISSUER)); + env.close(); + + // check that expired credentials were deleted + auto const jDelCred = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error) && + jDelCred[jss::result][jss::error] == "entryNotFound"); + } + } + } + + void + testDeleteFailed(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const subject{"subject"}; + Account const other{"other"}; + + { + using namespace jtx; + Env env{*this, features}; + + env.fund(XRP(5000), subject, issuer, other); + env.close(); + + { + testcase("CredentialsDelete fail, no Credentials."); + env(credentials::deleteCred(subject, subject, issuer, credType), + ter(tecNO_ENTRY)); + env.close(); + } + + { + testcase("CredentialsDelete fail, invalid Subject account."); + auto jv = + credentials::deleteCred(subject, subject, issuer, credType); + jv[jss::Subject] = to_string(xrpAccount()); + env(jv, ter(temINVALID_ACCOUNT_ID)); + env.close(); + } + + { + testcase("CredentialsDelete fail, invalid Issuer account."); + auto jv = + credentials::deleteCred(subject, subject, issuer, credType); + jv[jss::Issuer] = to_string(xrpAccount()); + env(jv, ter(temINVALID_ACCOUNT_ID)); + env.close(); + } + + { + testcase( + "CredentialsDelete fail, invalid credentialType param."); + auto jv = credentials::deleteCred(subject, subject, issuer, ""); + env(jv, ter(temMALFORMED)); + } + + { + const char credType2[] = "fghij"; + + env(credentials::create(subject, issuer, credType2)); + env.close(); + + // Other account can't delete credentials without expiration + env(credentials::deleteCred(other, subject, issuer, credType2), + ter(tecNO_PERMISSION)); + env.close(); + + // check credential still present + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType2); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + subject.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType2))); + } + + { + testcase("CredentialsDelete fail, time not expired yet."); + + auto jv = credentials::create(subject, issuer, credType); + // current time in ripple epoch + 1000s + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 1000; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // Other account can't delete credentials that not expired + env(credentials::deleteCred(other, subject, issuer, credType), + ter(tecNO_PERMISSION)); + env.close(); + + // check credential still present + auto const jle = + credentials::ledgerEntry(env, subject, issuer, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + subject.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType))); + } + + { + testcase("CredentialsDelete fail, no Issuer and Subject."); + + auto jv = + credentials::deleteCred(subject, subject, issuer, credType); + jv.removeMember(jss::Subject); + jv.removeMember(jss::Issuer); + env(jv, ter(temMALFORMED)); + env.close(); + } + + { + testcase("CredentialsDelete fail, invalid fee."); + + auto jv = + credentials::deleteCred(subject, subject, issuer, credType); + jv[jss::Fee] = -1; + env(jv, ter(temBAD_FEE)); + env.close(); + } + + { + testcase("deleteSLE fail, bad SLE."); + auto view = std::make_shared( + env.current().get(), ApplyFlags::tapNONE); + auto ter = + ripple::credentials::deleteSLE(*view, {}, env.journal); + BEAST_EXPECT(ter == tecNO_ENTRY); + } + } + } + + void + testFeatureFailed(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const subject{"subject"}; + + { + using namespace jtx; + Env env{*this, features}; + + env.fund(XRP(5000), subject, issuer); + env.close(); + + { + testcase("Credentials fail, Feature is not enabled."); + env(credentials::create(subject, issuer, credType), + ter(temDISABLED)); + env(credentials::accept(subject, issuer, credType), + ter(temDISABLED)); + env(credentials::deleteCred(subject, subject, issuer, credType), + ter(temDISABLED)); + } + } + } + + void + testRPC() + { + using namespace test::jtx; + + const char credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const subject{"subject"}; + + { + using namespace jtx; + Env env{*this}; + + env.fund(XRP(5000), subject, issuer); + env.close(); + + env(credentials::create(subject, issuer, credType)); + env.close(); + + env(credentials::accept(subject, issuer, credType)); + env.close(); + + testcase("account_tx"); + + std::string txHash0, txHash1; + { + Json::Value params; + params[jss::account] = subject.human(); + auto const jv = env.rpc( + "json", "account_tx", to_string(params))[jss::result]; + + BEAST_EXPECT(jv[jss::transactions].size() == 4); + auto const& tx0(jv[jss::transactions][0u][jss::tx]); + BEAST_EXPECT( + tx0[jss::TransactionType] == jss::CredentialAccept); + auto const& tx1(jv[jss::transactions][1u][jss::tx]); + BEAST_EXPECT( + tx1[jss::TransactionType] == jss::CredentialCreate); + txHash0 = tx0[jss::hash].asString(); + txHash1 = tx1[jss::hash].asString(); + } + + { + Json::Value params; + params[jss::account] = issuer.human(); + auto const jv = env.rpc( + "json", "account_tx", to_string(params))[jss::result]; + + BEAST_EXPECT(jv[jss::transactions].size() == 4); + auto const& tx0(jv[jss::transactions][0u][jss::tx]); + BEAST_EXPECT( + tx0[jss::TransactionType] == jss::CredentialAccept); + auto const& tx1(jv[jss::transactions][1u][jss::tx]); + BEAST_EXPECT( + tx1[jss::TransactionType] == jss::CredentialCreate); + + BEAST_EXPECT(txHash0 == tx0[jss::hash].asString()); + BEAST_EXPECT(txHash1 == tx1[jss::hash].asString()); + } + + testcase("account_objects"); + std::string objectIdx; + { + Json::Value params; + params[jss::account] = subject.human(); + auto jv = env.rpc( + "json", "account_objects", to_string(params))[jss::result]; + + BEAST_EXPECT(jv[jss::account_objects].size() == 1); + auto const& object(jv[jss::account_objects][0u]); + + BEAST_EXPECT( + object["LedgerEntryType"].asString() == jss::Credential); + objectIdx = object[jss::index].asString(); + } + + { + Json::Value params; + params[jss::account] = issuer.human(); + auto jv = env.rpc( + "json", "account_objects", to_string(params))[jss::result]; + + BEAST_EXPECT(jv[jss::account_objects].size() == 1); + auto const& object(jv[jss::account_objects][0u]); + + BEAST_EXPECT( + object["LedgerEntryType"].asString() == jss::Credential); + BEAST_EXPECT(objectIdx == object[jss::index].asString()); + } + } + } + + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testSuccessful(all); + testCredentialsDelete(all); + testCreateFailed(all); + testAcceptFailed(all); + testDeleteFailed(all); + testFeatureFailed(all - featureCredentials); + testRPC(); + } +}; + +BEAST_DEFINE_TESTSUITE(Credentials, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/DID_test.cpp b/src/test/app/DID_test.cpp index 20734518887..3f9cce1d33e 100644 --- a/src/test/app/DID_test.cpp +++ b/src/test/app/DID_test.cpp @@ -30,16 +30,6 @@ namespace ripple { namespace test { -// Helper function that returns the owner count of an account root. -static 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) { diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 9a11785b38c..0f2481a7c9e 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -20,6 +20,8 @@ #include #include +#include + namespace ripple { namespace test { @@ -381,6 +383,25 @@ struct DepositAuth_test : public beast::unit_test::suite } }; +static Json::Value +ledgerEntryDepositPreauth( + jtx::Env& env, + jtx::Account const& acc, + std::vector const& auth) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = acc.human(); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr(jvParams[jss::deposit_preauth][jss::authorized_credentials]); + for (auto const& o : auth) + { + arr.append(o.toLEJson()); + } + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + struct DepositPreauth_test : public beast::unit_test::suite { void @@ -634,6 +655,69 @@ struct DepositPreauth_test : public beast::unit_test::suite sendmax(XRP(10)), ter(expect)); env.close(); + + { + // becky setup depositpreauth with credentials + const char credType[] = "abcde"; + Account const carol{"carol"}; + env.fund(XRP(5000), carol); + + bool const supportsCredentials = features[featureCredentials]; + + TER const expectCredentials( + supportsCredentials ? TER(tesSUCCESS) : TER(temDISABLED)); + TER const expectPayment( + !supportsCredentials + ? TER(temDISABLED) + : (!supportsPreauth ? TER(tecNO_PERMISSION) + : TER(tesSUCCESS))); + TER const expectDP( + !supportsPreauth + ? TER(temDISABLED) + : (!supportsCredentials ? TER(temDISABLED) + : TER(tesSUCCESS))); + + env(deposit::authCredentials(becky, {{carol, credType}}), + ter(expectDP)); + env.close(); + + // gw accept credentials + env(credentials::create(gw, carol, credType), + ter(expectCredentials)); + env.close(); + env(credentials::accept(gw, carol, credType), + ter(expectCredentials)); + env.close(); + + auto jv = credentials::ledgerEntry(env, gw, carol, credType); + std::string const credIdx = supportsCredentials + ? jv[jss::result][jss::index].asString() + : "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6" + "EA288BE4"; + + env(pay(gw, becky, USD(100)), + credentials::ids({credIdx}), + ter(expectPayment)); + env.close(); + } + + { + using namespace std::chrono; + + if (!supportsPreauth) + { + auto const seq1 = env.seq(alice); + env(escrow(alice, becky, XRP(100)), + finish_time(env.now() + 1s)); + env.close(); + + // Failed as rule is disabled + env(finish(gw, alice, seq1), + fee(1500), + ter(tecNO_PERMISSION)); + env.close(); + } + } } if (supportsPreauth) @@ -724,14 +808,759 @@ struct DepositPreauth_test : public beast::unit_test::suite } } + void + testCredentialsPayment() + { + using namespace jtx; + + const char credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const maria{"maria"}; + Account const john{"john"}; + + { + testcase("Payment failed with disabled credentials rule."); + + Env env(*this, supported_amendments() - featureCredentials); + + env.fund(XRP(5000), issuer, bob, alice); + env.close(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + // Setup DepositPreauth object failed - amendent is not supported + env(deposit::authCredentials(bob, {{issuer, credType}}), + ter(temDISABLED)); + env.close(); + + // But can create old DepositPreauth + env(deposit::auth(bob, alice)); + env.close(); + + // And alice can't pay with any credentials, amendement is not + // enabled + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" + "01E034"; + env(pay(alice, bob, XRP(10)), + credentials::ids({invalidIdx}), + ter(temDISABLED)); + env.close(); + } + + { + testcase("Payment with credentials."); + + Env env(*this); + + env.fund(XRP(5000), issuer, alice, bob, john); + env.close(); + + // Issuer create credentials, but Alice didn't accept them yet + env(credentials::create(alice, issuer, credType)); + env.close(); + + // Get the index of the credentials + auto const jv = + credentials::ledgerEntry(env, alice, issuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + // Bob will accept payements from accounts with credentials signed + // by 'issuer' + env(deposit::authCredentials(bob, {{issuer, credType}})); + env.close(); + + auto const jDP = + ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); + BEAST_EXPECT( + jDP.isObject() && jDP.isMember(jss::result) && + !jDP[jss::result].isMember(jss::error) && + jDP[jss::result].isMember(jss::node) && + jDP[jss::result][jss::node].isMember("LedgerEntryType") && + jDP[jss::result][jss::node]["LedgerEntryType"] == + jss::DepositPreauth); + + // Alice can't pay - empty credentials array + { + auto jv = pay(alice, bob, XRP(100)); + jv[sfCredentialIDs.jsonName] = Json::arrayValue; + env(jv, ter(temMALFORMED)); + env.close(); + } + + // Alice can't pay - not accepted credentials + env(pay(alice, bob, XRP(100)), + credentials::ids({credIdx}), + ter(tecBAD_CREDENTIALS)); + env.close(); + + // Alice accept the credentials + env(credentials::accept(alice, issuer, credType)); + env.close(); + + // Now Alice can pay + env(pay(alice, bob, XRP(100)), credentials::ids({credIdx})); + env.close(); + + // Alice can pay Maria without depositPreauth enabled + env(pay(alice, maria, XRP(250)), credentials::ids({credIdx})); + env.close(); + + // john can accept payment with old depositPreauth and valid + // credentials + env(fset(john, asfDepositAuth)); + env(deposit::auth(john, alice)); + env(pay(alice, john, XRP(100)), credentials::ids({credIdx})); + env.close(); + } + + { + testcase("Payment failed with invalid credentials."); + + Env env(*this); + + env.fund(XRP(10000), issuer, alice, bob, maria); + env.close(); + + // Issuer create credentials, but Alice didn't accept them yet + env(credentials::create(alice, issuer, credType)); + env.close(); + // Alice accept the credentials + env(credentials::accept(alice, issuer, credType)); + env.close(); + // Get the index of the credentials + auto const jv = + credentials::ledgerEntry(env, alice, issuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + { + // Success as destination didn't enable preauthorization so + // valid credentials will not fail + env(pay(alice, bob, XRP(100)), credentials::ids({credIdx})); + } + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + { + // Fail as destination didn't setup DepositPreauth object + env(pay(alice, bob, XRP(100)), + credentials::ids({credIdx}), + ter(tecNO_PERMISSION)); + } + + // Bob setup DepositPreauth object, duplicates is not allowed + env(deposit::authCredentials( + bob, {{issuer, credType}, {issuer, credType}}), + ter(temMALFORMED)); + + // Bob setup DepositPreauth object + env(deposit::authCredentials(bob, {{issuer, credType}})); + env.close(); + + { + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" + "01E034"; + // Alice can't pay with non-existing credentials + env(pay(alice, bob, XRP(100)), + credentials::ids({invalidIdx}), + ter(tecBAD_CREDENTIALS)); + } + + { // maria can't pay using valid credentials but issued for + // different account + env(pay(maria, bob, XRP(100)), + credentials::ids({credIdx}), + ter(tecBAD_CREDENTIALS)); + } + + { + // create another valid credential + const char credType2[] = "fghij"; + env(credentials::create(alice, issuer, credType2)); + env.close(); + env(credentials::accept(alice, issuer, credType2)); + env.close(); + auto const jv = + credentials::ledgerEntry(env, alice, issuer, credType2); + std::string const credIdx2 = + jv[jss::result][jss::index].asString(); + + // Alice can't pay with invalid set of valid credentials + env(pay(alice, bob, XRP(100)), + credentials::ids({credIdx, credIdx2}), + ter(tecNO_PERMISSION)); + } + + // Error, duplicate credentials + env(pay(alice, bob, XRP(100)), + credentials::ids({credIdx, credIdx}), + ter(temMALFORMED)); + + // Alice can pay + env(pay(alice, bob, XRP(100)), credentials::ids({credIdx})); + env.close(); + } + } + + void + testCredentialsCreation() + { + using namespace jtx; + + const char credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const maria{"maria"}; + + { + testcase("Creating / deleting with credentials."); + + Env env(*this); + + env.fund(XRP(5000), issuer, alice, bob); + env.close(); + + { + // both included [AuthorizeCredentials UnauthorizeCredentials] + auto jv = deposit::authCredentials(bob, {{issuer, credType}}); + jv[sfUnauthorizeCredentials.jsonName] = Json::arrayValue; + env(jv, ter(temMALFORMED)); + } + + { + // both included [Unauthorize, AuthorizeCredentials] + auto jv = deposit::authCredentials(bob, {{issuer, credType}}); + jv[sfUnauthorize.jsonName] = issuer.human(); + env(jv, ter(temMALFORMED)); + } + + { + // both included [Authorize, AuthorizeCredentials] + auto jv = deposit::authCredentials(bob, {{issuer, credType}}); + jv[sfAuthorize.jsonName] = issuer.human(); + env(jv, ter(temMALFORMED)); + } + + { + // both included [Unauthorize, UnauthorizeCredentials] + auto jv = deposit::unauthCredentials(bob, {{issuer, credType}}); + jv[sfUnauthorize.jsonName] = issuer.human(); + env(jv, ter(temMALFORMED)); + } + + { + // both included [Authorize, UnauthorizeCredentials] + auto jv = deposit::unauthCredentials(bob, {{issuer, credType}}); + jv[sfAuthorize.jsonName] = issuer.human(); + env(jv, ter(temMALFORMED)); + } + + { + // AuthorizeCredentials is empty + auto jv = deposit::authCredentials(bob, {}); + env(jv, ter(temMALFORMED)); + } + + { + // invalid issuer + auto jv = deposit::authCredentials(bob, {}); + auto& arr(jv[sfAuthorizeCredentials.jsonName]); + Json::Value cred = Json::objectValue; + cred[jss::Issuer] = to_string(xrpAccount()); + cred[sfCredentialType.jsonName] = + strHex(std::string_view(credType)); + Json::Value credParent; + credParent[jss::Credential] = cred; + arr.append(std::move(credParent)); + + env(jv, ter(temINVALID_ACCOUNT_ID)); + } + + { + // empty credential type + auto jv = deposit::authCredentials(bob, {{issuer, {}}}); + env(jv, ter(temMALFORMED)); + } + + { + // AuthorizeCredentials is larger than 8 elements + Account const a("a"), b("b"), c("c"), d("d"), e("e"), f("f"), + g("g"), h("h"), i("i"); + auto const& z = credType; + auto jv = deposit::authCredentials( + bob, + {{a, z}, + {b, z}, + {c, z}, + {d, z}, + {e, z}, + {f, z}, + {g, z}, + {h, z}, + {i, z}}); + env(jv, ter(temMALFORMED)); + } + + { + // Can't create with non-existing issuer + Account const rick{"rick"}; + auto jv = deposit::authCredentials(bob, {{rick, credType}}); + env(jv, ter(tecNO_ISSUER)); + env.close(); + } + + { + // not enough reserve + Account const john{"john"}; + env.fund(env.current()->fees().accountReserve(0), john); + auto jv = deposit::authCredentials(john, {{issuer, credType}}); + env(jv, ter(tecINSUFFICIENT_RESERVE)); + } + + { + // NO deposit object exists + env(deposit::unauthCredentials(bob, {{issuer, credType}}), + ter(tecNO_ENTRY)); + } + + // Create DepositPreauth object + { + env(deposit::authCredentials(bob, {{issuer, credType}})); + env.close(); + + auto const jDP = + ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); + BEAST_EXPECT( + jDP.isObject() && jDP.isMember(jss::result) && + !jDP[jss::result].isMember(jss::error) && + jDP[jss::result].isMember(jss::node) && + jDP[jss::result][jss::node].isMember("LedgerEntryType") && + jDP[jss::result][jss::node]["LedgerEntryType"] == + jss::DepositPreauth); + + // Check object fields + BEAST_EXPECT( + jDP[jss::result][jss::node][jss::Account] == bob.human()); + auto const& credentials( + jDP[jss::result][jss::node]["AuthorizeCredentials"]); + BEAST_EXPECT(credentials.isArray() && credentials.size() == 1); + for (auto const& o : credentials) + { + auto const& c(o[jss::Credential]); + BEAST_EXPECT(c[jss::Issuer].asString() == issuer.human()); + BEAST_EXPECT( + c["CredentialType"].asString() == + strHex(std::string_view(credType))); + } + + // can't create duplicate + env(deposit::authCredentials(bob, {{issuer, credType}}), + ter(tecDUPLICATE)); + } + + // Delete DepositPreauth object + { + env(deposit::unauthCredentials(bob, {{issuer, credType}})); + env.close(); + auto const jDP = + ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); + BEAST_EXPECT( + jDP.isObject() && jDP.isMember(jss::result) && + jDP[jss::result].isMember(jss::error) && + jDP[jss::result][jss::error] == "entryNotFound"); + } + } + } + + void + testExpiredCreds() + { + using namespace jtx; + const char credType[] = "abcde"; + const char credType2[] = "fghijkl"; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + IOU const USD = gw["USD"]; + Account const zelda{"zelda"}; + + { + testcase("Payment failed with expired credentials."); + + Env env(*this); + + env.fund(XRP(10000), issuer, alice, bob, gw); + env.close(); + + // Create credentials + auto jv = credentials::create(alice, issuer, credType); + // Current time in ripple epoch. + // Every time ledger close, unittest timer increase by 10s + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 60; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // Alice accept the credentials + env(credentials::accept(alice, issuer, credType)); + env.close(); + + // Create credential which not expired + jv = credentials::create(alice, issuer, credType2); + uint32_t const t2 = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 1000; + jv[sfExpiration.jsonName] = t2; + env(jv); + env.close(); + env(credentials::accept(alice, issuer, credType2)); + env.close(); + + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, alice) == 2); + + // Get the index of the credentials + jv = credentials::ledgerEntry(env, alice, issuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + jv = credentials::ledgerEntry(env, alice, issuer, credType2); + std::string const credIdx2 = jv[jss::result][jss::index].asString(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + // Bob setup DepositPreauth object + env(deposit::authCredentials( + bob, {{issuer, credType}, {issuer, credType2}})); + env.close(); + + { + // Alice can pay + env(pay(alice, bob, XRP(100)), + credentials::ids({credIdx, credIdx2})); + env.close(); + env.close(); + + // Ledger closed, time increased, alice can't pay anymore + env(pay(alice, bob, XRP(100)), + credentials::ids({credIdx, credIdx2}), + ter(tecEXPIRED)); + env.close(); + + { + // check that expired credentials were deleted + auto const jDelCred = + credentials::ledgerEntry(env, alice, issuer, credType); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error) && + jDelCred[jss::result][jss::error] == "entryNotFound"); + } + + { + // check that non-expired credential still present + auto const jle = + credentials::ledgerEntry(env, alice, issuer, credType2); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember( + "LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential && + jle[jss::result][jss::node][jss::Issuer] == + issuer.human() && + jle[jss::result][jss::node][jss::Subject] == + alice.human() && + jle[jss::result][jss::node]["CredentialType"] == + strHex(std::string_view(credType2))); + } + + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, alice) == 1); + } + + { + auto jv = credentials::create(gw, issuer, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 40; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + env(credentials::accept(gw, issuer, credType)); + env.close(); + + jv = credentials::ledgerEntry(env, gw, issuer, credType); + std::string const credIdx = + jv[jss::result][jss::index].asString(); + + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, gw) == 1); + + env.close(); + env.close(); + env.close(); + + // credentials are expired + env(pay(gw, bob, USD(150)), + credentials::ids({credIdx}), + ter(tecEXPIRED)); + env.close(); + + // check that expired credentials were deleted + auto const jDelCred = + credentials::ledgerEntry(env, gw, issuer, credType); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error) && + jDelCred[jss::result][jss::error] == "entryNotFound"); + + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, gw) == 0); + } + } + + { + using namespace std::chrono; + + testcase("Escrow failed with expired credentials."); + + Env env(*this); + + env.fund(XRP(5000), issuer, alice, bob, zelda); + env.close(); + + // Create credentials + auto jv = credentials::create(zelda, issuer, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 50; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // Zelda accept the credentials + env(credentials::accept(zelda, issuer, credType)); + env.close(); + + // Get the index of the credentials + jv = credentials::ledgerEntry(env, zelda, issuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + // Bob setup DepositPreauth object + env(deposit::authCredentials(bob, {{issuer, credType}})); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env.close(); + + // zelda can't finish escrow with invalid credentials + { + env(finish(zelda, alice, seq), + credentials::ids({}), + ter(temMALFORMED)); + env.close(); + } + + { + // zelda can't finish escrow with invalid credentials + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" + "01E034"; + + env(finish(zelda, alice, seq), + credentials::ids({invalidIdx}), + ter(tecBAD_CREDENTIALS)); + env.close(); + } + + { // Ledger closed, time increased, zelda can't finish escrow + env(finish(zelda, alice, seq), + credentials::ids({credIdx}), + fee(1500), + ter(tecEXPIRED)); + env.close(); + } + + // check that expired credentials were deleted + auto const jDelCred = + credentials::ledgerEntry(env, zelda, issuer, credType); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error) && + jDelCred[jss::result][jss::error] == "entryNotFound"); + } + } + + void + testSortingCredentials() + { + using namespace jtx; + + Account const stock{"stock"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + Env env(*this); + + testcase("Sorting credentials."); + + env.fund(XRP(5000), stock, alice, bob); + + std::vector credentials = { + {"a", "a"}, + {"b", "b"}, + {"c", "c"}, + {"d", "d"}, + {"e", "e"}, + {"f", "f"}, + {"g", "g"}, + {"h", "h"}}; + + for (auto const& c : credentials) + env.fund(XRP(5000), c.issuer); + env.close(); + + std::random_device rd; + std::mt19937 gen(rd()); + + { + std::unordered_map pubKey2Acc; + for (auto const& c : credentials) + pubKey2Acc.emplace(c.issuer.human(), c.issuer); + + // check sorting in object + for (int i = 0; i < 10; ++i) + { + std::ranges::shuffle(credentials, gen); + env(deposit::authCredentials(stock, credentials)); + env.close(); + + auto const dp = + ledgerEntryDepositPreauth(env, stock, credentials); + auto const& authCred( + dp[jss::result][jss::node]["AuthorizeCredentials"]); + BEAST_EXPECT( + authCred.isArray() && + authCred.size() == credentials.size()); + std::vector> readedCreds; + for (auto const& o : authCred) + { + auto const& c(o[jss::Credential]); + auto issuer = c[jss::Issuer].asString(); + + if (BEAST_EXPECT(pubKey2Acc.contains(issuer))) + readedCreds.emplace_back( + pubKey2Acc.at(issuer), + c["CredentialType"].asString()); + } + + BEAST_EXPECT(std::ranges::is_sorted(readedCreds)); + + env(deposit::unauthCredentials(stock, credentials)); + env.close(); + } + } + + { + std::ranges::shuffle(credentials, gen); + env(deposit::authCredentials(stock, credentials)); + env.close(); + + // check sorting in params + for (int i = 0; i < 10; ++i) + { + std::ranges::shuffle(credentials, gen); + env(deposit::authCredentials(stock, credentials), + ter(tecDUPLICATE)); + } + } + + testcase("Check duplicate credentials."); + { + // check duplicates in depositPreauth params + std::ranges::shuffle(credentials, gen); + for (auto const& c : credentials) + { + auto credentials2 = credentials; + credentials2.push_back(c); + + env(deposit::authCredentials(stock, credentials2), + ter(temMALFORMED)); + } + + // create batch of credentials and save their hashes + std::vector credentialIDs; + for (auto const& c : credentials) + { + env(credentials::create(alice, c.issuer, c.credType)); + env.close(); + env(credentials::accept(alice, c.issuer, c.credType)); + env.close(); + + credentialIDs.push_back(credentials::ledgerEntry( + env, + alice, + c.issuer, + c.credType)[jss::result][jss::index] + .asString()); + } + + // check duplicates in payment params + for (auto const& h : credentialIDs) + { + auto credentialIDs2 = credentialIDs; + credentialIDs2.push_back(h); + + env(pay(alice, bob, XRP(100)), + credentials::ids(credentialIDs2), + ter(temMALFORMED)); + } + } + } + void run() override { testEnable(); testInvalid(); auto const supported{jtx::supported_amendments()}; + testPayment(supported - featureDepositPreauth - featureCredentials); testPayment(supported - featureDepositPreauth); + testPayment(supported - featureCredentials); testPayment(supported); + testCredentialsPayment(); + testCredentialsCreation(); + testExpiredCreds(); + testSortingCredentials(); } }; diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 0f465a14f4d..714fc7734d9 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -1508,6 +1508,154 @@ struct Escrow_test : public beast::unit_test::suite } } + void + testCredentials() + { + testcase("Test with credentials"); + + using namespace jtx; + using namespace std::chrono; + + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + Account const dillon{"dillon "}; + Account const zelda{"zelda"}; + + const char credType[] = "abcde"; + + { + // Credentials amendment not enabled + Env env(*this, supported_amendments() - featureCredentials); + env.fund(XRP(5000), alice, bob); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env.close(); + + env(fset(bob, asfDepositAuth)); + env.close(); + env(deposit::auth(bob, alice)); + env.close(); + + std::string const credIdx = + "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4"; + env(finish(bob, alice, seq), + credentials::ids({credIdx}), + ter(temDISABLED)); + } + + { + Env env(*this); + + env.fund(XRP(5000), alice, bob, carol, dillon, zelda); + env.close(); + + env(credentials::create(carol, zelda, credType)); + env.close(); + auto const jv = + credentials::ledgerEntry(env, carol, zelda, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + env.close(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + // Fail, credentials not accepted + env(finish(carol, alice, seq), + credentials::ids({credIdx}), + ter(tecBAD_CREDENTIALS)); + + env.close(); + + env(credentials::accept(carol, zelda, credType)); + env.close(); + + // Fail, credentials doesn’t belong to root account + env(finish(dillon, alice, seq), + credentials::ids({credIdx}), + ter(tecBAD_CREDENTIALS)); + + // Fail, no depositPreauth + env(finish(carol, alice, seq), + credentials::ids({credIdx}), + ter(tecNO_PERMISSION)); + + env(deposit::authCredentials(bob, {{zelda, credType}})); + env.close(); + + // Success + env.close(); + env(finish(carol, alice, seq), credentials::ids({credIdx})); + env.close(); + } + + { + testcase("Escrow with credentials without depositPreauth"); + using namespace std::chrono; + + Env env(*this); + + env.fund(XRP(5000), alice, bob, carol, dillon, zelda); + env.close(); + + env(credentials::create(carol, zelda, credType)); + env.close(); + env(credentials::accept(carol, zelda, credType)); + env.close(); + auto const jv = + credentials::ledgerEntry(env, carol, zelda, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + // time advance + env.close(); + env.close(); + env.close(); + env.close(); + env.close(); + env.close(); + + // Succeed, Bob doesn't require preauthorization + env(finish(carol, alice, seq), credentials::ids({credIdx})); + env.close(); + + { + const char credType2[] = "fghijk"; + + env(credentials::create(bob, zelda, credType2)); + env.close(); + env(credentials::accept(bob, zelda, credType2)); + env.close(); + auto const credIdxBob = + credentials::ledgerEntry( + env, bob, zelda, credType2)[jss::result][jss::index] + .asString(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env.close(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + env(deposit::authCredentials(bob, {{zelda, credType}})); + env.close(); + + // Use any valid credentials if account == dst + env(finish(bob, alice, seq), credentials::ids({credIdxBob})); + env.close(); + } + } + } + void run() override { @@ -1522,6 +1670,7 @@ struct Escrow_test : public beast::unit_test::suite testMetaAndOwnership(); testConsequences(); testEscrowWithTickets(); + testCredentials(); } }; diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp index dea6d4569e0..f8db4df4f92 100644 --- a/src/test/app/FixNFTokenPageLinks_test.cpp +++ b/src/test/app/FixNFTokenPageLinks_test.cpp @@ -27,16 +27,6 @@ namespace ripple { class FixNFTokenPageLinks_test : public beast::unit_test::suite { - // Helper function that returns the owner count of an account root. - static 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; - } - // Helper function that returns the number of nfts owned by an account. static std::uint32_t nftCount(test::jtx::Env& env, test::jtx::Account const& acct) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 69c5d90111c..796a3f14c88 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1316,6 +1316,157 @@ class MPToken_test : public beast::unit_test::suite } } + void + testDepositPreauth() + { + testcase("DepositPreauth"); + + using namespace test::jtx; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const diana("diana"); + Account const dpIssuer("dpIssuer"); // holder + + const char credType[] = "abcde"; + + { + Env env(*this); + + env.fund(XRP(50000), diana, dpIssuer); + env.close(); + + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTRequireAuth | tfMPTCanTransfer}); + + env(pay(diana, bob, XRP(500))); + env.close(); + + // bob creates an empty MPToken + mptAlice.authorize({.account = bob}); + // alice authorizes bob to hold funds + mptAlice.authorize({.account = alice, .holder = bob}); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + // alice try to send 100 MPT to bob, not authorized + mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); + env.close(); + + // Bob authorize alice + env(deposit::auth(bob, alice)); + env.close(); + + // alice sends 100 MPT to bob + mptAlice.pay(alice, bob, 100); + env.close(); + + // Create credentials + env(credentials::create(alice, dpIssuer, credType)); + env.close(); + env(credentials::accept(alice, dpIssuer, credType)); + env.close(); + auto const jv = + credentials::ledgerEntry(env, alice, dpIssuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // alice sends 100 MPT to bob with credentials which aren't required + mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}}); + env.close(); + + // Bob revoke authorization + env(deposit::unauth(bob, alice)); + env.close(); + + // alice try to send 100 MPT to bob, not authorized + mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); + env.close(); + + // alice sends 100 MPT to bob with credentials, not authorized + mptAlice.pay(alice, bob, 100, tecNO_PERMISSION, {{credIdx}}); + env.close(); + + // Bob authorize credentials + env(deposit::authCredentials(bob, {{dpIssuer, credType}})); + env.close(); + + // alice try to send 100 MPT to bob, not authorized + mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); + env.close(); + + // alice sends 100 MPT to bob with credentials + mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}}); + env.close(); + } + + testcase("DepositPreauth disabled featureCredentials"); + { + Env env(*this, supported_amendments() - featureCredentials); + + std::string const credIdx = + "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6" + "E2"; + + env.fund(XRP(50000), diana, dpIssuer); + env.close(); + + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTRequireAuth | tfMPTCanTransfer}); + + env(pay(diana, bob, XRP(500))); + env.close(); + + // bob creates an empty MPToken + mptAlice.authorize({.account = bob}); + // alice authorizes bob to hold funds + mptAlice.authorize({.account = alice, .holder = bob}); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + // alice try to send 100 MPT to bob, not authorized + mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); + env.close(); + + // alice try to send 100 MPT to bob with credentials, amendment + // disabled + mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}}); + env.close(); + + // Bob authorize alice + env(deposit::auth(bob, alice)); + env.close(); + + // alice sends 100 MPT to bob + mptAlice.pay(alice, bob, 100); + env.close(); + + // alice sends 100 MPT to bob with credentials, amendment disabled + mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}}); + env.close(); + + // Bob revoke authorization + env(deposit::unauth(bob, alice)); + env.close(); + + // alice try to send 100 MPT to bob + mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); + env.close(); + + // alice sends 100 MPT to bob with credentials, amendment disabled + mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}}); + env.close(); + } + } + void testMPTInvalidInTx(FeatureBitset features) { @@ -2105,6 +2256,7 @@ class MPToken_test : public beast::unit_test::suite // Test Direct Payment testPayment(all); + testDepositPreauth(); // Test MPT Amount is invalid in Tx, which don't support MPT testMPTInvalidInTx(all); diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index a84ac63da9d..a56f0a45674 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -28,16 +28,6 @@ namespace ripple { class NFTokenBurnBaseUtil_test : public beast::unit_test::suite { - // Helper function that returns the owner count of an account root. - static 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; - } - // Helper function that returns the number of nfts owned by an account. static std::uint32_t nftCount(test::jtx::Env& env, test::jtx::Account const& acct) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 9c0e09d6711..0d4786ae72e 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -31,16 +31,6 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite { FeatureBitset const disallowIncoming{featureDisallowIncoming}; - // Helper function that returns the owner count of an account root. - static 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; - } - // Helper function that returns the number of NFTs minted by an issuer. static std::uint32_t mintedCount(test::jtx::Env const& env, test::jtx::Account const& issuer) @@ -3948,7 +3938,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite for (Account const& acct : accounts) { if (std::uint32_t ownerCount = - this->ownerCount(env, acct); + test::jtx::ownerCount(env, acct); ownerCount != 1) { std::stringstream ss; diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index c2f3c271265..44eeb1c9f98 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -28,16 +28,6 @@ namespace oracle { struct Oracle_test : public beast::unit_test::suite { private: - // Helper function that returns the owner count of an account root. - static std::uint32_t - ownerCount(jtx::Env const& env, jtx::Account const& acct) - { - std::uint32_t ret{0}; - if (auto const sleAcct = env.le(acct)) - ret = sleAcct->at(sfOwnerCount); - return ret; - } - void testInvalidSet() { diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index e49e5cbd6dc..bc1cbba69c0 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -832,6 +832,190 @@ struct PayChan_test : public beast::unit_test::suite } } + void + testDepositAuthCreds() + { + testcase("Deposit Authorization with Credentials"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + const char credType[] = "abcde"; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dillon("dillon"); + Account const zelda("zelda"); + + { + Env env{*this}; + env.fund(XRP(10000), alice, bob, carol, dillon, zelda); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, XRP(1000), settleDelay, pk)); + env.close(); + + // alice add funds to the channel + env(fund(alice, chan, XRP(1000))); + env.close(); + + std::string const credBadIdx = + "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6" + "E1"; + + auto const delta = XRP(500).value(); + + { // create credentials + auto jv = credentials::create(alice, carol, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 100; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + } + + auto const jv = + credentials::ledgerEntry(env, alice, carol, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + // Fail, credentials not accepted + env(claim(alice, chan, delta, delta), + credentials::ids({credIdx}), + ter(tecBAD_CREDENTIALS)); + env.close(); + + env(credentials::accept(alice, carol, credType)); + env.close(); + + // Fail, no depositPreauth object + env(claim(alice, chan, delta, delta), + credentials::ids({credIdx}), + ter(tecNO_PERMISSION)); + env.close(); + + // Setup deposit authorization + env(deposit::authCredentials(bob, {{carol, credType}})); + env.close(); + + // Fail, credentials doesn’t belong to root account + env(claim(dillon, chan, delta, delta), + credentials::ids({credIdx}), + ter(tecBAD_CREDENTIALS)); + + // Fails because bob's lsfDepositAuth flag is set. + env(claim(alice, chan, delta, delta), ter(tecNO_PERMISSION)); + + // Fail, bad credentials index. + env(claim(alice, chan, delta, delta), + credentials::ids({credBadIdx}), + ter(tecBAD_CREDENTIALS)); + + // Fail, empty credentials + env(claim(alice, chan, delta, delta), + credentials::ids({}), + ter(temMALFORMED)); + + { + // claim fails cause of expired credentials + + // Every cycle +10sec. + for (int i = 0; i < 10; ++i) + env.close(); + + env(claim(alice, chan, delta, delta), + credentials::ids({credIdx}), + ter(tecEXPIRED)); + env.close(); + } + + { // create credentials once more + env(credentials::create(alice, carol, credType)); + env.close(); + env(credentials::accept(alice, carol, credType)); + env.close(); + + auto const jv = + credentials::ledgerEntry(env, alice, carol, credType); + std::string const credIdx = + jv[jss::result][jss::index].asString(); + + // Success + env(claim(alice, chan, delta, delta), + credentials::ids({credIdx})); + } + } + + { + Env env{*this}; + env.fund(XRP(10000), alice, bob, carol, dillon, zelda); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, XRP(1000), settleDelay, pk)); + env.close(); + + // alice add funds to the channel + env(fund(alice, chan, XRP(1000))); + env.close(); + + auto const delta = XRP(500).value(); + + { // create credentials + env(credentials::create(alice, carol, credType)); + env.close(); + env(credentials::accept(alice, carol, credType)); + env.close(); + } + + auto const jv = + credentials::ledgerEntry(env, alice, carol, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // Succeed, lsfDepositAuth is not set + env(claim(alice, chan, delta, delta), credentials::ids({credIdx})); + env.close(); + } + + { + // Credentials amendment not enabled + Env env(*this, supported_amendments() - featureCredentials); + env.fund(XRP(5000), "alice", "bob"); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, XRP(1000), settleDelay, pk)); + env.close(); + + env(fund(alice, chan, XRP(1000))); + env.close(); + std::string const credIdx = + "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4"; + + // can't claim with old DepositPreauth because rule is not enabled. + env(fset(bob, asfDepositAuth)); + env.close(); + env(deposit::auth(bob, alice)); + env.close(); + + env(claim(alice, chan, XRP(500).value(), XRP(500).value()), + credentials::ids({credIdx}), + ter(temDISABLED)); + } + } + void testMultiple(FeatureBitset features) { @@ -2116,6 +2300,7 @@ struct PayChan_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; testWithFeats(all - disallowIncoming); testWithFeats(all); + testDepositAuthCreds(); } }; diff --git a/src/test/jtx.h b/src/test/jtx.h index 49790e34022..b7b9a9fa05c 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 7165bc26970..d81551aa840 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -96,6 +96,10 @@ getAccountLines(Env& env, AccountID const& acctId, IOU... ious) [[nodiscard]] bool checkArraySize(Json::Value const& val, unsigned int size); +// Helper function that returns the owner count on an account. +std::uint32_t +ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); + /* Path finding */ /******************************************************************************/ void diff --git a/src/test/jtx/credentials.h b/src/test/jtx/credentials.h new file mode 100644 index 00000000000..2f5c63dccb8 --- /dev/null +++ b/src/test/jtx/credentials.h @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace credentials { + +// Sets the optional URI. +class uri +{ +private: + std::string const uri_; + +public: + explicit uri(std::string_view u) : uri_(strHex(u)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + jtx.jv[sfURI.jsonName] = uri_; + } +}; + +// Set credentialsIDs array +class ids +{ +private: + std::vector const credentials_; + +public: + explicit ids(std::vector const& creds) : credentials_(creds) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + auto& arr(jtx.jv[sfCredentialIDs.jsonName] = Json::arrayValue); + for (auto const& hash : credentials_) + arr.append(hash); + } +}; + +Json::Value +create( + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType); + +Json::Value +accept( + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType); + +Json::Value +deleteCred( + jtx::Account const& acc, + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType); + +Json::Value +ledgerEntry( + jtx::Env& env, + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType); + +Json::Value +ledgerEntry(jtx::Env& env, std::string const& credIdx); + +} // namespace credentials + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h index 720254e7eae..9de3416367c 100644 --- a/src/test/jtx/deposit.h +++ b/src/test/jtx/deposit.h @@ -38,6 +38,41 @@ auth(Account const& account, Account const& auth); Json::Value unauth(Account const& account, Account const& unauth); +struct AuthorizeCredentials +{ + jtx::Account issuer; + std::string credType; + + Json::Value + toJson() const + { + Json::Value jv; + jv[jss::Issuer] = issuer.human(); + jv[sfCredentialType.jsonName] = strHex(credType); + return jv; + } + + // "ledger_entry" uses a different naming convention + Json::Value + toLEJson() const + { + Json::Value jv; + jv[jss::issuer] = issuer.human(); + jv[jss::credential_type] = strHex(credType); + return jv; + } +}; + +Json::Value +authCredentials( + jtx::Account const& account, + std::vector const& auth); + +Json::Value +unauthCredentials( + jtx::Account const& account, + std::vector const& auth); + } // namespace deposit } // namespace jtx diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index b8105b1a631..b39cac7dcc1 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -50,6 +50,15 @@ checkArraySize(Json::Value const& val, unsigned int size) return val.isArray() && val.size() == size; } +std::uint32_t +ownerCount(Env const& env, Account const& account) +{ + std::uint32_t ret{0}; + if (auto const sleAccount = env.le(account)) + ret = sleAccount->getFieldU32(sfOwnerCount); + return ret; +} + /* Path finding */ /******************************************************************************/ void @@ -385,4 +394,4 @@ allpe(AccountID const& a, Issue const& iss) } // namespace jtx } // namespace test -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/test/jtx/impl/credentials.cpp b/src/test/jtx/impl/credentials.cpp new file mode 100644 index 00000000000..bc7ccf93cd4 --- /dev/null +++ b/src/test/jtx/impl/credentials.cpp @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +namespace test { +namespace jtx { + +namespace credentials { + +Json::Value +create( + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::CredentialCreate; + + jv[jss::Account] = issuer.human(); + jv[jss::Subject] = subject.human(); + + jv[jss::Flags] = tfUniversal; + jv[sfCredentialType.jsonName] = strHex(credType); + + return jv; +} + +Json::Value +accept( + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::CredentialAccept; + jv[jss::Account] = subject.human(); + jv[jss::Issuer] = issuer.human(); + jv[sfCredentialType.jsonName] = strHex(credType); + jv[jss::Flags] = tfUniversal; + + return jv; +} + +Json::Value +deleteCred( + jtx::Account const& acc, + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::CredentialDelete; + jv[jss::Account] = acc.human(); + jv[jss::Subject] = subject.human(); + jv[jss::Issuer] = issuer.human(); + jv[sfCredentialType.jsonName] = strHex(credType); + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +ledgerEntry( + jtx::Env& env, + jtx::Account const& subject, + jtx::Account const& issuer, + std::string_view credType) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::credential][jss::subject] = subject.human(); + jvParams[jss::credential][jss::issuer] = issuer.human(); + jvParams[jss::credential][jss::credential_type] = strHex(credType); + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +Json::Value +ledgerEntry(jtx::Env& env, std::string const& credIdx) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::credential] = credIdx; + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +} // namespace credentials + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/deposit.cpp b/src/test/jtx/impl/deposit.cpp index 09f0cb704b0..d91607c9906 100644 --- a/src/test/jtx/impl/deposit.cpp +++ b/src/test/jtx/impl/deposit.cpp @@ -48,6 +48,46 @@ unauth(jtx::Account const& account, jtx::Account const& unauth) return jv; } +// Add DepositPreauth. +Json::Value +authCredentials( + jtx::Account const& account, + std::vector const& auth) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfAuthorizeCredentials.jsonName] = Json::arrayValue; + auto& arr(jv[sfAuthorizeCredentials.jsonName]); + for (auto const& o : auth) + { + Json::Value j2; + j2[jss::Credential] = o.toJson(); + arr.append(std::move(j2)); + } + jv[sfTransactionType.jsonName] = jss::DepositPreauth; + return jv; +} + +// Remove DepositPreauth. +Json::Value +unauthCredentials( + jtx::Account const& account, + std::vector const& auth) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfUnauthorizeCredentials.jsonName] = Json::arrayValue; + auto& arr(jv[sfUnauthorizeCredentials.jsonName]); + for (auto const& o : auth) + { + Json::Value j2; + j2[jss::Credential] = o.toJson(); + arr.append(std::move(j2)); + } + jv[sfTransactionType.jsonName] = jss::DepositPreauth; + return jv; +} + } // namespace deposit } // namespace jtx diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index d3611efe462..ead6a47c25e 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -301,14 +301,23 @@ MPTTester::pay( Account const& src, Account const& dest, std::int64_t amount, - std::optional err) + std::optional err, + std::optional> credentials) { if (!id_) Throw("MPT has not been created"); auto const srcAmt = getBalance(src); auto const destAmt = getBalance(dest); auto const outstnAmt = getBalance(issuer_); - env_(jtx::pay(src, dest, mpt(amount)), ter(err.value_or(tesSUCCESS))); + + if (credentials) + env_( + jtx::pay(src, dest, mpt(amount)), + ter(err.value_or(tesSUCCESS)), + credentials::ids(*credentials)); + else + env_(jtx::pay(src, dest, mpt(amount)), ter(err.value_or(tesSUCCESS))); + if (env_.ter() != tesSUCCESS) amount = 0; if (close_) diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 16a08d8bad9..12b9d74d27c 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -186,7 +186,8 @@ class MPTTester pay(Account const& src, Account const& dest, std::int64_t amount, - std::optional err = std::nullopt); + std::optional err = std::nullopt, + std::optional> credentials = std::nullopt); void claw( diff --git a/src/test/rpc/DepositAuthorized_test.cpp b/src/test/rpc/DepositAuthorized_test.cpp index ebabe1fbe3f..46637d421e1 100644 --- a/src/test/rpc/DepositAuthorized_test.cpp +++ b/src/test/rpc/DepositAuthorized_test.cpp @@ -31,13 +31,22 @@ class DepositAuthorized_test : public beast::unit_test::suite depositAuthArgs( jtx::Account const& source, jtx::Account const& dest, - std::string const& ledger = "") + std::string const& ledger = "", + std::vector const& credentials = {}) { Json::Value args{Json::objectValue}; args[jss::source_account] = source.human(); args[jss::destination_account] = dest.human(); if (!ledger.empty()) args[jss::ledger_index] = ledger; + + if (!credentials.empty()) + { + auto& arr(args[jss::credentials] = Json::arrayValue); + for (auto const& s : credentials) + arr.append(s); + } + return args; } @@ -276,11 +285,351 @@ class DepositAuthorized_test : public beast::unit_test::suite } } + void + checkCredentialsResponse( + Json::Value const& result, + jtx::Account const& src, + jtx::Account const& dst, + bool authorized, + std::vector credentialIDs = {}, + std::string_view error = "") + { + BEAST_EXPECT( + result[jss::status] == authorized ? jss::success : jss::error); + if (result.isMember(jss::deposit_authorized)) + BEAST_EXPECT(result[jss::deposit_authorized] == authorized); + if (authorized) + BEAST_EXPECT( + result.isMember(jss::deposit_authorized) && + (result[jss::deposit_authorized] == true)); + + BEAST_EXPECT(result.isMember(jss::error) == !error.empty()); + if (!error.empty()) + BEAST_EXPECT(result[jss::error].asString() == error); + + if (authorized) + { + BEAST_EXPECT(result[jss::source_account] == src.human()); + BEAST_EXPECT(result[jss::destination_account] == dst.human()); + + for (unsigned i = 0; i < credentialIDs.size(); ++i) + BEAST_EXPECT(result[jss::credentials][i] == credentialIDs[i]); + } + else + { + BEAST_EXPECT(result[jss::request].isObject()); + + auto const& request = result[jss::request]; + BEAST_EXPECT(request[jss::command] == jss::deposit_authorized); + BEAST_EXPECT(request[jss::source_account] == src.human()); + BEAST_EXPECT(request[jss::destination_account] == dst.human()); + + for (unsigned i = 0; i < credentialIDs.size(); ++i) + BEAST_EXPECT(request[jss::credentials][i] == credentialIDs[i]); + } + } + + void + testCredentials() + { + using namespace jtx; + + const char credType[] = "abcde"; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const diana{"diana"}; + Account const carol{"carol"}; + + Env env(*this); + env.fund(XRP(1000), alice, becky, carol, diana); + env.close(); + + // carol recognize alice + env(credentials::create(alice, carol, credType)); + env.close(); + // retrieve the index of the credentials + auto const jv = credentials::ledgerEntry(env, alice, carol, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // becky sets the DepositAuth flag in the current ledger. + env(fset(becky, asfDepositAuth)); + env.close(); + + // becky authorize any account recognized by carol to make a payment + env(deposit::authCredentials(becky, {{carol, credType}})); + env.close(); + + { + testcase( + "deposit_authorized with credentials failed: empty array."); + + auto args = depositAuthArgs(alice, becky, "validated"); + args[jss::credentials] = Json::arrayValue; + + auto const jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + checkCredentialsResponse( + jv[jss::result], alice, becky, false, {}, "invalidParams"); + } + + { + testcase( + "deposit_authorized with credentials failed: not a string " + "credentials"); + + auto args = depositAuthArgs(alice, becky, "validated"); + args[jss::credentials] = Json::arrayValue; + args[jss::credentials].append(1); + args[jss::credentials].append(3); + + auto const jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + checkCredentialsResponse( + jv[jss::result], alice, becky, false, {}, "invalidParams"); + } + + { + testcase( + "deposit_authorized with credentials failed: not a hex string " + "credentials"); + + auto args = depositAuthArgs(alice, becky, "validated"); + args[jss::credentials] = Json::arrayValue; + args[jss::credentials].append("hello world"); + + auto const jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + checkCredentialsResponse( + jv[jss::result], + alice, + becky, + false, + {"hello world"}, + "invalidParams"); + } + + { + testcase( + "deposit_authorized with credentials failed: not a credential " + "index"); + + auto args = depositAuthArgs( + alice, + becky, + "validated", + {"0127AB8B4B29CCDBB61AA51C0799A8A6BB80B86A9899807C11ED576AF8516" + "473"}); + + auto const jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + checkCredentialsResponse( + jv[jss::result], + alice, + becky, + false, + {"0127AB8B4B29CCDBB61AA51C0799A8A6BB80B86A9899807C11ED576AF8516" + "473"}, + "badCredentials"); + } + + { + testcase( + "deposit_authorized with credentials not authorized: " + "credential not accepted"); + auto const jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx}) + .toStyledString()); + checkCredentialsResponse( + jv[jss::result], + alice, + becky, + false, + {credIdx}, + "badCredentials"); + } + + // alice accept credentials + env(credentials::accept(alice, carol, credType)); + env.close(); + + { + testcase("deposit_authorized with duplicates in credentials"); + auto const jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx, credIdx}) + .toStyledString()); + checkCredentialsResponse( + jv[jss::result], + alice, + becky, + false, + {credIdx, credIdx}, + "badCredentials"); + } + + { + static const std::vector credIds = { + "18004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "28004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "38004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "58004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "68004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "78004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "88004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4", + "98004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4"}; + assert(credIds.size() > maxCredentialsArraySize); + + testcase("deposit_authorized too long credentials"); + auto const jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", credIds) + .toStyledString()); + checkCredentialsResponse( + jv[jss::result], alice, becky, false, credIds, "invalidParams"); + } + + { + testcase("deposit_authorized with credentials"); + auto const jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx}) + .toStyledString()); + checkCredentialsResponse( + jv[jss::result], alice, becky, true, {credIdx}); + } + + { + // diana recognize becky + env(credentials::create(becky, diana, credType)); + env.close(); + env(credentials::accept(becky, diana, credType)); + env.close(); + + // retrieve the index of the credentials + auto jv = credentials::ledgerEntry(env, becky, diana, credType); + std::string const credBecky = + jv[jss::result][jss::index].asString(); + + testcase("deposit_authorized account without preauth"); + jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(becky, alice, "validated", {credBecky}) + .toStyledString()); + checkCredentialsResponse( + jv[jss::result], becky, alice, true, {credBecky}); + } + + { + // carol recognize diana + env(credentials::create(diana, carol, credType)); + env.close(); + env(credentials::accept(diana, carol, credType)); + env.close(); + // retrieve the index of the credentials + auto jv = credentials::ledgerEntry(env, alice, carol, credType); + std::string const credDiana = + jv[jss::result][jss::index].asString(); + + // alice try to use credential for different account + jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(becky, alice, "validated", {credDiana}) + .toStyledString()); + checkCredentialsResponse( + jv[jss::result], + becky, + alice, + false, + {credDiana}, + "badCredentials"); + } + + { + testcase("deposit_authorized with expired credentials"); + + // check expired credentials + const char credType2[] = "fghijk"; + std::uint32_t const x = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count() + + 40; + + // create credentials with expire time 40s + auto jv = credentials::create(alice, carol, credType2); + jv[sfExpiration.jsonName] = x; + env(jv); + env.close(); + env(credentials::accept(alice, carol, credType2)); + env.close(); + jv = credentials::ledgerEntry(env, alice, carol, credType2); + std::string const credIdx2 = jv[jss::result][jss::index].asString(); + + // becky sets the DepositAuth flag in the current ledger. + env(fset(becky, asfDepositAuth)); + env.close(); + + // becky authorize any account recognized by carol to make a payment + env(deposit::authCredentials(becky, {{carol, credType2}})); + env.close(); + + { + // this should be fine + jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx2}) + .toStyledString()); + checkCredentialsResponse( + jv[jss::result], alice, becky, true, {credIdx2}); + } + + // increase timer by 20s + env.close(); + env.close(); + { + // now credentials expired + jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx2}) + .toStyledString()); + + checkCredentialsResponse( + jv[jss::result], + alice, + becky, + false, + {credIdx2}, + "badCredentials"); + } + } + } + void run() override { testValid(); testErrors(); + testCredentials(); } }; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 792da88b5bc..c5e10198c49 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -727,6 +727,204 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryCredentials() + { + testcase("ledger_entry credentials"); + + using namespace test::jtx; + + Env env(*this); + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + const char credType[] = "abcde"; + + env.fund(XRP(5000), issuer, alice, bob); + env.close(); + + // Setup credentials with DepositAuth object for Alice and Bob + env(credentials::create(alice, issuer, credType)); + env.close(); + + { + // Succeed + auto jv = credentials::ledgerEntry(env, alice, issuer, credType); + BEAST_EXPECT( + jv.isObject() && jv.isMember(jss::result) && + !jv[jss::result].isMember(jss::error) && + jv[jss::result].isMember(jss::node) && + jv[jss::result][jss::node].isMember( + sfLedgerEntryType.jsonName) && + jv[jss::result][jss::node][sfLedgerEntryType.jsonName] == + jss::Credential); + + std::string const credIdx = jv[jss::result][jss::index].asString(); + + jv = credentials::ledgerEntry(env, credIdx); + BEAST_EXPECT( + jv.isObject() && jv.isMember(jss::result) && + !jv[jss::result].isMember(jss::error) && + jv[jss::result].isMember(jss::node) && + jv[jss::result][jss::node].isMember( + sfLedgerEntryType.jsonName) && + jv[jss::result][jss::node][sfLedgerEntryType.jsonName] == + jss::Credential); + } + + { + // Fail, index not a hash + auto const jv = credentials::ledgerEntry(env, ""); + checkErrorValue(jv[jss::result], "malformedRequest", ""); + } + + { + // Fail, credential doesn't exist + auto const jv = credentials::ledgerEntry( + env, + "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" + "E4"); + checkErrorValue(jv[jss::result], "entryNotFound", ""); + } + + { + // Fail, invalid subject + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = 42; + jv[jss::credential][jss::issuer] = issuer.human(); + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, invalid issuer + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = 42; + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, invalid credentials type + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = issuer.human(); + jv[jss::credential][jss::credential_type] = 42; + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, empty subject + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = ""; + jv[jss::credential][jss::issuer] = issuer.human(); + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, empty issuer + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = ""; + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, empty credentials type + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = issuer.human(); + jv[jss::credential][jss::credential_type] = ""; + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, no subject + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::issuer] = issuer.human(); + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, no issuer + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, no credentials type + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = issuer.human(); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, not AccountID subject + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = "wehsdbvasbdfvj"; + jv[jss::credential][jss::issuer] = issuer.human(); + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, not AccountID issuer + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = "c4p93ugndfbsiu"; + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Fail, credentials type isn't hex encoded + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = issuer.human(); + jv[jss::credential][jss::credential_type] = "12KK"; + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + } + void testLedgerEntryDepositPreauth() { @@ -858,6 +1056,302 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryDepositPreauthCred() + { + testcase("ledger_entry Deposit Preauth with credentials"); + + using namespace test::jtx; + + Env env(*this); + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + const char credType[] = "abcde"; + + env.fund(XRP(5000), issuer, alice, bob); + env.close(); + + { + // Setup Bob with DepositAuth + env(fset(bob, asfDepositAuth), fee(drops(10))); + env.close(); + env(deposit::authCredentials(bob, {{issuer, credType}})); + env.close(); + } + + { + // Succeed + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(std::move(jo)); + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + + BEAST_EXPECT( + jrr.isObject() && jrr.isMember(jss::result) && + !jrr[jss::result].isMember(jss::error) && + jrr[jss::result].isMember(jss::node) && + jrr[jss::result][jss::node].isMember( + sfLedgerEntryType.jsonName) && + jrr[jss::result][jss::node][sfLedgerEntryType.jsonName] == + jss::DepositPreauth); + } + + { + // Failed, invalid account + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = to_string(xrpAccount()); + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(std::move(jo)); + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizedCredentials", ""); + } + + { + // Failed, duplicates in credentials + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(jo); + arr.append(std::move(jo)); + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizedCredentials", ""); + } + + { + // Failed, invalid credential_type + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = ""; + arr.append(std::move(jo)); + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizedCredentials", ""); + } + + { + // Failed, authorized and authorized_credentials both present + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + jvParams[jss::deposit_preauth][jss::authorized] = alice.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(std::move(jo)); + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Failed, authorized_credentials is not an array + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = 42; + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Failed, authorized_credentials is empty array + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizedCredentials", ""); + } + + { + // Failed, authorized_credentials is too long + + static const std::string_view credTypes[] = { + "cred1", + "cred2", + "cred3", + "cred4", + "cred5", + "cred6", + "cred7", + "cred8", + "cred9"}; + static_assert( + sizeof(credTypes) / sizeof(credTypes[0]) > + maxCredentialsArraySize); + + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + for (unsigned i = 0; i < sizeof(credTypes) / sizeof(credTypes[0]); + ++i) + { + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = + strHex(std::string_view(credTypes[i])); + arr.append(std::move(jo)); + } + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizedCredentials", ""); + } + + { + // Failed, issuer isn't string + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = 42; + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(std::move(jo)); + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizedCredentials", ""); + } + + { + // Failed, issuer isn't valid encoded account + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = "invalid_account"; + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(std::move(jo)); + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizedCredentials", ""); + } + + { + // Failed, credential_type isn't string + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + jvParams[jss::deposit_preauth][jss::authorized] = alice.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = 42; + arr.append(std::move(jo)); + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Failed, credential_type isn't hex encoded + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + jvParams[jss::deposit_preauth][jss::authorized] = alice.human(); + + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr( + jvParams[jss::deposit_preauth][jss::authorized_credentials]); + + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = "12KK"; + arr.append(std::move(jo)); + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + } + void testLedgerEntryDirectory() { @@ -2447,7 +2941,9 @@ class LedgerRPC_test : public beast::unit_test::suite testLedgerAccounts(); testLedgerEntryAccountRoot(); testLedgerEntryCheck(); + testLedgerEntryCredentials(); testLedgerEntryDepositPreauth(); + testLedgerEntryDepositPreauthCred(); testLedgerEntryDirectory(); testLedgerEntryEscrow(); testLedgerEntryOffer(); diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index 5f13c9799a1..b812740fb3f 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -2458,7 +2458,15 @@ static RPCCallTestData const rpcCallTestArray[] = { {"deposit_authorized", "source_account_NotValidated", "destination_account_NotValidated", - "4294967295"}, + "4294967295", + "cred1", + "cred2", + "cred3", + "cred4", + "cred5", + "cred6", + "cred7", + "cred8"}, RPCCallTestData::no_exception, R"({ "method" : "deposit_authorized", @@ -2467,7 +2475,8 @@ static RPCCallTestData const rpcCallTestArray[] = { "api_version" : %API_VER%, "destination_account" : "destination_account_NotValidated", "ledger_index" : 4294967295, - "source_account" : "source_account_NotValidated" + "source_account" : "source_account_NotValidated", + "credentials": ["cred1", "cred2", "cred3", "cred4", "cred5", "cred6", "cred7", "cred8"] } ] })"}, @@ -2512,7 +2521,15 @@ static RPCCallTestData const rpcCallTestArray[] = { "source_account_NotValidated", "destination_account_NotValidated", "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789", - "spare"}, + "cred1", + "cred2", + "cred3", + "cred4", + "cred5", + "cred6", + "cred7", + "cred8", + "too_much"}, RPCCallTestData::no_exception, R"({ "method" : "deposit_authorized", diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index 54d5ab1f96a..169a6dad912 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/misc/CredentialHelpers.cpp b/src/xrpld/app/misc/CredentialHelpers.cpp new file mode 100644 index 00000000000..08b5d804d4b --- /dev/null +++ b/src/xrpld/app/misc/CredentialHelpers.cpp @@ -0,0 +1,262 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +namespace credentials { + +bool +checkExpired( + std::shared_ptr const& sleCredential, + NetClock::time_point const& closed) +{ + std::uint32_t const exp = (*sleCredential)[~sfExpiration].value_or( + std::numeric_limits::max()); + std::uint32_t const now = closed.time_since_epoch().count(); + return now > exp; +} + +bool +removeExpired(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 (sleCred && checkExpired(sleCred, closeTime)) + { + JLOG(j.trace()) + << "Credentials are expired. Cred: " << sleCred->getText(); + // delete expired credentials even if the transaction failed + deleteSLE(view, sleCred, j); + foundExpired = true; + } + } + + return foundExpired; +} + +TER +deleteSLE( + ApplyView& view, + std::shared_ptr const& sleCredential, + beast::Journal j) +{ + if (!sleCredential) + return tecNO_ENTRY; + + auto delSLE = + [&view, &sleCredential, j]( + AccountID const& account, SField const& node, bool isOwner) -> TER { + auto const sleAccount = view.peek(keylet::account(account)); + if (!sleAccount) + { + JLOG(j.fatal()) << "Internal error: can't retrieve Owner account."; + return tecINTERNAL; + } + + // Remove object from owner directory + std::uint64_t const page = sleCredential->getFieldU64(node); + if (!view.dirRemove( + keylet::ownerDir(account), page, sleCredential->key(), false)) + { + JLOG(j.fatal()) << "Unable to delete Credential from owner."; + return tefBAD_LEDGER; + } + + if (isOwner) + adjustOwnerCount(view, sleAccount, -1, j); + + return tesSUCCESS; + }; + + auto const issuer = sleCredential->getAccountID(sfIssuer); + auto const subject = sleCredential->getAccountID(sfSubject); + bool const accepted = sleCredential->getFlags() & lsfAccepted; + + auto err = delSLE(issuer, sfIssuerNode, !accepted || (subject == issuer)); + if (!isTesSuccess(err)) + return err; + + if (subject != issuer) + { + err = delSLE(subject, sfSubjectNode, accepted); + if (!isTesSuccess(err)) + return err; + } + + // Remove object from ledger + view.erase(sleCredential); + + return tesSUCCESS; +} + +NotTEC +checkFields(PreflightContext const& ctx) +{ + if (!ctx.tx.isFieldPresent(sfCredentialIDs)) + return tesSUCCESS; + + auto const& credentials = ctx.tx.getFieldV256(sfCredentialIDs); + if (credentials.empty() || (credentials.size() > maxCredentialsArraySize)) + { + JLOG(ctx.j.trace()) + << "Malformed transaction: Credentials array size is invalid: " + << credentials.size(); + return temMALFORMED; + } + + std::unordered_set duplicates; + for (auto const& cred : credentials) + { + auto [it, ins] = duplicates.insert(cred); + if (!ins) + { + JLOG(ctx.j.trace()) + << "Malformed transaction: duplicates in credentials."; + return temMALFORMED; + } + } + + return tesSUCCESS; +} + +TER +valid(PreclaimContext const& ctx, AccountID const& src) +{ + if (!ctx.tx.isFieldPresent(sfCredentialIDs)) + return tesSUCCESS; + + auto const& credIDs(ctx.tx.getFieldV256(sfCredentialIDs)); + for (auto const& h : credIDs) + { + auto const sleCred = ctx.view.read(keylet::credential(h)); + if (!sleCred) + { + JLOG(ctx.j.trace()) << "Credential doesn't exist. Cred: " << h; + return tecBAD_CREDENTIALS; + } + + if (sleCred->getAccountID(sfSubject) != src) + { + JLOG(ctx.j.trace()) + << "Credential doesn’t belong to the source account. Cred: " + << h; + return tecBAD_CREDENTIALS; + } + + if (!(sleCred->getFlags() & lsfAccepted)) + { + JLOG(ctx.j.trace()) << "Credential isn't accepted. Cred: " << h; + return tecBAD_CREDENTIALS; + } + + // Expiration checks are in doApply + } + + return tesSUCCESS; +} + +TER +authorized(ApplyContext const& ctx, AccountID const& dst) +{ + auto const& credIDs(ctx.tx.getFieldV256(sfCredentialIDs)); + std::set> sorted; + std::vector> lifeExtender; + lifeExtender.reserve(credIDs.size()); + for (auto const& h : credIDs) + { + auto sleCred = ctx.view().read(keylet::credential(h)); + if (!sleCred) // already checked in preclaim + return tefINTERNAL; + + auto [it, ins] = + sorted.emplace((*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]); + if (!ins) + return tefINTERNAL; + lifeExtender.push_back(std::move(sleCred)); + } + + if (!ctx.view().exists(keylet::depositPreauth(dst, sorted))) + { + JLOG(ctx.journal.trace()) << "DepositPreauth doesn't exist"; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +std::set> +makeSorted(STArray const& in) +{ + std::set> out; + for (auto const& cred : in) + { + auto [it, ins] = out.emplace(cred[sfIssuer], cred[sfCredentialType]); + if (!ins) + return {}; + } + return out; +} + +} // namespace credentials + +TER +verifyDepositPreauth( + ApplyContext& ctx, + AccountID const& src, + AccountID const& dst, + std::shared_ptr const& sleDst) +{ + // If depositPreauth is enabled, then an account that requires + // authorization has at least two ways to get a payment in: + // 1. If src == dst, or + // 2. If src is deposit preauthorized by dst (either by account or by + // credentials). + + bool const credentialsPresent = ctx.tx.isFieldPresent(sfCredentialIDs); + + if (credentialsPresent && + credentials::removeExpired(ctx.view(), ctx.tx, ctx.journal)) + return tecEXPIRED; + + if (sleDst && (sleDst->getFlags() & lsfDepositAuth)) + { + if (src != dst) + { + if (!ctx.view().exists(keylet::depositPreauth(dst, src))) + return !credentialsPresent ? tecNO_PERMISSION + : credentials::authorized(ctx, dst); + } + } + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/misc/CredentialHelpers.h b/src/xrpld/app/misc/CredentialHelpers.h new file mode 100644 index 00000000000..3291fc1daa6 --- /dev/null +++ b/src/xrpld/app/misc/CredentialHelpers.h @@ -0,0 +1,77 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#pragma once + +#include + +#include + +namespace ripple { +namespace credentials { + +// These function will be used by the code that use DepositPreauth / Credentials +// (and any future preauthorization modes) as part of authorization (all the +// transfer funds transactions) + +// Check if credential sfExpiration field has passed ledger's parentCloseTime +bool +checkExpired( + std::shared_ptr const& sleCredential, + NetClock::time_point const& closed); + +// Return true if at least 1 expired credentials was found(and deleted) +bool +removeExpired(ApplyView& view, STTx const& tx, beast::Journal const j); + +// Actually remove a credentials object from the ledger +TER +deleteSLE( + ApplyView& view, + std::shared_ptr const& sleCredential, + beast::Journal j); + +// Amendment and parameters checks for sfCredentialIDs field +NotTEC +checkFields(PreflightContext const& ctx); + +// Accessing the ledger to check if provided credentials are valid +TER +valid(PreclaimContext const& ctx, AccountID const& src); + +// This function is only called when we about to return tecNO_PERMISSION because +// all the checks for the DepositPreauth authorization failed. +TER +authorized(ApplyContext const& ctx, AccountID const& dst); + +// return empty set if there are duplicates +std::set> +makeSorted(STArray const& in); + +} // namespace credentials + +// Check expired credentials and for existing DepositPreauth ledger object +TER +verifyDepositPreauth( + ApplyContext& ctx, + AccountID const& src, + AccountID const& dst, + std::shared_ptr const& sleDst); + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Credentials.cpp b/src/xrpld/app/tx/detail/Credentials.cpp new file mode 100644 index 00000000000..4da875f8d7c --- /dev/null +++ b/src/xrpld/app/tx/detail/Credentials.cpp @@ -0,0 +1,382 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#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. +*/ + +using namespace credentials; + +// ------- CREATE -------------------------- + +NotTEC +CredentialCreate::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCredentials)) + { + JLOG(ctx.j.trace()) << "featureCredentials is disabled."; + return temDISABLED; + } + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const& tx = ctx.tx; + auto& j = ctx.j; + + if (!tx[sfSubject]) + { + JLOG(j.trace()) << "Malformed transaction: Invalid Subject"; + return temMALFORMED; + } + + auto const uri = tx[~sfURI]; + if (uri && (uri->empty() || (uri->size() > maxCredentialURILength))) + { + JLOG(j.trace()) << "Malformed transaction: invalid size of URI."; + return temMALFORMED; + } + + auto const credType = tx[sfCredentialType]; + if (credType.empty() || (credType.size() > maxCredentialTypeLength)) + { + JLOG(j.trace()) + << "Malformed transaction: invalid size of CredentialType."; + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +CredentialCreate::preclaim(PreclaimContext const& ctx) +{ + auto const credType(ctx.tx[sfCredentialType]); + auto const subject = ctx.tx[sfSubject]; + + if (!ctx.view.exists(keylet::account(subject))) + { + JLOG(ctx.j.trace()) << "Subject doesn't exist."; + return tecNO_TARGET; + } + + if (ctx.view.exists( + keylet::credential(subject, ctx.tx[sfAccount], credType))) + { + JLOG(ctx.j.trace()) << "Credential already exists."; + return tecDUPLICATE; + } + + return tesSUCCESS; +} + +TER +CredentialCreate::doApply() +{ + auto const subject = ctx_.tx[sfSubject]; + auto const credType(ctx_.tx[sfCredentialType]); + Keylet const credentialKey = + keylet::credential(subject, account_, credType); + + auto const sleCred = std::make_shared(credentialKey); + if (!sleCred) + return tefINTERNAL; + + 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)); + } + + auto const sleIssuer = view().peek(keylet::account(account_)); + if (!sleIssuer) + return tefINTERNAL; + + { + STAmount const reserve{view().fees().accountReserve( + sleIssuer->getFieldU32(sfOwnerCount) + 1)}; + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + sleCred->setAccountID(sfSubject, subject); + sleCred->setAccountID(sfIssuer, account_); + sleCred->setFieldVL(sfCredentialType, credType); + + if (ctx_.tx.isFieldPresent(sfURI)) + sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI)); + + { + auto const page = view().dirInsert( + keylet::ownerDir(account_), + credentialKey, + describeOwnerDir(account_)); + JLOG(j_.trace()) << "Adding Credential to owner directory " + << to_string(credentialKey.key) << ": " + << (page ? "success" : "failure"); + if (!page) + return tecDIR_FULL; + sleCred->setFieldU64(sfIssuerNode, *page); + + adjustOwnerCount(view(), sleIssuer, 1, j_); + } + + if (subject == account_) + { + sleCred->setFieldU32(sfFlags, lsfAccepted); + } + else + { + auto const page = view().dirInsert( + keylet::ownerDir(subject), + credentialKey, + describeOwnerDir(subject)); + JLOG(j_.trace()) << "Adding Credential to owner directory " + << to_string(credentialKey.key) << ": " + << (page ? "success" : "failure"); + if (!page) + return tecDIR_FULL; + sleCred->setFieldU64(sfSubjectNode, *page); + view().update(view().peek(keylet::account(subject))); + } + + view().insert(sleCred); + + return tesSUCCESS; +} + +// ------- DELETE -------------------------- +NotTEC +CredentialDelete::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCredentials)) + { + JLOG(ctx.j.trace()) << "featureCredentials is disabled."; + return temDISABLED; + } + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const subject = ctx.tx[~sfSubject]; + auto const issuer = ctx.tx[~sfIssuer]; + + if (!subject && !issuer) + { + // Neither field is present, the transaction is malformed. + JLOG(ctx.j.trace()) << "Malformed transaction: " + "No Subject or Issuer fields."; + return temMALFORMED; + } + + // Make sure that the passed account is valid. + if ((subject && subject->isZero()) || (issuer && issuer->isZero())) + { + JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer " + "field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + auto const credType = ctx.tx[sfCredentialType]; + if (credType.empty() || (credType.size() > maxCredentialTypeLength)) + { + JLOG(ctx.j.trace()) + << "Malformed transaction: invalid size of CredentialType."; + return temMALFORMED; + } + + 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::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 (!sleCred) + return tefINTERNAL; + + if ((subject != account_) && (issuer != account_) && + !checkExpired(sleCred, ctx_.view().info().parentCloseTime)) + { + JLOG(j_.trace()) << "Can't delete non-expired credential."; + return tecNO_PERMISSION; + } + + return deleteSLE(view(), sleCred, j_); +} + +// ------- APPLY -------------------------- + +NotTEC +CredentialAccept::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCredentials)) + { + JLOG(ctx.j.trace()) << "featureCredentials is disabled."; + return temDISABLED; + } + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!ctx.tx[sfIssuer]) + { + JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + auto const credType = ctx.tx[sfCredentialType]; + if (credType.empty() || (credType.size() > maxCredentialTypeLength)) + { + JLOG(ctx.j.trace()) + << "Malformed transaction: invalid size of CredentialType."; + return temMALFORMED; + } + + 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))) + { + JLOG(ctx.j.warn()) << "No issuer: " << to_string(issuer); + return tecNO_ISSUER; + } + + auto const sleCred = + ctx.view.read(keylet::credential(subject, issuer, credType)); + if (!sleCred) + { + JLOG(ctx.j.warn()) << "No credential: " << to_string(subject) << ", " + << to_string(issuer) << ", " << credType; + return tecNO_ENTRY; + } + + if (sleCred->getFieldU32(sfFlags) & lsfAccepted) + { + JLOG(ctx.j.warn()) << "Credential already accepted: " + << to_string(subject) << ", " << to_string(issuer) + << ", " << credType; + return tecDUPLICATE; + } + + return tesSUCCESS; +} + +TER +CredentialAccept::doApply() +{ + AccountID const issuer{ctx_.tx[sfIssuer]}; + + // Both exist as credential object exist itself (checked in preclaim) + auto const sleSubject = view().peek(keylet::account(account_)); + auto const sleIssuer = view().peek(keylet::account(issuer)); + + if (!sleSubject || !sleIssuer) + return tefINTERNAL; + + { + STAmount const reserve{view().fees().accountReserve( + sleSubject->getFieldU32(sfOwnerCount) + 1)}; + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + auto const credType(ctx_.tx[sfCredentialType]); + Keylet const credentialKey = keylet::credential(account_, issuer, credType); + auto const sleCred = view().peek(credentialKey); // Checked in preclaim() + + if (checkExpired(sleCred, view().info().parentCloseTime)) + { + JLOG(j_.trace()) << "Credential is expired: " << sleCred->getText(); + // delete expired credentials even if the transaction failed + auto const err = credentials::deleteSLE(view(), sleCred, j_); + return isTesSuccess(err) ? tecEXPIRED : err; + } + + sleCred->setFieldU32(sfFlags, lsfAccepted); + view().update(sleCred); + + adjustOwnerCount(view(), sleIssuer, -1, j_); + adjustOwnerCount(view(), sleSubject, 1, j_); + + 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..7e7522d82c1 --- /dev/null +++ b/src/xrpld/app/tx/detail/Credentials.h @@ -0,0 +1,87 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#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); + + 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; +}; + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index fb2f3fc507f..a7f33a3d8dd 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 @@ -41,6 +42,10 @@ DeleteAccount::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureDeletableAccounts)) return temDISABLED; + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return temDISABLED; + if (ctx.tx.getFlags() & tfUniversalMask) return temINVALID_FLAG; @@ -51,6 +56,9 @@ DeleteAccount::preflight(PreflightContext const& ctx) // An account cannot be deleted and give itself the resulting XRP. return temDST_IS_SRC; + if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + return err; + return preflight2(ctx); } @@ -110,14 +118,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 +167,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 credentials::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 +201,8 @@ nonObligationDeleter(LedgerEntryType t) return removeDIDFromLedger; case ltORACLE: return removeOracleFromLedger; + case ltCREDENTIAL: + return removeCredentialFromLedger; default: return nullptr; } @@ -202,12 +224,21 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) if ((*sleDst)[sfFlags] & lsfRequireDestTag && !ctx.tx[~sfDestinationTag]) return tecDST_TAG_NEEDED; - // Check whether the destination account requires deposit authorization. - if (ctx.view.rules().enabled(featureDepositAuth) && - (sleDst->getFlags() & lsfDepositAuth)) + // If credentials are provided - check them anyway + if (auto const err = credentials::valid(ctx, account); !isTesSuccess(err)) + return err; + + // if credentials then postpone auth check to doApply, to check for expired + // credentials + if (!ctx.tx.isFieldPresent(sfCredentialIDs)) { - if (!ctx.view.exists(keylet::depositPreauth(dst, account))) - return tecNO_PERMISSION; + // 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 sleAccount = ctx.view.read(keylet::account(account)); @@ -316,12 +347,21 @@ DeleteAccount::doApply() auto src = view().peek(keylet::account(account_)); assert(src); - auto dst = view().peek(keylet::account(ctx_.tx[sfDestination])); + auto const dstID = ctx_.tx[sfDestination]; + auto dst = view().peek(keylet::account(dstID)); assert(dst); if (!src || !dst) return tefBAD_LEDGER; + if (ctx_.view().rules().enabled(featureDepositAuth) && + ctx_.tx.isFieldPresent(sfCredentialIDs)) + { + if (auto err = verifyDepositPreauth(ctx_, account_, dstID, dst); + !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..73cd19e4120 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -17,14 +17,19 @@ */ //============================================================================== +#include #include #include #include #include #include #include +#include #include +#include +#include + namespace ripple { NotTEC @@ -33,45 +38,101 @@ DepositPreauth::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureDepositPreauth)) return temDISABLED; + bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); + bool const unauthArrPresent = + ctx.tx.isFieldPresent(sfUnauthorizeCredentials); + int const authCredPresent = + static_cast(authArrPresent) + static_cast(unauthArrPresent); + + if (authCredPresent && !ctx.rules.enabled(featureCredentials)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; auto& tx = ctx.tx; - auto& j = ctx.j; if (tx.getFlags() & tfUniversalMask) { - JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; + JLOG(ctx.j.trace()) << "Malformed transaction: Invalid flags set."; return temINVALID_FLAG; } auto const optAuth = ctx.tx[~sfAuthorize]; auto const optUnauth = ctx.tx[~sfUnauthorize]; - if (static_cast(optAuth) == static_cast(optUnauth)) + int const authPresent = static_cast(optAuth.has_value()) + + static_cast(optUnauth.has_value()); + + if (authPresent + authCredPresent != 1) { - // Either both fields are present or neither field is present. In - // either case the transaction is malformed. - JLOG(j.trace()) + // There can only be 1 field out of 4 or the transaction is malformed. + JLOG(ctx.j.trace()) << "Malformed transaction: " "Invalid Authorize and Unauthorize field combination."; return temMALFORMED; } - // Make sure that the passed account is valid. - AccountID const target{optAuth ? *optAuth : *optUnauth}; - if (target == beast::zero) + if (authPresent) { - JLOG(j.trace()) << "Malformed transaction: Authorized or Unauthorized " - "field zeroed."; - return temINVALID_ACCOUNT_ID; - } + // Make sure that the passed account is valid. + AccountID const& target(optAuth ? *optAuth : *optUnauth); + if (!target) + { + JLOG(ctx.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 (optAuth && (target == ctx.tx[sfAccount])) + { + JLOG(ctx.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; + STArray const& arr(ctx.tx.getFieldArray( + authArrPresent ? sfAuthorizeCredentials + : sfUnauthorizeCredentials)); + if (arr.empty() || (arr.size() > maxCredentialsArraySize)) + { + JLOG(ctx.j.trace()) << "Malformed transaction: " + "Invalid AuthorizeCredentials size: " + << arr.size(); + return temMALFORMED; + } + + std::unordered_set duplicates; + for (auto const& o : arr) + { + auto const& issuer(o[sfIssuer]); + if (!issuer) + { + JLOG(ctx.j.trace()) + << "Malformed transaction: " + "AuthorizeCredentials Issuer account is invalid."; + return temINVALID_ACCOUNT_ID; + } + + auto const ct = o[sfCredentialType]; + if (ct.empty() || (ct.size() > maxCredentialTypeLength)) + { + JLOG(ctx.j.trace()) + << "Malformed transaction: invalid size of CredentialType."; + return temMALFORMED; + } + + auto [it, ins] = duplicates.insert(sha512Half(issuer, ct)); + if (!ins) + { + JLOG(ctx.j.trace()) + << "Malformed transaction: duplicates in credentials."; + return temMALFORMED; + } + } } return preflight2(ctx); @@ -80,6 +141,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 +153,42 @@ 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, ctx.tx[sfUnauthorize]))) + return tecNO_ENTRY; + } + else if (ctx.tx.isFieldPresent(sfAuthorizeCredentials)) + { + STArray const& authCred(ctx.tx.getFieldArray(sfAuthorizeCredentials)); + std::set> sorted; + for (auto const& o : authCred) + { + auto const& issuer = o[sfIssuer]; + if (!ctx.view.exists(keylet::account(issuer))) + return tecNO_ISSUER; + auto [it, ins] = sorted.emplace(issuer, o[sfCredentialType]); + if (!ins) + return tefINTERNAL; + } + + // Verify that the Preauth entry they asked to add is not already + // in the ledger. + if (ctx.view.exists(keylet::depositPreauth(account, sorted))) + return tecDUPLICATE; + } + else if (ctx.tx.isFieldPresent(sfUnauthorizeCredentials)) + { + // Verify that the Preauth entry is in the ledger. + if (!ctx.view.exists(keylet::depositPreauth( + account, + credentials::makeSorted( + ctx.tx.getFieldArray(sfUnauthorizeCredentials))))) return tecNO_ENTRY; } return tesSUCCESS; @@ -133,7 +224,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,30 +239,92 @@ 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. + + auto const sortedTX = credentials::makeSorted( + ctx_.tx.getFieldArray(sfAuthorizeCredentials)); + STArray sortedLE(sfAuthorizeCredentials, sortedTX.size()); + for (auto const& p : sortedTX) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, p.first); + cred.setFieldVL(sfCredentialType, p.second); + sortedLE.push_back(std::move(cred)); + } + + Keylet const preauthKey = keylet::depositPreauth(account_, sortedTX); + auto slePreauth = std::make_shared(preauthKey); + if (!slePreauth) + return tefINTERNAL; + + slePreauth->setAccountID(sfAccount, account_); + slePreauth->peekFieldArray(sfAuthorizeCredentials) = + std::move(sortedLE); + + view().insert(slePreauth); + + auto const page = view().dirInsert( + keylet::ownerDir(account_), preauthKey, describeOwnerDir(account_)); + + JLOG(j_.trace()) << "Adding DepositPreauth to owner directory " + << to_string(preauthKey.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 preauthKey = keylet::depositPreauth( + account_, + credentials::makeSorted( + ctx_.tx.getFieldArray(sfUnauthorizeCredentials))); + return DepositPreauth::removeFromLedger(view(), preauthKey.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))}; + // Existence already checked in preclaim and DeleteAccount + auto const slePreauth{view.peek(keylet::depositPreauth(preauthIndex))}; if (!slePreauth) { JLOG(j.warn()) << "Selected DepositPreauth does not exist."; @@ -192,7 +344,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..76a7c080737 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.h +++ b/src/xrpld/app/tx/detail/DepositPreauth.h @@ -45,7 +45,6 @@ class DepositPreauth : public Transactor // Interface used by DeleteAccount static TER removeFromLedger( - Application& app, ApplyView& view, uint256 const& delIndex, beast::Journal j); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index e34b675998d..f98e72f23dd 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -19,6 +19,7 @@ #include +#include #include #include #include @@ -309,6 +310,10 @@ EscrowFinish::preflight(PreflightContext const& ctx) if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) return temINVALID_FLAG; + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -347,6 +352,9 @@ EscrowFinish::preflight(PreflightContext const& ctx) } } + if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + return err; + return tesSUCCESS; } @@ -363,6 +371,19 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) return Transactor::calculateBaseFee(view, tx) + extraFee; } +TER +EscrowFinish::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.rules().enabled(featureCredentials)) + return Transactor::preclaim(ctx); + + if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); + !isTesSuccess(err)) + return err; + + return tesSUCCESS; +} + TER EscrowFinish::doApply() { @@ -456,19 +477,9 @@ EscrowFinish::doApply() 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; - } - } + if (auto err = verifyDepositPreauth(ctx_, account_, destID, sled); + !isTesSuccess(err)) + return err; } AccountID const account = (*slep)[sfAccount]; diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 9e30da76175..78acdbee00c 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -63,6 +63,9 @@ class EscrowFinish : public Transactor static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); + static TER + preclaim(PreclaimContext const& ctx); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index d1eaf86844d..90fc399b344 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -481,6 +481,7 @@ LedgerEntryTypesMatch::visitEntry( case ltORACLE: case ltMPTOKEN_ISSUANCE: case ltMPTOKEN: + 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..b2d4c0c9449 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 @@ -403,6 +404,10 @@ PayChanFund::doApply() NotTEC PayChanClaim::preflight(PreflightContext const& ctx) { + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -453,9 +458,25 @@ PayChanClaim::preflight(PreflightContext const& ctx) return temBAD_SIGNATURE; } + if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + return err; + return preflight2(ctx); } +TER +PayChanClaim::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.rules().enabled(featureCredentials)) + return Transactor::preclaim(ctx); + + if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); + !isTesSuccess(err)) + return err; + + return tesSUCCESS; +} + TER PayChanClaim::doApply() { @@ -516,18 +537,11 @@ PayChanClaim::doApply() (txAccount == src && (sled->getFlags() & lsfDisallowXRP))) return tecNO_TARGET; - // Check whether the destination account requires deposit authorization. - if (depositAuth && (sled->getFlags() & lsfDepositAuth)) + if (depositAuth) { - // 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; - } + if (auto err = verifyDepositPreauth(ctx_, txAccount, dst, sled); + !isTesSuccess(err)) + return err; } (*slep)[sfBalance] = ctx_.tx[sfBalance]; diff --git a/src/xrpld/app/tx/detail/PayChan.h b/src/xrpld/app/tx/detail/PayChan.h index 5eef7e51c0c..2e09c473dc0 100644 --- a/src/xrpld/app/tx/detail/PayChan.h +++ b/src/xrpld/app/tx/detail/PayChan.h @@ -85,6 +85,9 @@ class PayChanClaim : public Transactor static NotTEC preflight(PreflightContext const& ctx); + static TER + preclaim(PreclaimContext const& ctx); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 77c8d015d1e..2ea13ffabc8 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -65,6 +66,10 @@ getMaxSourceAmount( NotTEC Payment::preflight(PreflightContext const& ctx) { + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -227,6 +232,9 @@ Payment::preflight(PreflightContext const& ctx) } } + if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) + return err; + return preflight2(ctx); } @@ -311,6 +319,10 @@ Payment::preclaim(PreclaimContext const& ctx) } } + if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); + !isTesSuccess(err)) + return err; + return tesSUCCESS; } @@ -362,8 +374,9 @@ Payment::doApply() } // Determine whether the destination requires deposit authorization. - bool const reqDepositAuth = sleDst->getFlags() & lsfDepositAuth && - view().rules().enabled(featureDepositAuth); + bool const depositAuth = view().rules().enabled(featureDepositAuth); + bool const reqDepositAuth = + sleDst->getFlags() & lsfDepositAuth && depositAuth; bool const depositPreauth = view().rules().enabled(featureDepositPreauth); @@ -380,18 +393,17 @@ Payment::doApply() // Ripple payment with at least one intermediate step and uses // transitive balances. - if (depositPreauth && reqDepositAuth) + if (depositPreauth && depositAuth) { // If depositPreauth is enabled, then an account that requires // authorization has two ways to get an IOU Payment in: // 1. If Account == Destination, or // 2. If Account is deposit preauthorized by destination. - if (dstAccountID != account_) - { - if (!view().exists( - keylet::depositPreauth(dstAccountID, account_))) - return tecNO_PERMISSION; - } + + if (auto err = + verifyDepositPreauth(ctx_, account_, dstAccountID, sleDst); + !isTesSuccess(err)) + return err; } path::RippleCalc::Input rcInput; @@ -458,6 +470,11 @@ Payment::doApply() ter != tesSUCCESS) return ter; + if (auto err = + verifyDepositPreauth(ctx_, account_, dstAccountID, sleDst); + !isTesSuccess(err)) + return err; + auto const& issuer = mptIssue.getIssuer(); // Transfer rate @@ -547,7 +564,7 @@ Payment::doApply() // The source account does have enough money. Make sure the // source account has authority to deposit to the destination. - if (reqDepositAuth) + if (depositAuth) { // If depositPreauth is enabled, then an account that requires // authorization has three ways to get an XRP Payment in: @@ -567,17 +584,17 @@ 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 (dstAccountID != account_) + + // Get the base reserve. + XRPAmount const dstReserve{view().fees().accountReserve(0)}; + + if (dstAmount > dstReserve || + sleDst->getFieldAmount(sfBalance) > dstReserve) { - if (!view().exists(keylet::depositPreauth(dstAccountID, account_))) - { - // Get the base reserve. - XRPAmount const dstReserve{view().fees().accountReserve(0)}; - - if (dstAmount > dstReserve || - sleDst->getFieldAmount(sfBalance) > dstReserve) - return tecNO_PERMISSION; - } + if (auto err = + verifyDepositPreauth(ctx_, account_, dstAccountID, sleDst); + !isTesSuccess(err)) + return err; } } diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 7ea024ee6dc..052a735a2fd 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -226,9 +227,9 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (balance < feePaid) { - JLOG(ctx.j.trace()) - << "Insufficient balance:" << " balance=" << to_string(balance) - << " paid=" << to_string(feePaid); + JLOG(ctx.j.trace()) << "Insufficient balance:" + << " balance=" << to_string(balance) + << " paid=" << to_string(feePaid); if ((balance > beast::zero) && !ctx.view.open()) { @@ -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))) + credentials::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 44c25cb22ef..b3c711084dc 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include diff --git a/src/xrpld/net/detail/RPCCall.cpp b/src/xrpld/net/detail/RPCCall.cpp index c0d2e205434..b92f4b1a205 100644 --- a/src/xrpld/net/detail/RPCCall.cpp +++ b/src/xrpld/net/detail/RPCCall.cpp @@ -416,7 +416,8 @@ class RPCParser return jvRequest; } - // deposit_authorized [] + // 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 0e9481bf540..af204eaedf7 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -33,7 +33,9 @@ #include #include #include + #include + #include namespace ripple { @@ -929,13 +931,14 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 24> + static constexpr std::array, 25> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, {jss::amm, ltAMM}, {jss::bridge, ltBRIDGE}, {jss::check, ltCHECK}, + {jss::credential, ltCREDENTIAL}, {jss::deposit_preauth, ltDEPOSIT_PREAUTH}, {jss::did, ltDID}, {jss::directory, ltDIR_NODE}, diff --git a/src/xrpld/rpc/handlers/DepositAuthorized.cpp b/src/xrpld/rpc/handlers/DepositAuthorized.cpp index 0efa584625b..50aa9ef2898 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,23 +90,111 @@ 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) && (srcAcct != dstAcct); + bool const credentialsPresent = params.isMember(jss::credentials); + + std::set> sorted; + std::vector> lifeExtender; + if (credentialsPresent) { - // 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) + { + return RPC::make_error( + rpcINVALID_PARAMS, + RPC::expected_field_message( + jss::credentials, + "is non-empty array of CredentialID(hash256)")); + } + else if (creds.size() > maxCredentialsArraySize) + { + return RPC::make_error( + rpcINVALID_PARAMS, + RPC::expected_field_message( + jss::credentials, "array too long")); + } + + lifeExtender.reserve(creds.size()); + for (auto const& jo : creds) { - // See if a preauthorization entry is in the ledger. - auto const sleDepositAuth = - ledger->read(keylet::depositPreauth(dstAcct, srcAcct)); - depositAuthorized = static_cast(sleDepositAuth); + 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) + { + RPC::inject_error( + rpcBAD_CREDENTIALS, "credentials don't exist", result); + return result; + } + + if (!(sleCred->getFlags() & lsfAccepted)) + { + RPC::inject_error( + rpcBAD_CREDENTIALS, "credentials aren't accepted", result); + return result; + } + + if (credentials::checkExpired( + sleCred, ledger->info().parentCloseTime)) + { + RPC::inject_error( + rpcBAD_CREDENTIALS, "credentials are expired", result); + return result; + } + + if ((*sleCred)[sfSubject] != srcAcct) + { + RPC::inject_error( + rpcBAD_CREDENTIALS, + "credentials doesn't belong to the root account", + result); + return result; + } + + auto [it, ins] = sorted.emplace( + (*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]); + if (!ins) + { + RPC::inject_error( + rpcBAD_CREDENTIALS, "duplicates in credentials", result); + return result; + } + lifeExtender.push_back(std::move(sleCred)); } } + + // If the two accounts are the same OR if that flag is + // not set, then the deposit should be fine. + bool depositAuthorized = true; + if (reqAuth) + depositAuthorized = + ledger->exists(keylet::depositPreauth(dstAcct, srcAcct)) || + (credentialsPresent && + ledger->exists(keylet::depositPreauth(dstAcct, sorted))); + result[jss::source_account] = params[jss::source_account].asString(); result[jss::destination_account] = params[jss::destination_account].asString(); + if (credentialsPresent) + result[jss::credentials] = params[jss::credentials]; result[jss::deposit_authorized] = depositAuthorized; return result; diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index b8937c528eb..6a3b7a48686 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -34,6 +35,31 @@ namespace ripple { +static STArray +parseAuthorizeCredentials(Json::Value const& jv) +{ + STArray arr(sfAuthorizeCredentials, jv.size()); + 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() || + credentialType->size() > maxCredentialTypeLength) + return {}; + + auto credential = STObject::makeInnerObject(sfCredential); + credential.setAccountID(sfIssuer, *issuer); + credential.setFieldVL(sfCredentialType, *credentialType); + arr.push_back(std::move(credential)); + } + + return arr; +} + // { // ledger_hash : // ledger_index : @@ -84,44 +110,63 @@ 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::authorized_credentials)) || + (dp.isMember(jss::authorized) && !dp[jss::authorized].isString()) || + (dp.isMember(jss::authorized_credentials) && !dp[jss::authorized_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::authorized_credentials]); + STArray const arr = parseAuthorizeCredentials(ac); + + if (arr.empty() || (arr.size() > maxCredentialsArraySize)) + jvResult[jss::error] = "malformedAuthorizedCredentials"; + else + { + auto sorted = credentials::makeSorted(arr); + if (sorted.empty()) + jvResult[jss::error] = + "malformedAuthorizedCredentials"; + else + uNodeIndex = + keylet::depositPreauth(*owner, sorted).key; + } + } } } else if (context.params.isMember(jss::directory)) @@ -644,6 +689,52 @@ 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.isString()) + { + if (!uNodeIndex.parseHex(cred.asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + } + else 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 subject = + parseBase58(cred[jss::subject].asString()); + auto const issuer = + parseBase58(cred[jss::issuer].asString()); + auto const credType = + strUnHex(cred[jss::credential_type].asString()); + if (!subject || subject->isZero() || !issuer || + issuer->isZero() || !credType || credType->empty()) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + uNodeIndex = keylet::credential( + *subject, + *issuer, + Slice(credType->data(), credType->size())) + .key; + } + } + } else if (context.params.isMember(jss::mpt_issuance)) { expectedType = ltMPTOKEN_ISSUANCE;