mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 08:46:46 +00:00
Merge branch 'develop' into ximinez/loanpay-assertion
This commit is contained in:
6
.github/workflows/publish-docs.yml
vendored
6
.github/workflows/publish-docs.yml
vendored
@@ -36,7 +36,7 @@ env:
|
||||
BUILD_DIR: build
|
||||
# ubuntu-latest has only 2 CPUs for private repositories
|
||||
# https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for--private-repositories
|
||||
NPROC_SUBTRACT: ${{ github.event.repository.private && '1' || '2' }}
|
||||
NPROC_SUBTRACT: ${{ github.event.repository.visibility == 'public' && '2' || '1' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -81,13 +81,13 @@ jobs:
|
||||
cmake --build . --target docs --parallel ${BUILD_NPROC}
|
||||
|
||||
- name: Create documentation artifact
|
||||
if: ${{ github.event.repository.visibility == 'public' && github.event_name == 'push' }}
|
||||
if: ${{ (github.repository_owner == 'XRPLF' || github.event.repository.visibility == 'public') && github.event_name == 'push' }}
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
|
||||
with:
|
||||
path: ${{ env.BUILD_DIR }}/docs/html
|
||||
|
||||
deploy:
|
||||
if: ${{ github.event.repository.visibility == 'public' && github.event_name == 'push' }}
|
||||
if: ${{ (github.repository_owner == 'XRPLF' || github.event.repository.visibility == 'public') && github.event_name == 'push' }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload the binary (Linux)
|
||||
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
|
||||
if: ${{ (github.repository_owner == 'XRPLF' || github.event.repository.visibility == 'public') && runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: xrpld-${{ inputs.config_name }}
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -allow-no-checks ${TARGETS} 2>&1 | tee clang-tidy-output.txt
|
||||
|
||||
- name: Upload clang-tidy output
|
||||
if: ${{ github.event.repository.visibility == 'public' && steps.run_clang_tidy.outcome != 'success' }}
|
||||
if: ${{ (github.repository_owner == 'XRPLF' || github.event.repository.visibility == 'public') && steps.run_clang_tidy.outcome != 'success' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: clang-tidy-results
|
||||
|
||||
2
BUILD.md
2
BUILD.md
@@ -141,7 +141,7 @@ Alternatively, you can pull our recipes from the repository and export them loca
|
||||
|
||||
```bash
|
||||
# Define which recipes to export.
|
||||
recipes=('abseil' 'ed25519' 'grpc' 'm4' 'mpt-crypto' 'nudb' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi')
|
||||
recipes=('abseil' 'ed25519' 'grpc' 'm4' 'mpt-crypto' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi')
|
||||
|
||||
# Selectively check out the recipes from our CCI fork.
|
||||
cd external
|
||||
|
||||
17
conan.lock
17
conan.lock
@@ -3,22 +3,22 @@
|
||||
"requires": [
|
||||
"zlib/1.3.1#cac0f6daea041b0ccf42934163defb20%1774439233.809",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
|
||||
"sqlite3/3.51.0#66aa11eabd0e34954c5c1c061ad44abe%1763899256.358",
|
||||
"sqlite3/3.51.0#66aa11eabd0e34954c5c1c061ad44abe%1774467355.988",
|
||||
"soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
|
||||
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
|
||||
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
|
||||
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1774398111.888",
|
||||
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1773224203.27",
|
||||
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
|
||||
"openssl/3.6.1#e6399de266349245a4542fc5f6c71552%1774458290.139",
|
||||
"nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32",
|
||||
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1774883011.384",
|
||||
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
|
||||
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
|
||||
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
|
||||
"libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1765850144.736",
|
||||
"jemalloc/5.3.0#e951da9cf599e956cebc117880d2d9f8%1729241615.244",
|
||||
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
|
||||
"grpc/1.78.1#b1a9e74b145cc471bed4dc64dc6eb2c1%1772623605.068",
|
||||
"grpc/1.78.1#b1a9e74b145cc471bed4dc64dc6eb2c1%1774467387.342",
|
||||
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772",
|
||||
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
|
||||
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
|
||||
@@ -29,7 +29,7 @@
|
||||
"build_requires": [
|
||||
"zlib/1.3.1#cac0f6daea041b0ccf42934163defb20%1774439233.809",
|
||||
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1774447376.964",
|
||||
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1773224203.27",
|
||||
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
|
||||
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
|
||||
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
|
||||
"m4/1.4.19#5d7a4994e5875d76faf7acf3ed056036%1774365463.87",
|
||||
@@ -41,16 +41,15 @@
|
||||
],
|
||||
"python_requires": [],
|
||||
"overrides": {
|
||||
"boost/1.90.0#d5e8defe7355494953be18524a7f135b": [
|
||||
null,
|
||||
"boost/1.90.0"
|
||||
],
|
||||
"protobuf/[>=5.27.0 <7]": [
|
||||
"protobuf/6.33.5"
|
||||
],
|
||||
"lz4/1.9.4": [
|
||||
"lz4/1.10.0"
|
||||
],
|
||||
"boost/[>=1.83.0 <1.91.0]": [
|
||||
"boost/1.90.0"
|
||||
],
|
||||
"sqlite3/[>=3.44 <4]": [
|
||||
"sqlite3/3.51.0"
|
||||
],
|
||||
|
||||
@@ -1155,57 +1155,87 @@ 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 const amount{mptIssue, r.second};
|
||||
STAmount const 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,
|
||||
"xrpl::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.
|
||||
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,6 +6,7 @@
|
||||
|
||||
#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>
|
||||
@@ -3272,6 +3273,123 @@ 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");
|
||||
|
||||
// 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
|
||||
@@ -3279,6 +3397,7 @@ public:
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testable_amendments()};
|
||||
|
||||
testMultiSendMaximumAmount(all);
|
||||
// MPTokenIssuanceCreate
|
||||
testCreateValidation(all - featureSingleAssetVault);
|
||||
testCreateValidation(all - featurePermissionedDomains);
|
||||
|
||||
Reference in New Issue
Block a user