Add PermissionDelegation feature (#5354)

This change implements the account permission delegation described in XLS-75d, see https://github.com/XRPLF/XRPL-Standards/pull/257.

* Introduces transaction-level and granular permissions that can be delegated to other accounts.
* Adds `DelegateSet` transaction to grant specified permissions to another account.
* Adds `ltDelegate` ledger object to maintain the permission list for delegating/delegated account pair.
* Adds an optional `Delegate` field in common fields, allowing a delegated account to send transactions on behalf of the delegating account within the granted permission scope. The `Account` field remains the delegating account; the `Delegate` field specifies the delegated account. The transaction is signed by the delegated account.
This commit is contained in:
yinyiqian1
2025-05-08 06:14:02 -04:00
committed by GitHub
parent 9ec2d7f8ff
commit 2db2791805
49 changed files with 2976 additions and 91 deletions

View File

@@ -107,6 +107,7 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
{rpcSRC_ACT_MALFORMED, "srcActMalformed", "Source account is malformed.", 400},
{rpcSRC_ACT_MISSING, "srcActMissing", "Source account not provided.", 400},
{rpcSRC_ACT_NOT_FOUND, "srcActNotFound", "Source account not found.", 404},
{rpcDELEGATE_ACT_NOT_FOUND, "delegateActNotFound", "Delegate account not found.", 404},
{rpcSRC_CUR_MALFORMED, "srcCurMalformed", "Source currency is malformed.", 400},
{rpcSRC_ISR_MALFORMED, "srcIsrMalformed", "Source issuer is malformed.", 400},
{rpcSTREAM_MALFORMED, "malformedStream", "Stream malformed.", 400},

View File

@@ -94,6 +94,7 @@ enum class LedgerNameSpace : std::uint16_t {
MPTOKEN = 't',
CREDENTIAL = 'D',
PERMISSIONED_DOMAIN = 'm',
DELEGATE = 'E',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -452,6 +453,14 @@ amm(uint256 const& id) noexcept
return {ltAMM, id};
}
Keylet
delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept
{
return {
ltDELEGATE,
indexHash(LedgerNameSpace::DELEGATE, account, authorizedAccount)};
}
Keylet
bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType)
{

View File

@@ -154,6 +154,10 @@ InnerObjectFormats::InnerObjectFormats()
{sfIssuer, soeREQUIRED},
{sfCredentialType, soeREQUIRED},
});
add(sfPermission.jsonName.c_str(),
sfPermission.getCode(),
{{sfPermissionValue, soeREQUIRED}});
}
InnerObjectFormats const&

View File

@@ -0,0 +1,148 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 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 <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
Permission::Permission()
{
delegatableTx_ = {
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, delegatable, fields) {value, delegatable},
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
};
granularPermissionMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {#type, type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
granularNameMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, #type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
granularTxTypeMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, txType},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
for ([[maybe_unused]] auto const& permission : granularPermissionMap_)
XRPL_ASSERT(
permission.second > UINT16_MAX,
"ripple::Permission::granularPermissionMap_ : granular permission "
"value must not exceed the maximum uint16_t value.");
}
Permission const&
Permission::getInstance()
{
static Permission const instance;
return instance;
}
std::optional<std::uint32_t>
Permission::getGranularValue(std::string const& name) const
{
auto const it = granularPermissionMap_.find(name);
if (it != granularPermissionMap_.end())
return static_cast<uint32_t>(it->second);
return std::nullopt;
}
std::optional<std::string>
Permission::getGranularName(GranularPermissionType const& value) const
{
auto const it = granularNameMap_.find(value);
if (it != granularNameMap_.end())
return it->second;
return std::nullopt;
}
std::optional<TxType>
Permission::getGranularTxType(GranularPermissionType const& gpType) const
{
auto const it = granularTxTypeMap_.find(gpType);
if (it != granularTxTypeMap_.end())
return it->second;
return std::nullopt;
}
bool
Permission::isDelegatable(std::uint32_t const& permissionValue) const
{
auto const granularPermission =
getGranularName(static_cast<GranularPermissionType>(permissionValue));
if (granularPermission)
// granular permissions are always allowed to be delegated
return true;
auto const it = delegatableTx_.find(permissionValue - 1);
if (it != delegatableTx_.end() && it->second == Delegation::notDelegatable)
return false;
return true;
}
uint32_t
Permission::txToPermissionType(TxType const& type) const
{
return static_cast<uint32_t>(type) + 1;
}
TxType
Permission::permissionToTxType(uint32_t const& value) const
{
return static_cast<TxType>(value - 1);
}
} // namespace ripple

