From 07f118caec0c5825168d541661529126847f833a Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 9 Jan 2025 16:32:32 -0500 Subject: [PATCH 1/2] chore: update deprecated Github Actions (#5241) --- .github/workflows/clang-format.yml | 96 +++++++++++++++--------------- .github/workflows/levelization.yml | 72 +++++++++++----------- .github/workflows/nix.yml | 15 ++--- 3 files changed, 90 insertions(+), 93 deletions(-) diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 91bbff48c09..1b86eed736d 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -8,52 +8,52 @@ jobs: env: CLANG_VERSION: 18 steps: - - uses: actions/checkout@v4 - - name: Install clang-format - run: | - codename=$( lsb_release --codename --short ) - sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null </dev/null < Date: Fri, 10 Jan 2025 12:44:14 -0500 Subject: [PATCH 2/2] Permissioned Domains (XLS-80d) (#5161) --- include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/Indexes.h | 5 + include/xrpl/protocol/Protocol.h | 4 + include/xrpl/protocol/detail/features.macro | 2 +- .../xrpl/protocol/detail/ledger_entries.macro | 13 + include/xrpl/protocol/detail/sfields.macro | 2 + .../xrpl/protocol/detail/transactions.macro | 10 + include/xrpl/protocol/jss.h | 1 + src/libxrpl/protocol/Indexes.cpp | 15 + src/test/app/DepositAuth_test.cpp | 14 +- src/test/app/PermissionedDomains_test.cpp | 569 ++++++++++++++++++ src/test/jtx.h | 1 + src/test/jtx/Account.h | 6 +- src/test/jtx/Env.h | 7 + src/test/jtx/deposit.h | 3 + src/test/jtx/fee.h | 3 +- src/test/jtx/impl/Env.cpp | 12 +- src/test/jtx/impl/TestHelpers.cpp | 5 +- src/test/jtx/impl/permissioned_domains.cpp | 181 ++++++ src/test/jtx/permissioned_domains.h | 71 +++ src/test/ledger/Invariants_test.cpp | 255 ++++++++ src/test/rpc/AccountObjects_test.cpp | 61 +- src/test/rpc/LedgerRPC_test.cpp | 129 ++++ src/xrpld/app/misc/CredentialHelpers.cpp | 51 +- src/xrpld/app/misc/CredentialHelpers.h | 11 +- src/xrpld/app/tx/detail/DepositPreauth.cpp | 49 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 104 ++++ src/xrpld/app/tx/detail/InvariantCheck.h | 40 +- .../tx/detail/PermissionedDomainDelete.cpp | 90 +++ .../app/tx/detail/PermissionedDomainDelete.h | 46 ++ .../app/tx/detail/PermissionedDomainSet.cpp | 149 +++++ .../app/tx/detail/PermissionedDomainSet.h | 45 ++ src/xrpld/app/tx/detail/applySteps.cpp | 2 + src/xrpld/rpc/handlers/AccountObjects.cpp | 3 +- src/xrpld/rpc/handlers/LedgerEntry.cpp | 108 +++- 35 files changed, 1961 insertions(+), 108 deletions(-) create mode 100644 src/test/app/PermissionedDomains_test.cpp create mode 100644 src/test/jtx/impl/permissioned_domains.cpp create mode 100644 src/test/jtx/permissioned_domains.h create mode 100644 src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp create mode 100644 src/xrpld/app/tx/detail/PermissionedDomainDelete.h create mode 100644 src/xrpld/app/tx/detail/PermissionedDomainSet.cpp create mode 100644 src/xrpld/app/tx/detail/PermissionedDomainSet.h diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 8821d531ff0..369ec3304ef 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 = 84; +static constexpr std::size_t numFeatures = 85; /** 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/Indexes.h b/include/xrpl/protocol/Indexes.h index 3ce6ef8e836..bbed5395927 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -330,6 +330,11 @@ mptoken(uint256 const& mptokenKey) Keylet mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; +Keylet +permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; + +Keylet +permissionedDomain(uint256 const& domainID) noexcept; } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 68134b4a5fe..81a45c383fb 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -105,6 +105,10 @@ std::size_t constexpr maxCredentialTypeLength = 64; /** The maximum number of credentials can be passed in array */ std::size_t constexpr maxCredentialsArraySize = 8; +/** The maximum number of credentials can be passed in array for permissioned + * domain */ +std::size_t constexpr maxPermissionedDomainCredentialsArraySize = 10; + /** The maximum length of MPTokenMetadata */ std::size_t constexpr maxMPTokenMetadataLength = 1024; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 287a1368a72..d90dc327780 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,6 +29,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo) @@ -100,7 +101,6 @@ XRPL_FEATURE(FlowCross, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo) - // 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 86360c4ba0e..5a652baf4f7 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -448,5 +448,18 @@ LEDGER_ENTRY(ltCREDENTIAL, 0x0081, Credential, credential, ({ {sfPreviousTxnLgrSeq, soeREQUIRED}, })) +/** A ledger object which tracks PermissionedDomain + \sa keylet::permissionedDomain + */ +LEDGER_ENTRY(ltPERMISSIONED_DOMAIN, 0x0082, PermissionedDomain, permissioned_domain, ({ + {sfOwner, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfAcceptedCredentials, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE + diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 8384025ee3b..3217bab9134 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -190,6 +190,7 @@ TYPED_SFIELD(sfHookStateKey, UINT256, 30) TYPED_SFIELD(sfHookHash, UINT256, 31) TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) +TYPED_SFIELD(sfDomainID, UINT256, 34) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -375,3 +376,4 @@ UNTYPED_SFIELD(sfPriceDataSeries, ARRAY, 24) UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25) UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) +UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 99e741036aa..dd3ac42325d 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -454,6 +454,16 @@ TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, ({ {sfURI, soeOPTIONAL}, })) +/** This transaction type creates or modifies a Permissioned Domain */ +TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, ({ + {sfDomainID, soeOPTIONAL}, + {sfAcceptedCredentials, soeREQUIRED}, +})) + +/** This transaction type deletes a Permissioned Domain */ +TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, ({ + {sfDomainID, soeREQUIRED}, +})) /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 60184b23874..c41d7ef2594 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -44,6 +44,7 @@ namespace jss { // clang-format off JSS(AL_size); // out: GetCounts JSS(AL_hit_rate); // out: GetCounts +JSS(AcceptedCredentials); // out: AccountObjects JSS(Account); // in: TransactionSign; field. JSS(AMMID); // field JSS(Amount); // in: TransactionSign; field. diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index c7f4441c7bc..046be444224 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -78,6 +78,7 @@ enum class LedgerNameSpace : std::uint16_t { MPTOKEN_ISSUANCE = '~', MPTOKEN = 't', CREDENTIAL = 'D', + PERMISSIONED_DOMAIN = 'm', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -527,6 +528,20 @@ credential( indexHash(LedgerNameSpace::CREDENTIAL, subject, issuer, credType)}; } +Keylet +permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept +{ + return { + ltPERMISSIONED_DOMAIN, + indexHash(LedgerNameSpace::PERMISSIONED_DOMAIN, account, seq)}; +} + +Keylet +permissionedDomain(uint256 const& domainID) noexcept +{ + return {ltPERMISSIONED_DOMAIN, domainID}; +} + } // namespace keylet } // namespace ripple diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 0f2481a7c9e..463c606dc61 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -1070,7 +1070,7 @@ struct DepositPreauth_test : public beast::unit_test::suite { // AuthorizeCredentials is empty auto jv = deposit::authCredentials(bob, {}); - env(jv, ter(temMALFORMED)); + env(jv, ter(temARRAY_EMPTY)); } { @@ -1110,7 +1110,7 @@ struct DepositPreauth_test : public beast::unit_test::suite {g, z}, {h, z}, {i, z}}); - env(jv, ter(temMALFORMED)); + env(jv, ter(temARRAY_TOO_LARGE)); } { @@ -1507,12 +1507,14 @@ struct DepositPreauth_test : public beast::unit_test::suite testcase("Check duplicate credentials."); { // check duplicates in depositPreauth params - std::ranges::shuffle(credentials, gen); - for (auto const& c : credentials) + std::vector copyCredentials( + credentials.begin(), credentials.end() - 1); + + std::ranges::shuffle(copyCredentials, gen); + for (auto const& c : copyCredentials) { - auto credentials2 = credentials; + auto credentials2 = copyCredentials; credentials2.push_back(c); - env(deposit::authCredentials(stock, credentials2), ter(temMALFORMED)); } diff --git a/src/test/app/PermissionedDomains_test.cpp b/src/test/app/PermissionedDomains_test.cpp new file mode 100644 index 00000000000..5184c462dac --- /dev/null +++ b/src/test/app/PermissionedDomains_test.cpp @@ -0,0 +1,569 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +namespace ripple { +namespace test { + +using namespace jtx; + +static std::string +exceptionExpected(Env& env, Json::Value const& jv) +{ + try + { + env(jv, ter(temMALFORMED)); + } + catch (std::exception const& ex) + { + return ex.what(); + } + return {}; +} + +class PermissionedDomains_test : public beast::unit_test::suite +{ + FeatureBitset withFeature_{ + supported_amendments() | featurePermissionedDomains}; + FeatureBitset withoutFeature_{supported_amendments()}; + + // Verify that each tx type can execute if the feature is enabled. + void + testEnabled() + { + testcase("Enabled"); + Account const alice("alice"); + Env env(*this, withFeature_); + env.fund(XRP(1000), alice); + pdomain::Credentials credentials{{alice, "first credential"}}; + env(pdomain::setTx(alice, credentials)); + BEAST_EXPECT(env.ownerCount(alice) == 1); + auto objects = pdomain::getObjects(alice, env); + BEAST_EXPECT(objects.size() == 1); + // Test that account_objects is correct without passing it the type + BEAST_EXPECT(objects == pdomain::getObjects(alice, env, false)); + auto const domain = objects.begin()->first; + env(pdomain::deleteTx(alice, domain)); + } + + // Verify that each tx does not execute if feature is disabled + void + testDisabled() + { + testcase("Disabled"); + Account const alice("alice"); + Env env(*this, withoutFeature_); + env.fund(XRP(1000), alice); + pdomain::Credentials credentials{{alice, "first credential"}}; + env(pdomain::setTx(alice, credentials), ter(temDISABLED)); + env(pdomain::deleteTx(alice, uint256(75)), ter(temDISABLED)); + } + + // Verify that bad inputs fail for each of create new and update + // behaviors of PermissionedDomainSet + void + testBadData( + Account const& account, + Env& env, + std::optional domain = std::nullopt) + { + Account const alice2("alice2"); + Account const alice3("alice3"); + Account const alice4("alice4"); + Account const alice5("alice5"); + Account const alice6("alice6"); + Account const alice7("alice7"); + Account const alice8("alice8"); + Account const alice9("alice9"); + Account const alice10("alice10"); + Account const alice11("alice11"); + Account const alice12("alice12"); + auto const setFee(drops(env.current()->fees().increment)); + + // Test empty credentials. + env(pdomain::setTx(account, pdomain::Credentials(), domain), + ter(temARRAY_EMPTY)); + + // Test 11 credentials. + pdomain::Credentials const credentials11{ + {alice2, "credential1"}, + {alice3, "credential2"}, + {alice4, "credential3"}, + {alice5, "credential4"}, + {alice6, "credential5"}, + {alice7, "credential6"}, + {alice8, "credential7"}, + {alice9, "credential8"}, + {alice10, "credential9"}, + {alice11, "credential10"}, + {alice12, "credential11"}}; + BEAST_EXPECT( + credentials11.size() == + maxPermissionedDomainCredentialsArraySize + 1); + env(pdomain::setTx(account, credentials11, domain), + ter(temARRAY_TOO_LARGE)); + + // Test credentials including non-existent issuer. + Account const nobody("nobody"); + pdomain::Credentials const credentialsNon{ + {alice2, "credential1"}, + {alice3, "credential2"}, + {alice4, "credential3"}, + {nobody, "credential4"}, + {alice5, "credential5"}, + {alice6, "credential6"}, + {alice7, "credential7"}}; + env(pdomain::setTx(account, credentialsNon, domain), ter(tecNO_ISSUER)); + + // Test bad fee + env(pdomain::setTx(account, credentials11, domain), + fee(1, true), + ter(temBAD_FEE)); + + pdomain::Credentials const credentials4{ + {alice2, "credential1"}, + {alice3, "credential2"}, + {alice4, "credential3"}, + {alice5, "credential4"}, + }; + auto txJsonMutable = pdomain::setTx(account, credentials4, domain); + auto const credentialOrig = txJsonMutable["AcceptedCredentials"][2u]; + + // Remove Issuer from a credential and apply. + txJsonMutable["AcceptedCredentials"][2u][jss::Credential].removeMember( + jss::Issuer); + BEAST_EXPECT( + exceptionExpected(env, txJsonMutable).starts_with("invalidParams")); + + // Make an empty CredentialType. + txJsonMutable["AcceptedCredentials"][2u] = credentialOrig; + txJsonMutable["AcceptedCredentials"][2u][jss::Credential] + ["CredentialType"] = ""; + env(txJsonMutable, ter(temMALFORMED)); + + // Make too long CredentialType. + constexpr std::string_view longCredentialType = + "Cred0123456789012345678901234567890123456789012345678901234567890"; + static_assert(longCredentialType.size() == maxCredentialTypeLength + 1); + txJsonMutable["AcceptedCredentials"][2u] = credentialOrig; + txJsonMutable["AcceptedCredentials"][2u][jss::Credential] + ["CredentialType"] = std::string(longCredentialType); + BEAST_EXPECT( + exceptionExpected(env, txJsonMutable).starts_with("invalidParams")); + + // Remove Credentialtype from a credential and apply. + txJsonMutable["AcceptedCredentials"][2u][jss::Credential].removeMember( + "CredentialType"); + BEAST_EXPECT( + exceptionExpected(env, txJsonMutable).starts_with("invalidParams")); + + // Remove both + txJsonMutable["AcceptedCredentials"][2u][jss::Credential].removeMember( + jss::Issuer); + BEAST_EXPECT( + exceptionExpected(env, txJsonMutable).starts_with("invalidParams")); + + // Make 2 identical credentials. Duplicates are not supported by + // permissioned domains, so transactions should return errors + { + pdomain::Credentials const credentialsDup{ + {alice7, "credential6"}, + {alice2, "credential1"}, + {alice3, "credential2"}, + {alice2, "credential1"}, + {alice5, "credential4"}, + }; + + std::unordered_map human2Acc; + for (auto const& c : credentialsDup) + human2Acc.emplace(c.issuer.human(), c.issuer); + + auto const sorted = pdomain::sortCredentials(credentialsDup); + BEAST_EXPECT(sorted.size() == 4); + env(pdomain::setTx(account, credentialsDup, domain), + ter(temMALFORMED)); + + env.close(); + env(pdomain::setTx(account, sorted, domain)); + + uint256 d; + if (domain) + d = *domain; + else + d = pdomain::getNewDomain(env.meta()); + env.close(); + auto objects = pdomain::getObjects(account, env); + auto const fromObject = + pdomain::credentialsFromJson(objects[d], human2Acc); + auto const sortedCreds = pdomain::sortCredentials(credentialsDup); + BEAST_EXPECT(fromObject == sortedCreds); + } + + // Have equal issuers but different credentials and make sure they + // sort correctly. + { + pdomain::Credentials const credentialsSame{ + {alice2, "credential3"}, + {alice3, "credential2"}, + {alice2, "credential9"}, + {alice5, "credential4"}, + {alice2, "credential6"}, + }; + std::unordered_map human2Acc; + for (auto const& c : credentialsSame) + human2Acc.emplace(c.issuer.human(), c.issuer); + + BEAST_EXPECT( + credentialsSame != pdomain::sortCredentials(credentialsSame)); + env(pdomain::setTx(account, credentialsSame, domain)); + + uint256 d; + if (domain) + d = *domain; + else + d = pdomain::getNewDomain(env.meta()); + env.close(); + auto objects = pdomain::getObjects(account, env); + auto const fromObject = + pdomain::credentialsFromJson(objects[d], human2Acc); + auto const sortedCreds = pdomain::sortCredentials(credentialsSame); + BEAST_EXPECT(fromObject == sortedCreds); + } + } + + // Test PermissionedDomainSet + void + testSet() + { + testcase("Set"); + Env env(*this, withFeature_); + env.set_parse_failure_expected(true); + + const int accNum = 12; + Account const alice[accNum] = { + "alice", + "alice2", + "alice3", + "alice4", + "alice5", + "alice6", + "alice7", + "alice8", + "alice9", + "alice10", + "alice11", + "alice12"}; + std::unordered_map human2Acc; + for (auto const& c : alice) + human2Acc.emplace(c.human(), c); + + for (int i = 0; i < accNum; ++i) + env.fund(XRP(1000), alice[i]); + + // Create new from existing account with a single credential. + pdomain::Credentials const credentials1{{alice[2], "credential1"}}; + { + env(pdomain::setTx(alice[0], credentials1)); + BEAST_EXPECT(env.ownerCount(alice[0]) == 1); + auto tx = env.tx()->getJson(JsonOptions::none); + BEAST_EXPECT(tx[jss::TransactionType] == "PermissionedDomainSet"); + BEAST_EXPECT(tx["Account"] == alice[0].human()); + auto objects = pdomain::getObjects(alice[0], env); + auto domain = objects.begin()->first; + BEAST_EXPECT(domain.isNonZero()); + auto object = objects.begin()->second; + BEAST_EXPECT(object["LedgerEntryType"] == "PermissionedDomain"); + BEAST_EXPECT(object["Owner"] == alice[0].human()); + BEAST_EXPECT(object["Sequence"] == tx["Sequence"]); + BEAST_EXPECT( + pdomain::credentialsFromJson(object, human2Acc) == + credentials1); + } + + // Make longest possible CredentialType. + { + constexpr std::string_view longCredentialType = + "Cred0123456789012345678901234567890123456789012345678901234567" + "89"; + static_assert(longCredentialType.size() == maxCredentialTypeLength); + pdomain::Credentials const longCredentials{ + {alice[1], std::string(longCredentialType)}}; + + env(pdomain::setTx(alice[0], longCredentials)); + + // One account can create multiple domains + BEAST_EXPECT(env.ownerCount(alice[0]) == 2); + + auto tx = env.tx()->getJson(JsonOptions::none); + BEAST_EXPECT(tx[jss::TransactionType] == "PermissionedDomainSet"); + BEAST_EXPECT(tx["Account"] == alice[0].human()); + + bool findSeq = false; + for (auto const& [domain, object] : + pdomain::getObjects(alice[0], env)) + { + findSeq = object["Sequence"] == tx["Sequence"]; + if (findSeq) + { + BEAST_EXPECT(domain.isNonZero()); + BEAST_EXPECT( + object["LedgerEntryType"] == "PermissionedDomain"); + BEAST_EXPECT(object["Owner"] == alice[0].human()); + BEAST_EXPECT( + pdomain::credentialsFromJson(object, human2Acc) == + longCredentials); + break; + } + } + BEAST_EXPECT(findSeq); + } + + // Create new from existing account with 10 credentials. + // Last credential describe domain owner itself + pdomain::Credentials const credentials10{ + {alice[2], "credential1"}, + {alice[3], "credential2"}, + {alice[4], "credential3"}, + {alice[5], "credential4"}, + {alice[6], "credential5"}, + {alice[7], "credential6"}, + {alice[8], "credential7"}, + {alice[9], "credential8"}, + {alice[10], "credential9"}, + {alice[0], "credential10"}, + }; + uint256 domain2; + { + BEAST_EXPECT( + credentials10.size() == + maxPermissionedDomainCredentialsArraySize); + BEAST_EXPECT( + credentials10 != pdomain::sortCredentials(credentials10)); + env(pdomain::setTx(alice[0], credentials10)); + auto tx = env.tx()->getJson(JsonOptions::none); + domain2 = pdomain::getNewDomain(env.meta()); + auto objects = pdomain::getObjects(alice[0], env); + auto object = objects[domain2]; + BEAST_EXPECT( + pdomain::credentialsFromJson(object, human2Acc) == + pdomain::sortCredentials(credentials10)); + } + + // Update with 1 credential. + env(pdomain::setTx(alice[0], credentials1, domain2)); + BEAST_EXPECT( + pdomain::credentialsFromJson( + pdomain::getObjects(alice[0], env)[domain2], human2Acc) == + credentials1); + + // Update with 10 credentials. + env(pdomain::setTx(alice[0], credentials10, domain2)); + env.close(); + BEAST_EXPECT( + pdomain::credentialsFromJson( + pdomain::getObjects(alice[0], env)[domain2], human2Acc) == + pdomain::sortCredentials(credentials10)); + + // Update from the wrong owner. + env(pdomain::setTx(alice[2], credentials1, domain2), + ter(tecNO_PERMISSION)); + + // Update a uint256(0) domain + env(pdomain::setTx(alice[0], credentials1, uint256(0)), + ter(temMALFORMED)); + + // Update non-existent domain + env(pdomain::setTx(alice[0], credentials1, uint256(75)), + ter(tecNO_ENTRY)); + + // Wrong flag + env(pdomain::setTx(alice[0], credentials1), + txflags(tfClawTwoAssets), + ter(temINVALID_FLAG)); + + // Test bad data when creating a domain. + testBadData(alice[0], env); + // Test bad data when updating a domain. + testBadData(alice[0], env, domain2); + + // Try to delete the account with domains. + auto const acctDelFee(drops(env.current()->fees().increment)); + constexpr std::size_t deleteDelta = 255; + { + // Close enough ledgers to make it potentially deletable if empty. + std::size_t ownerSeq = env.seq(alice[0]); + while (deleteDelta + ownerSeq > env.current()->seq()) + env.close(); + env(acctdelete(alice[0], alice[2]), + fee(acctDelFee), + ter(tecHAS_OBLIGATIONS)); + } + + { + // Delete the domains and then the owner account. + for (auto const& objs : pdomain::getObjects(alice[0], env)) + env(pdomain::deleteTx(alice[0], objs.first)); + env.close(); + std::size_t ownerSeq = env.seq(alice[0]); + while (deleteDelta + ownerSeq > env.current()->seq()) + env.close(); + env(acctdelete(alice[0], alice[2]), fee(acctDelFee)); + } + } + + // Test PermissionedDomainDelete + void + testDelete() + { + testcase("Delete"); + Env env(*this, withFeature_); + Account const alice("alice"); + + env.fund(XRP(1000), alice); + auto const setFee(drops(env.current()->fees().increment)); + + pdomain::Credentials credentials{{alice, "first credential"}}; + env(pdomain::setTx(alice, credentials)); + env.close(); + + auto objects = pdomain::getObjects(alice, env); + BEAST_EXPECT(objects.size() == 1); + auto const domain = objects.begin()->first; + + // Delete a domain that doesn't belong to the account. + Account const bob("bob"); + env.fund(XRP(1000), bob); + env(pdomain::deleteTx(bob, domain), ter(tecNO_PERMISSION)); + + // Delete a non-existent domain. + env(pdomain::deleteTx(alice, uint256(75)), ter(tecNO_ENTRY)); + + // Test bad fee + env(pdomain::deleteTx(alice, uint256(75)), + ter(temBAD_FEE), + fee(1, true)); + + // Wrong flag + env(pdomain::deleteTx(alice, domain), + ter(temINVALID_FLAG), + txflags(tfClawTwoAssets)); + + // Delete a zero domain. + env(pdomain::deleteTx(alice, uint256(0)), ter(temMALFORMED)); + + // Make sure owner count reflects the existing domain. + BEAST_EXPECT(env.ownerCount(alice) == 1); + auto const objID = pdomain::getObjects(alice, env).begin()->first; + BEAST_EXPECT(pdomain::objectExists(objID, env)); + + // Delete domain that belongs to user. + env(pdomain::deleteTx(alice, domain)); + auto const tx = env.tx()->getJson(JsonOptions::none); + BEAST_EXPECT(tx[jss::TransactionType] == "PermissionedDomainDelete"); + + // Make sure the owner count goes back to 0. + BEAST_EXPECT(env.ownerCount(alice) == 0); + + // The object needs to be gone. + BEAST_EXPECT(pdomain::getObjects(alice, env).empty()); + BEAST_EXPECT(!pdomain::objectExists(objID, env)); + } + + void + testAccountReserve() + { + // Verify that the reserve behaves as expected for creating. + testcase("Account Reserve"); + + using namespace test::jtx; + + Env env(*this, withFeature_); + Account const alice("alice"); + + // Fund alice enough to exist, but not enough to meet + // the reserve. + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + env.fund(acctReserve, alice); + env.close(); + BEAST_EXPECT(env.balance(alice) == acctReserve); + BEAST_EXPECT(env.ownerCount(alice) == 0); + + // alice does not have enough XRP to cover the reserve. + pdomain::Credentials credentials{{alice, "first credential"}}; + env(pdomain::setTx(alice, credentials), ter(tecINSUFFICIENT_RESERVE)); + BEAST_EXPECT(env.ownerCount(alice) == 0); + BEAST_EXPECT(pdomain::getObjects(alice, env).size() == 0); + env.close(); + + auto const baseFee = env.current()->fees().base.drops(); + + // Pay alice almost enough to make the reserve. + env(pay(env.master, alice, incReserve + drops(2 * baseFee) - drops(1))); + BEAST_EXPECT( + env.balance(alice) == + acctReserve + incReserve + drops(baseFee) - drops(1)); + env.close(); + + // alice still does not have enough XRP for the reserve. + env(pdomain::setTx(alice, credentials), ter(tecINSUFFICIENT_RESERVE)); + env.close(); + BEAST_EXPECT(env.ownerCount(alice) == 0); + + // Pay alice enough to make the reserve. + env(pay(env.master, alice, drops(baseFee) + drops(1))); + env.close(); + + // Now alice can create a PermissionedDomain. + env(pdomain::setTx(alice, credentials)); + env.close(); + BEAST_EXPECT(env.ownerCount(alice) == 1); + } + +public: + void + run() override + { + testEnabled(); + testDisabled(); + testSet(); + testDelete(); + testAccountReserve(); + } +}; + +BEAST_DEFINE_TESTSUITE(PermissionedDomains, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx.h b/src/test/jtx.h index b7b9a9fa05c..f3c69ce33c3 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Account.h b/src/test/jtx/Account.h index bcf117d9357..f49070dff03 100644 --- a/src/test/jtx/Account.h +++ b/src/test/jtx/Account.h @@ -158,10 +158,10 @@ hash_append(Hasher& h, Account const& v) noexcept hash_append(h, v.id()); } -inline bool -operator<(Account const& lhs, Account const& rhs) noexcept +inline auto +operator<=>(Account const& lhs, Account const& rhs) noexcept { - return lhs.id() < rhs.id(); + return lhs.id() <=> rhs.id(); } } // namespace jtx diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index d90d2bc1228..b3f49ef838b 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -406,6 +406,12 @@ class Env trace_ = 0; } + void + set_parse_failure_expected(bool b) + { + parseFailureExpected_ = b; + } + /** Turn off signature checks. */ void disable_sigs() @@ -693,6 +699,7 @@ class Env TestStopwatch stopwatch_; uint256 txid_; TER ter_ = tesSUCCESS; + bool parseFailureExpected_ = false; Json::Value do_rpc( diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h index 9de3416367c..9bd73d383dd 100644 --- a/src/test/jtx/deposit.h +++ b/src/test/jtx/deposit.h @@ -43,6 +43,9 @@ struct AuthorizeCredentials jtx::Account issuer; std::string credType; + auto + operator<=>(const AuthorizeCredentials&) const = default; + Json::Value toJson() const { diff --git a/src/test/jtx/fee.h b/src/test/jtx/fee.h index c671e0b2a1e..4e29fad1521 100644 --- a/src/test/jtx/fee.h +++ b/src/test/jtx/fee.h @@ -53,7 +53,8 @@ class fee Throw("fee: not XRP"); } - explicit fee(std::uint64_t amount) : fee{STAmount{amount}} + explicit fee(std::uint64_t amount, bool negative = false) + : fee{STAmount{amount, negative}} { } diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index ef5a2124e24..2ae57588589 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -203,6 +203,15 @@ Env::balance(Account const& account, Issue const& issue) const return {amount, lookup(issue.account).name()}; } +std::uint32_t +Env::ownerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfOwnerCount); +} + std::uint32_t Env::seq(Account const& account) const { @@ -503,7 +512,8 @@ Env::autofill(JTx& jt) } catch (parse_error const&) { - test.log << "parse failed:\n" << pretty(jv) << std::endl; + if (!parseFailureExpected_) + test.log << "parse failed:\n" << pretty(jv) << std::endl; Rethrow(); } } diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index b39cac7dcc1..3bf69729ab0 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -53,10 +53,7 @@ checkArraySize(Json::Value const& val, unsigned int 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; + return env.ownerCount(account); } /* Path finding */ diff --git a/src/test/jtx/impl/permissioned_domains.cpp b/src/test/jtx/impl/permissioned_domains.cpp new file mode 100644 index 00000000000..9cdf7c664f2 --- /dev/null +++ b/src/test/jtx/impl/permissioned_domains.cpp @@ -0,0 +1,181 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +namespace test { +namespace jtx { +namespace pdomain { + +// helpers +// Make json for PermissionedDomainSet transaction +Json::Value +setTx( + AccountID const& account, + Credentials const& credentials, + std::optional domain) +{ + Json::Value jv; + jv[sfTransactionType] = jss::PermissionedDomainSet; + jv[sfAccount] = to_string(account); + if (domain) + jv[sfDomainID] = to_string(*domain); + + Json::Value acceptedCredentials(Json::arrayValue); + for (auto const& credential : credentials) + { + Json::Value object(Json::objectValue); + object[sfCredential] = credential.toJson(); + acceptedCredentials.append(std::move(object)); + } + + jv[sfAcceptedCredentials] = acceptedCredentials; + return jv; +} + +// Make json for PermissionedDomainDelete transaction +Json::Value +deleteTx(AccountID const& account, uint256 const& domain) +{ + Json::Value jv{Json::objectValue}; + jv[sfTransactionType] = jss::PermissionedDomainDelete; + jv[sfAccount] = to_string(account); + jv[sfDomainID] = to_string(domain); + return jv; +} + +// Get PermissionedDomain objects by type from account_objects rpc call +std::map +getObjects(Account const& account, Env& env, bool withType) +{ + std::map ret; + Json::Value params; + params[jss::account] = account.human(); + if (withType) + params[jss::type] = jss::permissioned_domain; + + auto const& resp = env.rpc("json", "account_objects", to_string(params)); + Json::Value objects(Json::arrayValue); + objects = resp[jss::result][jss::account_objects]; + for (auto const& object : objects) + { + if (object["LedgerEntryType"] != "PermissionedDomain") + { + if (withType) + { // impossible to get there + Throw( + "Invalid object type: " + + object["LedgerEntryType"].asString()); // LCOV_EXCL_LINE + } + continue; + } + + uint256 index; + std::ignore = index.parseHex(object[jss::index].asString()); + ret[index] = object; + } + + return ret; +} + +// Check if ledger object is there +bool +objectExists(uint256 const& objID, Env& env) +{ + Json::Value params; + params[jss::index] = to_string(objID); + + auto const result = + env.rpc("json", "ledger_entry", to_string(params))["result"]; + + if ((result["status"] == "error") && (result["error"] == "entryNotFound")) + return false; + + if ((result["node"]["LedgerEntryType"] != jss::PermissionedDomain)) + return false; + + if (result["status"] == "success") + return true; + + throw std::runtime_error("Error getting ledger_entry RPC result."); +} + +// Extract credentials from account_object object +Credentials +credentialsFromJson( + Json::Value const& object, + std::unordered_map const& human2Acc) +{ + Credentials ret; + Json::Value credentials(Json::arrayValue); + credentials = object["AcceptedCredentials"]; + for (auto const& credential : credentials) + { + Json::Value obj(Json::objectValue); + obj = credential[jss::Credential]; + auto const& issuer = obj[jss::Issuer]; + auto const& credentialType = obj["CredentialType"]; + auto blob = strUnHex(credentialType.asString()).value(); + ret.push_back( + {human2Acc.at(issuer.asString()), + std::string(blob.begin(), blob.end())}); + } + return ret; +} + +// Sort credentials the same way as PermissionedDomainSet. Silently +// remove duplicates. +Credentials +sortCredentials(Credentials const& input) +{ + std::set credentialsSet; + for (auto const& credential : input) + credentialsSet.insert(credential); + return {credentialsSet.begin(), credentialsSet.end()}; +} + +uint256 +getNewDomain(std::shared_ptr const& meta) +{ + uint256 ret; + auto metaJson = meta->getJson(JsonOptions::none); + Json::Value a(Json::arrayValue); + a = metaJson["AffectedNodes"]; + + for (auto const& node : a) + { + if (!node.isMember("CreatedNode") || + node["CreatedNode"]["LedgerEntryType"] != "PermissionedDomain") + { + continue; + } + std::ignore = + ret.parseHex(node["CreatedNode"]["LedgerIndex"].asString()); + break; + } + + return ret; +} + +} // namespace pdomain +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/permissioned_domains.h b/src/test/jtx/permissioned_domains.h new file mode 100644 index 00000000000..49b420d3d20 --- /dev/null +++ b/src/test/jtx/permissioned_domains.h @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +/* + 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 test { +namespace jtx { +namespace pdomain { + +// Helpers for PermissionedDomains testing +using Credential = ripple::test::jtx::deposit::AuthorizeCredentials; +using Credentials = std::vector; + +// helpers +// Make json for PermissionedDomainSet transaction +Json::Value +setTx( + AccountID const& account, + Credentials const& credentials, + std::optional domain = std::nullopt); + +// Make json for PermissionedDomainDelete transaction +Json::Value +deleteTx(AccountID const& account, uint256 const& domain); + +// Get PermissionedDomain objects from account_objects rpc call +std::map +getObjects(Account const& account, Env& env, bool withType = true); + +// Check if ledger object is there +bool +objectExists(uint256 const& objID, Env& env); + +// Extract credentials from account_object object +Credentials +credentialsFromJson( + Json::Value const& object, + std::unordered_map const& human2Acc); + +// Sort credentials the same way as PermissionedDomainSet +Credentials +sortCredentials(Credentials const& input); + +// Get newly created domain from transaction metadata. +uint256 +getNewDomain(std::shared_ptr const& meta); + +} // namespace pdomain +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 8d7b08fa1ab..993104d6ad8 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -798,6 +798,260 @@ class Invariants_test : public beast::unit_test::suite }); } + void + testPermissionedDomainInvariants() + { + using namespace test::jtx; + + testcase << "PermissionedDomain"; + doInvariantCheck( + {{"permissioned domain with no rules."}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + slePd->setAccountID(sfOwner, A1); + slePd->setFieldU32(sfSequence, 10); + + ac.view().insert(slePd); + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + testcase << "PermissionedDomain 2"; + + auto constexpr tooBig = maxPermissionedDomainCredentialsArraySize + 1; + doInvariantCheck( + {{"permissioned domain bad credentials size " + + std::to_string(tooBig)}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + slePd->setAccountID(sfOwner, A1); + slePd->setFieldU32(sfSequence, 10); + + STArray credentials(sfAcceptedCredentials, tooBig); + for (std::size_t n = 0; n < tooBig; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + auto credType = + std::string("cred_type") + std::to_string(n); + cred.setFieldVL( + sfCredentialType, + Slice(credType.c_str(), credType.size())); + credentials.push_back(std::move(cred)); + } + slePd->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().insert(slePd); + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + testcase << "PermissionedDomain 3"; + doInvariantCheck( + {{"permissioned domain credentials aren't sorted"}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + slePd->setAccountID(sfOwner, A1); + slePd->setFieldU32(sfSequence, 10); + + STArray credentials(sfAcceptedCredentials, 2); + for (std::size_t n = 0; n < 2; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + auto credType = + std::string("cred_type") + std::to_string(9 - n); + cred.setFieldVL( + sfCredentialType, + Slice(credType.c_str(), credType.size())); + credentials.push_back(std::move(cred)); + } + slePd->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().insert(slePd); + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + testcase << "PermissionedDomain 4"; + doInvariantCheck( + {{"permissioned domain credentials aren't unique"}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + slePd->setAccountID(sfOwner, A1); + slePd->setFieldU32(sfSequence, 10); + + STArray credentials(sfAcceptedCredentials, 2); + for (std::size_t n = 0; n < 2; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + cred.setFieldVL(sfCredentialType, Slice("cred_type", 9)); + credentials.push_back(std::move(cred)); + } + slePd->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().insert(slePd); + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + auto const createPD = [](ApplyContext& ac, + std::shared_ptr& sle, + Account const& A1, + Account const& A2) { + sle->setAccountID(sfOwner, A1); + sle->setFieldU32(sfSequence, 10); + + STArray credentials(sfAcceptedCredentials, 2); + for (std::size_t n = 0; n < 2; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + auto credType = "cred_type" + std::to_string(n); + cred.setFieldVL( + sfCredentialType, Slice(credType.c_str(), credType.size())); + credentials.push_back(std::move(cred)); + } + sle->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().insert(sle); + }; + + testcase << "PermissionedDomain Set 1"; + doInvariantCheck( + {{"permissioned domain with no rules."}}, + [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + + // create PD + createPD(ac, slePd, A1, A2); + + // update PD with empty rules + { + STArray credentials(sfAcceptedCredentials, 2); + slePd->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().update(slePd); + } + + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + testcase << "PermissionedDomain Set 2"; + doInvariantCheck( + {{"permissioned domain bad credentials size " + + std::to_string(tooBig)}}, + [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + + // create PD + createPD(ac, slePd, A1, A2); + + // update PD + { + STArray credentials(sfAcceptedCredentials, tooBig); + + for (std::size_t n = 0; n < tooBig; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + auto credType = "cred_type2" + std::to_string(n); + cred.setFieldVL( + sfCredentialType, + Slice(credType.c_str(), credType.size())); + credentials.push_back(std::move(cred)); + } + + slePd->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().update(slePd); + } + + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + testcase << "PermissionedDomain Set 3"; + doInvariantCheck( + {{"permissioned domain credentials aren't sorted"}}, + [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + + // create PD + createPD(ac, slePd, A1, A2); + + // update PD + { + STArray credentials(sfAcceptedCredentials, 2); + for (std::size_t n = 0; n < 2; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + auto credType = + std::string("cred_type2") + std::to_string(9 - n); + cred.setFieldVL( + sfCredentialType, + Slice(credType.c_str(), credType.size())); + credentials.push_back(std::move(cred)); + } + + slePd->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().update(slePd); + } + + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + testcase << "PermissionedDomain Set 4"; + doInvariantCheck( + {{"permissioned domain credentials aren't unique"}}, + [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + + // create PD + createPD(ac, slePd, A1, A2); + + // update PD + { + STArray credentials(sfAcceptedCredentials, 2); + for (std::size_t n = 0; n < 2; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + cred.setFieldVL( + sfCredentialType, Slice("cred_type", 9)); + credentials.push_back(std::move(cred)); + } + slePd->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().update(slePd); + } + + return true; + }, + XRPAmount{}, + STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + } + public: void run() override @@ -813,6 +1067,7 @@ class Invariants_test : public beast::unit_test::suite testNoZeroEscrow(); testValidNewAccountRoot(); testNFTokenPageInvariants(); + testPermissionedDomainInvariants(); } }; diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 7326fff0c76..ae1e3c195d0 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -575,8 +575,8 @@ class AccountObjects_test : public beast::unit_test::suite Account const gw{"gateway"}; auto const USD = gw["USD"]; - auto const features = - supported_amendments() | FeatureBitset{featureXChainBridge}; + auto const features = supported_amendments() | featureXChainBridge | + featurePermissionedDomains; Env env(*this, features); // Make a lambda we can use to get "account_objects" easily. @@ -627,6 +627,7 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::ticket), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::amm), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::did), 0)); + BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::permissioned_domain), 0)); // we expect invalid field type reported for the following types BEAST_EXPECT(acctObjsTypeIsInvalid(acctObjs(gw, jss::amendments))); @@ -714,6 +715,47 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(escrow[sfDestination.jsonName] == gw.human()); BEAST_EXPECT(escrow[sfAmount.jsonName].asUInt() == 100'000'000); } + + { + std::string const credentialType1 = "credential1"; + Account issuer("issuer"); + env.fund(XRP(5000), issuer); + + // gw creates an PermissionedDomain. + env(pdomain::setTx(gw, {{issuer, credentialType1}})); + env.close(); + + // Find the PermissionedDomain. + Json::Value const resp = acctObjs(gw, jss::permissioned_domain); + BEAST_EXPECT(acctObjsIsSize(resp, 1)); + + auto const& permissionedDomain = + resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT( + permissionedDomain.isMember(jss::Owner) && + (permissionedDomain[jss::Owner] == gw.human())); + bool const check1 = BEAST_EXPECT( + permissionedDomain.isMember(jss::AcceptedCredentials) && + permissionedDomain[jss::AcceptedCredentials].isArray() && + (permissionedDomain[jss::AcceptedCredentials].size() == 1) && + (permissionedDomain[jss::AcceptedCredentials][0u].isMember( + jss::Credential))); + + if (check1) + { + auto const& credential = + permissionedDomain[jss::AcceptedCredentials][0u] + [jss::Credential]; + BEAST_EXPECT( + credential.isMember(sfIssuer.jsonName) && + (credential[sfIssuer.jsonName] == issuer.human())); + BEAST_EXPECT( + credential.isMember(sfCredentialType.jsonName) && + (credential[sfCredentialType.jsonName] == + strHex(credentialType1))); + } + } + { // Create a bridge test::jtx::XChainBridgeObjects x; @@ -925,10 +967,13 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(entry[sfAccount.jsonName] == alice.human()); BEAST_EXPECT(entry[sfSignerWeight.jsonName].asUInt() == 7); } - // Create a Ticket for gw. - env(ticket::create(gw, 1)); - env.close(); + { + auto const seq = env.seq(gw); + // Create a Ticket for gw. + env(ticket::create(gw, 1)); + env.close(); + // Find the ticket. Json::Value const resp = acctObjs(gw, jss::ticket); BEAST_EXPECT(acctObjsIsSize(resp, 1)); @@ -936,8 +981,9 @@ class AccountObjects_test : public beast::unit_test::suite auto const& ticket = resp[jss::result][jss::account_objects][0u]; BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human()); BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket); - BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 14); + BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == seq + 1); } + { // See how "deletion_blockers_only" handles gw's directory. Json::Value params; @@ -951,7 +997,8 @@ class AccountObjects_test : public beast::unit_test::suite jss::Check.c_str(), jss::NFTokenPage.c_str(), jss::RippleState.c_str(), - jss::PayChannel.c_str()}; + jss::PayChannel.c_str(), + jss::PermissionedDomain.c_str()}; std::sort(v.begin(), v.end()); return v; }(); diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 41b0239fb50..8e28eb98cdd 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -494,6 +494,18 @@ class LedgerRPC_test : public beast::unit_test::suite "json", "ledger", "{ \"ledger_index\" : 1000000000000000 }"); checkErrorValue(ret, "invalidParams", "Invalid parameters."); } + + { + // ask for an zero index + Json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::index] = + "00000000000000000000000000000000000000000000000000000000000000" + "0000"; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } } void @@ -3086,6 +3098,122 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryPermissionedDomain() + { + testcase("ledger_entry PermissionedDomain"); + + using namespace test::jtx; + + Env env(*this, supported_amendments() | featurePermissionedDomains); + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(5000), issuer, alice, bob); + env.close(); + + auto const seq = env.seq(alice); + env(pdomain::setTx(alice, {{alice, "first credential"}})); + env.close(); + auto const objects = pdomain::getObjects(alice, env); + if (!BEAST_EXPECT(objects.size() == 1)) + return; + + { + // Succeed + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain][jss::account] = alice.human(); + params[jss::permissioned_domain][jss::seq] = seq; + auto jv = env.rpc("json", "ledger_entry", to_string(params)); + 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::PermissionedDomain); + + std::string const pdIdx = jv[jss::result][jss::index].asString(); + BEAST_EXPECT( + strHex(keylet::permissionedDomain(alice, seq).key) == pdIdx); + + params.clear(); + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain] = pdIdx; + jv = env.rpc("json", "ledger_entry", to_string(params)); + 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::PermissionedDomain); + } + + { + // Fail, invalid permissioned domain index + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain] = + "12F1F1F1F180D67377B2FAB292A31C922470326268D2B9B74CD1E582645B9A" + "DE"; + auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); + checkErrorValue(jrr[jss::result], "entryNotFound", ""); + } + + { + // Fail, invalid permissioned domain index + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain] = "NotAHexString"; + auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); + checkErrorValue(jrr[jss::result], "malformedObjectId", ""); + } + + { + // Fail, permissioned domain is not an object + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain] = 10; + auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); + checkErrorValue(jrr[jss::result], "malformedObject", ""); + } + + { + // Fail, invalid account + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain][jss::account] = 1; + params[jss::permissioned_domain][jss::seq] = seq; + auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); + checkErrorValue(jrr[jss::result], "malformedAccount", ""); + } + + { + // Fail, no account + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain][jss::account] = ""; + params[jss::permissioned_domain][jss::seq] = seq; + auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); + checkErrorValue(jrr[jss::result], "malformedAccount", ""); + } + + { + // Fail, invalid sequence + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::permissioned_domain][jss::account] = alice.human(); + params[jss::permissioned_domain][jss::seq] = "12g"; + auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); + checkErrorValue(jrr[jss::result], "malformedSequence", ""); + } + } + public: void run() override @@ -3117,6 +3245,7 @@ class LedgerRPC_test : public beast::unit_test::suite testOracleLedgerEntry(); testLedgerEntryMPT(); testLedgerEntryCLI(); + testLedgerEntryPermissionedDomain(); forAllApiVersions(std::bind_front( &LedgerRPC_test::testLedgerEntryInvalidParams, this)); diff --git a/src/xrpld/app/misc/CredentialHelpers.cpp b/src/xrpld/app/misc/CredentialHelpers.cpp index 08b5d804d4b..a18cd40336b 100644 --- a/src/xrpld/app/misc/CredentialHelpers.cpp +++ b/src/xrpld/app/misc/CredentialHelpers.cpp @@ -19,6 +19,7 @@ #include #include +#include #include @@ -166,7 +167,7 @@ valid(PreclaimContext const& ctx, AccountID const& src) if (sleCred->getAccountID(sfSubject) != src) { JLOG(ctx.j.trace()) - << "Credential doesn’t belong to the source account. Cred: " + << "Credential doesn't belong to the source account. Cred: " << h; return tecBAD_CREDENTIALS; } @@ -213,10 +214,10 @@ authorized(ApplyContext const& ctx, AccountID const& dst) } std::set> -makeSorted(STArray const& in) +makeSorted(STArray const& credentials) { std::set> out; - for (auto const& cred : in) + for (auto const& cred : credentials) { auto [it, ins] = out.emplace(cred[sfIssuer], cred[sfCredentialType]); if (!ins) @@ -225,6 +226,50 @@ makeSorted(STArray const& in) return out; } +NotTEC +checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j) +{ + if (credentials.empty() || (credentials.size() > maxSize)) + { + JLOG(j.trace()) << "Malformed transaction: " + "Invalid credentials size: " + << credentials.size(); + return credentials.empty() ? temARRAY_EMPTY : temARRAY_TOO_LARGE; + } + + std::unordered_set duplicates; + for (auto const& credential : credentials) + { + auto const& issuer = credential[sfIssuer]; + if (!issuer) + { + JLOG(j.trace()) << "Malformed transaction: " + "Issuer account is invalid: " + << to_string(issuer); + return temINVALID_ACCOUNT_ID; + } + + auto const ct = credential[sfCredentialType]; + if (ct.empty() || (ct.size() > maxCredentialTypeLength)) + { + JLOG(j.trace()) << "Malformed transaction: " + "Invalid credentialType size: " + << ct.size(); + return temMALFORMED; + } + + auto [it, ins] = duplicates.insert(sha512Half(issuer, ct)); + if (!ins) + { + JLOG(j.trace()) << "Malformed transaction: " + "duplicates in credenentials."; + return temMALFORMED; + } + } + + return tesSUCCESS; +} + } // namespace credentials TER diff --git a/src/xrpld/app/misc/CredentialHelpers.h b/src/xrpld/app/misc/CredentialHelpers.h index 3291fc1daa6..acc4f2621db 100644 --- a/src/xrpld/app/misc/CredentialHelpers.h +++ b/src/xrpld/app/misc/CredentialHelpers.h @@ -21,8 +21,6 @@ #include -#include - namespace ripple { namespace credentials { @@ -60,9 +58,14 @@ valid(PreclaimContext const& ctx, AccountID const& src); TER authorized(ApplyContext const& ctx, AccountID const& dst); -// return empty set if there are duplicates +// Sort credentials array, return empty set if there are duplicates std::set> -makeSorted(STArray const& in); +makeSorted(STArray const& credentials); + +// Check credentials array passed to DepositPreauth/PermissionedDomainSet +// transactions +NotTEC +checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j); } // namespace credentials diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index 73cd19e4120..599fcd60526 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -24,11 +24,9 @@ #include #include #include -#include #include #include -#include namespace ripple { @@ -94,45 +92,14 @@ DepositPreauth::preflight(PreflightContext const& ctx) } else { - 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; - } - } + if (auto err = credentials::checkArray( + ctx.tx.getFieldArray( + authArrPresent ? sfAuthorizeCredentials + : sfUnauthorizeCredentials), + maxCredentialsArraySize, + ctx.j); + !isTesSuccess(err)) + return err; } return preflight2(ctx); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index def7914a49b..63794023d40 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -19,7 +19,9 @@ #include +#include #include +#include #include #include #include @@ -485,6 +487,7 @@ LedgerEntryTypesMatch::visitEntry( case ltMPTOKEN_ISSUANCE: case ltMPTOKEN: case ltCREDENTIAL: + case ltPERMISSIONED_DOMAIN: break; default: invalidTypeAdded_ = true; @@ -1123,4 +1126,105 @@ ValidMPTIssuance::finalize( mptokensCreated_ == 0 && mptokensDeleted_ == 0; } +//------------------------------------------------------------------------------ + +void +ValidPermissionedDomain::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (before && before->getType() != ltPERMISSIONED_DOMAIN) + return; + if (after && after->getType() != ltPERMISSIONED_DOMAIN) + return; + + auto check = [](SleStatus& sleStatus, + std::shared_ptr const& sle) { + auto const& credentials = sle->getFieldArray(sfAcceptedCredentials); + sleStatus.credentialsSize_ = credentials.size(); + auto const sorted = credentials::makeSorted(credentials); + sleStatus.isUnique_ = !sorted.empty(); + + // If array have duplicates then all the other checks are invalid + sleStatus.isSorted_ = false; + + if (sleStatus.isUnique_) + { + unsigned i = 0; + for (auto const& cred : sorted) + { + auto const& credTx = credentials[i++]; + sleStatus.isSorted_ = (cred.first == credTx[sfIssuer]) && + (cred.second == credTx[sfCredentialType]); + if (!sleStatus.isSorted_) + break; + } + } + }; + + if (before) + { + sleStatus_[0] = SleStatus(); + check(*sleStatus_[0], after); + } + + if (after) + { + sleStatus_[1] = SleStatus(); + check(*sleStatus_[1], after); + } +} + +bool +ValidPermissionedDomain::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS) + return true; + + auto check = [](SleStatus const& sleStatus, beast::Journal const& j) { + if (!sleStatus.credentialsSize_) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain with " + "no rules."; + return false; + } + + if (sleStatus.credentialsSize_ > + maxPermissionedDomainCredentialsArraySize) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain bad " + "credentials size " + << sleStatus.credentialsSize_; + return false; + } + + if (!sleStatus.isUnique_) + { + JLOG(j.fatal()) + << "Invariant failed: permissioned domain credentials " + "aren't unique"; + return false; + } + + if (!sleStatus.isSorted_) + { + JLOG(j.fatal()) + << "Invariant failed: permissioned domain credentials " + "aren't sorted"; + return false; + } + + return true; + }; + + return (sleStatus_[0] ? check(*sleStatus_[0], j) : true) && + (sleStatus_[1] ? check(*sleStatus_[1], j) : true); +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 23ec8005556..19c4ef3e23f 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -27,9 +27,7 @@ #include #include -#include #include -#include namespace ripple { @@ -475,6 +473,41 @@ class ValidMPTIssuance beast::Journal const&); }; +/** + * @brief Invariants: Permissioned Domains must have some rules and + * AcceptedCredentials must have length between 1 and 10 inclusive. + * + * Since only permissions constitute rules, an empty credentials list + * means that there are no rules and the invariant is violated. + * + * Credentials must be sorted and no duplicates allowed + * + */ +class ValidPermissionedDomain +{ + struct SleStatus + { + std::size_t credentialsSize_{0}; + bool isSorted_ = false, isUnique_ = false; + }; + std::optional sleStatus_[2]; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -491,7 +524,8 @@ using InvariantChecks = std::tuple< ValidNFTokenPage, NFTokenCountTracking, ValidClawback, - ValidMPTIssuance>; + ValidMPTIssuance, + ValidPermissionedDomain>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp new file mode 100644 index 00000000000..69a4d818314 --- /dev/null +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +NotTEC +PermissionedDomainDelete::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featurePermissionedDomains)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + JLOG(ctx.j.debug()) << "PermissionedDomainDelete: invalid flags."; + return temINVALID_FLAG; + } + + auto const domain = ctx.tx.getFieldH256(sfDomainID); + if (domain == beast::zero) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +PermissionedDomainDelete::preclaim(PreclaimContext const& ctx) +{ + auto const domain = ctx.tx.getFieldH256(sfDomainID); + auto const sleDomain = ctx.view.read({ltPERMISSIONED_DOMAIN, domain}); + + if (!sleDomain) + return tecNO_ENTRY; + + assert( + sleDomain->isFieldPresent(sfOwner) && ctx.tx.isFieldPresent(sfAccount)); + if (sleDomain->getAccountID(sfOwner) != ctx.tx.getAccountID(sfAccount)) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +/** Attempt to delete the Permissioned Domain. */ +TER +PermissionedDomainDelete::doApply() +{ + assert(ctx_.tx.isFieldPresent(sfDomainID)); + + auto const slePd = + view().peek({ltPERMISSIONED_DOMAIN, ctx_.tx.at(sfDomainID)}); + auto const page = (*slePd)[sfOwnerNode]; + + if (!view().dirRemove(keylet::ownerDir(account_), page, slePd->key(), true)) + { + JLOG(j_.fatal()) // LCOV_EXCL_LINE + << "Unable to delete permissioned domain directory entry."; // LCOV_EXCL_LINE + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + + auto const ownerSle = view().peek(keylet::account(account_)); + assert(ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0); + adjustOwnerCount(view(), ownerSle, -1, ctx_.journal); + view().erase(slePd); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h new file mode 100644 index 00000000000..ce0d1f14d86 --- /dev/null +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h @@ -0,0 +1,46 @@ +//------------------------------------------------------------------------------ +/* + 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 PermissionedDomainDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit PermissionedDomainDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + /** Attempt to delete the Permissioned Domain. */ + TER + doApply() override; +}; + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp new file mode 100644 index 00000000000..fcc5563b954 --- /dev/null +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp @@ -0,0 +1,149 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { + +NotTEC +PermissionedDomainSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featurePermissionedDomains)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + JLOG(ctx.j.debug()) << "PermissionedDomainSet: invalid flags."; + return temINVALID_FLAG; + } + + if (auto err = credentials::checkArray( + ctx.tx.getFieldArray(sfAcceptedCredentials), + maxPermissionedDomainCredentialsArraySize, + ctx.j); + !isTesSuccess(err)) + return err; + + auto const domain = ctx.tx.at(~sfDomainID); + if (domain && *domain == beast::zero) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +PermissionedDomainSet::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx.getAccountID(sfAccount); + + if (!ctx.view.exists(keylet::account(account))) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const& credentials = ctx.tx.getFieldArray(sfAcceptedCredentials); + for (auto const& credential : credentials) + { + if (!ctx.view.exists( + keylet::account(credential.getAccountID(sfIssuer)))) + return tecNO_ISSUER; + } + + if (ctx.tx.isFieldPresent(sfDomainID)) + { + auto const sleDomain = ctx.view.read( + keylet::permissionedDomain(ctx.tx.getFieldH256(sfDomainID))); + if (!sleDomain) + return tecNO_ENTRY; + if (sleDomain->getAccountID(sfOwner) != account) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +/** Attempt to create the Permissioned Domain. */ +TER +PermissionedDomainSet::doApply() +{ + auto const ownerSle = view().peek(keylet::account(account_)); + if (!ownerSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sortedTxCredentials = + credentials::makeSorted(ctx_.tx.getFieldArray(sfAcceptedCredentials)); + STArray sortedLE(sfAcceptedCredentials, sortedTxCredentials.size()); + for (auto const& p : sortedTxCredentials) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, p.first); + cred.setFieldVL(sfCredentialType, p.second); + sortedLE.push_back(std::move(cred)); + } + + if (ctx_.tx.isFieldPresent(sfDomainID)) + { + // Modify existing permissioned domain. + auto slePd = view().peek( + keylet::permissionedDomain(ctx_.tx.getFieldH256(sfDomainID))); + if (!slePd) + return tefINTERNAL; // LCOV_EXCL_LINE + slePd->peekFieldArray(sfAcceptedCredentials) = std::move(sortedLE); + view().update(slePd); + } + else + { + // Create new permissioned domain. + // Check reserve availability for new object creation + auto const balance = STAmount((*ownerSle)[sfBalance]).xrp(); + auto const reserve = + ctx_.view().fees().accountReserve((*ownerSle)[sfOwnerCount] + 1); + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + + Keylet const pdKeylet = keylet::permissionedDomain( + account_, ctx_.tx.getFieldU32(sfSequence)); + auto slePd = std::make_shared(pdKeylet); + if (!slePd) + return tefINTERNAL; // LCOV_EXCL_LINE + + slePd->setAccountID(sfOwner, account_); + slePd->setFieldU32(sfSequence, ctx_.tx.getFieldU32(sfSequence)); + slePd->peekFieldArray(sfAcceptedCredentials) = std::move(sortedLE); + auto const page = view().dirInsert( + keylet::ownerDir(account_), pdKeylet, describeOwnerDir(account_)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + + slePd->setFieldU64(sfOwnerNode, *page); + // If we succeeded, the new entry counts against the creator's reserve. + adjustOwnerCount(view(), ownerSle, 1, ctx_.journal); + view().insert(slePd); + } + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.h b/src/xrpld/app/tx/detail/PermissionedDomainSet.h new file mode 100644 index 00000000000..7419770946f --- /dev/null +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.h @@ -0,0 +1,45 @@ +//------------------------------------------------------------------------------ +/* + 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 PermissionedDomainSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit PermissionedDomainSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + /** Attempt to create the Permissioned Domain. */ + TER + doApply() override; +}; + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 02a7e070ab4..d17227fa684 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -53,6 +53,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/AccountObjects.cpp b/src/xrpld/rpc/handlers/AccountObjects.cpp index 538b1d79424..1bdd95a74ca 100644 --- a/src/xrpld/rpc/handlers/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/AccountObjects.cpp @@ -224,7 +224,8 @@ doAccountObjects(RPC::JsonContext& context) ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, {jss::bridge, ltBRIDGE}, {jss::mpt_issuance, ltMPTOKEN_ISSUANCE}, - {jss::mptoken, ltMPTOKEN}}; + {jss::mptoken, ltMPTOKEN}, + {jss::permissioned_domain, ltPERMISSIONED_DOMAIN}}; typeFilter.emplace(); typeFilter->reserve(std::size(deletionBlockers)); diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 4401b4dacd0..daf46ab1b38 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -67,7 +67,7 @@ parseAuthorizeCredentials(Json::Value const& jv) return arr; } -std::optional +static std::optional parseIndex(Json::Value const& params, Json::Value& jvResult) { uint256 uNodeIndex; @@ -80,7 +80,7 @@ parseIndex(Json::Value const& params, Json::Value& jvResult) return uNodeIndex; } -std::optional +static std::optional parseAccountRoot(Json::Value const& params, Json::Value& jvResult) { auto const account = parseBase58(params.asString()); @@ -93,7 +93,7 @@ parseAccountRoot(Json::Value const& params, Json::Value& jvResult) return keylet::account(*account).key; } -std::optional +static std::optional parseCheck(Json::Value const& params, Json::Value& jvResult) { uint256 uNodeIndex; @@ -106,7 +106,7 @@ parseCheck(Json::Value const& params, Json::Value& jvResult) return uNodeIndex; } -std::optional +static std::optional parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult) { if (!dp.isObject()) @@ -171,7 +171,7 @@ parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult) return keylet::depositPreauth(*owner, sorted).key; } -std::optional +static std::optional parseDirectory(Json::Value const& params, Json::Value& jvResult) { if (params.isNull()) @@ -237,7 +237,7 @@ parseDirectory(Json::Value const& params, Json::Value& jvResult) return std::nullopt; } -std::optional +static std::optional parseEscrow(Json::Value const& params, Json::Value& jvResult) { if (!params.isObject()) @@ -270,7 +270,7 @@ parseEscrow(Json::Value const& params, Json::Value& jvResult) return keylet::escrow(*id, params[jss::seq].asUInt()).key; } -std::optional +static std::optional parseOffer(Json::Value const& params, Json::Value& jvResult) { if (!params.isObject()) @@ -301,7 +301,7 @@ parseOffer(Json::Value const& params, Json::Value& jvResult) return keylet::offer(*id, params[jss::seq].asUInt()).key; } -std::optional +static std::optional parsePaymentChannel(Json::Value const& params, Json::Value& jvResult) { uint256 uNodeIndex; @@ -314,7 +314,7 @@ parsePaymentChannel(Json::Value const& params, Json::Value& jvResult) return uNodeIndex; } -std::optional +static std::optional parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult) { Currency uCurrency; @@ -351,7 +351,7 @@ parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult) return keylet::line(*id1, *id2, uCurrency).key; } -std::optional +static std::optional parseTicket(Json::Value const& params, Json::Value& jvResult) { if (!params.isObject()) @@ -382,7 +382,7 @@ parseTicket(Json::Value const& params, Json::Value& jvResult) return getTicketIndex(*id, params[jss::ticket_seq].asUInt()); } -std::optional +static std::optional parseNFTokenPage(Json::Value const& params, Json::Value& jvResult) { if (params.isString()) @@ -400,7 +400,7 @@ parseNFTokenPage(Json::Value const& params, Json::Value& jvResult) return std::nullopt; } -std::optional +static std::optional parseAMM(Json::Value const& params, Json::Value& jvResult) { if (!params.isObject()) @@ -433,7 +433,7 @@ parseAMM(Json::Value const& params, Json::Value& jvResult) } } -std::optional +static std::optional parseBridge(Json::Value const& params, Json::Value& jvResult) { // return the keylet for the specified bridge or nullopt if the @@ -484,7 +484,7 @@ parseBridge(Json::Value const& params, Json::Value& jvResult) return std::nullopt; } -std::optional +static std::optional parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult) { if (claim_id.isString()) @@ -556,7 +556,7 @@ parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult) return std::nullopt; } -std::optional +static std::optional parseXChainOwnedCreateAccountClaimID( Json::Value const& claim_id, Json::Value& jvResult) @@ -632,7 +632,7 @@ parseXChainOwnedCreateAccountClaimID( return std::nullopt; } -std::optional +static std::optional parseDID(Json::Value const& params, Json::Value& jvResult) { auto const account = parseBase58(params.asString()); @@ -645,7 +645,7 @@ parseDID(Json::Value const& params, Json::Value& jvResult) return keylet::did(*account).key; } -std::optional +static std::optional parseOracle(Json::Value const& params, Json::Value& jvResult) { if (!params.isObject()) @@ -699,7 +699,7 @@ parseOracle(Json::Value const& params, Json::Value& jvResult) return keylet::oracle(*account, *documentID).key; } -std::optional +static std::optional parseCredential(Json::Value const& cred, Json::Value& jvResult) { if (cred.isString()) @@ -738,7 +738,7 @@ parseCredential(Json::Value const& cred, Json::Value& jvResult) .key; } -std::optional +static std::optional parseMPTokenIssuance( Json::Value const& unparsedMPTIssuanceID, Json::Value& jvResult) @@ -759,7 +759,7 @@ parseMPTokenIssuance( return std::nullopt; } -std::optional +static std::optional parseMPToken(Json::Value const& mptJson, Json::Value& jvResult) { if (!mptJson.isObject()) @@ -806,8 +806,50 @@ parseMPToken(Json::Value const& mptJson, Json::Value& jvResult) } } +static std::optional +parsePermissionedDomains(Json::Value const& pd, Json::Value& jvResult) +{ + if (pd.isString()) + { + Json::Value result; + auto const index = parseIndex(pd, result); + if (!index) + jvResult[jss::error] = "malformedObjectId"; + return index; + } + + if (!pd.isObject()) + { + jvResult[jss::error] = "malformedObject"; + return std::nullopt; + } + + if (!pd.isMember(jss::account) || !pd[jss::account].isString()) + { + jvResult[jss::error] = "malformedAccount"; + return std::nullopt; + } + + if (!pd.isMember(jss::seq) || + (pd[jss::seq].isInt() && pd[jss::seq].asInt() < 0) || + (!pd[jss::seq].isInt() && !pd[jss::seq].isUInt())) + { + jvResult[jss::error] = "malformedSequence"; + return std::nullopt; + } + + auto const account = parseBase58(pd[jss::account].asString()); + if (!account) + { + jvResult[jss::error] = "malformedAccount"; + return std::nullopt; + } + + return keylet::permissionedDomain(*account, pd[jss::seq].asUInt()).key; +} + using FunctionType = - std::optional (*)(Json::Value const&, Json::Value&); + std::function(Json::Value const&, Json::Value&)>; struct LedgerEntry { @@ -851,6 +893,9 @@ doLedgerEntry(RPC::JsonContext& context) {jss::offer, parseOffer, ltOFFER}, {jss::oracle, parseOracle, ltORACLE}, {jss::payment_channel, parsePaymentChannel, ltPAYCHAN}, + {jss::permissioned_domain, + parsePermissionedDomains, + ltPERMISSIONED_DOMAIN}, {jss::ripple_state, parseRippleState, ltRIPPLE_STATE}, // This is an alias, since the `ledger_data` filter uses jss::state {jss::state, parseRippleState, ltRIPPLE_STATE}, @@ -891,6 +936,7 @@ doLedgerEntry(RPC::JsonContext& context) break; } } + if (!found) { if (context.apiVersion < 2u) @@ -965,7 +1011,7 @@ doLedgerEntryGrpc( grpc::Status status = grpc::Status::OK; std::shared_ptr ledger; - if (auto status = RPC::ledgerFromRequest(ledger, context)) + if (auto const status = RPC::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == rpcINVALID_PARAMS) @@ -996,16 +1042,14 @@ doLedgerEntryGrpc( grpc::StatusCode::NOT_FOUND, "object not found"}; return {response, errorStatus}; } - else - { - Serializer s; - sleNode->add(s); - auto& stateObject = *response.mutable_ledger_object(); - stateObject.set_data(s.peekData().data(), s.getLength()); - stateObject.set_key(request.key()); - *(response.mutable_ledger()) = request.ledger(); - return {response, status}; - } + Serializer s; + sleNode->add(s); + + auto& stateObject = *response.mutable_ledger_object(); + stateObject.set_data(s.peekData().data(), s.getLength()); + stateObject.set_key(request.key()); + *(response.mutable_ledger()) = request.ledger(); + return {response, status}; } } // namespace ripple