Compare commits

..

9 Commits

Author SHA1 Message Date
Bart
628230a633 ci: Only upload artifacts in the XRPLF/rippled repository (#6523)
This change will only attempt to upload artifacts for CI runs performed in the XRPLF/rippled repository.
2026-04-02 16:38:04 +01:00
Sergey Kuznetsov
24c43a2617 Fix build 2026-04-02 16:35:45 +01:00
Vito Tumas
0eb77f9040 fix: Enforce aggregate MaximumAmount in multi-send MPT (#6644)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-04-01 18:56:47 -04:00
Vito Tumas
a92483e077 fix: Improve loan invariant message (#6668) 2026-04-01 18:55:56 -04:00
Vito Tumas
10731f90b3 feat: Add placeholder amendment for assorted bug fixes (#6652) 2026-04-01 18:49:46 -04:00
Mayukha Vadari
3ba3fcff4c release: Bump version to 3.1.2 2026-03-12 15:01:01 -04:00
Mayukha Vadari
ecc58740d0 release: Bump version to 3.1.2-rc1 2026-03-11 18:17:24 -04:00
Mayukha Vadari
0e3600a18f refactor: Improve exception handling 2026-03-11 18:17:16 -04:00
Ed Hennis
c5988233d0 Set version to 3.1.1 (#6410) 2026-02-23 15:47:09 -05:00
21 changed files with 302 additions and 147 deletions

View File

@@ -115,7 +115,8 @@ jobs:
needs:
- should-run
- build-test
if: ${{ needs.should-run.outputs.go == 'true' && (startsWith(github.base_ref, 'release') || github.base_ref == 'master') }}
# Only run when committing to a PR that targets a release branch or master.
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && (startsWith(github.base_ref, 'release') || github.base_ref == 'master') }}
uses: ./.github/workflows/reusable-notify-clio.yml
secrets:
clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }}

View File

@@ -130,7 +130,7 @@ jobs:
--target "${CMAKE_TARGET}"
- name: Upload rippled artifact (Linux)
if: ${{ github.repository_owner == 'XRPLF' && runner.os == 'Linux' }}
if: ${{ github.repository == 'XRPLF/rippled' && runner.os == 'Linux' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
env:
BUILD_DIR: ${{ inputs.build_dir }}
@@ -201,7 +201,7 @@ jobs:
--target coverage
- name: Upload coverage report
if: ${{ github.repository_owner == 'XRPLF' && !inputs.build_only && env.ENABLED_COVERAGE == 'true' }}
if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.ENABLED_COVERAGE == 'true' }}
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with:
disable_search: true

View File

@@ -101,11 +101,11 @@ jobs:
log_verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }}
- name: Log into Conan remote
if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}"
- name: Upload Conan packages
if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
env:
FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}
run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION}

View File

@@ -156,7 +156,7 @@ public:
{
lowest_layer().shutdown(plain_socket::shutdown_both);
}
catch (boost::system::system_error& e)
catch (boost::system::system_error const& e)
{
ec = e.code();
}

View File

