mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-22 23:00:55 +00:00
Merge remote-tracking branch 'origin/develop' into tapanito/invariant-improvement
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -18,6 +17,7 @@
|
||||
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
#include <xrpl/tx/invariants/CheckInvariants.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -56,7 +57,9 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Performs early sanity checks on the txid */
|
||||
/**
|
||||
* Performs early sanity checks on the txid
|
||||
*/
|
||||
NotTEC
|
||||
preflight0(PreflightContext const& ctx, std::uint32_t flagMask)
|
||||
{
|
||||
@@ -111,7 +114,8 @@ preflight0(PreflightContext const& ctx, std::uint32_t flagMask)
|
||||
|
||||
namespace detail {
|
||||
|
||||
/** Checks the validity of the transactor signing key.
|
||||
/**
|
||||
* Checks the validity of the transactor signing key.
|
||||
*
|
||||
* Normally called from preflight1.
|
||||
*/
|
||||
@@ -169,7 +173,61 @@ preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::J
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/** Performs early sanity checks on the account and fee fields */
|
||||
static NotTEC
|
||||
preflight1Sponsor(PreflightContext const& ctx)
|
||||
{
|
||||
bool const hasSponsor = ctx.tx.isFieldPresent(sfSponsor);
|
||||
bool const hasSponsorFlags = ctx.tx.isFieldPresent(sfSponsorFlags);
|
||||
bool const hasSponsorSig = ctx.tx.isFieldPresent(sfSponsorSignature);
|
||||
|
||||
if ((hasSponsor || hasSponsorFlags || hasSponsorSig) && !ctx.rules.enabled(featureSponsor))
|
||||
return temDISABLED;
|
||||
|
||||
if (hasSponsor != hasSponsorFlags)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight1: sponsor and sponsor flags mismatch";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
if (hasSponsorSig && (!hasSponsor || !hasSponsorFlags))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight1: sponsor signature without sponsor definition";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (hasSponsorFlags)
|
||||
{
|
||||
auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags);
|
||||
if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
// Reserve sponsorship is only permitted for an explicit allow-list of
|
||||
// transaction types, for v1. All other tx types reject spfSponsorReserve here.
|
||||
if (isReserveSponsored(ctx.tx))
|
||||
{
|
||||
if (!isReserveSponsorAllowed(ctx.tx.getTxnType()))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "preflight1: spfSponsorReserve not allowed for this transaction type";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSponsor && ctx.tx.getAccountID(sfSponsor) == ctx.tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight1: Sponsor account cannot be the same as the account";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs early sanity checks on the account and fee fields
|
||||
*/
|
||||
NotTEC
|
||||
Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
|
||||
{
|
||||
@@ -222,18 +280,25 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
|
||||
if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID))
|
||||
return temINVALID;
|
||||
|
||||
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch))
|
||||
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1))
|
||||
return temINVALID_FLAG;
|
||||
|
||||
XRPL_ASSERT(
|
||||
ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() ||
|
||||
!ctx.rules.enabled(featureBatch),
|
||||
"Inner batch transaction must have a parent batch ID.");
|
||||
// Reject if the inner batch flag and parentBatchId are inconsistent.
|
||||
// A standalone tx with tfInnerBatchTxn but no parentBatchId is an
|
||||
// attack attempt. A tx with parentBatchId but without tfInnerBatchTxn
|
||||
// is a programming error.
|
||||
if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value())
|
||||
return temINVALID_INNER_BATCH;
|
||||
|
||||
if (auto const ter = preflight1Sponsor(ctx); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Checks whether the signature appears valid */
|
||||
/**
|
||||
* Checks whether the signature appears valid
|
||||
*/
|
||||
NotTEC
|
||||
Transactor::preflight2(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -244,15 +309,19 @@ Transactor::preflight2(PreflightContext const& ctx)
|
||||
return *ret;
|
||||
}
|
||||
|
||||
// It should be impossible for the InnerBatchTxn flag to be set without
|
||||
// featureBatch being enabled
|
||||
XRPL_ASSERT_PARTS(
|
||||
!ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatch),
|
||||
"xrpl::Transactor::preflight2",
|
||||
"InnerBatch flag only set if feature enabled");
|
||||
// Skip signature check on batch inner transactions
|
||||
if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch))
|
||||
// Skip the signature check on batch inner transactions. preflight1 already
|
||||
// enforces both conditions; re-checking them as defense in depth guarantees
|
||||
// we never return success (and so skip signature validation) for an inner
|
||||
// transaction unless the amendment is enabled and it really sits inside a
|
||||
// batch.
|
||||
if (ctx.tx.isFlag(tfInnerBatchTxn))
|
||||
{
|
||||
if (!ctx.rules.enabled(featureBatchV1_1))
|
||||
return temINVALID_FLAG;
|
||||
if (!ctx.parentBatchId.has_value())
|
||||
return temINVALID_INNER_BATCH;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
// Do not add any checks after this point that are relevant for
|
||||
// batch inner transactions. They will be skipped.
|
||||
|
||||
@@ -339,6 +408,44 @@ Transactor::checkPermission(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Transactor::checkSponsor(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
if (!tx.isFieldPresent(sfSponsor))
|
||||
return tesSUCCESS;
|
||||
|
||||
// Reserve sponsorship with permissioned delegation is disallowed.
|
||||
if (tx.isFieldPresent(sfDelegate) && isReserveSponsored(tx))
|
||||
return temINVALID;
|
||||
|
||||
if (!view.exists(keylet::account(tx.getAccountID(sfSponsor))))
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// Skip Sponsorship existence checks if the sponsor has signed the transaction - this
|
||||
// transaction is valid regardless of the Sponsorship object.
|
||||
// The use of the Sponsorship object is properly handled in
|
||||
// getFeePayer/checkReserve/increaseOwnerCount/decreaseOwnerCount.
|
||||
if (tx.isFieldPresent(sfSponsorSignature))
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the transaction contains sfDelegate, the Sponsorship object should be
|
||||
// between the sponsor and the delegate.
|
||||
auto const sponsorshipSle =
|
||||
view.read(keylet::sponsorship(tx.getAccountID(sfSponsor), tx.getInitiator()));
|
||||
|
||||
// sponsorship object missing for pre-funded (no co-signing) tx
|
||||
if (!sponsorshipSle)
|
||||
return terNO_PERMISSION;
|
||||
|
||||
if (isFeeSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee))
|
||||
return terNO_PERMISSION;
|
||||
|
||||
if (isReserveSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve))
|
||||
return terNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
@@ -347,6 +454,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
// The computation has two parts:
|
||||
// * The base fee, which is the same for most transactions.
|
||||
// * The additional cost of each multisignature on the transaction.
|
||||
// * The additional cost of each multisignature on the sponsor.
|
||||
XRPAmount const baseFee = view.fees().base;
|
||||
|
||||
// Each signer adds one more baseFee to the minimum required fee
|
||||
@@ -354,7 +462,24 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
std::size_t const signerCount =
|
||||
tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0;
|
||||
|
||||
return baseFee + (signerCount * baseFee);
|
||||
std::size_t sponsorSignerCount = 0;
|
||||
if (tx.isFieldPresent(sfSponsorSignature))
|
||||
{
|
||||
auto const sponsorObj = tx.getFieldObject(sfSponsorSignature);
|
||||
if (sponsorObj.isFieldPresent(sfSigners))
|
||||
sponsorSignerCount += sponsorObj.getFieldArray(sfSigners).size();
|
||||
}
|
||||
|
||||
return baseFee + ((signerCount + sponsorSignerCount) * baseFee);
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
Transactor::calculateBaseFee(
|
||||
ReadView const& view,
|
||||
STTx const& tx,
|
||||
std::uint32_t extraBaseFeeMultiplier)
|
||||
{
|
||||
return calculateBaseFee(view, tx) + view.fees().base * extraBaseFeeMultiplier;
|
||||
}
|
||||
|
||||
// Returns the fee in fee units, not scaled for load.
|
||||
@@ -424,12 +549,51 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
if (feePaid == beast::kZero)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const id = ctx.tx.getFeePayer();
|
||||
auto const sle = ctx.view.read(keylet::account(id));
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
auto const feePayer = getFeePayer(ctx.view, ctx.tx);
|
||||
auto const payerSle = ctx.view.read(feePayer.keylet);
|
||||
|
||||
auto const balance = (*sle)[sfBalance].xrp();
|
||||
if (!payerSle)
|
||||
{
|
||||
if (feePayer.type == FeePayerType::SponsorPreFunded)
|
||||
{
|
||||
// Sanity check: already checked in checkSponsor
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
return terNO_ACCOUNT;
|
||||
}
|
||||
|
||||
XRPAmount maxSpendable = beast::kZero;
|
||||
|
||||
if (feePayer.type == FeePayerType::SponsorPreFunded)
|
||||
{
|
||||
if (payerSle->getType() != ltSPONSORSHIP)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (payerSle->isFieldPresent(feePayer.balanceField))
|
||||
maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp();
|
||||
|
||||
if (payerSle->isFieldPresent(sfMaxFee))
|
||||
{
|
||||
auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp();
|
||||
maxSpendable = std::min(maxSpendable, cap);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (payerSle->getType() != ltACCOUNT_ROOT)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (feePayer.type == FeePayerType::SponsorCoSigned)
|
||||
{
|
||||
auto const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j);
|
||||
maxSpendable = payerSle->getFieldAmount(sfBalance).xrp() - sponsorReserve;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp();
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Because preclaim evaluates against a static readview, it
|
||||
// does not reflect fee deductions from other transactions paid by
|
||||
@@ -438,12 +602,12 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
// transactions, this check may pass optimistically.
|
||||
// The fee shortfall will be handled by the Transactor::reset mechanism,
|
||||
// which caps the fee to the remaining actual balance.
|
||||
if (balance < feePaid)
|
||||
if (maxSpendable < feePaid)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(balance)
|
||||
JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(maxSpendable)
|
||||
<< " paid=" << to_string(feePaid);
|
||||
|
||||
if ((balance > beast::kZero) && !ctx.view.open())
|
||||
if ((maxSpendable > beast::kZero) && !ctx.view.open())
|
||||
{
|
||||
// Closed ledger, non-zero balance, less than fee
|
||||
return tecINSUFF_FEE;
|
||||
@@ -460,16 +624,72 @@ Transactor::payFee()
|
||||
{
|
||||
auto const feePaid = ctx_.tx[sfFee].xrp();
|
||||
|
||||
auto const feePayer = ctx_.tx.getFeePayer();
|
||||
auto const sle = view().peek(keylet::account(feePayer));
|
||||
auto const feePayer = getFeePayer(view(), ctx_.tx);
|
||||
auto const sle = view().peek(feePayer.keylet);
|
||||
|
||||
JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id);
|
||||
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Deduct the fee, so it's not available during the transaction.
|
||||
// Will only write the account back if the transaction succeeds.
|
||||
sle->setFieldAmount(sfBalance, sle->getFieldAmount(sfBalance) - feePaid);
|
||||
if (feePayer != accountID_)
|
||||
view().update(sle); // done in `apply()` for the account
|
||||
if (feePaid == beast::kZero)
|
||||
return tesSUCCESS;
|
||||
|
||||
XRPAmount balance = beast::kZero;
|
||||
if (sle->isFieldPresent(feePayer.balanceField))
|
||||
{
|
||||
balance = sle->getFieldAmount(feePayer.balanceField).xrp();
|
||||
}
|
||||
else if (feePayer.balanceField != sfFeeAmount)
|
||||
{
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// A co-signed sponsor pays the fee out of its own account balance, but must
|
||||
// never be charged into its account reserve, and a pre-funded sponsorship's
|
||||
// fee is capped by sfMaxFee. Mirror the spendable amount computed in
|
||||
// checkFee() so both limits are enforced on the apply path too.
|
||||
XRPAmount spendable = balance;
|
||||
if (feePayer.type == FeePayerType::SponsorCoSigned)
|
||||
{
|
||||
auto const sponsorReserve = accountReserve(view(), sle, j_);
|
||||
// max(balance - reserve, 0) with overflow handling
|
||||
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
|
||||
}
|
||||
else if (feePayer.type == FeePayerType::SponsorPreFunded && sle->isFieldPresent(sfMaxFee))
|
||||
{
|
||||
auto const cap = sle->getFieldAmount(sfMaxFee).xrp();
|
||||
spendable = std::min(spendable, cap);
|
||||
}
|
||||
|
||||
// Only sponsor fee-payers reject here on insufficient funds. For an
|
||||
// ordinary account, the fee falls through and is capped by reset(), which
|
||||
// caps to the account's balance. That capping is wrong for sponsors: a
|
||||
// co-signed sponsor would be charged into its own reserve, and a prefunded
|
||||
// sponsorship's fee amount should be rejected rather than partially spent.
|
||||
if (feePaid > spendable &&
|
||||
(feePayer.type == FeePayerType::SponsorPreFunded ||
|
||||
feePayer.type == FeePayerType::SponsorCoSigned))
|
||||
{
|
||||
if ((spendable > beast::kZero) && !view().open())
|
||||
return tecINSUFF_FEE;
|
||||
|
||||
return terINSUF_FEE_B;
|
||||
}
|
||||
|
||||
auto const feeAmountAfter = balance - feePaid;
|
||||
|
||||
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
|
||||
{
|
||||
// Because ltSponsorship.sfFeeAmount is soeOptional
|
||||
sle->makeFieldAbsent(feePayer.balanceField);
|
||||
}
|
||||
else
|
||||
{
|
||||
sle->setFieldAmount(feePayer.balanceField, feeAmountAfter);
|
||||
}
|
||||
|
||||
view().update(sle);
|
||||
|
||||
// VFALCO Should we call view().rawDestroyXRP() here as well?
|
||||
return tesSUCCESS;
|
||||
@@ -529,7 +749,7 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
|
||||
}
|
||||
|
||||
// Transaction can never succeed if the Ticket is not in the ledger.
|
||||
if (!view.exists(keylet::kTicket(id, tSeqProx)))
|
||||
if (!view.exists(keylet::ticket(id, tSeqProx)))
|
||||
{
|
||||
JLOG(j.trace()) << "applyTransaction: ticket already used or never created "
|
||||
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
|
||||
@@ -594,7 +814,7 @@ Transactor::ticketDelete(
|
||||
{
|
||||
// Delete the Ticket, adjust the account root ticket count, and
|
||||
// reduce the owner count.
|
||||
SLE::pointer const sleTicket = view.peek(keylet::kTicket(ticketIndex));
|
||||
SLE::pointer const sleTicket = view.peek(keylet::ticket(ticketIndex));
|
||||
if (!sleTicket)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -643,7 +863,7 @@ Transactor::ticketDelete(
|
||||
}
|
||||
|
||||
// Update the Ticket owner's reserve.
|
||||
adjustOwnerCount(view, sleAccount, -1, j);
|
||||
decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j);
|
||||
|
||||
// Remove Ticket from ledger.
|
||||
view.erase(sleTicket);
|
||||
@@ -700,23 +920,26 @@ Transactor::checkSign(
|
||||
std::optional<uint256 const> const& parentBatchId,
|
||||
AccountID const& idAccount,
|
||||
STObject const& sigObject,
|
||||
beast::Journal const j)
|
||||
beast::Journal const j,
|
||||
bool permitUncreatedAccount)
|
||||
{
|
||||
{
|
||||
auto const sle = view.read(keylet::account(idAccount));
|
||||
|
||||
if (view.rules().enabled(featureLendingProtocol) && isPseudoAccount(sle))
|
||||
if ((view.rules().enabled(featureLendingProtocol) ||
|
||||
view.rules().enabled(featureBatchV1_1) || view.rules().enabled(fixCleanup3_3_0)) &&
|
||||
isPseudoAccount(sle))
|
||||
{
|
||||
// Pseudo-accounts can't sign transactions. This check is gated on
|
||||
// the Lending Protocol amendment because that's the project it was
|
||||
// added under, and it doesn't justify another amendment
|
||||
// Pseudo-accounts can't sign transactions. This check is gated on a
|
||||
// few different amendments so that it takes effect as soon as any of
|
||||
// them is activated.
|
||||
return tefBAD_AUTH;
|
||||
}
|
||||
}
|
||||
|
||||
auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
|
||||
// Ignore signature check on batch inner transactions
|
||||
if (parentBatchId && view.rules().enabled(featureBatch))
|
||||
if (parentBatchId && view.rules().enabled(featureBatchV1_1))
|
||||
{
|
||||
// Defensive Check: These values are also checked in Batch::preflight
|
||||
if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
|
||||
@@ -734,6 +957,21 @@ Transactor::checkSign(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (sigObject.isFieldPresent(sfSponsorSignature))
|
||||
{
|
||||
// Co-signed sponsorship
|
||||
|
||||
// Sanity check: already checked in preflight1
|
||||
if (!sigObject.isFieldPresent(sfSponsor))
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsorID = sigObject.getAccountID(sfSponsor);
|
||||
auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature);
|
||||
if (auto const ret = checkSign(view, flags, std::nullopt, sponsorID, sponsorSignature, j);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// If the pk is empty and not simulate or simulate and signers,
|
||||
// then we must be multi-signing.
|
||||
if (sigObject.isFieldPresent(sfSigners))
|
||||
@@ -754,7 +992,16 @@ Transactor::checkSign(
|
||||
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
|
||||
auto const sleAccount = view.read(keylet::account(idAccount));
|
||||
if (!sleAccount)
|
||||
return terNO_ACCOUNT;
|
||||
{
|
||||
// An account that does not exist yet can only be authorized by its own
|
||||
// master key, and only where an un-created signer is permitted (a batch
|
||||
// whose earlier inner creates the account). Otherwise it cannot sign.
|
||||
if (!permitUncreatedAccount)
|
||||
return terNO_ACCOUNT;
|
||||
if (idAccount != idSigner)
|
||||
return tefBAD_AUTH;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
return checkSingleSign(view, idSigner, idAccount, sleAccount, j);
|
||||
}
|
||||
@@ -767,50 +1014,6 @@ Transactor::checkSign(PreclaimContext const& ctx)
|
||||
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Transactor::checkBatchSign(PreclaimContext const& ctx)
|
||||
{
|
||||
NotTEC ret = tesSUCCESS;
|
||||
STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
auto const idAccount = signer.getAccountID(sfAccount);
|
||||
|
||||
Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
|
||||
if (pkSigner.empty())
|
||||
{
|
||||
if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
if (!publicKeyType(makeSlice(pkSigner)))
|
||||
return tefBAD_AUTH;
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
|
||||
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
|
||||
|
||||
// A batch can include transactions from an un-created account ONLY
|
||||
// when the account master key is the signer
|
||||
if (!sleAccount)
|
||||
{
|
||||
if (idAccount != idSigner)
|
||||
return tefBAD_AUTH;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (ret = checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Transactor::checkSingleSign(
|
||||
ReadView const& view,
|
||||
@@ -852,7 +1055,7 @@ Transactor::checkMultiSign(
|
||||
beast::Journal const j)
|
||||
{
|
||||
// Get id's SignerList and Quorum.
|
||||
STLedgerEntry::const_pointer const sleAccountSigners = view.read(keylet::signers(id));
|
||||
STLedgerEntry::const_pointer const sleAccountSigners = view.read(keylet::signerList(id));
|
||||
// If the signer list doesn't exist the account is not multi-signing.
|
||||
if (!sleAccountSigners)
|
||||
{
|
||||
@@ -1032,7 +1235,7 @@ removeExpiredNFTokenOffers(
|
||||
|
||||
for (auto const& index : offers)
|
||||
{
|
||||
if (auto const offer = view.peek(keylet::nftoffer(index)))
|
||||
if (auto const offer = view.peek(keylet::nftokenOffer(index)))
|
||||
{
|
||||
nft::deleteTokenOffer(view, offer);
|
||||
if (++removed == kExpiredOfferRemoveLimit)
|
||||
@@ -1081,10 +1284,11 @@ removeDeletedTrustLines(
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset the context, discarding any changes made and adjust the fee.
|
||||
|
||||
@param fee The transaction fee to be charged.
|
||||
@return A pair containing the transaction result and the actual fee charged.
|
||||
/**
|
||||
* Reset the context, discarding any changes made and adjust the fee.
|
||||
*
|
||||
* @param fee The transaction fee to be charged.
|
||||
* @return A pair containing the transaction result and the actual fee charged.
|
||||
*/
|
||||
std::pair<TER, XRPAmount>
|
||||
Transactor::reset(XRPAmount fee)
|
||||
@@ -1098,22 +1302,49 @@ Transactor::reset(XRPAmount fee)
|
||||
if (!txnAcct)
|
||||
return {tefINTERNAL, beast::kZero};
|
||||
|
||||
auto const payerSle = view().peek(keylet::account(ctx_.tx.getFeePayer()));
|
||||
auto const feePayer = getFeePayer(view(), ctx_.tx);
|
||||
auto const payerSle = view().peek(feePayer.keylet);
|
||||
|
||||
if (!payerSle)
|
||||
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
|
||||
|
||||
auto const balance = payerSle->getFieldAmount(sfBalance).xrp();
|
||||
XRPAmount balance = beast::kZero;
|
||||
if (payerSle->isFieldPresent(feePayer.balanceField))
|
||||
{
|
||||
balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
|
||||
}
|
||||
else if (feePayer.balanceField != sfFeeAmount)
|
||||
{
|
||||
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee))
|
||||
{
|
||||
auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp();
|
||||
fee = std::min(fee, cap);
|
||||
}
|
||||
|
||||
// A co-signed sponsor must never be charged into its own account reserve,
|
||||
// so the fee is capped to the balance above the reserve rather than to the
|
||||
// full balance.
|
||||
XRPAmount spendable = balance;
|
||||
if (feePayer.type == FeePayerType::SponsorCoSigned)
|
||||
{
|
||||
auto const sponsorReserve = accountReserve(view(), payerSle, j_);
|
||||
// max(balance - reserve, 0) with overflow handling
|
||||
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
|
||||
}
|
||||
|
||||
// balance should have already been checked in checkFee / preFlight.
|
||||
XRPL_ASSERT(
|
||||
balance != beast::kZero && (!view().open() || balance >= fee),
|
||||
(fee == beast::kZero || balance != beast::kZero) && (!view().open() || balance >= fee),
|
||||
"xrpl::Transactor::reset : valid balance");
|
||||
|
||||
// We retry/reject the transaction if the account balance is zero or
|
||||
// we're applying against an open ledger and the balance is less than
|
||||
// the fee
|
||||
if (fee > balance)
|
||||
fee = balance;
|
||||
if (fee > spendable)
|
||||
fee = spendable;
|
||||
|
||||
// Since we reset the context, we need to charge the fee and update
|
||||
// the account's sequence number (or consume the Ticket) again.
|
||||
@@ -1121,7 +1352,17 @@ Transactor::reset(XRPAmount fee)
|
||||
// If for some reason we are unable to consume the ticket or sequence
|
||||
// then the ledger is corrupted. Rather than make things worse we
|
||||
// reject the transaction.
|
||||
payerSle->setFieldAmount(sfBalance, balance - fee);
|
||||
auto const feeAmountAfter = balance - fee;
|
||||
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
|
||||
{
|
||||
// Because ltSponsorship.sfFeeAmount is soeOptional
|
||||
payerSle->makeFieldAbsent(feePayer.balanceField);
|
||||
}
|
||||
else
|
||||
{
|
||||
payerSle->setFieldAmount(feePayer.balanceField, feeAmountAfter);
|
||||
}
|
||||
|
||||
TER const ter{consumeSeqProxy(txnAcct)};
|
||||
XRPL_ASSERT(isTesSuccess(ter), "xrpl::Transactor::reset : result is tesSUCCESS");
|
||||
|
||||
@@ -1135,6 +1376,48 @@ Transactor::reset(XRPAmount fee)
|
||||
return {ter, fee};
|
||||
}
|
||||
|
||||
FeePayer
|
||||
Transactor::getFeePayer(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx))
|
||||
{
|
||||
auto const sponsorID = tx.getAccountID(sfSponsor);
|
||||
auto const sponseeID = tx.getInitiator();
|
||||
auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID);
|
||||
|
||||
// if pre-funded sponsorship exists, prefer it
|
||||
if (view.exists(sponsorshipKeylet))
|
||||
{
|
||||
// pre funded
|
||||
return FeePayer{
|
||||
.id = sponsorID,
|
||||
.keylet = sponsorshipKeylet,
|
||||
.balanceField = sfFeeAmount,
|
||||
.type = FeePayerType::SponsorPreFunded};
|
||||
}
|
||||
|
||||
// Checked in Transactor::checkSponsor
|
||||
XRPL_ASSERT(
|
||||
tx.isFieldPresent(sfSponsorSignature),
|
||||
"xrpl::getFeePayer has sponsor signature without a sponsorship object");
|
||||
|
||||
// co-signed
|
||||
return FeePayer{
|
||||
.id = sponsorID,
|
||||
.keylet = keylet::account(sponsorID),
|
||||
.balanceField = sfBalance,
|
||||
.type = FeePayerType::SponsorCoSigned};
|
||||
}
|
||||
|
||||
AccountID const payerID = tx.getInitiator();
|
||||
auto const payerAccountKeylet = keylet::account(payerID);
|
||||
auto const payerType =
|
||||
tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account;
|
||||
|
||||
return FeePayer{
|
||||
.id = payerID, .keylet = payerAccountKeylet, .balanceField = sfBalance, .type = payerType};
|
||||
}
|
||||
|
||||
// The sole purpose of this function is to provide a convenient, named
|
||||
// location to set a breakpoint, to be used when replaying transactions.
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user