Permissioned Domains (XLS-80d) (#5161)

This commit is contained in:
Olek
2025-01-10 12:44:14 -05:00
committed by GitHub
parent 07f118caec
commit ccc0889803
35 changed files with 1962 additions and 109 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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;

View File

@@ -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.
//

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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)
{
auto credentials2 = credentials;
credentials2.push_back(c);
std::vector<deposit::AuthorizeCredentials> copyCredentials(
credentials.begin(), credentials.end() - 1);
std::ranges::shuffle(copyCredentials, gen);
for (auto const& c : copyCredentials)
{
auto credentials2 = copyCredentials;
credentials2.push_back(c);
env(deposit::authCredentials(stock, credentials2),
ter(temMALFORMED));
}

View File

@@ -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 <test/jtx.h>
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
#include <xrpld/ledger/ApplyViewImpl.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/jss.h>
#include <exception>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
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<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() ==
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<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()
{
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<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::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

View File

@@ -50,6 +50,7 @@
#include <test/jtx/owners.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/prop.h>
#include <test/jtx/quality.h>
#include <test/jtx/rate.h>

View File

@@ -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

View File

@@ -406,6 +406,12 @@ public:
trace_ = 0;
}
void
set_parse_failure_expected(bool b)
{
parseFailureExpected_ = b;
}
/** Turn off signature checks. */
void
disable_sigs()
@@ -693,6 +699,7 @@ protected:
TestStopwatch stopwatch_;
uint256 txid_;
TER ter_ = tesSUCCESS;
bool parseFailureExpected_ = false;
Json::Value
do_rpc(

View File

@@ -43,6 +43,9 @@ struct AuthorizeCredentials
jtx::Account issuer;
std::string credType;
auto
operator<=>(const AuthorizeCredentials&) const = default;
Json::Value
toJson() const
{

View File

@@ -53,7 +53,8 @@ public:
Throw<std::runtime_error>("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}}
{
}

View File

@@ -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<std::runtime_error>("missing account root");
return sle->getFieldU32(sfOwnerCount);
}
std::uint32_t
Env::seq(Account const& account) const
{
@@ -503,6 +512,7 @@ Env::autofill(JTx& jt)
}
catch (parse_error const&)
{
if (!parseFailureExpected_)
test.log << "parse failed:\n" << pretty(jv) << std::endl;
Rethrow();
}

View File

@@ -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 */

View File

@@ -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 <test/jtx/permissioned_domains.h>
#include <exception>
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<uint256> 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<uint256, Json::Value>
getObjects(Account const& account, Env& env, bool withType)
{
std::map<uint256, Json::Value> 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<std::runtime_error>(
"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<std::string, Account> 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<Credential> credentialsSet;
for (auto const& credential : input)
credentialsSet.insert(credential);
return {credentialsSet.begin(), credentialsSet.end()};
}
uint256
getNewDomain(std::shared_ptr<STObject const> 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

View File

@@ -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 <test/jtx.h>
#include <test/jtx/deposit.h>
namespace ripple {
namespace test {
namespace jtx {
namespace pdomain {
// Helpers for PermissionedDomains testing
using Credential = ripple::test::jtx::deposit::AuthorizeCredentials;
using Credentials = std::vector<Credential>;
// helpers
// Make json for PermissionedDomainSet transaction
Json::Value
setTx(
AccountID const& account,
Credentials const& credentials,
std::optional<uint256> 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<uint256, Json::Value>
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<std::string, Account> 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<STObject const> const& meta);
} // namespace pdomain
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -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<SLE>(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<SLE>(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<SLE>(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<SLE>(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>& 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<SLE>(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<SLE>(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<SLE>(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<SLE>(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 @@ public:
testNoZeroEscrow();
testValidNewAccountRoot();
testNFTokenPageInvariants();
testPermissionedDomainInvariants();
}
};

View File

@@ -575,8 +575,8 @@ public:
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 @@ public:
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 @@ public:
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 @@ public:
BEAST_EXPECT(entry[sfAccount.jsonName] == alice.human());
BEAST_EXPECT(entry[sfSignerWeight.jsonName].asUInt() == 7);
}
{
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 @@ public:
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 @@ public:
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;
}();

View File

@@ -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 @@ public:
testOracleLedgerEntry();
testLedgerEntryMPT();
testLedgerEntryCLI();
testLedgerEntryPermissionedDomain();
forAllApiVersions(std::bind_front(
&LedgerRPC_test::testLedgerEntryInvalidParams, this));

View File

@@ -19,6 +19,7 @@
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/digest.h>
#include <unordered_set>
@@ -166,7 +167,7 @@ valid(PreclaimContext const& ctx, AccountID const& src)
if (sleCred->getAccountID(sfSubject) != src)
{
JLOG(ctx.j.trace())
<< "Credential doesnt 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<std::pair<AccountID, Slice>>
makeSorted(STArray const& in)
makeSorted(STArray const& credentials)
{
std::set<std::pair<AccountID, Slice>> 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<uint256> 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

View File

@@ -21,8 +21,6 @@
#include <xrpld/app/tx/detail/Transactor.h>
#include <optional>
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<std::pair<AccountID, Slice>>
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

View File

@@ -24,11 +24,9 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/st.h>
#include <optional>
#include <unordered_set>
namespace ripple {
@@ -94,45 +92,14 @@ DepositPreauth::preflight(PreflightContext const& ctx)
}
else
{
STArray const& arr(ctx.tx.getFieldArray(
if (auto err = credentials::checkArray(
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<uint256> 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;
}
}
: sfUnauthorizeCredentials),
maxCredentialsArraySize,
ctx.j);
!isTesSuccess(err))
return err;
}
return preflight2(ctx);

View File

@@ -19,7 +19,9 @@
#include <xrpld/app/tx/detail/InvariantCheck.h>
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
#include <xrpld/ledger/ReadView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
@@ -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<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (before && before->getType() != ltPERMISSIONED_DOMAIN)
return;
if (after && after->getType() != ltPERMISSIONED_DOMAIN)
return;
auto check = [](SleStatus& sleStatus,
std::shared_ptr<SLE const> 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

View File

@@ -27,9 +27,7 @@
#include <xrpl/protocol/TER.h>
#include <cstdint>
#include <map>
#include <tuple>
#include <utility>
namespace ripple {
@@ -475,6 +473,41 @@ public:
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> sleStatus_[2];
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> 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

View File

@@ -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 <xrpld/app/tx/detail/PermissionedDomainDelete.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/TxFlags.h>
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

View File

@@ -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 <xrpld/app/tx/detail/Transactor.h>
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

View File

@@ -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 <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TxFlags.h>
#include <optional>
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<SLE>(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

View File

@@ -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 <xrpld/app/tx/detail/Transactor.h>
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

View File

@@ -53,6 +53,8 @@
#include <xrpld/app/tx/detail/NFTokenModify.h>
#include <xrpld/app/tx/detail/PayChan.h>
#include <xrpld/app/tx/detail/Payment.h>
#include <xrpld/app/tx/detail/PermissionedDomainDelete.h>
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
#include <xrpld/app/tx/detail/SetAccount.h>
#include <xrpld/app/tx/detail/SetOracle.h>
#include <xrpld/app/tx/detail/SetRegularKey.h>

View File

@@ -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));

View File

@@ -67,7 +67,7 @@ parseAuthorizeCredentials(Json::Value const& jv)
return arr;
}
std::optional<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
parseAccountRoot(Json::Value const& params, Json::Value& jvResult)
{
auto const account = parseBase58<AccountID>(params.asString());
@@ -93,7 +93,7 @@ parseAccountRoot(Json::Value const& params, Json::Value& jvResult)
return keylet::account(*account).key;
}
std::optional<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
parseXChainOwnedCreateAccountClaimID(
Json::Value const& claim_id,
Json::Value& jvResult)
@@ -632,7 +632,7 @@ parseXChainOwnedCreateAccountClaimID(
return std::nullopt;
}
std::optional<uint256>
static std::optional<uint256>
parseDID(Json::Value const& params, Json::Value& jvResult)
{
auto const account = parseBase58<AccountID>(params.asString());
@@ -645,7 +645,7 @@ parseDID(Json::Value const& params, Json::Value& jvResult)
return keylet::did(*account).key;
}
std::optional<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
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<uint256>
static std::optional<uint256>
parseMPTokenIssuance(
Json::Value const& unparsedMPTIssuanceID,
Json::Value& jvResult)
@@ -759,7 +759,7 @@ parseMPTokenIssuance(
return std::nullopt;
}
std::optional<uint256>
static std::optional<uint256>
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<uint256>
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<AccountID>(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<uint256> (*)(Json::Value const&, Json::Value&);
std::function<std::optional<uint256>(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<ReadView const> 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,8 +1042,7 @@ doLedgerEntryGrpc(
grpc::StatusCode::NOT_FOUND, "object not found"};
return {response, errorStatus};
}
else
{
Serializer s;
sleNode->add(s);
@@ -1006,6 +1051,5 @@ doLedgerEntryGrpc(
stateObject.set_key(request.key());
*(response.mutable_ledger()) = request.ledger();
return {response, status};
}
}
} // namespace ripple