Compare commits

..

48 Commits

Author SHA1 Message Date
Ed Hennis
50dfaea465 Merge branch 'develop' into ximinez/fix/validator-cache 2026-03-30 22:32:09 -04:00
Ed Hennis
adb0d7ecea Merge branch 'develop' into ximinez/fix/validator-cache 2026-03-12 15:05:12 -04:00
Ed Hennis
88461b5d83 Merge branch 'develop' into ximinez/fix/validator-cache 2026-03-10 13:38:40 -04:00
Ed Hennis
0bb0e5c6c0 Merge branch 'develop' into ximinez/fix/validator-cache 2026-03-06 13:04:18 -04:00
Ed Hennis
a5af69171c Merge branch 'develop' into ximinez/fix/validator-cache 2026-03-04 17:11:43 -04:00
Ed Hennis
9b349510b8 Merge branch 'develop' into ximinez/fix/validator-cache 2026-03-03 20:46:52 -04:00
Ed Hennis
a495b41179 Merge branch 'develop' into ximinez/fix/validator-cache 2026-03-03 15:54:47 -04:00
Ed Hennis
ae41a712b3 Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-24 17:34:55 -04:00
Ed Hennis
7684f9dd58 Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-20 18:49:57 -04:00
Ed Hennis
7c34be898d Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-20 18:26:15 -04:00
Ed Hennis
5e282f49da Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-20 17:31:58 -04:00
Ed Hennis
86722689ac Merge remote-tracking branch 'upstream/develop' into ximinez/fix/validator-cache
* upstream/develop:
  ci: [DEPENDABOT] bump tj-actions/changed-files from 46.0.5 to 47.0.4 (6394)
  ci: [DEPENDABOT] bump codecov/codecov-action from 5.4.3 to 5.5.2 (6398)
  ci: Build docs in PRs and in private repos (6400)
  ci: Add dependabot config (6379)
  Fix tautological assertion (6393)
  chore: Apply clang-format width 100 (6387)
2026-02-20 16:25:05 -05:00
Ed Hennis
3759144bba Update formatting 2026-02-20 16:23:53 -05:00
Ed Hennis
16c41c2143 Merge commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c' into ximinez/fix/validator-cache
* commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c':
  chore: Set clang-format width to 100 in config file (6387)
2026-02-20 16:22:13 -05:00
Ed Hennis
af28042946 Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-19 16:24:50 -05:00
Ed Hennis
734426554d Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-18 20:49:50 -04:00
Ed Hennis
7f17daa95f Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-04 16:30:05 -04:00
Ed Hennis
f359cd8dad Merge branch 'develop' into ximinez/fix/validator-cache 2026-02-03 16:08:02 -04:00
Ed Hennis
bf0b10404d Fix formatting 2026-01-28 19:40:27 -05:00
Ed Hennis
d019ebaf36 Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-28 18:44:52 -04:00
Ed Hennis
b6e4620349 Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-15 13:03:28 -04:00
Ed Hennis
db0ef6a370 Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-15 12:05:56 -04:00
Ed Hennis
11a45a0ac2 Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-13 18:19:08 -04:00
Ed Hennis
aa035f4cfd Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-13 15:27:57 -04:00
Ed Hennis
8988f9117f Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-12 14:52:12 -04:00
Ed Hennis
ae4f379845 Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-11 00:50:40 -04:00
Ed Hennis
671aa11649 Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-08 17:06:06 -04:00
Ed Hennis
53d35fd8ea Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-08 13:04:16 -04:00
Ed Hennis
0c7ea2e333 Merge branch 'develop' into ximinez/fix/validator-cache 2026-01-06 14:02:10 -05:00
Ed Hennis
5f54be25e9 Merge branch 'develop' into ximinez/fix/validator-cache 2025-12-22 17:39:55 -05:00
Ed Hennis
d82756519c Merge branch 'develop' into ximinez/fix/validator-cache 2025-12-18 19:59:49 -05:00
Ed Hennis
1f23832659 Merge branch 'develop' into ximinez/fix/validator-cache 2025-12-12 20:34:55 -05:00
Ed Hennis
4c50969bde Merge branch 'develop' into ximinez/fix/validator-cache 2025-12-11 15:31:29 -05:00
Ed Hennis
aabdf372dd Merge branch 'develop' into ximinez/fix/validator-cache 2025-12-05 21:13:06 -05:00
Ed Hennis
c6d63a4b90 Merge branch 'develop' into ximinez/fix/validator-cache 2025-12-02 17:37:25 -05:00
Ed Hennis
1e6c3208db Merge branch 'develop' into ximinez/fix/validator-cache 2025-12-01 14:40:41 -05:00
Ed Hennis
a74f223efb Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-28 15:46:40 -05:00
Ed Hennis
1eb3a3ea5a Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-27 01:48:53 -05:00
Ed Hennis
630e428929 Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-26 00:25:12 -05:00
Ed Hennis
3f93edc5e0 Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-25 14:55:02 -05:00
Ed Hennis
baf62689ff Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-24 21:49:07 -05:00
Ed Hennis
ddf7d6cac4 Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-24 21:30:18 -05:00
Ed Hennis
fcd2ea2d6e Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-21 12:47:54 -05:00
Ed Hennis
a16aa5b12f Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-18 22:39:25 -05:00
Ed Hennis
ef2de81870 Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-15 03:08:38 -05:00
Ed Hennis
fce6757260 Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-13 12:19:10 -05:00
Ed Hennis
d759a0a2b0 Merge branch 'develop' into ximinez/fix/validator-cache 2025-11-12 14:12:51 -05:00
Ed Hennis
d2dda416e8 Use Validator List (VL) cache files in more scenarios
- If any [validator_list_keys] are not available after all
  [validator_list_sites] have had a chance to be queried, then fall
  back to loading cache files. Currently, cache files are only used if
  no sites are defined, or the request to one of them has an error. It
  does not include cases where not enough sites are defined, or if a
  site returns an invalid VL (or something else entirely).
- Resolves #5320
2025-11-10 19:53:02 -05:00
3 changed files with 39 additions and 140 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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_};