Files
rippled/src/libxrpl/ledger/PaymentSandbox.cpp
2026-04-22 17:42:15 +00:00

602 lines
17 KiB
C++

#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/RawView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <tuple>
#include <utility>
namespace xrpl {
namespace detail {
auto
DeferredCredits::makeKeyIOU(AccountID const& a1, AccountID const& a2, Currency const& c) -> KeyIOU
{
if (a1 < a2)
{
return std::make_tuple(a1, a2, c);
}
return std::make_tuple(a2, a1, c);
}
void
DeferredCredits::creditIOU(
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
STAmount const& preCreditSenderBalance)
{
XRPL_ASSERT(
sender != receiver, "xrpl::detail::DeferredCredits::creditIOU : sender is not receiver");
XRPL_ASSERT(!amount.negative(), "xrpl::detail::DeferredCredits::creditIOU : positive amount");
XRPL_ASSERT(
amount.holds<Issue>(), "xrpl::detail::DeferredCredits::creditIOU : amount is for Issue");
auto const k = makeKeyIOU(sender, receiver, amount.get<Issue>().currency);
auto i = creditsIOU_.find(k);
if (i == creditsIOU_.end())
{
ValueIOU v;
if (sender < receiver)
{
v.highAcctCredits = amount;
v.lowAcctCredits = amount.zeroed();
v.lowAcctOrigBalance = preCreditSenderBalance;
}
else
{
v.highAcctCredits = amount.zeroed();
v.lowAcctCredits = amount;
v.lowAcctOrigBalance = -preCreditSenderBalance;
}
creditsIOU_[k] = v;
}
else
{
// only record the balance the first time, do not record it here
auto& v = i->second;
if (sender < receiver)
{
v.highAcctCredits += amount;
}
else
{
v.lowAcctCredits += amount;
}
}
}
void
DeferredCredits::creditMPT(
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer)
{
XRPL_ASSERT(
amount.holds<MPTIssue>(),
"xrpl::detail::DeferredCredits::creditMPT : amount is for MPTIssue");
XRPL_ASSERT(!amount.negative(), "xrpl::detail::DeferredCredits::creditMPT : positive amount");
XRPL_ASSERT(
sender != receiver, "xrpl::detail::DeferredCredits::creditMPT : sender is not receiver");
auto const mptAmtVal = amount.mpt().value();
auto const& issuer = amount.getIssuer();
auto const& mptIssue = amount.get<MPTIssue>();
auto const& mptID = mptIssue.getMptID();
bool const isSenderIssuer = sender == issuer;
auto i = creditsMPT_.find(mptID);
if (i == creditsMPT_.end())
{
IssuerValueMPT v;
if (isSenderIssuer)
{
v.credit = mptAmtVal;
v.holders[receiver].origBalance = preCreditBalanceHolder;
}
else
{
v.holders[sender].debit = mptAmtVal;
v.holders[sender].origBalance = preCreditBalanceHolder;
}
v.origBalance = preCreditBalanceIssuer;
creditsMPT_.emplace(mptID, std::move(v));
}
else
{
// only record the balance the first time, do not record it here
auto& v = i->second;
if (isSenderIssuer)
{
v.credit += mptAmtVal;
if (!v.holders.contains(receiver))
{
v.holders[receiver].origBalance = preCreditBalanceHolder;
}
}
else
{
if (!v.holders.contains(sender))
{
v.holders[sender].debit = mptAmtVal;
v.holders[sender].origBalance = preCreditBalanceHolder;
}
else
{
v.holders[sender].debit += mptAmtVal;
}
}
}
}
void
DeferredCredits::issuerSelfDebitMPT(
MPTIssue const& issue,
std::uint64_t amount,
std::int64_t origBalance)
{
auto const& mptID = issue.getMptID();
auto i = creditsMPT_.find(mptID);
if (i == creditsMPT_.end())
{
IssuerValueMPT v;
v.origBalance = origBalance;
v.selfDebit = amount;
creditsMPT_.emplace(mptID, std::move(v));
}
else
{
i->second.selfDebit += amount;
}
}
void
DeferredCredits::ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next)
{
auto const v = std::max(cur, next);
auto r = ownerCounts_.emplace(id, v);
if (!r.second)
{
auto& mapVal = r.first->second;
mapVal = std::max(v, mapVal);
}
}
std::optional<std::uint32_t>
DeferredCredits::ownerCount(AccountID const& id) const
{
auto i = ownerCounts_.find(id);
if (i != ownerCounts_.end())
return i->second;
return std::nullopt;
}
// Get the adjustments for the balance between main and other.
auto
DeferredCredits::adjustmentsIOU(
AccountID const& main,
AccountID const& other,
Currency const& currency) const -> std::optional<AdjustmentIOU>
{
std::optional<AdjustmentIOU> result;
KeyIOU const k = makeKeyIOU(main, other, currency);
auto i = creditsIOU_.find(k);
if (i == creditsIOU_.end())
return result;
auto const& v = i->second;
if (main < other)
{
result.emplace(v.highAcctCredits, v.lowAcctCredits, v.lowAcctOrigBalance);
return result;
}
result.emplace(v.lowAcctCredits, v.highAcctCredits, -v.lowAcctOrigBalance);
return result;
}
auto
DeferredCredits::adjustmentsMPT(xrpl::MPTID const& mptID) const -> std::optional<AdjustmentMPT>
{
auto i = creditsMPT_.find(mptID);
if (i == creditsMPT_.end())
return std::nullopt;
return i->second;
}
void
DeferredCredits::apply(DeferredCredits& to)
{
for (auto const& i : creditsIOU_)
{
auto r = to.creditsIOU_.emplace(i);
if (!r.second)
{
auto& toVal = r.first->second;
auto const& fromVal = i.second;
toVal.lowAcctCredits += fromVal.lowAcctCredits;
toVal.highAcctCredits += fromVal.highAcctCredits;
// Do not update the orig balance, it's already correct
}
}
for (auto const& i : creditsMPT_)
{
auto r = to.creditsMPT_.emplace(i);
if (!r.second)
{
auto& toVal = r.first->second;
auto const& fromVal = i.second;
toVal.credit += fromVal.credit;
toVal.selfDebit += fromVal.selfDebit;
for (auto& [k, v] : fromVal.holders)
{
if (!toVal.holders.contains(k))
{
toVal.holders[k] = v;
}
else
{
toVal.holders[k].debit += v.debit;
}
}
// Do not update the orig balance, it's already correct
}
}
for (auto const& i : ownerCounts_)
{
auto r = to.ownerCounts_.emplace(i);
if (!r.second)
{
auto& toVal = r.first->second;
auto const& fromVal = i.second;
toVal = std::max(toVal, fromVal);
}
}
}
} // namespace detail
STAmount
PaymentSandbox::balanceHookIOU(
AccountID const& account,
AccountID const& issuer,
STAmount const& amount) const
{
XRPL_ASSERT(amount.holds<Issue>(), "balanceHookIOU: amount is for Issue");
/*
There are two algorithms here. The pre-switchover algorithm takes the
current amount and subtracts the recorded credits. The post-switchover
algorithm remembers the original balance, and subtracts the debits. The
post-switchover algorithm should be more numerically stable. Consider a
large credit with a small initial balance. The pre-switchover algorithm
computes (B+C)-C (where B+C will the amount passed in). The
post-switchover algorithm returns B. When B and C differ by large
magnitudes, (B+C)-C may not equal B.
*/
auto const& currency = amount.get<Issue>().currency;
auto delta = amount.zeroed();
auto lastBal = amount;
auto minBal = amount;
for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_)
{
if (auto adj = curSB->tab_.adjustmentsIOU(account, issuer, currency))
{
delta += adj->debits;
lastBal = adj->origBalance;
if (lastBal < minBal)
minBal = lastBal;
}
}
// The adjusted amount should never be larger than the balance. In
// some circumstances, it is possible for the deferred credits table
// to compute usable balance just slightly above what the ledger
// calculates (but always less than the actual balance).
auto adjustedAmt = std::min({amount, lastBal - delta, minBal});
adjustedAmt.get<Issue>().account = amount.getIssuer();
if (isXRP(issuer) && adjustedAmt < beast::zero)
{
// A calculated negative XRP balance is not an error case. Consider a
// payment snippet that credits a large XRP amount and then debits the
// same amount. The credit can't be used, but we subtract the debit and
// calculate a negative value. It's not an error case.
adjustedAmt.clear();
}
return adjustedAmt;
}
STAmount
PaymentSandbox::balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount)
const
{
auto const& issuer = issue.getIssuer();
bool const accountIsHolder = account != issuer;
std::int64_t delta = 0;
std::int64_t lastBal = amount;
std::int64_t minBal = amount;
for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_)
{
if (auto adj = curSB->tab_.adjustmentsMPT(issue))
{
if (accountIsHolder)
{
if (auto const i = adj->holders.find(account); i != adj->holders.end())
{
delta += i->second.debit;
lastBal = i->second.origBalance;
}
}
else
{
delta += adj->credit;
lastBal = adj->origBalance;
}
minBal = std::min(lastBal, minBal);
}
}
// The adjusted amount should never be larger than the balance.
auto const adjustedAmt = std::min({amount, lastBal - delta, minBal});
return adjustedAmt > 0 ? STAmount{issue, adjustedAmt} : STAmount{issue};
}
STAmount
PaymentSandbox::balanceHookSelfIssueMPT(xrpl::MPTIssue const& issue, std::int64_t amount) const
{
std::int64_t selfDebited = 0;
std::int64_t lastBal = amount;
for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_)
{
if (auto adj = curSB->tab_.adjustmentsMPT(issue))
{
selfDebited += adj->selfDebit;
lastBal = adj->origBalance;
}
}
if (lastBal > selfDebited)
return STAmount{issue, lastBal - selfDebited};
return STAmount{issue};
}
std::uint32_t
PaymentSandbox::ownerCountHook(AccountID const& account, std::uint32_t count) const
{
std::uint32_t result = count;
for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_)
{
if (auto adj = curSB->tab_.ownerCount(account))
result = std::max(result, *adj);
}
return result;
}
void
PaymentSandbox::creditHookIOU(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
STAmount const& preCreditBalance)
{
XRPL_ASSERT(amount.holds<Issue>(), "creditHookIOU: amount is for Issue");
tab_.creditIOU(from, to, amount, preCreditBalance);
}
void
PaymentSandbox::creditHookMPT(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer)
{
XRPL_ASSERT(amount.holds<MPTIssue>(), "creditHookMPT: amount is for MPTIssue");
tab_.creditMPT(from, to, amount, preCreditBalanceHolder, preCreditBalanceIssuer);
}
void
PaymentSandbox::issuerSelfDebitHookMPT(
MPTIssue const& issue,
std::uint64_t amount,
std::int64_t origBalance)
{
XRPL_ASSERT(amount > 0, "PaymentSandbox::issuerSelfDebitHookMPT: amount must be > 0");
tab_.issuerSelfDebitMPT(issue, amount, origBalance);
}
void
PaymentSandbox::adjustOwnerCountHook(
AccountID const& account,
std::uint32_t cur,
std::uint32_t next)
{
tab_.ownerCount(account, cur, next);
}
void
PaymentSandbox::apply(RawView& to)
{
XRPL_ASSERT(!ps_, "xrpl::PaymentSandbox::apply : non-null sandbox");
items_.apply(to);
}
void
PaymentSandbox::apply(PaymentSandbox& to)
{
XRPL_ASSERT(ps_ == &to, "xrpl::PaymentSandbox::apply : matching sandbox");
items_.apply(to);
tab_.apply(to.tab_);
}
std::map<std::tuple<AccountID, AccountID, Currency>, STAmount>
PaymentSandbox::balanceChanges(ReadView const& view) const
{
using key_t = std::tuple<AccountID, AccountID, Currency>;
// Map of delta trust lines. As a special case, when both ends of the trust
// line are the same currency, then it's delta currency for that issuer. To
// get the change in XRP balance, Account == root, issuer == root, currency
// == XRP
std::map<key_t, STAmount> result;
// populate a dictionary with low/high/currency/delta. This can be
// compared with the other versions payment code.
auto each = [&result](
uint256 const& key,
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) {
STAmount oldBalance;
STAmount newBalance;
AccountID lowID;
AccountID highID;
// before is read from prev view
if (isDelete)
{
if (!before)
return;
auto const bt = before->getType();
switch (bt)
{
case ltACCOUNT_ROOT:
lowID = xrpAccount();
highID = (*before)[sfAccount];
oldBalance = (*before)[sfBalance];
newBalance = oldBalance.zeroed();
break;
case ltRIPPLE_STATE:
lowID = (*before)[sfLowLimit].getIssuer();
highID = (*before)[sfHighLimit].getIssuer();
oldBalance = (*before)[sfBalance];
newBalance = oldBalance.zeroed();
break;
case ltOFFER:
// TBD
break;
default:
break;
}
}
else if (!before)
{
// insert
auto const at = after->getType();
switch (at)
{
case ltACCOUNT_ROOT:
lowID = xrpAccount();
highID = (*after)[sfAccount];
newBalance = (*after)[sfBalance];
oldBalance = newBalance.zeroed();
break;
case ltRIPPLE_STATE:
lowID = (*after)[sfLowLimit].getIssuer();
highID = (*after)[sfHighLimit].getIssuer();
newBalance = (*after)[sfBalance];
oldBalance = newBalance.zeroed();
break;
case ltOFFER:
// TBD
break;
default:
break;
}
}
else
{
// modify
auto const at = after->getType();
XRPL_ASSERT(
at == before->getType(),
"xrpl::PaymentSandbox::balanceChanges : after and before "
"types matching");
switch (at)
{
case ltACCOUNT_ROOT:
lowID = xrpAccount();
highID = (*after)[sfAccount];
oldBalance = (*before)[sfBalance];
newBalance = (*after)[sfBalance];
break;
case ltRIPPLE_STATE:
lowID = (*after)[sfLowLimit].getIssuer();
highID = (*after)[sfHighLimit].getIssuer();
oldBalance = (*before)[sfBalance];
newBalance = (*after)[sfBalance];
break;
case ltOFFER:
// TBD
break;
default:
break;
}
}
// The following are now set, put them in the map
auto delta = newBalance - oldBalance;
auto const cur = newBalance.get<Issue>().currency;
result[std::make_tuple(lowID, highID, cur)] = delta;
auto r = result.emplace(std::make_tuple(lowID, lowID, cur), delta);
if (r.second)
{
r.first->second += delta;
}
delta.negate();
r = result.emplace(std::make_tuple(highID, highID, cur), delta);
if (r.second)
{
r.first->second += delta;
}
};
items_.visit(view, each);
return result;
}
XRPAmount
PaymentSandbox::xrpDestroyed() const
{
return items_.dropsDestroyed();
}
} // namespace xrpl