mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-23 12:35:50 +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
|
// 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
|
// 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.
|
// 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.
|
/** Amendments that this server supports and the default voting behavior.
|
||||||
Whether they are enabled depends on the Rules defined in the validated
|
Whether they are enabled depends on the Rules defined in the validated
|
||||||
|
|||||||
@@ -330,6 +330,11 @@ mptoken(uint256 const& mptokenKey)
|
|||||||
Keylet
|
Keylet
|
||||||
mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept;
|
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
|
} // namespace keylet
|
||||||
|
|
||||||
// Everything below is deprecated and should be removed in favor of keylets:
|
// 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 */
|
/** The maximum number of credentials can be passed in array */
|
||||||
std::size_t constexpr maxCredentialsArraySize = 8;
|
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 */
|
/** The maximum length of MPTokenMetadata */
|
||||||
std::size_t constexpr maxMPTokenMetadataLength = 1024;
|
std::size_t constexpr maxMPTokenMetadataLength = 1024;
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||||
// in include/xrpl/protocol/Feature.h.
|
// in include/xrpl/protocol/Feature.h.
|
||||||
|
|
||||||
|
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
|
||||||
XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FEATURE(AMMClawback, 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(Flow, Supported::yes, VoteBehavior::DefaultYes)
|
||||||
XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo)
|
||||||
|
|
||||||
|
|
||||||
// The following amendments are obsolete, but must remain supported
|
// The following amendments are obsolete, but must remain supported
|
||||||
// because they could potentially get enabled.
|
// because they could potentially get enabled.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -448,5 +448,18 @@ LEDGER_ENTRY(ltCREDENTIAL, 0x0081, Credential, credential, ({
|
|||||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
{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 EXPAND
|
||||||
#undef LEDGER_ENTRY_DUPLICATE
|
#undef LEDGER_ENTRY_DUPLICATE
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ TYPED_SFIELD(sfHookStateKey, UINT256, 30)
|
|||||||
TYPED_SFIELD(sfHookHash, UINT256, 31)
|
TYPED_SFIELD(sfHookHash, UINT256, 31)
|
||||||
TYPED_SFIELD(sfHookNamespace, UINT256, 32)
|
TYPED_SFIELD(sfHookNamespace, UINT256, 32)
|
||||||
TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
|
TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
|
||||||
|
TYPED_SFIELD(sfDomainID, UINT256, 34)
|
||||||
|
|
||||||
// number (common)
|
// number (common)
|
||||||
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
||||||
@@ -375,3 +376,4 @@ UNTYPED_SFIELD(sfPriceDataSeries, ARRAY, 24)
|
|||||||
UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
|
UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
|
||||||
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
|
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
|
||||||
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
|
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
|
||||||
|
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
|
||||||
|
|||||||
@@ -454,6 +454,16 @@ TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, ({
|
|||||||
{sfURI, soeOPTIONAL},
|
{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.
|
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace jss {
|
|||||||
// clang-format off
|
// clang-format off
|
||||||
JSS(AL_size); // out: GetCounts
|
JSS(AL_size); // out: GetCounts
|
||||||
JSS(AL_hit_rate); // out: GetCounts
|
JSS(AL_hit_rate); // out: GetCounts
|
||||||
|
JSS(AcceptedCredentials); // out: AccountObjects
|
||||||
JSS(Account); // in: TransactionSign; field.
|
JSS(Account); // in: TransactionSign; field.
|
||||||
JSS(AMMID); // field
|
JSS(AMMID); // field
|
||||||
JSS(Amount); // in: TransactionSign; field.
|
JSS(Amount); // in: TransactionSign; field.
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
|||||||
MPTOKEN_ISSUANCE = '~',
|
MPTOKEN_ISSUANCE = '~',
|
||||||
MPTOKEN = 't',
|
MPTOKEN = 't',
|
||||||
CREDENTIAL = 'D',
|
CREDENTIAL = 'D',
|
||||||
|
PERMISSIONED_DOMAIN = 'm',
|
||||||
|
|
||||||
// No longer used or supported. Left here to reserve the space
|
// No longer used or supported. Left here to reserve the space
|
||||||
// to avoid accidental reuse.
|
// to avoid accidental reuse.
|
||||||
@@ -527,6 +528,20 @@ credential(
|
|||||||
indexHash(LedgerNameSpace::CREDENTIAL, subject, issuer, credType)};
|
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 keylet
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -1070,7 +1070,7 @@ struct DepositPreauth_test : public beast::unit_test::suite
|
|||||||
{
|
{
|
||||||
// AuthorizeCredentials is empty
|
// AuthorizeCredentials is empty
|
||||||
auto jv = deposit::authCredentials(bob, {});
|
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},
|
{g, z},
|
||||||
{h, z},
|
{h, z},
|
||||||
{i, 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.");
|
testcase("Check duplicate credentials.");
|
||||||
{
|
{
|
||||||
// check duplicates in depositPreauth params
|
// check duplicates in depositPreauth params
|
||||||
std::ranges::shuffle(credentials, gen);
|
std::vector<deposit::AuthorizeCredentials> copyCredentials(
|
||||||
for (auto const& c : credentials)
|
credentials.begin(), credentials.end() - 1);
|
||||||
{
|
|
||||||
auto credentials2 = credentials;
|
|
||||||
credentials2.push_back(c);
|
|
||||||
|
|
||||||
|
std::ranges::shuffle(copyCredentials, gen);
|
||||||
|
for (auto const& c : copyCredentials)
|
||||||
|
{
|
||||||
|
auto credentials2 = copyCredentials;
|
||||||
|
credentials2.push_back(c);
|
||||||
env(deposit::authCredentials(stock, credentials2),
|
env(deposit::authCredentials(stock, credentials2),
|
||||||
ter(temMALFORMED));
|
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/owners.h>
|
||||||
#include <test/jtx/paths.h>
|
#include <test/jtx/paths.h>
|
||||||
#include <test/jtx/pay.h>
|
#include <test/jtx/pay.h>
|
||||||
|
#include <test/jtx/permissioned_domains.h>
|
||||||
#include <test/jtx/prop.h>
|
#include <test/jtx/prop.h>
|
||||||
#include <test/jtx/quality.h>
|
#include <test/jtx/quality.h>
|
||||||
#include <test/jtx/rate.h>
|
#include <test/jtx/rate.h>
|
||||||
|
|||||||
@@ -158,10 +158,10 @@ hash_append(Hasher& h, Account const& v) noexcept
|
|||||||
hash_append(h, v.id());
|
hash_append(h, v.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool
|
inline auto
|
||||||
operator<(Account const& lhs, Account const& rhs) noexcept
|
operator<=>(Account const& lhs, Account const& rhs) noexcept
|
||||||
{
|
{
|
||||||
return lhs.id() < rhs.id();
|
return lhs.id() <=> rhs.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace jtx
|
} // namespace jtx
|
||||||
|
|||||||
@@ -406,6 +406,12 @@ public:
|
|||||||
trace_ = 0;
|
trace_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
set_parse_failure_expected(bool b)
|
||||||
|
{
|
||||||
|
parseFailureExpected_ = b;
|
||||||
|
}
|
||||||
|
|
||||||
/** Turn off signature checks. */
|
/** Turn off signature checks. */
|
||||||
void
|
void
|
||||||
disable_sigs()
|
disable_sigs()
|
||||||
@@ -693,6 +699,7 @@ protected:
|
|||||||
TestStopwatch stopwatch_;
|
TestStopwatch stopwatch_;
|
||||||
uint256 txid_;
|
uint256 txid_;
|
||||||
TER ter_ = tesSUCCESS;
|
TER ter_ = tesSUCCESS;
|
||||||
|
bool parseFailureExpected_ = false;
|
||||||
|
|
||||||
Json::Value
|
Json::Value
|
||||||
do_rpc(
|
do_rpc(
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ struct AuthorizeCredentials
|
|||||||
jtx::Account issuer;
|
jtx::Account issuer;
|
||||||
std::string credType;
|
std::string credType;
|
||||||
|
|
||||||
|
auto
|
||||||
|
operator<=>(const AuthorizeCredentials&) const = default;
|
||||||
|
|
||||||
Json::Value
|
Json::Value
|
||||||
toJson() const
|
toJson() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ public:
|
|||||||
Throw<std::runtime_error>("fee: not XRP");
|
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()};
|
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
|
std::uint32_t
|
||||||
Env::seq(Account const& account) const
|
Env::seq(Account const& account) const
|
||||||
{
|
{
|
||||||
@@ -503,6 +512,7 @@ Env::autofill(JTx& jt)
|
|||||||
}
|
}
|
||||||
catch (parse_error const&)
|
catch (parse_error const&)
|
||||||
{
|
{
|
||||||
|
if (!parseFailureExpected_)
|
||||||
test.log << "parse failed:\n" << pretty(jv) << std::endl;
|
test.log << "parse failed:\n" << pretty(jv) << std::endl;
|
||||||
Rethrow();
|
Rethrow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ checkArraySize(Json::Value const& val, unsigned int size)
|
|||||||
std::uint32_t
|
std::uint32_t
|
||||||
ownerCount(Env const& env, Account const& account)
|
ownerCount(Env const& env, Account const& account)
|
||||||
{
|
{
|
||||||
std::uint32_t ret{0};
|
return env.ownerCount(account);
|
||||||
if (auto const sleAccount = env.le(account))
|
|
||||||
ret = sleAccount->getFieldU32(sfOwnerCount);
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Path finding */
|
/* 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:
|
public:
|
||||||
void
|
void
|
||||||
run() override
|
run() override
|
||||||
@@ -813,6 +1067,7 @@ public:
|
|||||||
testNoZeroEscrow();
|
testNoZeroEscrow();
|
||||||
testValidNewAccountRoot();
|
testValidNewAccountRoot();
|
||||||
testNFTokenPageInvariants();
|
testNFTokenPageInvariants();
|
||||||
|
testPermissionedDomainInvariants();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -575,8 +575,8 @@ public:
|
|||||||
Account const gw{"gateway"};
|
Account const gw{"gateway"};
|
||||||
auto const USD = gw["USD"];
|
auto const USD = gw["USD"];
|
||||||
|
|
||||||
auto const features =
|
auto const features = supported_amendments() | featureXChainBridge |
|
||||||
supported_amendments() | FeatureBitset{featureXChainBridge};
|
featurePermissionedDomains;
|
||||||
Env env(*this, features);
|
Env env(*this, features);
|
||||||
|
|
||||||
// Make a lambda we can use to get "account_objects" easily.
|
// 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::ticket), 0));
|
||||||
BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::amm), 0));
|
BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::amm), 0));
|
||||||
BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::did), 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
|
// we expect invalid field type reported for the following types
|
||||||
BEAST_EXPECT(acctObjsTypeIsInvalid(acctObjs(gw, jss::amendments)));
|
BEAST_EXPECT(acctObjsTypeIsInvalid(acctObjs(gw, jss::amendments)));
|
||||||
@@ -714,6 +715,47 @@ public:
|
|||||||
BEAST_EXPECT(escrow[sfDestination.jsonName] == gw.human());
|
BEAST_EXPECT(escrow[sfDestination.jsonName] == gw.human());
|
||||||
BEAST_EXPECT(escrow[sfAmount.jsonName].asUInt() == 100'000'000);
|
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
|
// Create a bridge
|
||||||
test::jtx::XChainBridgeObjects x;
|
test::jtx::XChainBridgeObjects x;
|
||||||
@@ -925,10 +967,13 @@ public:
|
|||||||
BEAST_EXPECT(entry[sfAccount.jsonName] == alice.human());
|
BEAST_EXPECT(entry[sfAccount.jsonName] == alice.human());
|
||||||
BEAST_EXPECT(entry[sfSignerWeight.jsonName].asUInt() == 7);
|
BEAST_EXPECT(entry[sfSignerWeight.jsonName].asUInt() == 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto const seq = env.seq(gw);
|
||||||
// Create a Ticket for gw.
|
// Create a Ticket for gw.
|
||||||
env(ticket::create(gw, 1));
|
env(ticket::create(gw, 1));
|
||||||
env.close();
|
env.close();
|
||||||
{
|
|
||||||
// Find the ticket.
|
// Find the ticket.
|
||||||
Json::Value const resp = acctObjs(gw, jss::ticket);
|
Json::Value const resp = acctObjs(gw, jss::ticket);
|
||||||
BEAST_EXPECT(acctObjsIsSize(resp, 1));
|
BEAST_EXPECT(acctObjsIsSize(resp, 1));
|
||||||
@@ -936,8 +981,9 @@ public:
|
|||||||
auto const& ticket = resp[jss::result][jss::account_objects][0u];
|
auto const& ticket = resp[jss::result][jss::account_objects][0u];
|
||||||
BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human());
|
BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human());
|
||||||
BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket);
|
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.
|
// See how "deletion_blockers_only" handles gw's directory.
|
||||||
Json::Value params;
|
Json::Value params;
|
||||||
@@ -951,7 +997,8 @@ public:
|
|||||||
jss::Check.c_str(),
|
jss::Check.c_str(),
|
||||||
jss::NFTokenPage.c_str(),
|
jss::NFTokenPage.c_str(),
|
||||||
jss::RippleState.c_str(),
|
jss::RippleState.c_str(),
|
||||||
jss::PayChannel.c_str()};
|
jss::PayChannel.c_str(),
|
||||||
|
jss::PermissionedDomain.c_str()};
|
||||||
std::sort(v.begin(), v.end());
|
std::sort(v.begin(), v.end());
|
||||||
return v;
|
return v;
|
||||||
}();
|
}();
|
||||||
|
|||||||
@@ -494,6 +494,18 @@ class LedgerRPC_test : public beast::unit_test::suite
|
|||||||
"json", "ledger", "{ \"ledger_index\" : 1000000000000000 }");
|
"json", "ledger", "{ \"ledger_index\" : 1000000000000000 }");
|
||||||
checkErrorValue(ret, "invalidParams", "Invalid parameters.");
|
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
|
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:
|
public:
|
||||||
void
|
void
|
||||||
run() override
|
run() override
|
||||||
@@ -3117,6 +3245,7 @@ public:
|
|||||||
testOracleLedgerEntry();
|
testOracleLedgerEntry();
|
||||||
testLedgerEntryMPT();
|
testLedgerEntryMPT();
|
||||||
testLedgerEntryCLI();
|
testLedgerEntryCLI();
|
||||||
|
testLedgerEntryPermissionedDomain();
|
||||||
|
|
||||||
forAllApiVersions(std::bind_front(
|
forAllApiVersions(std::bind_front(
|
||||||
&LedgerRPC_test::testLedgerEntryInvalidParams, this));
|
&LedgerRPC_test::testLedgerEntryInvalidParams, this));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <xrpld/app/misc/CredentialHelpers.h>
|
#include <xrpld/app/misc/CredentialHelpers.h>
|
||||||
#include <xrpld/ledger/View.h>
|
#include <xrpld/ledger/View.h>
|
||||||
|
#include <xrpl/protocol/digest.h>
|
||||||
|
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
@@ -166,7 +167,7 @@ valid(PreclaimContext const& ctx, AccountID const& src)
|
|||||||
if (sleCred->getAccountID(sfSubject) != src)
|
if (sleCred->getAccountID(sfSubject) != src)
|
||||||
{
|
{
|
||||||
JLOG(ctx.j.trace())
|
JLOG(ctx.j.trace())
|
||||||
<< "Credential doesn’t belong to the source account. Cred: "
|
<< "Credential doesn't belong to the source account. Cred: "
|
||||||
<< h;
|
<< h;
|
||||||
return tecBAD_CREDENTIALS;
|
return tecBAD_CREDENTIALS;
|
||||||
}
|
}
|
||||||
@@ -213,10 +214,10 @@ authorized(ApplyContext const& ctx, AccountID const& dst)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::set<std::pair<AccountID, Slice>>
|
std::set<std::pair<AccountID, Slice>>
|
||||||
makeSorted(STArray const& in)
|
makeSorted(STArray const& credentials)
|
||||||
{
|
{
|
||||||
std::set<std::pair<AccountID, Slice>> out;
|
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]);
|
auto [it, ins] = out.emplace(cred[sfIssuer], cred[sfCredentialType]);
|
||||||
if (!ins)
|
if (!ins)
|
||||||
@@ -225,6 +226,50 @@ makeSorted(STArray const& in)
|
|||||||
return out;
|
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
|
} // namespace credentials
|
||||||
|
|
||||||
TER
|
TER
|
||||||
|
|||||||
@@ -21,8 +21,6 @@
|
|||||||
|
|
||||||
#include <xrpld/app/tx/detail/Transactor.h>
|
#include <xrpld/app/tx/detail/Transactor.h>
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
namespace credentials {
|
namespace credentials {
|
||||||
|
|
||||||
@@ -60,9 +58,14 @@ valid(PreclaimContext const& ctx, AccountID const& src);
|
|||||||
TER
|
TER
|
||||||
authorized(ApplyContext const& ctx, AccountID const& dst);
|
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>>
|
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
|
} // namespace credentials
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,9 @@
|
|||||||
#include <xrpl/protocol/Feature.h>
|
#include <xrpl/protocol/Feature.h>
|
||||||
#include <xrpl/protocol/Indexes.h>
|
#include <xrpl/protocol/Indexes.h>
|
||||||
#include <xrpl/protocol/TxFlags.h>
|
#include <xrpl/protocol/TxFlags.h>
|
||||||
#include <xrpl/protocol/digest.h>
|
|
||||||
#include <xrpl/protocol/st.h>
|
#include <xrpl/protocol/st.h>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <unordered_set>
|
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
@@ -94,45 +92,14 @@ DepositPreauth::preflight(PreflightContext const& ctx)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
STArray const& arr(ctx.tx.getFieldArray(
|
if (auto err = credentials::checkArray(
|
||||||
|
ctx.tx.getFieldArray(
|
||||||
authArrPresent ? sfAuthorizeCredentials
|
authArrPresent ? sfAuthorizeCredentials
|
||||||
: sfUnauthorizeCredentials));
|
: sfUnauthorizeCredentials),
|
||||||
if (arr.empty() || (arr.size() > maxCredentialsArraySize))
|
maxCredentialsArraySize,
|
||||||
{
|
ctx.j);
|
||||||
JLOG(ctx.j.trace()) << "Malformed transaction: "
|
!isTesSuccess(err))
|
||||||
"Invalid AuthorizeCredentials size: "
|
return err;
|
||||||
<< 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return preflight2(ctx);
|
return preflight2(ctx);
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
|
|
||||||
#include <xrpld/app/tx/detail/InvariantCheck.h>
|
#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/NFTokenUtils.h>
|
||||||
|
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
|
||||||
#include <xrpld/ledger/ReadView.h>
|
#include <xrpld/ledger/ReadView.h>
|
||||||
#include <xrpld/ledger/View.h>
|
#include <xrpld/ledger/View.h>
|
||||||
#include <xrpl/basics/Log.h>
|
#include <xrpl/basics/Log.h>
|
||||||
@@ -485,6 +487,7 @@ LedgerEntryTypesMatch::visitEntry(
|
|||||||
case ltMPTOKEN_ISSUANCE:
|
case ltMPTOKEN_ISSUANCE:
|
||||||
case ltMPTOKEN:
|
case ltMPTOKEN:
|
||||||
case ltCREDENTIAL:
|
case ltCREDENTIAL:
|
||||||
|
case ltPERMISSIONED_DOMAIN:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
invalidTypeAdded_ = true;
|
invalidTypeAdded_ = true;
|
||||||
@@ -1123,4 +1126,105 @@ ValidMPTIssuance::finalize(
|
|||||||
mptokensCreated_ == 0 && mptokensDeleted_ == 0;
|
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
|
} // namespace ripple
|
||||||
|
|||||||
@@ -27,9 +27,7 @@
|
|||||||
#include <xrpl/protocol/TER.h>
|
#include <xrpl/protocol/TER.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
@@ -475,6 +473,41 @@ public:
|
|||||||
beast::Journal const&);
|
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
|
// additional invariant checks can be declared above and then added to this
|
||||||
// tuple
|
// tuple
|
||||||
using InvariantChecks = std::tuple<
|
using InvariantChecks = std::tuple<
|
||||||
@@ -491,7 +524,8 @@ using InvariantChecks = std::tuple<
|
|||||||
ValidNFTokenPage,
|
ValidNFTokenPage,
|
||||||
NFTokenCountTracking,
|
NFTokenCountTracking,
|
||||||
ValidClawback,
|
ValidClawback,
|
||||||
ValidMPTIssuance>;
|
ValidMPTIssuance,
|
||||||
|
ValidPermissionedDomain>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief get a tuple of all invariant checks
|
* @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/NFTokenModify.h>
|
||||||
#include <xrpld/app/tx/detail/PayChan.h>
|
#include <xrpld/app/tx/detail/PayChan.h>
|
||||||
#include <xrpld/app/tx/detail/Payment.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/SetAccount.h>
|
||||||
#include <xrpld/app/tx/detail/SetOracle.h>
|
#include <xrpld/app/tx/detail/SetOracle.h>
|
||||||
#include <xrpld/app/tx/detail/SetRegularKey.h>
|
#include <xrpld/app/tx/detail/SetRegularKey.h>
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ doAccountObjects(RPC::JsonContext& context)
|
|||||||
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
|
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
|
||||||
{jss::bridge, ltBRIDGE},
|
{jss::bridge, ltBRIDGE},
|
||||||
{jss::mpt_issuance, ltMPTOKEN_ISSUANCE},
|
{jss::mpt_issuance, ltMPTOKEN_ISSUANCE},
|
||||||
{jss::mptoken, ltMPTOKEN}};
|
{jss::mptoken, ltMPTOKEN},
|
||||||
|
{jss::permissioned_domain, ltPERMISSIONED_DOMAIN}};
|
||||||
|
|
||||||
typeFilter.emplace();
|
typeFilter.emplace();
|
||||||
typeFilter->reserve(std::size(deletionBlockers));
|
typeFilter->reserve(std::size(deletionBlockers));
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ parseAuthorizeCredentials(Json::Value const& jv)
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseIndex(Json::Value const& params, Json::Value& jvResult)
|
parseIndex(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
uint256 uNodeIndex;
|
uint256 uNodeIndex;
|
||||||
@@ -80,7 +80,7 @@ parseIndex(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return uNodeIndex;
|
return uNodeIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseAccountRoot(Json::Value const& params, Json::Value& jvResult)
|
parseAccountRoot(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
auto const account = parseBase58<AccountID>(params.asString());
|
auto const account = parseBase58<AccountID>(params.asString());
|
||||||
@@ -93,7 +93,7 @@ parseAccountRoot(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return keylet::account(*account).key;
|
return keylet::account(*account).key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseCheck(Json::Value const& params, Json::Value& jvResult)
|
parseCheck(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
uint256 uNodeIndex;
|
uint256 uNodeIndex;
|
||||||
@@ -106,7 +106,7 @@ parseCheck(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return uNodeIndex;
|
return uNodeIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult)
|
parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (!dp.isObject())
|
if (!dp.isObject())
|
||||||
@@ -171,7 +171,7 @@ parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult)
|
|||||||
return keylet::depositPreauth(*owner, sorted).key;
|
return keylet::depositPreauth(*owner, sorted).key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseDirectory(Json::Value const& params, Json::Value& jvResult)
|
parseDirectory(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (params.isNull())
|
if (params.isNull())
|
||||||
@@ -237,7 +237,7 @@ parseDirectory(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseEscrow(Json::Value const& params, Json::Value& jvResult)
|
parseEscrow(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (!params.isObject())
|
if (!params.isObject())
|
||||||
@@ -270,7 +270,7 @@ parseEscrow(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return keylet::escrow(*id, params[jss::seq].asUInt()).key;
|
return keylet::escrow(*id, params[jss::seq].asUInt()).key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseOffer(Json::Value const& params, Json::Value& jvResult)
|
parseOffer(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (!params.isObject())
|
if (!params.isObject())
|
||||||
@@ -301,7 +301,7 @@ parseOffer(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return keylet::offer(*id, params[jss::seq].asUInt()).key;
|
return keylet::offer(*id, params[jss::seq].asUInt()).key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parsePaymentChannel(Json::Value const& params, Json::Value& jvResult)
|
parsePaymentChannel(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
uint256 uNodeIndex;
|
uint256 uNodeIndex;
|
||||||
@@ -314,7 +314,7 @@ parsePaymentChannel(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return uNodeIndex;
|
return uNodeIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult)
|
parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
Currency uCurrency;
|
Currency uCurrency;
|
||||||
@@ -351,7 +351,7 @@ parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult)
|
|||||||
return keylet::line(*id1, *id2, uCurrency).key;
|
return keylet::line(*id1, *id2, uCurrency).key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseTicket(Json::Value const& params, Json::Value& jvResult)
|
parseTicket(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (!params.isObject())
|
if (!params.isObject())
|
||||||
@@ -382,7 +382,7 @@ parseTicket(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return getTicketIndex(*id, params[jss::ticket_seq].asUInt());
|
return getTicketIndex(*id, params[jss::ticket_seq].asUInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseNFTokenPage(Json::Value const& params, Json::Value& jvResult)
|
parseNFTokenPage(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (params.isString())
|
if (params.isString())
|
||||||
@@ -400,7 +400,7 @@ parseNFTokenPage(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseAMM(Json::Value const& params, Json::Value& jvResult)
|
parseAMM(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (!params.isObject())
|
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)
|
parseBridge(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
// return the keylet for the specified bridge or nullopt if the
|
// 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;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult)
|
parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (claim_id.isString())
|
if (claim_id.isString())
|
||||||
@@ -556,7 +556,7 @@ parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseXChainOwnedCreateAccountClaimID(
|
parseXChainOwnedCreateAccountClaimID(
|
||||||
Json::Value const& claim_id,
|
Json::Value const& claim_id,
|
||||||
Json::Value& jvResult)
|
Json::Value& jvResult)
|
||||||
@@ -632,7 +632,7 @@ parseXChainOwnedCreateAccountClaimID(
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseDID(Json::Value const& params, Json::Value& jvResult)
|
parseDID(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
auto const account = parseBase58<AccountID>(params.asString());
|
auto const account = parseBase58<AccountID>(params.asString());
|
||||||
@@ -645,7 +645,7 @@ parseDID(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return keylet::did(*account).key;
|
return keylet::did(*account).key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseOracle(Json::Value const& params, Json::Value& jvResult)
|
parseOracle(Json::Value const& params, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (!params.isObject())
|
if (!params.isObject())
|
||||||
@@ -699,7 +699,7 @@ parseOracle(Json::Value const& params, Json::Value& jvResult)
|
|||||||
return keylet::oracle(*account, *documentID).key;
|
return keylet::oracle(*account, *documentID).key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseCredential(Json::Value const& cred, Json::Value& jvResult)
|
parseCredential(Json::Value const& cred, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (cred.isString())
|
if (cred.isString())
|
||||||
@@ -738,7 +738,7 @@ parseCredential(Json::Value const& cred, Json::Value& jvResult)
|
|||||||
.key;
|
.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseMPTokenIssuance(
|
parseMPTokenIssuance(
|
||||||
Json::Value const& unparsedMPTIssuanceID,
|
Json::Value const& unparsedMPTIssuanceID,
|
||||||
Json::Value& jvResult)
|
Json::Value& jvResult)
|
||||||
@@ -759,7 +759,7 @@ parseMPTokenIssuance(
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint256>
|
static std::optional<uint256>
|
||||||
parseMPToken(Json::Value const& mptJson, Json::Value& jvResult)
|
parseMPToken(Json::Value const& mptJson, Json::Value& jvResult)
|
||||||
{
|
{
|
||||||
if (!mptJson.isObject())
|
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 =
|
using FunctionType =
|
||||||
std::optional<uint256> (*)(Json::Value const&, Json::Value&);
|
std::function<std::optional<uint256>(Json::Value const&, Json::Value&)>;
|
||||||
|
|
||||||
struct LedgerEntry
|
struct LedgerEntry
|
||||||
{
|
{
|
||||||
@@ -851,6 +893,9 @@ doLedgerEntry(RPC::JsonContext& context)
|
|||||||
{jss::offer, parseOffer, ltOFFER},
|
{jss::offer, parseOffer, ltOFFER},
|
||||||
{jss::oracle, parseOracle, ltORACLE},
|
{jss::oracle, parseOracle, ltORACLE},
|
||||||
{jss::payment_channel, parsePaymentChannel, ltPAYCHAN},
|
{jss::payment_channel, parsePaymentChannel, ltPAYCHAN},
|
||||||
|
{jss::permissioned_domain,
|
||||||
|
parsePermissionedDomains,
|
||||||
|
ltPERMISSIONED_DOMAIN},
|
||||||
{jss::ripple_state, parseRippleState, ltRIPPLE_STATE},
|
{jss::ripple_state, parseRippleState, ltRIPPLE_STATE},
|
||||||
// This is an alias, since the `ledger_data` filter uses jss::state
|
// This is an alias, since the `ledger_data` filter uses jss::state
|
||||||
{jss::state, parseRippleState, ltRIPPLE_STATE},
|
{jss::state, parseRippleState, ltRIPPLE_STATE},
|
||||||
@@ -891,6 +936,7 @@ doLedgerEntry(RPC::JsonContext& context)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found)
|
if (!found)
|
||||||
{
|
{
|
||||||
if (context.apiVersion < 2u)
|
if (context.apiVersion < 2u)
|
||||||
@@ -965,7 +1011,7 @@ doLedgerEntryGrpc(
|
|||||||
grpc::Status status = grpc::Status::OK;
|
grpc::Status status = grpc::Status::OK;
|
||||||
|
|
||||||
std::shared_ptr<ReadView const> ledger;
|
std::shared_ptr<ReadView const> ledger;
|
||||||
if (auto status = RPC::ledgerFromRequest(ledger, context))
|
if (auto const status = RPC::ledgerFromRequest(ledger, context))
|
||||||
{
|
{
|
||||||
grpc::Status errorStatus;
|
grpc::Status errorStatus;
|
||||||
if (status.toErrorCode() == rpcINVALID_PARAMS)
|
if (status.toErrorCode() == rpcINVALID_PARAMS)
|
||||||
@@ -996,8 +1042,7 @@ doLedgerEntryGrpc(
|
|||||||
grpc::StatusCode::NOT_FOUND, "object not found"};
|
grpc::StatusCode::NOT_FOUND, "object not found"};
|
||||||
return {response, errorStatus};
|
return {response, errorStatus};
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
Serializer s;
|
Serializer s;
|
||||||
sleNode->add(s);
|
sleNode->add(s);
|
||||||
|
|
||||||
@@ -1006,6 +1051,5 @@ doLedgerEntryGrpc(
|
|||||||
stateObject.set_key(request.key());
|
stateObject.set_key(request.key());
|
||||||
*(response.mutable_ledger()) = request.ledger();
|
*(response.mutable_ledger()) = request.ledger();
|
||||||
return {response, status};
|
return {response, status};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
Reference in New Issue
Block a user