Files
rippled/src/test/app/PermissionedDomains_test.cpp
2026-05-13 20:46:30 +00:00

549 lines
20 KiB
C++

#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/acctdelete.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
#include <test/jtx/fee.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/ter.h>
#include <test/jtx/txflags.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <cstddef>
#include <exception>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace xrpl::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_{
(testableAmendments() | featurePermissionedDomains | featureCredentials) - fixCleanup3_1_3};
FeatureBitset withFix_{
testableAmendments() | featurePermissionedDomains | featureCredentials | fixCleanup3_1_3};
// Verify that each tx type can execute if the feature is enabled.
void
testEnabled(FeatureBitset features)
{
testcase("Enabled");
Account const alice("alice");
Env env(*this, features);
env.fund(XRP(1000), alice);
pdomain::Credentials const 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 = testableAmendments();
amendments.set(featurePermissionedDomains);
amendments.reset(featureCredentials);
testcase("Credentials disabled");
Account const alice("alice");
Env env(*this, amendments);
env.fund(XRP(1000), alice);
pdomain::Credentials const 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, testableAmendments() - featurePermissionedDomains);
env.fund(XRP(1000), alice);
pdomain::Credentials const 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<uint256> 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() == kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE + 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 kLONG_CREDENTIAL_TYPE =
"Cred0123456789012345678901234567890123456789012345678901234567890";
static_assert(kLONG_CREDENTIAL_TYPE.size() == kMAX_CREDENTIAL_TYPE_LENGTH + 1);
txJsonMutable["AcceptedCredentials"][2u] = credentialOrig;
txJsonMutable["AcceptedCredentials"][2u][jss::Credential]["CredentialType"] =
std::string(kLONG_CREDENTIAL_TYPE);
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<std::string, Account> 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<std::string, Account> 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(FeatureBitset features)
{
testcase("Set");
Env env(*this, features);
env.setParseFailureExpected(true);
int const accNum = 12;
Account const alice[accNum] = {
"alice",
"alice2",
"alice3",
"alice4",
"alice5",
"alice6",
"alice7",
"alice8",
"alice9",
"alice10",
"alice11",
"alice12"};
std::unordered_map<std::string, Account> 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::Values::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 kLONG_CREDENTIAL_TYPE =
"Cred0123456789012345678901234567890123456789012345678901234567"
"89";
static_assert(kLONG_CREDENTIAL_TYPE.size() == kMAX_CREDENTIAL_TYPE_LENGTH);
pdomain::Credentials const longCredentials{
{alice[1], std::string(kLONG_CREDENTIAL_TYPE)}};
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::Values::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() == kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE);
BEAST_EXPECT(credentials10 != pdomain::sortCredentials(credentials10));
env(pdomain::setTx(alice[0], credentials10));
auto tx = env.tx()->getJson(JsonOptions::Values::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 kDELETE_DELTA = 255;
{
// Close enough ledgers to make it potentially deletable if empty.
std::size_t const ownerSeq = env.seq(alice[0]);
while (kDELETE_DELTA + 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 const ownerSeq = env.seq(alice[0]);
while (kDELETE_DELTA + ownerSeq > env.current()->seq())
env.close();
env(acctdelete(alice[0], alice[2]), Fee(acctDelFee));
}
}
// Test PermissionedDomainDelete
void
testDelete(FeatureBitset features)
{
testcase("Delete");
Env env(*this, features);
Account const alice("alice");
env.fund(XRP(1000), alice);
auto const setFee(drops(env.current()->fees().increment));
pdomain::Credentials const 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::Values::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(FeatureBitset features)
{
// Verify that the reserve behaves as expected for creating.
testcase("Account Reserve");
using namespace test::jtx;
Env env(*this, features);
Account const alice("alice");
// Fund alice enough to exist, but not enough to meet
// the reserve.
auto const acctReserve = env.current()->fees().reserve;
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 const credentials{{alice, "first credential"}};
env(pdomain::setTx(alice, credentials), Ter(tecINSUFFICIENT_RESERVE));
BEAST_EXPECT(env.ownerCount(alice) == 0);
BEAST_EXPECT(pdomain::getObjects(alice, env).empty());
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(withFeature_);
testEnabled(withFix_);
testCredentialsDisabled();
testDisabled();
testSet(withFeature_);
testSet(withFix_);
testDelete(withFeature_);
testDelete(withFix_);
testAccountReserve(withFeature_);
testAccountReserve(withFix_);
}
};
BEAST_DEFINE_TESTSUITE(PermissionedDomains, app, xrpl);
} // namespace xrpl::test