View File

@@ -22,6 +22,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STInteger.h>
@@ -177,6 +178,27 @@ template <>
Json::Value
STUInt32::getJson(JsonOptions) const
{
if (getFName() == sfPermissionValue)
{
auto const permissionValue =
static_cast<GranularPermissionType>(value_);
auto const granular =
Permission::getInstance().getGranularName(permissionValue);
if (granular)
{
return *granular;
}
else
{
auto const txType =
Permission::getInstance().permissionToTxType(value_);
auto item = TxFormats::getInstance().findByType(txType);
if (item != nullptr)
return item->getName();
}
}
return value_;
}

View File

@@ -27,6 +27,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/STAmount.h>
@@ -373,10 +374,35 @@ parseLeaf(
{
if (value.isString())
{
ret = detail::make_stvar<STUInt32>(
field,
beast::lexicalCastThrow<std::uint32_t>(
value.asString()));
if (field == sfPermissionValue)
{
std::string const strValue = value.asString();
auto const granularPermission =
Permission::getInstance().getGranularValue(
strValue);
if (granularPermission)
{
ret = detail::make_stvar<STUInt32>(
field, *granularPermission);
}
else
{
auto const& txType =
TxFormats::getInstance().findTypeByName(
strValue);
ret = detail::make_stvar<STUInt32>(
field,
Permission::getInstance().txToPermissionType(
txType));
}
}
else
{
ret = detail::make_stvar<STUInt32>(
field,
beast::lexicalCastThrow<std::uint32_t>(
value.asString()));
}
}
else if (value.isInt())
{

View File

@@ -46,6 +46,7 @@ TxFormats::TxFormats()
{sfTxnSignature, soeOPTIONAL},
{sfSigners, soeOPTIONAL}, // submit_multisigned
{sfNetworkID, soeOPTIONAL},
{sfDelegate, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")
@@ -54,7 +55,7 @@ TxFormats::TxFormats()
#undef TRANSACTION
#define UNWRAP(...) __VA_ARGS__
#define TRANSACTION(tag, value, name, fields) \
#define TRANSACTION(tag, value, name, delegatable, fields) \
add(jss::name, tag, UNWRAP fields, commonFields);
#include <xrpl/protocol/detail/transactions.macro>

File diff suppressed because it is too large Load Diff

62
src/test/jtx/delegate.h Normal file
View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 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/Account.h>
#include <test/jtx/Env.h>
namespace ripple {
namespace test {
namespace jtx {
namespace delegate {
Json::Value
set(jtx::Account const& account,
jtx::Account const& authorize,
std::vector<std::string> const& permissions);
Json::Value
entry(
jtx::Env& env,
jtx::Account const& account,
jtx::Account const& authorize);
struct as
{
private:
jtx::Account delegate_;
public:
explicit as(jtx::Account const& account) : delegate_(account)
{
}
void
operator()(jtx::Env&, jtx::JTx& jtx) const
{
jtx.jv[sfDelegate.jsonName] = delegate_.human();
}
};
} // namespace delegate
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -84,6 +84,18 @@ private:
case asfAllowTrustLineClawback:
mask_ |= lsfAllowTrustLineClawback;
break;
case asfDisallowIncomingCheck:
mask_ |= lsfDisallowIncomingCheck;
break;
case asfDisallowIncomingNFTokenOffer:
mask_ |= lsfDisallowIncomingNFTokenOffer;
break;
case asfDisallowIncomingPayChan:
mask_ |= lsfDisallowIncomingPayChan;
break;
case asfDisallowIncomingTrustline:
mask_ |= lsfDisallowIncomingTrustline;
break;
default:
Throw<std::runtime_error>("unknown flag");
}

View File

@@ -465,7 +465,9 @@ Env::autofill_sig(JTx& jt)
return jt.signer(*this, jt);
if (!jt.fill_sig)
return;
auto const account = lookup(jv[jss::Account].asString());
auto const account = jv.isMember(sfDelegate.jsonName)
? lookup(jv[sfDelegate.jsonName].asString())
: lookup(jv[jss::Account].asString());
if (!app().checkSigs())
{
jv[jss::SigningPubKey] = strHex(account.pk().slice());

View File

@@ -0,0 +1,67 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 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/delegate.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
namespace jtx {
namespace delegate {
Json::Value
set(jtx::Account const& account,
jtx::Account const& authorize,
std::vector<std::string> const& permissions)
{
Json::Value jv;
jv[jss::TransactionType] = jss::DelegateSet;
jv[jss::Account] = account.human();
jv[sfAuthorize.jsonName] = authorize.human();
Json::Value permissionsJson(Json::arrayValue);
for (auto const& permission : permissions)
{
Json::Value permissionValue;
permissionValue[sfPermissionValue.jsonName] = permission;
Json::Value permissionObj;
permissionObj[sfPermission.jsonName] = permissionValue;
permissionsJson.append(permissionObj);
}
jv[sfPermissions.jsonName] = permissionsJson;
return jv;
}
Json::Value
entry(jtx::Env& env, jtx::Account const& account, jtx::Account const& authorize)
{
Json::Value jvParams;
jvParams[jss::ledger_index] = jss::validated;
jvParams[jss::delegate][jss::account] = account.human();
jvParams[jss::delegate][jss::authorize] = authorize.human();
return env.rpc("json", "ledger_entry", to_string(jvParams));
}
} // namespace delegate
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -233,6 +233,8 @@ MPTTester::set(MPTSet const& arg)
}
if (arg.holder)
jv[sfHolder] = arg.holder->human();
if (arg.delegate)
jv[sfDelegate] = arg.delegate->human();
if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0))
{
auto require = [&](std::optional<Account> const& holder,

View File

@@ -136,6 +136,7 @@ struct MPTSet
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<TER> err = std::nullopt;
};

View File

@@ -2042,6 +2042,78 @@ static constexpr TxnTestData txnTestArray[] = {
"Cannot specify differing 'Amount' and 'DeliverMax'",
"Cannot specify differing 'Amount' and 'DeliverMax'"}}},
{"Minimal delegated transaction.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "a",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
"TransactionType": "Payment",
"Delegate": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"
}
})",
{{"",
"",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
{"Delegate not well formed.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "a",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi",
"TransactionType": "Payment",
"Delegate": "NotAnAccount"
}
})",
{{"Invalid field 'tx_json.Delegate'.",
"Invalid field 'tx_json.Delegate'.",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
{"Delegate not in ledger.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "a",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi",
"TransactionType": "Payment",
"Delegate": "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd"
}
})",
{{"Delegate account not found.",
"Delegate account not found.",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
{"Delegate and secret not match.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "aa",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi",
"TransactionType": "Payment",
"Delegate": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"
}
})",
{{"Secret does not match account.",
"Secret does not match account.",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
};
class JSONRPC_test : public beast::unit_test::suite

View File

@@ -20,6 +20,7 @@
#include <test/jtx.h>
#include <test/jtx/Oracle.h>
#include <test/jtx/attester.h>
#include <test/jtx/delegate.h>
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>
@@ -439,6 +440,116 @@ class LedgerEntry_test : public beast::unit_test::suite
}
}
void
testLedgerEntryDelegate()
{
testcase("ledger_entry Delegate");
using namespace test::jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(delegate::set(alice, bob, {"Payment", "CheckCreate"}));
env.close();
std::string const ledgerHash{to_string(env.closed()->info().hash)};
std::string delegateIndex;
{
// Request by account and authorize
Json::Value jvParams;
jvParams[jss::delegate][jss::account] = alice.human();
jvParams[jss::delegate][jss::authorize] = bob.human();
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(
jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
delegateIndex = jrr[jss::node][jss::index].asString();
}
{
// Request by index.
Json::Value jvParams;
jvParams[jss::delegate] = delegateIndex;
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(
jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
}
{
// Malformed request: delegate neither object nor string.
Json::Value jvParams;
jvParams[jss::delegate] = 5;
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedRequest", "");
}
{
// Malformed request: delegate not hex string.
Json::Value jvParams;
jvParams[jss::delegate] = "0123456789ABCDEFG";
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedRequest", "");
}
{
// Malformed request: account not a string
Json::Value jvParams;
jvParams[jss::delegate][jss::account] = 5;
jvParams[jss::delegate][jss::authorize] = bob.human();
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedAddress", "");
}
{
// Malformed request: authorize not a string
Json::Value jvParams;
jvParams[jss::delegate][jss::account] = alice.human();
jvParams[jss::delegate][jss::authorize] = 5;
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedAddress", "");
}
{
// this lambda function is used test malformed account and authroize
auto testMalformedAccount =
[&](std::optional<std::string> const& account,
std::optional<std::string> const& authorize,
std::string const& error) {
Json::Value jvParams;
jvParams[jss::ledger_hash] = ledgerHash;
if (account)
jvParams[jss::delegate][jss::account] = *account;
if (authorize)
jvParams[jss::delegate][jss::authorize] = *authorize;
auto const jrr = env.rpc(
"json",
"ledger_entry",
to_string(jvParams))[jss::result];
checkErrorValue(jrr, error, "");
};
// missing account
testMalformedAccount(std::nullopt, bob.human(), "malformedRequest");
// missing authorize
testMalformedAccount(
alice.human(), std::nullopt, "malformedRequest");
// malformed account
testMalformedAccount("-", bob.human(), "malformedAddress");
// malformed authorize
testMalformedAccount(alice.human(), "-", "malformedAddress");
}
}
void
testLedgerEntryDepositPreauth()
{
@@ -2266,6 +2377,7 @@ public:
testLedgerEntryAccountRoot();
testLedgerEntryCheck();
testLedgerEntryCredentials();
testLedgerEntryDelegate();
testLedgerEntryDepositPreauth();
testLedgerEntryDepositPreauthCred();
testLedgerEntryDirectory();

View File

@@ -20,6 +20,7 @@
#include <test/jtx.h>
#include <test/jtx/Oracle.h>
#include <test/jtx/attester.h>
#include <test/jtx/delegate.h>
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>

View File

@@ -17,8 +17,8 @@
*/
//==============================================================================
#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
#define RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED
#define RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED
#include <xrpld/ledger/View.h>
@@ -127,4 +127,4 @@ isOnlyLiquidityProvider(
} // namespace ripple
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 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.
*/
//==============================================================================
#ifndef RIPPLE_APP_MISC_DELEGATEUTILS_H_INCLUDED
#define RIPPLE_APP_MISC_DELEGATEUTILS_H_INCLUDED
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
namespace ripple {
/**
* Check if the delegate account has permission to execute the transaction.
* @param delegate The delegate account.
* @param tx The transaction that the delegate account intends to execute.
* @return tesSUCCESS if the transaction is allowed, tecNO_PERMISSION if not.
*/
TER
checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx);
/**
* Load the granular permissions granted to the delegate account for the
* specified transaction type
* @param delegate The delegate account.
* @param type Used to determine which granted granular permissions to load,
* based on the transaction type.
* @param granularPermissions Granted granular permissions tied to the
* transaction type.
*/
void
loadGranularPermission(
std::shared_ptr<SLE const> const& delegate,
TxType const& type,
std::unordered_set<GranularPermissionType>& granularPermissions);
} // namespace ripple
#endif // RIPPLE_APP_MISC_DELEGATEUTILS_H_INCLUDED

View File

@@ -0,0 +1,66 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 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/DelegateUtils.h>
#include <xrpl/protocol/STArray.h>
namespace ripple {
TER
checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx)
{
if (!delegate)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
auto const permissionArray = delegate->getFieldArray(sfPermissions);
auto const txPermission = tx.getTxnType() + 1;
for (auto const& permission : permissionArray)
{
auto const permissionValue = permission[sfPermissionValue];
if (permissionValue == txPermission)
return tesSUCCESS;
}
return tecNO_PERMISSION;
}
void
loadGranularPermission(
std::shared_ptr<SLE const> const& delegate,
TxType const& txType,
std::unordered_set<GranularPermissionType>& granularPermissions)
{
if (!delegate)
return; // LCOV_EXCL_LINE
auto const permissionArray = delegate->getFieldArray(sfPermissions);
for (auto const& permission : permissionArray)
{
auto const permissionValue = permission[sfPermissionValue];
auto const granularValue =
static_cast<GranularPermissionType>(permissionValue);
auto const& type =
Permission::getInstance().getGranularTxType(granularValue);
if (type && *type == txType)
granularPermissions.insert(granularValue);
}
}
} // namespace ripple

View File

@@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 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/DelegateSet.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/st.h>
namespace ripple {
NotTEC
DelegateSet::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featurePermissionDelegation))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto const& permissions = ctx.tx.getFieldArray(sfPermissions);
if (permissions.size() > permissionMaxSize)
return temARRAY_TOO_LARGE;
// can not authorize self
if (ctx.tx[sfAccount] == ctx.tx[sfAuthorize])
return temMALFORMED;
std::unordered_set<std::uint32_t> permissionSet;
for (auto const& permission : permissions)
{
if (!permissionSet.insert(permission[sfPermissionValue]).second)
return temMALFORMED;
}
return preflight2(ctx);
}
TER
DelegateSet::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.exists(keylet::account(ctx.tx[sfAccount])))
return terNO_ACCOUNT; // LCOV_EXCL_LINE
if (!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize])))
return terNO_ACCOUNT;
auto const& permissions = ctx.tx.getFieldArray(sfPermissions);
for (auto const& permission : permissions)
{
auto const permissionValue = permission[sfPermissionValue];
if (!Permission::getInstance().isDelegatable(permissionValue))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
DelegateSet::doApply()
{
auto const sleOwner = ctx_.view().peek(keylet::account(account_));
if (!sleOwner)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const& authAccount = ctx_.tx[sfAuthorize];
auto const delegateKey = keylet::delegate(account_, authAccount);
auto sle = ctx_.view().peek(delegateKey);
if (sle)
{
auto const& permissions = ctx_.tx.getFieldArray(sfPermissions);
if (permissions.empty())
// if permissions array is empty, delete the ledger object.
return deleteDelegate(view(), sle, account_, j_);
sle->setFieldArray(sfPermissions, permissions);
ctx_.view().update(sle);
return tesSUCCESS;
}
STAmount const reserve{ctx_.view().fees().accountReserve(
sleOwner->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
auto const& permissions = ctx_.tx.getFieldArray(sfPermissions);
if (!permissions.empty())
{
sle = std::make_shared<SLE>(delegateKey);
sle->setAccountID(sfAccount, account_);
sle->setAccountID(sfAuthorize, authAccount);
sle->setFieldArray(sfPermissions, permissions);
auto const page = ctx_.view().dirInsert(
keylet::ownerDir(account_),
delegateKey,
describeOwnerDir(account_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*sle)[sfOwnerNode] = *page;
ctx_.view().insert(sle);
adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal);
}
return tesSUCCESS;
}
TER
DelegateSet::deleteDelegate(
ApplyView& view,
std::shared_ptr<SLE> const& sle,
AccountID const& account,
beast::Journal j)
{
if (!sle)
return tecINTERNAL; // LCOV_EXCL_LINE
if (!view.dirRemove(
keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Delegate from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(account));
if (!sleOwner)
return tecINTERNAL; // LCOV_EXCL_LINE
adjustOwnerCount(view, sleOwner, -1, j);
view.erase(sle);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 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.
*/
//==============================================================================
#ifndef RIPPLE_TX_DELEGATESET_H_INCLUDED
#define RIPPLE_TX_DELEGATESET_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class DelegateSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DelegateSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
// Interface used by DeleteAccount
static TER
deleteDelegate(
ApplyView& view,
std::shared_ptr<SLE> const& sle,
AccountID const& account,
beast::Journal j);
};
} // namespace ripple
#endif

View File

@@ -19,6 +19,7 @@
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/tx/detail/DID.h>
#include <xrpld/app/tx/detail/DelegateSet.h>
#include <xrpld/app/tx/detail/DeleteAccount.h>
#include <xrpld/app/tx/detail/DeleteOracle.h>
#include <xrpld/app/tx/detail/DepositPreauth.h>
@@ -180,6 +181,18 @@ removeCredentialFromLedger(
return credentials::deleteSLE(view, sleDel, j);
}
TER
removeDelegateFromLedger(
Application& app,
ApplyView& view,
AccountID const& account,
uint256 const& delIndex,
std::shared_ptr<SLE> const& sleDel,
beast::Journal j)
{
return DelegateSet::deleteDelegate(view, sleDel, account, j);
}
// Return nullptr if the LedgerEntryType represents an obligation that can't
// be deleted. Otherwise return the pointer to the function that can delete
// the non-obligation
@@ -204,6 +217,8 @@ nonObligationDeleter(LedgerEntryType t)
return removeOracleFromLedger;
case ltCREDENTIAL:
return removeCredentialFromLedger;
case ltDELEGATE:
return removeDelegateFromLedger;
default:
return nullptr;
}

View File

@@ -387,6 +387,7 @@ AccountRootsDeletedClean::finalize(
view.rules().enabled(featureInvariantsV1_1);
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
(void)enforce;
if (auto const sle = view.read(keylet))
{
// Finding the object is bad
@@ -463,6 +464,7 @@ LedgerEntryTypesMatch::visitEntry(
switch (after->getType())
{
case ltACCOUNT_ROOT:
case ltDELEGATE:
case ltDIR_NODE:
case ltRIPPLE_STATE:
case ltTICKET:

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
#include <xrpl/protocol/Feature.h>
@@ -50,6 +51,43 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
if (checkTxPermission(sle, tx) == tesSUCCESS)
return tesSUCCESS;
auto const txFlags = tx.getFlags();
// this is added in case more flags will be added for MPTokenIssuanceSet
// in the future. Currently unreachable.
if (txFlags & tfMPTokenIssuanceSetPermissionMask)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions);
if (txFlags & tfMPTLock &&
!granularPermissions.contains(MPTokenIssuanceLock))
return tecNO_PERMISSION;
if (txFlags & tfMPTUnlock &&
!granularPermissions.contains(MPTokenIssuanceUnlock))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
{

View File

@@ -36,6 +36,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/paths/RippleCalc.h>
#include <xrpld/app/tx/detail/Payment.h>
#include <xrpld/ledger/View.h>
@@ -238,6 +239,39 @@ Payment::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
Payment::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
if (checkTxPermission(sle, tx) == tesSUCCESS)
return tesSUCCESS;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttPAYMENT, granularPermissions);
auto const& dstAmount = tx.getFieldAmount(sfAmount);
auto const& amountIssue = dstAmount.issue();
if (granularPermissions.contains(PaymentMint) && !isXRP(amountIssue) &&
amountIssue.account == tx[sfAccount])
return tesSUCCESS;
if (granularPermissions.contains(PaymentBurn) && !isXRP(amountIssue) &&
amountIssue.account == tx[sfDestination])
return tesSUCCESS;
return tecNO_PERMISSION;
}
TER
Payment::preclaim(PreclaimContext const& ctx)
{

View File

@@ -45,6 +45,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/SetAccount.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/View.h>
@@ -188,6 +189,61 @@ SetAccount::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
SetAccount::checkPermission(ReadView const& view, STTx const& tx)
{
// SetAccount is prohibited to be granted on a transaction level,
// but some granular permissions are allowed.
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions);
auto const uSetFlag = tx.getFieldU32(sfSetFlag);
auto const uClearFlag = tx.getFieldU32(sfClearFlag);
auto const uTxFlags = tx.getFlags();
// We don't support any flag based granular permission under
// AccountSet transaction. If any delegated account is trying to
// update the flag on behalf of another account, it is not
// authorized.
if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags != tfFullyCanonicalSig)
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfEmailHash) &&
!granularPermissions.contains(AccountEmailHashSet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfWalletLocator) ||
tx.isFieldPresent(sfNFTokenMinter))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfMessageKey) &&
!granularPermissions.contains(AccountMessageKeySet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfDomain) &&
!granularPermissions.contains(AccountDomainSet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfTransferRate) &&
!granularPermissions.contains(AccountTransferRateSet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfTickSize) &&
!granularPermissions.contains(AccountTickSizeSet))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
SetAccount::preclaim(PreclaimContext const& ctx)
{

View File

@@ -41,6 +41,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/SetTrust.h>
#include <xrpld/ledger/View.h>
@@ -127,6 +128,69 @@ SetTrust::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
SetTrust::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
if (checkTxPermission(sle, tx) == tesSUCCESS)
return tesSUCCESS;
std::uint32_t const txFlags = tx.getFlags();
// Currently we only support TrustlineAuthorize, TrustlineFreeze and
// TrustlineUnfreeze granular permission. Setting other flags returns
// error.
if (txFlags & tfTrustSetPermissionMask)
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut))
return tecNO_PERMISSION;
auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount);
auto const sleRippleState = view.read(keylet::line(
tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.getCurrency()));
// if the trustline does not exist, granular permissions are
// not allowed to create trustline
if (!sleRippleState)
return tecNO_PERMISSION;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttTRUST_SET, granularPermissions);
if (txFlags & tfSetfAuth &&
!granularPermissions.contains(TrustlineAuthorize))
return tecNO_PERMISSION;
if (txFlags & tfSetFreeze && !granularPermissions.contains(TrustlineFreeze))
return tecNO_PERMISSION;
if (txFlags & tfClearFreeze &&
!granularPermissions.contains(TrustlineUnfreeze))
return tecNO_PERMISSION;
// updating LimitAmount is not allowed only with granular permissions,
// unless there's a new granular permission for this in the future.
auto const curLimit = tx[sfAccount] > saLimitAmount.getIssuer()
? sleRippleState->getFieldAmount(sfHighLimit)
: sleRippleState->getFieldAmount(sfLowLimit);
STAmount saLimitAllow = saLimitAmount;
saLimitAllow.setIssuer(tx[sfAccount]);
if (curLimit != saLimitAllow)
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
SetTrust::preclaim(PreclaimContext const& ctx)
{

View File

@@ -38,6 +38,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -19,6 +19,7 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
@@ -89,6 +90,15 @@ preflight1(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfDelegate))
{
if (!ctx.rules.enabled(featurePermissionDelegation))
return temDISABLED;
if (ctx.tx[sfDelegate] == ctx.tx[sfAccount])
return temBAD_SIGNER;
}
auto const ret = preflight0(ctx);
if (!isTesSuccess(ret))
return ret;
@@ -190,6 +200,22 @@ Transactor::Transactor(ApplyContext& ctx)
{
}
TER
Transactor::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
return checkTxPermission(sle, tx);
}
XRPAmount
Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
{
@@ -246,7 +272,9 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
if (feePaid == beast::zero)
return tesSUCCESS;
auto const id = ctx.tx.getAccountID(sfAccount);
auto const id = ctx.tx.isFieldPresent(sfDelegate)
? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
auto const sle = ctx.view.read(keylet::account(id));
if (!sle)
return terNO_ACCOUNT;
@@ -276,17 +304,32 @@ Transactor::payFee()
{
auto const feePaid = ctx_.tx[sfFee].xrp();
auto const sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
if (ctx_.tx.isFieldPresent(sfDelegate))
{
// Delegated transactions are paid by the delegated account.
auto const delegate = ctx_.tx.getAccountID(sfDelegate);
auto const delegatedSle = view().peek(keylet::account(delegate));
if (!delegatedSle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Deduct the fee, so it's not available during the transaction.
// Will only write the account back if the transaction succeeds.
delegatedSle->setFieldAmount(
sfBalance, delegatedSle->getFieldAmount(sfBalance) - feePaid);
view().update(delegatedSle);
}
else
{
auto const sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
mSourceBalance -= feePaid;
sle->setFieldAmount(sfBalance, mSourceBalance);
// Deduct the fee, so it's not available during the transaction.
// Will only write the account back if the transaction succeeds.
// VFALCO Should we call view().rawDestroyXRP() here as well?
mSourceBalance -= feePaid;
sle->setFieldAmount(sfBalance, mSourceBalance);
// VFALCO Should we call view().rawDestroyXRP() here as well?
}
return tesSUCCESS;
}
@@ -542,7 +585,9 @@ Transactor::checkSingleSign(PreclaimContext const& ctx)
}
// Look up the account.
auto const idAccount = ctx.tx.getAccountID(sfAccount);
auto const idAccount = ctx.tx.isFieldPresent(sfDelegate)
? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
if (!sleAccount)
return terNO_ACCOUNT;
@@ -612,7 +657,9 @@ Transactor::checkSingleSign(PreclaimContext const& ctx)
NotTEC
Transactor::checkMultiSign(PreclaimContext const& ctx)
{
auto const id = ctx.tx.getAccountID(sfAccount);
auto const id = ctx.tx.isFieldPresent(sfDelegate)
? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
// Get mTxnAccountID's SignerList and Quorum.
std::shared_ptr<STLedgerEntry const> sleAccountSigners =
ctx.view.read(keylet::signers(id));
@@ -870,15 +917,22 @@ Transactor::reset(XRPAmount fee)
// is missing then we can't very well charge it a fee, can we?
return {tefINTERNAL, beast::zero};
auto const balance = txnAcct->getFieldAmount(sfBalance).xrp();
auto const payerSle = ctx_.tx.isFieldPresent(sfDelegate)
? view().peek(keylet::account(ctx_.tx.getAccountID(sfDelegate)))
: txnAcct;
if (!payerSle)
return {tefINTERNAL, beast::zero}; // LCOV_EXCL_LINE
auto const balance = payerSle->getFieldAmount(sfBalance).xrp();
// balance should have already been checked in checkFee / preFlight.
XRPL_ASSERT(
balance != beast::zero && (!view().open() || balance >= fee),
"ripple::Transactor::reset : valid balance");
// We retry/reject the transaction if the account balance is zero or we're
// applying against an open ledger and the balance is less than the fee
// We retry/reject the transaction if the account balance is zero or
// we're applying against an open ledger and the balance is less than
// the fee
if (fee > balance)
fee = balance;
@@ -888,13 +942,17 @@ Transactor::reset(XRPAmount fee)
// If for some reason we are unable to consume the ticket or sequence
// then the ledger is corrupted. Rather than make things worse we
// reject the transaction.
txnAcct->setFieldAmount(sfBalance, balance - fee);
payerSle->setFieldAmount(sfBalance, balance - fee);
TER const ter{consumeSeqProxy(txnAcct)};
XRPL_ASSERT(
isTesSuccess(ter), "ripple::Transactor::reset : result is tesSUCCESS");
if (isTesSuccess(ter))
{
view().update(txnAcct);
if (payerSle != txnAcct)
view().update(payerSle);
}
return {ter, fee};
}

View File

@@ -24,6 +24,7 @@
#include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
@@ -149,6 +150,9 @@ public:
// after checkSeq/Fee/Sign.
return tesSUCCESS;
}
static TER
checkPermission(ReadView const& view, STTx const& tx);
/////////////////////////////////////////////////////
// Interface used by DeleteAccount

