mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-21 03:26:01 +00:00
Permissioned Domains (XLS-80d) (#5161)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
569
src/test/app/PermissionedDomains_test.cpp
Normal file
569
src/test/app/PermissionedDomains_test.cpp
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -43,6 +43,9 @@ struct AuthorizeCredentials
|
||||
jtx::Account issuer;
|
||||
std::string credType;
|
||||
|
||||
auto
|
||||
operator<=>(const AuthorizeCredentials&) const = default;
|
||||
|
||||
Json::Value
|
||||
toJson() const
|
||||
{
|
||||
|
||||
@@ -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}}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
181
src/test/jtx/impl/permissioned_domains.cpp
Normal file
181
src/test/jtx/impl/permissioned_domains.cpp
Normal 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
|
||||
71
src/test/jtx/permissioned_domains.h
Normal file
71
src/test/jtx/permissioned_domains.h
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 doesn’t belong to the source account. Cred: "
|
||||
<< "Credential doesn't belong to the source account. Cred: "
|
||||
<< h;
|
||||
return tecBAD_CREDENTIALS;
|
||||
}
|
||||
@@ -213,10 +214,10 @@ authorized(ApplyContext const& ctx, AccountID const& dst)
|
||||
}
|
||||
|
||||
std::set<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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
90
src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp
Normal file
90
src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp
Normal 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
|
||||
46
src/xrpld/app/tx/detail/PermissionedDomainDelete.h
Normal file
46
src/xrpld/app/tx/detail/PermissionedDomainDelete.h
Normal 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
|
||||
149
src/xrpld/app/tx/detail/PermissionedDomainSet.cpp
Normal file
149
src/xrpld/app/tx/detail/PermissionedDomainSet.cpp
Normal 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
|
||||
45
src/xrpld/app/tx/detail/PermissionedDomainSet.h
Normal file
45
src/xrpld/app/tx/detail/PermissionedDomainSet.h
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user