Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions include/xrpl/protocol/Indexes.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ signerList(AccountID const& account) noexcept;
Keylet
sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept;

/**
* An account's beneficiary designation. One per account.
*/
Keylet
beneficiary(AccountID const& account) noexcept;

/**
* A Check
*/
Expand Down
7 changes: 7 additions & 0 deletions include/xrpl/protocol/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ tenthBipsOfValue(T value, TenthBips<TBips> bips)
return value * bips.value() / kTenthBipsPerUnity.value();
}

/**
* The longest inactivity period a beneficiary designation may require, ten
* years in seconds. Long enough for the intended use and short enough that the
* value still means something.
*/
constexpr std::uint32_t kMaxBeneficiaryTimeLock = 10 * 365 * 24 * 60 * 60;

namespace lending {
/**
* The maximum management fee rate allowed by a loan broker in 1/10 bips.
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/protocol/detail/features.macro
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.

XRPL_FEATURE(Beneficiary, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
Expand Down
15 changes: 15 additions & 0 deletions include/xrpl/protocol/detail/ledger_entries.macro
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfAMMID, SoeOptional}, // pseudo-account designator
{sfVaultID, SoeOptional}, // pseudo-account designator
{sfLoanBrokerID, SoeOptional}, // pseudo-account designator
{sfLastInteraction, SoeOptional},
}))

/** A ledger object which contains a list of object identifiers.
Expand Down Expand Up @@ -649,5 +650,19 @@ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({
{sfSponseeNode, SoeRequired},
}))

/** A designation of an account to receive this account's regular key after a
period of inactivity.

\sa keylet::beneficiary
*/
LEDGER_ENTRY(ltBENEFICIARY, 0x0096, Beneficiary, beneficiary, ({
{sfAccount, SoeRequired},
{sfBeneficiary, SoeRequired},
{sfTimeLock, SoeRequired},
{sfOwnerNode, SoeRequired},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
}))

#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE
3 changes: 3 additions & 0 deletions include/xrpl/protocol/detail/sfields.macro
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82)
TYPED_SFIELD(sfGasPrice, UINT32, 83)
TYPED_SFIELD(sfGas, UINT32, 84)
TYPED_SFIELD(sfGasUsed, UINT32, 85)
TYPED_SFIELD(sfTimeLock, UINT32, 96)
TYPED_SFIELD(sfLastInteraction, UINT32, 97)

// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
Expand Down Expand Up @@ -360,6 +362,7 @@ TYPED_SFIELD(sfHighSponsor, ACCOUNT, 28)
TYPED_SFIELD(sfLowSponsor, ACCOUNT, 29)
TYPED_SFIELD(sfCounterpartySponsor, ACCOUNT, 30)
TYPED_SFIELD(sfSponsee, ACCOUNT, 31)
TYPED_SFIELD(sfBeneficiary, ACCOUNT, 33)

// vector of 256-bit
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::kSmdNever)
Expand Down
11 changes: 11 additions & 0 deletions include/xrpl/protocol/detail/transactions.macro
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,17 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet,
{sfRemainingOwnerCountDelta, SoeOptional},
}))

/** This transaction designates, updates or clears an account's beneficiary. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/beneficiary/BeneficiarySet.h>
#endif
TRANSACTION(ttBENEFICIARY_SET, 119, BeneficiarySet,
({.amendment = featureBeneficiary}),
({
{sfBeneficiary, SoeOptional},
{sfTimeLock, SoeOptional},
}))

/** This system-generated transaction type is used to update the status of the various amendments.

For details, see: https://xrpl.org/amendments.html
Expand Down
7 changes: 7 additions & 0 deletions include/xrpl/tx/Transactor.h
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ class Transactor : public TxInvariantCheck
beast::Journal j);

protected:
/**
* Whether this transaction was signed by the account's beneficiary rather
* than by one of the account's own keys.
*/
bool
signedByBeneficiary() const;

TER
apply();

Expand Down
39 changes: 38 additions & 1 deletion include/xrpl/tx/invariants/InvariantCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,42 @@ class ObjectHasPseudoAccount
};
// additional invariant checks can be declared above and then added to this
// tuple
/**
* @brief Invariant: a beneficiary designation is well formed and paired with
* its timestamp.
*
* The following checks are made for every transaction:
* - An account has a Beneficiary entry if and only if its AccountRoot carries
* sfLastInteraction.
* - The entry's Account is never equal to its Beneficiary, and TimeLock is
* neither zero nor above kMaxBeneficiaryTimeLock.
* - The entry's Account never changes after creation.
* - A Beneficiary entry is deleted only by BeneficiarySet.
* - sfLastInteraction never moves backwards.
*/
class ValidBeneficiary
{
// <before, after>. before is unseated when the entry is being created.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> entries_;

// The accounts whose designation appeared or vanished this transaction, and
// the accounts whose sfLastInteraction did, so the two sets can be compared.
std::set<AccountID> designationAdded_;
std::set<AccountID> designationRemoved_;
std::set<AccountID> stampAdded_;
std::set<AccountID> stampRemoved_;

bool deleted_ = false;
bool stampWentBackwards_ = false;

public:
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};