View File

@@ -36,6 +36,7 @@
#include <xrpld/app/tx/detail/CreateTicket.h>
#include <xrpld/app/tx/detail/Credentials.h>
#include <xrpld/app/tx/detail/DID.h>
#include <xrpld/app/tx/detail/DelegateSet.h>
#include <xrpld/app/tx/detail/DeleteAccount.h>
#include <xrpld/app/tx/detail/DeleteOracle.h>
#include <xrpld/app/tx/detail/DepositPreauth.h>
@@ -89,8 +90,8 @@ with_txn_type(TxType txnType, F&& f)
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, fields) \
case tag: \
#define TRANSACTION(tag, value, name, delegatable, fields) \
case tag: \
return f.template operator()<name>();
#include <xrpl/protocol/detail/transactions.macro>
@@ -193,6 +194,11 @@ invoke_preclaim(PreclaimContext const& ctx)
result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx));
if (result != tesSUCCESS)
return result;
result = T::checkPermission(ctx.view, ctx.tx);
if (result != tesSUCCESS)
return result;

View File

@@ -531,10 +531,40 @@ transactionPreProcessImpl(
if (!signingArgs.isMultiSigning())
{
// Make sure the account and secret belong together.
auto const err = acctMatchesPubKey(sle, srcAddressID, pk);
if (tx_json.isMember(sfDelegate.jsonName))
{
// Delegated transaction
auto const delegateJson = tx_json[sfDelegate.jsonName];
auto const ptrDelegatedAddressID = delegateJson.isString()
? parseBase58<AccountID>(delegateJson.asString())
: std::nullopt;
if (err != rpcSUCCESS)
return rpcError(err);
if (!ptrDelegatedAddressID)
{
return RPC::make_error(
rpcSRC_ACT_MALFORMED,
RPC::invalid_field_message("tx_json.Delegate"));
}
auto delegatedAddressID = *ptrDelegatedAddressID;
auto delegatedSle = app.openLedger().current()->read(
keylet::account(delegatedAddressID));
if (!delegatedSle)
return rpcError(rpcDELEGATE_ACT_NOT_FOUND);
auto const err =
acctMatchesPubKey(delegatedSle, delegatedAddressID, pk);
if (err != rpcSUCCESS)
return rpcError(err);
}
else
{
auto const err = acctMatchesPubKey(sle, srcAddressID, pk);
if (err != rpcSUCCESS)
return rpcError(err);
}
}
}

