mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-01 01:22:27 +00:00
Compare commits
48 Commits
tapanito/f
...
ximinez/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50dfaea465 | ||
|
|
adb0d7ecea | ||
|
|
88461b5d83 | ||
|
|
0bb0e5c6c0 | ||
|
|
a5af69171c | ||
|
|
9b349510b8 | ||
|
|
a495b41179 | ||
|
|
ae41a712b3 | ||
|
|
7684f9dd58 | ||
|
|
7c34be898d | ||
|
|
5e282f49da | ||
|
|
86722689ac | ||
|
|
3759144bba | ||
|
|
16c41c2143 | ||
|
|
af28042946 | ||
|
|
734426554d | ||
|
|
7f17daa95f | ||
|
|
f359cd8dad | ||
|
|
bf0b10404d | ||
|
|
d019ebaf36 | ||
|
|
b6e4620349 | ||
|
|
db0ef6a370 | ||
|
|
11a45a0ac2 | ||
|
|
aa035f4cfd | ||
|
|
8988f9117f | ||
|
|
ae4f379845 | ||
|
|
671aa11649 | ||
|
|
53d35fd8ea | ||
|
|
0c7ea2e333 | ||
|
|
5f54be25e9 | ||
|
|
d82756519c | ||
|
|
1f23832659 | ||
|
|
4c50969bde | ||
|
|
aabdf372dd | ||
|
|
c6d63a4b90 | ||
|
|
1e6c3208db | ||
|
|
a74f223efb | ||
|
|
1eb3a3ea5a | ||
|
|
630e428929 | ||
|
|
3f93edc5e0 | ||
|
|
baf62689ff | ||
|
|
ddf7d6cac4 | ||
|
|
fcd2ea2d6e | ||
|
|
a16aa5b12f | ||
|
|
ef2de81870 | ||
|
|
fce6757260 | ||
|
|
d759a0a2b0 | ||
|
|
d2dda416e8 |
@@ -1155,86 +1155,57 @@ rippleSendMultiMPT(
|
||||
beast::Journal j,
|
||||
WaiveTransferFee waiveFee)
|
||||
{
|
||||
// Safe to get MPT since rippleSendMultiMPT is only called by
|
||||
// accountSendMultiMPT
|
||||
auto const& issuer = mptIssue.getIssuer();
|
||||
|
||||
auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()));
|
||||
if (!sle)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// For the issuer-as-sender case, track the running total to validate
|
||||
// against MaximumAmount. The read-only SLE (view.read) is not updated
|
||||
// by rippleCreditMPT, so a per-iteration SLE read would be stale.
|
||||
// Use int64_t, not STAmount, to keep MaximumAmount comparisons in exact
|
||||
// integer arithmetic. STAmount implicitly converts to Number, whose
|
||||
// small-scale mantissa (~16 digits) can lose precision for values near
|
||||
// maxMPTokenAmount (19 digits).
|
||||
std::uint64_t totalSendAmount{0};
|
||||
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
|
||||
auto const outstandingAmount = sle->getFieldU64(sfOutstandingAmount);
|
||||
|
||||
// actual accumulates the total cost to the sender (includes transfer
|
||||
// fees for third-party transit sends). takeFromSender accumulates only
|
||||
// the transit portion that is debited to the issuer in bulk after the
|
||||
// loop. They diverge when there are transfer fees.
|
||||
// These may diverge
|
||||
STAmount takeFromSender{mptIssue};
|
||||
actual = takeFromSender;
|
||||
|
||||
for (auto const& [receiverID, amt] : receivers)
|
||||
for (auto const& r : receivers)
|
||||
{
|
||||
STAmount const amount{mptIssue, amt};
|
||||
auto const& receiverID = r.first;
|
||||
STAmount amount{mptIssue, r.second};
|
||||
|
||||
if (amount < beast::zero)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (!amount || senderID == receiverID)
|
||||
/* If we aren't sending anything or if the sender is the same as the
|
||||
* receiver then we don't need to do anything.
|
||||
*/
|
||||
if (!amount || (senderID == receiverID))
|
||||
continue;
|
||||
|
||||
if (senderID == issuer || receiverID == issuer)
|
||||
{
|
||||
// if sender is issuer, check that the new OutstandingAmount will
|
||||
// not exceed MaximumAmount
|
||||
if (senderID == issuer)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
takeFromSender == beast::zero,
|
||||
"xrpl::rippleSendMultiMPT",
|
||||
"sender == issuer, takeFromSender == zero");
|
||||
|
||||
auto const sendAmount = amount.mpt().value();
|
||||
|
||||
if (view.rules().enabled(fixSecurity3_1_3))
|
||||
{
|
||||
// Post-fixSecurity3_1_3: aggregate MaximumAmount
|
||||
// check. Each condition guards the subtraction
|
||||
// in the next to prevent underflow.
|
||||
auto const exceedsMaximumAmount =
|
||||
// This send alone exceeds the max cap
|
||||
sendAmount > maximumAmount ||
|
||||
// The aggregate of all sends exceeds the max cap
|
||||
totalSendAmount > maximumAmount - sendAmount ||
|
||||
// Outstanding + aggregate exceeds the max cap
|
||||
outstandingAmount > maximumAmount - sendAmount - totalSendAmount;
|
||||
|
||||
if (exceedsMaximumAmount)
|
||||
return tecPATH_DRY;
|
||||
totalSendAmount += sendAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-fixSecurity3_1_3: per-iteration MaximumAmount
|
||||
// check. Reads sfOutstandingAmount from a stale
|
||||
// view.read() snapshot — incorrect for multi-destination
|
||||
// sends but retained for ledger replay compatibility.
|
||||
if (sendAmount > maximumAmount ||
|
||||
outstandingAmount > maximumAmount - sendAmount)
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
|
||||
if (sendAmount > maximumAmount ||
|
||||
sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount)
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
|
||||
// Direct send: redeeming MPTs and/or sending own MPTs.
|
||||
if (auto const ter = rippleCreditMPT(view, senderID, receiverID, amount, j))
|
||||
return ter;
|
||||
actual += amount;
|
||||
// Do not add amount to takeFromSender, because rippleCreditMPT
|
||||
// took it.
|
||||
// Do not add amount to takeFromSender, because rippleCreditMPT took
|
||||
// it
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -3273,93 +3272,6 @@ class MPToken_test : public beast::unit_test::suite
|
||||
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
|
||||
}
|
||||
|
||||
void
|
||||
testMultiSendMaximumAmount(FeatureBitset features)
|
||||
{
|
||||
// Verify that rippleSendMultiMPT correctly enforces MaximumAmount
|
||||
// when the issuer sends to multiple receivers. Pre-fixSecurity3_1_3,
|
||||
// a stale view.read() snapshot caused per-iteration checks to miss
|
||||
// aggregate overflows. Post-fix, a running total is used instead.
|
||||
testcase("Multi-send MaximumAmount enforcement");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const issuer("issuer");
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
std::uint64_t constexpr maxAmt = 150;
|
||||
Env env{*this, features};
|
||||
|
||||
MPTTester mptt(env, issuer, {.holders = {alice, bob}});
|
||||
mptt.create({.maxAmt = maxAmt, .ownerCount = 1, .flags = tfMPTCanTransfer});
|
||||
mptt.authorize({.account = alice});
|
||||
mptt.authorize({.account = bob});
|
||||
|
||||
Asset const asset{MPTIssue{mptt.issuanceID()}};
|
||||
|
||||
// Each test case creates a fresh ApplyView and calls
|
||||
// accountSendMulti from the issuer to the given receivers.
|
||||
auto const runTest = [&](MultiplePaymentDestinations const& receivers,
|
||||
TER expectedTer,
|
||||
std::optional<std::uint64_t> expectedOutstanding,
|
||||
std::string const& label) {
|
||||
ApplyViewImpl av(&*env.current(), tapNONE);
|
||||
auto const ter =
|
||||
accountSendMulti(av, issuer.id(), asset, receivers, env.app().getJournal("View"));
|
||||
BEAST_EXPECTS(ter == expectedTer, label);
|
||||
|
||||
// Only verify OutstandingAmount on success — on error the
|
||||
// view may contain partial state and must be discarded.
|
||||
if (expectedOutstanding)
|
||||
{
|
||||
auto const sle = av.peek(keylet::mptIssuance(mptt.issuanceID()));
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return;
|
||||
BEAST_EXPECTS(sle->getFieldU64(sfOutstandingAmount) == *expectedOutstanding, label);
|
||||
}
|
||||
};
|
||||
|
||||
using R = MultiplePaymentDestinations;
|
||||
|
||||
// Post-amendment: aggregate check with running total
|
||||
runTest(
|
||||
R{{alice.id(), 100}, {bob.id(), 100}},
|
||||
tecPATH_DRY,
|
||||
std::nullopt,
|
||||
"aggregate exceeds max");
|
||||
|
||||
runTest(R{{alice.id(), 75}, {bob.id(), 75}}, tesSUCCESS, maxAmt, "aggregate at boundary");
|
||||
|
||||
runTest(R{{alice.id(), 50}, {bob.id(), 50}}, tesSUCCESS, 100, "aggregate within limit");
|
||||
|
||||
runTest(
|
||||
R{{alice.id(), 150}, {bob.id(), 0}},
|
||||
tesSUCCESS,
|
||||
maxAmt,
|
||||
"one receiver at max, other zero");
|
||||
|
||||
runTest(
|
||||
R{{alice.id(), 151}, {bob.id(), 0}},
|
||||
tecPATH_DRY,
|
||||
std::nullopt,
|
||||
"one receiver exceeds max, other zero");
|
||||
|
||||
// Pre-amendment: the stale per-iteration check allows each
|
||||
// individual send (100 <= 150) even though the aggregate (200)
|
||||
// exceeds MaximumAmount. Preserved for ledger replay.
|
||||
{
|
||||
// KNOWN BUG (pre-fixSecurity3_1_3): preserved for ledger replay only
|
||||
env.disableFeature(fixSecurity3_1_3);
|
||||
runTest(
|
||||
R{{alice.id(), 100}, {bob.id(), 100}},
|
||||
tesSUCCESS,
|
||||
200,
|
||||
"pre-amendment allows over-send");
|
||||
env.enableFeature(fixSecurity3_1_3);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -3367,7 +3279,6 @@ public:
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testable_amendments()};
|
||||
|
||||
testMultiSendMaximumAmount(all);
|
||||
// MPTokenIssuanceCreate
|
||||
testCreateValidation(all - featureSingleAssetVault);
|
||||
testCreateValidation(all - featurePermissionedDomains);
|
||||
|
||||
@@ -130,7 +130,11 @@ ValidatorSite::load(
|
||||
{
|
||||
try
|
||||
{
|
||||
sites_.emplace_back(uri);
|
||||
// This is not super efficient, but it doesn't happen often.
|
||||
bool found = std::ranges::any_of(
|
||||
sites_, [&uri](auto const& site) { return site.loadedResource->uri == uri; });
|
||||
if (!found)
|
||||
sites_.emplace_back(uri);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
@@ -191,6 +195,16 @@ ValidatorSite::setTimer(
|
||||
std::lock_guard<std::mutex> const& site_lock,
|
||||
std::lock_guard<std::mutex> const& state_lock)
|
||||
{
|
||||
if (!sites_.empty() && //
|
||||
std::ranges::all_of(
|
||||
sites_, [](auto const& site) { return site.lastRefreshStatus.has_value(); }))
|
||||
{
|
||||
// If all of the sites have been handled at least once (including
|
||||
// errors and timeouts), call missingSite, which will load the cache
|
||||
// files for any lists that are still unavailable.
|
||||
missingSite(site_lock);
|
||||
}
|
||||
|
||||
auto next = std::min_element(sites_.begin(), sites_.end(), [](Site const& a, Site const& b) {
|
||||
return a.nextRefresh < b.nextRefresh;
|
||||
});
|
||||
@@ -301,7 +315,7 @@ ValidatorSite::onRequestTimeout(std::size_t siteIdx, error_code const& ec)
|
||||
// processes a network error. Usually, this function runs first,
|
||||
// but on extremely rare occasions, the response handler can run
|
||||
// first, which will leave activeResource empty.
|
||||
auto const& site = sites_[siteIdx];
|
||||
auto& site = sites_[siteIdx];
|
||||
if (site.activeResource)
|
||||
{
|
||||
JLOG(j_.warn()) << "Request for " << site.activeResource->uri << " took too long";
|
||||
@@ -309,6 +323,9 @@ ValidatorSite::onRequestTimeout(std::size_t siteIdx, error_code const& ec)
|
||||
else
|
||||
JLOG(j_.error()) << "Request took too long, but a response has "
|
||||
"already been processed";
|
||||
if (!site.lastRefreshStatus)
|
||||
site.lastRefreshStatus.emplace(
|
||||
Site::Status{clock_type::now(), ListDisposition::invalid, "timeout"});
|
||||
}
|
||||
|
||||
std::lock_guard lock_state{state_mutex_};
|
||||
|
||||
Reference in New Issue
Block a user