diff --git a/src/etl/LoadBalancer.hpp b/src/etl/LoadBalancer.hpp index 158e03caa..aa7e70c29 100644 --- a/src/etl/LoadBalancer.hpp +++ b/src/etl/LoadBalancer.hpp @@ -24,6 +24,7 @@ #include "etl/NetworkValidatedLedgersInterface.hpp" #include "etl/Source.hpp" #include "feed/SubscriptionManagerInterface.hpp" +#include "rpc/Errors.hpp" #include "util/Mutex.hpp" #include "util/ResponseExpirationCache.hpp" #include "util/config/Config.hpp" diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index ee8a66d93..557e843d6 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources( Factories.cpp AMMHelpers.cpp RPCHelpers.cpp + CredentialHelpers.cpp Counters.cpp WorkQueue.cpp common/Specs.cpp diff --git a/src/rpc/CredentialHelpers.cpp b/src/rpc/CredentialHelpers.cpp new file mode 100644 index 000000000..adff73acf --- /dev/null +++ b/src/rpc/CredentialHelpers.cpp @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and 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 "data/BackendInterface.hpp" +#include "rpc/Errors.hpp" +#include "rpc/JS.hpp" +#include "rpc/common/Types.hpp" +#include "util/Assert.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rpc::credentials { + +bool +checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger) +{ + if (sleCred.isFieldPresent(ripple::sfExpiration)) { + std::uint32_t const exp = sleCred.getFieldU32(ripple::sfExpiration); + std::uint32_t const now = ledger.parentCloseTime.time_since_epoch().count(); + return now > exp; + } + return false; +} + +std::set> +createAuthCredentials(ripple::STArray const& in) +{ + std::set> out; + for (auto const& cred : in) + out.insert({cred[ripple::sfIssuer], cred[ripple::sfCredentialType]}); + + return out; +} + +ripple::STArray +parseAuthorizeCredentials(boost::json::array const& jv) +{ + ripple::STArray arr; + for (auto const& jo : jv) { + ASSERT( + jo.at(JS(issuer)).is_string(), + "issuer must be string, should already be checked in AuthorizeCredentialValidator" + ); + auto const issuer = + ripple::parseBase58(static_cast(jo.at(JS(issuer)).as_string())); + ASSERT( + issuer.has_value(), "issuer must be present, should already be checked in AuthorizeCredentialValidator." + ); + + ASSERT( + jo.at(JS(credential_type)).is_string(), + "credential_type must be string, should already be checked in AuthorizeCredentialValidator" + ); + auto const credentialType = ripple::strUnHex(static_cast(jo.at(JS(credential_type)).as_string())); + ASSERT( + credentialType.has_value(), + "credential_type must be present, should already be checked in AuthorizeCredentialValidator." + ); + + auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential); + credential.setAccountID(ripple::sfIssuer, *issuer); + credential.setFieldVL(ripple::sfCredentialType, *credentialType); + arr.push_back(std::move(credential)); + } + + return arr; +} + +std::expected +fetchCredentialArray( + std::optional const& credID, + ripple::AccountID const& srcAcc, + BackendInterface const& backend, + ripple::LedgerHeader const& info, + boost::asio::yield_context const& yield +) +{ + ripple::STArray authCreds; + std::unordered_set elems; + for (auto const& elem : credID.value()) { + ASSERT(elem.is_string(), "should already be checked in validators.hpp that elem is a string."); + + if (elems.contains(elem.as_string())) + return Error{Status{RippledError::rpcBAD_CREDENTIALS, "duplicates in credentials."}}; + elems.insert(elem.as_string()); + + ripple::uint256 credHash; + ASSERT( + credHash.parseHex(boost::json::value_to(elem)), + "should already be checked in validators.hpp that elem is a uint256 hex" + ); + + auto const credKeylet = ripple::keylet::credential(credHash).key; + auto const credLedgerObject = backend.fetchLedgerObject(credKeylet, info.seq, yield); + if (!credLedgerObject) + return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't exist."}}; + + auto credIt = ripple::SerialIter{credLedgerObject->data(), credLedgerObject->size()}; + auto const sleCred = ripple::SLE{credIt, credKeylet}; + + if ((sleCred.getType() != ripple::ltCREDENTIAL) || + ((sleCred.getFieldU32(ripple::sfFlags) & ripple::lsfAccepted) == 0u)) + return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials aren't accepted"}}; + + if (credentials::checkExpired(sleCred, info)) + return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials are expired"}}; + + if (sleCred.getAccountID(ripple::sfSubject) != srcAcc) + return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't belong to the root account"}}; + + auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential); + credential.setAccountID(ripple::sfIssuer, sleCred.getAccountID(ripple::sfIssuer)); + credential.setFieldVL(ripple::sfCredentialType, sleCred.getFieldVL(ripple::sfCredentialType)); + authCreds.push_back(std::move(credential)); + } + + return authCreds; +} + +} // namespace rpc::credentials diff --git a/src/rpc/CredentialHelpers.hpp b/src/rpc/CredentialHelpers.hpp new file mode 100644 index 000000000..d4b84bb37 --- /dev/null +++ b/src/rpc/CredentialHelpers.hpp @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and 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 "data/BackendInterface.hpp" +#include "rpc/Errors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace rpc::credentials { + +/** + * @brief Check if credential is expired + * + * @param sleCred The credential to check + * @param ledger The ledger to check the closed time of + * @return true if credential not expired, false otherwise + */ +bool +checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger); + +/** + * @brief Creates authentication credential field (which is a set of pairs of AccountID and Credential ID) + * + * @param in The array of Credential objects to check + * @return Auth Credential array + */ +std::set> +createAuthCredentials(ripple::STArray const& in); + +/** + * @brief Parses each credential object and makes sure the credential type and values are correct + * + * @param jv The boost json array of credentials to parse + * @return Array of credentials after parsing + */ +ripple::STArray +parseAuthorizeCredentials(boost::json::array const& jv); + +/** + * @brief Get Array of Credential objects + * + * @param credID Array of CredentialID's to parse + * @param srcAcc The Source Account + * @param backend backend interface + * @param info The ledger header + * @param yield The coroutine context + * @return Array of credential objects, error if failed otherwise + */ +std::expected +fetchCredentialArray( + std::optional const& credID, + ripple::AccountID const& srcAcc, + BackendInterface const& backend, + ripple::LedgerHeader const& info, + boost::asio::yield_context const& yield +); + +} // namespace rpc::credentials diff --git a/src/rpc/Errors.cpp b/src/rpc/Errors.cpp index 622c7ecab..b63d77b43 100644 --- a/src/rpc/Errors.cpp +++ b/src/rpc/Errors.cpp @@ -83,6 +83,9 @@ getErrorInfo(ClioError code) {ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."}, {ClioError::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."}, {ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."}, + {ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, + "malformedAuthorizedCredentials", + "Malformed authorized credentials."}, // special system errors {ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."}, {ClioError::rpcCOMMAND_IS_MISSING, JS(missingCommand), "Method is not specified or is not a string."}, diff --git a/src/rpc/Errors.hpp b/src/rpc/Errors.hpp index 7ddbd1479..e177b6304 100644 --- a/src/rpc/Errors.hpp +++ b/src/rpc/Errors.hpp @@ -43,6 +43,7 @@ enum class ClioError { rpcUNKNOWN_OPTION = 5005, rpcFIELD_NOT_FOUND_TRANSACTION = 5006, rpcMALFORMED_ORACLE_DOCUMENT_ID = 5007, + rpcMALFORMED_AUTHORIZED_CREDENTIALS = 5008, // special system errors start with 6000 rpcINVALID_API_VERSION = 6000, diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index d2ab1d380..cfd875c37 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -29,8 +29,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -253,4 +255,67 @@ CustomValidator CustomValidators::CurrencyIssueValidator = return MaybeError{}; }}; +CustomValidator CustomValidators::CredentialTypeValidator = + CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { + if (not value.is_string()) + return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + " NotString"}}; + + auto const& credTypeHex = ripple::strViewUnHex(value.as_string()); + if (!credTypeHex.has_value()) + return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " NotHexString"}}; + + if (credTypeHex->empty()) + return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " is empty"}}; + + if (credTypeHex->size() > ripple::maxCredentialTypeLength) + return Error{ + Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " greater than max length"} + }; + + return MaybeError{}; + }}; + +CustomValidator CustomValidators::AuthorizeCredentialValidator = + CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { + if (not value.is_array()) + return Error{Status{ClioError::rpcMALFORMED_REQUEST, std::string(key) + " not array"}}; + + auto const& authCred = value.as_array(); + if (authCred.size() == 0) { + return Error{Status{ + ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, + fmt::format("Requires at least one element in authorized_credentials array") + }}; + } + + if (authCred.size() > ripple::maxCredentialsArraySize) { + return Error{Status{ + ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, + fmt::format( + "Max {} number of credentials in authorized_credentials array", ripple::maxCredentialsArraySize + ) + }}; + } + + for (auto const& credObj : value.as_array()) { + auto const& obj = credObj.as_object(); + + if (!obj.contains("issuer")) + return Error{Status{ClioError::rpcMALFORMED_REQUEST, "Field 'Issuer' is required but missing."}}; + + if (auto const err = IssuerValidator.verify(credObj, "issuer"); !err) + return err; + + if (!obj.contains("credential_type")) { + return Error{Status{ClioError::rpcMALFORMED_REQUEST, "Field 'CredentialType' is required but missing."} + }; + } + + if (auto const err = CredentialTypeValidator.verify(credObj, "credential_type"); !err) + return err; + } + + return MaybeError{}; + }}; + } // namespace rpc::validation diff --git a/src/rpc/common/Validators.hpp b/src/rpc/common/Validators.hpp index c0a039f43..36e499ada 100644 --- a/src/rpc/common/Validators.hpp +++ b/src/rpc/common/Validators.hpp @@ -27,15 +27,13 @@ #include #include #include +#include #include #include -#include #include #include #include -#include -#include #include #include #include @@ -153,7 +151,7 @@ struct Type final { verify(boost::json::value const& value, std::string_view key) const { if (not value.is_object() or not value.as_object().contains(key.data())) - return {}; // ignore. field does not exist, let 'required' fail instead + return {}; // ignore. If field is supposed to exist, let 'required' fail instead auto const& res = value.as_object().at(key.data()); auto const convertible = (checkType(res) || ...); @@ -559,6 +557,51 @@ struct CustomValidators final { * Used by amm_info. */ static CustomValidator CurrencyIssueValidator; + + /** + * @brief Provides a validator for validating authorized_credentials json array. + * + * Used by deposit_preauth. + */ + static CustomValidator AuthorizeCredentialValidator; + + /** + * @brief Provides a validator for validating credential_type. + * + * Used by AuthorizeCredentialValidator in deposit_preauth. + */ + static CustomValidator CredentialTypeValidator; +}; + +/** + * @brief Validates that the elements of the array is of type Hex256 uint + */ +struct Hex256ItemType final { + /** + * @brief Validates given the prerequisite that the type of the json value is an array, + * verifies all values within the array is of uint256 hash + * + * @param value the value to verify + * @param key The key used to retrieve the tested value from the outer object + * @return `RippledError::rpcINVALID_PARAMS` if validation failed; otherwise no error is returned + */ + [[nodiscard]] static MaybeError + verify(boost::json::value const& value, std::string_view key) + { + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. If field is supposed to exist, let 'required' fail instead + + auto const& res = value.as_object().at(key.data()); + + // loop through each item in the array and make sure it is uint256 hex string + for (auto const& elem : res.as_array()) { + ripple::uint256 num; + if (!elem.is_string() || !num.parseHex(elem.as_string())) { + return Error{Status{RippledError::rpcINVALID_PARAMS, "Item is not a valid uint256 type."}}; + } + } + return {}; + } }; } // namespace rpc::validation diff --git a/src/rpc/handlers/DepositAuthorized.cpp b/src/rpc/handlers/DepositAuthorized.cpp index 63f60bba1..fc3f24c14 100644 --- a/src/rpc/handlers/DepositAuthorized.cpp +++ b/src/rpc/handlers/DepositAuthorized.cpp @@ -19,25 +19,34 @@ #include "rpc/handlers/DepositAuthorized.hpp" +#include "rpc/CredentialHelpers.hpp" #include "rpc/Errors.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" +#include "util/Assert.hpp" +#include #include #include #include #include +#include +#include #include #include #include #include +#include #include #include +#include #include #include +#include #include +#include #include namespace rpc { @@ -71,26 +80,55 @@ DepositAuthorizedHandler::process(DepositAuthorizedHandler::Input input, Context Output response; + auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()}; + auto const sleDest = ripple::SLE{it, dstKeylet}; + bool const reqAuth = sleDest.isFlag(ripple::lsfDepositAuth) && (sourceAccountID != destinationAccountID); + auto const& creds = input.credentials; + bool const credentialsPresent = creds.has_value(); + + ripple::STArray authCreds; + if (credentialsPresent) { + if (creds.value().empty()) { + return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array has no elements."}}; + } + if (creds.value().size() > ripple::maxCredentialsArraySize) { + return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array too long."}}; + } + auto const credArray = credentials::fetchCredentialArray( + input.credentials, *sourceAccountID, *sharedPtrBackend_, lgrInfo, ctx.yield + ); + if (!credArray.has_value()) + return Error{std::move(credArray).error()}; + authCreds = std::move(credArray).value(); + } + + // 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) { + ripple::uint256 hashKey; + if (credentialsPresent) { + auto const sortedAuthCreds = credentials::createAuthCredentials(authCreds); + ASSERT( + sortedAuthCreds.size() == authCreds.size(), "should already be checked above that there is no duplicate" + ); + + hashKey = ripple::keylet::depositPreauth(*destinationAccountID, sortedAuthCreds).key; + } else { + hashKey = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID).key; + } + + depositAuthorized = sharedPtrBackend_->fetchLedgerObject(hashKey, lgrInfo.seq, ctx.yield).has_value(); + } + response.sourceAccount = input.sourceAccount; response.destinationAccount = input.destinationAccount; response.ledgerHash = ripple::strHex(lgrInfo.hash); response.ledgerIndex = lgrInfo.seq; - - // If the two accounts are the same, then the deposit should be fine. - if (sourceAccountID != destinationAccountID) { - auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()}; - auto sle = ripple::SLE{it, dstKeylet}; - - // Check destination for the DepositAuth flag. - // If that flag is not set then a deposit should be just fine. - if ((sle.getFieldU32(ripple::sfFlags) & ripple::lsfDepositAuth) != 0u) { - // See if a preauthorization entry is in the ledger. - auto const depositPreauthKeylet = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID); - auto const sleDepositAuth = - sharedPtrBackend_->fetchLedgerObject(depositPreauthKeylet.key, lgrInfo.seq, ctx.yield); - response.depositAuthorized = static_cast(sleDepositAuth); - } - } + response.depositAuthorized = depositAuthorized; + if (credentialsPresent) + response.credentials = input.credentials.value(); return response; } @@ -115,6 +153,10 @@ tag_invoke(boost::json::value_to_tag, boost::js } } + if (jsonObject.contains(JS(credentials))) { + input.credentials = boost::json::value_to(jv.at(JS(credentials))); + } + return input; } @@ -127,8 +169,10 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, DepositAuthorize {JS(destination_account), output.destinationAccount}, {JS(ledger_hash), output.ledgerHash}, {JS(ledger_index), output.ledgerIndex}, - {JS(validated), output.validated}, + {JS(validated), output.validated} }; + if (output.credentials) + jv.as_object()[JS(credentials)] = *output.credentials; } } // namespace rpc diff --git a/src/rpc/handlers/DepositAuthorized.hpp b/src/rpc/handlers/DepositAuthorized.hpp index a71b8144a..5383962a3 100644 --- a/src/rpc/handlers/DepositAuthorized.hpp +++ b/src/rpc/handlers/DepositAuthorized.hpp @@ -25,8 +25,10 @@ #include "rpc/common/Types.hpp" #include "rpc/common/Validators.hpp" +#include #include #include +#include #include #include @@ -59,6 +61,8 @@ class DepositAuthorizedHandler { std::string destinationAccount; std::string ledgerHash; uint32_t ledgerIndex{}; + std::optional credentials; + // validated should be sent via framework bool validated = true; }; @@ -71,6 +75,7 @@ class DepositAuthorizedHandler { std::string destinationAccount; std::optional ledgerHash; std::optional ledgerIndex; + std::optional credentials; }; using Result = HandlerReturnType; @@ -99,6 +104,7 @@ class DepositAuthorizedHandler { {JS(destination_account), validation::Required{}, validation::CustomValidators::AccountValidator}, {JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator}, {JS(ledger_index), validation::CustomValidators::LedgerIndexValidator}, + {JS(credentials), validation::Type{}, validation::Hex256ItemType()} }; return rpcSpec; diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 4f5ea1b1c..50675b4f1 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -19,6 +19,7 @@ #include "rpc/handlers/LedgerEntry.hpp" +#include "rpc/CredentialHelpers.hpp" #include "rpc/Errors.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" @@ -30,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -38,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -97,11 +101,30 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) auto const owner = util::parseBase58Wrapper( boost::json::value_to(input.depositPreauth->at(JS(owner))) ); - auto const authorized = util::parseBase58Wrapper( - boost::json::value_to(input.depositPreauth->at(JS(authorized))) - ); + // Only one of authorize or authorized_credentials MUST exist; + if (input.depositPreauth->contains(JS(authorized)) == + input.depositPreauth->contains(JS(authorized_credentials))) { + return Error{ + Status{ClioError::rpcMALFORMED_REQUEST, "Must have one of authorized or authorized_credentials."} + }; + } + + if (input.depositPreauth->contains(JS(authorized))) { + auto const authorized = util::parseBase58Wrapper( + boost::json::value_to(input.depositPreauth->at(JS(authorized))) + ); + key = ripple::keylet::depositPreauth(*owner, *authorized).key; + } else { + auto const authorizedCredentials = rpc::credentials::parseAuthorizeCredentials( + input.depositPreauth->at(JS(authorized_credentials)).as_array() + ); + + auto const authCreds = credentials::createAuthCredentials(authorizedCredentials); + if (authCreds.size() != authorizedCredentials.size()) + return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "duplicates in credentials."}}; - key = ripple::keylet::depositPreauth(*owner, *authorized).key; + key = ripple::keylet::depositPreauth(owner.value(), authCreds).key; + } } else if (input.ticket) { auto const id = util::parseBase58Wrapper(boost::json::value_to(input.ticket->at(JS(account)) @@ -145,6 +168,8 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) } } else if (input.oracleNode) { key = input.oracleNode.value(); + } else if (input.credential) { + key = input.credential.value(); } else if (input.mptIssuance) { auto const mptIssuanceID = ripple::uint192{std::string_view(*(input.mptIssuance))}; key = ripple::keylet::mptIssuance(mptIssuanceID).key; @@ -287,6 +312,7 @@ tag_invoke(boost::json::value_to_tag, boost::json::va {JS(xchain_owned_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, {JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID}, {JS(oracle), ripple::ltORACLE}, + {JS(credential), ripple::ltCREDENTIAL}, {JS(mptoken), ripple::ltMPTOKEN}, }; @@ -313,6 +339,16 @@ tag_invoke(boost::json::value_to_tag, boost::json::va return ripple::keylet::oracle(*account, documentId).key; }; + auto const parseCredentialFromJson = [](boost::json::value const& json) { + auto const subject = + util::parseBase58Wrapper(boost::json::value_to(json.at(JS(subject)))); + auto const issuer = + util::parseBase58Wrapper(boost::json::value_to(json.at(JS(issuer)))); + auto const credType = ripple::strUnHex(boost::json::value_to(json.at(JS(credential_type)))); + + return ripple::keylet::credential(*subject, *issuer, ripple::Slice(credType->data(), credType->size())).key; + }; + auto const indexFieldType = std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& pair) { auto const& [field, _] = pair; @@ -361,6 +397,8 @@ tag_invoke(boost::json::value_to_tag, boost::json::va ); } else if (jsonObject.contains(JS(oracle))) { input.oracleNode = parseOracleFromJson(jv.at(JS(oracle))); + } else if (jsonObject.contains(JS(credential))) { + input.credential = parseCredentialFromJson(jv.at(JS(credential))); } else if (jsonObject.contains(JS(mptoken))) { input.mptoken = jv.at(JS(mptoken)).as_object(); } diff --git a/src/rpc/handlers/LedgerEntry.hpp b/src/rpc/handlers/LedgerEntry.hpp index dfb7991c7..87980b758 100644 --- a/src/rpc/handlers/LedgerEntry.hpp +++ b/src/rpc/handlers/LedgerEntry.hpp @@ -30,6 +30,7 @@ #include "rpc/common/Validators.hpp" #include "util/AccountUtils.hpp" +#include #include #include #include @@ -107,6 +108,7 @@ class LedgerEntryHandler { std::optional chainClaimId; std::optional createAccountClaimId; std::optional oracleNode; + std::optional credential; bool includeDeleted = false; }; @@ -197,7 +199,8 @@ class LedgerEntryHandler { meta::WithCustomError{ validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_OWNER) }}, - {JS(authorized), validation::Required{}, validation::CustomValidators::AccountBase58Validator}, + {JS(authorized), validation::CustomValidators::AccountBase58Validator}, + {JS(authorized_credentials), validation::CustomValidators::AuthorizeCredentialValidator} }, }}, {JS(directory), @@ -318,6 +321,30 @@ class LedgerEntryHandler { }, meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}}, }}}, + {JS(credential), + meta::WithCustomError{ + validation::Type{}, Status(ClioError::rpcMALFORMED_REQUEST) + }, + meta::IfType{ + meta::WithCustomError{malformedRequestHexStringValidator, Status(ClioError::rpcMALFORMED_ADDRESS)} + }, + meta::IfType{meta::Section{ + {JS(subject), + meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + meta::WithCustomError{ + validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS) + }}, + {JS(issuer), + meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + meta::WithCustomError{ + validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS) + }}, + { + JS(credential_type), + meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + meta::WithCustomError{validation::Type{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + }, + }}}, {JS(mpt_issuance), meta::WithCustomError{ validation::CustomValidators::Uint192HexStringValidator, Status(ClioError::rpcMALFORMED_REQUEST) diff --git a/src/util/AccountUtils.hpp b/src/util/AccountUtils.hpp index ac9ede78d..ce4ea9f98 100644 --- a/src/util/AccountUtils.hpp +++ b/src/util/AccountUtils.hpp @@ -19,6 +19,8 @@ #pragma once +#include +#include #include #include diff --git a/src/util/LedgerUtils.hpp b/src/util/LedgerUtils.hpp index dbc3afad9..1a4b3ff04 100644 --- a/src/util/LedgerUtils.hpp +++ b/src/util/LedgerUtils.hpp @@ -112,6 +112,7 @@ class LedgerTypes { ), LedgerTypeAttribute::AccountOwnedLedgerType(JS(did), ripple::ltDID), LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE), + LedgerTypeAttribute::AccountOwnedLedgerType(JS(credential), ripple::ltCREDENTIAL), LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL), LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE), LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN), diff --git a/src/web/impl/ErrorHandling.hpp b/src/web/impl/ErrorHandling.hpp index 464e9bdc9..b78876335 100644 --- a/src/web/impl/ErrorHandling.hpp +++ b/src/web/impl/ErrorHandling.hpp @@ -90,6 +90,7 @@ class ErrorHelper { case rpc::ClioError::rpcINVALID_HOT_WALLET: case rpc::ClioError::rpcFIELD_NOT_FOUND_TRANSACTION: case rpc::ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID: + case rpc::ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS: case rpc::ClioError::etlCONNECTION_ERROR: case rpc::ClioError::etlREQUEST_ERROR: case rpc::ClioError::etlREQUEST_TIMEOUT: diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index 2f9e8bae7..8186d8e6e 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -48,6 +49,7 @@ #include #include +#include #include #include #include @@ -540,7 +542,7 @@ CreateCheckLedgerObject(std::string_view account, std::string_view dest) } ripple::STObject -CreateDepositPreauthLedgerObject(std::string_view account, std::string_view auth) +CreateDepositPreauthLedgerObjectByAuth(std::string_view account, std::string_view auth) { ripple::STObject depositPreauth(ripple::sfLedgerEntry); depositPreauth.setFieldU16(ripple::sfLedgerEntryType, ripple::ltDEPOSIT_PREAUTH); @@ -553,6 +555,27 @@ CreateDepositPreauthLedgerObject(std::string_view account, std::string_view auth return depositPreauth; } +ripple::STObject +CreateDepositPreauthLedgerObjectByAuthCredentials( + std::string_view account, + std::string_view issuer, + std::string_view credType +) +{ + ripple::STObject depositPreauth(ripple::sfLedgerEntry); + depositPreauth.setFieldU16(ripple::sfLedgerEntryType, ripple::ltDEPOSIT_PREAUTH); + depositPreauth.setAccountID(ripple::sfAccount, GetAccountIDWithString(account)); + depositPreauth.setFieldArray( + ripple::sfAuthorizeCredentials, + CreateAuthCredentialArray(std::vector{issuer}, std::vector{credType}) + ); + depositPreauth.setFieldU32(ripple::sfFlags, 0); + depositPreauth.setFieldU64(ripple::sfOwnerNode, 0); + depositPreauth.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + depositPreauth.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + return depositPreauth; +} + data::NFT CreateNFT(std::string_view tokenID, std::string_view account, ripple::LedgerIndex seq, ripple::Blob uri, bool isBurned) { @@ -1192,3 +1215,47 @@ CreateOracleObject( return ledgerObject; } + +// acc2 issue credential for acc1 so acc2 is issuer +ripple::STObject +CreateCredentialObject( + std::string_view acc1, + std::string_view acc2, + std::string_view credType, + bool accept, + std::optional expiration +) +{ + ripple::STObject credObj(ripple::sfCredential); + credObj.setFieldU16(ripple::sfLedgerEntryType, ripple::ltCREDENTIAL); + credObj.setFieldVL(ripple::sfCredentialType, ripple::Blob{credType.begin(), credType.end()}); + credObj.setAccountID(ripple::sfSubject, GetAccountIDWithString(acc1)); + credObj.setAccountID(ripple::sfIssuer, GetAccountIDWithString(acc2)); + if (expiration.has_value()) + credObj.setFieldU32(ripple::sfExpiration, expiration.value()); + + if (accept) { + credObj.setFieldU32(ripple::sfFlags, ripple::lsfAccepted); + } else { + credObj.setFieldU32(ripple::sfFlags, 0); + } + credObj.setFieldU64(ripple::sfSubjectNode, 0); + credObj.setFieldU64(ripple::sfIssuerNode, 0); + credObj.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + credObj.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + return credObj; +} + +ripple::STArray +CreateAuthCredentialArray(std::vector issuer, std::vector credType) +{ + ripple::STArray arr; + ASSERT(issuer.size() == credType.size(), "issuer and credtype vector must be same length"); + for (std::size_t i = 0; i < issuer.size(); ++i) { + auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential); + credential.setAccountID(ripple::sfIssuer, GetAccountIDWithString(issuer[i])); + credential.setFieldVL(ripple::sfCredentialType, ripple::strUnHex(std::string(credType[i])).value()); + arr.push_back(credential); + } + return arr; +} diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index bf9bc107e..1669e93f2 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -22,6 +22,7 @@ #include "data/Types.hpp" #include +#include #include #include #include @@ -268,7 +269,14 @@ CreateEscrowLedgerObject(std::string_view account, std::string_view dest); CreateCheckLedgerObject(std::string_view account, std::string_view dest); [[nodiscard]] ripple::STObject -CreateDepositPreauthLedgerObject(std::string_view account, std::string_view auth); +CreateDepositPreauthLedgerObjectByAuth(std::string_view account, std::string_view auth); + +[[nodiscard]] ripple::STObject +CreateDepositPreauthLedgerObjectByAuthCredentials( + std::string_view account, + std::string_view issuer, + std::string_view credType +); [[nodiscard]] data::NFT CreateNFT( @@ -435,3 +443,15 @@ CreateOracleSetTxWithMetadata( bool created, std::string_view previousTxnId ); + +[[nodiscard]] ripple::STObject +CreateCredentialObject( + std::string_view acc1, + std::string_view acc2, + std::string_view credType, + bool accept = true, + std::optional expiration = std::nullopt +); + +[[nodiscard]] ripple::STArray +CreateAuthCredentialArray(std::vector issuer, std::vector credType); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 69ac7ba1e..5509d895f 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -67,6 +67,7 @@ target_sources( rpc/handlers/AMMInfoTests.cpp rpc/handlers/BookChangesTests.cpp rpc/handlers/BookOffersTests.cpp + rpc/handlers/CredentialHelpersTests.cpp rpc/handlers/DefaultProcessorTests.cpp rpc/handlers/DepositAuthorizedTests.cpp rpc/handlers/FeatureTests.cpp diff --git a/tests/unit/rpc/RPCHelpersTests.cpp b/tests/unit/rpc/RPCHelpersTests.cpp index faaffb308..0b99a5dc1 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -30,10 +30,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -47,6 +49,7 @@ #include #include #include +#include #include #include #include diff --git a/tests/unit/rpc/handlers/CredentialHelpersTests.cpp b/tests/unit/rpc/handlers/CredentialHelpersTests.cpp new file mode 100644 index 000000000..347d3376b --- /dev/null +++ b/tests/unit/rpc/handlers/CredentialHelpersTests.cpp @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and 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 "rpc/CredentialHelpers.hpp" +#include "rpc/Errors.hpp" +#include "rpc/JS.hpp" +#include "util/AsioContextTestFixture.hpp" +#include "util/MockBackendTestFixture.hpp" +#include "util/MockPrometheus.hpp" +#include "util/TestObject.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace rpc; +using namespace testing; + +constexpr static auto Account = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto Account2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto Index1 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; +constexpr static auto CredentialID = "c7a14f6b9d5d4a9cb9c223a61b8e5c7df58e8b7ad1c6b4f8e7a321fa4e5b4c9d"; +constexpr static std::string_view CredentialType = "credType"; + +TEST(CreateAuthCredentialsTest, UniqueCredentials) +{ + ripple::STArray credentials; + auto const cred1 = CreateCredentialObject(Account, Account2, CredentialType); + auto const cred2 = CreateCredentialObject(Account2, Account, CredentialType); + + credentials.push_back(cred1); + credentials.push_back(cred2); + + auto const result = credentials::createAuthCredentials(credentials); + + // Validate that the result contains the correct set of credentials + ASSERT_EQ(result.size(), 2); + + auto const cred1Type = cred1.getFieldVL(ripple::sfCredentialType); + auto const cred2Type = cred2.getFieldVL(ripple::sfCredentialType); + + auto const expected_cred1 = + std::make_pair(cred1.getAccountID(ripple::sfIssuer), ripple::Slice{cred1Type.data(), cred1Type.size()}); + auto const expected_cred2 = + std::make_pair(cred2.getAccountID(ripple::sfIssuer), ripple::Slice{cred2Type.data(), cred2Type.size()}); + + EXPECT_TRUE(result.count(expected_cred1)); + EXPECT_TRUE(result.count(expected_cred2)); +} + +TEST(ParseAuthorizeCredentialsTest, ValidCredentialsArray) +{ + boost::json::array credentials; + boost::json::object credential1; + credential1[JS(issuer)] = Account; + credential1[JS(credential_type)] = ripple::strHex(CredentialType); + + credentials.push_back(credential1); + ripple::STArray const parsedCredentials = credentials::parseAuthorizeCredentials(credentials); + + ASSERT_EQ(parsedCredentials.size(), 1); + + ripple::STObject const& cred = parsedCredentials[0]; + ASSERT_TRUE(cred.isFieldPresent(ripple::sfIssuer)); + ASSERT_TRUE(cred.isFieldPresent(ripple::sfCredentialType)); + + auto const expectedIssuer = + *ripple::parseBase58(static_cast(credential1[JS(issuer)].as_string())); + auto const expectedCredentialType = + ripple::strUnHex(static_cast(credential1[JS(credential_type)].as_string())).value(); + + EXPECT_EQ(cred.getAccountID(ripple::sfIssuer), expectedIssuer); + EXPECT_EQ(cred.getFieldVL(ripple::sfCredentialType), expectedCredentialType); +} + +class CredentialHelperTest : public util::prometheus::WithPrometheus, + public MockBackendTest, + public SyncAsioContextTest {}; + +TEST_F(CredentialHelperTest, GetInvalidCredentialArray) +{ + boost::json::array credentialsArray = {CredentialID}; + auto const info = CreateLedgerHeader(Index1, 30); + + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto const ret = + credentials::fetchCredentialArray(credentialsArray, GetAccountIDWithString(Account), *backend, info, yield); + ASSERT_FALSE(ret.has_value()); + auto const status = ret.error(); + EXPECT_EQ(status, RippledError::rpcBAD_CREDENTIALS); + EXPECT_EQ(status.message, "credentials don't exist."); + }); + ctx.run(); +} + +TEST_F(CredentialHelperTest, GetValidCredentialArray) +{ + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(Index1, 30); + auto const credLedgerObject = CreateCredentialObject(Account, Account2, CredentialType, true); + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(credLedgerObject.getSerializer().peekData())); + EXPECT_CALL(*backend, doFetchLedgerObject).Times(1); + + boost::json::array credentialsArray = {CredentialID}; + + ripple::STArray expectedAuthCreds; + ripple::STObject credential(ripple::sfCredential); + credential.setAccountID(ripple::sfIssuer, GetAccountIDWithString(Account2)); + credential.setFieldVL(ripple::sfCredentialType, ripple::Blob{std::begin(CredentialType), std::end(CredentialType)}); + expectedAuthCreds.push_back(std::move(credential)); + + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto const result = credentials::fetchCredentialArray( + credentialsArray, GetAccountIDWithString(Account), *backend, ledgerHeader, yield + ); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), expectedAuthCreds); + }); + ctx.run(); +} diff --git a/tests/unit/rpc/handlers/DepositAuthorizedTests.cpp b/tests/unit/rpc/handlers/DepositAuthorizedTests.cpp index 0ea5aba42..fc478a5ab 100644 --- a/tests/unit/rpc/handlers/DepositAuthorizedTests.cpp +++ b/tests/unit/rpc/handlers/DepositAuthorizedTests.cpp @@ -28,24 +28,34 @@ #include #include +#include #include #include +#include #include +#include +#include #include #include +#include +#include #include +#include #include +#include #include -constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; -constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; -constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; -constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; -constexpr static auto INDEX2 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B1"; +constexpr static auto Account = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto Account2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto Index1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr static auto Index2 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B1"; +constexpr static std::string_view CredentialType = "credType"; +constexpr static auto CredentialHash = "F245428267E6177AEEFDD4FEA3533285712A4B1091CF82A7EA7BC39A62C3FB1A"; -constexpr static auto RANGEMIN = 10; -constexpr static auto RANGEMAX = 30; +constexpr static auto RangeMin = 10; +constexpr static auto RangeMax = 30; using namespace rpc; namespace json = boost::json; @@ -156,6 +166,38 @@ generateTestValuesForParametersTest() "invalidParams", "ledgerIndexMalformed", }, + { + "CredentialsNotArray", + R"({ + "source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "credentials": "x" + })", + "invalidParams", + "Invalid parameters.", + }, + { + "CredentialsNotStringsInArray", + R"({ + "source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "credentials": [123] + })", + "invalidParams", + "Item is not a valid uint256 type.", + }, + { + "CredentialsNotHexedStringInArray", + R"({ + "source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "credentials": ["234", "432"] + })", + "invalidParams", + "Item is not a valid uint256 type.", + } }; } @@ -184,10 +226,10 @@ TEST_P(DepositAuthorizedParameterTest, InvalidParams) TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaIntSequence) { - backend->setRange(RANGEMIN, RANGEMAX); + backend->setRange(RangeMin, RangeMax); EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt)); + ON_CALL(*backend, fetchLedgerBySequence(RangeMax, _)).WillByDefault(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; @@ -197,9 +239,9 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaIntSequence) "destination_account": "{}", "ledger_index": {} }})", - ACCOUNT, - ACCOUNT2, - RANGEMAX + Account, + Account2, + RangeMax )); auto const output = handler.process(req, Context{yield}); @@ -213,10 +255,10 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaIntSequence) TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaStringSequence) { - backend->setRange(RANGEMIN, RANGEMAX); + backend->setRange(RangeMin, RangeMax); EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1); - ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt)); + ON_CALL(*backend, fetchLedgerBySequence(RangeMax, _)).WillByDefault(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; @@ -226,9 +268,9 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaStringSequence) "destination_account": "{}", "ledger_index": "{}" }})", - ACCOUNT, - ACCOUNT2, - RANGEMAX + Account, + Account2, + RangeMax )); auto const output = handler.process(req, Context{yield}); @@ -242,10 +284,10 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaStringSequence) TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaHash) { - backend->setRange(RANGEMIN, RANGEMAX); + backend->setRange(RangeMin, RangeMax); EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(std::nullopt)); + ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; @@ -255,9 +297,9 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaHash) "destination_account": "{}", "ledger_hash": "{}" }})", - ACCOUNT, - ACCOUNT2, - LEDGERHASH + Account, + Account2, + LedgerHash )); auto const output = handler.process(req, Context{yield}); @@ -273,9 +315,9 @@ TEST_F(RPCDepositAuthorizedTest, SourceAccountDoesNotExist) { backend->setRange(10, 30); - auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30); + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader)); + ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(std::optional{})); @@ -287,9 +329,9 @@ TEST_F(RPCDepositAuthorizedTest, SourceAccountDoesNotExist) "destination_account": "{}", "ledger_hash": "{}" }})", - ACCOUNT, - ACCOUNT2, - LEDGERHASH + Account, + Account2, + LedgerHash )); runSpawn([&, this](auto yield) { @@ -308,14 +350,14 @@ TEST_F(RPCDepositAuthorizedTest, DestinationAccountDoesNotExist) { backend->setRange(10, 30); - auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30); + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader)); + ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); - auto const accountRoot = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); + auto const accountRoot = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(accountRoot.getSerializer().peekData())); - ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _)) + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) .WillByDefault(Return(std::optional{})); EXPECT_CALL(*backend, doFetchLedgerObject).Times(2); @@ -326,9 +368,9 @@ TEST_F(RPCDepositAuthorizedTest, DestinationAccountDoesNotExist) "destination_account": "{}", "ledger_hash": "{}" }})", - ACCOUNT, - ACCOUNT2, - LEDGERHASH + Account, + Account2, + LedgerHash )); runSpawn([&, this](auto yield) { @@ -357,12 +399,12 @@ TEST_F(RPCDepositAuthorizedTest, AccountsAreEqual) backend->setRange(10, 30); - auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30); + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader)); + ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); - auto const accountRoot = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); + auto const accountRoot = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(accountRoot.getSerializer().peekData())); EXPECT_CALL(*backend, doFetchLedgerObject).Times(2); @@ -372,9 +414,9 @@ TEST_F(RPCDepositAuthorizedTest, AccountsAreEqual) "destination_account": "{}", "ledger_hash": "{}" }})", - ACCOUNT, - ACCOUNT, - LEDGERHASH + Account, + Account, + LedgerHash )); runSpawn([&, this](auto yield) { @@ -400,17 +442,17 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsNoDepositAuthFlag) backend->setRange(10, 30); - auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30); + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader)); + ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); - auto const account1Root = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); - auto const account2Root = CreateAccountRootObject(ACCOUNT2, 0, 2, 200, 2, INDEX2, 2); + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, 0, 2, 200, 2, Index2, 2); - ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, _, _)) + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) .WillByDefault(Return(account1Root.getSerializer().peekData())); - ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _)) + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) .WillByDefault(Return(account2Root.getSerializer().peekData())); EXPECT_CALL(*backend, doFetchLedgerObject).Times(2); @@ -420,9 +462,9 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsNoDepositAuthFlag) "destination_account": "{}", "ledger_hash": "{}" }})", - ACCOUNT, - ACCOUNT2, - LEDGERHASH + Account, + Account2, + LedgerHash )); runSpawn([&, this](auto yield) { @@ -448,18 +490,18 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsFals backend->setRange(10, 30); - auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30); + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader)); + ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); - auto const account1Root = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); - auto const account2Root = CreateAccountRootObject(ACCOUNT2, ripple::lsfDepositAuth, 2, 200, 2, INDEX2, 2); + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::nullopt)); - ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, _, _)) + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) .WillByDefault(Return(account1Root.getSerializer().peekData())); - ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _)) + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) .WillByDefault(Return(account2Root.getSerializer().peekData())); EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); @@ -469,9 +511,9 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsFals "destination_account": "{}", "ledger_hash": "{}" }})", - ACCOUNT, - ACCOUNT2, - LEDGERHASH + Account, + Account2, + LedgerHash )); runSpawn([&, this](auto yield) { @@ -497,18 +539,18 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsTrue backend->setRange(10, 30); - auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30); + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); - ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader)); + ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*backend, fetchLedgerByHash).Times(1); - auto const account1Root = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); - auto const account2Root = CreateAccountRootObject(ACCOUNT2, ripple::lsfDepositAuth, 2, 200, 2, INDEX2, 2); + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); - ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, _, _)) + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) .WillByDefault(Return(account1Root.getSerializer().peekData())); - ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _)) + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) .WillByDefault(Return(account2Root.getSerializer().peekData())); EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); @@ -518,9 +560,9 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsTrue "destination_account": "{}", "ledger_hash": "{}" }})", - ACCOUNT, - ACCOUNT2, - LEDGERHASH + Account, + Account2, + LedgerHash )); runSpawn([&, this](auto yield) { @@ -531,3 +573,380 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsTrue EXPECT_EQ(*output.result, json::parse(expectedOut)); }); } + +TEST_F(RPCDepositAuthorizedTest, CredentialAcceptedAndNotExpiredReturnsTrue) +{ + static auto const expectedOut = fmt::format( + R"({{ + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": 30, + "validated": true, + "deposit_authorized": true, + "source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "destination_account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "credentials": ["{}"] + }})", + CredentialHash // CREDENTIALHASH should match credentialIndex + ); + + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); + + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader)); + + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); + auto const credential = CreateCredentialObject(Account, Account2, CredentialType); + auto const credentialIndex = ripple::keylet::credential( + GetAccountIDWithString(Account), + GetAccountIDWithString(Account2), + ripple::Slice(CredentialType.data(), CredentialType.size()) + ) + .key; + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) + .WillByDefault(Return(account1Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) + .WillByDefault(Return(account2Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _)) + .WillByDefault(Return(credential.getSerializer().peekData())); + EXPECT_CALL(*backend, doFetchLedgerObject).Times(4); + + auto const input = json::parse(fmt::format( + R"({{ + "source_account": "{}", + "destination_account": "{}", + "ledger_hash": "{}", + "credentials": ["{}"] + }})", + Account, + Account2, + LedgerHash, + ripple::strHex(credentialIndex) + )); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; + auto const output = handler.process(input, Context{yield}); + + ASSERT_TRUE(output); + EXPECT_EQ(*output.result, json::parse(expectedOut)); + }); +} + +TEST_F(RPCDepositAuthorizedTest, CredentialNotAuthorizedReturnsFalse) +{ + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); + + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader)); + + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); + auto const credential = CreateCredentialObject(Account, Account2, CredentialType, false); + auto const credentialIndex = ripple::keylet::credential( + GetAccountIDWithString(Account), + GetAccountIDWithString(Account2), + ripple::Slice(CredentialType.data(), CredentialType.size()) + ) + .key; + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) + .WillByDefault(Return(account1Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) + .WillByDefault(Return(account2Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _)) + .WillByDefault(Return(credential.getSerializer().peekData())); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); + + auto const input = json::parse(fmt::format( + R"({{ + "source_account": "{}", + "destination_account": "{}", + "ledger_hash": "{}", + "credentials": ["{}"] + }})", + Account, + Account2, + LedgerHash, + ripple::strHex(credentialIndex) + )); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; + auto const output = handler.process(input, Context{yield}); + + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "badCredentials"); + EXPECT_EQ(err.at("error_message").as_string(), "credentials aren't accepted"); + }); +} + +TEST_F(RPCDepositAuthorizedTest, CredentialExpiredReturnsFalse) +{ + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 100); + + // set parent close time to 500 seconds + ledgerHeader.parentCloseTime = ripple::NetClock::time_point{std::chrono::seconds{500}}; + + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader)); + + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); + + // credential expire time is 23 seconds, so credential will fail + auto const expiredCredential = CreateCredentialObject(Account, Account2, CredentialType, true, 23); + + auto const credentialIndex = ripple::keylet::credential( + GetAccountIDWithString(Account), + GetAccountIDWithString(Account2), + ripple::Slice(CredentialType.data(), CredentialType.size()) + ) + .key; + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) + .WillByDefault(Return(account1Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) + .WillByDefault(Return(account2Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _)) + .WillByDefault(Return(expiredCredential.getSerializer().peekData())); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); + + auto const input = json::parse(fmt::format( + R"({{ + "source_account": "{}", + "destination_account": "{}", + "ledger_hash": "{}", + "credentials": ["{}"] + }})", + Account, + Account2, + LedgerHash, + ripple::strHex(credentialIndex) + )); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; + auto const output = handler.process(input, Context{yield}); + + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "badCredentials"); + EXPECT_EQ(err.at("error_message").as_string(), "credentials are expired"); + }); +} + +TEST_F(RPCDepositAuthorizedTest, DuplicateCredentialsReturnsFalse) +{ + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 34); + + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader)); + + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); + auto const credential = CreateCredentialObject(Account, Account2, CredentialType); + auto const credentialIndex = ripple::keylet::credential( + GetAccountIDWithString(Account), + GetAccountIDWithString(Account2), + ripple::Slice(CredentialType.data(), CredentialType.size()) + ) + .key; + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) + .WillByDefault(Return(account1Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) + .WillByDefault(Return(account2Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _)) + .WillByDefault(Return(credential.getSerializer().peekData())); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); + + auto const input = json::parse(fmt::format( + R"({{ + "source_account": "{}", + "destination_account": "{}", + "ledger_hash": "{}", + "credentials": ["{}", "{}"] + }})", + Account, + Account2, + LedgerHash, + ripple::strHex(credentialIndex), + ripple::strHex(credentialIndex) + )); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; + auto const output = handler.process(input, Context{yield}); + + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "badCredentials"); + EXPECT_EQ(err.at("error_message").as_string(), "duplicates in credentials."); + }); +} + +TEST_F(RPCDepositAuthorizedTest, NoElementsInCredentialsReturnsFalse) +{ + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 34); + + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader)); + + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) + .WillByDefault(Return(account1Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) + .WillByDefault(Return(account2Root.getSerializer().peekData())); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(2); + + auto const input = json::parse(fmt::format( + R"({{ + "source_account": "{}", + "destination_account": "{}", + "ledger_hash": "{}", + "credentials": [] + }})", + Account, + Account2, + LedgerHash + )); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; + auto const output = handler.process(input, Context{yield}); + + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "credential array has no elements."); + }); +} + +TEST_F(RPCDepositAuthorizedTest, MoreThanMaxNumberOfCredentialsReturnsFalse) +{ + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 34); + + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader)); + + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); + auto const credential = CreateCredentialObject(Account, Account2, CredentialType); + auto const credentialIndex = ripple::keylet::credential( + GetAccountIDWithString(Account), + GetAccountIDWithString(Account2), + ripple::Slice(CredentialType.data(), CredentialType.size()) + ) + .key; + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) + .WillByDefault(Return(account1Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) + .WillByDefault(Return(account2Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _)) + .WillByDefault(Return(credential.getSerializer().peekData())); + + EXPECT_CALL(*backend, doFetchLedgerObject).Times(2); + + std::vector credentials(9, ripple::strHex(credentialIndex)); + + auto const input = json::parse(fmt::format( + R"({{ + "source_account": "{}", + "destination_account": "{}", + "ledger_hash": "{}", + "credentials": [{}] + }})", + Account, + Account2, + LedgerHash, + fmt::join( + credentials | std::views::transform([](std::string const& cred) { return fmt::format("\"{}\"", cred); }), + ", " + ) + )); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; + auto const output = handler.process(input, Context{yield}); + + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "credential array too long."); + }); +} + +TEST_F(RPCDepositAuthorizedTest, DifferenSubjectAccountForCredentialReturnsFalse) +{ + backend->setRange(10, 30); + + auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30); + + EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader)); + + auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2); + auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2); + + // reverse the subject and issuer account. Now subject is ACCOUNT2 + auto const credential = CreateCredentialObject(Account2, Account, CredentialType); + auto const credentialIndex = ripple::keylet::credential( + GetAccountIDWithString(Account2), + GetAccountIDWithString(Account), + ripple::Slice(CredentialType.data(), CredentialType.size()) + ) + .key; + + ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional{{1, 2, 3}})); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _)) + .WillByDefault(Return(account1Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _)) + .WillByDefault(Return(account2Root.getSerializer().peekData())); + ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _)) + .WillByDefault(Return(credential.getSerializer().peekData())); + EXPECT_CALL(*backend, doFetchLedgerObject).Times(3); + + auto const input = json::parse(fmt::format( + R"({{ + "source_account": "{}", + "destination_account": "{}", + "ledger_hash": "{}", + "credentials": ["{}"] + }})", + Account, + Account2, + LedgerHash, + ripple::strHex(credentialIndex) + )); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{DepositAuthorizedHandler{backend}}; + auto const output = handler.process(input, Context{yield}); + + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "badCredentials"); + EXPECT_EQ(err.at("error_message").as_string(), "credentials don't belong to the root account"); + }); +} diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index 26b7101f5..f8528cef9 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -18,6 +18,7 @@ //============================================================================== #include "data/Types.hpp" +#include "rpc/CredentialHelpers.hpp" #include "rpc/Errors.hpp" #include "rpc/common/AnyHandler.hpp" #include "rpc/common/Types.hpp" @@ -29,6 +30,8 @@ #include #include #include +#include +#include #include #include #include @@ -36,6 +39,8 @@ #include #include #include +#include +#include #include #include #include @@ -49,6 +54,7 @@ #include #include #include +#include #include #include @@ -66,6 +72,7 @@ constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A constexpr static auto TOKENID = "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA"; constexpr static auto NFTID = "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004"; constexpr static auto TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; +constexpr static auto CREDENTIALTYPE = "4B5943"; class RPCLedgerEntryTest : public HandlerBaseTest {}; @@ -200,6 +207,206 @@ generateTestValuesForParametersTest() "authorizedNotString" }, + ParamTestCaseBundle{ + "InvalidDepositPreauthJsonAuthorizeCredentialsNotArray", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": "asdf" + }} + }})", + ACCOUNT + ), + "malformedRequest", + "authorized_credentials not array" + }, + + ParamTestCaseBundle{ + "DepositPreauthBothAuthAndAuthCredentialsDoesNotExists", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}" + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Must have one of authorized or authorized_credentials." + }, + + ParamTestCaseBundle{ + "DepositPreauthBothAuthAndAuthCredentialsExists", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized": "{}", + "authorized_credentials": [ + {{ + "issuer": "{}", + "credential_type": "{}" + }} + ] + }} + }})", + ACCOUNT, + ACCOUNT2, + ACCOUNT3, + CREDENTIALTYPE + ), + "malformedRequest", + "Must have one of authorized or authorized_credentials." + }, + + ParamTestCaseBundle{ + "DepositPreauthEmptyAuthorizeCredentials", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + ] + }} + }})", + ACCOUNT + ), + "malformedAuthorizedCredentials", + "Requires at least one element in authorized_credentials array" + }, + + ParamTestCaseBundle{ + "DepositPreauthAuthorizeCredentialsMissingCredentialType", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + {{ + "issuer": "{}" + }} + ] + }} + }})", + ACCOUNT, + ACCOUNT2 + ), + "malformedRequest", + "Field 'CredentialType' is required but missing." + }, + + ParamTestCaseBundle{ + "DepositPreauthAuthorizeCredentialsMissingIssuer", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + {{ + "credential_type": "{}" + }} + ] + }} + }})", + ACCOUNT, + CREDENTIALTYPE + ), + "malformedRequest", + "Field 'Issuer' is required but missing." + }, + + ParamTestCaseBundle{ + "DepositPreauthAuthorizeCredentialsIncorrectCredentialType", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + {{ + "issuer": "{}", + "credential_type": 432 + }} + ] + }} + }})", + ACCOUNT, + ACCOUNT2 + ), + "invalidParams", + "credential_type NotString" + }, + + ParamTestCaseBundle{ + "DepositPreauthAuthorizeCredentialsCredentialTypeNotHex", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + {{ + "issuer": "{}", + "credential_type": "hello world" + }} + ] + }} + }})", + ACCOUNT, + ACCOUNT2 + ), + "malformedAuthorizedCredentials", + "credential_type NotHexString" + }, + + ParamTestCaseBundle{ + "DepositPreauthAuthorizeCredentialsCredentialTypeEmpty", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + {{ + "issuer": "{}", + "credential_type": "" + }} + ] + }} + }})", + ACCOUNT, + ACCOUNT2 + ), + "malformedAuthorizedCredentials", + "credential_type is empty" + }, + + ParamTestCaseBundle{ + "DepositPreauthDuplicateAuthorizeCredentials", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + {{ + "issuer": "{}", + "credential_type": "{}" + }}, + {{ + "issuer": "{}", + "credential_type": "{}" + }} + ] + }} + }})", + ACCOUNT, + ACCOUNT2, + CREDENTIALTYPE, + ACCOUNT2, + CREDENTIALTYPE + ), + "malformedAuthorizedCredentials", + "duplicates in credentials." + }, + ParamTestCaseBundle{ "InvalidTicketType", R"({ @@ -1759,6 +1966,29 @@ generateTestValuesForParametersTest() "malformedRequest", "Malformed request." }, + ParamTestCaseBundle{ + "CredentialInvalidSubjectType", + R"({ + "credential": { + "subject": 123 + } + })", + "malformedAddress", + "Malformed address." + }, + ParamTestCaseBundle{ + "CredentialInvalidIssuerType", + fmt::format( + R"({{ + "credential": {{ + "issuer": ["{}"] + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request." + }, ParamTestCaseBundle{ "InvalidMPTIssuanceStringIndex", R"({ @@ -1806,6 +2036,37 @@ generateTestValuesForParametersTest() "malformedRequest", "Malformed request." }, + ParamTestCaseBundle{ + "CredentialInvalidCredentialType", + fmt::format( + R"({{ + "credential": {{ + "subject": "{}", + "issuer": "{}", + "credential_type": 1234 + }} + }})", + ACCOUNT, + ACCOUNT2 + ), + "malformedRequest", + "Malformed request." + }, + ParamTestCaseBundle{ + "CredentialMissingIssuerField", + fmt::format( + R"({{ + "credential": {{ + "subject": "{}", + "credential_type": "1234" + }} + }})", + ACCOUNT, + ACCOUNT2 + ), + "malformedRequest", + "Malformed request." + }, ParamTestCaseBundle{ "InvalidMPTokenAccount", fmt::format( @@ -1828,7 +2089,7 @@ generateTestValuesForParametersTest() ), "malformedRequest", "Malformed request." - }, + } }; } @@ -2068,7 +2329,7 @@ generateTestValuesForNormalPathTest() INDEX1 ), ripple::uint256{INDEX1}, - CreateDepositPreauthLedgerObject(ACCOUNT, ACCOUNT2) + CreateDepositPreauthLedgerObjectByAuth(ACCOUNT, ACCOUNT2) }, NormalPathTestBundle{ "AccountRoot", @@ -2155,7 +2416,7 @@ generateTestValuesForNormalPathTest() CreateEscrowLedgerObject(ACCOUNT, ACCOUNT2) }, NormalPathTestBundle{ - "DepositPreauth", + "DepositPreauthByAuth", fmt::format( R"({{ "binary": true, @@ -2168,7 +2429,58 @@ generateTestValuesForNormalPathTest() ACCOUNT2 ), ripple::keylet::depositPreauth(account1, account2).key, - CreateDepositPreauthLedgerObject(ACCOUNT, ACCOUNT2) + CreateDepositPreauthLedgerObjectByAuth(ACCOUNT, ACCOUNT2) + }, + NormalPathTestBundle{ + "DepositPreauthByAuthCredentials", + fmt::format( + R"({{ + "binary": true, + "deposit_preauth": {{ + "owner": "{}", + "authorized_credentials": [ + {{ + "issuer": "{}", + "credential_type": "{}" + }} + ] + }} + }})", + ACCOUNT, + ACCOUNT2, + CREDENTIALTYPE + ), + ripple::keylet::depositPreauth( + account1, + credentials::createAuthCredentials(CreateAuthCredentialArray( + std::vector{ACCOUNT2}, std::vector{CREDENTIALTYPE} + )) + ) + .key, + CreateDepositPreauthLedgerObjectByAuthCredentials(ACCOUNT, ACCOUNT2, CREDENTIALTYPE) + }, + NormalPathTestBundle{ + "Credentials", + fmt::format( + R"({{ + "binary": true, + "credential": {{ + "subject": "{}", + "issuer": "{}", + "credential_type": "{}" + }} + }})", + ACCOUNT, + ACCOUNT2, + CREDENTIALTYPE + ), + ripple::keylet::credential( + account1, + account2, + ripple::Slice(ripple::strUnHex(CREDENTIALTYPE)->data(), ripple::strUnHex(CREDENTIALTYPE)->size()) + ) + .key, + CreateCredentialObject(ACCOUNT, ACCOUNT2, CREDENTIALTYPE) }, NormalPathTestBundle{ "RippleState", diff --git a/tests/unit/util/LedgerUtilsTests.cpp b/tests/unit/util/LedgerUtilsTests.cpp index 60ac92260..45daccd4b 100644 --- a/tests/unit/util/LedgerUtilsTests.cpp +++ b/tests/unit/util/LedgerUtilsTests.cpp @@ -55,6 +55,7 @@ TEST(LedgerUtilsTests, LedgerObjectTypeList) JS(mpt_issuance), JS(mptoken), JS(oracle), + JS(credential), JS(nunl) }; @@ -86,6 +87,7 @@ TEST(LedgerUtilsTests, AccountOwnedTypeList) JS(xchain_owned_create_account_claim_id), JS(did), JS(oracle), + JS(credential), JS(mpt_issuance), JS(mptoken) };