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

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>