mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-02 16:26:48 +00:00
part 3
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
/** @file
|
||||
* Implementation of the PaymentChannelClaim transactor.
|
||||
*
|
||||
* Settles an off-chain XRP micropayment channel on the ledger by advancing
|
||||
* the channel's cumulative `sfBalance`, closing the channel on expiry or
|
||||
* request, or clearing a pending close via `tfRenew`. This is the only
|
||||
* transactor in the payment-channel family that transfers XRP to the
|
||||
* destination; `PaymentChannelCreate` and `PaymentChannelFund` only lock
|
||||
* or add XRP into the channel's escrow.
|
||||
*
|
||||
* @see PaymentChannelClaim.h for the full class and method contracts.
|
||||
*/
|
||||
#include <xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h>
|
||||
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
@@ -150,10 +162,7 @@ PaymentChannelClaim::doApply()
|
||||
return tecUNFUNDED_PAYMENT;
|
||||
|
||||
if (reqBalance <= chanBalance)
|
||||
{
|
||||
// nothing requested
|
||||
return tecUNFUNDED_PAYMENT;
|
||||
}
|
||||
|
||||
auto const sled = ctx_.view().peek(keylet::account(dst));
|
||||
if (!sled)
|
||||
|
||||
@@ -25,25 +25,24 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/*
|
||||
PaymentChannel
|
||||
|
||||
Payment channels permit off-ledger checkpoints of XRP payments flowing
|
||||
in a single direction. A channel sequesters the owner's XRP in its own
|
||||
ledger entry. The owner can authorize the recipient to claim up to a
|
||||
given balance by giving the receiver a signed message (off-ledger). The
|
||||
recipient can use this signed message to claim any unpaid balance while
|
||||
the channel remains open. The owner can top off the line as needed. If
|
||||
the channel has not paid out all its funds, the owner must wait out a
|
||||
delay to close the channel to give the recipient a chance to supply any
|
||||
claims. The recipient can close the channel at any time. Any transaction
|
||||
that touches the channel after the expiration time will close the
|
||||
channel. The total amount paid increases monotonically as newer claims
|
||||
are issued. When the channel is closed any remaining balance is returned
|
||||
to the owner. Channels are intended to permit intermittent off-ledger
|
||||
settlement of ILP trust lines as balances get substantial. For
|
||||
bidirectional channels, a payment channel can be used in each direction.
|
||||
*/
|
||||
/** @file
|
||||
* Implements `PaymentChannelCreate` — the transactor that opens a
|
||||
* unidirectional XRP payment channel.
|
||||
*
|
||||
* Payment channels sequesters the owner's XRP in a dedicated `PayChannel`
|
||||
* SLE so the owner can issue signed off-ledger claims to a recipient without
|
||||
* touching the ledger for every payment. The recipient presents claims
|
||||
* on-chain to settle; the paid total increases monotonically. When the channel
|
||||
* closes, any remaining balance is returned to the owner. For bidirectional
|
||||
* flow, two channels (one in each direction) are used.
|
||||
*
|
||||
* This file covers only channel construction. Funding additions, claim
|
||||
* settlement, and closure are handled by `PaymentChannelFund.cpp` and
|
||||
* `PaymentChannelClaim.cpp` respectively. Crucially, `PaymentChannelCreate`
|
||||
* is the only transactor that allocates the `PayChannel` SLE and inserts
|
||||
* both directory entries; the sibling transactors always assume those
|
||||
* structures already exist.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -76,7 +75,11 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// Check reserve and funds availability
|
||||
// Two-step reserve check: first ensure the account can hold one more owned
|
||||
// object (tecINSUFFICIENT_RESERVE), then that it can also fund the channel
|
||||
// at the requested level (tecUNFUNDED). The distinction is intentional —
|
||||
// callers can use it to tell whether the problem is reserve capacity or
|
||||
// channel size.
|
||||
{
|
||||
auto const balance = (*sle)[sfBalance];
|
||||
auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1);
|
||||
@@ -91,26 +94,25 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
|
||||
auto const dst = ctx.tx[sfDestination];
|
||||
|
||||
{
|
||||
// Check destination account
|
||||
auto const sled = ctx.view.read(keylet::account(dst));
|
||||
if (!sled)
|
||||
// Deliberate design choice: channels to non-existent accounts are
|
||||
// rejected even though XRP would be held in escrow. Allowing it
|
||||
// would reserve funds to an address that may never be activated,
|
||||
// effectively burning the sender's XRP in a dead-end channel.
|
||||
return tecNO_DST;
|
||||
|
||||
auto const flags = sled->getFlags();
|
||||
|
||||
// Check if they have disallowed incoming payment channels
|
||||
if ((flags & lsfDisallowIncomingPayChan) != 0u)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (((flags & lsfRequireDestTag) != 0u) && !ctx.tx[~sfDestinationTag])
|
||||
return tecDST_TAG_NEEDED;
|
||||
|
||||
// Pseudo-accounts cannot receive payment channels, other than native
|
||||
// to their underlying ledger object - implemented in their respective
|
||||
// transaction types. Note, this is not amendment-gated because all
|
||||
// writes to pseudo-account discriminator fields **are** amendment
|
||||
// gated, hence the behaviour of this check will always match the
|
||||
// currently active amendments.
|
||||
// Not amendment-gated: pseudo-account discriminator fields are only
|
||||
// written under their own amendment guards, so this check automatically
|
||||
// tracks the active amendment set without additional gating here.
|
||||
if (isPseudoAccount(sled))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
@@ -135,16 +137,19 @@ PaymentChannelCreate::doApply()
|
||||
|
||||
auto const dst = ctx_.tx[sfDestination];
|
||||
|
||||
// Create PayChan in ledger.
|
||||
//
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
// payChan sequence. For more explanation see comments in SeqProxy.h.
|
||||
// The keylet hashes account, destination, and sequence (or ticket) number.
|
||||
// Including the sequence ensures that two channels between the same pair of
|
||||
// accounts are always addressable distinctly — each channel open has a
|
||||
// unique sequence value, so their keylets never collide. getSeqValue()
|
||||
// transparently returns either the transaction sequence or the ticket
|
||||
// number, so the keylet is deterministic for both regular and ticket-based
|
||||
// transactions. See SeqProxy.h for details.
|
||||
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
|
||||
auto const slep = std::make_shared<SLE>(payChanKeylet);
|
||||
|
||||
// Funds held in this channel
|
||||
(*slep)[sfAmount] = ctx_.tx[sfAmount];
|
||||
// Amount channel has already paid
|
||||
// zeroed() preserves the XRPAmount type, keeping sfBalance type-consistent
|
||||
// with sfAmount from the start (as opposed to a bare integer zero).
|
||||
(*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed();
|
||||
(*slep)[sfAccount] = account;
|
||||
(*slep)[sfDestination] = dst;
|
||||
@@ -153,6 +158,11 @@ PaymentChannelCreate::doApply()
|
||||
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
|
||||
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
|
||||
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
|
||||
// fixIncludeKeyletFields is a bug-fix amendment: channels created before it
|
||||
// was activated lack sfSequence in the SLE, so callers must fetch the
|
||||
// originating transaction to recompute the keylet. When active, storing
|
||||
// sfSequence directly in the SLE lets any tool reconstruct the channel's
|
||||
// keylet from the object alone, without additional transaction lookup.
|
||||
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
|
||||
{
|
||||
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
|
||||
@@ -160,7 +170,8 @@ PaymentChannelCreate::doApply()
|
||||
|
||||
ctx_.view().insert(slep);
|
||||
|
||||
// Add PayChan to owner directory
|
||||
// Dual directory insertion: both owner and recipient can enumerate this
|
||||
// channel, and the stored page indices make O(1) removal possible on close.
|
||||
{
|
||||
auto const page = ctx_.view().dirInsert(
|
||||
keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account));
|
||||
@@ -169,7 +180,6 @@ PaymentChannelCreate::doApply()
|
||||
(*slep)[sfOwnerNode] = *page;
|
||||
}
|
||||
|
||||
// Add PayChan to the recipient's owner directory
|
||||
{
|
||||
auto const page =
|
||||
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
|
||||
@@ -178,7 +188,6 @@ PaymentChannelCreate::doApply()
|
||||
(*slep)[sfDestinationNode] = *page;
|
||||
}
|
||||
|
||||
// Deduct owner's balance, increment owner count
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
|
||||
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
|
||||
ctx_.view().update(sle);
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* Implements `PaymentChannelFund`, the "refill" transactor for XRPL payment
|
||||
* channels. A channel owner calls this to deposit additional XRP into an
|
||||
* existing channel or to extend its voluntary expiration deadline without
|
||||
* closing and reopening the channel.
|
||||
*
|
||||
* The apply phase enforces a specific guard order: expiry-triggered cleanup
|
||||
* runs before the ownership check, so any account touching a stale channel
|
||||
* will garbage-collect it regardless of whether they own it.
|
||||
*/
|
||||
#include <xrpl/tx/transactors/payment_channel/PaymentChannelFund.h>
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -22,12 +32,33 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Construct `TxConsequences` that reflect the full XRP deposit, not just the fee.
|
||||
*
|
||||
* The transaction queue uses this to account for the XRP locked in flight.
|
||||
* Reporting only the fee would undercount resource consumption and allow
|
||||
* conflicting transactions to be queued that assume the deposited XRP is
|
||||
* still available.
|
||||
*
|
||||
* @param ctx Preflight context providing the transaction fields.
|
||||
* @return `TxConsequences` with consumed XRP set to `sfAmount.xrp()`.
|
||||
*/
|
||||
TxConsequences
|
||||
PaymentChannelFund::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
|
||||
}
|
||||
|
||||
/** Stateless validation: `sfAmount` must be a positive XRP value.
|
||||
*
|
||||
* The `sfAmount` field is polymorphic at the protocol level and could carry
|
||||
* a non-XRP (IOU) amount from a malformed transaction; the `isXRP()` guard
|
||||
* rejects those. All ledger-state checks (channel existence, permissions,
|
||||
* reserve) are deferred to `doApply`.
|
||||
*
|
||||
* @param ctx Preflight context providing the transaction and current rules.
|
||||
* @return `tesSUCCESS`, or `temBAD_AMOUNT` if `sfAmount` is non-XRP or
|
||||
* non-positive.
|
||||
*/
|
||||
NotTEC
|
||||
PaymentChannelFund::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -37,6 +68,47 @@ PaymentChannelFund::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Apply the funding operation: validate channel state then mutate it.
|
||||
*
|
||||
* Guards are evaluated in this deliberate order:
|
||||
*
|
||||
* 1. **Channel existence** — looks up the `ltPAYCHAN` SLE; `tecNO_ENTRY` if
|
||||
* absent.
|
||||
* 2. **Expiry short-circuit** — if `parentCloseTime` has reached either
|
||||
* `sfCancelAfter` (immutable hard deadline) or `sfExpiration` (mutable soft
|
||||
* deadline), calls `closeChannel()` and returns immediately, *before* the
|
||||
* ownership check. Any account touching an expired channel thus triggers
|
||||
* cleanup — this is how XRPL garbage-collects stale channels without
|
||||
* requiring owner action.
|
||||
* 3. **Ownership** — only the original channel creator (`sfAccount` on the
|
||||
* channel SLE) may fund or extend; others receive `tecNO_PERMISSION`.
|
||||
* 4. **Expiration extension** — if the transaction carries `sfExpiration`, the
|
||||
* new value must be ≥ `parentCloseTime + sfSettleDelay` (floored to the
|
||||
* existing expiration when it is earlier). This prevents the owner from
|
||||
* sneaking in a very short expiration that deprives the recipient of their
|
||||
* full settle window; violation returns `temBAD_EXPIRATION`.
|
||||
* 5. **Reserve check** — owner balance must cover `accountReserve(ownerCount)`;
|
||||
* shortfall returns `tecINSUFFICIENT_RESERVE`. Checked separately from the
|
||||
* balance check to give callers a distinct error code when the account is
|
||||
* already underwater before considering the deposit.
|
||||
* 6. **Balance check** — owner balance must also cover reserve + `sfAmount`;
|
||||
* shortfall returns `tecUNFUNDED`.
|
||||
* 7. **Destination existence** — destination account must still exist; if it
|
||||
* was deleted after channel creation, adding funds would lock XRP into an
|
||||
* unclaimable channel; returns `tecNO_DST`.
|
||||
*
|
||||
* On success, `sfAmount` on the channel SLE (total funded capacity) is
|
||||
* incremented and the owner's account `sfBalance` is decremented by the same
|
||||
* value. Both SLEs are committed via `ctx_.view().update()`.
|
||||
*
|
||||
* @return `tesSUCCESS`, or one of `tecNO_ENTRY`, `tecNO_PERMISSION`,
|
||||
* `temBAD_EXPIRATION`, `tecINSUFFICIENT_RESERVE`, `tecUNFUNDED`,
|
||||
* `tecNO_DST`, or `tefINTERNAL` (unreachable in practice — see below).
|
||||
*
|
||||
* @note The `tefINTERNAL` path (source account SLE not found) is marked
|
||||
* `LCOV_EXCL_LINE` because a signed, accepted transaction implies the
|
||||
* submitting account must exist; the check is purely defensive.
|
||||
*/
|
||||
TER
|
||||
PaymentChannelFund::doApply()
|
||||
{
|
||||
@@ -58,7 +130,6 @@ PaymentChannelFund::doApply()
|
||||
|
||||
if (src != txAccount)
|
||||
{
|
||||
// only the owner can add funds or extend
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
@@ -80,7 +151,6 @@ PaymentChannelFund::doApply()
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
{
|
||||
// Check reserve and funds availability
|
||||
auto const balance = (*sle)[sfBalance];
|
||||
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]);
|
||||
|
||||
@@ -91,7 +161,6 @@ PaymentChannelFund::doApply()
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
|
||||
// do not allow adding funds if dst does not exist
|
||||
if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst)))
|
||||
{
|
||||
return tecNO_DST;
|
||||
@@ -106,6 +175,7 @@ PaymentChannelFund::doApply()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** No-op invariant visitor; no per-entry checks are defined for this transaction type yet. */
|
||||
void
|
||||
PaymentChannelFund::visitInvariantEntry(
|
||||
bool,
|
||||
@@ -115,6 +185,9 @@ PaymentChannelFund::visitInvariantEntry(
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** No-op invariant finalizer; always returns `true` until transaction-specific
|
||||
* invariants are defined.
|
||||
*/
|
||||
bool
|
||||
PaymentChannelFund::finalizeInvariants(
|
||||
STTx const&,
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file
|
||||
* Implements the `PermissionedDomainDelete` transactor, which removes a
|
||||
* permissioned domain SLE from the XRPL ledger and recovers the owner
|
||||
* reserve locked at creation.
|
||||
*
|
||||
* The three-phase pipeline is: `preflight` (structural check on `sfDomainID`)
|
||||
* → `preclaim` (existence + ownership, read-only) → `doApply` (directory
|
||||
* removal, owner-count decrement, SLE erasure).
|
||||
*/
|
||||
#include <xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -16,6 +25,16 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Reject structurally invalid transactions before consulting ledger state.
|
||||
*
|
||||
* A zero-valued `sfDomainID` is analogous to a null pointer — it cannot
|
||||
* reference any real domain object. All ledger-state checks (existence,
|
||||
* ownership) are deferred to `preclaim` so that they can influence
|
||||
* fee-claiming behaviour.
|
||||
*
|
||||
* @param ctx The preflight context containing the raw transaction.
|
||||
* @return `temMALFORMED` if `sfDomainID` is zero; `tesSUCCESS` otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
PermissionedDomainDelete::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -26,6 +45,19 @@ PermissionedDomainDelete::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Verify domain existence and submitter ownership against live ledger state.
|
||||
*
|
||||
* Resolves `sfDomainID` to a `PermissionedDomain` SLE via
|
||||
* `keylet::permissionedDomain`. Only the account that created the domain
|
||||
* (`sfOwner`) may delete it — there is no admin override or co-ownership
|
||||
* model. Placing these checks here (rather than in `doApply`) ensures that
|
||||
* an unauthorized or nonexistent-domain attempt still charges the fee.
|
||||
*
|
||||
* @param ctx The preclaim context with read-only ledger view.
|
||||
* @return `tecNO_ENTRY` if the domain SLE does not exist in the current
|
||||
* ledger; `tecNO_PERMISSION` if the submitter is not the domain owner;
|
||||
* `tesSUCCESS` otherwise.
|
||||
*/
|
||||
TER
|
||||
PermissionedDomainDelete::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -44,7 +76,22 @@ PermissionedDomainDelete::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Attempt to delete the Permissioned Domain. */
|
||||
/** Remove the permissioned domain object and release the owner reserve.
|
||||
*
|
||||
* Performs three mutations in strict order:
|
||||
* 1. `view().dirRemove()` — removes the SLE's back-reference from the
|
||||
* account's owner directory using the page index stored in `sfOwnerNode`.
|
||||
* Passing `true` also cleans up the directory page if it becomes empty.
|
||||
* Failure is unreachable for a well-formed ledger and is marked
|
||||
* `LCOV_EXCL`; it returns `tefBAD_LEDGER` if somehow triggered.
|
||||
* 2. `adjustOwnerCount(..., -1, j)` — decrements `sfOwnerCount`, recovering
|
||||
* the base reserve that was locked when the domain was created.
|
||||
* 3. `view().erase(slePd)` — removes the `PermissionedDomain` SLE from the
|
||||
* ledger view. This is the final, irreversible step.
|
||||
*
|
||||
* @return `tefBAD_LEDGER` on owner-directory corruption (unreachable under
|
||||
* normal operation); `tesSUCCESS` otherwise.
|
||||
*/
|
||||
TER
|
||||
PermissionedDomainDelete::doApply()
|
||||
{
|
||||
@@ -73,6 +120,10 @@ PermissionedDomainDelete::doApply()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Per-entry invariant visitor — no domain-deletion-specific checks yet.
|
||||
*
|
||||
* @note This is a placeholder for future per-entry invariant enforcement.
|
||||
*/
|
||||
void
|
||||
PermissionedDomainDelete::visitInvariantEntry(
|
||||
bool,
|
||||
@@ -82,6 +133,12 @@ PermissionedDomainDelete::visitInvariantEntry(
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** Post-transaction invariant finalization — always passes.
|
||||
*
|
||||
* @note This is a placeholder for future transaction-level invariant checks;
|
||||
* no domain-deletion-specific invariants are enforced yet.
|
||||
* @return `true` unconditionally.
|
||||
*/
|
||||
bool
|
||||
PermissionedDomainDelete::finalizeInvariants(
|
||||
STTx const&,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/** @file
|
||||
* Implementation of the `PermissionedDomainSet` transactor.
|
||||
*
|
||||
* Handles both creation and in-place update of `PermissionedDomain` ledger
|
||||
* objects. The presence of `sfDomainID` in the transaction selects the
|
||||
* update path; its absence triggers creation. See
|
||||
* `PermissionedDomainSet.h` for the public interface contract.
|
||||
*/
|
||||
#include <xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h>
|
||||
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
@@ -73,7 +81,30 @@ PermissionedDomainSet::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Attempt to create the Permissioned Domain. */
|
||||
/** Create or update a `PermissionedDomain` SLE.
|
||||
*
|
||||
* Canonicalises `sfAcceptedCredentials` via `credentials::makeSorted` before
|
||||
* writing, ensuring the stored array is in deterministic order regardless of
|
||||
* the order in which credentials were submitted.
|
||||
*
|
||||
* **Update path** (`sfDomainID` present): replaces the existing domain SLE's
|
||||
* `sfAcceptedCredentials` array in-place and calls `view().update()`. No
|
||||
* reserve change occurs because the object already exists.
|
||||
*
|
||||
* **Create path** (`sfDomainID` absent): verifies the account has sufficient
|
||||
* XRP to cover `accountReserve(ownerCount + 1)`, then allocates a new SLE
|
||||
* keyed by `keylet::permissionedDomain(account_, sfSequence)`. Using the
|
||||
* transaction sequence as the key differentiator gives each domain a globally
|
||||
* unique, collision-resistant identifier without a separate ID-generation
|
||||
* step. The SLE is inserted into the owner directory and the owner count is
|
||||
* incremented by one.
|
||||
*
|
||||
* @return `tesSUCCESS` on success; `tecINSUFFICIENT_RESERVE` if the account
|
||||
* cannot cover the new reserve (create path only); `tecDIR_FULL` if the
|
||||
* owner directory is full (create path, unreachable in practice —
|
||||
* LCOV_EXCL); `tefINTERNAL` if the owner SLE or, on the update path, the
|
||||
* domain SLE is unexpectedly absent (unreachable — LCOV_EXCL).
|
||||
*/
|
||||
TER
|
||||
PermissionedDomainSet::doApply()
|
||||
{
|
||||
@@ -94,7 +125,6 @@ PermissionedDomainSet::doApply()
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfDomainID))
|
||||
{
|
||||
// Modify existing permissioned domain.
|
||||
auto slePd = view().peek(keylet::permissionedDomain(ctx_.tx.getFieldH256(sfDomainID)));
|
||||
if (!slePd)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -103,8 +133,6 @@ PermissionedDomainSet::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new permissioned domain.
|
||||
// Check reserve availability for new object creation
|
||||
auto const balance = STAmount((*ownerSle)[sfBalance]).xrp();
|
||||
auto const reserve = ctx_.view().fees().accountReserve((*ownerSle)[sfOwnerCount] + 1);
|
||||
if (balance < reserve)
|
||||
@@ -123,7 +151,6 @@ PermissionedDomainSet::doApply()
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
|
||||
slePd->setFieldU64(sfOwnerNode, *page);
|
||||
// If we succeeded, the new entry counts against the creator's reserve.
|
||||
adjustOwnerCount(view(), ownerSle, 1, ctx_.journal);
|
||||
view().insert(slePd);
|
||||
}
|
||||
|
||||
@@ -30,32 +30,29 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Calculates the total base fee for a batch transaction.
|
||||
/** Compute the total base fee for a Batch transaction.
|
||||
*
|
||||
* This function computes the required base fee for a batch transaction,
|
||||
* including the base fee for the batch itself, the sum of base fees for
|
||||
* all inner transactions, and additional fees for each batch signer.
|
||||
* It performs overflow checks and validates the structure of the batch
|
||||
* and its signers.
|
||||
* Fee = `view.fees().base` (batch overhead) + `Transactor::calculateBaseFee`
|
||||
* (normal outer-tx base) + sum(inner tx fees) + signerCount × `view.fees().base`.
|
||||
* Each `sfBatchSigners` entry is expanded to its actual key count: a
|
||||
* single-sig entry contributes 1, a multi-sig entry contributes the number of
|
||||
* sub-signers in `sfSigners`.
|
||||
*
|
||||
* @param view The ledger view providing fee and state information.
|
||||
* @param tx The batch transaction to calculate the fee for.
|
||||
* @return XRPAmount The total base fee required for the batch transaction.
|
||||
* Every accumulation is overflow-checked. On overflow or any structural error
|
||||
* that should have been caught by `preflight`, `kINITIAL_XRP` is returned as
|
||||
* a sentinel and the error is logged. These paths are unreachable in practice
|
||||
* (covered by LCOV exclusions) because `preflight` enforces all size limits
|
||||
* and rejects nested Batch transactions.
|
||||
*
|
||||
* @throws std::overflow_error If any fee calculation would overflow the
|
||||
* XRPAmount type.
|
||||
* @throws std::length_error If the number of inner transactions or signers
|
||||
* exceeds the allowed maximum.
|
||||
* @throws std::invalid_argument If an inner transaction is itself a batch
|
||||
* transaction.
|
||||
* @param view The ledger view, used to obtain the network base fee.
|
||||
* @param tx The outer Batch transaction.
|
||||
* @return Total fee in drops, or `kINITIAL_XRP` if an overflow is detected.
|
||||
*/
|
||||
XRPAmount
|
||||
Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount const maxAmount{std::numeric_limits<XRPAmount::value_type>::max()};
|
||||
|
||||
// batchBase: view.fees().base for batch processing + default base fee
|
||||
XRPAmount const baseFee = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
// LCOV_EXCL_START
|
||||
@@ -68,7 +65,6 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
|
||||
XRPAmount const batchBase = view.fees().base + baseFee;
|
||||
|
||||
// Calculate the Inner Txn Fees
|
||||
XRPAmount txnFees{0};
|
||||
if (tx.isFieldPresent(sfRawTransactions))
|
||||
{
|
||||
@@ -107,7 +103,6 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the Signers/BatchSigners Fees
|
||||
std::int32_t signerCount = 0;
|
||||
if (tx.isFieldPresent(sfBatchSigners))
|
||||
{
|
||||
@@ -157,7 +152,6 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
// 10 drops per batch signature + sum of inner tx fees + batchBase
|
||||
return signerFees + txnFees + batchBase;
|
||||
}
|
||||
|
||||
@@ -167,38 +161,26 @@ Batch::getFlagsMask(PreflightContext const& ctx)
|
||||
return tfBatchMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Performs preflight validation checks for a Batch transaction.
|
||||
/** Validate the structural integrity of the Batch transaction.
|
||||
*
|
||||
* This function validates the structure and contents of a Batch transaction
|
||||
* before it is processed. It ensures that the Batch feature is enabled,
|
||||
* checks for valid flags, validates the number and uniqueness of inner
|
||||
* transactions, and enforces correct signing and fee requirements.
|
||||
* `std::popcount` is used on the policy-flag bits to guarantee exactly one
|
||||
* execution policy is active — a simple range or switch check would silently
|
||||
* pass flag combinations.
|
||||
*
|
||||
* The following validations are performed:
|
||||
* - The Batch feature must be enabled in the current rules.
|
||||
* - Only one of the mutually exclusive batch flags must be set.
|
||||
* - The batch must contain at least two and no more than the maximum allowed
|
||||
* inner transactions.
|
||||
* - Each inner transaction must:
|
||||
* - Be unique within the batch.
|
||||
* - Not itself be a Batch transaction.
|
||||
* - Have the tfInnerBatchTxn flag set.
|
||||
* - Not include a TxnSignature or Signers field.
|
||||
* - Have an empty SigningPubKey.
|
||||
* - Pass its own preflight checks.
|
||||
* - Have a fee of zero.
|
||||
* - Have either Sequence or TicketSequence set, but not both or neither.
|
||||
* - Not duplicate Sequence or TicketSequence values for the same account (for
|
||||
* certain flags).
|
||||
* - Validates that all required inner transaction accounts are present in the
|
||||
* batch signers array, and that all batch signers are unique and not the outer
|
||||
* account.
|
||||
* - Verifies the batch signature if batch signers are present.
|
||||
* Duplicate-sequence detection (via `accountSeqTicket`) is enforced only for
|
||||
* `tfAllOrNothing` and `tfUntilFailure`. Those modes commit or abort as a
|
||||
* unit, so two inner transactions from the same account consuming the same
|
||||
* sequence slot would be structurally incoherent. `tfIndependent` and
|
||||
* `tfOnlyOne` relax this: only one of a pair may actually execute, so the
|
||||
* collision is not guaranteed to matter.
|
||||
*
|
||||
* @param ctx The PreflightContext containing the transaction and environment.
|
||||
* @return NotTEC Returns tesSUCCESS if all checks pass, or an appropriate error
|
||||
* code otherwise.
|
||||
* Signer reconciliation (building `requiredSigners` and comparing against
|
||||
* `sfBatchSigners`) is deferred to `preflightSigValidated` so it runs only
|
||||
* after the outer account's own signature is confirmed valid by the framework.
|
||||
*
|
||||
* @param ctx Preflight context for the outer Batch transaction.
|
||||
* @return `tesSUCCESS` on success, or a `tem*` code describing the first
|
||||
* structural violation found.
|
||||
*/
|
||||
NotTEC
|
||||
Batch::preflight(PreflightContext const& ctx)
|
||||
@@ -228,7 +210,6 @@ Batch::preflight(PreflightContext const& ctx)
|
||||
return temARRAY_TOO_LARGE;
|
||||
}
|
||||
|
||||
// Validation Inner Batch Txns
|
||||
std::unordered_set<uint256> uniqueHashes;
|
||||
std::unordered_map<AccountID, std::unordered_set<std::uint32_t>> accountSeqTicket;
|
||||
auto checkSignatureFields =
|
||||
@@ -310,7 +291,6 @@ Batch::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the Fee is native asset (XRP) and zero
|
||||
if (auto const fee = stx.getFieldAmount(sfFee); !fee.native() || fee.xrp() != beast::kZERO)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
|
||||
@@ -331,7 +311,6 @@ Batch::preflight(PreflightContext const& ctx)
|
||||
return temINVALID_INNER_BATCH;
|
||||
}
|
||||
|
||||
// Check that Sequence and TicketSequence are not both present
|
||||
if (stx.isFieldPresent(sfTicketSequence) && stx.getFieldU32(sfSequence) != 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
|
||||
@@ -341,7 +320,6 @@ Batch::preflight(PreflightContext const& ctx)
|
||||
return temSEQ_AND_TICKET;
|
||||
}
|
||||
|
||||
// Verify that either Sequence or TicketSequence is present
|
||||
if (!stx.isFieldPresent(sfTicketSequence) && stx.getFieldU32(sfSequence) == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
|
||||
@@ -351,7 +329,6 @@ Batch::preflight(PreflightContext const& ctx)
|
||||
return temSEQ_AND_TICKET;
|
||||
}
|
||||
|
||||
// Duplicate sequence and ticket checks
|
||||
if ((flags & (tfAllOrNothing | tfUntilFailure)) != 0u)
|
||||
{
|
||||
if (auto const seq = stx.getFieldU32(sfSequence); seq != 0)
|
||||
@@ -382,6 +359,25 @@ Batch::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Validate batch-signer authorization after the outer signature is verified.
|
||||
*
|
||||
* Builds `requiredSigners` from every inner-transaction account and every
|
||||
* `sfCounterparty` field that differs from the outer account, then performs a
|
||||
* bidirectional reconciliation against `sfBatchSigners`: each batch signer is
|
||||
* removed from `requiredSigners` as matched; a signer absent from
|
||||
* `requiredSigners` is extraneous (`temBAD_SIGNER`). After the loop,
|
||||
* any non-empty `requiredSigners` means a required party did not provide a
|
||||
* batch signature. Finally, `ctx.tx.checkBatchSign()` verifies the
|
||||
* cryptographic payload produced by `serializeBatch()`.
|
||||
*
|
||||
* Called by the framework only after the outer account's own signature is
|
||||
* confirmed valid, ensuring the submitter is authenticated before evaluating
|
||||
* whether other parties have also signed.
|
||||
*
|
||||
* @param ctx Preflight context, post outer-signature verification.
|
||||
* @return `tesSUCCESS` if all required parties have valid batch signatures;
|
||||
* a `tem*` code otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -389,7 +385,6 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
auto const outerAccount = ctx.tx.getAccountID(sfAccount);
|
||||
auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions);
|
||||
|
||||
// Build the signers list
|
||||
std::unordered_set<AccountID> requiredSigners;
|
||||
for (STObject const& rb : rawTxns)
|
||||
{
|
||||
@@ -406,13 +401,11 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
requiredSigners.insert(*counterparty);
|
||||
}
|
||||
|
||||
// Validation Batch Signers
|
||||
std::unordered_set<AccountID> batchSigners;
|
||||
if (ctx.tx.isFieldPresent(sfBatchSigners))
|
||||
{
|
||||
STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners);
|
||||
|
||||
// Check that the batch signers array is not too large.
|
||||
if (signers.size() > kMAX_BATCH_TX_COUNT)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
|
||||
@@ -420,11 +413,6 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
return temARRAY_TOO_LARGE;
|
||||
}
|
||||
|
||||
// Add batch signers to the set to ensure all signer accounts are
|
||||
// unique. Meanwhile, remove signer accounts from the set of inner
|
||||
// transaction accounts (`requiredSigners`). By the end of the loop,
|
||||
// `requiredSigners` should be empty, indicating that all inner
|
||||
// accounts are matched with signers.
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
AccountID const signerAccount = signer.getAccountID(sfAccount);
|
||||
@@ -442,8 +430,6 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
return temREDUNDANT;
|
||||
}
|
||||
|
||||
// Check that the batch signer is in the required signers set.
|
||||
// Remove it if it does, as it can be crossed off the list.
|
||||
if (requiredSigners.erase(signerAccount) == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
|
||||
@@ -452,7 +438,6 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Check the batch signers signatures.
|
||||
auto const sigResult = ctx.tx.checkBatchSign(ctx.rules);
|
||||
|
||||
if (!sigResult)
|
||||
@@ -472,22 +457,15 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks the validity of signatures for a batch transaction.
|
||||
/** Verify the outer account's signature and the batch-signers' signatures.
|
||||
*
|
||||
* This method first verifies the standard transaction signature by calling
|
||||
* Transactor::checkSign. If the signature is not valid it returns the
|
||||
* corresponding error code.
|
||||
* Chains `Transactor::checkSign` (outer account: regular key, master key, or
|
||||
* signer list) and `Transactor::checkBatchSign` (on-ledger credentials for
|
||||
* each entry in `sfBatchSigners`). Both must pass; failure at the first check
|
||||
* short-circuits.
|
||||
*
|
||||
* Next, it verifies the batch-specific signature requirements by calling
|
||||
* Transactor::checkBatchSign. If this check fails, it also returns the
|
||||
* corresponding error code.
|
||||
*
|
||||
* If both checks succeed, the function returns tesSUCCESS.
|
||||
*
|
||||
* @param ctx The PreclaimContext containing transaction and environment data.
|
||||
* @return NotTEC Returns tesSUCCESS if all signature checks pass, or an error
|
||||
* code otherwise.
|
||||
* @param ctx Preclaim context providing a read-only ledger view.
|
||||
* @return `tesSUCCESS` if both checks pass; a `tef*` code otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
Batch::checkSign(PreclaimContext const& ctx)
|
||||
@@ -501,15 +479,15 @@ Batch::checkSign(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Applies the outer batch transaction.
|
||||
/** Apply the outer Batch transaction.
|
||||
*
|
||||
* This method is responsible for applying the outer batch transaction.
|
||||
* The inner transactions within the batch are applied separately in the
|
||||
* `applyBatchTransactions` method after the outer transaction is processed.
|
||||
* Returns `tesSUCCESS` immediately. The outer Batch does not directly mutate
|
||||
* any ledger objects — fee deduction and sequence consumption are handled by
|
||||
* the base-class `apply()` method. Inner transaction execution is performed
|
||||
* by `applyBatchTransactions()` in `apply.cpp`, called by `applyTransaction()`
|
||||
* after this method returns.
|
||||
*
|
||||
* @return TER Returns tesSUCCESS to indicate successful application of the
|
||||
* outer batch transaction.
|
||||
* @return Always `tesSUCCESS`.
|
||||
*/
|
||||
TER
|
||||
Batch::doApply()
|
||||
@@ -517,19 +495,33 @@ Batch::doApply()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Invariant visitor for the outer Batch transaction — currently a no-op.
|
||||
*
|
||||
* The outer Batch does not directly create or modify ledger objects beyond
|
||||
* fee and sequence handling, so there are no Batch-specific per-entry
|
||||
* invariants to enforce. Inner-transaction invariant checks are performed by
|
||||
* each inner transactor's own apply context.
|
||||
*/
|
||||
void
|
||||
Batch::visitInvariantEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** Finalize invariant checks for the outer Batch transaction — currently a no-op.
|
||||
*
|
||||
* Returns `true` unconditionally. No outer-Batch invariant violations are
|
||||
* possible: the outer transaction only deducts a fee and consumes a sequence,
|
||||
* both of which are covered by shared invariant checkers. Inner-transaction
|
||||
* invariants are finalized within each inner transactor's own apply context.
|
||||
*
|
||||
* @return Always `true`.
|
||||
*/
|
||||
bool
|
||||
Batch::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* Implements the `Change` transactor, which processes the three
|
||||
* consensus-synthesized pseudo-transaction types: `ttAMENDMENT` (amendment
|
||||
* lifecycle events), `ttFEE` (fee-schedule updates), and `ttUNL_MODIFY`
|
||||
* (Negative UNL validator staging). None of these are submitted by users;
|
||||
* they are injected directly into ledger proposals by the consensus machinery.
|
||||
*
|
||||
* @see Change.h for class-level documentation and the `EnableAmendment`,
|
||||
* `SetFee`, and `UNLModify` semantic aliases.
|
||||
*/
|
||||
#include <xrpl/tx/transactors/system/Change.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -29,14 +39,27 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Full specialization of `Transactor::invokePreflight` for pseudo-transactions.
|
||||
*
|
||||
* Pseudo-transactions violate nearly every rule enforced by the generic
|
||||
* pipeline: they carry no signature, no signing public key, no multisig
|
||||
* signers list, a zero fee, a zero sequence, and the zero account ID as
|
||||
* their source. This specialization validates exactly those invariants
|
||||
* instead of delegating to `preflight1` / `preflight2`.
|
||||
*
|
||||
* Flag-mask enforcement (`tfEnableAmendmentMask`) is gated behind
|
||||
* `featureLendingProtocol`. That feature introduced the flag parameter; a
|
||||
* dedicated amendment purely to enable this guard would add protocol
|
||||
* complexity for no meaningful gain, so the existing feature acts as a proxy.
|
||||
*
|
||||
* @param ctx Preflight context carrying the transaction and current rules.
|
||||
* @return `tesSUCCESS`, `temBAD_SRC_ACCOUNT`, `temBAD_FEE`,
|
||||
* `temBAD_SIGNATURE`, or `temBAD_SEQUENCE`.
|
||||
*/
|
||||
template <>
|
||||
NotTEC
|
||||
Transactor::invokePreflight<Change>(PreflightContext const& ctx)
|
||||
{
|
||||
// 0 means "Allow any flags"
|
||||
// The check for tfEnableAmendmentMask is gated by LendingProtocol because
|
||||
// that feature introduced this parameter, and it's not worth adding another
|
||||
// amendment just for this.
|
||||
if (auto const ret =
|
||||
preflight0(ctx, ctx.rules.enabled(featureLendingProtocol) ? tfEnableAmendmentMask : 0))
|
||||
return ret;
|
||||
@@ -48,7 +71,6 @@ Transactor::invokePreflight<Change>(PreflightContext const& ctx)
|
||||
return temBAD_SRC_ACCOUNT;
|
||||
}
|
||||
|
||||
// No point in going any further if the transaction fee is malformed.
|
||||
auto const fee = ctx.tx.getFieldAmount(sfFee);
|
||||
if (!fee.native() || fee != beast::kZERO)
|
||||
{
|
||||
@@ -201,7 +223,6 @@ Change::applyAmendment()
|
||||
}
|
||||
else
|
||||
{
|
||||
// pass through
|
||||
newMajorities.pushBack(majority);
|
||||
}
|
||||
}
|
||||
@@ -212,7 +233,6 @@ Change::applyAmendment()
|
||||
|
||||
if (gotMajority)
|
||||
{
|
||||
// This amendment now has a majority
|
||||
newMajorities.pushBack(STObject::makeInnerObject(sfMajority));
|
||||
auto& entry = newMajorities.back();
|
||||
entry[sfAmendment] = amendment;
|
||||
@@ -225,7 +245,6 @@ Change::applyAmendment()
|
||||
}
|
||||
else if (!lostMajority)
|
||||
{
|
||||
// No flags, enable amendment
|
||||
amendments.pushBack(amendment);
|
||||
amendmentObject->setFieldV256(sfAmendments, amendments);
|
||||
|
||||
@@ -351,14 +370,12 @@ Change::applyUNLModify()
|
||||
|
||||
if (disabling)
|
||||
{
|
||||
// cannot have more than one toDisable
|
||||
if (negUnlObject->isFieldPresent(sfValidatorToDisable))
|
||||
{
|
||||
JLOG(j_.warn()) << "N-UNL: applyUNLModify, already has ToDisable";
|
||||
return tefFAILURE;
|
||||
}
|
||||
|
||||
// cannot be the same as toReEnable
|
||||
if (negUnlObject->isFieldPresent(sfValidatorToReEnable))
|
||||
{
|
||||
if (negUnlObject->getFieldVL(sfValidatorToReEnable) == validator)
|
||||
@@ -368,7 +385,6 @@ Change::applyUNLModify()
|
||||
}
|
||||
}
|
||||
|
||||
// cannot be in negative UNL already
|
||||
if (found)
|
||||
{
|
||||
JLOG(j_.warn()) << "N-UNL: applyUNLModify, ToDisable already in negative UNL";
|
||||
@@ -379,14 +395,12 @@ Change::applyUNLModify()
|
||||
}
|
||||
else
|
||||
{
|
||||
// cannot have more than one toReEnable
|
||||
if (negUnlObject->isFieldPresent(sfValidatorToReEnable))
|
||||
{
|
||||
JLOG(j_.warn()) << "N-UNL: applyUNLModify, already has ToReEnable";
|
||||
return tefFAILURE;
|
||||
}
|
||||
|
||||
// cannot be the same as toDisable
|
||||
if (negUnlObject->isFieldPresent(sfValidatorToDisable))
|
||||
{
|
||||
if (negUnlObject->getFieldVL(sfValidatorToDisable) == validator)
|
||||
@@ -396,7 +410,6 @@ Change::applyUNLModify()
|
||||
}
|
||||
}
|
||||
|
||||
// must be in negative UNL
|
||||
if (!found)
|
||||
{
|
||||
JLOG(j_.warn()) << "N-UNL: applyUNLModify, ToReEnable is not in negative UNL";
|
||||
@@ -416,13 +429,11 @@ Change::visitInvariantEntry(
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
bool
|
||||
Change::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/** @file
|
||||
* Implements the LedgerStateFix transactor (`ttLEDGER_STATE_FIX`), a
|
||||
* privileged maintenance mechanism for correcting corrupted ledger state.
|
||||
*
|
||||
* Currently supports one repair operation (`NfTokenPageLink`) that heals
|
||||
* broken doubly-linked page chains in an account's NFToken directory.
|
||||
* New repair types can be added by extending `FixType` and adding matching
|
||||
* cases to each pipeline phase.
|
||||
*
|
||||
* Gated on the `fixNFTokenPageLinks` amendment; the framework rejects the
|
||||
* transaction type entirely before preflight when the amendment is inactive.
|
||||
*/
|
||||
|
||||
#include <xrpl/tx/transactors/system/LedgerStateFix.h>
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -33,11 +46,18 @@ LedgerStateFix::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Override that prices the transaction at one owner reserve rather than the
|
||||
* standard base fee.
|
||||
*
|
||||
* The elevated fee (identical to `AccountDelete`'s pricing) deters speculative
|
||||
* repair submissions: an operator pays a full reserve increment whether or not
|
||||
* `nft::repairNFTokenDirectoryLinks` finds anything to fix. This signals that
|
||||
* the transaction should only be submitted when there is strong reason to
|
||||
* believe the target account's NFToken directory is actually corrupt.
|
||||
*/
|
||||
XRPAmount
|
||||
LedgerStateFix::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
// The fee required for LedgerStateFix is one owner reserve, just like
|
||||
// the fee for AccountDelete.
|
||||
return calculateOwnerReserveFee(view, tx);
|
||||
}
|
||||
|
||||
@@ -57,6 +77,13 @@ LedgerStateFix::preclaim(PreclaimContext const& ctx)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
/** @note A `false` return from `nft::repairNFTokenDirectoryLinks` means the
|
||||
* directory was already consistent — no corrections were applied. `doApply`
|
||||
* maps this to `tecFAILED_PROCESSING` so the submitter is charged the owner
|
||||
* reserve fee with no ledger state change; submitting a repair for a healthy
|
||||
* account is not an internal error, but is a failed operation from the
|
||||
* transaction's point of view.
|
||||
*/
|
||||
TER
|
||||
LedgerStateFix::doApply()
|
||||
{
|
||||
|
||||
@@ -21,13 +21,36 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Build a `TxConsequences` that reports the true sequence consumption.
|
||||
*
|
||||
* A normal transaction consumes exactly one sequence number. `TicketCreate`
|
||||
* burns `sfTicketCount` sequence numbers in a single shot, so the default
|
||||
* `Normal` factory would cause the transaction queue to undercount the
|
||||
* consumed sequence space and misjudge ordering for subsequent same-account
|
||||
* transactions. Passing `sfTicketCount` as `sequencesConsumed` gives the
|
||||
* queue the correct value.
|
||||
*
|
||||
* @param ctx Preflight context providing access to the transaction fields.
|
||||
* @return `TxConsequences` encoding the multi-sequence claim.
|
||||
*/
|
||||
TxConsequences
|
||||
TicketCreate::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
// Create TxConsequences identifying the number of sequences consumed.
|
||||
return TxConsequences{ctx.tx, ctx.tx[sfTicketCount]};
|
||||
}
|
||||
|
||||
/** Validate `sfTicketCount` without accessing ledger state.
|
||||
*
|
||||
* Rejects counts outside `[kMIN_VALID_COUNT, kMAX_VALID_COUNT]`. The upper
|
||||
* bound of 250 is a CPU-budget ceiling: benchmarking showed that creating
|
||||
* 250 tickets in one transaction takes roughly the same validator time as a
|
||||
* compute-intensive three-path `Payment`, keeping per-transaction cost
|
||||
* predictable across node hardware.
|
||||
*
|
||||
* @param ctx Preflight context providing access to the transaction fields.
|
||||
* @return `tesSUCCESS` if the count is within range; `temINVALID_COUNT`
|
||||
* otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
TicketCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -38,6 +61,23 @@ TicketCreate::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Enforce the per-account ticket inventory ceiling before any mutations.
|
||||
*
|
||||
* Computes the net ticket delta: `curTicketCount + addedTickets −
|
||||
* consumedTickets`. The `consumedTickets` term is 1 when this transaction
|
||||
* was itself submitted via a ticket (i.e. `getSeqProxy().isTicket()` is
|
||||
* true), otherwise 0. The subtraction prevents a false rejection when an
|
||||
* account exactly at the 250-ticket limit submits a `TicketCreate` via a
|
||||
* ticket to add one more — the net change is zero and the transaction should
|
||||
* succeed. Unsigned underflow cannot occur because `addedTickets >= 1` and
|
||||
* `consumedTickets <= 1`, so the worst case is `addedTickets ==
|
||||
* consumedTickets`, yielding `curTicketCount` unchanged.
|
||||
*
|
||||
* @param ctx Preclaim context providing read-only ledger access.
|
||||
* @return `tesSUCCESS` if the net delta stays within `kMAX_TICKET_THRESHOLD`;
|
||||
* `tecDIR_FULL` if the threshold would be exceeded; `terNO_ACCOUNT` if
|
||||
* the submitting account does not exist.
|
||||
*/
|
||||
TER
|
||||
TicketCreate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -46,24 +86,56 @@ TicketCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (!sleAccountRoot)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// Make sure the TicketCreate would not cause the account to own
|
||||
// too many tickets.
|
||||
std::uint32_t const curTicketCount = (*sleAccountRoot)[~sfTicketCount].value_or(0u);
|
||||
std::uint32_t const addedTickets = ctx.tx[sfTicketCount];
|
||||
std::uint32_t const consumedTickets = ctx.tx.getSeqProxy().isTicket() ? 1u : 0u;
|
||||
|
||||
// Note that unsigned integer underflow can't currently happen because
|
||||
// o curTicketCount >= 0
|
||||
// o addedTickets >= 1
|
||||
// o consumedTickets <= 1
|
||||
// So in the worst case addedTickets == consumedTickets and the
|
||||
// computation yields curTicketCount.
|
||||
if (curTicketCount + addedTickets - consumedTickets > kMAX_TICKET_THRESHOLD)
|
||||
return tecDIR_FULL;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Create the requested tickets and update all associated account-root fields.
|
||||
*
|
||||
* Execution proceeds in four stages:
|
||||
*
|
||||
* 1. **Reserve check.** The reserve for `ticketCount` additional owned objects
|
||||
* is computed and compared against `preFeeBalance_` (the balance *before*
|
||||
* the transaction fee was deducted). Using the pre-fee snapshot deliberately
|
||||
* permits the account to consume reserve XRP to cover the fee, while still
|
||||
* requiring that the full reserve for the new tickets exists before fee
|
||||
* payment is considered.
|
||||
*
|
||||
* 2. **Sequence anchor.** `firstTicketSeq` is read from `sfSequence` on the
|
||||
* account root, which the transaction machinery has already incremented
|
||||
* before `doApply` runs. A defensive guard checks that the transaction's
|
||||
* own `sfSequence` equals `firstTicketSeq − 1` (or is 0 for
|
||||
* ticket-submitted transactions). This invariant is guaranteed by the
|
||||
* framework; the guard is `LCOV_EXCL_LINE` because it should never fire
|
||||
* outside of a framework bug.
|
||||
*
|
||||
* 3. **Ticket creation loop.** For each of the `ticketCount` tickets, an
|
||||
* `ltTICKET` SLE is allocated with a contiguous sequence number
|
||||
* (`firstTicketSeq + i`), inserted into the ledger, and linked into the
|
||||
* account's owner directory via `dirInsert`. The returned directory page
|
||||
* number is stored in `sfOwnerNode` to enable O(1) deletion later.
|
||||
*
|
||||
* 4. **Account-root update.** After the loop: `sfTicketCount` is incremented
|
||||
* by `ticketCount` (the field `preclaim` will read on future
|
||||
* `TicketCreate` calls); `adjustOwnerCount` raises `sfOwnerCount` by
|
||||
* `ticketCount`; and `sfSequence` is advanced to `firstTicketSeq +
|
||||
* ticketCount`. This final sequence advance — by more than one — is
|
||||
* unique to `TicketCreate` in the XRPL protocol and is the mechanism by
|
||||
* which those sequence numbers are reserved as tickets rather than left as
|
||||
* gaps.
|
||||
*
|
||||
* @return `tesSUCCESS` on success; `tecINSUFFICIENT_RESERVE` if `preFeeBalance_`
|
||||
* cannot cover the new reserve obligation; `tecDIR_FULL` if the owner
|
||||
* directory is exhausted (practically unreachable); `tefINTERNAL` if the
|
||||
* account SLE is missing or the sequence invariant is violated (both
|
||||
* indicate framework bugs and are excluded from coverage analysis).
|
||||
*/
|
||||
TER
|
||||
TicketCreate::doApply()
|
||||
{
|
||||
@@ -71,9 +143,6 @@ TicketCreate::doApply()
|
||||
if (!sleAccountRoot)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Each ticket counts against the reserve of the issuing account, but we
|
||||
// check the starting balance because we want to allow dipping into the
|
||||
// reserve to pay fees.
|
||||
std::uint32_t const ticketCount = ctx_.tx[sfTicketCount];
|
||||
{
|
||||
XRPAmount const reserve =
|
||||
@@ -85,14 +154,8 @@ TicketCreate::doApply()
|
||||
|
||||
beast::Journal const viewJ{ctx_.registry.get().getJournal("View")};
|
||||
|
||||
// The starting ticket sequence is the same as the current account
|
||||
// root sequence. Before we got here to doApply(), the transaction
|
||||
// machinery already incremented the account root sequence if that
|
||||
// was appropriate.
|
||||
std::uint32_t const firstTicketSeq = (*sleAccountRoot)[sfSequence];
|
||||
|
||||
// Sanity check that the transaction machinery really did already
|
||||
// increment the account root Sequence.
|
||||
if (std::uint32_t const txSeq = ctx_.tx[sfSequence];
|
||||
txSeq != 0 && txSeq != (firstTicketSeq - 1))
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -119,21 +182,26 @@ TicketCreate::doApply()
|
||||
sleTicket->setFieldU64(sfOwnerNode, *page);
|
||||
}
|
||||
|
||||
// Update the record of the number of Tickets this account owns.
|
||||
std::uint32_t const oldTicketCount = (*(sleAccountRoot))[~sfTicketCount].valueOr(0u);
|
||||
|
||||
sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount);
|
||||
|
||||
// Every added Ticket counts against the creator's reserve.
|
||||
adjustOwnerCount(view(), sleAccountRoot, ticketCount, viewJ);
|
||||
|
||||
// TicketCreate is the only transaction that can cause an account root's
|
||||
// Sequence field to increase by more than one. October 2018.
|
||||
// TicketCreate is the only transaction that can advance sfSequence by
|
||||
// more than one — this is the mechanism that reserves those sequence
|
||||
// numbers as tickets rather than leaving gaps.
|
||||
sleAccountRoot->setFieldU32(sfSequence, firstTicketSeq + ticketCount);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Invariant visitor stub — no per-entry checks implemented yet.
|
||||
*
|
||||
* Ticket creation and deletion are already validated by the generic
|
||||
* `AccountRootsDeletedClean` and `XRPNotCreated` framework invariants.
|
||||
* This override exists as a placeholder for any future `ltTICKET`-specific
|
||||
* checks.
|
||||
*/
|
||||
void
|
||||
TicketCreate::visitInvariantEntry(
|
||||
bool,
|
||||
@@ -143,6 +211,14 @@ TicketCreate::visitInvariantEntry(
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** Invariant finalization stub — unconditionally passes.
|
||||
*
|
||||
* No transaction-specific finalization logic is currently required.
|
||||
* Always returns `true` so the invariant framework treats this transactor
|
||||
* as passing its own checks.
|
||||
*
|
||||
* @return `true` unconditionally.
|
||||
*/
|
||||
bool
|
||||
TicketCreate::finalizeInvariants(
|
||||
STTx const&,
|
||||
|
||||
@@ -33,6 +33,24 @@ template <ValidIssueType T>
|
||||
static NotTEC
|
||||
preflightHelper(PreflightContext const& ctx);
|
||||
|
||||
/** Validate IOU clawback transaction fields before any ledger access.
|
||||
*
|
||||
* The IOU encoding packs the token holder's account into the `issuer`
|
||||
* sub-field of `sfAmount` rather than a separate `sfHolder` field.
|
||||
* Consequently, `sfHolder` must be absent — its presence is treated as a
|
||||
* malformed transaction to enforce mutual exclusivity between the two
|
||||
* encoding conventions.
|
||||
*
|
||||
* Three structural constraints are checked:
|
||||
* - `sfHolder` absent (IOU encoding uses `sfAmount.issuer` for the holder)
|
||||
* - `sfAmount` is not XRP and is strictly positive
|
||||
* - The holder encoded in `sfAmount` differs from `sfAccount` (the issuer)
|
||||
*
|
||||
* @param ctx Stateless preflight context.
|
||||
* @return `tesSUCCESS` if valid; `temMALFORMED` if `sfHolder` is present;
|
||||
* `temBAD_AMOUNT` if the amount is XRP, non-positive, or names the
|
||||
* issuer as the holder.
|
||||
*/
|
||||
template <>
|
||||
NotTEC
|
||||
preflightHelper<Issue>(PreflightContext const& ctx)
|
||||
@@ -52,6 +70,20 @@ preflightHelper<Issue>(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Validate MPT clawback transaction fields before any ledger access.
|
||||
*
|
||||
* MPT clawback uses the conventional `sfHolder` field rather than the IOU
|
||||
* encoding trick. The function checks:
|
||||
* - `featureMPTokensV1` is enabled (otherwise the transaction type itself
|
||||
* does not exist on the network)
|
||||
* - `sfHolder` is present and names a different account from `sfAccount`
|
||||
* - `sfAmount` is strictly positive and within `kMAX_MP_TOKEN_AMOUNT`
|
||||
*
|
||||
* @param ctx Stateless preflight context.
|
||||
* @return `tesSUCCESS` if valid; `temDISABLED` if the MPT amendment is off;
|
||||
* `temMALFORMED` if `sfHolder` is absent or equals `sfAccount`;
|
||||
* `temBAD_AMOUNT` if the amount is out of range or non-positive.
|
||||
*/
|
||||
template <>
|
||||
NotTEC
|
||||
preflightHelper<MPTIssue>(PreflightContext const& ctx)
|
||||
@@ -75,6 +107,16 @@ preflightHelper<MPTIssue>(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Dispatch to the asset-type-specific preflight helper.
|
||||
*
|
||||
* Resolves the runtime `std::variant<Issue, MPTIssue>` inside `sfAmount`
|
||||
* to a compile-time template parameter and forwards to
|
||||
* `preflightHelper<Issue>` or `preflightHelper<MPTIssue>`. No ledger state
|
||||
* is accessed; all checks are purely structural.
|
||||
*
|
||||
* @param ctx Stateless preflight context.
|
||||
* @return The result of the appropriate `preflightHelper` specialisation.
|
||||
*/
|
||||
NotTEC
|
||||
Clawback::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -96,6 +138,41 @@ preclaimHelper(
|
||||
AccountID const& holder,
|
||||
STAmount const& clawAmount);
|
||||
|
||||
/** Validate ledger preconditions for an IOU clawback.
|
||||
*
|
||||
* Checks are applied in this order:
|
||||
*
|
||||
* 1. **Flag constraints on the issuer**: `lsfAllowTrustLineClawback` must
|
||||
* be set and `lsfNoFreeze` must not be set. These flags are mutually
|
||||
* exclusive by design — an issuer that permanently waived freeze authority
|
||||
* also loses clawback authority because clawback is a strictly stronger
|
||||
* power.
|
||||
*
|
||||
* 2. **Trust-line existence**: the trust line between `holder` and `issuer`
|
||||
* for the given currency must exist; missing → `tecNO_LINE`.
|
||||
*
|
||||
* 3. **Balance sign/address ordering invariant**: XRPL stores trust-line
|
||||
* balances with a sign convention keyed to account address ordering. A
|
||||
* positive raw `sfBalance` means the account with the higher address is
|
||||
* the net holder; a negative value means the lower address is the holder.
|
||||
* If the raw balance contradicts the issuer/holder address order, the
|
||||
* transaction is rejected with `tecNO_PERMISSION` to prevent targeting
|
||||
* the wrong side of the trust line.
|
||||
*
|
||||
* 4. **Non-zero spendable balance**: `accountHolds` with `fhIGNORE_FREEZE`
|
||||
* is used rather than the raw `sfBalance` from the SLE, because the
|
||||
* available balance is subject to additional constraints (e.g., XLS-34
|
||||
* lock-ups) that the raw field does not reflect.
|
||||
*
|
||||
* @param ctx Read-only preclaim context.
|
||||
* @param sleIssuer The issuer's `AccountRoot` SLE.
|
||||
* @param issuer The issuer's `AccountID`.
|
||||
* @param holder The holder's `AccountID`.
|
||||
* @param clawAmount The amount specified in the transaction (IOU variant).
|
||||
* @return `tesSUCCESS` on success; `tecNO_PERMISSION` if flag constraints or
|
||||
* balance-sign ordering fail; `tecNO_LINE` if the trust line is absent;
|
||||
* `tecINSUFFICIENT_FUNDS` if the spendable balance is zero.
|
||||
*/
|
||||
template <>
|
||||
TER
|
||||
preclaimHelper<Issue>(
|
||||
@@ -128,15 +205,9 @@ preclaimHelper<Issue>(
|
||||
if (balance < beast::kZERO && issuer > holder)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// At this point, we know that issuer and holder accounts
|
||||
// are correct and a trustline exists between them.
|
||||
//
|
||||
// Must now explicitly check the balance to make sure
|
||||
// available balance is non-zero.
|
||||
//
|
||||
// We can't directly check the balance of trustline because
|
||||
// the available balance of a trustline is prone to new changes (eg.
|
||||
// XLS-34). So we must use `accountHolds`.
|
||||
// Must use `accountHolds` rather than reading the raw trust-line balance
|
||||
// directly: the spendable balance can be further constrained by features
|
||||
// such as XLS-34 that impose holds on top of the nominal sfBalance.
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
holder,
|
||||
@@ -149,6 +220,39 @@ preclaimHelper<Issue>(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Validate ledger preconditions for an MPT clawback.
|
||||
*
|
||||
* Checks are applied in this order:
|
||||
*
|
||||
* 1. **MPT issuance object exists**: the `MPTIssuance` SLE identified by
|
||||
* the `MPTIssue` in `sfAmount` must be present in the ledger.
|
||||
*
|
||||
* 2. **`lsfMPTCanClawback` flag**: the flag is immutable after issuance
|
||||
* creation. If it was not set at creation time the issuer permanently
|
||||
* lacks clawback authority.
|
||||
*
|
||||
* 3. **Issuer ownership**: the `sfIssuer` field on the issuance object must
|
||||
* match the transaction submitter. This prevents a third party from
|
||||
* constructing a clawback against an issuance they do not own.
|
||||
*
|
||||
* 4. **Holder `MPToken` object exists**: `keylet::mptoken` must resolve to
|
||||
* an existing ledger object; missing → `tecOBJECT_NOT_FOUND`.
|
||||
*
|
||||
* 5. **Non-zero spendable balance**: `accountHolds` with both
|
||||
* `fhIGNORE_FREEZE` and `ahIGNORE_AUTH` is used. Authorization status
|
||||
* is irrelevant for a forced reclaim by the issuer.
|
||||
*
|
||||
* @param ctx Read-only preclaim context.
|
||||
* @param sleIssuer The issuer's `AccountRoot` SLE (unused; kept for uniform
|
||||
* signature with the `Issue` specialisation).
|
||||
* @param issuer The issuer's `AccountID`.
|
||||
* @param holder The holder's `AccountID`.
|
||||
* @param clawAmount The amount specified in the transaction (MPT variant).
|
||||
* @return `tesSUCCESS` on success; `tecOBJECT_NOT_FOUND` if the issuance or
|
||||
* holder token object is missing; `tecNO_PERMISSION` if the clawback flag
|
||||
* is unset or the caller is not the issuer; `tecINSUFFICIENT_FUNDS` if
|
||||
* the spendable balance is zero.
|
||||
*/
|
||||
template <>
|
||||
TER
|
||||
preclaimHelper<MPTIssue>(
|
||||
@@ -184,6 +288,28 @@ preclaimHelper<MPTIssue>(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Validate ledger preconditions for a clawback transaction.
|
||||
*
|
||||
* Resolves the holder account ID from the asset-type-dependent encoding
|
||||
* (IOU: from `sfAmount.issuer`; MPT: from `sfHolder`), then applies
|
||||
* protocol-level guards before dispatching to the asset-specific helper.
|
||||
*
|
||||
* Order of checks:
|
||||
* 1. Both issuer and holder `AccountRoot` SLEs must exist.
|
||||
* 2. When `featureSingleAssetVault` is active, the holder must not be a
|
||||
* pseudo-account (`isPseudoAccount` → `tecPSEUDO_ACCOUNT`). When SAV is
|
||||
* active this check subsumes the AMM check that follows.
|
||||
* 3. The holder must not be an AMM liquidity-pool account (`sfAMMID` present
|
||||
* → `tecAMM_ACCOUNT`). AMM pool balances are managed via `AMMClawback`
|
||||
* rather than the standard clawback path.
|
||||
* 4. Asset-type-specific preconditions via `preclaimHelper<T>`.
|
||||
*
|
||||
* @param ctx Read-only preclaim context.
|
||||
* @return `tesSUCCESS` on success; `terNO_ACCOUNT` if either account is
|
||||
* missing; `tecPSEUDO_ACCOUNT` or `tecAMM_ACCOUNT` if the holder is a
|
||||
* protocol-internal account; or any code returned by the type-specific
|
||||
* `preclaimHelper` specialisation.
|
||||
*/
|
||||
TER
|
||||
Clawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -196,8 +322,9 @@ Clawback::preclaim(PreclaimContext const& ctx)
|
||||
if (!sleIssuer || !sleHolder)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// Note the order of checks - when SAV is active, this check here will make
|
||||
// the one which follows `sleHolder->isFieldPresent(sfAMMID)` redundant.
|
||||
// When SAV is active, isPseudoAccount() also catches AMM accounts, making
|
||||
// the sfAMMID check below redundant. Both are kept for defence-in-depth
|
||||
// and to preserve the pre-SAV code path without amendment branching.
|
||||
if (ctx.view.rules().enabled(featureSingleAssetVault) && isPseudoAccount(sleHolder))
|
||||
{
|
||||
return tecPSEUDO_ACCOUNT;
|
||||
@@ -218,6 +345,30 @@ template <ValidIssueType T>
|
||||
static TER
|
||||
applyHelper(ApplyContext& ctx);
|
||||
|
||||
/** Execute an IOU clawback by transferring tokens from holder to issuer.
|
||||
*
|
||||
* The `sfAmount` field as received encodes the holder account in its
|
||||
* `issuer` sub-field (the IOU wire-protocol trick used throughout the
|
||||
* clawback path). Before invoking the transfer, this function corrects the
|
||||
* field by writing the real issuer account ID into `clawAmount.get<Issue>().account`,
|
||||
* so that `directSendNoFee` sees a properly formed amount where `issuer`
|
||||
* identifies the issuer, not the holder.
|
||||
*
|
||||
* The amount transferred is `min(spendableAmount, clawAmount)`. The
|
||||
* spendable amount is re-queried via `accountHolds` (with `fhIGNORE_FREEZE`)
|
||||
* at apply time rather than relying on the preclaim result, because ledger
|
||||
* state may have changed between the two phases. This also correctly handles
|
||||
* XLS-34-style holds that reduce the available balance below the nominal
|
||||
* trust-line balance.
|
||||
*
|
||||
* Passing `/*checkIssuer*/ true` to `directSendNoFee` causes it to verify
|
||||
* issuer involvement in the trust-line accounting, which is appropriate for
|
||||
* the IOU model.
|
||||
*
|
||||
* @param ctx Mutable apply context.
|
||||
* @return `tesSUCCESS` on success; `tecINTERNAL` if holder equals issuer
|
||||
* (should never occur after preclaim; LCOV-excluded).
|
||||
*/
|
||||
template <>
|
||||
TER
|
||||
applyHelper<Issue>(ApplyContext& ctx)
|
||||
@@ -231,7 +382,9 @@ applyHelper<Issue>(ApplyContext& ctx)
|
||||
if (holder == issuer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Get the spendable balance. Must use `accountHolds`.
|
||||
// Re-query spendable balance at apply time: ledger state may have
|
||||
// changed since preclaim, and accountHolds accounts for XLS-34 holds
|
||||
// that the raw sfBalance does not reflect.
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
ctx.view(),
|
||||
holder,
|
||||
@@ -244,6 +397,26 @@ applyHelper<Issue>(ApplyContext& ctx)
|
||||
ctx.view(), holder, issuer, std::min(spendableAmount, clawAmount), true, ctx.journal);
|
||||
}
|
||||
|
||||
/** Execute an MPT clawback by transferring tokens from holder to issuer.
|
||||
*
|
||||
* For MPTs the holder account comes from the conventional `sfHolder` field
|
||||
* (not the `sfAmount.issuer` encoding used by IOUs). No field correction is
|
||||
* required before calling the transfer helper.
|
||||
*
|
||||
* As with the IOU path, the transfer amount is capped at
|
||||
* `min(spendableAmount, clawAmount)`. The spendable balance is re-queried
|
||||
* at apply time via `accountHolds` with both `fhIGNORE_FREEZE` and
|
||||
* `ahIGNORE_AUTH` — authorization status does not gate a forced issuer
|
||||
* reclaim.
|
||||
*
|
||||
* `/*checkIssuer*/ false` is passed to `directSendNoFee` because MPT
|
||||
* issuance ownership is already established by the issuance object itself
|
||||
* (verified in preclaim); the IOU-specific issuer-involvement check in
|
||||
* `directSendNoFee` is not applicable to the MPT model.
|
||||
*
|
||||
* @param ctx Mutable apply context.
|
||||
* @return The result of `directSendNoFee` — `tesSUCCESS` on success.
|
||||
*/
|
||||
template <>
|
||||
TER
|
||||
applyHelper<MPTIssue>(ApplyContext& ctx)
|
||||
@@ -252,7 +425,9 @@ applyHelper<MPTIssue>(ApplyContext& ctx)
|
||||
auto clawAmount = ctx.tx[sfAmount];
|
||||
AccountID const holder = ctx.tx[sfHolder];
|
||||
|
||||
// Get the spendable balance. Must use `accountHolds`.
|
||||
// Re-query spendable balance at apply time with both freeze and auth
|
||||
// handling suppressed: authorization is irrelevant for a forced issuer
|
||||
// reclaim, and the issuer may have frozen the holder's token.
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
ctx.view(),
|
||||
holder,
|
||||
@@ -270,6 +445,16 @@ applyHelper<MPTIssue>(ApplyContext& ctx)
|
||||
ctx.journal);
|
||||
}
|
||||
|
||||
/** Dispatch to the asset-type-specific apply helper and commit mutations.
|
||||
*
|
||||
* Resolves the runtime `std::variant<Issue, MPTIssue>` in `sfAmount` to a
|
||||
* compile-time template parameter and forwards to `applyHelper<Issue>` or
|
||||
* `applyHelper<MPTIssue>`. The chosen helper transfers tokens from the
|
||||
* holder to the issuer via `directSendNoFee`, capped at the holder's
|
||||
* current spendable balance.
|
||||
*
|
||||
* @return The result of the appropriate `applyHelper` specialisation.
|
||||
*/
|
||||
TER
|
||||
Clawback::doApply()
|
||||
{
|
||||
@@ -278,6 +463,13 @@ Clawback::doApply()
|
||||
ctx_.tx[sfAmount].asset().value());
|
||||
}
|
||||
|
||||
/** Per-SLE invariant visitor hook — currently a no-op.
|
||||
*
|
||||
* Reserved for future transaction-specific invariant checks. The global
|
||||
* `ValidClawback` invariant checker (in `InvariantCheck.cpp`) verifies that
|
||||
* at most one trust line or MPT object is modified and that the holder's
|
||||
* balance remains non-negative; no per-transactor check is needed here yet.
|
||||
*/
|
||||
void
|
||||
Clawback::visitInvariantEntry(
|
||||
bool,
|
||||
@@ -287,6 +479,14 @@ Clawback::visitInvariantEntry(
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** Invariant finalisation hook — currently a no-op that always passes.
|
||||
*
|
||||
* Reserved for future transaction-specific invariant checks. Global invariant
|
||||
* enforcement (e.g., `XRPNotCreated`, `ValidClawback`) is performed by the
|
||||
* framework independently of this hook.
|
||||
*
|
||||
* @return Always `true`; no violations are currently checked.
|
||||
*/
|
||||
bool
|
||||
Clawback::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/** @file
|
||||
* Implements the `MPTokenAuthorize` transactor, which handles all
|
||||
* authorization state transitions for Multi-Purpose Tokens (MPTs).
|
||||
*
|
||||
* A single transaction type covers four operations differentiated by the
|
||||
* presence of the optional `sfHolder` field and the `tfMPTUnauthorize` flag:
|
||||
* holder opt-in (create `MPToken` SLE), holder opt-out (delete `MPToken` SLE),
|
||||
* issuer allowlist grant (`lsfMPTAuthorized` set), and issuer allowlist revoke
|
||||
* (`lsfMPTAuthorized` cleared). All ledger mutations are delegated to
|
||||
* `authorizeMPToken()` in `MPTokenHelpers.cpp`.
|
||||
*/
|
||||
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
|
||||
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
@@ -19,12 +30,29 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Return `tfMPTokenAuthorizeMask` — the complete set of valid flag bits for
|
||||
* this transaction type.
|
||||
*
|
||||
* Called by the `preflight1()` framework before field validation; any flag bit
|
||||
* not present in the mask causes immediate rejection with `temINVALID_FLAG`.
|
||||
*/
|
||||
std::uint32_t
|
||||
MPTokenAuthorize::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfMPTokenAuthorizeMask;
|
||||
}
|
||||
|
||||
/** Stateless preflight guard — rejects self-authorization attempts.
|
||||
*
|
||||
* The only check needed here is that `sfAccount != sfHolder`. An account
|
||||
* naming itself as the holder it wants to authorize would be degenerate: the
|
||||
* holder and issuer paths in `preclaim` both assume these two identities are
|
||||
* distinct, so catching this early avoids undefined branching downstream.
|
||||
* Amendment gating and fee/sequence checks are handled by the surrounding
|
||||
* `invokePreflight<T>()` machinery; this method need not repeat them.
|
||||
*
|
||||
* @return `temMALFORMED` if `sfAccount == sfHolder`; `tesSUCCESS` otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
MPTokenAuthorize::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -34,30 +62,63 @@ MPTokenAuthorize::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Validate ledger state for all four authorization operations.
|
||||
*
|
||||
* Branches on whether `sfHolder` is present in the transaction:
|
||||
*
|
||||
* **Holder path** (`sfHolder` absent — submitter is the holder):
|
||||
*
|
||||
* The `tfMPTUnauthorize` check is performed before reading the issuance SLE.
|
||||
* This ordering is intentional: when all holders reach a zero balance before
|
||||
* the issuer destroys the issuance, the outstanding `MPToken` objects must
|
||||
* still be cleanable after the issuance is gone. Checking the unauthorize flag
|
||||
* first avoids a spurious `tecOBJECT_NOT_FOUND` in that cleanup scenario.
|
||||
*
|
||||
* - With `tfMPTUnauthorize` (delete):
|
||||
* - `MPToken` SLE must exist (`tecOBJECT_NOT_FOUND`).
|
||||
* - `sfMPTAmount` must be zero (`tecHAS_OBLIGATIONS`).
|
||||
* - `sfLockedAmount` (optional) must also be zero (`tecHAS_OBLIGATIONS`);
|
||||
* a zero net balance with non-zero locked amount indicates active escrow
|
||||
* or vault operations that have not yet settled.
|
||||
* - When `featureSingleAssetVault` is active, `lsfMPTLocked` must be clear
|
||||
* on the `MPToken` SLE (`tecNO_PERMISSION`); a vault hold prevents opt-out.
|
||||
*
|
||||
* - Without `tfMPTUnauthorize` (create / opt-in):
|
||||
* - `MPTokenIssuance` must exist (`tecOBJECT_NOT_FOUND`).
|
||||
* - Submitter must not be the issuer (`tecNO_PERMISSION`).
|
||||
* - `MPToken` SLE must not already exist (`tecDUPLICATE`).
|
||||
*
|
||||
* **Issuer path** (`sfHolder` present — submitter is the issuer):
|
||||
*
|
||||
* - Named holder account must exist on the ledger (`tecNO_DST`).
|
||||
* - `MPTokenIssuance` must exist (`tecOBJECT_NOT_FOUND`).
|
||||
* - Submitter must be `sfIssuer` on the issuance (`tecNO_PERMISSION`).
|
||||
* - Issuance must carry `lsfMPTRequireAuth` (`tecNO_AUTH`); managing an
|
||||
* allowlist on a non-auth issuance is meaningless.
|
||||
* - Holder must have already created their `MPToken` SLE (`tecOBJECT_NOT_FOUND`);
|
||||
* the protocol enforces a holder-first, issuer-second handshake.
|
||||
* - Holder must not be a pseudo-account (`tecNO_PERMISSION`); vault and loan
|
||||
* broker pseudo-accounts (identified by `sfVaultID` / `sfLoanBrokerID`) are
|
||||
* implicitly always authorized by the protocol and cannot be managed via this
|
||||
* path. No amendment gate is needed: such accounts only exist when
|
||||
* `featureSingleAssetVault` is already active.
|
||||
*
|
||||
* @return `tesSUCCESS`, or one of the `tec`/`tef` error codes described above.
|
||||
*/
|
||||
TER
|
||||
MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const accountID = ctx.tx[sfAccount];
|
||||
auto const holderID = ctx.tx[~sfHolder];
|
||||
|
||||
// if non-issuer account submits this tx, then they are trying either:
|
||||
// 1. Unauthorize/delete MPToken
|
||||
// 2. Use/create MPToken
|
||||
//
|
||||
// Note: `accountID` is holder's account
|
||||
// `holderID` is NOT used
|
||||
if (!holderID)
|
||||
{
|
||||
std::shared_ptr<SLE const> const sleMpt =
|
||||
ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], accountID));
|
||||
|
||||
// There is an edge case where all holders have zero balance, issuance
|
||||
// is legally destroyed, then outstanding MPT(s) are deleted afterwards.
|
||||
// Thus, there is no need to check for the existence of the issuance if
|
||||
// the MPT is being deleted with a zero balance. Check for unauthorize
|
||||
// before fetching the MPTIssuance object.
|
||||
|
||||
// if holder wants to delete/unauthorize a mpt
|
||||
// Check for unauthorize before fetching the MPTIssuance object: a
|
||||
// holder may delete a zero-balance MPToken even after the issuance has
|
||||
// been destroyed (post-destruction cleanup path).
|
||||
if ((ctx.tx.getFlags() & tfMPTUnauthorize) != 0u)
|
||||
{
|
||||
if (!sleMpt)
|
||||
@@ -73,6 +134,8 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
}
|
||||
|
||||
// A zero net balance with non-zero locked amount indicates tokens
|
||||
// still held in escrow or vault; must settle before opt-out.
|
||||
if ((*sleMpt)[~sfLockedAmount].value_or(0) != 0)
|
||||
{
|
||||
auto const sleMptIssuance =
|
||||
@@ -88,7 +151,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Now test when the holder wants to hold/create/authorize a new MPT
|
||||
auto const sleMptIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
|
||||
if (!sleMptIssuance)
|
||||
@@ -97,7 +159,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
if (accountID == (*sleMptIssuance)[sfIssuer])
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// if holder wants to use and create a mpt
|
||||
if (sleMpt)
|
||||
return tecDUPLICATE;
|
||||
|
||||
@@ -114,22 +175,16 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
std::uint32_t const mptIssuanceFlags = sleMptIssuance->getFieldU32(sfFlags);
|
||||
|
||||
// If tx is submitted by issuer, they would either try to do the following
|
||||
// for allowlisting:
|
||||
// 1. authorize an account
|
||||
// 2. unauthorize an account
|
||||
//
|
||||
// Note: `accountID` is issuer's account
|
||||
// `holderID` is holder's account
|
||||
if (accountID != (*sleMptIssuance)[sfIssuer])
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If tx is submitted by issuer, it only applies for MPT with
|
||||
// lsfMPTRequireAuth set
|
||||
// Allowlist management only applies when the issuance requires auth;
|
||||
// granting/revoking on an open issuance is meaningless.
|
||||
if ((mptIssuanceFlags & lsfMPTRequireAuth) == 0u)
|
||||
return tecNO_AUTH;
|
||||
|
||||
// The holder must create the MPT before the issuer can authorize it.
|
||||
// The holder must opt in before the issuer can authorize them
|
||||
// (holder-first, issuer-second handshake).
|
||||
if (!ctx.view.exists(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID)))
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
@@ -142,6 +197,21 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Apply the authorization change to the mutable ledger view.
|
||||
*
|
||||
* Delegates entirely to `authorizeMPToken()`, passing `preFeeBalance_` — the
|
||||
* XRP balance captured by the base `Transactor` class before the transaction
|
||||
* fee was deducted — as the reserve baseline for new `MPToken` SLE creation.
|
||||
* All ledger mutations (SLE create/delete, owner directory linking, flag
|
||||
* toggling) happen inside that helper. Because `preclaim` has already
|
||||
* established all preconditions, any SLE-lookup failure inside
|
||||
* `authorizeMPToken` is treated as `tecINTERNAL` and annotated
|
||||
* `LCOV_EXCL_LINE`.
|
||||
*
|
||||
* @return `tesSUCCESS`, `tecINSUFFICIENT_RESERVE` when a new `MPToken` SLE
|
||||
* cannot be created due to insufficient XRP reserve, or a `tef` code on
|
||||
* internal invariant violations in `authorizeMPToken`.
|
||||
*/
|
||||
TER
|
||||
MPTokenAuthorize::doApply()
|
||||
{
|
||||
@@ -156,6 +226,13 @@ MPTokenAuthorize::doApply()
|
||||
tx[~sfHolder]);
|
||||
}
|
||||
|
||||
/** No-op stub satisfying the `Transactor` pure-virtual interface.
|
||||
*
|
||||
* No transaction-specific invariants are defined for `MPTokenAuthorize` yet.
|
||||
* Global invariants in `InvariantCheck.cpp` (e.g., `ValidMPTIssuance`,
|
||||
* `ValidMPTPayment`) cover the relevant MPT ledger state; this method is
|
||||
* reserved for any future per-transactor checks.
|
||||
*/
|
||||
void
|
||||
MPTokenAuthorize::visitInvariantEntry(
|
||||
bool,
|
||||
@@ -165,6 +242,15 @@ MPTokenAuthorize::visitInvariantEntry(
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** No-op stub satisfying the `Transactor` pure-virtual interface.
|
||||
*
|
||||
* Always returns `true`. No transaction-specific finalization invariants are
|
||||
* defined for `MPTokenAuthorize`; the global checker suite handles MPT
|
||||
* issuance and holder-count accounting. This method is the extension point
|
||||
* for future per-transactor post-apply assertions.
|
||||
*
|
||||
* @return Always `true`.
|
||||
*/
|
||||
bool
|
||||
MPTokenAuthorize::finalizeInvariants(
|
||||
STTx const&,
|
||||
|
||||
@@ -25,6 +25,23 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Gate optional field categories on the amendments that enable them.
|
||||
*
|
||||
* Runs before the main flag-mask check in `invokePreflight`, so rejections
|
||||
* here are cheaper than a full preflight. Two independent gates are applied:
|
||||
*
|
||||
* - `sfDomainID` is only meaningful when both `featurePermissionedDomains`
|
||||
* and `featureSingleAssetVault` are active. Requiring both ensures the
|
||||
* domain registry and the vault subsystem are available before any
|
||||
* domain-scoped issuance can be created.
|
||||
*
|
||||
* - `sfMutableFlags` requires `featureDynamicMPT`. Without that amendment
|
||||
* the ledger has no mechanism to honor mutability requests, so a non-null
|
||||
* value would be silently dropped — returning false here prevents that.
|
||||
*
|
||||
* @return `true` if all present gated fields are covered by active amendments;
|
||||
* `false` causes `invokePreflight` to return `temDISABLED`.
|
||||
*/
|
||||
bool
|
||||
MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -39,18 +56,59 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Return the valid-flags mask for this transaction type.
|
||||
*
|
||||
* The framework passes this value to `preflight1`, which rejects any
|
||||
* transaction whose `sfFlags` field has bits set outside the mask. The
|
||||
* mask constant `tfMPTokenIssuanceCreateMask` is defined in `TxFlags.h` and
|
||||
* covers all flags meaningful to MPT issuance creation (e.g.
|
||||
* `tfMPTCanTransfer`, `tfMPTRequireAuth`, `tfMPTCanEscrow`, etc.).
|
||||
*
|
||||
* The `ctx` parameter is accepted for interface uniformity but unused; the
|
||||
* mask is unconditional for this transaction type.
|
||||
*/
|
||||
std::uint32_t
|
||||
MPTokenIssuanceCreate::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
// This mask is only compared against sfFlags
|
||||
return tfMPTokenIssuanceCreateMask;
|
||||
}
|
||||
|
||||
/** Validate the semantic field-level constraints of an MPT issuance creation.
|
||||
*
|
||||
* This runs after `checkExtraFeatures` and `preflight1` have passed, so
|
||||
* amendment gates and unknown flag bits are already ruled out. The checks
|
||||
* are ordered from cheapest to most likely to fail in practice:
|
||||
*
|
||||
* 1. **`sfMutableFlags`**: If present, must be non-zero (at least one mutable
|
||||
* flag declared) and must not have bits set outside
|
||||
* `tmfMPTokenIssuanceCreateMutableMask`. The condition
|
||||
* `(*mutableFlags & mask) != 0` is true when reserved bits ARE set — a
|
||||
* defensive pattern in this codebase where the mask captures allowed bits,
|
||||
* not disallowed ones. Returns `temINVALID_FLAG`.
|
||||
*
|
||||
* 2. **`sfTransferFee`**: Must not exceed `kMAX_TRANSFER_FEE` (50,000 basis
|
||||
* points = 50%). A non-zero fee without `tfMPTCanTransfer` is incoherent
|
||||
* by protocol design — the fee would never be applied — so that
|
||||
* combination returns `temMALFORMED`.
|
||||
*
|
||||
* 3. **`sfDomainID`**: An all-zeros value (`beast::kZERO`) is rejected as a
|
||||
* sentinel for "no domain". A valid domain ID mandates `tfMPTRequireAuth`
|
||||
* because a domain-scoped issuance restricts which holders are eligible;
|
||||
* without authorization the domain constraint cannot be enforced. Returns
|
||||
* `temMALFORMED` for either violation.
|
||||
*
|
||||
* 4. **`sfMPTokenMetadata`**: Must be non-empty and at most
|
||||
* `kMAX_MP_TOKEN_METADATA_LENGTH` (1,024) bytes. Returns `temMALFORMED`.
|
||||
*
|
||||
* 5. **`sfMaximumAmount`**: Must be positive and ≤ `kMAX_MP_TOKEN_AMOUNT`
|
||||
* (0x7FFF_FFFF_FFFF_FFFF). The ceiling ensures amounts fit within the
|
||||
* XRPL `Number` type's representable range. Returns `temMALFORMED`.
|
||||
*
|
||||
* @return `tesSUCCESS` if all checks pass; a `tem*` error code otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
// If the mutable flags field is included, at least one flag must be
|
||||
// specified.
|
||||
if (auto const mutableFlags = ctx.tx[~sfMutableFlags]; mutableFlags &&
|
||||
((*mutableFlags == 0u) || ((*mutableFlags & tmfMPTokenIssuanceCreateMutableMask) != 0u)))
|
||||
return temINVALID_FLAG;
|
||||
@@ -82,7 +140,6 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// Check if maximumAmount is within unsigned 63 bit range
|
||||
if (auto const maxAmt = ctx.tx[~sfMaximumAmount])
|
||||
{
|
||||
if (maxAmt == 0)
|
||||
@@ -94,6 +151,56 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Create a new `MPTokenIssuance` SLE and insert it into the ledger.
|
||||
*
|
||||
* This is the single authoritative path for all MPT issuance creation.
|
||||
* `doApply()` calls it directly; `VaultCreate` calls it to mint the
|
||||
* vault's share token from a pseudo-account. Keeping the logic here rather
|
||||
* than inline in `doApply()` allows other transactors to reuse it without a
|
||||
* full `ApplyContext`.
|
||||
*
|
||||
* **Execution sequence:**
|
||||
*
|
||||
* 1. Peek the issuer's `AccountRoot` SLE (write access required for the
|
||||
* subsequent `adjustOwnerCount`). A missing account returns
|
||||
* `tecINTERNAL`; this branch is excluded from coverage because a valid
|
||||
* transaction reaching apply phase always has its account in the ledger.
|
||||
*
|
||||
* 2. If `args.priorBalance` is provided, verify the issuer can cover the
|
||||
* reserve for `ownerCount + 1` new objects. Callers that manage the
|
||||
* reserve externally — most notably `VaultCreate`, which operates on a
|
||||
* freshly-created pseudo-account — pass `std::nullopt` to skip this gate.
|
||||
*
|
||||
* 3. Compute the deterministic `MPTID` via `makeMptID(args.sequence,
|
||||
* args.account)`. The 192-bit identifier is derived from the issuer's
|
||||
* `AccountID` and the transaction sequence number; monotonically
|
||||
* increasing sequences guarantee uniqueness under normal ledger operation.
|
||||
*
|
||||
* 4. Insert the issuance into the issuer's owner directory via
|
||||
* `view.dirInsert()`. A full directory returns `tecDIR_FULL`; also
|
||||
* excluded from coverage as a theoretical-only edge case.
|
||||
*
|
||||
* 5. Construct the `MPTokenIssuance` SLE. Mandatory fields written
|
||||
* unconditionally: `sfFlags` (with universal bits stripped via
|
||||
* `~tfUniversal`), `sfIssuer`, `sfOutstandingAmount` (initialized to 0),
|
||||
* `sfOwnerNode`, and `sfSequence`. All optional fields
|
||||
* (`sfMaximumAmount`, `sfAssetScale`, `sfTransferFee`,
|
||||
* `sfMPTokenMetadata`, `sfDomainID`, `sfMutableFlags`) are written only
|
||||
* when present in `args`, keeping the SLE sparse. `sfOutstandingAmount`
|
||||
* starting at 0 is the exact condition that `MPTokenIssuanceDestroy`
|
||||
* checks — the ledger enforces that an issuance with circulating supply
|
||||
* cannot be destroyed.
|
||||
*
|
||||
* 6. Increment `sfOwnerCount` via `adjustOwnerCount(+1)`. This raises the
|
||||
* issuer's reserve threshold and protects the issuance from
|
||||
* garbage-collection until it is explicitly destroyed.
|
||||
*
|
||||
* @param view Mutable ledger view to write into.
|
||||
* @param journal Logging sink.
|
||||
* @param args Aggregate of all creation parameters; see `MPTCreateArgs`.
|
||||
* @return On success, the newly minted `MPTID`. On failure, an `Unexpected`
|
||||
* wrapping `tecINTERNAL`, `tecINSUFFICIENT_RESERVE`, or `tecDIR_FULL`.
|
||||
*/
|
||||
Expected<MPTID, TER>
|
||||
MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args)
|
||||
{
|
||||
@@ -108,7 +215,6 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate
|
||||
auto const mptId = makeMptID(args.sequence, args.account);
|
||||
auto const mptIssuanceKeylet = keylet::mptIssuance(mptId);
|
||||
|
||||
// create the MPTokenIssuance
|
||||
{
|
||||
auto const ownerNode = view.dirInsert(
|
||||
keylet::ownerDir(args.account), mptIssuanceKeylet, describeOwnerDir(args.account));
|
||||
@@ -144,12 +250,27 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate
|
||||
view.insert(mptIssuance);
|
||||
}
|
||||
|
||||
// Update owner count.
|
||||
adjustOwnerCount(view, acct, 1, journal);
|
||||
|
||||
return mptId;
|
||||
}
|
||||
|
||||
/** Apply the transaction by packaging its fields into `MPTCreateArgs` and
|
||||
* delegating all ledger mutation to `create()`.
|
||||
*
|
||||
* `preFeeBalance_` is passed as `priorBalance` so the reserve check inside
|
||||
* `create()` uses the pre-fee snapshot — consistent with the codebase
|
||||
* convention that reserve adequacy is measured before the fee is deducted
|
||||
* (see "Reserve Check Convention" in the transactors skill).
|
||||
*
|
||||
* All optional transaction fields are forwarded unchanged; absent fields
|
||||
* become `std::nullopt` in `MPTCreateArgs` and are omitted from the SLE,
|
||||
* keeping the ledger entry sparse.
|
||||
*
|
||||
* @return `tesSUCCESS` if `create()` succeeds; otherwise the `TER` embedded
|
||||
* in the `Unexpected` result (one of `tecINTERNAL`,
|
||||
* `tecINSUFFICIENT_RESERVE`, or `tecDIR_FULL`).
|
||||
*/
|
||||
TER
|
||||
MPTokenIssuanceCreate::doApply()
|
||||
{
|
||||
@@ -172,6 +293,15 @@ MPTokenIssuanceCreate::doApply()
|
||||
return result ? tesSUCCESS : result.error();
|
||||
}
|
||||
|
||||
/** No-op stub satisfying the `Transactor` invariant-entry interface.
|
||||
*
|
||||
* No transaction-specific invariants are defined for
|
||||
* `MPTokenIssuanceCreate` yet. The global `ValidMPTIssuance` and
|
||||
* `ValidMPTPayment` checkers in `InvariantCheck.cpp` cover the structural
|
||||
* properties of the newly inserted SLE. This method exists as an extension
|
||||
* point for future per-transaction checks (e.g., verifying that exactly one
|
||||
* new `ltMPTOKEN_ISSUANCE` entry appears in the diff).
|
||||
*/
|
||||
void
|
||||
MPTokenIssuanceCreate::visitInvariantEntry(
|
||||
bool,
|
||||
@@ -181,6 +311,14 @@ MPTokenIssuanceCreate::visitInvariantEntry(
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** No-op stub satisfying the `Transactor` invariant-finalize interface.
|
||||
*
|
||||
* Always returns `true`. No transaction-specific invariants are defined for
|
||||
* `MPTokenIssuanceCreate` yet; the global invariant framework handles all
|
||||
* structural checks. Reserved as an extension point for future checks.
|
||||
*
|
||||
* @return `true` unconditionally.
|
||||
*/
|
||||
bool
|
||||
MPTokenIssuanceCreate::finalizeInvariants(
|
||||
STTx const&,
|
||||
|
||||
@@ -13,25 +13,59 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Trivial preflight: unconditionally succeeds.
|
||||
*
|
||||
* A destroy transaction carries only `sfMPTokenIssuanceID` — no flags,
|
||||
* amounts, or complex parameters that can be validated without ledger
|
||||
* state. The framework's `preflight0`/`preflight1` layers have already
|
||||
* checked fee, sequence, and signature format by the time this runs.
|
||||
* All substantive validation is deferred to `preclaim`, where a read-only
|
||||
* ledger view is available.
|
||||
*
|
||||
* @param ctx Preflight context (unused beyond framework consumption).
|
||||
* @return `tesSUCCESS` always.
|
||||
*/
|
||||
NotTEC
|
||||
MPTokenIssuanceDestroy::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Validate destruction preconditions against a read-only ledger view.
|
||||
*
|
||||
* Performs three ordered checks against the `MPTokenIssuance` SLE:
|
||||
*
|
||||
* 1. **Existence** — looks up the SLE via `keylet::mptIssuance(sfMPTokenIssuanceID)`.
|
||||
* An absent entry returns `tecOBJECT_NOT_FOUND`; the keylet lookup also
|
||||
* implicitly validates the ID's format.
|
||||
*
|
||||
* 2. **Ownership** — `sfIssuer` on the SLE must match the submitting account.
|
||||
* No other account may destroy an issuance, regardless of administrative
|
||||
* authority. Returns `tecNO_PERMISSION` on mismatch.
|
||||
*
|
||||
* 3. **Zero supply** — both `sfOutstandingAmount` (circulating tokens) and the
|
||||
* optional `sfLockedAmount` (tokens held in escrow or protocol positions)
|
||||
* must be exactly zero, checked separately because they represent distinct
|
||||
* accounting concepts. Either non-zero returns `tecHAS_OBLIGATIONS`.
|
||||
* Destroying while holders exist would orphan their `MPToken` SLEs,
|
||||
* corrupting the ledger. The `sfLockedAmount` branch is `LCOV_EXCL_LINE`
|
||||
* because reaching it with zero `sfOutstandingAmount` would indicate
|
||||
* already-corrupted ledger state not reachable through normal flow.
|
||||
*
|
||||
* @param ctx Preclaim context providing the read-only ledger view.
|
||||
* @return `tesSUCCESS` if all guards pass; `tecOBJECT_NOT_FOUND`,
|
||||
* `tecNO_PERMISSION`, or `tecHAS_OBLIGATIONS` otherwise.
|
||||
*/
|
||||
TER
|
||||
MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// ensure that issuance exists
|
||||
auto const sleMPT = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
if (!sleMPT)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// ensure it is issued by the tx submitter
|
||||
if ((*sleMPT)[sfIssuer] != ctx.tx[sfAccount])
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// ensure it has no outstanding balances
|
||||
if ((*sleMPT)[sfOutstandingAmount] != 0)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
@@ -41,6 +75,45 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Apply the destruction: remove the issuance SLE and unwind its accounting.
|
||||
*
|
||||
* Executes the symmetric inverse of `MPTokenIssuanceCreate::create()` in
|
||||
* four steps:
|
||||
*
|
||||
* 1. **Defensive re-verification** — re-checks that `account_` equals the
|
||||
* SLE's `sfIssuer`. This guard is redundant with `preclaim` by design:
|
||||
* `doApply` runs against a mutable view that is logically distinct from
|
||||
* the read-only snapshot used by `preclaim`, and the framework could, in
|
||||
* theory, present a different ledger state here. `tecINTERNAL` on
|
||||
* mismatch signals a framework-level bug, not user error; the branch is
|
||||
* `LCOV_EXCL_LINE` because it cannot be reached through correct
|
||||
* transaction processing.
|
||||
*
|
||||
* 2. **Owner-directory removal** — calls `view().dirRemove(keylet::ownerDir(account_),
|
||||
* (*mpt)[sfOwnerNode], mpt->key(), false)`. The `sfOwnerNode` back-pointer
|
||||
* stored in the SLE at creation time gives the exact directory page, making
|
||||
* this an O(1) lookup rather than a linear scan. The `false` argument
|
||||
* preserves the owner directory itself even if it becomes empty. Failure
|
||||
* means the directory entry was never recorded or has already been removed —
|
||||
* ledger corruption — so `tefBAD_LEDGER` is returned (also `LCOV_EXCL_LINE`).
|
||||
*
|
||||
* 3. **SLE erasure** — `view().erase(mpt)` removes the `MPTokenIssuance`
|
||||
* object from the ledger. This is safe only after the directory entry is
|
||||
* gone; erasing first would lose the `sfOwnerNode` page index needed for
|
||||
* step 2.
|
||||
*
|
||||
* 4. **Owner-count adjustment** — `adjustOwnerCount(..., -1, j_)` decrements
|
||||
* the issuer's reserve-tracked owner count, freeing the one reserve slot
|
||||
* that `MPTokenIssuanceCreate` claimed.
|
||||
*
|
||||
* All four mutations are applied to the `ApplyContext` view and are committed
|
||||
* atomically if `tesSUCCESS` is returned, or discarded entirely on any non-`tes`
|
||||
* result per the standard transactor rollback contract.
|
||||
*
|
||||
* @return `tesSUCCESS` on success; `tecINTERNAL` if the issuer mismatch guard
|
||||
* fires (framework bug); `tefBAD_LEDGER` if the owner-directory entry is
|
||||
* missing (ledger corruption).
|
||||
*/
|
||||
TER
|
||||
MPTokenIssuanceDestroy::doApply()
|
||||
{
|
||||
@@ -58,6 +131,15 @@ MPTokenIssuanceDestroy::doApply()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** No-op stub satisfying the invariant-visitor interface.
|
||||
*
|
||||
* No transaction-specific per-entry invariants are defined for
|
||||
* `MPTokenIssuanceDestroy` yet. Global invariant checkers (e.g.,
|
||||
* `ValidMPTIssuance`, `NoZeroEscrow`) cover the relevant ledger
|
||||
* state transitions without needing a per-transactor hook here.
|
||||
* This method is the designated extension point for any future
|
||||
* per-transaction invariants.
|
||||
*/
|
||||
void
|
||||
MPTokenIssuanceDestroy::visitInvariantEntry(
|
||||
bool,
|
||||
@@ -67,6 +149,16 @@ MPTokenIssuanceDestroy::visitInvariantEntry(
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
/** No-op stub satisfying the invariant-finalizer interface.
|
||||
*
|
||||
* Always returns `true` because no transaction-specific finalization
|
||||
* invariants exist for `MPTokenIssuanceDestroy`. The global invariant
|
||||
* framework (e.g., `ValidMPTIssuance`, `XRPNotCreated`) handles all
|
||||
* post-apply consistency checks for this transaction type. Reserved
|
||||
* for future per-transaction invariants.
|
||||
*
|
||||
* @return `true` always — no invariants to fail.
|
||||
*/
|
||||
bool
|
||||
MPTokenIssuanceDestroy::finalizeInvariants(
|
||||
STTx const&,
|
||||
|
||||
@@ -42,16 +42,32 @@ MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx)
|
||||
return tfMPTokenIssuanceSetMask;
|
||||
}
|
||||
|
||||
// Maps set/clear mutable flags in an MPTokenIssuanceSet transaction to the
|
||||
// corresponding ledger mutable flags that control whether the change is
|
||||
// allowed.
|
||||
/** Mapping between the set/clear bits in a transaction's `sfMutableFlags`
|
||||
* field and the corresponding `lsmfMPTCanMutate*` gate stored on the
|
||||
* `MPTokenIssuance` SLE.
|
||||
*
|
||||
* The three fields implement a two-level flag design: `setFlag` and
|
||||
* `clearFlag` are the transaction-level request bits; `canMutateFlag` is
|
||||
* the issuance-level permission bit that must already be set before the
|
||||
* corresponding change is allowed. The same struct drives three phases:
|
||||
* `preflight` (conflict detection), `preclaim` (permission gating), and
|
||||
* `doApply` (flag mutation) — ensuring all three always reason about the
|
||||
* same six properties with no possibility of a phase mismatch.
|
||||
*/
|
||||
struct MPTMutabilityFlags
|
||||
{
|
||||
std::uint32_t setFlag;
|
||||
std::uint32_t clearFlag;
|
||||
std::uint32_t canMutateFlag;
|
||||
std::uint32_t setFlag; /**< Transaction bit requesting the feature be enabled. */
|
||||
std::uint32_t clearFlag; /**< Transaction bit requesting the feature be disabled. */
|
||||
std::uint32_t canMutateFlag; /**< Issuance SLE bit that gates whether this change is permitted. */
|
||||
};
|
||||
|
||||
/** Table-driven mapping of the six mutable properties of an MPTokenIssuance.
|
||||
*
|
||||
* Each entry covers one logical capability (lock, require-auth, escrow,
|
||||
* trade, transfer, clawback). Using a compile-time array rather than
|
||||
* ad-hoc flag checks ensures that `preflight`, `preclaim`, and `doApply`
|
||||
* always agree on which properties exist and how they map to ledger bits.
|
||||
*/
|
||||
static constexpr std::array<MPTMutabilityFlags, 6> kMPT_MUTABILITY_FLAGS = {
|
||||
{{.setFlag = tmfMPTSetCanLock,
|
||||
.clearFlag = tmfMPTClearCanLock,
|
||||
@@ -72,6 +88,31 @@ static constexpr std::array<MPTMutabilityFlags, 6> kMPT_MUTABILITY_FLAGS = {
|
||||
.clearFlag = tmfMPTClearCanClawback,
|
||||
.canMutateFlag = lsmfMPTCanMutateCanClawback}}};
|
||||
|
||||
/** Stateless validation for `MPTokenIssuanceSet` transactions.
|
||||
*
|
||||
* Operates in two modes depending on which fields are present:
|
||||
*
|
||||
* **Lock/unlock mode** (no `sfMutableFlags`, `sfMPTokenMetadata`, or
|
||||
* `sfTransferFee`): validates flag exclusivity (`tfMPTLock` and
|
||||
* `tfMPTUnlock` cannot both be set) and that the submitter is not also
|
||||
* the named holder. The no-op check under `featureSingleAssetVault` or
|
||||
* `featureDynamicMPT` rejects a transaction that changes nothing at all
|
||||
* (zero flags, no domain, no mutation fields), preventing fee-burning
|
||||
* submissions.
|
||||
*
|
||||
* **Mutation mode** (any of `sfMutableFlags`, `sfMPTokenMetadata`,
|
||||
* `sfTransferFee` present): requires `featureDynamicMPT`; `sfHolder` and
|
||||
* non-universal tx flags are both forbidden because mutation targets the
|
||||
* issuance object rather than any individual holder slot. The check for a
|
||||
* non-zero `sfTransferFee` alongside `tmfMPTClearCanTransfer` in
|
||||
* `sfMutableFlags` catches a single-transaction contradiction (setting a
|
||||
* fee while simultaneously removing transfer capability) before it reaches
|
||||
* the ledger.
|
||||
*
|
||||
* The mutual exclusion of `sfDomainID` and `sfHolder` is validated here
|
||||
* because domain assignment operates on the issuance object while holder
|
||||
* operations target an individual `MPToken` slot — the two cannot coexist.
|
||||
*/
|
||||
NotTEC
|
||||
MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -88,7 +129,6 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
|
||||
auto const txFlags = ctx.tx.getFlags();
|
||||
|
||||
// fails if both flags are set
|
||||
if (((txFlags & tfMPTLock) != 0u) && ((txFlags & tfMPTUnlock) != 0u))
|
||||
return temINVALID_FLAG;
|
||||
|
||||
@@ -99,18 +139,15 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
|
||||
if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT))
|
||||
{
|
||||
// Is this transaction actually changing anything ?
|
||||
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.rules.enabled(featureDynamicMPT))
|
||||
{
|
||||
// Holder field is not allowed when mutating MPTokenIssuance
|
||||
if (isMutate && holderID)
|
||||
return temMALFORMED;
|
||||
|
||||
// Can not set flags when mutating MPTokenIssuance
|
||||
if (isMutate && ((txFlags & tfUniversalMask) != 0u))
|
||||
return temMALFORMED;
|
||||
|
||||
@@ -125,14 +162,11 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if ((*mutableFlags == 0u) || ((*mutableFlags & tmfMPTokenIssuanceSetMutableMask) != 0u))
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// Can not set and clear the same flag
|
||||
if (std::ranges::any_of(kMPT_MUTABILITY_FLAGS, [mutableFlags](auto const& f) {
|
||||
return (*mutableFlags & f.setFlag) && (*mutableFlags & f.clearFlag);
|
||||
}))
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// Trying to set a non-zero TransferFee and clear MPTCanTransfer
|
||||
// in the same transaction is not allowed.
|
||||
if ((transferFee.value_or(0) != 0u) && ((*mutableFlags & tmfMPTClearCanTransfer) != 0u))
|
||||
return temMALFORMED;
|
||||
}
|
||||
@@ -141,6 +175,23 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Two-tier delegate authorization check for lock/unlock operations.
|
||||
*
|
||||
* If `sfDelegate` is absent the issuer signed directly and is
|
||||
* unconditionally permitted (early return `tesSUCCESS`).
|
||||
*
|
||||
* When a delegate is present, authorization is evaluated in order:
|
||||
* 1. A broad transaction-type permission via `checkTxPermission()` — if
|
||||
* the delegate holds blanket `ttMPTOKEN_ISSUANCE_SET` authority, no
|
||||
* further checks are needed.
|
||||
* 2. A flag-level guard is applied first (currently unreachable because no
|
||||
* transaction-level flags beyond lock/unlock exist, but retained as
|
||||
* forward-compatibility infrastructure — see `LCOV_EXCL_LINE`).
|
||||
* 3. Granular fall-back: `MPTokenIssuanceLock` is required for `tfMPTLock`
|
||||
* and `MPTokenIssuanceUnlock` is required for `tfMPTUnlock`. The
|
||||
* `loadGranularPermission()` helper populates the set from the delegate
|
||||
* SLE's permission list.
|
||||
*/
|
||||
NotTEC
|
||||
MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
@@ -176,17 +227,55 @@ MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Read-only ledger-state validation for `MPTokenIssuanceSet`.
|
||||
*
|
||||
* Checks are ordered from cheap/general to expensive/specific:
|
||||
*
|
||||
* 1. **Issuance existence and ownership**: the `MPTokenIssuance` SLE must
|
||||
* exist and its `sfIssuer` must match the submitting account.
|
||||
*
|
||||
* 2. **`lsfMPTCanLock` gate** (asymmetric): under the original rules the
|
||||
* flag must be set for _any_ transaction. Under `featureSingleAssetVault`
|
||||
* or `featureDynamicMPT`, the flag is only required when the transaction
|
||||
* actually requests a lock or unlock — mutation-only transactions on
|
||||
* issuances that lack locking capability are still permitted. Two
|
||||
* separate `if` blocks are used rather than `||` to preserve this
|
||||
* readable asymmetric logic.
|
||||
*
|
||||
* 3. **Holder checks** (when `sfHolder` present): the holder account must
|
||||
* exist and the holder's `MPToken` slot for this issuance must exist.
|
||||
*
|
||||
* 4. **Domain checks** (when `sfDomainID` present): the issuance must have
|
||||
* `lsfMPTRequireAuth` set (binding a domain to an unauthorized issuance
|
||||
* is meaningless); a non-zero domain ID must reference an existing
|
||||
* `PermissionedDomain` object.
|
||||
*
|
||||
* 5. **Mutation permission checks** (`featureDynamicMPT`): each flag the
|
||||
* transaction sets or clears via `sfMutableFlags` must have the
|
||||
* corresponding `lsmfMPTCanMutate*` bit already set on the issuance SLE
|
||||
* (table-driven via `kMPT_MUTABILITY_FLAGS`). Clearing
|
||||
* `lsfMPTRequireAuth` while a `DomainID` is set on the issuance is
|
||||
* blocked here because it would produce an internally inconsistent
|
||||
* ledger state. `sfMPTokenMetadata` and `sfTransferFee` mutations
|
||||
* require `lsmfMPTCanMutateMetadata` and `lsmfMPTCanMutateTransferFee`
|
||||
* respectively.
|
||||
*
|
||||
* 6. **Transfer fee pre-existence requirement**: a non-zero `sfTransferFee`
|
||||
* requires `lsfMPTCanTransfer` to already be set on the _current_ ledger
|
||||
* object. Enabling `tmfMPTSetCanTransfer` in the same transaction does
|
||||
* not satisfy this requirement — `preclaim` is the first phase where the
|
||||
* current ledger value of the flag is visible, so this check belongs here
|
||||
* rather than in `preflight`.
|
||||
*/
|
||||
TER
|
||||
MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// ensure that issuance exists
|
||||
auto const sleMptIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
if (!sleMptIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleMptIssuance->isFlag(lsfMPTCanLock))
|
||||
{
|
||||
// For readability two separate `if` rather than `||` of two conditions
|
||||
if (!ctx.view.rules().enabled(featureSingleAssetVault) &&
|
||||
!ctx.view.rules().enabled(featureDynamicMPT))
|
||||
{
|
||||
@@ -198,17 +287,14 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure it is issued by the tx submitter
|
||||
if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount])
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (auto const holderID = ctx.tx[~sfHolder])
|
||||
{
|
||||
// make sure holder account exists
|
||||
if (!ctx.view.exists(keylet::account(*holderID)))
|
||||
return tecNO_DST;
|
||||
|
||||
// the mptoken must exist
|
||||
if (!ctx.view.exists(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID)))
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
}
|
||||
@@ -226,8 +312,7 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// sfMutableFlags is soeDEFAULT, defaulting to 0 if not specified on
|
||||
// the ledger.
|
||||
// sfMutableFlags defaults to 0 when absent (soeDEFAULT field).
|
||||
auto const currentMutableFlags = sleMptIssuance->getFieldU32(sfMutableFlags);
|
||||
|
||||
auto isMutableFlag = [&](std::uint32_t mutableFlag) -> bool {
|
||||
@@ -255,10 +340,6 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (auto const fee = ctx.tx[~sfTransferFee])
|
||||
{
|
||||
// A non-zero TransferFee is only valid if the lsfMPTCanTransfer flag
|
||||
// was previously enabled (at issuance or via a prior mutation). Setting
|
||||
// it by tmfMPTSetCanTransfer in the current transaction does not meet
|
||||
// this requirement.
|
||||
if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
@@ -269,6 +350,40 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Apply the mutations described by the transaction to the ledger.
|
||||
*
|
||||
* **SLE selection**: if `sfHolder` is present the holder's `MPToken` SLE is
|
||||
* used; otherwise the `MPTokenIssuance` SLE is used. This single branch
|
||||
* lets both issuance-wide and per-holder locking share the same lock/unlock
|
||||
* code path. A missing SLE at this point is a ledger-corruption sentinel
|
||||
* (`tecINTERNAL`) since `preclaim` already verified existence.
|
||||
*
|
||||
* **Lock/unlock**: `lsfMPTLocked` is set or cleared directly on whichever
|
||||
* SLE was selected. The two flags are mutually exclusive (enforced in
|
||||
* `preflight`), so a simple if/else-if suffices.
|
||||
*
|
||||
* **Mutable-flag iteration**: `kMPT_MUTABILITY_FLAGS` is iterated to
|
||||
* translate each `tmfMPTSet*`/`tmfMPTClear*` bit in `sfMutableFlags` into
|
||||
* the corresponding `lsmfMPTCanMutate*` bit change on the issuance `sfFlags`.
|
||||
* Clearing `tmfMPTClearCanTransfer` atomically removes `sfTransferFee` from
|
||||
* the same SLE — you cannot have a fee-bearing issuance with transfer
|
||||
* capability disabled, and removing the fee in the same write keeps the
|
||||
* ledger internally consistent.
|
||||
*
|
||||
* **`soeDEFAULT` semantics for `sfTransferFee`**: a zero fee value removes
|
||||
* the field entirely rather than storing zero, matching the convention that
|
||||
* an absent field is interpreted as zero by readers.
|
||||
*
|
||||
* **`soeDEFAULT` semantics for `sfMPTokenMetadata`**: an empty blob removes
|
||||
* the field entirely.
|
||||
*
|
||||
* **`beast::kZERO` sentinel for `sfDomainID`**: the zero 256-bit value
|
||||
* signals "remove the existing domain link"; any other value sets or
|
||||
* replaces the domain. This mirrors the sentinel-clear pattern used for
|
||||
* similar optional-reference fields elsewhere in the protocol.
|
||||
*
|
||||
* All changes are written in a single `view().update(sle)` at the end.
|
||||
*/
|
||||
TER
|
||||
MPTokenIssuanceSet::doApply()
|
||||
{
|
||||
@@ -317,11 +432,7 @@ MPTokenIssuanceSet::doApply()
|
||||
}
|
||||
|
||||
if ((mutableFlags & tmfMPTClearCanTransfer) != 0u)
|
||||
{
|
||||
// If the lsfMPTCanTransfer flag is being cleared, then also clear
|
||||
// the TransferFee field.
|
||||
sle->makeFieldAbsent(sfTransferFee);
|
||||
}
|
||||
}
|
||||
|
||||
if (flagsIn != flagsOut)
|
||||
@@ -329,10 +440,6 @@ MPTokenIssuanceSet::doApply()
|
||||
|
||||
if (auto const transferFee = ctx_.tx[~sfTransferFee])
|
||||
{
|
||||
// TransferFee uses soeDEFAULT style:
|
||||
// - If the field is absent, it is interpreted as 0.
|
||||
// - If the field is present, it must be non-zero.
|
||||
// Therefore, when TransferFee is 0, the field should be removed.
|
||||
if (transferFee == 0)
|
||||
{
|
||||
sle->makeFieldAbsent(sfTransferFee);
|
||||
@@ -357,7 +464,6 @@ MPTokenIssuanceSet::doApply()
|
||||
|
||||
if (domainID)
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
@@ -32,6 +32,37 @@
|
||||
|
||||
namespace {
|
||||
|
||||
/** Apply normal-freeze and deep-freeze flag changes to a trust-line flag word.
|
||||
*
|
||||
* This helper is the single authoritative place that translates the four
|
||||
* transaction flags (`tfSetFreeze`, `tfClearFreeze`, `tfSetDeepFreeze`,
|
||||
* `tfClearDeepFreeze`) into mutations of the per-side `lsfLow*`/`lsfHigh*`
|
||||
* flag bits stored in the `RippleState` SLE. It is called identically from
|
||||
* both `preclaim` (to simulate the post-transaction flag state for invariant
|
||||
* validation) and `doApply` (to compute the value actually written to the
|
||||
* ledger). Sharing a single implementation guarantees the two phases always
|
||||
* agree on the resulting flag state, preventing silent divergence bugs.
|
||||
*
|
||||
* Semantics:
|
||||
* - Set-freeze wins over clear-freeze when both are present (the pair is
|
||||
* treated as a no-op). `bNoFreeze` additionally suppresses setting.
|
||||
* - Deep-freeze follows the same mutual-exclusion rule independently of
|
||||
* normal freeze, but `preclaim` enforces the invariant that deep-freeze
|
||||
* requires normal freeze to be set.
|
||||
*
|
||||
* @param uFlags Current flag word from the `RippleState` SLE (or
|
||||
* zero when the line does not yet exist).
|
||||
* @param bHigh `true` if the transacting account is the high side
|
||||
* of the trust line (account ID > counterparty ID).
|
||||
* Selects `lsfHigh*` vs `lsfLow*` flag constants.
|
||||
* @param bNoFreeze `true` if the transacting account has `lsfNoFreeze`
|
||||
* set, permanently waiving freeze authority.
|
||||
* @param bSetFreeze `true` if `tfSetFreeze` is present in the tx flags.
|
||||
* @param bClearFreeze `true` if `tfClearFreeze` is present in the tx flags.
|
||||
* @param bSetDeepFreeze `true` if `tfSetDeepFreeze` is present in the tx flags.
|
||||
* @param bClearDeepFreeze `true` if `tfClearDeepFreeze` is present in the tx flags.
|
||||
* @return Updated flag word with freeze bits adjusted.
|
||||
*/
|
||||
uint32_t
|
||||
computeFreezeFlags(
|
||||
uint32_t uFlags,
|
||||
@@ -144,9 +175,6 @@ TrustSet::checkPermission(ReadView const& view, STTx const& tx)
|
||||
|
||||
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) != 0u)
|
||||
return terNO_DELEGATE_PERMISSION;
|
||||
|
||||
@@ -188,6 +216,40 @@ TrustSet::checkPermission(ReadView const& view, STTx const& tx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Read-only ledger checks for `TrustSet`.
|
||||
*
|
||||
* Implementation notes for each check (in execution order):
|
||||
*
|
||||
* **`tfSetfAuth` guard**: the flag is meaningful only when the issuer account
|
||||
* has `lsfRequireAuth` enabled; attempting it otherwise returns
|
||||
* `tefNO_AUTH_REQUIRED` (no fee charged).
|
||||
*
|
||||
* **`lsfDisallowIncomingTrustline` softening**: the original `featureDisallowIncoming`
|
||||
* implementation blocked ALL trust-set operations when the destination had
|
||||
* opted out, including modifications to lines that already existed. The
|
||||
* `fixDisallowIncomingV1` amendment corrects this by allowing the transaction
|
||||
* to proceed when a line already exists between the two parties — only new
|
||||
* line creation is gated by the opt-out preference.
|
||||
*
|
||||
* **Pseudo-account allow-listing**: trust lines to pseudo-accounts are
|
||||
* generally prohibited. The block is not amendment-gated because the
|
||||
* pseudo-account discriminator fields (`sfAMMID`, `sfVaultID`,
|
||||
* `sfLoanBrokerID`) are only populated when the corresponding amendment is
|
||||
* active, so the guard is implicitly gated. Three narrow exceptions apply:
|
||||
* - AMM pseudo-accounts: a new trust line is allowed only when the currency
|
||||
* matches the pool's LP token and the AMM holds non-zero liquidity
|
||||
* (`lpTokenBalance > 0`); modifying an existing line is always permitted.
|
||||
* - Vault and loan-broker pseudo-accounts: only modification of an existing
|
||||
* line passes; new line creation returns `tecNO_PERMISSION`.
|
||||
* - Any other pseudo-account type returns `tecPSEUDO_ACCOUNT`.
|
||||
*
|
||||
* **Deep-freeze invariant simulation**: when `featureDeepFreeze` is active,
|
||||
* `computeFreezeFlags()` is called with the current flag word (zero if the
|
||||
* line does not yet exist) to predict the post-transaction flag state. The
|
||||
* invariant `deepFrozen → frozen` is then checked on that simulated state.
|
||||
* Rejecting the violation here — before any writes — is cheaper and keeps
|
||||
* `doApply` free of conditional rollback logic.
|
||||
*/
|
||||
TER
|
||||
TrustSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -247,8 +309,6 @@ TrustSet::preclaim(PreclaimContext const& ctx)
|
||||
// field populated, unless the appropriate amendment was already enabled.
|
||||
if (sleDst && isPseudoAccount(sleDst))
|
||||
{
|
||||
// If destination is AMM and the trustline doesn't exist then only allow
|
||||
// TrustSet if the asset is AMM LP token and AMM is not in empty state.
|
||||
if (sleDst->isFieldPresent(sfAMMID))
|
||||
{
|
||||
if (ctx.view.exists(keylet::line(id, uDstAccountID, currency)))
|
||||
@@ -284,7 +344,6 @@ TrustSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Checking all freeze/deep freeze flag invariants.
|
||||
if (ctx.view.rules().enabled(featureDeepFreeze))
|
||||
{
|
||||
bool const bNoFreeze = sle->isFlag(lsfNoFreeze);
|
||||
@@ -307,19 +366,16 @@ TrustSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
bool const bHigh = id > uDstAccountID;
|
||||
// Fetching current state of trust line
|
||||
auto const sleRippleState = ctx.view.read(keylet::line(id, uDstAccountID, currency));
|
||||
std::uint32_t uFlags = sleRippleState ? sleRippleState->getFieldU32(sfFlags) : 0u;
|
||||
// Computing expected trust line state
|
||||
uFlags = computeFreezeFlags(
|
||||
uFlags, bHigh, bNoFreeze, bSetFreeze, bClearFreeze, bSetDeepFreeze, bClearDeepFreeze);
|
||||
|
||||
auto const frozen = uFlags & (bHigh ? lsfHighFreeze : lsfLowFreeze);
|
||||
auto const deepFrozen = uFlags & (bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
|
||||
|
||||
// Trying to set deep freeze on not already frozen trust line must
|
||||
// fail. This also checks that clearing normal freeze while deep
|
||||
// frozen must not work
|
||||
// Enforce: deepFrozen → frozen. Covers both "set deep without
|
||||
// normal freeze" and "clear normal freeze while deep-frozen".
|
||||
if ((deepFrozen != 0u) && (frozen == 0u))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
@@ -329,6 +385,52 @@ TrustSet::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Apply the `TrustSet` transaction to the mutable ledger view.
|
||||
*
|
||||
* **`bHigh` convention**: a `RippleState` SLE stores both sides of a trust
|
||||
* relationship in one shared entry. Per-side fields come in symmetric pairs
|
||||
* (`sfLowLimit`/`sfHighLimit`, `sfLowQualityIn`/`sfHighQualityIn`,
|
||||
* `lsfLowFreeze`/`lsfHighFreeze`, etc.). `bHigh = (account_ > uDstAccountID)`
|
||||
* selects the correct field of each pair throughout the function without
|
||||
* duplicating logic. Two concurrent `TrustSet` transactions by each party
|
||||
* both write to the same SLE but modify disjoint fields.
|
||||
*
|
||||
* **Reserve exemption for onboarding**: the incremental reserve for creating a
|
||||
* new trust line is waived entirely when the submitter owns fewer than two
|
||||
* objects (`uOwnerCount < 2`). Without this exemption a gateway funding a
|
||||
* brand-new user would need to deposit not just the base account reserve but
|
||||
* also the trust-line reserve — surplus XRP the user could pocket without ever
|
||||
* using the gateway. The exemption caps the minimum viable onboarding cost at
|
||||
* the account reserve alone.
|
||||
*
|
||||
* **Quality normalization**: a quality value of exactly `QUALITY_ONE`
|
||||
* (1 000 000 000) is the canonical "no adjustment" value and is stored as zero
|
||||
* (field made absent via `makeFieldAbsent`). `uQualityOut` is normalized to
|
||||
* zero immediately after reading from the transaction; `uQualityIn` and the
|
||||
* values read back from the existing line are normalized before the reserve
|
||||
* decision is made. This prevents callers from writing an explicit
|
||||
* `QUALITY_ONE` and wasting 4 bytes of ledger storage per side.
|
||||
*
|
||||
* **Reserve recomputation**: after updating all fields, the need for a reserve
|
||||
* on each side is derived from scratch by testing whether any per-side state
|
||||
* deviates from defaults (non-zero quality, non-zero limit, freeze flag, positive
|
||||
* balance, or a `noRipple` preference that disagrees with the account's
|
||||
* `lsfDefaultRipple`). If the computed need differs from `lsfLowReserve`/
|
||||
* `lsfHighReserve`, `adjustOwnerCount` is called with ±1 to keep the owner
|
||||
* count accurate.
|
||||
*
|
||||
* **Auto-deletion (`bDefault`)**: when both sides reach fully default state
|
||||
* (`bLowReserveClear && bHighReserveClear`) or the currency is the
|
||||
* `badCurrency()` sentinel, `trustDelete` removes the `RippleState` SLE from
|
||||
* the ledger and both accounts' owner directories, preventing stale
|
||||
* zero-balance objects from accumulating.
|
||||
*
|
||||
* **Delegation to `RippleStateHelpers`**: `trustCreate` and `trustDelete`
|
||||
* handle the low-level SLE construction, directory insertion/removal, and
|
||||
* initial `adjustOwnerCount` for the creating account. This keeps mutation
|
||||
* logic centralised and reusable by other transactors that implicitly create
|
||||
* trust lines (e.g., `issueIOU`).
|
||||
*/
|
||||
TER
|
||||
TrustSet::doApply()
|
||||
{
|
||||
@@ -341,7 +443,6 @@ TrustSet::doApply()
|
||||
Currency const currency(saLimitAmount.get<Issue>().currency);
|
||||
AccountID const uDstAccountID(saLimitAmount.getIssuer());
|
||||
|
||||
// true, if current is high account.
|
||||
bool const bHigh = account_ > uDstAccountID;
|
||||
|
||||
auto const sle = view().peek(keylet::account(account_));
|
||||
@@ -350,24 +451,6 @@ TrustSet::doApply()
|
||||
|
||||
std::uint32_t const uOwnerCount = sle->getFieldU32(sfOwnerCount);
|
||||
|
||||
// The reserve that is required to create the line. Note
|
||||
// that although the reserve increases with every item
|
||||
// an account owns, in the case of trust lines we only
|
||||
// *enforce* a reserve if the user owns more than two
|
||||
// items.
|
||||
//
|
||||
// We do this because being able to exchange currencies,
|
||||
// which needs trust lines, is a powerful XRPL feature.
|
||||
// So we want to make it easy for a gateway to fund the
|
||||
// accounts of its users without fear of being tricked.
|
||||
//
|
||||
// Without this logic, a gateway that wanted to have a
|
||||
// new user use its services, would have to give that
|
||||
// user enough XRP to cover not only the account reserve
|
||||
// but the incremental reserve for the trust line as
|
||||
// well. A person with no intention of using the gateway
|
||||
// could use the extra XRP for their own purposes.
|
||||
|
||||
XRPAmount const reserveCreate(
|
||||
(uOwnerCount < 2) ? XRPAmount(beast::kZERO)
|
||||
: view().fees().accountReserve(uOwnerCount + 1));
|
||||
@@ -441,15 +524,11 @@ TrustSet::doApply()
|
||||
|
||||
if (!bQualityIn)
|
||||
{
|
||||
// Not setting. Just get it.
|
||||
|
||||
uLowQualityIn = sleRippleState->getFieldU32(sfLowQualityIn);
|
||||
uHighQualityIn = sleRippleState->getFieldU32(sfHighQualityIn);
|
||||
}
|
||||
else if (uQualityIn != 0u)
|
||||
{
|
||||
// Setting.
|
||||
|
||||
sleRippleState->setFieldU32(!bHigh ? sfLowQualityIn : sfHighQualityIn, uQualityIn);
|
||||
|
||||
uLowQualityIn = !bHigh ? uQualityIn : sleRippleState->getFieldU32(sfLowQualityIn);
|
||||
@@ -457,8 +536,6 @@ TrustSet::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clearing.
|
||||
|
||||
sleRippleState->makeFieldAbsent(!bHigh ? sfLowQualityIn : sfHighQualityIn);
|
||||
|
||||
uLowQualityIn = !bHigh ? 0 : sleRippleState->getFieldU32(sfLowQualityIn);
|
||||
@@ -477,15 +554,11 @@ TrustSet::doApply()
|
||||
|
||||
if (!bQualityOut)
|
||||
{
|
||||
// Not setting. Just get it.
|
||||
|
||||
uLowQualityOut = sleRippleState->getFieldU32(sfLowQualityOut);
|
||||
uHighQualityOut = sleRippleState->getFieldU32(sfHighQualityOut);
|
||||
}
|
||||
else if (uQualityOut != 0u)
|
||||
{
|
||||
// Setting.
|
||||
|
||||
sleRippleState->setFieldU32(!bHigh ? sfLowQualityOut : sfHighQualityOut, uQualityOut);
|
||||
|
||||
uLowQualityOut = !bHigh ? uQualityOut : sleRippleState->getFieldU32(sfLowQualityOut);
|
||||
@@ -493,8 +566,6 @@ TrustSet::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clearing.
|
||||
|
||||
sleRippleState->makeFieldAbsent(!bHigh ? sfLowQualityOut : sfHighQualityOut);
|
||||
|
||||
uLowQualityOut = !bHigh ? 0 : sleRippleState->getFieldU32(sfLowQualityOut);
|
||||
@@ -565,7 +636,6 @@ TrustSet::doApply()
|
||||
|
||||
if (bLowReserveSet && !bLowReserved)
|
||||
{
|
||||
// Set reserve for low account.
|
||||
adjustOwnerCount(view(), sleLowAccount, 1, viewJ);
|
||||
uFlagsOut |= lsfLowReserve;
|
||||
|
||||
@@ -575,14 +645,12 @@ TrustSet::doApply()
|
||||
|
||||
if (bLowReserveClear && bLowReserved)
|
||||
{
|
||||
// Clear reserve for low account.
|
||||
adjustOwnerCount(view(), sleLowAccount, -1, viewJ);
|
||||
uFlagsOut &= ~lsfLowReserve;
|
||||
}
|
||||
|
||||
if (bHighReserveSet && !bHighReserved)
|
||||
{
|
||||
// Set reserve for high account.
|
||||
adjustOwnerCount(view(), sleHighAccount, 1, viewJ);
|
||||
uFlagsOut |= lsfHighReserve;
|
||||
|
||||
@@ -592,7 +660,6 @@ TrustSet::doApply()
|
||||
|
||||
if (bHighReserveClear && bHighReserved)
|
||||
{
|
||||
// Clear reserve for high account.
|
||||
adjustOwnerCount(view(), sleHighAccount, -1, viewJ);
|
||||
uFlagsOut &= ~lsfHighReserve;
|
||||
}
|
||||
@@ -602,8 +669,6 @@ TrustSet::doApply()
|
||||
|
||||
if (bDefault || badCurrency() == currency)
|
||||
{
|
||||
// Delete.
|
||||
|
||||
terResult = trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ);
|
||||
}
|
||||
// Reserve is not scaled by load.
|
||||
@@ -623,7 +688,6 @@ TrustSet::doApply()
|
||||
JLOG(j_.trace()) << "Modify ripple line";
|
||||
}
|
||||
}
|
||||
// Line does not exist.
|
||||
else if (
|
||||
!saLimitAmount && // Setting default limit.
|
||||
(!bQualityIn || (uQualityIn == 0u)) && // Not setting quality in or
|
||||
@@ -647,14 +711,12 @@ TrustSet::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Zero balance in currency.
|
||||
STAmount const saBalance(Issue{currency, noAccount()});
|
||||
|
||||
auto const k = keylet::line(account_, uDstAccountID, currency);
|
||||
|
||||
JLOG(j_.trace()) << "doTrustSet: Creating ripple line: " << to_string(k.key);
|
||||
|
||||
// Create a new ripple line.
|
||||
terResult = trustCreate(
|
||||
view(),
|
||||
bHigh,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
VaultClawback::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -60,6 +61,27 @@ VaultClawback::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Resolve the effective clawback target when `sfAmount` is absent from the
|
||||
* transaction.
|
||||
*
|
||||
* A zero (absent) `sfAmount` is intentionally underspecified: the correct
|
||||
* default depends on the submitter's role. When the submitter is the vault
|
||||
* owner, the implicit target is shares (`sfShareMPTID`), activating the
|
||||
* share-burn cleanup path. For any other submitter (the asset issuer), the
|
||||
* implicit target is the vault's underlying asset, activating asset-clawback
|
||||
* mode.
|
||||
*
|
||||
* Callers must guard against the dual-role ambiguity (owner == issuer) before
|
||||
* invoking this function. `preclaim` rejects that case with `tecWRONG_ASSET`,
|
||||
* so this helper is never called with an ambiguous zero-amount.
|
||||
*
|
||||
* @param vault Vault SLE from which the share MPT ID and owner are read.
|
||||
* @param maybeAmount The optional `sfAmount` field value from the transaction.
|
||||
* @param account The submitting account (used to distinguish owner from issuer).
|
||||
* @return The resolved `STAmount`: the explicit amount verbatim if present,
|
||||
* a zero-value share amount for the vault owner, or a zero-value
|
||||
* vault-asset amount for the asset issuer.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
clawbackAmount(
|
||||
std::shared_ptr<SLE const> const& vault,
|
||||
@@ -76,6 +98,33 @@ clawbackAmount(
|
||||
return STAmount{vault->at(sfAsset)};
|
||||
}
|
||||
|
||||
/** Verify authorization and resolve the operating mode against ledger state.
|
||||
*
|
||||
* Execution ordering:
|
||||
* 1. Load the vault SLE and the share MPT issuance (missing issuance is
|
||||
* treated as ledger corruption → `tefINTERNAL`).
|
||||
* 2. Guard the owner == issuer ambiguity: if `sfAmount` is absent and the
|
||||
* vault asset's issuer is also the vault owner, it is impossible to infer
|
||||
* whether the caller intends share-burn or asset-clawback, so `preclaim`
|
||||
* returns `tecWRONG_ASSET` rather than silently picking one path. The
|
||||
* caller must supply an explicit `sfAmount` to disambiguate.
|
||||
* 3. Call `clawbackAmount()` to resolve the effective amount.
|
||||
* 4. Branch on the resolved amount's asset type:
|
||||
* - **Share (`sfShareMPTID`)**: only the vault owner may burn shares;
|
||||
* the vault must have outstanding shares and zero `sfAssetsTotal` /
|
||||
* `sfAssetsAvailable`; a non-zero explicit amount must equal the
|
||||
* holder's entire balance (partial burns are rejected with
|
||||
* `tecLIMIT_EXCEEDED`).
|
||||
* - **Vault asset**: must be non-XRP; submitter must be the asset issuer
|
||||
* and must differ from the holder; clawback flags must be set
|
||||
* (`lsfMPTCanClawback` for MPT, `lsfAllowTrustLineClawback` without
|
||||
* `lsfNoFreeze` for IOU).
|
||||
* - Anything else: `tecWRONG_ASSET`.
|
||||
*
|
||||
* @param ctx Preclaim context providing read-only ledger access.
|
||||
* @return `tesSUCCESS` on authorization success, or a failure `TER`
|
||||
* documented on the header declaration.
|
||||
*/
|
||||
TER
|
||||
VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -99,7 +148,6 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
Asset const share = MPTIssue{mptIssuanceID};
|
||||
|
||||
// Ambiguous case: If Issuer is Owner they must specify the asset
|
||||
if (!maybeAmount && !vaultAsset.native() && vaultAsset.getIssuer() == vault->at(sfOwner))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: must specify amount when issuer is owner.";
|
||||
@@ -114,7 +162,6 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
// so here we just enforce checks.
|
||||
if (amount.asset() == share)
|
||||
{
|
||||
// Only the Vault Owner may clawback shares
|
||||
if (account != vault->at(sfOwner))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: only vault owner can clawback shares.";
|
||||
@@ -125,7 +172,6 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
auto const assetsAvailable = vault->at(sfAssetsAvailable);
|
||||
auto const sharesTotal = sleShareIssuance->at(sfOutstandingAmount);
|
||||
|
||||
// Owner can clawback funds when the vault has shares but no assets
|
||||
if (sharesTotal == 0 || (assetsTotal != 0 || assetsAvailable != 0))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: vault owner can clawback shares only"
|
||||
@@ -133,7 +179,6 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// If amount is non-zero, the VaultOwner must burn all shares
|
||||
if (amount != beast::kZERO)
|
||||
{
|
||||
Number const& sharesHeld = accountHolds(
|
||||
@@ -144,7 +189,6 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
AuthHandling::IgnoreAuth,
|
||||
ctx.j);
|
||||
|
||||
// The VaultOwner must burn all shares
|
||||
if (amount != sharesHeld)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: vault owner must clawback all "
|
||||
@@ -156,24 +200,20 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// The asset that is being clawed back is the vault asset
|
||||
if (amount.asset() == vaultAsset)
|
||||
{
|
||||
// XRP cannot be clawed back
|
||||
if (vaultAsset.native())
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// Only the Asset Issuer may clawback the asset
|
||||
if (account != vaultAsset.getIssuer())
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: only asset issuer can clawback asset.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// The issuer cannot clawback from itself
|
||||
if (account == holder)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: issuer cannot be the holder.";
|
||||
@@ -219,10 +259,49 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
});
|
||||
}
|
||||
|
||||
// Invalid asset
|
||||
return tecWRONG_ASSET;
|
||||
}
|
||||
|
||||
/** Compute the (assetsRecovered, sharesDestroyed) pair for asset-clawback mode.
|
||||
*
|
||||
* Algorithm overview:
|
||||
* - **Zero amount ("all")**: read the holder's full share balance via
|
||||
* `accountHolds`, then convert shares → assets once via
|
||||
* `sharesToAssetsWithdraw` to obtain the asset quantity.
|
||||
* - **Non-zero amount**: perform a double-pass — assets → shares via
|
||||
* `assetsToSharesWithdraw`, then shares → assets via
|
||||
* `sharesToAssetsWithdraw`. The round-trip is necessary because MPT shares
|
||||
* are integer-valued, so converting assets → shares truncates; converting
|
||||
* back yields the true net asset quantity recoverable for those shares.
|
||||
*
|
||||
* After either path, `assetsRecovered` is clamped to `sfAssetsAvailable`.
|
||||
* This cap exists because outstanding loans may have deployed some vault
|
||||
* assets externally, making `sfAssetsAvailable < sfAssetsTotal`. When
|
||||
* clamping is applied, shares are recomputed with `TruncateShares::Yes`
|
||||
* — deliberate truncation ensures that the re-derived asset amount from the
|
||||
* truncated share count cannot overshoot the cap. The result is then verified
|
||||
* a second time; any breach after truncation is treated as an arithmetic bug
|
||||
* (`tecINTERNAL`).
|
||||
*
|
||||
* **`fixSecurity3_1_3` branching (ledger replay compatibility)**: before this
|
||||
* amendment, a zero-amount clawback returned early without clamping to
|
||||
* `sfAssetsAvailable`, which allowed recovering more assets than the vault
|
||||
* had liquid (bypassing outstanding loans). The pre-fix code path is retained
|
||||
* verbatim under the amendment gate so that historical ledgers replay
|
||||
* identically on old rules.
|
||||
*
|
||||
* Arithmetic overflow from the `Number` subsystem (common with large `sfScale`
|
||||
* values) is caught and returned as `tecPATH_DRY`.
|
||||
*
|
||||
* @param vault Mutable vault SLE; used for exchange-rate, cap, and
|
||||
* scale reads.
|
||||
* @param sleShareIssuance Read-only share MPT issuance SLE.
|
||||
* @param holder Account whose shares are being destroyed.
|
||||
* @param clawbackAmount Resolved asset amount to recover; zero means all.
|
||||
* @return `{assetsRecovered, sharesDestroyed}` on success, or `Unexpected`:
|
||||
* `tecPATH_DRY` on arithmetic overflow, `tecINTERNAL` on asset-mismatch
|
||||
* guard or conversion helper failure.
|
||||
*/
|
||||
Expected<std::pair<STAmount, STAmount>, TER>
|
||||
VaultClawback::assetsToClawback(
|
||||
std::shared_ptr<SLE> const& vault,
|
||||
@@ -232,7 +311,6 @@ VaultClawback::assetsToClawback(
|
||||
{
|
||||
if (clawbackAmount.asset() != vault->at(sfAsset))
|
||||
{
|
||||
// preclaim should have blocked this , now it's an internal error
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "VaultClawback: asset mismatch in clawback.";
|
||||
return Unexpected(tecINTERNAL);
|
||||
@@ -292,9 +370,6 @@ VaultClawback::assetsToClawback(
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
assetsRecovered = *assetsAvailable;
|
||||
// Note, it is important to truncate the number of shares,
|
||||
// otherwise the corresponding assets might breach the
|
||||
// AssetsAvailable
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault, sleShareIssuance, assetsRecovered, TruncateShares::Yes);
|
||||
@@ -333,6 +408,46 @@ VaultClawback::assetsToClawback(
|
||||
return std::make_pair(assetsRecovered, sharesDestroyed);
|
||||
}
|
||||
|
||||
/** Execute vault clawback mutations in a strictly ordered sequence.
|
||||
*
|
||||
* Entry invariant: asserts `sfLossUnrealized ≤ sfAssetsTotal −
|
||||
* sfAssetsAvailable`, a structural guarantee of the vault's accounting model.
|
||||
* A violation here indicates out-of-band mutation of vault fields and is
|
||||
* surfaced as a debug-build assertion failure.
|
||||
*
|
||||
* Mutation sequence:
|
||||
* 1. **Mode dispatch** — if the submitter is the vault owner and the resolved
|
||||
* amount targets shares, set `sharesDestroyed` to the holder's full share
|
||||
* balance (share-burn mode, `assetsRecovered` stays zero). Otherwise,
|
||||
* delegate to `assetsToClawback()` to compute both quantities.
|
||||
* 2. **Zero-shares guard** — if `sharesDestroyed` is zero after conversion,
|
||||
* return `tecPRECISION_LOSS`; no mutations have occurred yet.
|
||||
* 3. **Vault accounting update** — decrement `sfAssetsTotal` and
|
||||
* `sfAssetsAvailable` on the vault SLE, then call `view().update(vault)`
|
||||
* to stage the change before any `accountSend` calls.
|
||||
* 4. **Share transfer** — move `sharesDestroyed` shares from the holder to
|
||||
* the vault pseudo-account, waiving the transfer fee.
|
||||
* 5. **MPToken cleanup** — attempt to remove the holder's now-empty MPToken
|
||||
* entry via `removeEmptyHolding()`. The vault pseudo-account never sets
|
||||
* `lsfMPTAuthorized`, so authorization flags are ignored. If the holder
|
||||
* is the vault owner, this step is skipped entirely: the owner's MPToken
|
||||
* anchors the share issuance and must not be deleted.
|
||||
* `tecHAS_OBLIGATIONS` (holder still has a balance) is tolerated silently;
|
||||
* any other error is fatal.
|
||||
* 6. **Asset transfer** (skipped when `assetsRecovered == 0`) — move the
|
||||
* recovered assets from the vault pseudo-account to the submitting issuer,
|
||||
* waiving the transfer fee. Then `accountHolds` is called on the vault
|
||||
* pseudo-account as a belt-and-suspenders check: a negative result would
|
||||
* indicate an arithmetic bug producing an invalid ledger state.
|
||||
* 7. **`associateAsset`** — re-rounds all `STNumber` fields on the vault SLE
|
||||
* against the vault asset's precision. Per `STTakesAsset` contract, this
|
||||
* must be the very last operation after all mutations are complete.
|
||||
*
|
||||
* @return `tesSUCCESS` on success, `tecPRECISION_LOSS` if the share/asset
|
||||
* conversion produced zero shares, forwarded `TER` from `assetsToClawback`
|
||||
* or `accountSend` on failure, `tefINTERNAL` on ledger-corruption
|
||||
* conditions.
|
||||
*/
|
||||
TER
|
||||
VaultClawback::doApply()
|
||||
{
|
||||
@@ -367,13 +482,12 @@ VaultClawback::doApply()
|
||||
STAmount sharesDestroyed = {share};
|
||||
STAmount assetsRecovered = {vault->at(sfAsset)};
|
||||
|
||||
// The Owner is burning shares
|
||||
if (account_ == vault->at(sfOwner) && amount.asset() == share)
|
||||
{
|
||||
sharesDestroyed = accountHolds(
|
||||
view(), holder, share, FreezeHandling::IgnoreFreeze, AuthHandling::IgnoreAuth, j_);
|
||||
}
|
||||
else // The Issuer is clawbacking vault assets
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(amount.asset() == vaultAsset, "xrpl::VaultClawback::doApply : matching asset");
|
||||
|
||||
@@ -393,7 +507,6 @@ VaultClawback::doApply()
|
||||
view().update(vault);
|
||||
|
||||
auto const& vaultAccount = vault->at(sfAccount);
|
||||
// Transfer shares from holder to vault.
|
||||
if (auto const ter =
|
||||
accountSend(view(), holder, vaultAccount, sharesDestroyed, j_, WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
@@ -423,18 +536,15 @@ VaultClawback::doApply()
|
||||
return ter;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// else quietly ignore, holder balance is not zero
|
||||
}
|
||||
|
||||
if (assetsRecovered > beast::kZERO)
|
||||
{
|
||||
// Transfer assets from vault to issuer.
|
||||
if (auto const ter = accountSend(
|
||||
view(), vaultAccount, account_, assetsRecovered, j_, WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Sanity check
|
||||
if (accountHolds(
|
||||
view(),
|
||||
vaultAccount,
|
||||
|
||||
@@ -31,6 +31,15 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Pure amendment gate called before `preflight1`.
|
||||
*
|
||||
* Rejects the transaction with `temDISABLED` if `featureMPTokensV1` is not
|
||||
* active, because vaults rely entirely on MPT share issuance. A secondary
|
||||
* gate protects `sfDomainID`: using it requires `featurePermissionedDomains`,
|
||||
* so the two features can activate independently without cross-coupling.
|
||||
* Keeping these checks here (rather than in `preflight`) means a disabled
|
||||
* transaction is rejected before any field parsing occurs.
|
||||
*/
|
||||
bool
|
||||
VaultCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -43,12 +52,38 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns `tfVaultCreateMask`, restricting valid flags to `tfVaultPrivate`
|
||||
* and `tfVaultShareNonTransferable`. Any bit outside this mask causes
|
||||
* `preflight0` to reject with `temINVALID_FLAG`.
|
||||
*/
|
||||
std::uint32_t
|
||||
VaultCreate::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfVaultCreateMask;
|
||||
}
|
||||
|
||||
/** Stateless, ledger-free field validation run between `preflight1` and
|
||||
* `preflight2`.
|
||||
*
|
||||
* Checks in order, returning `temMALFORMED` on the first violation:
|
||||
* - `sfData` length must not exceed `kMAX_DATA_PAYLOAD_LENGTH`.
|
||||
* - `sfWithdrawalPolicy`, if present, must equal
|
||||
* `kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE` — the only currently supported
|
||||
* strategy; the field exists for future extensibility.
|
||||
* - `sfDomainID`, if present, must be non-zero and `tfVaultPrivate` must be
|
||||
* set. Associating a domain with a public vault would be incoherent:
|
||||
* public vaults admit all holders, so domain-restricted access has no
|
||||
* effect.
|
||||
* - `sfAssetsMaximum`, if present, must not be negative.
|
||||
* - `sfMPTokenMetadata`, if present, must be non-empty and within
|
||||
* `kMAX_MP_TOKEN_METADATA_LENGTH`.
|
||||
* - `sfScale`, if present, is only valid for IOU assets. XRP and MPT assets
|
||||
* have fixed integer representations for which a scale factor is meaningless,
|
||||
* so they are rejected. The upper bound `kVAULT_MAXIMUM_IOU_SCALE` (18) is
|
||||
* chosen because 10^19 exceeds the maximum MPT amount (2^63 − 1 ≈ 10^18.9),
|
||||
* ensuring that a single IOU unit can always be converted to at least one
|
||||
* share.
|
||||
*/
|
||||
NotTEC
|
||||
VaultCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -57,7 +92,6 @@ VaultCreate::preflight(PreflightContext const& ctx)
|
||||
|
||||
if (auto const withdrawalPolicy = ctx.tx[~sfWithdrawalPolicy])
|
||||
{
|
||||
// Enforce valid withdrawal policy
|
||||
if (*withdrawalPolicy != kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE)
|
||||
return temMALFORMED;
|
||||
}
|
||||
@@ -70,7 +104,8 @@ VaultCreate::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
if ((ctx.tx.getFlags() & tfVaultPrivate) == 0)
|
||||
{
|
||||
return temMALFORMED; // DomainID only allowed on private vaults
|
||||
// DomainID only allowed on private vaults
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +134,27 @@ VaultCreate::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Read-only ledger checks executed after signature verification.
|
||||
*
|
||||
* Checks in order, returning the first failing code:
|
||||
* 1. `canAddHolding` — delegates to the asset-type-appropriate helper to
|
||||
* verify the pseudo-account will be able to hold the given asset (e.g.,
|
||||
* MPT issuance limits and similar constraints).
|
||||
* 2. Pseudo-account issuer rejection — assets issued by a pseudo-account
|
||||
* (e.g., shares of another vault or AMM LP tokens) are rejected with
|
||||
* `tecWRONG_ASSET`. Such assets cannot be clawed back through the normal
|
||||
* mechanism because the issuer has no private key and no direct authority
|
||||
* path; allowing the vault to hold them would create an irrecoverable
|
||||
* liability if emergency intervention were ever needed.
|
||||
* 3. Freeze check — returns `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if the
|
||||
* asset is frozen for the vault owner.
|
||||
* 4. Domain existence — if `sfDomainID` is present, the referenced
|
||||
* `PermissionedDomain` object must exist; returns `tecOBJECT_NOT_FOUND`
|
||||
* otherwise.
|
||||
* 5. Address collision — pre-computes the deterministic pseudo-account address
|
||||
* from the vault keylet; if the result is zero (collision with an existing
|
||||
* account), returns `terADDRESS_COLLISION` before any state change.
|
||||
*/
|
||||
TER
|
||||
VaultCreate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -108,16 +164,12 @@ VaultCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (auto const ter = canAddHolding(ctx.view, vaultAsset))
|
||||
return ter;
|
||||
|
||||
// Check for pseudo-account issuers - we do not want a vault to hold such
|
||||
// assets (e.g. MPT shares to other vaults or AMM LPTokens) as they would be
|
||||
// impossible to clawback (should the need arise)
|
||||
if (!vaultAsset.native())
|
||||
{
|
||||
if (isPseudoAccount(ctx.view, vaultAsset.getIssuer()))
|
||||
return tecWRONG_ASSET;
|
||||
}
|
||||
|
||||
// Cannot create Vault for an Asset frozen for the vault owner
|
||||
if (isFrozen(ctx.view, account, vaultAsset))
|
||||
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
|
||||
|
||||
@@ -136,13 +188,57 @@ VaultCreate::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Mutates the ledger to create the vault and all supporting infrastructure.
|
||||
*
|
||||
* All return codes here are `tec`, `ter`, or `tes`; a `tec` return signals
|
||||
* the framework to discard all mutations via `ApplyContext::discard()`, then
|
||||
* re-apply only the fee and sequence. Steps are ordered so that the reserve
|
||||
* check occurs before any irreversible state is written.
|
||||
*
|
||||
* Execution order:
|
||||
* 1. **Directory link + owner count**: `dirLink` inserts the new vault SLE
|
||||
* into the owner's directory. `adjustOwnerCount` then increments by 2 —
|
||||
* one for the vault SLE and one for the pseudo-account — *before* the
|
||||
* reserve check. This order is intentional: the reserve must reflect the
|
||||
* true post-creation object count to prevent the account from being left
|
||||
* underfunded. If `preFeeBalance_` is insufficient, returns
|
||||
* `tecINSUFFICIENT_RESERVE` and the framework rolls back all mutations.
|
||||
* 2. **Pseudo-account**: `createPseudoAccount` creates a new `AccountRoot`
|
||||
* SLE at the deterministically-derived address, storing the vault's key in
|
||||
* `sfVaultID`. This synthetic account holds the pooled asset, issues
|
||||
* shares, and has no private key — it is controlled solely by the
|
||||
* transactor logic. Failure here is an invariant violation that `preclaim`
|
||||
* should have prevented; the branch is marked `LCOV_EXCL_LINE`.
|
||||
* 3. **Empty asset holding**: `addEmptyHolding` establishes a zero-balance
|
||||
* `MPToken` (MPT assets) or `TrustLine`/`RippleState` (IOU assets) on the
|
||||
* pseudo-account, ready to receive deposits. XRP vaults require no holding
|
||||
* object, but the dispatch is handled uniformly.
|
||||
* 4. **Share MPT issuance**: `MPTokenIssuanceCreate::create` is called from
|
||||
* the pseudo-account's perspective at sequence 1 (its first issuance).
|
||||
* This creates the `MPTokenIssuance` SLE that represents vault shares —
|
||||
* distinct from the asset holding created in step 3. Flag mapping:
|
||||
* `tfVaultShareNonTransferable` absent → `lsfMPTCanEscrow | lsfMPTCanTrade
|
||||
* | lsfMPTCanTransfer`; `tfVaultPrivate` set → `lsfMPTRequireAuth`.
|
||||
* 5. **Vault SLE population**: All fields are written to the in-memory SLE
|
||||
* before `view().insert` commits it: `sfAsset`, `sfFlags` (private bit
|
||||
* only), `sfOwner`, `sfAccount` (pseudo-account ID), `sfAssetsTotal`,
|
||||
* `sfAssetsAvailable`, `sfLossUnrealized` (all zero), optional
|
||||
* `sfAssetsMaximum`, `sfShareMPTID`, `sfData`, `sfWithdrawalPolicy`
|
||||
* (defaulting to `kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE`), and `sfScale`
|
||||
* (omitted for XRP/MPT assets where scale is always 0).
|
||||
* 6. **Owner MPToken authorization**: `authorizeMPToken` creates an `MPToken`
|
||||
* SLE for the vault creator so they can hold shares immediately. For
|
||||
* private vaults, a second call authorizes the pseudo-account itself (with
|
||||
* the owner as the authorizing party), which is required for the
|
||||
* pseudo-account to participate in share issuance mechanics.
|
||||
* 7. **`associateAsset`**: Must be the final write. Propagates the vault's
|
||||
* asset type through all `sMD_NeedsAsset` fields (primarily `STNumber`
|
||||
* fields), tying the asset's decimal scale to the number representation
|
||||
* so that serialization rounds correctly.
|
||||
*/
|
||||
TER
|
||||
VaultCreate::doApply()
|
||||
{
|
||||
// All return codes in `doApply` must be `tec`, `ter`, or `tes`.
|
||||
// As we move checks into `preflight` and `preclaim`,
|
||||
// we can consider downgrading them to `tef` or `tem`.
|
||||
|
||||
auto const& tx = ctx_.tx;
|
||||
auto const sequence = tx.getSeqValue();
|
||||
auto const owner = view().peek(keylet::account(account_));
|
||||
@@ -153,7 +249,6 @@ VaultCreate::doApply()
|
||||
|
||||
if (auto ter = dirLink(view(), account_, vault))
|
||||
return ter;
|
||||
// We will create Vault and PseudoAccount, hence increase OwnerCount by 2
|
||||
adjustOwnerCount(view(), owner, 2, j_);
|
||||
auto const ownerCount = owner->at(sfOwnerCount);
|
||||
if (preFeeBalance_ < view().fees().accountReserve(ownerCount))
|
||||
@@ -180,10 +275,10 @@ VaultCreate::doApply()
|
||||
if ((txFlags & tfVaultPrivate) != 0u)
|
||||
mptFlags |= lsfMPTRequireAuth;
|
||||
|
||||
// Note, here we are **not** creating an MPToken for the assets held in
|
||||
// the vault. That MPToken or TrustLine/RippleState is created above, in
|
||||
// addEmptyHolding. Here we are creating MPTokenIssuance for the shares
|
||||
// in the vault
|
||||
// This creates the MPTokenIssuance for vault *shares*, not for the
|
||||
// underlying asset. The asset holding (MPToken or TrustLine/RippleState)
|
||||
// was already created above via addEmptyHolding — the two calls are
|
||||
// structurally similar, and the distinction is easy to miss.
|
||||
auto maybeShare = MPTokenIssuanceCreate::create(
|
||||
view(),
|
||||
j_,
|
||||
@@ -210,13 +305,11 @@ VaultCreate::doApply()
|
||||
vault->at(sfAssetsTotal) = Number(0);
|
||||
vault->at(sfAssetsAvailable) = Number(0);
|
||||
vault->at(sfLossUnrealized) = Number(0);
|
||||
// Leave default values for AssetTotal and AssetAvailable, both zero.
|
||||
if (auto value = tx[~sfAssetsMaximum])
|
||||
vault->at(sfAssetsMaximum) = *value;
|
||||
vault->at(sfShareMPTID) = mptIssuanceID;
|
||||
if (auto value = tx[~sfData])
|
||||
vault->at(sfData) = *value;
|
||||
// Required field, default to vaultStrategyFirstComeFirstServe
|
||||
if (auto value = tx[~sfWithdrawalPolicy])
|
||||
{
|
||||
vault->at(sfWithdrawalPolicy) = *value;
|
||||
@@ -229,13 +322,11 @@ VaultCreate::doApply()
|
||||
vault->at(sfScale) = scale;
|
||||
view().insert(vault);
|
||||
|
||||
// Explicitly create MPToken for the vault owner
|
||||
if (auto const err =
|
||||
authorizeMPToken(view(), preFeeBalance_, mptIssuanceID, account_, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
// If the vault is private, set the authorized flag for the vault owner
|
||||
if ((txFlags & tfVaultPrivate) != 0u)
|
||||
{
|
||||
if (auto const err = authorizeMPToken(
|
||||
|
||||
@@ -33,6 +33,25 @@ VaultDelete::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Enforces pre-deletion invariants against the live ledger.
|
||||
*
|
||||
* `sfAssetsAvailable` and `sfAssetsTotal` are checked independently because
|
||||
* a vault carrying unrealized losses (e.g. defaulted loans) may have
|
||||
* `sfAssetsTotal` > `sfAssetsAvailable`; both must reach zero before the
|
||||
* vault can be destroyed. Checking only one would allow deletion while the
|
||||
* other still records outstanding obligations.
|
||||
*
|
||||
* The two `MPTokenIssuance` guards — existence of the share issuance SLE and
|
||||
* the issuer-match check — are wrapped in `LCOV_EXCL_START` because they
|
||||
* defend against ledger state that cannot arise from valid transaction
|
||||
* sequences. They are reachable only if earlier transactions have already
|
||||
* corrupted the ledger; their presence prevents silent destruction of a
|
||||
* partially dismantled vault cluster.
|
||||
*
|
||||
* All user-correctable failures (`tecNO_PERMISSION`, `tecHAS_OBLIGATIONS`,
|
||||
* `tecNO_ENTRY`) consume the transaction fee, signalling that the submitter
|
||||
* must resolve the issue before resubmitting.
|
||||
*/
|
||||
TER
|
||||
VaultDelete::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -86,6 +105,57 @@ VaultDelete::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Dismantles the vault cluster in strict dependency order.
|
||||
*
|
||||
* The five-step sequence must not be reordered: each step removes an object
|
||||
* that a later step depends on having already cleaned up.
|
||||
*
|
||||
* **Step 1 — Asset holding removal.**
|
||||
* `removeEmptyHolding` erases the trust line (`RippleState`) or `MPToken`
|
||||
* that the pseudo-account used to hold the underlying asset. The holding is
|
||||
* guaranteed empty by `preclaim`'s `sfAssetsTotal == 0` check; the call also
|
||||
* removes the object from the pseudo-account's owner directory and
|
||||
* decrements its owner count.
|
||||
*
|
||||
* **Step 2 — Vault owner's share MPToken removal.**
|
||||
* If the vault creator holds an `MPToken` for the share issuance
|
||||
* (`keylet::mptoken(shareMPTID, account_)`), a second `removeEmptyHolding`
|
||||
* call cleans it up. This is conditioned on the token's existence; the
|
||||
* vault owner is not required to hold shares. The `LCOV_EXCL` guard covers
|
||||
* the failure branch, which is unreachable in valid ledger state because
|
||||
* `preclaim` has already verified `sfOutstandingAmount == 0`.
|
||||
*
|
||||
* **Step 3 — Share issuance removal.**
|
||||
* The `MPTokenIssuance` SLE is removed directly rather than via
|
||||
* `MPTokenIssuanceDestroy` because that transactor carries fee and
|
||||
* amendment logic irrelevant here. `dirRemove` uses the cached
|
||||
* `sfOwnerNode` from the issuance SLE for O(1) directory removal, then
|
||||
* `adjustOwnerCount(view(), pseudoAcct, -1, j_)` decrements the
|
||||
* pseudo-account's count before the SLE is erased.
|
||||
*
|
||||
* **Step 4 — Pseudo-account cleanup verification and erasure.**
|
||||
* After Steps 1–3, the pseudo-account's owner directory must be empty.
|
||||
* The `view().peek(keylet::ownerDir(pseudoID))` guard is the one
|
||||
* `tec` code emitted from `doApply` (vs. `tef` codes for true corruption),
|
||||
* marked `LCOV_EXCL_LINE` because it is a forward-safety valve: a future
|
||||
* ledger feature could attach additional objects to the pseudo-account's
|
||||
* directory, and this guard prevents silently destroying a pseudo-account
|
||||
* that still owns unhandled objects. The subsequent balance and owner-count
|
||||
* checks are `LCOV_EXCL`-guarded corruption sentinels; if reached, they
|
||||
* return `tecHAS_OBLIGATIONS` rather than `tef` to let the invariant checker
|
||||
* log diagnostics before fee collection.
|
||||
*
|
||||
* **Step 5 — Vault SLE removal and owner-count adjustment.**
|
||||
* The vault is removed from the real owner's `ownerDir` via `dirRemove`,
|
||||
* then `adjustOwnerCount(view(), owner, -2, j_)` fires. The `-2` is the
|
||||
* exact inverse of `VaultCreate`'s `+2`, accounting for both the vault SLE
|
||||
* and the pseudo-account destroyed in Step 4. The vault SLE is erased last.
|
||||
*
|
||||
* Errors indicating impossible ledger state (missing pseudo-account,
|
||||
* mismatched issuance, failed directory removal) return `tefBAD_LEDGER` or
|
||||
* `tefINTERNAL` rather than fee-claiming `tec` codes, signalling internal
|
||||
* inconsistency rather than a user-correctable condition.
|
||||
*/
|
||||
TER
|
||||
VaultDelete::doApply()
|
||||
{
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file VaultDeposit.cpp
|
||||
* Implementation of the `ttVAULT_DEPOSIT` transactor.
|
||||
*
|
||||
* Deposits a fungible asset (XRP, IOU, or MPT) into a vault and mints
|
||||
* vault-share MPTokens for the depositor. The share/asset exchange rate is
|
||||
* determined by the vault's current `sfAssetsTotal` and the share issuance's
|
||||
* `sfOutstandingAmount`; on a fresh vault both are zero and the initial rate
|
||||
* is seeded via `sfScale`. See VaultHelpers.h for the arithmetic.
|
||||
*/
|
||||
#include <xrpl/tx/transactors/vault/VaultDeposit.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -40,6 +49,31 @@ VaultDeposit::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Read-only ledger validation for a vault deposit.
|
||||
*
|
||||
* Checks are ordered from cheapest to most expensive and fail-fast on the
|
||||
* first violation. The private-vault authorization path has two deliberate
|
||||
* design choices worth noting:
|
||||
*
|
||||
* - **`tecEXPIRED` suppression**: `credentials::validDomain` may return
|
||||
* `tecEXPIRED` when the account's credential has expired. This code is
|
||||
* suppressed here (the transaction proceeds) because deleting the expired
|
||||
* credential is a ledger state mutation, which is only permitted in
|
||||
* `doApply`. The `doApply` call to `enforceMPTokenAuthorization` handles
|
||||
* the deletion.
|
||||
*
|
||||
* - **No issuer-grant fallback**: the vault's share MPT issuance is managed
|
||||
* by the vault pseudo-account, which cannot sign transactions and therefore
|
||||
* cannot proactively grant MPT authorizations to individual holders. The
|
||||
* only supported admission mechanism for private vaults is the domain-
|
||||
* credential path via `sfDomainID` on the share issuance. Absence of
|
||||
* `sfDomainID` on a private vault's issuance is an unconditional
|
||||
* `tecNO_AUTH`.
|
||||
*
|
||||
* The `lsfMPTLocked` and identical-asset-type checks are classified as
|
||||
* `tefINTERNAL` and marked `LCOV_EXCL` because the `VaultCreate` transactor
|
||||
* guarantees they cannot occur under correct ledger logic.
|
||||
*/
|
||||
TER
|
||||
VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -87,26 +121,17 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Cannot deposit inside Vault an Asset frozen for the depositor
|
||||
if (isFrozen(ctx.view, account, vaultAsset))
|
||||
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
|
||||
|
||||
// Cannot deposit if the shares of the vault are frozen
|
||||
if (isFrozen(ctx.view, account, vaultShare))
|
||||
return tecLOCKED;
|
||||
|
||||
if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner))
|
||||
{
|
||||
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
|
||||
// Since this is a private vault and the account is not its owner, we
|
||||
// perform authorization check based on DomainID read from sleIssuance.
|
||||
// Had the vault shares been a regular MPToken, we would allow
|
||||
// authorization granted by the Issuer explicitly, but Vault uses Issuer
|
||||
// pseudo-account, which cannot grant an authorization.
|
||||
if (maybeDomainID)
|
||||
{
|
||||
// As per validDomain documentation, we suppress tecEXPIRED error
|
||||
// here, so we can delete any expired credentials inside doApply.
|
||||
if (auto const err = credentials::validDomain(ctx.view, *maybeDomainID, account);
|
||||
!isTesSuccess(err) && err != tecEXPIRED)
|
||||
return err;
|
||||
@@ -117,7 +142,6 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Source MPToken must exist (if asset is an MPT)
|
||||
if (auto const ter = requireAuth(ctx.view, vaultAsset, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
@@ -134,6 +158,63 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Mutate the ledger to execute a vault deposit.
|
||||
*
|
||||
* ### MPToken provisioning
|
||||
*
|
||||
* Before any arithmetic, the depositor must hold an MPToken for vault shares.
|
||||
* Two distinct code paths handle this:
|
||||
*
|
||||
* - **Private vault, non-owner**: `enforceMPTokenAuthorization` is called.
|
||||
* It deletes any expired credentials found in the ledger (the deferred
|
||||
* side-effect suppressed in `preclaim`) and then creates the MPToken entry
|
||||
* for the depositor if it does not yet exist.
|
||||
*
|
||||
* - **Public vault, or vault owner**: `authorizeMPToken` is called only when
|
||||
* the MPToken does not yet exist — no domain check is required. When the
|
||||
* depositor is the vault owner of a *private* vault, a second
|
||||
* `authorizeMPToken` call additionally provisions an MPToken on the vault's
|
||||
* pseudo-account (`sleIssuance->at(sfIssuer)`) authorised for the owner.
|
||||
* The pseudo-account cannot self-authorise, so this is the only mechanism
|
||||
* by which it can hold shares for internal accounting.
|
||||
*
|
||||
* ### Exchange arithmetic (two-step, depositor-protective)
|
||||
*
|
||||
* 1. `assetsToSharesDeposit(vault, sleIssuance, amount)` — converts the
|
||||
* offered asset amount into shares, **truncating** (floor) to an integral
|
||||
* MPT unit. On a fresh vault (`sfAssetsTotal == 0`) the initial rate is
|
||||
* seeded using `sfScale`: `shares = floor(assets.mantissa * 10^(assets.exponent + scale))`.
|
||||
* Returns `tecPRECISION_LOSS` when the floor rounds to zero — the deposit
|
||||
* is too small relative to the current share price.
|
||||
*
|
||||
* 2. `sharesToAssetsDeposit(vault, sleIssuance, sharesCreated)` — inverts the
|
||||
* truncated share count back to an exact asset amount, guaranteeing
|
||||
* `assetsDeposited <= amount`. Any sub-share-unit fractional asset
|
||||
* remainder stays with the depositor. An assertion verifies this invariant;
|
||||
* violation indicates a bug in the arithmetic helpers.
|
||||
*
|
||||
* `std::overflow_error` from either helper (reachable when `sfScale` is large
|
||||
* and balances are high) is caught and converted to `tecPATH_DRY`. The log
|
||||
* level is intentionally `debug`, not `error`, because this is a
|
||||
* user-triggerable condition rather than a ledger-corruption sentinel.
|
||||
*
|
||||
* ### Mutation ordering and cap enforcement
|
||||
*
|
||||
* `sfAssetsTotal` and `sfAssetsAvailable` are incremented and the vault SLE
|
||||
* flushed to the view *before* the cap check and before either `accountSend`.
|
||||
* This ordering is deliberate: the `sfAssetsMaximum` guard must see the
|
||||
* post-deposit total to correctly reject a deposit that would overflow the
|
||||
* cap. If it would exceed the cap the transaction returns `tecLIMIT_EXCEEDED`
|
||||
* and the framework discards all view mutations.
|
||||
*
|
||||
* Both `accountSend` calls use `WaiveTransferFee::Yes` to prevent asset
|
||||
* transfer fees from distorting `sfAssetsTotal` accounting and corrupting the
|
||||
* exchange rate for future depositors.
|
||||
*
|
||||
* `associateAsset` is the final call; per the `STTakesAsset` contract, it
|
||||
* re-rounds stored numeric values to the asset's precision and must execute
|
||||
* after all other writes to the vault SLE are complete.
|
||||
*/
|
||||
TER
|
||||
VaultDeposit::doApply()
|
||||
{
|
||||
@@ -143,7 +224,6 @@ VaultDeposit::doApply()
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
// Make sure the depositor can hold shares.
|
||||
auto const mptIssuanceID = (*vault)[sfShareMPTID];
|
||||
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
@@ -155,7 +235,6 @@ VaultDeposit::doApply()
|
||||
}
|
||||
|
||||
auto const& vaultAccount = vault->at(sfAccount);
|
||||
// Note, vault owner is always authorized
|
||||
if (vault->isFlag(lsfVaultPrivate) && account_ != vault->at(sfOwner))
|
||||
{
|
||||
if (auto const err = enforceMPTokenAuthorization(
|
||||
@@ -163,9 +242,8 @@ VaultDeposit::doApply()
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
else // !vault->isFlag(lsfVaultPrivate) || account_ == vault->at(sfOwner)
|
||||
else
|
||||
{
|
||||
// No authorization needed, but must ensure there is MPToken
|
||||
if (!view().exists(keylet::mptoken(mptIssuanceID, account_)))
|
||||
{
|
||||
if (auto const err = authorizeMPToken(
|
||||
@@ -174,7 +252,6 @@ VaultDeposit::doApply()
|
||||
return err;
|
||||
}
|
||||
|
||||
// If the vault is private, set the authorized flag for the vault owner
|
||||
if (vault->isFlag(lsfVaultPrivate))
|
||||
{
|
||||
// This follows from the reverse of the outer enclosing if condition
|
||||
@@ -197,7 +274,6 @@ VaultDeposit::doApply()
|
||||
STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited;
|
||||
try
|
||||
{
|
||||
// Compute exchange before transferring any amounts.
|
||||
{
|
||||
auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount);
|
||||
if (!maybeShares)
|
||||
@@ -223,8 +299,6 @@ VaultDeposit::doApply()
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultDeposit: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
@@ -241,18 +315,15 @@ VaultDeposit::doApply()
|
||||
vault->at(sfAssetsAvailable) += assetsDeposited;
|
||||
view().update(vault);
|
||||
|
||||
// A deposit must not push the vault over its limit.
|
||||
auto const maximum = *vault->at(sfAssetsMaximum);
|
||||
if (maximum != 0 && *vault->at(sfAssetsTotal) > maximum)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
// Transfer assets from depositor to vault.
|
||||
if (auto const ter =
|
||||
accountSend(view(), account_, vaultAccount, assetsDeposited, j_, WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Sanity check
|
||||
if (accountHolds(
|
||||
view(),
|
||||
account_,
|
||||
@@ -267,7 +338,6 @@ VaultDeposit::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Transfer shares from vault to depositor.
|
||||
if (auto const ter =
|
||||
accountSend(view(), vaultAccount, account_, sharesCreated, j_, WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
|
||||
@@ -19,12 +19,32 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Gate `sfDomainID` on the `featurePermissionedDomains` amendment.
|
||||
*
|
||||
* Allows domain support to be deployed independently of vault support: a
|
||||
* network that has vaults but not permissioned domains will reject any
|
||||
* `VaultSet` that tries to set `sfDomainID`, while all other `VaultSet`
|
||||
* transactions continue to work normally.
|
||||
*/
|
||||
bool
|
||||
VaultSet::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
return !ctx.tx.isFieldPresent(sfDomainID) || ctx.rules.enabled(featurePermissionedDomains);
|
||||
}
|
||||
|
||||
/** Validate transaction fields without ledger access.
|
||||
*
|
||||
* Checks are ordered from cheapest to most specific:
|
||||
* 1. `sfVaultID` must be non-zero — a zero ID cannot address any real vault.
|
||||
* 2. `sfData`, if present, must be non-empty and within `kMAX_DATA_PAYLOAD_LENGTH`.
|
||||
* 3. `sfAssetsMaximum`, if present, must be non-negative (zero means no cap).
|
||||
* 4. At least one mutable field must be present — a no-op transaction is
|
||||
* rejected as `temMALFORMED` to prevent fee-burning with no ledger effect.
|
||||
*
|
||||
* The cap-vs-total comparison (`tecLIMIT_EXCEEDED`) cannot be done here
|
||||
* because `sfAssetsTotal` is only readable from the ledger; that check lives
|
||||
* in `doApply`.
|
||||
*/
|
||||
NotTEC
|
||||
VaultSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -62,6 +82,25 @@ VaultSet::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Read-only ownership and consistency checks after signature verification.
|
||||
*
|
||||
* Checks are ordered to fail as early as possible on the cheapest condition:
|
||||
* 1. Vault existence — `tecNO_ENTRY` if the vault SLE is absent.
|
||||
* 2. Ownership — `tecNO_PERMISSION` if submitter is not `sfOwner`.
|
||||
* 3. Issuance existence — `tefINTERNAL` if the vault's `MPTokenIssuance` is
|
||||
* missing. This path is covered by `LCOV_EXCL_*` because `VaultCreate`
|
||||
* and the `ValidVault` invariant checker together ensure a vault always
|
||||
* has a corresponding issuance; reaching this branch implies ledger
|
||||
* corruption, not a user error.
|
||||
* 4. Domain checks (only when `sfDomainID` is present in the transaction):
|
||||
* - `lsfVaultPrivate` must be set on the vault (`tecNO_PERMISSION`);
|
||||
* domain restrictions can only be applied to private vaults.
|
||||
* - A non-zero domain hash must resolve to an existing
|
||||
* `PermissionedDomain` object (`tecOBJECT_NOT_FOUND`).
|
||||
* - `lsfMPTRequireAuth` must be set on the issuance (`tefINTERNAL`);
|
||||
* this flag is enforced at `VaultCreate` time for private vaults, so
|
||||
* its absence indicates ledger corruption (also `LCOV_EXCL_*`).
|
||||
*/
|
||||
TER
|
||||
VaultSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -88,7 +127,6 @@ VaultSet::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (auto const domain = ctx.tx[~sfDomainID])
|
||||
{
|
||||
// We can only set domain if private flag was originally set
|
||||
if (!vault->isFlag(lsfVaultPrivate))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultSet: vault is not private";
|
||||
@@ -102,7 +140,6 @@ VaultSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Sanity check only, this should be enforced by VaultCreate
|
||||
if ((sleIssuance->getFlags() & lsfMPTRequireAuth) == 0)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -115,6 +152,37 @@ VaultSet::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Apply vault configuration changes to the mutable ledger view.
|
||||
*
|
||||
* Re-fetches the vault and its `MPTokenIssuance` via `view().peek()` to
|
||||
* obtain mutable SLE references, then applies updates in order:
|
||||
*
|
||||
* **sfData**: written directly to the vault SLE when present.
|
||||
*
|
||||
* **sfAssetsMaximum**: the ledger-state check that `preflight` cannot perform
|
||||
* happens here — if the requested cap is non-zero but less than the current
|
||||
* `sfAssetsTotal`, the transaction fails with `tecLIMIT_EXCEEDED`. Zero is
|
||||
* the sentinel for "no cap" and always succeeds.
|
||||
*
|
||||
* **sfDomainID**: written to (or removed from) the `MPTokenIssuance` SLE, NOT
|
||||
* the vault SLE. The domain lives on the issuance because the MPT
|
||||
* authorization machinery (`lsfMPTRequireAuth`, `credentials::validDomain`)
|
||||
* operates at the issuance level; this avoids special-casing the vault layer
|
||||
* in the depositor authorization path. A zero hash calls `makeFieldAbsent`
|
||||
* to clear the restriction compactly rather than storing a zero value.
|
||||
* Clearing the domain does not remove `lsfVaultPrivate` — once private,
|
||||
* always private (making a private vault public is not currently supported).
|
||||
*
|
||||
* **view().update(vault)** is always called even when only the issuance
|
||||
* changed. The `ValidVault` invariant checker inspects modified vault SLEs
|
||||
* to validate the operation; omitting the update would make it invisible to
|
||||
* the checker.
|
||||
*
|
||||
* **associateAsset** is the final step and must remain last. It re-rounds
|
||||
* all `STNumber` / `STTakesAsset`-derived fields in the vault SLE to the
|
||||
* precision of the vault's underlying asset type. Calling it earlier would
|
||||
* produce silent precision corruption on any field written afterwards.
|
||||
*/
|
||||
TER
|
||||
VaultSet::doApply()
|
||||
{
|
||||
@@ -124,7 +192,6 @@ VaultSet::doApply()
|
||||
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
// Update existing object.
|
||||
auto vault = view().peek(keylet::vault(tx[sfVaultID]));
|
||||
if (!vault)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -141,7 +208,6 @@ VaultSet::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Update mutable flags and fields if given.
|
||||
if (tx.isFieldPresent(sfData))
|
||||
vault->at(sfData) = tx[sfData];
|
||||
if (tx.isFieldPresent(sfAssetsMaximum))
|
||||
@@ -155,11 +221,6 @@ VaultSet::doApply()
|
||||
{
|
||||
if (*domainId != beast::kZERO)
|
||||
{
|
||||
// In VaultSet::preclaim we enforce that lsfVaultPrivate must have
|
||||
// been set in the vault. We currently do not support making such a
|
||||
// vault public (i.e. removal of lsfVaultPrivate flag). The
|
||||
// sfDomainID flag must be set in the MPTokenIssuance object and can
|
||||
// be freely updated.
|
||||
sleIssuance->setFieldH256(sfDomainID, *domainId);
|
||||
}
|
||||
else if (sleIssuance->isFieldPresent(sfDomainID))
|
||||
@@ -169,9 +230,6 @@ VaultSet::doApply()
|
||||
view().update(sleIssuance);
|
||||
}
|
||||
|
||||
// Note, we must update Vault object even if only DomainID is being updated
|
||||
// in Issuance object. Otherwise it's really difficult for Vault invariants
|
||||
// to verify the operation.
|
||||
view().update(vault);
|
||||
|
||||
associateAsset(*vault, vaultAsset);
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/** @file VaultWithdraw.cpp
|
||||
* Implementation of the `ttVAULT_WITHDRAW` transactor.
|
||||
*
|
||||
* Redeems vault-share MPTokens for the underlying pooled asset — the inverse
|
||||
* of `VaultDeposit`. The user specifies `sfAmount` in either the vault's
|
||||
* underlying asset (fixed assets, variable shares) or the vault's share MPT
|
||||
* (fixed shares, variable assets); the complementary quantity is computed via
|
||||
* `assetsToSharesWithdraw` / `sharesToAssetsWithdraw` from VaultHelpers.h.
|
||||
*
|
||||
* Liquidity is gated on `sfAssetsAvailable` rather than the vault
|
||||
* pseudo-account's raw balance, so that assets pledged to lending brokers
|
||||
* are correctly excluded from on-demand redemptions.
|
||||
*/
|
||||
#include <xrpl/tx/transactors/vault/VaultWithdraw.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -26,6 +39,21 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Stateless, ledger-free validation of a `ttVAULT_WITHDRAW` transaction.
|
||||
*
|
||||
* Checks are ordered cheapest-first and fail on the first violation. Each
|
||||
* rejected condition is a programming error or a structurally invalid
|
||||
* request that would be meaningless against any ledger state:
|
||||
*
|
||||
* - A zero `sfVaultID` is always a null key; no valid vault can have this ID.
|
||||
* - A non-positive `sfAmount` cannot represent a redemption of any value.
|
||||
* - A zero explicit `sfDestination`, when present, cannot identify any real
|
||||
* account; only the absent-field case (withdraw to self) is permitted to
|
||||
* omit a destination.
|
||||
*
|
||||
* @param ctx Preflight context carrying the transaction and active rules.
|
||||
* @return `tesSUCCESS`, `temMALFORMED`, or `temBAD_AMOUNT`.
|
||||
*/
|
||||
NotTEC
|
||||
VaultWithdraw::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -49,6 +77,41 @@ VaultWithdraw::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Read-only ledger validation for a vault withdrawal.
|
||||
*
|
||||
* Checks are ordered from cheapest to most expensive and fail fast on the
|
||||
* first violation. Several design choices merit explanation:
|
||||
*
|
||||
* **`fixSecurity3_1_3` and the share-limit gap**: before this amendment, when
|
||||
* `sfAmount` was share-denominated, the withdrawal-limit check (`canWithdraw`)
|
||||
* was silently skipped — only the simpler two-argument overload ran. Post-
|
||||
* amendment, shares are first converted to an equivalent asset amount via
|
||||
* `sharesToAssetsWithdraw`, and that asset amount is fed to the full
|
||||
* four-argument `canWithdraw` overload. Overflow during this conversion
|
||||
* (easily reachable with large `sfScale` values) returns `tecPATH_DRY` rather
|
||||
* than crashing; the log level is deliberately `debug` because this is a
|
||||
* user-triggerable condition, not a ledger-corruption sentinel.
|
||||
*
|
||||
* **Two-tier authorization**: if the destination is the submitting account
|
||||
* itself, `WeakAuth` is used — `doApply` will create a trust line or MPToken
|
||||
* on the submitter's behalf as needed. If `sfDestination` names a third party,
|
||||
* `StrongAuth` is required: the holding must already exist before the
|
||||
* withdrawal is applied.
|
||||
*
|
||||
* **Dual freeze checks**: one call verifies the vault's underlying asset is
|
||||
* not frozen for the destination (the receiver must be able to accept it), and
|
||||
* a second call verifies the vault's share MPT is not frozen or locked for the
|
||||
* submitter (the redeemer must be able to surrender shares).
|
||||
*
|
||||
* The `sfWithdrawalPolicy` guard and the missing-issuance guard are marked
|
||||
* `LCOV_EXCL` because `VaultCreate` invariants make them unreachable under
|
||||
* correct ledger state.
|
||||
*
|
||||
* @param ctx Preclaim context carrying a read-only ledger view and rules.
|
||||
* @return `tesSUCCESS`, `tecNO_ENTRY`, `tecWRONG_ASSET`, `tecPATH_DRY`,
|
||||
* `tefINTERNAL`, or a code from `canTransfer`, `canWithdraw`,
|
||||
* `requireAuth`, or `checkFrozen`.
|
||||
*/
|
||||
TER
|
||||
VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -71,7 +134,6 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
return ter;
|
||||
}
|
||||
|
||||
// Enforce valid withdrawal policy
|
||||
if (vault->at(sfWithdrawalPolicy) != kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -82,10 +144,6 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (ctx.view.rules().enabled(fixSecurity3_1_3) && amount.asset() == vaultShare)
|
||||
{
|
||||
// Post-fixSecurity3_1_3: if the user specified shares, convert
|
||||
// to the equivalent asset amount before checking withdrawal
|
||||
// limits. Pre-amendment the limit check was skipped for
|
||||
// share-denominated withdrawals.
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(vaultShare));
|
||||
if (!sleIssuance)
|
||||
{
|
||||
@@ -128,25 +186,80 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
return ret;
|
||||
}
|
||||
|
||||
// If sending to Account (i.e. not a transfer), we will also create (only
|
||||
// if authorized) a trust line or MPToken as needed, in doApply().
|
||||
// Destination MPToken or trust line must exist if _not_ sending to Account.
|
||||
AuthType const authType = account == dstAcct ? AuthType::WeakAuth : AuthType::StrongAuth;
|
||||
if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Cannot withdraw from a Vault an Asset frozen for the destination account
|
||||
if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset))
|
||||
return ret;
|
||||
|
||||
// Cannot return shares to the vault, if the underlying asset was frozen for
|
||||
// the submitter
|
||||
if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
|
||||
return ret;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Mutate the ledger to execute a vault withdrawal.
|
||||
*
|
||||
* ### Exchange rate computation (two modes)
|
||||
*
|
||||
* **Asset-denominated** (`sfAmount.asset() == vaultAsset`): `assetsToSharesWithdraw`
|
||||
* converts the requested asset quantity to an integer share count (truncating).
|
||||
* The result is immediately re-converted via `sharesToAssetsWithdraw` to
|
||||
* determine the exact assets to disburse — this double-conversion is not
|
||||
* redundant: it corrects for MPT integer truncation so the vault always pays
|
||||
* out the precise amount that the integer share count entitles, never more.
|
||||
* If the first conversion produces zero shares, `tecPRECISION_LOSS` is returned
|
||||
* (the requested amount is too small to represent even one share unit).
|
||||
*
|
||||
* **Share-denominated** (`sfAmount.asset() == share`): the share count is used
|
||||
* directly; `sharesToAssetsWithdraw` computes the corresponding asset payout.
|
||||
* No rounding step is needed because the share count is already integral.
|
||||
*
|
||||
* Both modes catch `std::overflow_error` and return `tecPATH_DRY`. The log
|
||||
* level is `debug` because large `sfScale` values make overflow arithmetically
|
||||
* trivial for legitimate users — this is not a ledger-corruption sentinel.
|
||||
*
|
||||
* ### Liquidity gating
|
||||
*
|
||||
* The check against `sfAssetsAvailable` (not the raw pseudo-account balance)
|
||||
* is deliberate: a vault may have pledged assets to lending brokers, reducing
|
||||
* what is actually liquid. `sfAssetsAvailable` tracks only unencumbered assets.
|
||||
* Both `sfAssetsTotal` and `sfAssetsAvailable` are decremented by
|
||||
* `assetsWithdrawn` on success.
|
||||
*
|
||||
* ### Share redemption and MPToken cleanup
|
||||
*
|
||||
* Shares flow back to the vault pseudo-account via `accountSend` with
|
||||
* `WaiveTransferFee::Yes` — shares are internal bookkeeping tokens, not
|
||||
* economic transfers subject to issuer-configured transfer fees.
|
||||
*
|
||||
* After redemption, if the submitter's share balance drops to zero and the
|
||||
* submitter is not the vault owner, `removeEmptyHolding` attempts to delete
|
||||
* the now-empty MPToken. `tecHAS_OBLIGATIONS` is silently ignored (the holder
|
||||
* has associated obligations and must retain the MPToken). Vault owners are
|
||||
* excluded from cleanup because their MPToken may be needed for future
|
||||
* deposits and internal accounting.
|
||||
*
|
||||
* ### `lsfVaultPrivate` omission (intentional)
|
||||
*
|
||||
* `doApply` does not re-check the `lsfVaultPrivate` flag. Possession of vault
|
||||
* shares is proof of prior authorized deposit; withdrawal authorization is
|
||||
* effectively irrevocable once shares are held, regardless of whether the
|
||||
* vault's private flag or domain credentials have subsequently changed.
|
||||
*
|
||||
* ### Completion ordering
|
||||
*
|
||||
* `associateAsset` is called before `doWithdraw` to re-round stored `STNumber`
|
||||
* fields to the vault asset's precision. Per the `STTakesAsset` contract, this
|
||||
* must be the final write to the vault SLE. `doWithdraw` then handles trust
|
||||
* line or MPToken creation for the destination (under `WeakAuth`) and executes
|
||||
* the final `accountSend` from the vault pseudo-account to the destination.
|
||||
*
|
||||
* @return `tesSUCCESS`, `tecPRECISION_LOSS`, `tecPATH_DRY`,
|
||||
* `tecINSUFFICIENT_FUNDS`, `tefINTERNAL`, or a code from `accountSend`,
|
||||
* `removeEmptyHolding`, or `doWithdraw`.
|
||||
*/
|
||||
TER
|
||||
VaultWithdraw::doApply()
|
||||
{
|
||||
@@ -164,11 +277,6 @@ VaultWithdraw::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Note, we intentionally do not check lsfVaultPrivate flag on the Vault. If
|
||||
// you have a share in the vault, it means you were at some point authorized
|
||||
// to deposit into it, and this means you are also indefinitely authorized
|
||||
// to withdraw from it.
|
||||
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
Asset const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
@@ -179,7 +287,6 @@ VaultWithdraw::doApply()
|
||||
{
|
||||
if (amount.asset() == vaultAsset)
|
||||
{
|
||||
// Fixed assets, variable shares.
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(vault, sleIssuance, amount);
|
||||
if (!maybeShares)
|
||||
@@ -196,7 +303,6 @@ VaultWithdraw::doApply()
|
||||
}
|
||||
else if (amount.asset() == share)
|
||||
{
|
||||
// Fixed shares, variable assets.
|
||||
sharesRedeemed = amount;
|
||||
auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed);
|
||||
if (!maybeAssets)
|
||||
@@ -236,9 +342,6 @@ VaultWithdraw::doApply()
|
||||
lossUnrealized <= (assetsTotal - assetsAvailable),
|
||||
"xrpl::VaultWithdraw::doApply : loss and assets do balance");
|
||||
|
||||
// The vault must have enough assets on hand. The vault may hold assets
|
||||
// that it has already pledged. That is why we look at AssetAvailable
|
||||
// instead of the pseudo-account balance.
|
||||
if (*assetsAvailable < assetsWithdrawn)
|
||||
{
|
||||
JLOG(j_.debug()) << "VaultWithdraw: vault doesn't hold enough assets";
|
||||
@@ -250,15 +353,12 @@ VaultWithdraw::doApply()
|
||||
view().update(vault);
|
||||
|
||||
auto const& vaultAccount = vault->at(sfAccount);
|
||||
// Transfer shares from depositor to vault.
|
||||
if (auto const ter =
|
||||
accountSend(view(), account_, vaultAccount, sharesRedeemed, j_, WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Try to remove MPToken for shares, if the account balance is zero. Vault
|
||||
// pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
|
||||
// Keep MPToken if holder is the vault owner.
|
||||
// Vault pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
|
||||
if (account_ != vault->at(sfOwner))
|
||||
{
|
||||
if (auto const ter = removeEmptyHolding(view(), account_, sharesRedeemed.asset(), j_);
|
||||
@@ -280,7 +380,6 @@ VaultWithdraw::doApply()
|
||||
return ter;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// else quietly ignore, account balance is not zero
|
||||
}
|
||||
|
||||
auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_);
|
||||
|
||||
@@ -13,16 +13,50 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// {
|
||||
// feature : <feature>
|
||||
// vetoed : true/false
|
||||
// }
|
||||
/** RPC handler for the `feature` command.
|
||||
*
|
||||
* Exposes the XRPL amendment system to external clients and privileged
|
||||
* operators. Operates in two modes depending on whether the `feature`
|
||||
* parameter is present.
|
||||
*
|
||||
* **List mode** (no `feature` param): returns a `features` object keyed by
|
||||
* amendment hash, sourced from `AmendmentTable::getJson(isAdmin)` and
|
||||
* augmented with majority timestamps read directly from the validated ledger's
|
||||
* amendment SLE. Reading majority data from the ledger (rather than relying
|
||||
* solely on the `AmendmentTable`'s internal vote tracking) is authoritative
|
||||
* because the two can briefly diverge.
|
||||
*
|
||||
* **Single-feature mode** (with `feature` param): resolves the identifier
|
||||
* first as a human-readable amendment name via `AmendmentTable::find()`,
|
||||
* then falls back to parsing it as a 256-bit hex hash. Returns
|
||||
* `rpcBAD_FEATURE` if neither succeeds, or if the resolved hash is not
|
||||
* known to the amendment table.
|
||||
*
|
||||
* **Veto control** (admin only): when `vetoed` is also present, calls
|
||||
* `AmendmentTable::veto()` or `AmendmentTable::unVeto()` to control whether
|
||||
* the local validator votes for the amendment. Non-admin callers who supply
|
||||
* `vetoed` receive `rpcNO_PERMISSION`. The handler is registered as
|
||||
* `Role::USER`, so any client can invoke the read paths; only the mutation
|
||||
* path is gated here by the explicit admin check.
|
||||
*
|
||||
* Majority timestamps in the response are raw `NetClock::time_point` ticks
|
||||
* — seconds since the Ripple epoch (2000-01-01 00:00:00 UTC), **not** Unix
|
||||
* time. Callers must add the 946684800-second offset to convert to Unix time.
|
||||
*
|
||||
* @param context The RPC dispatch context, including request params, role,
|
||||
* app reference, and ledger master.
|
||||
* @return A JSON object with a top-level `features` map (list mode) or a
|
||||
* single-amendment object keyed by hash (single-feature mode), or an
|
||||
* RPC error value on invalid input or insufficient permissions.
|
||||
* @note The `feature` parameter must be a JSON string; passing a number,
|
||||
* boolean, null, object, or array returns `rpcINVALID_PARAMS`.
|
||||
* @see AmendmentTable, getMajorityAmendments
|
||||
*/
|
||||
json::Value
|
||||
doFeature(RPC::JsonContext& context)
|
||||
{
|
||||
if (context.params.isMember(jss::feature))
|
||||
{
|
||||
// ensure that the `feature` param is a string
|
||||
if (!context.params[jss::feature].isString())
|
||||
{
|
||||
return rpcError(RpcInvalidParams);
|
||||
@@ -30,7 +64,6 @@ doFeature(RPC::JsonContext& context)
|
||||
}
|
||||
|
||||
bool const isAdmin = context.role == Role::ADMIN;
|
||||
// Get majority amendment status
|
||||
majorityAmendments_t majorities;
|
||||
|
||||
if (auto const valLedger = context.ledgerMaster.getValidatedLedger())
|
||||
@@ -54,8 +87,6 @@ doFeature(RPC::JsonContext& context)
|
||||
|
||||
auto feature = table.find(context.params[jss::feature].asString());
|
||||
|
||||
// If the feature is not found by name, try to parse the `feature` param as
|
||||
// a feature ID. If that fails, return an error.
|
||||
if (!feature && !feature.parseHex(context.params[jss::feature].asString()))
|
||||
return rpcError(RpcBadFeature);
|
||||
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/** @file
|
||||
* Handler for the `fee` RPC command.
|
||||
*
|
||||
* Exposes the node's current transaction fee environment to any connected
|
||||
* client (`Role::USER`). The response is assembled entirely by
|
||||
* `TxQ::doRPC`; this translation unit contributes only the dispatch point
|
||||
* and a defensive type check on the return value.
|
||||
*
|
||||
* Registration in `Handler.cpp`:
|
||||
* @code
|
||||
* {"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER}
|
||||
* @endcode
|
||||
* The `NEEDS_CURRENT_LEDGER` condition means the framework guarantees that
|
||||
* the open ledger exists before `doFee` is ever called, making the
|
||||
* null-view guard inside `TxQ::doRPC` unreachable from this handler.
|
||||
*/
|
||||
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/TxQ.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
@@ -7,6 +24,28 @@
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Return the node's current transaction fee schedule.
|
||||
*
|
||||
* Delegates entirely to `TxQ::doRPC`, which queries the open ledger via
|
||||
* `TxQ::getMetrics` and returns a JSON object with two sub-objects:
|
||||
*
|
||||
* - `levels` — fee values in normalized fee-level units:
|
||||
* `reference_level`, `minimum_level`, `median_level`, `open_ledger_level`.
|
||||
* - `drops` — absolute amounts in XRP drops:
|
||||
* `base_fee`, `median_fee`, `minimum_fee`, `open_ledger_fee`.
|
||||
*
|
||||
* Top-level fields include `ledger_current_index`, `current_ledger_size`,
|
||||
* `expected_ledger_size`, `current_queue_size`, and (when a queue size cap
|
||||
* is configured) `max_queue_size`.
|
||||
*
|
||||
* @param context RPC dispatch context; must have a current open ledger
|
||||
* (enforced by the `NEEDS_CURRENT_LEDGER` handler condition).
|
||||
* @return JSON object with `levels` and `drops` sub-objects on success.
|
||||
* The failure branch — triggered only when `TxQ::doRPC` returns a
|
||||
* non-object value, which cannot happen via this handler — is excluded
|
||||
* from coverage (`LCOV_EXCL`) and marked `UNREACHABLE`.
|
||||
*/
|
||||
json::Value
|
||||
doFee(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
// Copyright (c) 2019 Dev Null Productions
|
||||
|
||||
/** @file
|
||||
* Implements the `manifest` RPC handler, which exposes a read-only view of
|
||||
* the validator manifest cache over RPC.
|
||||
*
|
||||
* A manifest is a signed certificate binding a long-lived validator master key
|
||||
* to a rotating ephemeral signing key. Clients can supply either key type;
|
||||
* the handler resolves whichever was provided to the canonical master key
|
||||
* before querying the cache.
|
||||
*/
|
||||
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
|
||||
@@ -11,6 +21,36 @@
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
|
||||
namespace xrpl {
|
||||
/** Look up and return the manifest for a validator public key.
|
||||
*
|
||||
* Accepts either a master key or an active ephemeral signing key in
|
||||
* `public_key`. The input is resolved to a master key via
|
||||
* `ManifestCache::getMasterKey`, which returns the input unchanged when no
|
||||
* reverse ephemeral→master mapping exists, making the lookup uniform for
|
||||
* both key types.
|
||||
*
|
||||
* If the resolved master key has no active signing key in the cache (manifest
|
||||
* absent or revoked), the response contains only the `requested` field and no
|
||||
* error is raised — absence is not treated as a failure.
|
||||
*
|
||||
* On success the response contains:
|
||||
* - `requested` — the caller's original input, always present.
|
||||
* - `manifest` — Base64-encoded raw manifest bytes, when available.
|
||||
* - `details` — object with `master_key` and `ephemeral_key` (Base58),
|
||||
* and optionally `seq` and `domain` when recorded.
|
||||
*
|
||||
* @param context RPC dispatch context carrying `params` and the application
|
||||
* reference used to reach `ManifestCache`.
|
||||
* @return JSON response object. Returns a `missingFieldError` if `public_key`
|
||||
* is absent, `rpcINVALID_PARAMS` if the value is not a valid Base58
|
||||
* NodePublic key, or a sparse response (only `requested`) when the
|
||||
* manifest is not found.
|
||||
* @note All `ManifestCache` methods called here are protected by a
|
||||
* `std::shared_mutex` and are safe for concurrent invocation. This
|
||||
* handler holds no locks and carries no mutable state.
|
||||
* @see doValidatorInfo for the analogous handler that looks up the node's own
|
||||
* configured validation key without accepting external input.
|
||||
*/
|
||||
json::Value
|
||||
doManifest(RPC::JsonContext& context)
|
||||
{
|
||||
@@ -31,14 +71,10 @@ doManifest(RPC::JsonContext& context)
|
||||
return ret;
|
||||
}
|
||||
|
||||
// first attempt to use params as ephemeral key,
|
||||
// if this lookup succeeds master key will be returned,
|
||||
// else an unseated optional is returned
|
||||
auto const mk = context.app.getValidatorManifests().getMasterKey(*pk);
|
||||
|
||||
auto const ek = context.app.getValidatorManifests().getSigningKey(mk);
|
||||
|
||||
// if ephemeral key not found, we don't have specified manifest
|
||||
if (!ek)
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* Implements the `server_definitions` RPC handler, which exposes the
|
||||
* complete XRPL protocol schema as a static JSON payload.
|
||||
*
|
||||
* The payload is assembled once at startup (Meyers singleton), hashed via
|
||||
* SHA-512-half for cache validation, and then served read-only for the
|
||||
* lifetime of the process. Clients that already hold a matching hash
|
||||
* receive only the hash in the response, avoiding re-transfer of the
|
||||
* large definitions object on every call.
|
||||
*/
|
||||
#include <xrpld/rpc/handlers/server_info/ServerDefinitions.h>
|
||||
|
||||
#include <xrpld/rpc/Context.h>
|
||||
@@ -29,25 +39,75 @@ namespace xrpl {
|
||||
|
||||
namespace detail {
|
||||
|
||||
/** Immutable protocol schema snapshot used by the `server_definitions` RPC endpoint.
|
||||
*
|
||||
* Built exactly once at first use (Meyers singleton via `getDefinitions()`).
|
||||
* Populates nine top-level JSON sections — TYPES, LEDGER_ENTRY_TYPES,
|
||||
* TRANSACTION_TYPES, FIELDS, TRANSACTION_RESULTS, TRANSACTION_FORMATS,
|
||||
* LEDGER_ENTRY_FORMATS, TRANSACTION_FLAGS, LEDGER_ENTRY_FLAGS, and
|
||||
* ACCOUNT_SET_FLAGS — then computes a SHA-512-half fingerprint of the
|
||||
* serialized payload for client-side cache invalidation.
|
||||
*
|
||||
* @note Thread-safe after construction; all public methods are read-only.
|
||||
*/
|
||||
class ServerDefinitions
|
||||
{
|
||||
private:
|
||||
/** Convert a raw `STI_`-stripped type name to the client-facing naming convention.
|
||||
*
|
||||
* Applies the following rules in order:
|
||||
* - Names containing `UINT` with a fixed bit-width (128, 160, 192, 256, 384, 512)
|
||||
* become `Hash<N>` (e.g., `UINT256` → `Hash256`), reflecting that these
|
||||
* fixed-width types are used as cryptographic digests, not arithmetic integers.
|
||||
* - Other `UINT` names become `UInt<N>` (e.g., `UINT32` → `UInt32`).
|
||||
* - A fixed lookup table handles special cases: `VL` → `Blob`,
|
||||
* `ACCOUNT` → `AccountID`, `OBJECT` → `STObject`, `ARRAY` → `STArray`, etc.
|
||||
* - All remaining names are converted from SCREAMING_SNAKE_CASE to CamelCase.
|
||||
*
|
||||
* @param inp Raw type name with the `STI_` prefix already removed.
|
||||
* @return Client-facing type name string.
|
||||
*/
|
||||
static std::string
|
||||
// translate e.g. STI_LEDGERENTRY to LedgerEntry
|
||||
translate(std::string const& inp);
|
||||
|
||||
/** SHA-512-half fingerprint of the serialized definitions payload. */
|
||||
uint256 defsHash_;
|
||||
|
||||
/** The complete protocol schema as a JSON object. */
|
||||
json::Value defs_;
|
||||
|
||||
public:
|
||||
/** Construct and populate the full protocol schema.
|
||||
*
|
||||
* Iterates `SField::getKnownCodeToField()`, `LedgerFormats`, `TxFormats`,
|
||||
* `transResults()`, `getAllTxFlags()`, `getAllLedgerFlags()`, and
|
||||
* `getAsfFlagMap()` to build all nine sections. After all sections are
|
||||
* assembled, serializes the object via `Json::FastWriter` and stores the
|
||||
* SHA-512-half of the result in both `defsHash_` and `defs_[jss::hash]`.
|
||||
*/
|
||||
ServerDefinitions();
|
||||
|
||||
/** Return true if @p hash matches the current definitions fingerprint.
|
||||
*
|
||||
* Used by `doServerDefinitions` to implement the bandwidth-saving
|
||||
* short-circuit: when the caller already holds an up-to-date copy,
|
||||
* only the hash is returned rather than the full payload.
|
||||
*
|
||||
* @param hash The `uint256` hash supplied by the caller.
|
||||
* @return `true` if @p hash equals `defsHash_`; `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
hashMatches(uint256 hash) const
|
||||
{
|
||||
return defsHash_ == hash;
|
||||
}
|
||||
|
||||
/** Return a const reference to the complete definitions JSON object.
|
||||
*
|
||||
* The returned reference is valid for the lifetime of the process.
|
||||
*
|
||||
* @return The fully populated protocol schema, including the `hash` field.
|
||||
*/
|
||||
[[nodiscard]] json::Value const&
|
||||
get() const
|
||||
{
|
||||
@@ -64,7 +124,7 @@ ServerDefinitions::translate(std::string const& inp)
|
||||
return out;
|
||||
};
|
||||
|
||||
// TODO: use string::contains with C++23
|
||||
// TODO: use string::contains with C++23 once the minimum language version is raised.
|
||||
auto contains = [&](std::string_view s) -> bool { return inp.find(s) != std::string::npos; };
|
||||
|
||||
if (contains("UINT"))
|
||||
@@ -124,7 +184,9 @@ ServerDefinitions::translate(std::string const& inp)
|
||||
|
||||
ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
{
|
||||
// populate SerializedTypeID names and values
|
||||
// --- TYPES ---
|
||||
// Map client-facing type names to their SerializedTypeID integer codes.
|
||||
// typeMap is retained for reverse lookup when setting each field's `type` string below.
|
||||
defs_[jss::TYPES] = json::ValueType::Object;
|
||||
|
||||
defs_[jss::TYPES]["Done"] = -1;
|
||||
@@ -136,7 +198,8 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
typeMap[typeValue] = typeName;
|
||||
}
|
||||
|
||||
// populate LedgerEntryType names and values
|
||||
// --- LEDGER_ENTRY_TYPES ---
|
||||
// Seed with sentinel Invalid = -1; then append all registered ledger entry names.
|
||||
defs_[jss::LEDGER_ENTRY_TYPES] = json::ValueType::Object;
|
||||
defs_[jss::LEDGER_ENTRY_TYPES][jss::Invalid] = -1;
|
||||
|
||||
@@ -145,7 +208,9 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::LEDGER_ENTRY_TYPES][f.getName()] = f.getType();
|
||||
}
|
||||
|
||||
// populate SField serialization data
|
||||
// --- FIELDS ---
|
||||
// Six entries are hard-coded before the registry loop because they either have no
|
||||
// canonical SField entry or require explicit control over their serialization attributes.
|
||||
defs_[jss::FIELDS] = json::ValueType::Array;
|
||||
|
||||
uint32_t i = 0;
|
||||
@@ -189,6 +254,8 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::FIELDS][i++] = a;
|
||||
}
|
||||
|
||||
// taker_gets_funded / taker_pays_funded: synthetic DEX fields (nth 258/259) that exist
|
||||
// in the offer-crossing path but are not persisted or signed.
|
||||
{
|
||||
json::Value a = json::ValueType::Array;
|
||||
a[0U] = "taker_gets_funded";
|
||||
@@ -215,7 +282,8 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::FIELDS][i++] = a;
|
||||
}
|
||||
|
||||
// copy into a sorted map to ensure deterministic output order (sorted by fieldCode)
|
||||
// Sort by fieldCode for deterministic output; the unordered source map gives no ordering
|
||||
// guarantee across platforms.
|
||||
static std::map<int, SField const*> const kSORTED_FIELDS(
|
||||
xrpl::SField::getKnownCodeToField().begin(), xrpl::SField::getKnownCodeToField().end());
|
||||
|
||||
@@ -230,21 +298,21 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
|
||||
innerObj[jss::nth] = field->fieldValue;
|
||||
|
||||
// whether the field is variable-length encoded this means that the length is included
|
||||
// before the content
|
||||
// VL-encoded types encode their byte length as a prefix in the wire format.
|
||||
// Only three types qualify: Blob (7), AccountID (8), Vector256 (19).
|
||||
innerObj[jss::isVLEncoded] =
|
||||
(type == STI_VL || type == STI_ACCOUNT || type == STI_VECTOR256);
|
||||
static_assert(
|
||||
STI_VL == 7U && STI_ACCOUNT == 8U && STI_VECTOR256 == 19U,
|
||||
"STI_VL, STI_ACCOUNT, STI_VECTOR256 must be 7, 8, 19 respectively");
|
||||
|
||||
// whether the field is included in serialization
|
||||
// Container pseudo-types (type >= 10000: STI_TRANSACTION, STI_LEDGERENTRY,
|
||||
// STI_VALIDATION, STI_METADATA) and the computed fields `hash` and `index`
|
||||
// are not stored in serialized objects.
|
||||
innerObj[jss::isSerialized] =
|
||||
(type < 10000 && field->fieldName != "hash" &&
|
||||
field->fieldName !=
|
||||
"index"); // hash, index, TRANSACTION, LEDGER_ENTRY, VALIDATION, METADATA
|
||||
field->fieldName != "index");
|
||||
|
||||
// whether the field is included in serialization when signing
|
||||
innerObj[jss::isSigningField] = field->shouldInclude(false);
|
||||
|
||||
innerObj[jss::type] = typeMap[type];
|
||||
@@ -256,7 +324,8 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::FIELDS][i++] = innerArray;
|
||||
}
|
||||
|
||||
// populate TER code names and values
|
||||
// --- TRANSACTION_RESULTS ---
|
||||
// Maps every TER code name (e.g., "tesSUCCESS", "tecDIR_FULL") to its integer value.
|
||||
defs_[jss::TRANSACTION_RESULTS] = json::ValueType::Object;
|
||||
|
||||
for (auto const& [code, terInfo] : transResults())
|
||||
@@ -264,7 +333,8 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::TRANSACTION_RESULTS][terInfo.first] = code;
|
||||
}
|
||||
|
||||
// populate TxType names and values
|
||||
// --- TRANSACTION_TYPES ---
|
||||
// Seed with sentinel Invalid = -1; then append all registered transaction type names.
|
||||
defs_[jss::TRANSACTION_TYPES] = json::ValueType::Object;
|
||||
defs_[jss::TRANSACTION_TYPES][jss::Invalid] = -1;
|
||||
for (auto const& f : TxFormats::getInstance())
|
||||
@@ -272,7 +342,10 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::TRANSACTION_TYPES][f.getName()] = f.getType();
|
||||
}
|
||||
|
||||
// populate TxFormats
|
||||
// --- TRANSACTION_FORMATS ---
|
||||
// Two-level schema: "common" lists fields shared by every transaction type; each
|
||||
// type-specific key lists only the fields not already in "common". The txCommonFields
|
||||
// set drives the skip check in the per-type loop.
|
||||
defs_[jss::TRANSACTION_FORMATS] = json::ValueType::Object;
|
||||
|
||||
defs_[jss::TRANSACTION_FORMATS][jss::common] = json::ValueType::Array;
|
||||
@@ -302,7 +375,8 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::TRANSACTION_FORMATS][format.getName()] = templateArray;
|
||||
}
|
||||
|
||||
// populate LedgerFormats
|
||||
// --- LEDGER_ENTRY_FORMATS ---
|
||||
// Same two-level common/per-type pattern as TRANSACTION_FORMATS.
|
||||
defs_[jss::LEDGER_ENTRY_FORMATS] = json::ValueType::Object;
|
||||
defs_[jss::LEDGER_ENTRY_FORMATS][jss::common] = json::ValueType::Array;
|
||||
auto ledgerCommonFields = std::set<std::string>();
|
||||
@@ -330,6 +404,9 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::LEDGER_ENTRY_FORMATS][format.getName()] = templateArray;
|
||||
}
|
||||
|
||||
// --- TRANSACTION_FLAGS / LEDGER_ENTRY_FLAGS ---
|
||||
// Both sourced from X-macro-driven Meyers singletons; keyed by type name
|
||||
// with a "universal" entry for globally applicable flags.
|
||||
defs_[jss::TRANSACTION_FLAGS] = json::ValueType::Object;
|
||||
for (auto const& [name, value] : getAllTxFlags())
|
||||
{
|
||||
@@ -352,13 +429,15 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
defs_[jss::LEDGER_ENTRY_FLAGS][name] = ledgerObj;
|
||||
}
|
||||
|
||||
// --- ACCOUNT_SET_FLAGS ---
|
||||
defs_[jss::ACCOUNT_SET_FLAGS] = json::ValueType::Object;
|
||||
for (auto const& [name, value] : getAsfFlagMap())
|
||||
{
|
||||
defs_[jss::ACCOUNT_SET_FLAGS][name] = value;
|
||||
}
|
||||
|
||||
// generate hash
|
||||
// Fingerprint the complete payload before embedding the hash field.
|
||||
// The hash covers all nine sections but not the hash field itself.
|
||||
{
|
||||
std::string const out = json::FastWriter().write(defs_);
|
||||
defsHash_ = xrpl::sha512Half(xrpl::Slice{out.data(), out.size()});
|
||||
@@ -366,6 +445,12 @@ ServerDefinitions::ServerDefinitions() : defs_{json::ValueType::Object}
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the process-lifetime singleton `ServerDefinitions` instance.
|
||||
*
|
||||
* Constructed on first call via C++11 thread-safe static initialization.
|
||||
*
|
||||
* @return Const reference to the singleton; valid for the lifetime of the process.
|
||||
*/
|
||||
ServerDefinitions const&
|
||||
getDefinitions()
|
||||
{
|
||||
@@ -375,12 +460,37 @@ getDefinitions()
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/** Return the complete protocol schema JSON built by the `server_definitions` singleton.
|
||||
*
|
||||
* Provides direct access to the definitions payload for non-RPC contexts
|
||||
* (e.g., the `--definitions` CLI flag in `Main.cpp`). The returned reference
|
||||
* is valid for the lifetime of the process.
|
||||
*
|
||||
* @return Const reference to the fully populated definitions JSON object,
|
||||
* including the `hash` field.
|
||||
* @see doServerDefinitions
|
||||
*/
|
||||
json::Value const&
|
||||
getServerDefinitionsJson()
|
||||
{
|
||||
return detail::getDefinitions().get();
|
||||
}
|
||||
|
||||
/** Handle the `server_definitions` RPC request.
|
||||
*
|
||||
* Returns the complete XRPL protocol schema assembled at startup. If the
|
||||
* caller supplies a `hash` parameter matching the current definitions
|
||||
* fingerprint, only `{"hash": "..."}` is returned to save bandwidth. A
|
||||
* hash mismatch (or no hash) causes the full payload to be returned so the
|
||||
* client can update its cached copy.
|
||||
*
|
||||
* @param context RPC dispatch context; `context.params` may contain an
|
||||
* optional `hash` string field (64 hex characters).
|
||||
* @return The full definitions JSON object on a cache miss, or a
|
||||
* single-key `{"hash": "..."}` object on a cache hit. Returns an
|
||||
* `invalidFieldError` response if `hash` is present but not a valid
|
||||
* 64-hex-character `uint256`.
|
||||
*/
|
||||
json::Value
|
||||
doServerDefinitions(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
/** @file
|
||||
* Public interface for the `server_definitions` RPC endpoint.
|
||||
*
|
||||
* Exposes a single function that returns the complete XRPL protocol schema
|
||||
* as a static, process-lifetime JSON object. The payload is built once
|
||||
* (Meyers singleton) and includes a SHA-512-half fingerprint for
|
||||
* client-side cache validation.
|
||||
*
|
||||
* @see ServerDefinitions.cpp for the full construction details.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Return the complete XRPL protocol schema assembled at process startup.
|
||||
*
|
||||
* The returned object contains eleven top-level keys:
|
||||
* `TYPES`, `LEDGER_ENTRY_TYPES`, `TRANSACTION_TYPES`, `FIELDS`,
|
||||
* `TRANSACTION_RESULTS`, `TRANSACTION_FORMATS`, `LEDGER_ENTRY_FORMATS`,
|
||||
* `TRANSACTION_FLAGS`, `LEDGER_ENTRY_FLAGS`, `ACCOUNT_SET_FLAGS`, and
|
||||
* `hash` (a SHA-512-half fingerprint of the other ten keys).
|
||||
*
|
||||
* This function is the non-RPC access path to the same singleton used by
|
||||
* the `server_definitions` JSON-RPC handler — for example, it is called by
|
||||
* the `--definitions` CLI flag in `Main.cpp` to dump the schema to stdout.
|
||||
*
|
||||
* @return Const reference to the fully populated definitions JSON object,
|
||||
* valid for the lifetime of the process.
|
||||
* @note The reference is safe to capture and hold indefinitely; the
|
||||
* underlying singleton is never destroyed.
|
||||
*/
|
||||
json::Value const&
|
||||
getServerDefinitionsJson();
|
||||
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
/** @file
|
||||
* Implements the `server_info` JSON-RPC handler (`doServerInfo`).
|
||||
*
|
||||
* The handler is intentionally thin: it constructs an empty JSON object,
|
||||
* delegates all data assembly to `NetworkOPs::getServerInfo()`, and wraps
|
||||
* the result under the `jss::info` key. All substantive logic — gathering
|
||||
* warnings, ledger positions, peer counts, consensus state, and optional
|
||||
* performance counters — lives in `NetworkOPsImp::getServerInfo()`.
|
||||
*
|
||||
* The `human = true` flag passed here is the sole behavioral difference
|
||||
* between this handler and its sibling `ServerState.cpp` (`human = false`).
|
||||
* When `human` is true, `getServerInfo` includes human-readable fields such
|
||||
* as `hostid` and formats numeric quantities for readability; when false, it
|
||||
* emits compact machine-friendly representations. Both handlers use `jss::info`
|
||||
* vs. `jss::state` as their respective response keys over the same data source.
|
||||
*
|
||||
* Role gating (`context.role == Role::ADMIN`) is forwarded directly to
|
||||
* `getServerInfo`, which uses it to suppress sensitive fields (amendment-majority
|
||||
* warnings, node configuration) for non-admin callers. Authentication and role
|
||||
* resolution are performed upstream by the RPC framework before this handler
|
||||
* is ever invoked.
|
||||
*
|
||||
* The `counters` parameter is guarded with `isMember` before `asBool()` to
|
||||
* avoid undefined behavior on a missing JSON field. If the field is present
|
||||
* but not boolean, `asBool()` raises `Json::LogicError` — acceptable since
|
||||
* such a request is malformed.
|
||||
*
|
||||
* @see doServerState Sibling handler emitting machine-friendly (`human=false`)
|
||||
* representation of the same data under `jss::state`.
|
||||
* @see NetworkOPs::getServerInfo The function that assembles the full response.
|
||||
*/
|
||||
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
/** @file
|
||||
* Implements the `server_state` JSON-RPC handler (`doServerState`).
|
||||
*
|
||||
* This handler is the machine-readable counterpart to `ServerInfo.cpp`.
|
||||
* Both handlers delegate all data assembly to `NetworkOPs::getServerInfo()`
|
||||
* and differ only in the `human` flag passed to that function:
|
||||
* - `doServerState` passes `human = false` — XRP amounts are returned as
|
||||
* raw integer drop counts, fees as integer basis points, and timestamps as
|
||||
* Unix epoch integers, suited for programmatic clients and monitoring tools.
|
||||
* - `doServerInfo` passes `human = true` — the same fields are formatted with
|
||||
* units and floating-point representations for human operators.
|
||||
*
|
||||
* The response is wrapped under the `jss::state` key; the parallel sibling
|
||||
* uses `jss::info`. Both keys are compile-time constants from the `jss`
|
||||
* namespace, preventing typo-class bugs at compile time.
|
||||
*
|
||||
* Role gating (`context.role == Role::ADMIN`) is resolved upstream by the
|
||||
* RPC framework before this handler is invoked; passing the boolean directly
|
||||
* means no trust decisions are made here. Admin callers receive additional
|
||||
* fields such as peer counts, internal queue depths, and load factor details.
|
||||
*
|
||||
* The `counters` parameter opt-in is guarded with `isMember` before
|
||||
* `asBool()` to avoid a `Json::LogicError` on a missing field. A malformed
|
||||
* (non-boolean) value will raise `Json::LogicError`; an absent or falsy
|
||||
* value silently opts out via short-circuit `&&`.
|
||||
*
|
||||
* @see doServerInfo Sibling handler emitting human-readable (`human=true`)
|
||||
* representation of the same data under `jss::info`.
|
||||
* @see NetworkOPs::getServerInfo The function that assembles the full response.
|
||||
*/
|
||||
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
|
||||
@@ -7,6 +38,19 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Handle the `server_state` RPC command.
|
||||
*
|
||||
* Returns machine-readable server status by delegating to
|
||||
* `NetworkOPs::getServerInfo` with `human = false`. Numeric quantities
|
||||
* (XRP amounts, fees, timestamps) are emitted as raw integers rather than
|
||||
* human-friendly strings. The result is nested under the `jss::state` key
|
||||
* in the returned JSON object.
|
||||
*
|
||||
* @param context RPC dispatch context carrying the JSON params, resolved
|
||||
* role, and references to application subsystems.
|
||||
* @return JSON object with a single `"state"` key whose value is the full
|
||||
* server status payload from `NetworkOPs::getServerInfo`.
|
||||
*/
|
||||
json::Value
|
||||
doServerState(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
@@ -4,20 +4,73 @@
|
||||
|
||||
namespace xrpl::RPC {
|
||||
|
||||
/** New-style class-based handler for the `version` RPC endpoint.
|
||||
*
|
||||
* Allows API clients to discover which API versions a node supports before
|
||||
* making substantive requests. One of only two class-based ("new-style")
|
||||
* handlers in the codebase — the other is `LedgerHandler`. All other RPC
|
||||
* commands are registered as bare function pointers in the legacy
|
||||
* `handlerArray` table in `Handler.cpp`. This class is registered via
|
||||
* `HandlerTable::addHandler<VersionHandler>()` during singleton construction.
|
||||
*
|
||||
* The new-style handler contract: expose a constructor taking `JsonContext&`,
|
||||
* a static `check()` method, a `writeResult()` method, and the static
|
||||
* constexpr metadata fields (`name`, `minApiVer`, `maxApiVer`, `role`,
|
||||
* `condition`). The `handlerFrom<T>()` template bridges this shape into the
|
||||
* `Handler` struct used by the dispatch infrastructure.
|
||||
*
|
||||
* `maxApiVer` is deliberately set to `kAPI_MAXIMUM_VALID_VERSION` (the beta
|
||||
* ceiling) rather than `kAPI_MAXIMUM_SUPPORTED_VERSION`. This allows clients
|
||||
* running against a beta-enabled node to reach this endpoint at API version 3
|
||||
* and discover that the beta version is available — if the cap were lower, the
|
||||
* `HandlerTable` lookup at version 3 would return `nullptr` before the client
|
||||
* could query capabilities.
|
||||
*
|
||||
* @note `role = USER` and `condition = NoCondition`: no admin credentials are
|
||||
* required and network-sync/ledger-availability checks are bypassed. A node
|
||||
* that is still syncing or amendment-blocked can still truthfully report the
|
||||
* API versions it supports.
|
||||
*
|
||||
* @see setVersion() in `ApiVersion.h` for the format of the emitted JSON.
|
||||
*/
|
||||
class VersionHandler
|
||||
{
|
||||
public:
|
||||
/** Construct the handler, capturing version context from the request.
|
||||
*
|
||||
* Only `apiVersion` and the `BETA_RPC_API` config flag are needed; all
|
||||
* other context fields are ignored by this handler.
|
||||
*
|
||||
* @param c The RPC dispatch context for the incoming request.
|
||||
*/
|
||||
explicit VersionHandler(JsonContext& c)
|
||||
: apiVersion_(c.apiVersion), betaEnabled_(c.app.config().BETA_RPC_API)
|
||||
{
|
||||
}
|
||||
|
||||
/** Validate the request prior to result generation.
|
||||
*
|
||||
* Always succeeds — asking "what versions do you support?" is valid
|
||||
* regardless of node state.
|
||||
*
|
||||
* @return `Status::kOK` unconditionally.
|
||||
*/
|
||||
static Status
|
||||
check()
|
||||
{
|
||||
return Status::kOK;
|
||||
}
|
||||
|
||||
/** Populate the response object with supported API version information.
|
||||
*
|
||||
* Delegates to `setVersion()`. For API version 1 callers, emits legacy
|
||||
* semver strings (`first`, `good`, `last` all `"1.0.0"`). For version 2+
|
||||
* callers, emits integer bounds: `first = kAPI_MINIMUM_SUPPORTED_VERSION`,
|
||||
* `last = kAPI_BETA_VERSION` if beta is enabled, otherwise
|
||||
* `kAPI_MAXIMUM_SUPPORTED_VERSION`.
|
||||
*
|
||||
* @param obj The JSON object into which the `version` sub-object is written.
|
||||
*/
|
||||
void
|
||||
writeResult(json::Value& obj) const
|
||||
{
|
||||
@@ -25,14 +78,24 @@ public:
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
/** RPC method name used for handler table registration and dispatch. */
|
||||
static constexpr char const* name = "version";
|
||||
|
||||
/** Lowest API version that routes to this handler (inclusive). */
|
||||
static constexpr unsigned minApiVer = RPC::kAPI_MINIMUM_SUPPORTED_VERSION;
|
||||
|
||||
/** Highest API version that routes to this handler (inclusive).
|
||||
*
|
||||
* Set to `kAPI_MAXIMUM_VALID_VERSION` (the beta ceiling) so that clients
|
||||
* can reach this discovery endpoint even when requesting at the beta
|
||||
* version level.
|
||||
*/
|
||||
static constexpr unsigned maxApiVer = RPC::kAPI_MAXIMUM_VALID_VERSION;
|
||||
|
||||
/** Minimum caller role; `USER` means no admin credentials are required. */
|
||||
static constexpr Role role = Role::USER;
|
||||
|
||||
/** Node-state prerequisite; `NoCondition` bypasses all sync/ledger checks. */
|
||||
static constexpr Condition condition = Condition::NoCondition;
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file
|
||||
* Implements the `subscribe` RPC/WebSocket command handler.
|
||||
*
|
||||
* Registers a client connection as a listener on named event streams, account
|
||||
* filters, or order books. The symmetric counterpart `doUnsubscribe` in
|
||||
* Unsubscribe.cpp tears down these registrations using identical parameter
|
||||
* parsing logic.
|
||||
*/
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
@@ -26,6 +35,56 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Register event subscriptions for a WebSocket or HTTP-callback client.
|
||||
*
|
||||
* Resolves the subscriber identity, then processes up to five independent
|
||||
* subscription categories from the request:
|
||||
*
|
||||
* - **streams** — named fan-out queues (`ledger`, `transactions`,
|
||||
* `transactions_proposed`, `validations`, `manifests`, `server`,
|
||||
* `book_changes`, `consensus`, `peer_status`).
|
||||
* - **accounts** — validated transaction notifications for specific accounts.
|
||||
* - **accounts_proposed** — proposed (pre-validation) transaction notifications.
|
||||
* - **books** — order book updates, with an optional current-state snapshot.
|
||||
* - **account_history_tx_stream** — experimental historical + live replay for
|
||||
* one account (requires `useTxTables()`).
|
||||
*
|
||||
* Subscriber identity is determined as follows: if the request contains a
|
||||
* `url` parameter an `RPCSub` HTTP-push object is created (or reused from the
|
||||
* server registry); otherwise `context.infoSub` — the WebSocket connection's
|
||||
* `InfoSub` — is used directly. The `url` path requires `Role::ADMIN`.
|
||||
*
|
||||
* For book subscriptions the handler registers the live listener *before*
|
||||
* reading the snapshot, eliminating the race where an offer update fires
|
||||
* between the snapshot read and the subscription registration. Clients may
|
||||
* therefore observe duplicates between the snapshot and the live stream, but
|
||||
* will never miss an update.
|
||||
*
|
||||
* Every validation step returns immediately on failure without partial
|
||||
* side effects, so the client either gets all requested subscriptions or
|
||||
* an error with no state changed.
|
||||
*
|
||||
* @param context RPC dispatch context carrying the parsed request params,
|
||||
* the `InfoSub` for the WebSocket connection (if any), the `NetworkOPs`
|
||||
* reference used for all sub/unsub calls, and the resource consumer.
|
||||
* @return JSON object with the initial subscription state; includes current
|
||||
* ledger info for `ledger` stream, offer snapshots under `offers`/
|
||||
* `bids`/`asks` when `snapshot` is requested, and a `warning` field
|
||||
* for `account_history_tx_stream`. Returns an `rpcError` object on any
|
||||
* validation failure.
|
||||
* @note Callers without an active WebSocket connection (plain HTTP) must
|
||||
* supply a `url` parameter; without either, `rpcINVALID_PARAMS` is
|
||||
* returned immediately.
|
||||
* @note `rt_transactions` is a deprecated alias for `transactions_proposed`;
|
||||
* `rt_accounts` is a deprecated alias for `accounts_proposed`;
|
||||
* `both_sides` is a deprecated alias for `both`; `state_now` is a
|
||||
* deprecated alias for `snapshot`. All aliases remain supported.
|
||||
* @note The `book_changes` stream has no unsubscribe path; once registered,
|
||||
* it cannot be removed for the lifetime of the connection.
|
||||
* @note Book snapshot reads charge `feeMediumBurdenRPC` to the resource
|
||||
* consumer, as does `account_history_tx_stream` subscription.
|
||||
* @see doUnsubscribe
|
||||
*/
|
||||
json::Value
|
||||
doSubscribe(RPC::JsonContext& context)
|
||||
{
|
||||
@@ -34,7 +93,6 @@ doSubscribe(RPC::JsonContext& context)
|
||||
|
||||
if (!context.infoSub && !context.params.isMember(jss::url))
|
||||
{
|
||||
// Must be a JSON-RPC call.
|
||||
JLOG(context.j.info()) << "doSubscribe: RPC subscribe requires a url";
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file
|
||||
* Implements the `unsubscribe` RPC/WebSocket command handler.
|
||||
*
|
||||
* Tears down active push subscriptions previously registered by
|
||||
* `doSubscribe`. The symmetric counterpart is Subscribe.cpp in the same
|
||||
* directory. All state removal is delegated to `NetworkOPs` via
|
||||
* `context.netOps`.
|
||||
*/
|
||||
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
@@ -17,6 +26,57 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Remove active push subscriptions for a WebSocket or HTTP-callback client.
|
||||
*
|
||||
* Resolves the subscriber identity, then processes up to five independent
|
||||
* subscription categories from the request, each gated by an `isMember`
|
||||
* check so clients need only include the categories they want removed:
|
||||
*
|
||||
* - **streams** — named fan-out queues: `server`, `ledger`, `manifests`,
|
||||
* `transactions`, `transactions_proposed` (alias: `rt_transactions`),
|
||||
* `validations`, `peer_status`, `consensus`. Note: `book_changes` has no
|
||||
* unsubscribe path; those subscriptions are implicitly torn down when the
|
||||
* connection closes.
|
||||
* - **accounts** — validated transaction notifications for specific accounts.
|
||||
* - **accounts_proposed** (alias: `rt_accounts`) — proposed transaction
|
||||
* notifications.
|
||||
* - **books** — order book updates. `both`/`both_sides` (deprecated) triggers
|
||||
* a second `unsubBook` call with the reversed book to match the symmetric
|
||||
* subscribe behavior. Unlike `doSubscribe`, no `isConsistent` check is
|
||||
* performed — a subscription must be removable regardless of current state.
|
||||
* - **account_history_tx_stream** — experimental historical + live replay;
|
||||
* the optional `stop_history_tx_only` boolean halts historical replay while
|
||||
* preserving the live account subscription.
|
||||
*
|
||||
* Subscriber identity is determined as follows: if the request carries a
|
||||
* `url` parameter, the existing `RPCSub` HTTP-push object is looked up by URL
|
||||
* (requires `Role::ADMIN`); if not found the handler returns an empty success
|
||||
* rather than an error — unsubscribing from a URL that was never registered
|
||||
* is a no-op. Otherwise `context.infoSub` (the live WebSocket `InfoSub`) is
|
||||
* used. A call with neither is rejected with `rpcINVALID_PARAMS`.
|
||||
*
|
||||
* When the `url` path is taken, `tryRemoveRpcSub` is called only *after* all
|
||||
* individual `unsub*` operations complete. This ensures the subscriber is
|
||||
* fully drained before the URL registration is cleaned up.
|
||||
*
|
||||
* @param context RPC dispatch context carrying the parsed request params,
|
||||
* the `InfoSub` for the WebSocket connection (if any), the `NetworkOPs`
|
||||
* reference used for all unsub calls, and the resolved `Role`.
|
||||
* @return Empty JSON object on success. Returns an `rpcError` object on the
|
||||
* first validation failure encountered:
|
||||
* `rpcINVALID_PARAMS` for structural problems,
|
||||
* `rpcNO_PERMISSION` for the admin-only URL path,
|
||||
* `rpcSTREAM_MALFORMED` for unknown or non-string stream names,
|
||||
* `rpcACT_MALFORMED` for unparseable account addresses,
|
||||
* `rpcBAD_MARKET` for degenerate (same-asset) order books,
|
||||
* `rpcDOMAIN_MALFORMED` for malformed domain hex.
|
||||
* @note `peer_status` requires `Role::ADMIN` to *subscribe* but no role
|
||||
* check to *unsubscribe* — removal is always safe regardless of
|
||||
* permission level.
|
||||
* @note `book_changes` cannot be unsubscribed via this handler; it is torn
|
||||
* down automatically on connection close.
|
||||
* @see doSubscribe
|
||||
*/
|
||||
json::Value
|
||||
doUnsubscribe(RPC::JsonContext& context)
|
||||
{
|
||||
@@ -26,7 +86,6 @@ doUnsubscribe(RPC::JsonContext& context)
|
||||
|
||||
if (!context.infoSub && !context.params.isMember(jss::url))
|
||||
{
|
||||
// Must be a JSON-RPC call.
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
@@ -75,7 +134,7 @@ doUnsubscribe(RPC::JsonContext& context)
|
||||
}
|
||||
else if (
|
||||
streamName == "transactions_proposed" ||
|
||||
streamName == "rt_transactions") // DEPRECATED
|
||||
streamName == "rt_transactions")
|
||||
{
|
||||
context.netOps.unsubRTTransactions(ispSub->getSeq());
|
||||
}
|
||||
@@ -100,7 +159,7 @@ doUnsubscribe(RPC::JsonContext& context)
|
||||
|
||||
auto accountsProposed = context.params.isMember(jss::accounts_proposed)
|
||||
? jss::accounts_proposed
|
||||
: jss::rt_accounts; // DEPRECATED
|
||||
: jss::rt_accounts;
|
||||
if (context.params.isMember(accountsProposed))
|
||||
{
|
||||
if (!context.params[accountsProposed].isArray())
|
||||
@@ -188,7 +247,6 @@ doUnsubscribe(RPC::JsonContext& context)
|
||||
|
||||
context.netOps.unsubBook(ispSub->getSeq(), book);
|
||||
|
||||
// both_sides is deprecated.
|
||||
if ((jv.isMember(jss::both) && jv[jss::both].asBool()) ||
|
||||
(jv.isMember(jss::both_sides) && jv[jss::both_sides].asBool()))
|
||||
{
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/** @file
|
||||
* Implements the `simulate` RPC command for dry-run transaction execution.
|
||||
*
|
||||
* A client can test a transaction against the current ledger state and
|
||||
* receive realistic metadata and a TER result code without broadcasting
|
||||
* to the network, spending XRP, or persisting any state change. The
|
||||
* mechanism is `TapDryRun`: after the full transactor execution path
|
||||
* (including metadata generation), the snapshot of the open ledger is
|
||||
* discarded rather than committed.
|
||||
*
|
||||
* @see doSimulate
|
||||
*/
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/Transaction.h>
|
||||
@@ -42,6 +55,24 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Derive the sequence number to use when autofilling a simulated transaction.
|
||||
*
|
||||
* Queries `TxQ::nextQueuableSeq()` against the live open ledger to obtain a
|
||||
* sequence number that is consistent with the account's current state. When
|
||||
* the transaction carries a `TicketSequence`, the wire-format rule requires
|
||||
* `Sequence = 0`, so that value is returned directly without a ledger lookup.
|
||||
*
|
||||
* @param txJson The transaction JSON object; must contain a valid `Account`
|
||||
* field.
|
||||
* @param context The RPC dispatch context providing access to the open ledger
|
||||
* and transaction queue.
|
||||
* @return The sequence number to insert, or an error JSON value if the source
|
||||
* account does not exist in the current ledger and no `TicketSequence` is
|
||||
* present, or if `Account` is malformed.
|
||||
* @note The account-not-found branch returns `rpcSRC_ACT_NOT_FOUND`, not a
|
||||
* fatal error — callers that supply `TicketSequence` bypass this check
|
||||
* entirely.
|
||||
*/
|
||||
static Expected<std::uint32_t, json::Value>
|
||||
getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context)
|
||||
{
|
||||
@@ -75,6 +106,27 @@ getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context)
|
||||
return hasTicketSeq ? 0 : context.app.getTxQ().nextQueuableSeq(sle).value();
|
||||
}
|
||||
|
||||
/** Autofill signature-related fields on a transaction or signer object.
|
||||
*
|
||||
* Sets `SigningPubKey` and `TxnSignature` to empty strings when absent —
|
||||
* the canonical XRPL representation of an unsigned transaction. The
|
||||
* `TapDryRun` flag tells `Transactor::checkSign` to skip validation when
|
||||
* these fields are empty.
|
||||
*
|
||||
* If the caller has pre-populated `TxnSignature` with a non-empty value
|
||||
* (on the top-level transaction or on any element of the `Signers` array),
|
||||
* the function returns `rpcTX_SIGNED` immediately. Simulate must not silently
|
||||
* execute an already-signed transaction, as that would mislead the caller
|
||||
* into believing signature validation passed.
|
||||
*
|
||||
* @param sigObject The transaction JSON object (or a `Signer` sub-object for
|
||||
* multi-sign) to inspect and mutate.
|
||||
* @return An error JSON value if the object is already signed or structurally
|
||||
* invalid; `std::nullopt` on success.
|
||||
* @note `Transactor.cpp` contains a defensive comment: "This code should
|
||||
* never be hit because it's checked in the `simulate` RPC" — this
|
||||
* function is the authoritative first line of defence.
|
||||
*/
|
||||
static std::optional<json::Value>
|
||||
autofillSignature(json::Value& sigObject)
|
||||
{
|
||||
@@ -128,6 +180,25 @@ autofillSignature(json::Value& sigObject)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/** Synthesize mandatory transaction fields that the caller omitted.
|
||||
*
|
||||
* Handles `Fee`, `Sequence`, `NetworkID`, and the signature fields in one
|
||||
* pass. Each field is only written when absent — explicit caller values are
|
||||
* preserved unchanged.
|
||||
*
|
||||
* Fee is computed last because `RPC::getCurrentNetworkFee()` may depend on
|
||||
* other fields (e.g. transaction type) already being set. Sequence is filled
|
||||
* via `getAutofillSequence()`, which queries the live open ledger.
|
||||
* `NetworkID` is injected only when the network ID exceeds 1024, per the
|
||||
* XRPL protocol rule that mainnet and other low-numbered networks omit the
|
||||
* field.
|
||||
*
|
||||
* @param txJson The transaction JSON object to mutate in place.
|
||||
* @param context RPC dispatch context providing access to fee-track, TxQ,
|
||||
* and the network-ID service.
|
||||
* @return An error JSON value if any autofill step fails (e.g. account not
|
||||
* found, transaction already signed); `std::nullopt` on success.
|
||||
*/
|
||||
static std::optional<json::Value>
|
||||
autofillTx(json::Value& txJson, RPC::JsonContext& context)
|
||||
{
|
||||
@@ -169,6 +240,22 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/** Extract and normalize the transaction from the RPC request parameters.
|
||||
*
|
||||
* Accepts either `tx_blob` (hex-encoded binary) or `tx_json` (structured
|
||||
* object), but not both. When `tx_blob` is supplied it is hex-decoded and
|
||||
* deserialized through `SerialIter` into an `STObject`, then immediately
|
||||
* re-serialized to JSON — this round-trip through canonical binary format
|
||||
* normalizes the input before downstream processing.
|
||||
*
|
||||
* @param params The top-level RPC request parameters object.
|
||||
* @return The extracted transaction as a JSON object, or a JSON error object
|
||||
* (containing an `error` key) if both or neither input key is present,
|
||||
* if `tx_blob` cannot be decoded or deserialized, or if the resulting
|
||||
* object is missing `TransactionType` or `Account`.
|
||||
* @note Mutual exclusion between `tx_blob` and `tx_json` is checked first
|
||||
* and is fatal — the caller must not supply both.
|
||||
*/
|
||||
static json::Value
|
||||
getTxJsonFromParams(json::Value const& params)
|
||||
{
|
||||
@@ -228,6 +315,31 @@ getTxJsonFromParams(json::Value const& params)
|
||||
return txJson;
|
||||
}
|
||||
|
||||
/** Execute a dry-run simulation of a transaction and assemble the response.
|
||||
*
|
||||
* Copies the current `OpenView` by value, then invokes `TxQ::apply()` with
|
||||
* `TapDryRun`. The flag causes the full transactor execution path — including
|
||||
* metadata generation — to run, but forces `applied = false` at the end of
|
||||
* `Transactor::apply()` so the ledger snapshot is never committed. The copy
|
||||
* is therefore discarded without side effects.
|
||||
*
|
||||
* The response always contains `applied`, `ledger_index`, `engine_result`,
|
||||
* `engine_result_code`, and `engine_result_message`. For `tesSUCCESS` the
|
||||
* message is overridden to "The simulated transaction would have been
|
||||
* applied." to prevent the generic success string from implying the
|
||||
* transaction was committed. When metadata is available it is serialized as
|
||||
* `meta_blob` (hex) or `meta` (JSON) according to the `binary` parameter;
|
||||
* JSON metadata receives the same three enrichment calls used in the live
|
||||
* transaction pipeline (`insertDeliveredAmount`, `insertNFTSyntheticInJson`,
|
||||
* `insertMPTokenIssuanceID`). The autofilled transaction is echoed back as
|
||||
* `tx_blob` or `tx_json`.
|
||||
*
|
||||
* @param context RPC dispatch context providing access to the open
|
||||
* ledger and transaction queue.
|
||||
* @param transaction The fully constructed and autofilled transaction to
|
||||
* simulate.
|
||||
* @return A JSON object containing the simulation result.
|
||||
*/
|
||||
static json::Value
|
||||
simulateTxn(RPC::JsonContext& context, std::shared_ptr<Transaction> transaction)
|
||||
{
|
||||
@@ -299,6 +411,39 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr<Transaction> transaction)
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
/** Handler for the `simulate` RPC command.
|
||||
*
|
||||
* Validates the request, autofills any absent mandatory fields, and runs the
|
||||
* transaction through the full engine execution path with `TapDryRun` so no
|
||||
* state is committed. The caller receives a realistic TER result code and
|
||||
* complete transaction metadata without broadcasting to the network or
|
||||
* spending XRP.
|
||||
*
|
||||
* Accepted parameters:
|
||||
* - `tx_blob` **XOR** `tx_json` — the transaction to simulate.
|
||||
* - `binary` (bool, optional) — if true, metadata and transaction are
|
||||
* returned as hex blobs (`meta_blob`, `tx_blob`) instead of JSON objects.
|
||||
*
|
||||
* Credential fields (`secret`, `seed`, `seed_hex`, `passphrase`) are
|
||||
* explicitly rejected before any other processing — simulate is not a signing
|
||||
* endpoint and must not be a channel for key material.
|
||||
*
|
||||
* `ttBATCH` transactions are rejected with `rpcNOT_IMPL`: batch execution
|
||||
* semantics cannot be faithfully replicated by the single-transaction
|
||||
* dry-run path.
|
||||
*
|
||||
* @param context RPC dispatch context for this call.
|
||||
* @return JSON object containing `applied`, `ledger_index`, `engine_result*`,
|
||||
* metadata (if generated), and the autofilled transaction. Returns a JSON
|
||||
* error object on invalid input or if the transaction cannot be
|
||||
* constructed.
|
||||
* @note Resource cost is `feeMediumBurdenRPC` — a full engine execution is
|
||||
* more expensive than a read-only query, though cheaper than a real
|
||||
* submission that triggers peer propagation and queue management.
|
||||
* @note The outer `try/catch` around `simulateTxn()` is marked `LCOV_EXCL`
|
||||
* and is not expected to fire under normal conditions; it exists solely
|
||||
* to prevent an unexpected exception from crashing the server.
|
||||
*/
|
||||
// {
|
||||
// tx_blob: <string> XOR tx_json: <object>,
|
||||
// binary: <bool>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/** @file
|
||||
* Implements the `submit` RPC command handler.
|
||||
*
|
||||
* Supports two mutually exclusive submission modes: a pre-signed binary blob
|
||||
* (`tx_blob`) and a server-side signing path (`tx_json` + `secret`). The blob
|
||||
* path is the production-safe mode; the signing path is deprecated.
|
||||
*/
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/misc/Transaction.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
@@ -26,6 +34,19 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Parse and validate the optional `fail_hard` request parameter.
|
||||
*
|
||||
* Converts the boolean `fail_hard` field from the request JSON into a
|
||||
* `NetworkOPs::FailHard` enum value. When `fail_hard` is true, the network
|
||||
* layer will reject the transaction outright if it cannot be applied to the
|
||||
* current open ledger, rather than queuing it for a future ledger. Omitting
|
||||
* the field produces lenient (non-hard-fail) behavior.
|
||||
*
|
||||
* @param context The RPC request context; `context.params` is inspected for
|
||||
* `jss::fail_hard`.
|
||||
* @return The resolved `NetworkOPs::FailHard` value on success, or an error
|
||||
* JSON value if `fail_hard` is present but not a boolean.
|
||||
*/
|
||||
static Expected<NetworkOPs::FailHard, json::Value>
|
||||
getFailHard(RPC::JsonContext const& context)
|
||||
{
|
||||
@@ -37,10 +58,41 @@ getFailHard(RPC::JsonContext const& context)
|
||||
context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool());
|
||||
}
|
||||
|
||||
// {
|
||||
// tx_blob: <string> XOR tx_json: <object>,
|
||||
// secret: <secret>
|
||||
// }
|
||||
/** Handle the `submit` RPC command.
|
||||
*
|
||||
* Dispatches to one of two submission paths based on the presence of `tx_blob`
|
||||
* in the request parameters:
|
||||
*
|
||||
* - **tx_blob path (primary):** The hex-encoded, pre-signed transaction binary
|
||||
* is decoded, deserialized into an `STTx`, validated (signature and local
|
||||
* rules), wrapped in a `Transaction`, and forwarded to `NetworkOPs` for
|
||||
* application to the open ledger and P2P broadcast.
|
||||
* - **tx_json path (deprecated):** Falls back to server-side signing via
|
||||
* `RPC::transactionSubmit`. Requires `ADMIN` role or `canSign()` enabled in
|
||||
* configuration. Every response from this path includes a `deprecated`
|
||||
* warning.
|
||||
*
|
||||
* On success the response contains `tx_json`, `tx_blob`, and — when the
|
||||
* network reached a deterministic result — `engine_result`,
|
||||
* `engine_result_code`, `engine_result_message`, `accepted`, `applied`,
|
||||
* `broadcast`, `queued`, `kept`, and advisory ledger-state fields
|
||||
* (`account_sequence_next`, `account_sequence_available`,
|
||||
* `open_ledger_cost`, `validated_ledger_index`).
|
||||
*
|
||||
* @param context The RPC request context. `context.params` must contain
|
||||
* either `tx_blob` (hex string) or `tx_json` + `secret`. The optional
|
||||
* `fail_hard` boolean controls queuing behaviour on rejection.
|
||||
* @return A JSON object representing the submission outcome, or a structured
|
||||
* error object. Possible error keys: `invalidTransaction` (bad binary or
|
||||
* local-check failure), `internalSubmit` (exception during network
|
||||
* dispatch), `internalJson` (exception during response serialization).
|
||||
* @note When `context.app.checkSigs()` is false, signature verification is
|
||||
* skipped via `forceValidity` (pre-marking the transaction as
|
||||
* `SigGoodOnly` in the `HashRouter` cache). This is intended only for
|
||||
* trusted internal submissions where the caller guarantees validity.
|
||||
* @note Resource cost is registered as `feeMediumBurdenRPC` at entry,
|
||||
* regardless of outcome.
|
||||
*/
|
||||
json::Value
|
||||
doSubmit(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
@@ -8,10 +8,30 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// {
|
||||
// SigningAccounts <array>,
|
||||
// tx_json: <object>,
|
||||
// }
|
||||
/** Entry point for the `submit_multisigned` RPC command.
|
||||
*
|
||||
* Classifies the request as `kFEE_HEAVY_BURDEN_RPC` (weight 3000) before
|
||||
* doing any other work, so the resource manager can rate-limit the call even
|
||||
* if it fails early. The handler itself is intentionally thin: it extracts
|
||||
* the optional `fail_hard` boolean (defaults to `false` when absent), converts
|
||||
* it to `NetworkOPs::FailHard` via `NetworkOPs::doFailHard`, then delegates
|
||||
* all structural validation, cryptographic signature checking, ledger lookups,
|
||||
* and network submission to `RPC::transactionSubmitMultiSigned`.
|
||||
*
|
||||
* The validated-ledger age is forwarded from `LedgerMaster` so the callee can
|
||||
* refuse fee auto-fill when the node's view of the network is stale (>2 min).
|
||||
* The `ProcessTransactionFn` closure produced by `getProcessTxnFn` captures
|
||||
* `NetworkOPs` by reference, decoupling the business-logic layer from the full
|
||||
* RPC context and enabling injection of a mock in unit tests.
|
||||
*
|
||||
* @param context RPC dispatch context carrying `params`, `role`, `apiVersion`,
|
||||
* `ledgerMaster`, `netOps`, and the resource load tracker.
|
||||
* @return JSON object with `tx_json`, `tx_blob`, and engine result fields on
|
||||
* success, or a JSON error object on validation or submission failure.
|
||||
* @note `fail_hard: true` rejects the transaction if it cannot be immediately
|
||||
* applied to the open ledger; the default (`false`) allows it to be queued.
|
||||
* @see RPC::transactionSubmitMultiSigned, RPC::getProcessTxnFn
|
||||
*/
|
||||
json::Value
|
||||
doSubmitMultiSigned(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
@@ -13,13 +13,49 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// {
|
||||
// ledger_hash : <ledger>,
|
||||
// ledger_index : <ledger_index>
|
||||
// }
|
||||
//
|
||||
// XXX In this case, not specify either ledger does not mean ledger current. It
|
||||
// means any ledger.
|
||||
/** Retrieve a transaction and its metadata from a specific, caller-identified ledger.
|
||||
*
|
||||
* Implements the `transaction_entry` RPC command. Unlike `tx` (which searches all
|
||||
* available history by hash alone), this handler requires the caller to name the
|
||||
* containing ledger via `ledger_hash` or `ledger_index`. Omitting both fields is
|
||||
* NOT equivalent to requesting the current ledger — `lookupLedger` will resolve
|
||||
* to an arbitrary ledger, and the open-ledger guard below will then reject it
|
||||
* with `notYetImplemented`. Callers must always supply an explicit ledger specifier.
|
||||
*
|
||||
* The validation chain is layered from cheapest to most expensive:
|
||||
* 1. Ledger resolution — `RPC::lookupLedger` validates the specifier and populates
|
||||
* `lpLedger`; returns the error immediately if the ledger cannot be resolved.
|
||||
* 2. `tx_hash` presence — absence yields `fieldNotFoundTransaction`.
|
||||
* 3. Open/current ledger rejection — if `lookupLedger` did not set `ledger_hash`
|
||||
* in `jvResult` the resolved ledger is open/current, which is unsupported;
|
||||
* yields `notYetImplemented`.
|
||||
* 4. Hex parsing — `parseHex` on the `tx_hash` string; invalid input yields
|
||||
* `malformedRequest`. In practice `parseHex` rejects any non-hex or wrong-length
|
||||
* value, so this is the effective input-validation gate.
|
||||
* 5. Transaction existence — `txRead` against the `ReadView`; absence yields
|
||||
* `transactionNotFound`.
|
||||
*
|
||||
* Response shape diverges by API version:
|
||||
* - **v1**: `tx_json` with legacy serialisation (`JsonOptions::none`); metadata
|
||||
* key is `metadata`; `hash` is embedded inside `tx_json`.
|
||||
* - **v2+**: `tx_json` with `DisableApiPriorV2` serialisation; top-level `hash`,
|
||||
* `ledger_hash` (closed ledgers only), `validated`, `ledger_index`, and
|
||||
* `close_time_iso` (validated ledgers only); metadata key is `meta`.
|
||||
* For Payment transactions both versions have `insertDeliverMax` applied, which
|
||||
* copies (v1) or renames (v2+) `Amount` to `DeliverMax`.
|
||||
*
|
||||
* @param context RPC dispatch context carrying `params`, `apiVersion`,
|
||||
* `ledgerMaster`, and the resource consumer for this request.
|
||||
* @return JSON object with `tx_json` and metadata on success, or an error
|
||||
* object whose `error` field is one of: `fieldNotFoundTransaction`,
|
||||
* `notYetImplemented`, `malformedRequest`, `transactionNotFound`, or
|
||||
* any error code propagated by `RPC::lookupLedger`.
|
||||
* @note `stobj` (transaction metadata) may legitimately be null for
|
||||
* transactions in open ledgers — metadata is produced only at close.
|
||||
* The open-ledger guard above makes this case unreachable in practice,
|
||||
* but the serialisation step is still guarded by a null check for safety.
|
||||
* @see doTxJson for the broader sibling command that searches all history.
|
||||
*/
|
||||
json::Value
|
||||
doTransactionEntry(RPC::JsonContext& context)
|
||||
{
|
||||
@@ -35,16 +71,11 @@ doTransactionEntry(RPC::JsonContext& context)
|
||||
}
|
||||
else if (jvResult.get(jss::ledger_hash, json::ValueType::Null).isNull())
|
||||
{
|
||||
// We don't work on ledger current.
|
||||
|
||||
// XXX We don't support any transaction yet.
|
||||
jvResult[jss::error] = "notYetImplemented";
|
||||
}
|
||||
else
|
||||
{
|
||||
uint256 uTransID;
|
||||
// XXX Relying on trusted WSS client. Would be better to have a strict
|
||||
// routine, returning success or failure.
|
||||
if (!uTransID.parseHex(context.params[jss::tx_hash].asString()))
|
||||
{
|
||||
jvResult[jss::error] = "malformedRequest";
|
||||
@@ -89,9 +120,6 @@ doTransactionEntry(RPC::JsonContext& context)
|
||||
auto const jsonMeta = (context.apiVersion > 1 ? jss::meta : jss::metadata);
|
||||
if (stobj)
|
||||
jvResult[jsonMeta] = stobj->getJson(JsonOptions::Values::None);
|
||||
// 'accounts'
|
||||
// 'engine_...'
|
||||
// 'ledger_...'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/** @file
|
||||
* Implements `doTxJson`, the server-side handler for the XRPL `tx` RPC method.
|
||||
*
|
||||
* A client sends a transaction hash or a Concise Transaction ID (CTID) and
|
||||
* receives back the full transaction, its execution metadata, and validation
|
||||
* status. The file is organised into three tightly scoped phases:
|
||||
*
|
||||
* 1. **`doTxJson`** — argument parsing and network-ID validation.
|
||||
* 2. **`doTxHelp`** — ledger-range checks, database lookup via
|
||||
* `TransactionMaster::fetch`, and CTID re-encoding.
|
||||
* 3. **`populateJsonResponse`** — pure serialization; reads only from
|
||||
* `TxResult`, never touches the database or ledger state.
|
||||
*
|
||||
* @see RPC::decodeCTID, RPC::encodeCTID, TransactionMaster::fetch
|
||||
*/
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/misc/DeliverMax.h>
|
||||
@@ -37,6 +52,24 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Determine whether a closed ledger identified by sequence and hash is fully validated.
|
||||
*
|
||||
* Applies three sequential guards before concluding a ledger is validated:
|
||||
* (1) the local store must actually hold the ledger, (2) its sequence must
|
||||
* not exceed the current validated ledger's sequence (a closed-but-not-yet-
|
||||
* validated ledger would fail here), and (3) the stored hash for that
|
||||
* sequence must match the supplied hash — the final guard against ledgers
|
||||
* that were present but later reorganised out of the canonical chain.
|
||||
*
|
||||
* @param ledgerMaster The ledger master instance used to query local state.
|
||||
* @param seq The ledger sequence number to check.
|
||||
* @param hash The expected ledger hash at @p seq.
|
||||
* @return `true` only when all three guards pass; `false` otherwise.
|
||||
*
|
||||
* @note This is intentionally stricter than merely checking `haveLedger`:
|
||||
* a node can hold a closed ledger whose sequence is still above the
|
||||
* validated frontier, in which case `"validated": true` would be wrong.
|
||||
*/
|
||||
static bool
|
||||
isValidated(LedgerMaster& ledgerMaster, std::uint32_t seq, uint256 const& hash)
|
||||
{
|
||||
@@ -49,25 +82,121 @@ isValidated(LedgerMaster& ledgerMaster, std::uint32_t seq, uint256 const& hash)
|
||||
return ledgerMaster.getHashBySeq(seq) == hash;
|
||||
}
|
||||
|
||||
/** Carries the raw output of a `tx` lookup across the phase boundary into the
|
||||
* response-serialization layer.
|
||||
*
|
||||
* All fields except `txn` and `searchedAll` are populated only when the
|
||||
* transaction was located in a closed ledger and the requisite data was
|
||||
* available.
|
||||
*
|
||||
* The `meta` field is a discriminated union: in binary mode `doTxHelp`
|
||||
* serializes `TxMeta` to a raw `Blob` up front, so the response layer never
|
||||
* needs to re-serialize. In non-binary mode the `shared_ptr<TxMeta>` is
|
||||
* stored directly.
|
||||
*
|
||||
* `searchedAll` defaults to `TxSearched::Unknown` (field absent from
|
||||
* response) and is only set to `All` or `Some` when a ledger-range search
|
||||
* was requested and the transaction was not found.
|
||||
*/
|
||||
struct TxResult
|
||||
{
|
||||
/** The located transaction object, or null if lookup failed. */
|
||||
Transaction::pointer txn;
|
||||
|
||||
/** Execution metadata: `Blob` in binary mode, `shared_ptr<TxMeta>` otherwise.
|
||||
* Empty (default-constructed variant) until `doTxHelp` populates it. */
|
||||
std::variant<std::shared_ptr<TxMeta>, Blob> meta;
|
||||
|
||||
/** Whether the containing ledger has been validated by consensus. */
|
||||
bool validated = false;
|
||||
|
||||
/** Re-encoded CTID for this transaction, absent if any component exceeds
|
||||
* its bit budget (`txnIdx > 0xFFFF`, `netID >= 0xFFFF`, or
|
||||
* `lgrSeq >= 0x0FFF'FFFF`). */
|
||||
std::optional<std::string> ctid;
|
||||
|
||||
/** Close time of the validated ledger that included this transaction.
|
||||
* Populated only when `validated` is true. */
|
||||
std::optional<NetClock::time_point> closeTime;
|
||||
|
||||
/** Hash of the closed ledger that included this transaction.
|
||||
* Absent for open ledgers. */
|
||||
std::optional<uint256> ledgerHash;
|
||||
|
||||
/** Completeness of the range search. `Unknown` suppresses the
|
||||
* `searched_all` field in the response for backward compatibility. */
|
||||
TxSearched searchedAll = TxSearched::Unknown;
|
||||
};
|
||||
|
||||
/** Parsed and validated inputs for a single `tx` RPC call.
|
||||
*
|
||||
* Exactly one of `hash` or `ctid` will be non-empty after `doTxJson`
|
||||
* finishes parsing; supplying both is rejected before this struct is
|
||||
* populated. If `ctid` is present, `doTxHelp` resolves it to a hash via
|
||||
* `LedgerMaster::txnIdFromIndex` before dispatching to `TransactionMaster`.
|
||||
*/
|
||||
struct TxArgs
|
||||
{
|
||||
/** Raw transaction hash supplied via `"transaction"` param, or the hash
|
||||
* resolved from a CTID by `doTxHelp`. */
|
||||
std::optional<uint256> hash;
|
||||
|
||||
/** Decoded CTID as `{ledgerSeq, txnIndex}`. The network ID has already
|
||||
* been validated against the local node before this struct is used. */
|
||||
std::optional<std::pair<uint32_t, uint16_t>> ctid;
|
||||
|
||||
/** Whether to return transaction and metadata in binary (hex) form. */
|
||||
bool binary = false;
|
||||
|
||||
/** Optional inclusive ledger-sequence range `{min_ledger, max_ledger}`.
|
||||
* When present, the lookup is restricted to this range and `searched_all`
|
||||
* is reported in the response. Range span is capped at 1000 ledgers. */
|
||||
std::optional<std::pair<uint32_t, uint32_t>> ledgerRange;
|
||||
};
|
||||
|
||||
/** Resolve and fetch a transaction from the local database.
|
||||
*
|
||||
* Performs ledger-range validation, hash resolution, database lookup, and
|
||||
* CTID re-encoding in sequence:
|
||||
*
|
||||
* 1. **Range validation.** If `args.ledgerRange` is set, checks that the
|
||||
* range is non-inverted and spans no more than 1000 ledgers
|
||||
* (`kMAX_RANGE`). Returns `RpcInvalidLgrRange` or
|
||||
* `RpcExcessiveLgrRange` on violation.
|
||||
*
|
||||
* 2. **Hash resolution.** If `args.ctid` is set, calls
|
||||
* `LedgerMaster::txnIdFromIndex` to convert `{ledgerSeq, txnIndex}`
|
||||
* into a transaction hash. A successful resolution also collapses the
|
||||
* search range to the single ledger named by the CTID. If the index
|
||||
* lookup returns no hash, `RpcTxnNotFound` is returned immediately.
|
||||
*
|
||||
* 3. **Database fetch.** Calls the appropriate overload of
|
||||
* `TransactionMaster::fetch`: the range-bounded overload when a
|
||||
* `ledgerRange` was supplied, the full-history overload otherwise.
|
||||
* Both return `std::variant<TxPair, TxSearched>`. A `TxSearched`
|
||||
* result means the transaction was absent; the sentinel is forwarded
|
||||
* into `TxResult::searchedAll` and `RpcTxnNotFound` is returned.
|
||||
*
|
||||
* 4. **Metadata serialization.** In binary mode, `TxMeta` is serialized
|
||||
* to a raw `Blob` here — avoiding re-serialization in the response
|
||||
* phase. In non-binary mode the `shared_ptr<TxMeta>` is stored as-is.
|
||||
*
|
||||
* 5. **Validation check.** Calls `isValidated` with the ledger's sequence
|
||||
* and hash. Only if validated is the close time fetched and stored.
|
||||
*
|
||||
* 6. **CTID re-encoding.** Attempts to encode a CTID from the found
|
||||
* transaction's `sfTransactionIndex`, the ledger sequence, and the
|
||||
* local network ID. Omits the field silently if any component exceeds
|
||||
* its bit budget rather than returning a truncated value.
|
||||
*
|
||||
* @param context RPC dispatch context providing access to app services.
|
||||
* @param args Validated input arguments from `doTxJson`.
|
||||
* @return A pair of `TxResult` (populated on success) and `RPC::Status`
|
||||
* indicating success or the specific failure reason.
|
||||
*
|
||||
* @note This function must not emit JSON directly; all output goes through
|
||||
* `TxResult` fields consumed by `populateJsonResponse`.
|
||||
*/
|
||||
std::pair<TxResult, RPC::Status>
|
||||
doTxHelp(RPC::Context& context, TxArgs args)
|
||||
{
|
||||
@@ -133,10 +262,12 @@ doTxHelp(RPC::Context& context, TxArgs args)
|
||||
return {result, RpcTxnNotFound};
|
||||
}
|
||||
|
||||
// populate transaction data
|
||||
result.txn = txn;
|
||||
if (txn->getLedger() == 0)
|
||||
{
|
||||
// Transaction is known but not yet included in any closed ledger
|
||||
// (e.g., still in the transaction queue). Return what we have with
|
||||
// no metadata or validation status.
|
||||
return {result, RpcSuccess};
|
||||
}
|
||||
|
||||
@@ -161,7 +292,9 @@ doTxHelp(RPC::Context& context, TxArgs args)
|
||||
if (result.validated)
|
||||
result.closeTime = context.ledgerMaster.getCloseTimeBySeq(txn->getLedger());
|
||||
|
||||
// compute outgoing CTID
|
||||
// Attempt to re-encode a CTID. All three components must fit within
|
||||
// their respective bit budgets; if any exceeds the limit the field is
|
||||
// simply omitted rather than emitting a truncated or invalid value.
|
||||
if (meta->getAsObject().isFieldPresent(sfTransactionIndex))
|
||||
{
|
||||
uint32_t const lgrSeq = ledger->header().seq;
|
||||
@@ -176,6 +309,43 @@ doTxHelp(RPC::Context& context, TxArgs args)
|
||||
return {result, RpcSuccess};
|
||||
}
|
||||
|
||||
/** Build the final JSON response for a `tx` RPC request.
|
||||
*
|
||||
* Pure serialization layer — must not access the database, ledger master,
|
||||
* or any other application service. All required data is already in `res`.
|
||||
*
|
||||
* **Error path.** When `res.second` indicates failure, the error is injected
|
||||
* via `RPC::Status::inject`. The one special case is `RpcTxnNotFound` with
|
||||
* a known `searchedAll` value (i.e., a range-bounded search that concluded):
|
||||
* in that case a `searched_all` boolean is added before the error fields so
|
||||
* the client can distinguish genuine absence from an incomplete local history.
|
||||
*
|
||||
* **API version branching.** For API v2+, the response is restructured:
|
||||
* - Transaction goes under `"tx_json"` (non-binary) or `"tx_blob"` (binary).
|
||||
* - `"ledger_hash"`, `"hash"`, and `"close_time_iso"` become top-level fields.
|
||||
* - `JsonOptions::disable_API_prior_V2` suppresses deprecated legacy fields.
|
||||
* For API v1 the legacy flat layout from `Transaction::getJson` is used.
|
||||
*
|
||||
* **Metadata enrichment.** Three post-processing calls augment the `"meta"`
|
||||
* JSON object after core serialization:
|
||||
* - `insertDeliveredAmount` — fills `delivered_amount` for payments and
|
||||
* check-cash transactions.
|
||||
* - `RPC::insertNFTSyntheticInJson` — adds NFT-specific synthetic fields.
|
||||
* - `RPC::insertMPTokenIssuanceID` — injects `mpt_issuance_id` for
|
||||
* `MPTokenIssuanceCreate` transactions.
|
||||
* These are applied only in non-binary mode where `result.meta` holds a
|
||||
* `shared_ptr<TxMeta>`.
|
||||
*
|
||||
* @param res The lookup result produced by `doTxHelp`.
|
||||
* @param args The original parsed arguments (used for `binary` flag and
|
||||
* API-version-neutral field selection).
|
||||
* @param context RPC context supplying `apiVersion` for branching logic.
|
||||
* @return A `Json::Value` object ready to be returned to the caller.
|
||||
*
|
||||
* @note `result.ledgerHash` is only populated for closed or validated
|
||||
* ledgers (never for open ones), so `"ledger_hash"` is correctly absent
|
||||
* when the transaction is pending.
|
||||
*/
|
||||
json::Value
|
||||
populateJsonResponse(
|
||||
std::pair<TxResult, RPC::Status> const& res,
|
||||
@@ -185,7 +355,6 @@ populateJsonResponse(
|
||||
json::Value response;
|
||||
RPC::Status const& error = res.second;
|
||||
TxResult const& result = res.first;
|
||||
// handle errors
|
||||
if (error.toErrorCode() != RpcSuccess)
|
||||
{
|
||||
if (error.toErrorCode() == RpcTxnNotFound && result.searchedAll != TxSearched::Unknown)
|
||||
@@ -199,7 +368,6 @@ populateJsonResponse(
|
||||
error.inject(response);
|
||||
}
|
||||
}
|
||||
// no errors
|
||||
else if (result.txn)
|
||||
{
|
||||
auto const& sttx = result.txn->getSTransaction();
|
||||
@@ -219,8 +387,6 @@ populateJsonResponse(
|
||||
response[jss::tx_json], sttx->getTxnType(), context.apiVersion);
|
||||
}
|
||||
|
||||
// Note, result.ledgerHash is only set in a closed or validated
|
||||
// ledger - as seen in `doTxHelp`
|
||||
if (result.ledgerHash)
|
||||
response[jss::ledger_hash] = to_string(*result.ledgerHash);
|
||||
|
||||
@@ -239,14 +405,12 @@ populateJsonResponse(
|
||||
RPC::insertDeliverMax(response, sttx->getTxnType(), context.apiVersion);
|
||||
}
|
||||
|
||||
// populate binary metadata
|
||||
if (auto blob = std::get_if<Blob>(&result.meta))
|
||||
{
|
||||
XRPL_ASSERT(args.binary, "xrpl::populateJsonResponse : binary is set");
|
||||
auto jsonMeta = (context.apiVersion > 1 ? jss::meta_blob : jss::meta);
|
||||
response[jsonMeta] = strHex(makeSlice(*blob));
|
||||
}
|
||||
// populate meta data
|
||||
else if (auto m = std::get_if<std::shared_ptr<TxMeta>>(&result.meta))
|
||||
{
|
||||
auto& meta = *m;
|
||||
@@ -266,21 +430,45 @@ populateJsonResponse(
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Entry point for the `tx` RPC method — parses arguments and drives the lookup pipeline.
|
||||
*
|
||||
* Validates that exactly one of `"transaction"` (a 64-hex-character hash) or
|
||||
* `"ctid"` (a 16-hex-character Concise Transaction ID) is present. Supplying
|
||||
* both is rejected with `rpcINVALID_PARAMS` because the intent would be
|
||||
* ambiguous. Supplying neither is also rejected.
|
||||
*
|
||||
* **CTID network-ID check.** A CTID encodes the network it was issued on. If
|
||||
* the decoded `networkID` does not match `NetworkIDService::getNetworkID()`, a
|
||||
* human-readable error is returned naming the expected network rather than
|
||||
* silently doing a hash lookup on a different chain.
|
||||
*
|
||||
* **Ledger range.** When both `"min_ledger"` and `"max_ledger"` are present
|
||||
* they are extracted via `Json::Value::asUInt`, which throws on type mismatch.
|
||||
* The `try`/`catch` converts that exception into `rpcINVALID_LGR_RANGE`; the
|
||||
* range span limit (1000 ledgers) is enforced later in `doTxHelp`.
|
||||
*
|
||||
* **Transaction-table guard.** The handler returns `rpcNOT_ENABLED`
|
||||
* immediately when `config().useTxTables()` is false — the relational
|
||||
* database tables that back the lookup are not present.
|
||||
*
|
||||
* After argument parsing, delegates to `doTxHelp` for the database lookup
|
||||
* and then to `populateJsonResponse` for serialization.
|
||||
*
|
||||
* @param context RPC dispatch context carrying JSON params, app services,
|
||||
* and the resolved API version.
|
||||
* @return A `Json::Value` with the transaction, metadata, and validation
|
||||
* status, or an error object if any validation or lookup step fails.
|
||||
*/
|
||||
json::Value
|
||||
doTxJson(RPC::JsonContext& context)
|
||||
{
|
||||
if (!context.app.config().useTxTables())
|
||||
return rpcError(RpcNotEnabled);
|
||||
|
||||
// Deserialize and validate JSON arguments
|
||||
|
||||
TxArgs args;
|
||||
|
||||
if (context.params.isMember(jss::transaction) && context.params.isMember(jss::ctid))
|
||||
{
|
||||
// specifying both is ambiguous
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
if (context.params.isMember(jss::transaction))
|
||||
{
|
||||
@@ -322,7 +510,7 @@ doTxJson(RPC::JsonContext& context)
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// One of the calls to `asUInt ()` failed.
|
||||
// asUInt() throws if the JSON value is not an unsigned integer.
|
||||
return rpcError(RpcInvalidLgrRange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,51 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// {
|
||||
// start: <index>
|
||||
// }
|
||||
/** @file
|
||||
* Implements the `tx_history` RPC command, a paginated raw transaction log
|
||||
* browser backed by the node's relational database.
|
||||
*
|
||||
* This command is available only in API version 1; API v2+ returns
|
||||
* `unknownCmd` via the handler table's version range restriction.
|
||||
*/
|
||||
|
||||
/** Return a page of up to 20 historical transactions starting at a given
|
||||
* offset, sorted in descending ledger-sequence order.
|
||||
*
|
||||
* The handler is a thin bridge between the RPC dispatch layer and
|
||||
* `RelationalDatabase::getTxHistory()`. It performs no filtering by account
|
||||
* or transaction type; it is a raw log browser, not a query engine.
|
||||
*
|
||||
* Validation sequence (short-circuits on first failure):
|
||||
* 1. `useTxTables()` config flag — returns `rpcNOT_ENABLED` if absent.
|
||||
* 2. Presence of the `start` field — returns `rpcINVALID_PARAMS` if missing.
|
||||
* 3. Deep-pagination cap: `start > 10000` is rejected with `rpcNO_PERMISSION`
|
||||
* unless `isUnlimited(context.role)` is true (ADMIN / IDENTIFIED roles).
|
||||
*
|
||||
* After validation, `getRelationalDatabase().getTxHistory(startIndex)` is
|
||||
* called. The database layer enforces the 20-transaction page size. Each
|
||||
* transaction is serialized via `getJson(JsonOptions::None)` and enriched
|
||||
* with `RPC::insertDeliverMax()` to inject the `DeliverMax` field (and, for
|
||||
* API version > 1, remove the legacy `Amount` field) on Payment transactions,
|
||||
* consistent with the enrichment applied in `Tx.cpp` and
|
||||
* `TransactionEntry.cpp`.
|
||||
*
|
||||
* The response echoes the requested `index` value so callers can track their
|
||||
* pagination position across calls.
|
||||
*
|
||||
* @note `context.params[jss::start].asUInt()` silently converts non-integer
|
||||
* JSON values to `0` rather than returning a type error. A `start` of
|
||||
* `0` is valid, so this is harmless in practice and is consistent with
|
||||
* how other XRPL RPC handlers treat loosely-typed JSON integers.
|
||||
* @note This command is retired in API v2 and later. Clients on v2+ receive
|
||||
* `unknownCmd` from the handler table before this function is called.
|
||||
* @note The resource load type is set to `kFEE_MEDIUM_BURDEN_RPC` because
|
||||
* the backing call is a database scan rather than a point lookup.
|
||||
* @param context The RPC dispatch context carrying the request parameters,
|
||||
* application state, caller role, and API version.
|
||||
* @return A JSON object with `"index"` (the requested start offset) and
|
||||
* `"txs"` (array of transaction JSON objects), or an RPC error object.
|
||||
*/
|
||||
json::Value
|
||||
doTxHistory(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* RPC handler for the `tx_reduce_relay` command.
|
||||
*
|
||||
* Provides a read-only introspection endpoint that surfaces transaction relay
|
||||
* performance metrics collected by the node's peer overlay network. The
|
||||
* handler is registered with `Role::USER` and `NO_CONDITION`, so any connected
|
||||
* client may query it without special privileges or an active network
|
||||
* connection requirement.
|
||||
*/
|
||||
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
@@ -6,6 +16,38 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Return a rolling-average snapshot of transaction reduce-relay metrics.
|
||||
*
|
||||
* Delegates unconditionally to `Overlay::txMetrics()`, which serializes the
|
||||
* `metrics::TxMetrics` struct maintained by `OverlayImpl` into a JSON object.
|
||||
* There is no input parsing, no parameter validation, and no transformation of
|
||||
* the result — the `Json::Value` produced by the overlay is returned directly
|
||||
* to the caller.
|
||||
*
|
||||
* The returned object contains thirteen `jss::txr_*`-keyed fields covering:
|
||||
* - Per-second rolling averages (message count and byte size) for the five
|
||||
* tracked protocol message types: `TMTransaction`, `TMHaveTransactions`,
|
||||
* `TMGetLedger`, `TMLedgerData`, and `TMTransactions`.
|
||||
* - Per-transaction sample averages for selected, suppressed, and
|
||||
* feature-disabled peers — the three-way partition produced by the relay
|
||||
* algorithm each time a transaction is forwarded.
|
||||
* - A per-second rate of transactions received in `TMTransactions` bundles
|
||||
* that the local node was missing, indicating reactive-request load induced
|
||||
* by relay suppression.
|
||||
*
|
||||
* All values reflect a trailing ~30-second exponential window. Fields with
|
||||
* no recent activity report `"0"`. Values are serialized as JSON strings
|
||||
* rather than JSON integers; callers must parse them for numeric comparison.
|
||||
*
|
||||
* Thread safety and data correctness are the responsibility of `TxMetrics`
|
||||
* (which guards its state with an internal `mutex`) and `OverlayImpl` (which
|
||||
* additionally serializes writes via its Boost.Asio strand).
|
||||
*
|
||||
* @param context Standard RPC dispatch context; only `context.app` is used to
|
||||
* reach the overlay via `getOverlay()`.
|
||||
* @return A `Json::Value` object containing the current reduce-relay metric
|
||||
* snapshot, as produced by `Overlay::txMetrics()`.
|
||||
*/
|
||||
json::Value
|
||||
doTxReduceRelay(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,43 @@ namespace RPC {
|
||||
struct JsonContext;
|
||||
} // namespace RPC
|
||||
|
||||
/** Handle the `ping` RPC command, returning a role-conditional session
|
||||
* introspection object.
|
||||
*
|
||||
* The response is sparse by design — only the fields relevant to the
|
||||
* caller's privilege level are emitted:
|
||||
*
|
||||
* - `Role::ADMIN`: sets `"role": "admin"`.
|
||||
* - `Role::IDENTIFIED`: sets `"role": "identified"`, `"username"` from
|
||||
* `context.headers.user`, and `"ip"` from `context.headers.forwardedFor`
|
||||
* when non-empty. This role is assigned when a `secure_gateway` proxy
|
||||
* has authenticated a downstream user via the `X-User` HTTP header.
|
||||
* - `Role::PROXY`: sets `"role": "proxied"` and `"ip"` when non-empty.
|
||||
* The client is behind a gateway but has not been individually identified.
|
||||
* - `Role::GUEST`, `Role::USER`, `Role::FORBID`: fall through to `default`,
|
||||
* producing an empty object. These callers learn nothing about their
|
||||
* classification from the response.
|
||||
*
|
||||
* The `"ip"` field is written only when `forwardedFor` is non-empty, so
|
||||
* direct connections never receive a spurious `"ip": ""` key.
|
||||
*
|
||||
* For WebSocket sessions, `context.infoSub` is non-null and an additional
|
||||
* `"unlimited": true` field is emitted when the consumer is exempt from
|
||||
* resource throttling. HTTP callers always receive a null `infoSub` and
|
||||
* therefore never see this field; the null check is the sole guard.
|
||||
*
|
||||
* All JSON keys are referenced via `jss::` namespace constants rather than
|
||||
* raw string literals, ensuring a single point of change across the codebase.
|
||||
*
|
||||
* @param context The JSON-RPC dispatch envelope, pre-populated with the
|
||||
* resolved `role`, `headers` (`user`, `forwardedFor`), and `infoSub`
|
||||
* by the HTTP/WebSocket dispatch layer before this handler is called.
|
||||
* @return A `json::Value` object containing the applicable session fields,
|
||||
* or an empty object for unprivileged / unclassified callers.
|
||||
* @note `role` is resolved by `requestRole()` (in `Role.h`) before dispatch,
|
||||
* so this handler contains no authorization logic of its own.
|
||||
* @see Role.h, Context.h
|
||||
*/
|
||||
json::Value
|
||||
doPing(RPC::JsonContext& context)
|
||||
{
|
||||
@@ -33,7 +70,6 @@ doPing(RPC::JsonContext& context)
|
||||
default:;
|
||||
}
|
||||
|
||||
// This is only accessible on ws sessions.
|
||||
if (context.infoSub)
|
||||
{
|
||||
if (context.infoSub->getConsumer().isUnlimited())
|
||||
|
||||
@@ -14,15 +14,51 @@ namespace RPC {
|
||||
struct JsonContext;
|
||||
} // namespace RPC
|
||||
|
||||
// Result:
|
||||
// {
|
||||
// random: <uint256>
|
||||
// }
|
||||
/** Handler for the `random` RPC command.
|
||||
*
|
||||
* Draws 256 bits of entropy from the node's cryptographically secure PRNG
|
||||
* and returns the value to the caller as a 64-character lowercase hex string
|
||||
* under the `random` key.
|
||||
*
|
||||
* Registered in `Handler.cpp` as:
|
||||
* @code
|
||||
* {"random", byRef(&doRandom), Role::USER, NO_CONDITION}
|
||||
* @endcode
|
||||
*
|
||||
* The `Role::USER` designation allows any connected client — not just
|
||||
* admins — to invoke it. `NO_CONDITION` means no ledger state is required;
|
||||
* the handler runs unconditionally because it never touches ledger data.
|
||||
*
|
||||
* **Algorithm:**
|
||||
* 1. Declare a zero-initialised `uint256` (32 bytes).
|
||||
* 2. Fill it via `beast::rngfill` using `cryptoPrng()`, which returns a
|
||||
* reference to the process-wide mutex-protected `csprng_engine` singleton.
|
||||
* The engine seeds from `std::random_device` and satisfies the C++
|
||||
* `UniformRandomNumberEngine` named requirement.
|
||||
* 3. Serialise the filled value to hex via `to_string()` and return it under
|
||||
* `jss::random`.
|
||||
*
|
||||
* **Exception handling:** The body is wrapped in a `try/catch` that returns
|
||||
* `rpcError(RpcInternal)` on any `std::exception`. This guard is a legacy
|
||||
* defensive pattern — the RPC dispatch layer already has a top-level catch —
|
||||
* and the branch is marked `LCOV_EXCL_LINE` because it is never expected to
|
||||
* execute in practice.
|
||||
*
|
||||
* **Use case:** Clients that want a trusted external entropy source (e.g., for
|
||||
* combined randomness schemes) can call this endpoint and mix the result with
|
||||
* locally generated entropy. The trust model is that the client trusts the
|
||||
* node operator's CSPRNG seeding.
|
||||
*
|
||||
* @param context The RPC dispatch context. Its contents are not read by this
|
||||
* handler; the parameter exists only to satisfy the handler signature
|
||||
* contract.
|
||||
* @return A JSON object `{ "random": "<64-char hex string>" }` on success,
|
||||
* or `rpcError(RpcInternal)` if an unexpected exception propagates out
|
||||
* of `beast::rngfill` (effectively unreachable).
|
||||
*/
|
||||
json::Value
|
||||
doRandom(RPC::JsonContext& context)
|
||||
{
|
||||
// TODO(tom): the try/catch is almost certainly redundant, we catch at the
|
||||
// top level too.
|
||||
try
|
||||
{
|
||||
uint256 rand;
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/** @file
|
||||
* Custom Boost.Beast HTTP body type that holds a `Json::Value` as the
|
||||
* in-memory representation and serializes it to wire bytes on demand.
|
||||
*
|
||||
* Used by `OverlayImpl` (diagnostic endpoints: `/crawl`, `/health`, `/vl/`)
|
||||
* and `ServerHandler` (RPC dispatch layer) when building outbound HTTP
|
||||
* responses. There is intentionally no BodyWriter (deserialization) — every
|
||||
* call site uses `JsonBody` only for responses; incoming request bodies are
|
||||
* parsed separately via `Json::Reader`.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
@@ -8,13 +19,40 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/// Body that holds JSON
|
||||
/** Boost.Beast HTTP body type whose in-memory value is a `Json::Value`.
|
||||
*
|
||||
* Satisfies the Beast `Body` concept by providing a `value_type` alias and
|
||||
* nested `reader`/`writer` classes that both implement the Beast BodyReader
|
||||
* interface (serialization only — there is no deserialization path).
|
||||
*
|
||||
* The struct itself carries no data; Beast only instantiates `reader` and
|
||||
* `writer` internally. The `explicit` default constructor suppresses accidental
|
||||
* implicit construction while preserving the Beast concept requirements.
|
||||
*
|
||||
* @note Both `reader` and `writer` serialize eagerly at construction time, so
|
||||
* by the time Beast's async I/O layer requests bytes they are already
|
||||
* available. Neither class defers or streams output incrementally.
|
||||
* @see JsonBody::reader, JsonBody::writer
|
||||
*/
|
||||
struct JsonBody
|
||||
{
|
||||
explicit JsonBody() = default;
|
||||
|
||||
/** The in-memory body representation used by Beast message types. */
|
||||
using value_type = json::Value;
|
||||
|
||||
/** Old-style Beast BodyReader that serializes a `Json::Value` into a
|
||||
* `boost::beast::multi_buffer` using `Json::stream()`.
|
||||
*
|
||||
* The entire JSON payload is written into `buffer_` at construction time
|
||||
* via a chunked-write callback. Subsequent calls to `init()`, `get()`,
|
||||
* and `finish()` are therefore trivial. `is_deferred = std::false_type`
|
||||
* informs Beast that buffer preparation does not need to be deferred.
|
||||
*
|
||||
* This older constructor form takes the full `http::message` (header +
|
||||
* body together), as opposed to the newer split-argument form used by
|
||||
* `writer`.
|
||||
*/
|
||||
class reader // NOLINT(readability-identifier-naming) -- Boost.Beast body concept name
|
||||
{
|
||||
using dynamic_buffer_type = boost::beast::multi_buffer;
|
||||
@@ -22,10 +60,22 @@ struct JsonBody
|
||||
dynamic_buffer_type buffer_;
|
||||
|
||||
public:
|
||||
/** Buffer sequence type returned by `get()`. Satisfies the
|
||||
* Beast `ConstBufferSequence` requirement; may span multiple
|
||||
* discontiguous memory regions. */
|
||||
using const_buffers_type = typename dynamic_buffer_type::const_buffers_type;
|
||||
|
||||
/** Tells Beast that buffer preparation is not deferred; the buffer is
|
||||
* fully populated by the constructor before `init()` is called. */
|
||||
using is_deferred = std::false_type;
|
||||
|
||||
/** Construct the reader and eagerly serialize `m.body()` into an
|
||||
* internal `multi_buffer` via `Json::stream()`.
|
||||
*
|
||||
* @tparam IsRequest Whether the enclosing message is a request.
|
||||
* @tparam Fields The HTTP header fields type.
|
||||
* @param m The HTTP message whose body is to be serialized.
|
||||
*/
|
||||
template <bool IsRequest, class Fields>
|
||||
explicit reader(boost::beast::http::message<IsRequest, JsonBody, Fields> const& m)
|
||||
{
|
||||
@@ -35,32 +85,66 @@ struct JsonBody
|
||||
});
|
||||
}
|
||||
|
||||
/** No-op: the buffer is already fully populated by the constructor.
|
||||
*
|
||||
* @param ec Unused; not modified.
|
||||
*/
|
||||
void
|
||||
init(boost::beast::error_code&) noexcept
|
||||
{
|
||||
}
|
||||
|
||||
// get() must return a boost::optional (not a std::optional) to meet
|
||||
// requirements of a boost::beast::BodyReader.
|
||||
/** Return all serialized bytes in a single call.
|
||||
*
|
||||
* Returns `boost::optional` (not `std::optional`) to satisfy the
|
||||
* Beast BodyReader concept. The `bool` element of the pair is `false`,
|
||||
* signaling that all data is available and no further `get()` calls
|
||||
* are needed.
|
||||
*
|
||||
* @param ec Unused; not modified.
|
||||
* @return An optional pair of (buffer sequence, more-data flag). The
|
||||
* more-data flag is always `false`.
|
||||
*/
|
||||
boost::optional<std::pair<const_buffers_type, bool>>
|
||||
get(boost::beast::error_code& ec)
|
||||
{
|
||||
return {{buffer_.data(), false}};
|
||||
}
|
||||
|
||||
/** No-op: no resources require explicit release.
|
||||
*
|
||||
* @param ec Unused; not modified.
|
||||
*/
|
||||
void
|
||||
finish(boost::beast::error_code&)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/** Newer-style Beast BodyReader that serializes a `Json::Value` into a
|
||||
* single `std::string` using `Json::to_string()`.
|
||||
*
|
||||
* The constructor takes the HTTP header and body value as separate
|
||||
* arguments (the newer Beast BodyReader interface), rather than the full
|
||||
* message. The entire JSON payload is produced in one allocation at
|
||||
* construction time and exposed as a `boost::asio::const_buffer`.
|
||||
*/
|
||||
class writer // NOLINT(readability-identifier-naming) -- Boost.Beast body concept name
|
||||
{
|
||||
std::string body_string_;
|
||||
|
||||
public:
|
||||
/** Single contiguous buffer wrapping `body_string_`. */
|
||||
using const_buffers_type = boost::asio::const_buffer;
|
||||
|
||||
/** Construct the writer and eagerly serialize `value` to a string.
|
||||
*
|
||||
* @tparam IsRequest Whether the enclosing message is a request.
|
||||
* @tparam Fields The HTTP header fields type.
|
||||
* @param fields The HTTP header (unused; present to satisfy the
|
||||
* Beast BodyReader concept).
|
||||
* @param value The JSON value to serialize.
|
||||
*/
|
||||
template <bool IsRequest, class Fields>
|
||||
explicit writer(
|
||||
boost::beast::http::header<IsRequest, Fields> const& fields,
|
||||
@@ -69,14 +153,29 @@ struct JsonBody
|
||||
{
|
||||
}
|
||||
|
||||
/** Clear `ec` to signal that initialization succeeded.
|
||||
*
|
||||
* Unlike `reader::init()`, this method explicitly clears the error
|
||||
* code rather than leaving it unmodified.
|
||||
*
|
||||
* @param ec Set to a zero/success value.
|
||||
*/
|
||||
static void
|
||||
init(boost::beast::error_code& ec)
|
||||
{
|
||||
ec.assign(0, ec.category());
|
||||
}
|
||||
|
||||
// get() must return a boost::optional (not a std::optional) to meet
|
||||
// requirements of a boost::beast::BodyWriter.
|
||||
/** Return the serialized body as a single const buffer.
|
||||
*
|
||||
* Returns `boost::optional` (not `std::optional`) to satisfy the
|
||||
* Beast BodyWriter concept. The `bool` element of the pair is `false`,
|
||||
* signaling that all data is available in one shot.
|
||||
*
|
||||
* @param ec Set to a zero/success value before returning.
|
||||
* @return An optional pair of (const buffer over `body_string_`,
|
||||
* more-data flag). The more-data flag is always `false`.
|
||||
*/
|
||||
boost::optional<std::pair<const_buffers_type, bool>>
|
||||
get(boost::beast::error_code& ec)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user