//------------------------------------------------------------------------------ /* 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 withoutFeature_{ supported_amendments() - featurePermissionedDomains}; FeatureBitset withFeature_{ supported_amendments() // | featurePermissionedDomains | featureCredentials}; // 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 PD cannot be created or updated if credentials are disabled void testCredentialsDisabled() { auto amendments = supported_amendments() | featureCredentials; amendments.set(featurePermissionedDomains); amendments.reset(featureCredentials); testcase("Credentials disabled"); Account const alice("alice"); Env env(*this, amendments); env.fund(XRP(1000), alice); pdomain::Credentials credentials{{alice, "first credential"}}; env(pdomain::setTx(alice, credentials), ter(temDISABLED)); } // 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(); testCredentialsDisabled(); testDisabled(); testSet(); testDelete(); testAccountReserve(); } }; BEAST_DEFINE_TESTSUITE(PermissionedDomains, app, ripple); } // namespace test } // namespace ripple