@@ -32,6 +32,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FIX (Security3_1_3, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -412,14 +412,14 @@ ApplyStateTable::erase(ReadView const& base, std::shared_ptr<SLE> const& sle)
{
auto const iter = items_.find(sle->key());
if (iter == items_.end())
LogicError("ApplyStateTable::erase: missing key");
Throw<std::logic_error>("ApplyStateTable::erase: missing key");
auto& item = iter->second;
if (item.second != sle)
LogicError("ApplyStateTable::erase: unknown SLE");
Throw<std::logic_error>("ApplyStateTable::erase: unknown SLE");
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::erase: double erase");
Throw<std::logic_error>("ApplyStateTable::erase: double erase");
break;
case Action::insert:
items_.erase(iter);
@@ -445,7 +445,7 @@ ApplyStateTable::rawErase(ReadView const& base, std::shared_ptr<SLE> const& sle)
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::rawErase: double erase");
Throw<std::logic_error>("ApplyStateTable::rawErase: double erase");
break;
case Action::insert:
items_.erase(result.first);
@@ -476,11 +476,13 @@ ApplyStateTable::insert(ReadView const& base, std::shared_ptr<SLE> const& sle)
switch (item.first)
{
case Action::cache:
LogicError("ApplyStateTable::insert: already cached");
Throw<std::logic_error>("ApplyStateTable::insert: already cached");
case Action::insert:
LogicError("ApplyStateTable::insert: already inserted");
Throw<std::logic_error>(
"ApplyStateTable::insert: already inserted");
case Action::modify:
LogicError("ApplyStateTable::insert: already modified");
Throw<std::logic_error>(
"ApplyStateTable::insert: already modified");
case Action::erase:
break;
}
@@ -506,7 +508,7 @@ ApplyStateTable::replace(ReadView const& base, std::shared_ptr<SLE> const& sle)
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::replace: already erased");
Throw<std::logic_error>("ApplyStateTable::replace: already erased");
case Action::cache:
item.first = Action::modify;
break;
@@ -522,14 +524,14 @@ ApplyStateTable::update(ReadView const& base, std::shared_ptr<SLE> const& sle)
{
auto const iter = items_.find(sle->key());
if (iter == items_.end())
LogicError("ApplyStateTable::update: missing key");
Throw<std::logic_error>("ApplyStateTable::update: missing key");
auto& item = iter->second;
if (item.second != sle)
LogicError("ApplyStateTable::update: unknown SLE");
Throw<std::logic_error>("ApplyStateTable::update: unknown SLE");
switch (item.first)
{
case Action::erase:
LogicError("ApplyStateTable::update: erased");
Throw<std::logic_error>("ApplyStateTable::update: erased");
break;
case Action::cache:
item.first = Action::modify;

View File

@@ -59,10 +59,8 @@ findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start)
{
node = view.peek(keylet::page(directory, page));
if (!node)
{ // LCOV_EXCL_START
LogicError("Directory chain: root back-pointer broken.");
// LCOV_EXCL_STOP
}
Throw<std::logic_error>(
"Directory chain: root back-pointer broken."); // LCOV_EXCL_LINE
}
auto indexes = node->getFieldV256(sfIndexes);
@@ -81,21 +79,22 @@ insertKey(
if (preserveOrder)
{
if (std::find(indexes.begin(), indexes.end(), key) != indexes.end())
LogicError("dirInsert: double insertion"); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"dirInsert: double insertion"); // LCOV_EXCL_LINE
indexes.push_back(key);
}
else
{
// We can't be sure if this page is already sorted because
// it may be a legacy page we haven't yet touched. Take
// the time to sort it.
// We can't be sure if this page is already sorted because it may be a
// legacy page we haven't yet touched. Take the time to sort it.
std::sort(indexes.begin(), indexes.end());
auto pos = std::lower_bound(indexes.begin(), indexes.end(), key);
if (pos != indexes.end() && key == *pos)
LogicError("dirInsert: double insertion"); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"dirInsert: double insertion"); // LCOV_EXCL_LINE
indexes.insert(pos, key);
}
@@ -149,8 +148,7 @@ insertPage(
node->setFieldH256(sfRootIndex, directory.key);
node->setFieldV256(sfIndexes, indexes);
// Save some space by not specifying the value 0 since
// it's the default.
// Save some space by not specifying the value 0 since it's the default.
if (page != 1)
node->setFieldU64(sfIndexPrevious, page - 1);
XRPL_ASSERT_PARTS(
@@ -226,28 +224,27 @@ ApplyView::emptyDirDelete(Keylet const& directory)
auto nextPage = node->getFieldU64(sfIndexNext);
if (nextPage == rootPage && prevPage != rootPage)
LogicError("Directory chain: fwd link broken"); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"Directory chain: fwd link broken"); // LCOV_EXCL_LINE
if (prevPage == rootPage && nextPage != rootPage)
LogicError("Directory chain: rev link broken"); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"Directory chain: rev link broken"); // LCOV_EXCL_LINE
// Older versions of the code would, in some cases, allow the last
// page to be empty. Remove such pages:
// Older versions of the code would, in some cases, allow the last page to
// be empty. Remove such pages:
if (nextPage == prevPage && nextPage != rootPage)
{
auto last = peek(keylet::page(directory, nextPage));
if (!last)
{ // LCOV_EXCL_START
LogicError("Directory chain: fwd link broken.");
// LCOV_EXCL_STOP
}
Throw<std::logic_error>(
"Directory chain: fwd link broken."); // LCOV_EXCL_LINE
if (!last->getFieldV256(sfIndexes).empty())
return false;
// Update the first page's linked list and
// mark it as updated.
// Update the first page's linked list and mark it as updated.
node->setFieldU64(sfIndexNext, rootPage);
node->setFieldU64(sfIndexPrevious, rootPage);
update(node);
@@ -255,8 +252,7 @@ ApplyView::emptyDirDelete(Keylet const& directory)
// And erase the empty last page:
erase(last);
// Make sure our local values reflect the
// updated information:
// Make sure our local values reflect the updated information:
nextPage = rootPage;
prevPage = rootPage;
}
@@ -300,46 +296,36 @@ ApplyView::dirRemove(
return true;
}
// The current page is now empty; check if it can be
// deleted, and, if so, whether the entire directory
// can now be removed.
// The current page is now empty; check if it can be deleted, and, if so,
// whether the entire directory can now be removed.
auto prevPage = node->getFieldU64(sfIndexPrevious);
auto nextPage = node->getFieldU64(sfIndexNext);
// The first page is the directory's root node and is
// treated specially: it can never be deleted even if
// it is empty, unless we plan on removing the entire
// directory.
// The first page is the directory's root node and is treated specially: it
// can never be deleted even if it is empty, unless we plan on removing the
// entire directory.
if (page == rootPage)
{
if (nextPage == page && prevPage != page)
{ // LCOV_EXCL_START
LogicError("Directory chain: fwd link broken");
// LCOV_EXCL_STOP
}
Throw<std::logic_error>(
"Directory chain: fwd link broken"); // LCOV_EXCL_LINE
if (prevPage == page && nextPage != page)
{ // LCOV_EXCL_START
LogicError("Directory chain: rev link broken");
// LCOV_EXCL_STOP
}
Throw<std::logic_error>(
"Directory chain: rev link broken"); // LCOV_EXCL_LINE
// Older versions of the code would, in some cases,
// allow the last page to be empty. Remove such
// pages if we stumble on them:
// Older versions of the code would, in some cases, allow the last page
// to be empty. Remove such pages if we stumble on them:
if (nextPage == prevPage && nextPage != page)
{
auto last = peek(keylet::page(directory, nextPage));
if (!last)
{ // LCOV_EXCL_START
LogicError("Directory chain: fwd link broken.");
// LCOV_EXCL_STOP
}
Throw<std::logic_error>(
"Directory chain: fwd link broken."); // LCOV_EXCL_LINE
if (last->getFieldV256(sfIndexes).empty())
{
// Update the first page's linked list and
// mark it as updated.
// Update the first page's linked list and mark it as updated.
node->setFieldU64(sfIndexNext, page);
node->setFieldU64(sfIndexPrevious, page);
update(node);
@@ -347,8 +333,7 @@ ApplyView::dirRemove(
// And erase the empty last page:
erase(last);
// Make sure our local values reflect the
// updated information:
// Make sure our local values reflect the updated information:
nextPage = page;
prevPage = page;
}
@@ -366,25 +351,28 @@ ApplyView::dirRemove(
// This can never happen for nodes other than the root:
if (nextPage == page)
LogicError("Directory chain: fwd link broken"); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"Directory chain: fwd link broken"); // LCOV_EXCL_LINE
if (prevPage == page)
LogicError("Directory chain: rev link broken"); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"Directory chain: rev link broken"); // LCOV_EXCL_LINE
// This node isn't the root, so it can either be in the
// middle of the list, or at the end. Unlink it first
// and then check if that leaves the list with only a
// root:
// This node isn't the root, so it can either be in the middle of the list,
// or at the end. Unlink it first and then check if that leaves the list
// with only a root:
auto prev = peek(keylet::page(directory, prevPage));
if (!prev)
LogicError("Directory chain: fwd link broken."); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"Directory chain: fwd link broken."); // LCOV_EXCL_LINE
// Fix previous to point to its new next.
prev->setFieldU64(sfIndexNext, nextPage);
update(prev);
auto next = peek(keylet::page(directory, nextPage));
if (!next)
LogicError("Directory chain: rev link broken."); // LCOV_EXCL_LINE
Throw<std::logic_error>(
"Directory chain: rev link broken."); // LCOV_EXCL_LINE
// Fix next to point to its new previous.
next->setFieldU64(sfIndexPrevious, prevPage);
update(next);
@@ -392,13 +380,12 @@ ApplyView::dirRemove(
// The page is no longer linked. Delete it.
erase(node);
// Check whether the next page is the last page and, if
// so, whether it's empty. If it is, delete it.
// Check whether the next page is the last page and, if so, whether it's
// empty. If it is, delete it.
if (nextPage != rootPage && next->getFieldU64(sfIndexNext) == rootPage &&
next->getFieldV256(sfIndexes).empty())
{
// Since next doesn't point to the root, it
// can't be pointing to prev.
// Since next doesn't point to the root, it can't be pointing to prev.
erase(next);
// The previous page is now the last page:
@@ -408,18 +395,17 @@ ApplyView::dirRemove(
// And the root points to the last page:
auto root = peek(keylet::page(directory, rootPage));
if (!root)
{ // LCOV_EXCL_START
LogicError("Directory chain: root link broken.");
// LCOV_EXCL_STOP
}
Throw<std::logic_error>(
"Directory chain: root link broken."); // LCOV_EXCL_LINE
root->setFieldU64(sfIndexPrevious, prevPage);
update(root);
nextPage = rootPage;
}
// If we're not keeping the root, then check to see if
// it's left empty. If so, delete it as well.
// If we're not keeping the root, then check to see if it's left empty.
// If so, delete it as well.
if (!keepRoot && nextPage == rootPage && prevPage == rootPage)
{
if (prev->getFieldV256(sfIndexes).empty())

View File

@@ -266,7 +266,8 @@ OpenView::rawTxInsert(
std::forward_as_tuple(key),
std::forward_as_tuple(txn, metaData));
if (!result.second)
LogicError("rawTxInsert: duplicate TX id: " + to_string(key));
Throw<std::logic_error>(
"rawTxInsert: duplicate TX id: " + to_string(key));
}
} // namespace ripple

View File

@@ -252,7 +252,7 @@ RawStateTable::erase(std::shared_ptr<SLE> const& sle)
switch (item.action)
{
case Action::erase:
LogicError("RawStateTable::erase: already erased");
Throw<std::logic_error>("RawStateTable::erase: already erased");
break;
case Action::insert:
items_.erase(result.first);
@@ -281,10 +281,10 @@ RawStateTable::insert(std::shared_ptr<SLE> const& sle)
item.sle = sle;
break;
case Action::insert:
LogicError("RawStateTable::insert: already inserted");
Throw<std::logic_error>("RawStateTable::insert: already inserted");
break;
case Action::replace:
LogicError("RawStateTable::insert: already exists");
Throw<std::logic_error>("RawStateTable::insert: already exists");
break;
}
}
@@ -302,7 +302,7 @@ RawStateTable::replace(std::shared_ptr<SLE> const& sle)
switch (item.action)
{
case Action::erase:
LogicError("RawStateTable::replace: was erased");
Throw<std::logic_error>("RawStateTable::replace: was erased");
break;
case Action::insert:
case Action::replace:

View File

@@ -1178,10 +1178,9 @@ getPseudoAccountFields()
if (!ar)
{
// LCOV_EXCL_START
LogicError(
Throw<std::logic_error>(
"ripple::getPseudoAccountFields : unable to find account root "
"ledger "
"format");
"ledger format");
// LCOV_EXCL_STOP
}
auto const& soTemplate = ar->getSOTemplate();
@@ -2670,51 +2669,82 @@ 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;
// These may diverge
// 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 uint64_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};
std::uint64_t const maximumAmount =
sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
std::uint64_t 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.
STAmount takeFromSender{mptIssue};
actual = takeFromSender;
for (auto const& r : receivers)
for (auto const& [receiverID, amt] : receivers)
{
auto const& receiverID = r.first;
STAmount amount{mptIssue, r.second};
STAmount amount{mptIssue, amt};
if (amount < beast::zero)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
/* 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))
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,
"rippler::rippleSendMultiMPT",
"sender == issuer, takeFromSender == zero");
auto const sendAmount = amount.mpt().value();
auto const maximumAmount =
sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount ||
sle->getFieldU64(sfOutstandingAmount) >
maximumAmount - sendAmount)
return tecPATH_DRY;
std::uint64_t const sendAmount = amount.mpt().value();
if (view.rules().enabled(fixSecurity3_1_3))
{
// Post-fixSecurity3_1_3: aggregate MaximumAmount
// check. WARNING: the order of conditions is
// critical — each guards the subtraction in the
// next against unsigned underflow. Do not reorder.
bool 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;
}
}
// Direct send: redeeming MPTs and/or sending own MPTs.
@@ -2722,8 +2752,8 @@ rippleSendMultiMPT(
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

@@ -36,7 +36,7 @@ namespace BuildInfo {
// and follow the format described at http://semver.org/
//------------------------------------------------------------------------------
// clang-format off
char const* const versionString = "3.1.1-rc1"
char const* const versionString = "3.1.2"
// clang-format on
#if defined(DEBUG) || defined(SANITIZER)

View File

@@ -3625,6 +3625,139 @@ 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().journal("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");
// Issue 50 tokens so outstandingAmount is nonzero, then verify
// the third condition: outstandingAmount > maximumAmount - sendAmount -
// totalSendAmount
mptt.pay(issuer, alice, 50);
env.close();
// maxAmt=150, outstanding=50, so 100 more available
runTest(
R{{alice.id(), 50}, {bob.id(), 50}},
tesSUCCESS,
maxAmt,
"nonzero outstanding, aggregate at boundary");
runTest(
R{{alice.id(), 50}, {bob.id(), 51}},
tecPATH_DRY,
std::nullopt,
"nonzero outstanding, aggregate exceeds max");
runTest(
R{{alice.id(), 100}, {bob.id(), 0}},
tesSUCCESS,
maxAmt,
"nonzero outstanding, single send at remaining capacity");
runTest(
R{{alice.id(), 101}, {bob.id(), 0}},
tecPATH_DRY,
std::nullopt,
"nonzero outstanding, single send exceeds remaining capacity");
// 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,
250,
"pre-amendment allows over-send");
env.enableFeature(fixSecurity3_1_3);
}
}
public:
void
run() override
@@ -3632,6 +3765,7 @@ public:
using namespace test::jtx;
FeatureBitset const all{testable_amendments()};
testMultiSendMaximumAmount(all);
// MPTokenIssuanceCreate
testCreateValidation(all - featureSingleAssetVault);
testCreateValidation(all - featurePermissionedDomains);

View File

@@ -413,7 +413,7 @@ port_wss_admin
c.loadFromString(
boost::str(configTemplate % validationSeed % token));
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -434,7 +434,7 @@ port_wss_admin
main
)rippleConfig");
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -447,7 +447,7 @@ main
c.loadFromString(R"rippleConfig(
)rippleConfig");
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -462,7 +462,7 @@ main
255
)rippleConfig");
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -477,7 +477,7 @@ main
10000
)rippleConfig");
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -505,7 +505,7 @@ main
Config c;
c.loadFromString(boost::str(cc % missingPath));
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -526,7 +526,7 @@ main
Config c;
c.loadFromString(boost::str(cc % invalidFile.string()));
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -646,7 +646,7 @@ trustthesevalidators.gov
c.loadFromString(toLoad);
fail();
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -675,7 +675,7 @@ value = 2
c.loadFromString(toLoad);
fail();
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -724,7 +724,7 @@ trustthesevalidators.gov
c.loadFromString(toLoad);
fail();
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -901,7 +901,7 @@ trustthesevalidators.gov
c.loadFromString(boost::str(cc % vtg.validatorsFile()));
fail();
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -928,7 +928,7 @@ trustthesevalidators.gov
Config c2;
c2.loadFromString(boost::str(cc % vtg.validatorsFile()));
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -1411,7 +1411,7 @@ r.ripple.com:51235
else
fail();
}
catch (std::runtime_error&)
catch (std::runtime_error const&)
{
if (!shouldPass)
pass();
@@ -1434,7 +1434,7 @@ r.ripple.com:51235
c.loadFromString("[overlay]\nmax_unknown_time=" + value);
return c.MAX_UNKNOWN_TIME;
}
catch (std::runtime_error&)
catch (std::runtime_error const&)
{
return {};
}
@@ -1469,7 +1469,7 @@ r.ripple.com:51235
c.loadFromString("[overlay]\nmax_diverged_time=" + value);
return c.MAX_DIVERGED_TIME;
}
catch (std::runtime_error&)
catch (std::runtime_error const&)
{
return {};
}

View File

@@ -1412,7 +1412,7 @@ vp_enable=0
{
c.loadFromString(toLoad);
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}
@@ -1456,7 +1456,7 @@ vp_base_squelch_max_selected_peers=2
{
c2.loadFromString(toLoad);
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
error = e.what();
}

View File

@@ -2079,7 +2079,7 @@ class STParsedJSON_test : public beast::unit_test::suite
STParsedJSONObject parsed("test", faultyJson);
BEAST_EXPECT(!parsed.object);
}
catch (std::runtime_error& e)
catch (std::runtime_error const& e)
{
std::string what(e.what());
unexpected(

View File

@@ -517,14 +517,14 @@ void
Ledger::rawErase(std::shared_ptr<SLE> const& sle)
{
if (!stateMap_.delItem(sle->key()))
LogicError("Ledger::rawErase: key not found");
Throw<std::logic_error>("Ledger::rawErase: key not found");
}
void
Ledger::rawErase(uint256 const& key)
{
if (!stateMap_.delItem(key))
LogicError("Ledger::rawErase: key not found");
Throw<std::logic_error>("Ledger::rawErase: key not found");
}
void
@@ -535,7 +535,7 @@ Ledger::rawInsert(std::shared_ptr<SLE> const& sle)
if (!stateMap_.addGiveItem(
SHAMapNodeType::tnACCOUNT_STATE,
make_shamapitem(sle->key(), ss.slice())))
LogicError("Ledger::rawInsert: key already exists");
Throw<std::logic_error>("Ledger::rawInsert: key already exists");
}
void
@@ -546,7 +546,7 @@ Ledger::rawReplace(std::shared_ptr<SLE> const& sle)
if (!stateMap_.updateGiveItem(
SHAMapNodeType::tnACCOUNT_STATE,
make_shamapitem(sle->key(), ss.slice())))
LogicError("Ledger::rawReplace: key not found");
Throw<std::logic_error>("Ledger::rawReplace: key not found");
}
void
@@ -564,7 +564,7 @@ Ledger::rawTxInsert(
s.addVL(metaData->peekData());
if (!txMap_.addGiveItem(
SHAMapNodeType::tnTRANSACTION_MD, make_shamapitem(key, s.slice())))
LogicError("duplicate_tx: " + to_string(key));
Throw<std::logic_error>("duplicate_tx: " + to_string(key));
}
uint256
@@ -584,7 +584,7 @@ Ledger::rawTxInsertWithHash(
auto item = make_shamapitem(key, s.slice());
auto hash = sha512Half(HashPrefix::txNode, item->slice(), item->key());
if (!txMap_.addGiveItem(SHAMapNodeType::tnTRANSACTION_MD, std::move(item)))
LogicError("duplicate_tx: " + to_string(key));
Throw<std::logic_error>("duplicate_tx: " + to_string(key));
return hash;
}

View File

@@ -2593,8 +2593,8 @@ ValidLoan::finalize(
after->at(sfPrincipalOutstanding) == beast::zero &&
after->at(sfManagementFeeOutstanding) == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
"remaining has not been paid off";
JLOG(j.fatal()) << "Invariant failed: Fully paid off Loan still "
"has payments remaining";
return false;
}
if (before &&

View File

@@ -1621,7 +1621,7 @@ rpcClient(
// YYY We could have a command line flag for single line output for
// scripts. YYY We would intercept output here and simplify it.
}
catch (RequestNotParseable& e)
catch (RequestNotParseable const& e)
{
jvOutput = rpcError(rpcINVALID_PARAMS);
jvOutput["error_what"] = e.what();

View File

@@ -653,7 +653,7 @@ transactionPreProcessImpl(
stTx = std::make_shared<STTx>(std::move(parsed.object.value()));
}
catch (STObject::FieldErr& err)
catch (STObject::FieldErr const& err)
{
return RPC::make_error(rpcINVALID_PARAMS, err.what());
}
@@ -1364,7 +1364,7 @@ transactionSubmitMultiSigned(
stTx =
std::make_shared<STTx>(std::move(parsedTx_json.object.value()));
}
catch (STObject::FieldErr& err)
catch (STObject::FieldErr const& err)
{
return RPC::make_error(rpcINVALID_PARAMS, err.what());
}

View File

@@ -821,7 +821,7 @@ doLedgerEntry(RPC::JsonContext& context)
return RPC::make_param_error("No ledger_entry params provided.");
}
}
catch (Json::error& e)
catch (Json::error const& e)
{
if (context.apiVersion > 1u)
{

View File

@@ -85,7 +85,7 @@ doSubscribe(RPC::JsonContext& context)
ispSub = context.netOps.addRpcSub(
strUrl, std::dynamic_pointer_cast<InfoSub>(rspSub));
}
catch (std::runtime_error& ex)
catch (std::runtime_error const& ex)
{
return RPC::make_param_error(ex.what());
}