mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
Merge remote-tracking branch 'Transia-RnD-rippled/dangell7/beneficiary' into develop
# Conflicts: # include/xrpl/protocol/detail/ledger_entries.macro # include/xrpl/tx/invariants/InvariantCheck.h # src/libxrpl/protocol/Indexes.cpp # src/libxrpl/tx/Transactor.cpp
This commit is contained in:
@@ -211,6 +211,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
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -161,3 +161,4 @@ XRPL_FEATURE(AMMCurves, Supported::Yes, VoteBehavior::DefaultN
|
||||
XRPL_FEATURE(OfferQualifiers, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Subscription, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Repo, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Beneficiary, Supported::No, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -899,5 +899,19 @@ LEDGER_ENTRY(ltREPO, 0x0098, Repo, repo, ({
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
}))
|
||||
|
||||
/** A designation of an account to receive this account's regular key after a
|
||||
period of inactivity.
|
||||
|
||||
\sa keylet::beneficiary
|
||||
*/
|
||||
LEDGER_ENTRY(ltBENEFICIARY, 0x0099, Beneficiary, beneficiary, ({
|
||||
{sfAccount, SoeRequired},
|
||||
{sfBeneficiary, SoeRequired},
|
||||
{sfTimeLock, SoeRequired},
|
||||
{sfOwnerNode, SoeRequired},
|
||||
{sfPreviousTxnID, SoeRequired},
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
@@ -569,3 +569,6 @@ TYPED_SFIELD(sfMaturityDate, UINT32, 112)
|
||||
TYPED_SFIELD(sfRepoID, UINT256, 50)
|
||||
TYPED_SFIELD(sfCollateralAmount, AMOUNT, 45)
|
||||
TYPED_SFIELD(sfPurchasePrice, AMOUNT, 46)
|
||||
TYPED_SFIELD(sfTimeLock, UINT32, 113)
|
||||
TYPED_SFIELD(sfLastInteraction, UINT32, 114)
|
||||
TYPED_SFIELD(sfBeneficiary, ACCOUNT, 33)
|
||||
|
||||
@@ -1557,3 +1557,9 @@ TRANSACTION(ttREPO_DEFAULT, 128, RepoDefault,
|
||||
({
|
||||
{sfRepoID, SoeRequired},
|
||||
}))
|
||||
TRANSACTION(ttBENEFICIARY_SET, 129, BeneficiarySet,
|
||||
({.amendment = featureBeneficiary}),
|
||||
({
|
||||
{sfBeneficiary, SoeOptional},
|
||||
{sfTimeLock, SoeOptional},
|
||||
}))
|
||||
|
||||
@@ -375,6 +375,13 @@ public:
|
||||
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();
|
||||
|
||||
|
||||
@@ -480,6 +480,42 @@ private:
|
||||
};
|
||||
// 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,
|
||||
@@ -520,7 +556,8 @@ using InvariantChecks = std::tuple<
|
||||
SponsorshipAccountCountMatchesField,
|
||||
ValidTokenIssuance,
|
||||
ValidCouponSchedule,
|
||||
ValidBallot>;
|
||||
ValidBallot,
|
||||
ValidBeneficiary>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
44
include/xrpl/tx/transactors/beneficiary/BeneficiarySet.h
Normal file
44
include/xrpl/tx/transactors/beneficiary/BeneficiarySet.h
Normal file
@@ -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
|
||||
@@ -120,6 +120,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
BallotVote = 'v',
|
||||
Subscription = 'w',
|
||||
Repo = 'M',
|
||||
Beneficiary = 'j',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
|
||||
Generator [[deprecated]] = 'g',
|
||||
@@ -370,6 +371,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
|
||||
{
|
||||
|
||||
@@ -886,6 +886,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()
|
||||
{
|
||||
@@ -916,6 +933,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);
|
||||
}
|
||||
|
||||
@@ -1073,6 +1099,26 @@ Transactor::checkSingleSign(
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
128
src/libxrpl/tx/invariants/BeneficiaryInvariant.cpp
Normal file
128
src/libxrpl/tx/invariants/BeneficiaryInvariant.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheck.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidBeneficiary::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
if (before && before->getType() == ltBENEFICIARY)
|
||||
{
|
||||
if (isDelete)
|
||||
{
|
||||
designationRemoved_.insert((*before)[sfAccount]);
|
||||
deleted_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (after && after->getType() == ltBENEFICIARY)
|
||||
{
|
||||
entries_.emplace_back(before, after);
|
||||
if (!before)
|
||||
designationAdded_.insert((*after)[sfAccount]);
|
||||
}
|
||||
|
||||
// The timestamp lives on the AccountRoot, so its comings and goings are
|
||||
// tracked separately and compared against the entries at the end.
|
||||
auto const stamp = [](SLE::const_ref sle) -> std::optional<std::uint32_t> {
|
||||
if (!sle || sle->getType() != ltACCOUNT_ROOT)
|
||||
return std::nullopt;
|
||||
return (*sle)[~sfLastInteraction];
|
||||
};
|
||||
|
||||
if (after && after->getType() == ltACCOUNT_ROOT)
|
||||
{
|
||||
auto const wasStamped = before ? stamp(before) : std::nullopt;
|
||||
auto const isStamped = isDelete ? std::nullopt : stamp(after);
|
||||
|
||||
if (!wasStamped && isStamped)
|
||||
stampAdded_.insert((*after)[sfAccount]);
|
||||
else if (wasStamped && !isStamped)
|
||||
stampRemoved_.insert((*after)[sfAccount]);
|
||||
else if (wasStamped && isStamped && *isStamped < *wasStamped)
|
||||
stampWentBackwards_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ValidBeneficiary::finalize(
|
||||
STTx const& tx,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
if (stampWentBackwards_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: LastInteraction moved backwards";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A designation and its timestamp are created together and removed
|
||||
// together, so the two sets of accounts must match exactly.
|
||||
if (designationAdded_ != stampAdded_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: a beneficiary designation was created without its "
|
||||
"LastInteraction, or the reverse";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (designationRemoved_ != stampRemoved_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: a beneficiary designation was removed without its "
|
||||
"LastInteraction, or the reverse";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deleted_)
|
||||
{
|
||||
switch (tx.getTxnType())
|
||||
{
|
||||
case ttBENEFICIARY_SET:
|
||||
break;
|
||||
default:
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: a beneficiary designation was deleted by transaction "
|
||||
"type "
|
||||
<< tx.getTxnType();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& [before, after] : entries_)
|
||||
{
|
||||
if ((*after)[sfAccount] == (*after)[sfBeneficiary])
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: an account is its own beneficiary";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const timeLock = (*after)[sfTimeLock];
|
||||
if (timeLock == 0 || timeLock > kMaxBeneficiaryTimeLock)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: the beneficiary time lock is out of range";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (before && (*before)[sfAccount] != (*after)[sfAccount])
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: the beneficiary entry changed account";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
175
src/libxrpl/tx/transactors/beneficiary/BeneficiarySet.cpp
Normal file
175
src/libxrpl/tx/transactors/beneficiary/BeneficiarySet.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include <xrpl/tx/transactors/beneficiary/BeneficiarySet.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
BeneficiarySet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
auto const beneficiary = ctx.tx[~sfBeneficiary];
|
||||
auto const timeLock = ctx.tx[~sfTimeLock];
|
||||
|
||||
// The two fields describe one designation, so they arrive together or not
|
||||
// at all; neither alone is a meaningful instruction.
|
||||
if (beneficiary.has_value() != timeLock.has_value())
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "BeneficiarySet: Beneficiary and TimeLock disagree";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (!beneficiary)
|
||||
return tesSUCCESS;
|
||||
|
||||
if (*beneficiary == ctx.tx[sfAccount])
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "BeneficiarySet: the beneficiary is the account";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (*beneficiary == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "BeneficiarySet: the beneficiary is the zero account";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// Zero would make the designation invocable in the ledger that set it.
|
||||
if (*timeLock == 0 || *timeLock > kMaxBeneficiaryTimeLock)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "BeneficiarySet: the time lock is out of range";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
BeneficiarySet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const beneficiary = ctx.tx[~sfBeneficiary];
|
||||
|
||||
if (!beneficiary)
|
||||
{
|
||||
if (!ctx.view.exists(keylet::beneficiary(ctx.tx[sfAccount])))
|
||||
return tecNO_ENTRY;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(keylet::account(*beneficiary)))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "BeneficiarySet: the beneficiary does not exist";
|
||||
return tecNO_TARGET;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
BeneficiarySet::doApply()
|
||||
{
|
||||
auto const sleAccount = view().peek(keylet::account(accountID_));
|
||||
if (!sleAccount)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
Keylet const beneficiaryKeylet = keylet::beneficiary(accountID_);
|
||||
auto const sle = view().peek(beneficiaryKeylet);
|
||||
|
||||
// No Beneficiary field means clear: the entry goes, and so does the
|
||||
// timestamp, leaving the account as it was before any designation.
|
||||
if (!ctx_.tx.isFieldPresent(sfBeneficiary))
|
||||
{
|
||||
if (!sle)
|
||||
return tecNO_ENTRY; // LCOV_EXCL_LINE
|
||||
|
||||
if (!view().dirRemove(keylet::ownerDir(accountID_), (*sle)[sfOwnerNode], sle->key(), true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.fatal()) << "BeneficiarySet: cannot remove the entry from the owner directory";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
decreaseOwnerCountForObject(view(), sleAccount, sle, 1, j_);
|
||||
view().erase(sle);
|
||||
|
||||
sleAccount->makeFieldAbsent(sfLastInteraction);
|
||||
view().update(sleAccount);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (sle)
|
||||
{
|
||||
(*sle)[sfBeneficiary] = ctx_.tx[sfBeneficiary];
|
||||
(*sle)[sfTimeLock] = ctx_.tx[sfTimeLock];
|
||||
view().update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
{
|
||||
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
|
||||
auto const reserve = accountReserve(view(), sleAccount, j_, {.ownerCountDelta = 1});
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
auto const sleNew = std::make_shared<SLE>(beneficiaryKeylet);
|
||||
(*sleNew)[sfAccount] = accountID_;
|
||||
(*sleNew)[sfBeneficiary] = ctx_.tx[sfBeneficiary];
|
||||
(*sleNew)[sfTimeLock] = ctx_.tx[sfTimeLock];
|
||||
|
||||
view().insert(sleNew);
|
||||
|
||||
auto const page =
|
||||
view().dirInsert(keylet::ownerDir(accountID_), sleNew->key(), describeOwnerDir(accountID_));
|
||||
if (!page)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*sleNew)[sfOwnerNode] = *page;
|
||||
|
||||
increaseOwnerCount(view(), sleAccount, {}, 1, j_);
|
||||
|
||||
// Transactor::apply already stamped the field if it was present; this is the
|
||||
// first time it is not, so the timer starts here.
|
||||
sleAccount->setFieldU32(sfLastInteraction, view().parentCloseTime().time_since_epoch().count());
|
||||
view().update(sleAccount);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
BeneficiarySet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
// The ValidBeneficiary check covers this transaction.
|
||||
}
|
||||
|
||||
bool
|
||||
BeneficiarySet::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
// The ValidBeneficiary check covers this transaction.
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
489
src/test/app/Beneficiary_test.cpp
Normal file
489
src/test/app/Beneficiary_test.cpp
Normal file
@@ -0,0 +1,489 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/acctdelete.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/owners.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/regkey.h>
|
||||
#include <test/jtx/sig.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/utility.h>
|
||||
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class Beneficiary_test : public beast::unit_test::Suite
|
||||
{
|
||||
static json::Value
|
||||
set(jtx::Account const& account, jtx::Account const& beneficiary, std::uint32_t timeLock)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::BeneficiarySet;
|
||||
jv[sfAccount] = account.human();
|
||||
jv[sfBeneficiary] = beneficiary.human();
|
||||
jv[sfTimeLock] = timeLock;
|
||||
return jv;
|
||||
}
|
||||
|
||||
static json::Value
|
||||
clear(jtx::Account const& account)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::BeneficiarySet;
|
||||
jv[sfAccount] = account.human();
|
||||
return jv;
|
||||
}
|
||||
|
||||
static bool
|
||||
exists(jtx::Env const& env, jtx::Account const& account)
|
||||
{
|
||||
return env.le(keylet::beneficiary(account.id())) != nullptr;
|
||||
}
|
||||
|
||||
static std::optional<std::uint32_t>
|
||||
stamp(jtx::Env const& env, jtx::Account const& account)
|
||||
{
|
||||
auto const sle = env.le(keylet::account(account.id()));
|
||||
if (!sle)
|
||||
return std::nullopt;
|
||||
return (*sle)[~sfLastInteraction];
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("enabled");
|
||||
using namespace jtx;
|
||||
|
||||
for (bool const withFeature : {false, true})
|
||||
{
|
||||
auto const amend = withFeature ? features : features - featureBeneficiary;
|
||||
Env env{*this, amend};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const expected = withFeature ? Ter(tesSUCCESS) : Ter(temDISABLED);
|
||||
env(set(alice, bob, 3600), expected);
|
||||
env.close();
|
||||
|
||||
if (!withFeature)
|
||||
{
|
||||
BEAST_EXPECT(!exists(env, alice));
|
||||
continue;
|
||||
}
|
||||
|
||||
BEAST_EXPECT(exists(env, alice));
|
||||
BEAST_EXPECT(stamp(env, alice).has_value());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSetMalformed(FeatureBitset features)
|
||||
{
|
||||
testcase("malformed set");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// An account cannot inherit from itself.
|
||||
env(set(alice, alice, 3600), Ter(temMALFORMED));
|
||||
|
||||
// A time lock of zero is invocable in the ledger that sets it.
|
||||
env(set(alice, bob, 0), Ter(temMALFORMED));
|
||||
env(set(alice, bob, kMaxBeneficiaryTimeLock + 1), Ter(temMALFORMED));
|
||||
|
||||
// The two fields describe one designation and travel together.
|
||||
{
|
||||
json::Value jv = clear(alice);
|
||||
jv[sfTimeLock] = 3600;
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::BeneficiarySet;
|
||||
jv[sfAccount] = alice.human();
|
||||
jv[sfBeneficiary] = bob.human();
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// The beneficiary has to be an account that exists.
|
||||
env(set(alice, carol, 3600), Ter(tecNO_TARGET));
|
||||
|
||||
// Clearing when there is nothing to clear.
|
||||
env(clear(alice), Ter(tecNO_ENTRY));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!exists(env, alice));
|
||||
BEAST_EXPECT(!stamp(env, alice).has_value());
|
||||
|
||||
// The ceiling itself is allowed.
|
||||
env(set(alice, bob, kMaxBeneficiaryTimeLock));
|
||||
env.close();
|
||||
BEAST_EXPECT(exists(env, alice));
|
||||
}
|
||||
|
||||
void
|
||||
testSetUpdateClear(FeatureBitset features)
|
||||
{
|
||||
testcase("set, update and clear");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"};
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
BEAST_EXPECT(exists(env, alice));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
{
|
||||
auto const sle = env.le(keylet::beneficiary(alice.id()));
|
||||
BEAST_EXPECT((*sle)[sfAccount] == alice.id());
|
||||
BEAST_EXPECT((*sle)[sfBeneficiary] == bob.id());
|
||||
BEAST_EXPECT((*sle)[sfTimeLock] == 3600);
|
||||
}
|
||||
|
||||
// Updating overwrites in place: still one entry, still one reserve.
|
||||
env(set(alice, carol, 7200));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
{
|
||||
auto const sle = env.le(keylet::beneficiary(alice.id()));
|
||||
BEAST_EXPECT((*sle)[sfBeneficiary] == carol.id());
|
||||
BEAST_EXPECT((*sle)[sfTimeLock] == 7200);
|
||||
}
|
||||
|
||||
// Clearing leaves the account as it was before any designation.
|
||||
env(clear(alice));
|
||||
env.close();
|
||||
BEAST_EXPECT(!exists(env, alice));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(!stamp(env, alice).has_value());
|
||||
}
|
||||
|
||||
// The beneficiary becomes a second regular key once the time lock has run.
|
||||
// Nothing on the account changes: no key is replaced, no entry deleted.
|
||||
void
|
||||
testBeneficiarySigns(FeatureBitset features)
|
||||
{
|
||||
testcase("the beneficiary signs once the time lock has run");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"};
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
|
||||
// Before the time lock, the beneficiary is just another account.
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob), Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
|
||||
env.close(std::chrono::seconds(4000));
|
||||
|
||||
// After it, the beneficiary signs for the account.
|
||||
auto const carolBefore = env.balance(carol);
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(carol) == carolBefore + XRP(1));
|
||||
|
||||
// The account is untouched: no regular key was set and the designation
|
||||
// is still there.
|
||||
auto const sle = env.le(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRegularKey));
|
||||
BEAST_EXPECT(exists(env, alice));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// An unrelated account still cannot sign.
|
||||
env(pay(alice, carol, XRP(1)), Sig(carol), Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// The beneficiary's own transactions must not reset the timer, or the first
|
||||
// one would shut the door behind it.
|
||||
void
|
||||
testBeneficiarySigningDoesNotResetTheTimer(FeatureBitset features)
|
||||
{
|
||||
testcase("a beneficiary-signed transaction does not reset the timer");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"};
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
auto const stampAtSet = stamp(env, alice);
|
||||
env.close(std::chrono::seconds(4000));
|
||||
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob));
|
||||
env.close();
|
||||
BEAST_EXPECT(stamp(env, alice) == stampAtSet);
|
||||
|
||||
// So the beneficiary can keep signing.
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob));
|
||||
env.close();
|
||||
BEAST_EXPECT(stamp(env, alice) == stampAtSet);
|
||||
}
|
||||
|
||||
// The owner is never locked out: signing with their own key closes the
|
||||
// beneficiary's access again.
|
||||
void
|
||||
testOwnerReclaims(FeatureBitset features)
|
||||
{
|
||||
testcase("the owner reclaims by signing");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"};
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
env.close(std::chrono::seconds(4000));
|
||||
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob));
|
||||
env.close();
|
||||
|
||||
// Alice comes back and uses her own key.
|
||||
env(pay(alice, carol, XRP(1)));
|
||||
env.close();
|
||||
|
||||
// Bob is shut out again, and Alice never lost anything.
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob), Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
BEAST_EXPECT(exists(env, alice));
|
||||
}
|
||||
|
||||
// The case that drove this design: an owner who signs with a regular key
|
||||
// and goes quiet keeps their account.
|
||||
void
|
||||
testRegularKeyOwnerNotLockedOut(FeatureBitset features)
|
||||
{
|
||||
testcase("an owner signing with a regular key is not locked out");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"}, key{"key"};
|
||||
env.fund(XRP(10000), alice, bob, carol, key);
|
||||
env.close();
|
||||
|
||||
env(regkey(alice, key));
|
||||
env.close();
|
||||
env(fset(alice, asfDisableMaster), Sig(alice));
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600), Sig(key));
|
||||
env.close();
|
||||
env.close(std::chrono::seconds(4000));
|
||||
|
||||
// The beneficiary can sign now.
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob));
|
||||
env.close();
|
||||
|
||||
// And so can the owner, with the regular key they have always used.
|
||||
// The regular key was never overwritten.
|
||||
BEAST_EXPECT((*env.le(keylet::account(alice.id())))[~sfRegularKey] == key.id());
|
||||
env(pay(alice, carol, XRP(1)), Sig(key));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, carol, XRP(1)), Sig(bob), Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// The whole point of the mechanism: using the account holds it off.
|
||||
void
|
||||
testActivityResetsTheTimer(FeatureBitset features)
|
||||
{
|
||||
testcase("activity resets the timer");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
auto const first = stamp(env, alice);
|
||||
BEAST_EXPECT(first.has_value());
|
||||
|
||||
env.close(std::chrono::seconds(3000));
|
||||
|
||||
// An ordinary outgoing payment, nothing to do with this amendment.
|
||||
env(pay(alice, bob, XRP(1)));
|
||||
env.close();
|
||||
auto const second = stamp(env, alice);
|
||||
BEAST_EXPECT(second.has_value() && *second > *first);
|
||||
|
||||
// The original deadline has now passed, and the beneficiary still
|
||||
// cannot sign, because the payment moved it.
|
||||
env.close(std::chrono::seconds(1000));
|
||||
env(pay(alice, bob, XRP(1)), Sig(bob), Ter(tefBAD_AUTH));
|
||||
env.close();
|
||||
|
||||
env.close(std::chrono::seconds(4000));
|
||||
env(pay(alice, bob, XRP(1)), Sig(bob));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Receiving is not activity: only the sender's own transactions count.
|
||||
void
|
||||
testIncomingIsNotActivity(FeatureBitset features)
|
||||
{
|
||||
testcase("an incoming payment is not activity");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
auto const before = stamp(env, alice);
|
||||
|
||||
env.close(std::chrono::seconds(2000));
|
||||
env(pay(bob, alice, XRP(100)));
|
||||
env.close();
|
||||
BEAST_EXPECT(stamp(env, alice) == before);
|
||||
|
||||
env.close(std::chrono::seconds(2000));
|
||||
env(pay(alice, bob, XRP(1)), Sig(bob));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// An account that never set a beneficiary is untouched by the amendment.
|
||||
void
|
||||
testUnrelatedAccountUnstamped(FeatureBitset features)
|
||||
{
|
||||
testcase("an account without a designation is never stamped");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
env(pay(alice, bob, XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!stamp(env, alice).has_value());
|
||||
BEAST_EXPECT(!stamp(env, bob).has_value());
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDeleteBlocked(FeatureBitset features)
|
||||
{
|
||||
testcase("a designation blocks account deletion");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"}, sink{"sink"};
|
||||
env.fund(XRP(10000), alice, bob, sink);
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
env(acctdelete(alice, sink),
|
||||
Fee(drops(env.current()->fees().increment)),
|
||||
Ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
// With the designation cleared, the same account deletes.
|
||||
env(clear(alice));
|
||||
env.close();
|
||||
env(acctdelete(alice, sink), Fee(drops(env.current()->fees().increment)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::account(alice.id())));
|
||||
}
|
||||
|
||||
void
|
||||
testRpc(FeatureBitset features)
|
||||
{
|
||||
testcase("account_objects and ledger_entry");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
env(set(alice, bob, 3600));
|
||||
env.close();
|
||||
|
||||
{
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::type] = "beneficiary";
|
||||
auto const jv = env.rpc("json", "account_objects", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jv[jss::account_objects].size() == 1);
|
||||
auto const& object = jv[jss::account_objects][0u];
|
||||
BEAST_EXPECT(object["LedgerEntryType"].asString() == "Beneficiary");
|
||||
BEAST_EXPECT(object["Beneficiary"].asString() == bob.human());
|
||||
}
|
||||
|
||||
// The beneficiary does not own the entry, so it is not in their
|
||||
// directory.
|
||||
{
|
||||
json::Value params;
|
||||
params[jss::account] = bob.human();
|
||||
params[jss::type] = "beneficiary";
|
||||
auto const jv = env.rpc("json", "account_objects", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jv[jss::account_objects].size() == 0);
|
||||
}
|
||||
|
||||
{
|
||||
json::Value params;
|
||||
params[jss::beneficiary] = alice.human();
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(
|
||||
jv[jss::index].asString() == to_string(keylet::beneficiary(alice.id()).key));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace jtx;
|
||||
auto const all = jtx::testableAmendments();
|
||||
testEnabled(all);
|
||||
testSetMalformed(all);
|
||||
testSetUpdateClear(all);
|
||||
testBeneficiarySigns(all);
|
||||
testBeneficiarySigningDoesNotResetTheTimer(all);
|
||||
testOwnerReclaims(all);
|
||||
testRegularKeyOwnerNotLockedOut(all);
|
||||
testActivityResetsTheTimer(all);
|
||||
testIncomingIsNotActivity(all);
|
||||
testUnrelatedAccountUnstamped(all);
|
||||
testAccountDeleteBlocked(all);
|
||||
testRpc(all);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Beneficiary, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
210
src/test/app/invariants/InvariantsBeneficiary_test.cpp
Normal file
210
src/test/app/invariants/InvariantsBeneficiary_test.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include <test/app/invariants/InvariantsBase.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class InvariantsBeneficiary_test : public InvariantsBase
|
||||
{
|
||||
// Put a designation on a1 so the checks have something to corrupt.
|
||||
static bool
|
||||
designate(jtx::Account const& a1, jtx::Account const& a2, jtx::Env& env)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfTransactionType] = jss::BeneficiarySet;
|
||||
jv[sfAccount] = a1.human();
|
||||
jv[sfBeneficiary] = a2.human();
|
||||
jv[sfTimeLock] = 3600;
|
||||
env(jv);
|
||||
env.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
testEntryAndStampTravelTogether()
|
||||
{
|
||||
testcase("a designation and its timestamp travel together");
|
||||
using namespace jtx;
|
||||
|
||||
// The entry appears with no timestamp beside it.
|
||||
doInvariantCheck(
|
||||
{{"a beneficiary designation was created without its LastInteraction"}},
|
||||
[](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto sle = std::make_shared<SLE>(keylet::beneficiary(a1.id()));
|
||||
(*sle)[sfAccount] = a1.id();
|
||||
(*sle)[sfBeneficiary] = a2.id();
|
||||
(*sle)[sfTimeLock] = 3600;
|
||||
(*sle)[sfOwnerNode] = 0;
|
||||
ac.view().insert(sle);
|
||||
return true;
|
||||
});
|
||||
|
||||
// The timestamp appears with no entry beside it.
|
||||
doInvariantCheck(
|
||||
{{"a beneficiary designation was created without its LastInteraction"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::account(a1.id()));
|
||||
if (!sle)
|
||||
return false;
|
||||
sle->setFieldU32(sfLastInteraction, 1);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
});
|
||||
|
||||
// The entry goes and the timestamp stays behind.
|
||||
doInvariantCheck(
|
||||
{{"a beneficiary designation was removed without its LastInteraction"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::beneficiary(a1.id()));
|
||||
if (!sle)
|
||||
return false;
|
||||
ac.view().erase(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttBENEFICIARY_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
designate);
|
||||
}
|
||||
|
||||
void
|
||||
testFieldBounds()
|
||||
{
|
||||
testcase("the designation's fields are bounded");
|
||||
using namespace jtx;
|
||||
|
||||
doInvariantCheck(
|
||||
{{"an account is its own beneficiary"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::beneficiary(a1.id()));
|
||||
if (!sle)
|
||||
return false;
|
||||
(*sle)[sfBeneficiary] = a1.id();
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttBENEFICIARY_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
designate);
|
||||
|
||||
doInvariantCheck(
|
||||
{{"the beneficiary time lock is out of range"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::beneficiary(a1.id()));
|
||||
if (!sle)
|
||||
return false;
|
||||
(*sle)[sfTimeLock] = 0;
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttBENEFICIARY_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
designate);
|
||||
|
||||
doInvariantCheck(
|
||||
{{"the beneficiary time lock is out of range"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::beneficiary(a1.id()));
|
||||
if (!sle)
|
||||
return false;
|
||||
(*sle)[sfTimeLock] = kMaxBeneficiaryTimeLock + 1;
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttBENEFICIARY_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
designate);
|
||||
|
||||
doInvariantCheck(
|
||||
{{"the beneficiary entry changed account"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::beneficiary(a1.id()));
|
||||
if (!sle)
|
||||
return false;
|
||||
(*sle)[sfAccount] = Account{"someone else"}.id();
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttBENEFICIARY_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
designate);
|
||||
}
|
||||
|
||||
void
|
||||
testTimerNeverGoesBackwards()
|
||||
{
|
||||
testcase("the timestamp never moves backwards");
|
||||
using namespace jtx;
|
||||
|
||||
doInvariantCheck(
|
||||
{{"LastInteraction moved backwards"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::account(a1.id()));
|
||||
if (!sle || !sle->isFieldPresent(sfLastInteraction) ||
|
||||
(*sle)[sfLastInteraction] == 0)
|
||||
return false;
|
||||
sle->setFieldU32(sfLastInteraction, 1);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttBENEFICIARY_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
[](Account const& a1, Account const& a2, jtx::Env& env) {
|
||||
// The ledger clock starts at zero, so it is moved forward
|
||||
// before the designation is made; otherwise there is no
|
||||
// earlier value for the timestamp to move back to.
|
||||
env.close(std::chrono::seconds(100000));
|
||||
return designate(a1, a2, env);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testDeletedByWrongTransaction()
|
||||
{
|
||||
testcase("a designation is deleted by the wrong transaction");
|
||||
using namespace jtx;
|
||||
|
||||
doInvariantCheck(
|
||||
{{"a beneficiary designation was deleted by transaction type"}},
|
||||
[](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::beneficiary(a1.id()));
|
||||
auto sleAcct = ac.view().peek(keylet::account(a1.id()));
|
||||
if (!sle || !sleAcct)
|
||||
return false;
|
||||
ac.view().erase(sle);
|
||||
sleAcct->makeFieldAbsent(sfLastInteraction);
|
||||
ac.view().update(sleAcct);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
designate);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testEntryAndStampTravelTogether();
|
||||
testFieldBounds();
|
||||
testTimerNeverGoesBackwards();
|
||||
testDeletedByWrongTransaction();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(InvariantsBeneficiary, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -909,6 +909,22 @@ parseSignerList(
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseBeneficiary(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
// One designation per account, so the account alone identifies the entry.
|
||||
auto const account = ledger_entry_helpers::parse<AccountID>(params);
|
||||
if (!account)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID");
|
||||
}
|
||||
|
||||
return keylet::beneficiary(*account).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseSponsorship(
|
||||
json::Value const& params,
|
||||
|
||||
Reference in New Issue
Block a user