View File

@@ -230,6 +230,46 @@ parseAuthorizeCredentials(Json::Value const& jv)
return arr;
}
static std::optional<uint256>
parseDelegate(Json::Value const& params, Json::Value& jvResult)
{
if (!params.isObject())
{
uint256 uNodeIndex;
if (!params.isString() || !uNodeIndex.parseHex(params.asString()))
{
jvResult[jss::error] = "malformedRequest";
return std::nullopt;
}
return uNodeIndex;
}
if (!params.isMember(jss::account) || !params.isMember(jss::authorize))
{
jvResult[jss::error] = "malformedRequest";
return std::nullopt;
}
if (!params[jss::account].isString() || !params[jss::authorize].isString())
{
jvResult[jss::error] = "malformedAddress";
return std::nullopt;
}
auto const account =
parseBase58<AccountID>(params[jss::account].asString());
if (!account)
{
jvResult[jss::error] = "malformedAddress";
return std::nullopt;
}
auto const authorize =
parseBase58<AccountID>(params[jss::authorize].asString());
if (!authorize)
{
jvResult[jss::error] = "malformedAddress";
return std::nullopt;
}
return keylet::delegate(*account, *authorize).key;
}
static std::optional<uint256>
parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult)
{
@@ -884,6 +924,7 @@ doLedgerEntry(RPC::JsonContext& context)
{jss::bridge, parseBridge, ltBRIDGE},
{jss::check, parseCheck, ltCHECK},
{jss::credential, parseCredential, ltCREDENTIAL},
{jss::delegate, parseDelegate, ltDELEGATE},
{jss::deposit_preauth, parseDepositPreauth, ltDEPOSIT_PREAUTH},
{jss::did, parseDID, ltDID},
{jss::directory, parseDirectory, ltDIR_NODE},