using InvariantChecks = std::tuple<
TransactionFeeCheck,
AccountRootsNotDeleted,
Expand Down Expand Up @@ -463,7 +499,8 @@ using InvariantChecks = std::tuple<
ValidMPTTransfer,
ObjectHasPseudoAccount,
SponsorshipOwnerCountsMatch,
SponsorshipAccountCountMatchesField>;
SponsorshipAccountCountMatchesField,
ValidBeneficiary>;

/**
* @brief get a tuple of all invariant checks
Expand Down
44 changes: 44 additions & 0 deletions include/xrpl/tx/transactors/beneficiary/BeneficiarySet.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>

namespace xrpl {

class BeneficiarySet : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;

explicit BeneficiarySet(ApplyContext& ctx) : Transactor(ctx)
{
}

static NotTEC
preflight(PreflightContext const& ctx);

static TER
preclaim(PreclaimContext const& ctx);

TER
doApply() override;

void
visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override;

[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};

} // namespace xrpl
7 changes: 7 additions & 0 deletions src/libxrpl/protocol/Indexes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ enum class LedgerNameSpace : std::uint16_t {
LoanBroker = 'l', // lower-case L
Loan = 'L',
Sponsorship = '>',
Beneficiary = 'Y',

// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
Contract [[deprecated]] = 'c',
Expand Down Expand Up @@ -355,6 +356,12 @@ sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept
return {ltSPONSORSHIP, indexHash(LedgerNameSpace::Sponsorship, sponsor, sponsee)};
}

Keylet
beneficiary(AccountID const& account) noexcept
{
return {ltBENEFICIARY, indexHash(LedgerNameSpace::Beneficiary, account)};
}

Keylet
check(AccountID const& id, SeqProxy const& seq) noexcept
{
Expand Down
46 changes: 46 additions & 0 deletions src/libxrpl/tx/Transactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,23 @@ Transactor::preCompute()
XRPL_ASSERT(accountID_ != beast::kZero, "xrpl::Transactor::preCompute : nonzero account");
}

bool
Transactor::signedByBeneficiary() const
{
auto const& signingPubKey = ctx_.tx.getSigningPubKey();

// Multi-signed transactions carry no signing key here and never reach the
// beneficiary path, which is single-sign only.
if (signingPubKey.empty() || !publicKeyType(makeSlice(signingPubKey)))
return false;

auto const sle = view().read(keylet::beneficiary(accountID_));
if (!sle)
return false;

return (*sle)[sfBeneficiary] == calcAccountID(PublicKey(makeSlice(signingPubKey)));
}

TER
Transactor::apply()
{
Expand Down Expand Up @@ -908,6 +925,15 @@ Transactor::apply()
if (sle->isFieldPresent(sfAccountTxnID))
sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID());

// The field is present only while a beneficiary designation exists, and
// it records the owner's own activity: a transaction the beneficiary
// signed must not reset the timer, or the beneficiary's first
// transaction would shut the door behind it.
if (view().rules().enabled(featureBeneficiary) && sle->isFieldPresent(sfLastInteraction) &&
!signedByBeneficiary())
sle->setFieldU32(
sfLastInteraction, view().parentCloseTime().time_since_epoch().count());

view().update(sle);
}

Expand Down Expand Up @@ -1043,6 +1069,26 @@ Transactor::checkSingleSign(
return tefMASTER_DISABLED;
}

// Signed by the beneficiary, once the account has been silent for the
// designated period. The designation is a second regular key that only
// starts working after the time lock, so the owner is never displaced and
// nothing about the account's own keys changes.
if (view.rules().enabled(featureBeneficiary))
{
if (auto const sle = view.read(keylet::beneficiary(idAccount));
sle && (*sle)[sfBeneficiary] == idSigner)
{
auto const last = (*sleAccount)[~sfLastInteraction];
auto const now = view.parentCloseTime().time_since_epoch().count();
if (last && now >= *last && now - *last >= (*sle)[sfTimeLock])
return tesSUCCESS;

JLOG(j.trace()) << "checkSingleSign: the account is not yet silent enough for its "
"beneficiary to sign";
return tefBAD_AUTH;
}
}

// Signed with any other key.
return tefBAD_AUTH;
}
Expand Down
Loading