Merge branch 'develop' into ripple/wamr

This commit is contained in:
Mayukha Vadari
2025-09-26 15:51:40 -04:00
committed by GitHub
21 changed files with 1162 additions and 36 deletions

View File

@@ -72,15 +72,15 @@ It generates many files of [results](results):
desired as described above. In a perfect repo, this file will be
empty.
This file is committed to the repo, and is used by the [levelization
Github workflow](../../workflows/check-levelization.yml) to validate
Github workflow](../../workflows/reusable-check-levelization.yml) to validate
that nothing changed.
- [`ordering.txt`](results/ordering.txt): A list showing relationships
between modules where there are no loops as they actually exist, as
opposed to how they are desired as described above.
This file is committed to the repo, and is used by the [levelization
Github workflow](../../workflows/check-levelization.yml) to validate
Github workflow](../../workflows/reusable-check-levelization.yml) to validate
that nothing changed.
- [`levelization.yml`](../../workflows/check-levelization.yml)
- [`levelization.yml`](../../workflows/reusable-check-levelization.yml)
Github Actions workflow to test that levelization loops haven't
changed. Unfortunately, if changes are detected, it can't tell if
they are improvements or not, so if you have resolved any issues or

View File

@@ -138,6 +138,7 @@ test.toplevel > test.csf
test.toplevel > xrpl.json
test.unit_test > xrpl.basics
tests.libxrpl > xrpl.basics
tests.libxrpl > xrpl.net
xrpl.json > xrpl.basics
xrpl.ledger > xrpl.basics
xrpl.ledger > xrpl.protocol

View File

@@ -50,8 +50,8 @@ jobs:
files: |
# These paths are unique to `on-pr.yml`.
.github/scripts/levelization/**
.github/workflows/check-levelization.yml
.github/workflows/notify-clio.yml
.github/workflows/reusable-check-levelization.yml
.github/workflows/reusable-notify-clio.yml
.github/workflows/on-pr.yml
# Keep the paths below in sync with those in `on-trigger.yml`.
@@ -59,7 +59,7 @@ jobs:
.github/actions/build-test/**
.github/actions/setup-conan/**
.github/scripts/strategy-matrix/**
.github/workflows/build-test.yml
.github/workflows/reusable-build-test.yml
.github/workflows/reusable-strategy-matrix.yml
.codecov.yml
cmake/**
@@ -93,12 +93,12 @@ jobs:
check-levelization:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/check-levelization.yml
uses: ./.github/workflows/reusable-check-levelization.yml
build-test:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/build-test.yml
uses: ./.github/workflows/reusable-build-test.yml
strategy:
matrix:
os: [linux, macos, windows]
@@ -112,7 +112,7 @@ jobs:
- should-run
- build-test
if: ${{ needs.should-run.outputs.go == 'true' && contains(fromJSON('["release", "master"]'), github.ref_name) }}
uses: ./.github/workflows/notify-clio.yml
uses: ./.github/workflows/reusable-notify-clio.yml
secrets:
clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }}
conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}

View File

@@ -14,7 +14,7 @@ on:
- master
paths:
# These paths are unique to `on-trigger.yml`.
- ".github/workflows/check-missing-commits.yml"
- ".github/workflows/reusable-check-missing-commits.yml"
- ".github/workflows/on-trigger.yml"
- ".github/workflows/publish-docs.yml"
@@ -23,7 +23,7 @@ on:
- ".github/actions/build-test/**"
- ".github/actions/setup-conan/**"
- ".github/scripts/strategy-matrix/**"
- ".github/workflows/build-test.yml"
- ".github/workflows/reusable-build-test.yml"
- ".github/workflows/reusable-strategy-matrix.yml"
- ".codecov.yml"
- "cmake/**"
@@ -71,10 +71,10 @@ defaults:
jobs:
check-missing-commits:
if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && contains(fromJSON('["develop", "release"]'), github.ref_name) }}
uses: ./.github/workflows/check-missing-commits.yml
uses: ./.github/workflows/reusable-check-missing-commits.yml
build-test:
uses: ./.github/workflows/build-test.yml
uses: ./.github/workflows/reusable-build-test.yml
strategy:
matrix:
os: [linux, macos, windows]

View File

@@ -7,8 +7,9 @@ on:
workflow_dispatch:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@af1b0f0d764cda2e5435f5ac97b240d4bd4d95d3
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit" }'
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-d1496b8" }'

View File

@@ -27,7 +27,7 @@ env:
jobs:
publish:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/ci/tools-rippled-documentation
container: ghcr.io/xrplf/ci/tools-rippled-documentation:sha-d1496b8
permissions:
contents: write
steps:

View File

@@ -63,7 +63,7 @@ jobs:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
max-parallel: 10
runs-on: ${{ matrix.architecture.runner }}
container: ${{ inputs.os == 'linux' && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }}
container: ${{ inputs.os == 'linux' && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }}
steps:
- name: Check strategy matrix
run: |

View File

@@ -40,7 +40,7 @@ jobs:
upload:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13
container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13-sha-5dd7158
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0

View File

@@ -56,7 +56,7 @@ jobs:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
max-parallel: 10
runs-on: ${{ matrix.architecture.runner }}
container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }}
container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }}
steps:
- name: Cleanup workspace

View File

@@ -36,7 +36,7 @@ XRPL_FIX (IncludeKeyletFields, Supported::no, VoteBehavior::DefaultNo
XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -383,7 +383,7 @@ public:
static boost::regex reStatus{
"\\`HTTP/1\\S+ (\\d{3}) .*\\'"}; // HTTP/1.1 200 OK
static boost::regex reSize{
"\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'"};
"\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'", boost::regex::icase};
static boost::regex reBody{"\\`.*\\r\\n\\r\\n(.*)\\'"};
boost::smatch smMatch;

View File

@@ -19,11 +19,203 @@
#include <test/jtx.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/misc/FeeVote.h>
#include <xrpld/app/tx/apply.h>
#include <xrpl/basics/BasicConfig.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
namespace ripple {
namespace test {
struct FeeSettingsFields
{
std::optional<std::uint64_t> baseFee = std::nullopt;
std::optional<std::uint32_t> reserveBase = std::nullopt;
std::optional<std::uint32_t> reserveIncrement = std::nullopt;
std::optional<std::uint32_t> referenceFeeUnits = std::nullopt;
std::optional<XRPAmount> baseFeeDrops = std::nullopt;
std::optional<XRPAmount> reserveBaseDrops = std::nullopt;
std::optional<XRPAmount> reserveIncrementDrops = std::nullopt;
};
STTx
createFeeTx(
Rules const& rules,
std::uint32_t seq,
FeeSettingsFields const& fields)
{
auto fill = [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldU32(sfLedgerSequence, seq);
if (rules.enabled(featureXRPFees))
{
// New XRPFees format - all three fields are REQUIRED
obj.setFieldAmount(
sfBaseFeeDrops,
fields.baseFeeDrops ? *fields.baseFeeDrops : XRPAmount{0});
obj.setFieldAmount(
sfReserveBaseDrops,
fields.reserveBaseDrops ? *fields.reserveBaseDrops
: XRPAmount{0});
obj.setFieldAmount(
sfReserveIncrementDrops,
fields.reserveIncrementDrops ? *fields.reserveIncrementDrops
: XRPAmount{0});
}
else
{
// Legacy format - all four fields are REQUIRED
obj.setFieldU64(sfBaseFee, fields.baseFee ? *fields.baseFee : 0);
obj.setFieldU32(
sfReserveBase, fields.reserveBase ? *fields.reserveBase : 0);
obj.setFieldU32(
sfReserveIncrement,
fields.reserveIncrement ? *fields.reserveIncrement : 0);
obj.setFieldU32(
sfReferenceFeeUnits,
fields.referenceFeeUnits ? *fields.referenceFeeUnits : 0);
}
};
return STTx(ttFEE, fill);
}
STTx
createInvalidFeeTx(
Rules const& rules,
std::uint32_t seq,
bool missingRequiredFields = true,
bool wrongFeatureFields = false,
std::uint32_t uniqueValue = 42)
{
auto fill = [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldU32(sfLedgerSequence, seq);
if (wrongFeatureFields)
{
if (rules.enabled(featureXRPFees))
{
obj.setFieldU64(sfBaseFee, 10 + uniqueValue);
obj.setFieldU32(sfReserveBase, 200000);
obj.setFieldU32(sfReserveIncrement, 50000);
obj.setFieldU32(sfReferenceFeeUnits, 10);
}
else
{
obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue});
obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000});
obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000});
}
}
else if (!missingRequiredFields)
{
// Create valid transaction (all required fields present)
if (rules.enabled(featureXRPFees))
{
obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue});
obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000});
obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000});
}
else
{
obj.setFieldU64(sfBaseFee, 10 + uniqueValue);
obj.setFieldU32(sfReserveBase, 200000);
obj.setFieldU32(sfReserveIncrement, 50000);
obj.setFieldU32(sfReferenceFeeUnits, 10);
}
}
// If missingRequiredFields is true, we don't add the required fields
// (default behavior)
};
return STTx(ttFEE, fill);
}
bool
applyFeeAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx)
{
auto const res =
apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal);
return res.ter == tesSUCCESS;
}
bool
verifyFeeObject(
std::shared_ptr<Ledger const> const& ledger,
Rules const& rules,
FeeSettingsFields const& expected)
{
auto const feeObject = ledger->read(keylet::fees());
if (!feeObject)
return false;
auto checkEquality = [&](auto const& field, auto const& expected) {
if (!feeObject->isFieldPresent(field))
return false;
return feeObject->at(field) == expected;
};
if (rules.enabled(featureXRPFees))
{
if (feeObject->isFieldPresent(sfBaseFee) ||
feeObject->isFieldPresent(sfReserveBase) ||
feeObject->isFieldPresent(sfReserveIncrement) ||
feeObject->isFieldPresent(sfReferenceFeeUnits))
return false;
if (!checkEquality(
sfBaseFeeDrops, expected.baseFeeDrops.value_or(XRPAmount{0})))
return false;
if (!checkEquality(
sfReserveBaseDrops,
expected.reserveBaseDrops.value_or(XRPAmount{0})))
return false;
if (!checkEquality(
sfReserveIncrementDrops,
expected.reserveIncrementDrops.value_or(XRPAmount{0})))
return false;
}
else
{
if (feeObject->isFieldPresent(sfBaseFeeDrops) ||
feeObject->isFieldPresent(sfReserveBaseDrops) ||
feeObject->isFieldPresent(sfReserveIncrementDrops))
return false;
// Read sfBaseFee as a hex string and compare to expected.baseFee
if (!checkEquality(sfBaseFee, expected.baseFee))
return false;
if (!checkEquality(sfReserveBase, expected.reserveBase))
return false;
if (!checkEquality(sfReserveIncrement, expected.reserveIncrement))
return false;
if (!checkEquality(sfReferenceFeeUnits, expected.referenceFeeUnits))
return false;
}
return true;
}
std::vector<STTx>
getTxs(std::shared_ptr<SHAMap> const& txSet)
{
std::vector<STTx> txs;
for (auto i = txSet->begin(); i != txSet->end(); ++i)
{
auto const data = i->slice();
auto serialIter = SerialIter(data);
txs.push_back(STTx(serialIter));
}
return txs;
};
class FeeVote_test : public beast::unit_test::suite
{
void
@@ -93,10 +285,517 @@ class FeeVote_test : public beast::unit_test::suite
}
}
void
testBasic()
{
testcase("Basic SetFee transaction");
// Test with XRPFees disabled (legacy format)
{
jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
// Test successful fee transaction with legacy fields
FeeSettingsFields fields{
.baseFee = 10,
.reserveBase = 200000,
.reserveIncrement = 50000,
.referenceFeeUnits = 10};
auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields);
OpenView accum(ledger.get());
BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx));
accum.apply(*ledger);
// Verify fee object was created/updated correctly
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields));
}
// Test with XRPFees enabled (new format)
{
jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
FeeSettingsFields fields{
.baseFeeDrops = XRPAmount{10},
.reserveBaseDrops = XRPAmount{200000},
.reserveIncrementDrops = XRPAmount{50000}};
// Test successful fee transaction with new fields
auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields);
OpenView accum(ledger.get());
BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx));
accum.apply(*ledger);
// Verify fee object was created/updated correctly
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields));
}
}
void
testTransactionValidation()
{
testcase("Fee Transaction Validation");
{
jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
// Test transaction with missing required legacy fields
auto invalidTx = createInvalidFeeTx(
ledger->rules(), ledger->seq(), true, false, 1);
OpenView accum(ledger.get());
BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx));
// Test transaction with new format fields when XRPFees is disabled
auto disallowedTx = createInvalidFeeTx(
ledger->rules(), ledger->seq(), false, true, 2);
BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx));
}
{
jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
// Test transaction with missing required new fields
auto invalidTx = createInvalidFeeTx(
ledger->rules(), ledger->seq(), true, false, 3);
OpenView accum(ledger.get());
BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx));
// Test transaction with legacy fields when XRPFees is enabled
auto disallowedTx = createInvalidFeeTx(
ledger->rules(), ledger->seq(), false, true, 4);
BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx));
}
}
void
testPseudoTransactionProperties()
{
testcase("Pseudo Transaction Properties");
jtx::Env env(*this, jtx::testable_amendments());
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
auto feeTx = createFeeTx(
ledger->rules(),
ledger->seq(),
{.baseFeeDrops = XRPAmount{10},
.reserveBaseDrops = XRPAmount{200000},
.reserveIncrementDrops = XRPAmount{50000}});
// Verify pseudo-transaction properties
BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID());
BEAST_EXPECT(feeTx.getFieldAmount(sfFee) == XRPAmount{0});
BEAST_EXPECT(feeTx.getSigningPubKey().empty());
BEAST_EXPECT(feeTx.getSignature().empty());
BEAST_EXPECT(!feeTx.isFieldPresent(sfSigners));
BEAST_EXPECT(feeTx.getFieldU32(sfSequence) == 0);
BEAST_EXPECT(!feeTx.isFieldPresent(sfPreviousTxnID));
// But can be applied to a closed ledger
{
OpenView closedAccum(ledger.get());
BEAST_EXPECT(applyFeeAndTestResult(env, closedAccum, feeTx));
}
}
void
testMultipleFeeUpdates()
{
testcase("Multiple Fee Updates");
jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
FeeSettingsFields fields1{
.baseFeeDrops = XRPAmount{10},
.reserveBaseDrops = XRPAmount{200000},
.reserveIncrementDrops = XRPAmount{50000}};
auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1);
{
OpenView accum(ledger.get());
BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1));
accum.apply(*ledger);
}
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1));
// Apply second fee transaction with different values
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
FeeSettingsFields fields2{
.baseFeeDrops = XRPAmount{20},
.reserveBaseDrops = XRPAmount{300000},
.reserveIncrementDrops = XRPAmount{75000}};
auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2);
{
OpenView accum(ledger.get());
BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2));
accum.apply(*ledger);
}
// Verify second update overwrote the first
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2));
}
void
testWrongLedgerSequence()
{
testcase("Wrong Ledger Sequence");
jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
// Test transaction with wrong ledger sequence
auto feeTx = createFeeTx(
ledger->rules(),
ledger->seq() + 5, // Wrong sequence (should be ledger->seq())
{.baseFeeDrops = XRPAmount{10},
.reserveBaseDrops = XRPAmount{200000},
.reserveIncrementDrops = XRPAmount{50000}});
OpenView accum(ledger.get());
// The transaction should still succeed as long as other fields are
// valid
// The ledger sequence field is only used for informational purposes
BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx));
}
void
testPartialFieldUpdates()
{
testcase("Partial Field Updates");
jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
FeeSettingsFields fields1{
.baseFeeDrops = XRPAmount{10},
.reserveBaseDrops = XRPAmount{200000},
.reserveIncrementDrops = XRPAmount{50000}};
auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1);
{
OpenView accum(ledger.get());
BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1));
accum.apply(*ledger);
}
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1));
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
// Apply partial update (only some fields)
FeeSettingsFields fields2{
.baseFeeDrops = XRPAmount{20},
.reserveBaseDrops = XRPAmount{200000}};
auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2);
{
OpenView accum(ledger.get());
BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2));
accum.apply(*ledger);
}
// Verify the partial update worked
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2));
}
void
testSingleInvalidTransaction()
{
testcase("Single Invalid Transaction");
jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees);
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
// Test invalid transaction with non-zero account - this should fail
// validation
auto invalidTx = STTx(ttFEE, [&](auto& obj) {
obj.setAccountID(
sfAccount,
AccountID(1)); // Should be zero (this makes it invalid)
obj.setFieldU32(sfLedgerSequence, ledger->seq());
obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10});
obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000});
obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000});
});
OpenView accum(ledger.get());
BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx));
}
void
testDoValidation()
{
testcase("doValidation");
using namespace jtx;
FeeSetup setup;
setup.reference_fee = 42;
setup.account_reserve = 1234567;
setup.owner_reserve = 7654321;
// Test with XRPFees enabled
{
Env env(*this, testable_amendments() | featureXRPFees);
auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote"));
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
auto sec = randomSecretKey();
auto pub = derivePublicKey(KeyType::secp256k1, sec);
auto val = std::make_shared<STValidation>(
env.app().timeKeeper().now(),
pub,
sec,
calcNodeID(pub),
[](STValidation& v) {
v.setFieldU32(sfLedgerSequence, 12345);
});
// Use the current ledger's fees as the "current" fees for
// doValidation
auto const& currentFees = ledger->fees();
feeVote->doValidation(currentFees, ledger->rules(), *val);
BEAST_EXPECT(val->isFieldPresent(sfBaseFeeDrops));
BEAST_EXPECT(
val->getFieldAmount(sfBaseFeeDrops) ==
XRPAmount(setup.reference_fee));
}
// Test with XRPFees disabled (legacy format)
{
Env env(*this, testable_amendments() - featureXRPFees);
auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote"));
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
auto sec = randomSecretKey();
auto pub = derivePublicKey(KeyType::secp256k1, sec);
auto val = std::make_shared<STValidation>(
env.app().timeKeeper().now(),
pub,
sec,
calcNodeID(pub),
[](STValidation& v) {
v.setFieldU32(sfLedgerSequence, 12345);
});
auto const& currentFees = ledger->fees();
feeVote->doValidation(currentFees, ledger->rules(), *val);
// In legacy mode, should vote using legacy fields
BEAST_EXPECT(val->isFieldPresent(sfBaseFee));
BEAST_EXPECT(val->getFieldU64(sfBaseFee) == setup.reference_fee);
}
}
void
testDoVoting()
{
testcase("doVoting");
using namespace jtx;
FeeSetup setup;
setup.reference_fee = 42;
setup.account_reserve = 1234567;
setup.owner_reserve = 7654321;
Env env(*this, testable_amendments() | featureXRPFees);
// establish what the current fees are
BEAST_EXPECT(
env.current()->fees().base == XRPAmount{UNIT_TEST_REFERENCE_FEE});
BEAST_EXPECT(env.current()->fees().reserve == XRPAmount{200'000'000});
BEAST_EXPECT(env.current()->fees().increment == XRPAmount{50'000'000});
auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote"));
auto ledger = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
// doVoting requires a flag ledger (every 256th ledger)
// We need to create a ledger at sequence 256 to make it a flag ledger
for (int i = 0; i < 256 - 1; ++i)
{
ledger = std::make_shared<Ledger>(
*ledger, env.app().timeKeeper().closeTime());
}
BEAST_EXPECT(ledger->isFlagLedger());
// Create some mock validations with fee votes
std::vector<std::shared_ptr<STValidation>> validations;
for (int i = 0; i < 5; i++)
{
auto sec = randomSecretKey();
auto pub = derivePublicKey(KeyType::secp256k1, sec);
auto val = std::make_shared<STValidation>(
env.app().timeKeeper().now(),
pub,
sec,
calcNodeID(pub),
[&](STValidation& v) {
v.setFieldU32(sfLedgerSequence, ledger->seq());
// Vote for different fees than current
v.setFieldAmount(
sfBaseFeeDrops, XRPAmount{setup.reference_fee});
v.setFieldAmount(
sfReserveBaseDrops, XRPAmount{setup.account_reserve});
v.setFieldAmount(
sfReserveIncrementDrops,
XRPAmount{setup.owner_reserve});
});
if (i % 2)
val->setTrusted();
validations.push_back(val);
}
auto txSet = std::make_shared<SHAMap>(
SHAMapType::TRANSACTION, env.app().getNodeFamily());
// This should not throw since we have a flag ledger
feeVote->doVoting(ledger, validations, txSet);
auto const txs = getTxs(txSet);
BEAST_EXPECT(txs.size() == 1);
auto const& feeTx = txs[0];
BEAST_EXPECT(feeTx.getTxnType() == ttFEE);
BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID());
BEAST_EXPECT(feeTx.getFieldU32(sfLedgerSequence) == ledger->seq() + 1);
BEAST_EXPECT(feeTx.isFieldPresent(sfBaseFeeDrops));
BEAST_EXPECT(feeTx.isFieldPresent(sfReserveBaseDrops));
BEAST_EXPECT(feeTx.isFieldPresent(sfReserveIncrementDrops));
// The legacy fields should NOT be present
BEAST_EXPECT(!feeTx.isFieldPresent(sfBaseFee));
BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveBase));
BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveIncrement));
BEAST_EXPECT(!feeTx.isFieldPresent(sfReferenceFeeUnits));
// Check the values
BEAST_EXPECT(
feeTx.getFieldAmount(sfBaseFeeDrops) ==
XRPAmount{setup.reference_fee});
BEAST_EXPECT(
feeTx.getFieldAmount(sfReserveBaseDrops) ==
XRPAmount{setup.account_reserve});
BEAST_EXPECT(
feeTx.getFieldAmount(sfReserveIncrementDrops) ==
XRPAmount{setup.owner_reserve});
}
void
run() override
{
testSetup();
testBasic();
testTransactionValidation();
testPseudoTransactionProperties();
testMultipleFeeUpdates();
testWrongLedgerSequence();
testPartialFieldUpdates();
testSingleInvalidTransaction();
testDoValidation();
testDoVoting();
}
};

View File

@@ -768,6 +768,24 @@ private:
expectUntrusted(lists.at(7));
expectTrusted(lists.at(2));
// try empty or mangled manifest
checkResult(
trustedKeys->applyLists(
"", version, {{blob7, sig7, {}}, {blob6, sig6, {}}}, siteUri),
publisherPublic,
ListDisposition::invalid,
ListDisposition::invalid);
checkResult(
trustedKeys->applyLists(
base64_encode("not a manifest"),
version,
{{blob7, sig7, {}}, {blob6, sig6, {}}},
siteUri),
publisherPublic,
ListDisposition::invalid,
ListDisposition::invalid);
// do not use list from untrusted publisher
auto const untrustedManifest = base64_encode(makeManifestString(
randomMasterKey(),

View File

@@ -12,3 +12,5 @@ xrpl_add_test(basics)
target_link_libraries(xrpl.test.basics PRIVATE xrpl.imports.test)
xrpl_add_test(crypto)
target_link_libraries(xrpl.test.crypto PRIVATE xrpl.imports.test)
xrpl_add_test(net)
target_link_libraries(xrpl.test.net PRIVATE xrpl.imports.test)

View File

@@ -0,0 +1,346 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/basics/Log.h>
#include <xrpl/net/HTTPClient.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <doctest/doctest.h>
#include <atomic>
#include <map>
#include <thread>
using namespace ripple;
namespace {
// Simple HTTP server using Beast for testing
class TestHTTPServer
{
private:
boost::asio::io_context ioc_;
boost::asio::ip::tcp::acceptor acceptor_;
boost::asio::ip::tcp::endpoint endpoint_;
std::atomic<bool> running_{true};
unsigned short port_;
// Custom headers to return
std::map<std::string, std::string> custom_headers_;
std::string response_body_;
unsigned int status_code_{200};
public:
TestHTTPServer() : acceptor_(ioc_), port_(0)
{
// Bind to any available port
endpoint_ = {boost::asio::ip::tcp::v4(), 0};
acceptor_.open(endpoint_.protocol());
acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
acceptor_.bind(endpoint_);
acceptor_.listen();
// Get the actual port that was assigned
port_ = acceptor_.local_endpoint().port();
accept();
}
~TestHTTPServer()
{
stop();
}
boost::asio::io_context&
ioc()
{
return ioc_;
}
unsigned short
port() const
{
return port_;
}
void
setHeader(std::string const& name, std::string const& value)
{
custom_headers_[name] = value;
}
void
setResponseBody(std::string const& body)
{
response_body_ = body;
}
void
setStatusCode(unsigned int code)
{
status_code_ = code;
}
private:
void
stop()
{
running_ = false;
acceptor_.close();
}
void
accept()
{
if (!running_)
return;
acceptor_.async_accept(
ioc_,
endpoint_,
[&](boost::system::error_code const& error,
boost::asio::ip::tcp::socket peer) {
if (!running_)
return;
if (!error)
{
handleConnection(std::move(peer));
}
});
}
void
handleConnection(boost::asio::ip::tcp::socket socket)
{
try
{
// Read the HTTP request
boost::beast::flat_buffer buffer;
boost::beast::http::request<boost::beast::http::string_body> req;
boost::beast::http::read(socket, buffer, req);
// Create response
boost::beast::http::response<boost::beast::http::string_body> res;
res.version(req.version());
res.result(status_code_);
res.set(boost::beast::http::field::server, "TestServer");
// Add custom headers
for (auto const& [name, value] : custom_headers_)
{
res.set(name, value);
}
// Set body and prepare payload first
res.body() = response_body_;
res.prepare_payload();
// Override Content-Length with custom headers after prepare_payload
// This allows us to test case-insensitive header parsing
for (auto const& [name, value] : custom_headers_)
{
if (boost::iequals(name, "Content-Length"))
{
res.erase(boost::beast::http::field::content_length);
res.set(name, value);
}
}
// Send response
boost::beast::http::write(socket, res);
// Shutdown socket gracefully
boost::system::error_code ec;
socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
}
catch (std::exception const&)
{
// Connection handling errors are expected
}
if (running_)
accept();
}
};
// Helper function to run HTTP client test
bool
runHTTPTest(
TestHTTPServer& server,
std::string const& path,
std::atomic<bool>& completed,
std::atomic<int>& result_status,
std::string& result_data,
boost::system::error_code& result_error)
{
// Create a null journal for testing
beast::Journal j{beast::Journal::getNullSink()};
// Initialize HTTPClient SSL context
HTTPClient::initializeSSLContext("", "", false, j);
HTTPClient::get(
false, // no SSL
server.ioc(),
"127.0.0.1",
server.port(),
path,
1024, // max response size
std::chrono::seconds(5),
[&](boost::system::error_code const& ec,
int status,
std::string const& data) -> bool {
result_error = ec;
result_status = status;
result_data = data;
completed = true;
return false; // don't retry
},
j);
// Run the IO context until completion
auto start = std::chrono::steady_clock::now();
while (!completed &&
std::chrono::steady_clock::now() - start < std::chrono::seconds(10))
{
if (server.ioc().run_one() == 0)
{
break;
}
}
return completed;
}
} // anonymous namespace
TEST_CASE("HTTPClient case insensitive Content-Length")
{
// Test different cases of Content-Length header
std::vector<std::string> header_cases = {
"Content-Length", // Standard case
"content-length", // Lowercase - this tests the regex icase fix
"CONTENT-LENGTH", // Uppercase
"Content-length", // Mixed case
"content-Length" // Mixed case 2
};
for (auto const& header_name : header_cases)
{
TestHTTPServer server;
std::string test_body = "Hello World!";
server.setResponseBody(test_body);
server.setHeader(header_name, std::to_string(test_body.size()));
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server,
"/test",
completed,
result_status,
result_data,
result_error);
// Verify results
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == 200);
CHECK(result_data == test_body);
}
}
TEST_CASE("HTTPClient basic HTTP request")
{
TestHTTPServer server;
std::string test_body = "Test response body";
server.setResponseBody(test_body);
server.setHeader("Content-Type", "text/plain");
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server, "/basic", completed, result_status, result_data, result_error);
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == 200);
CHECK(result_data == test_body);
}
TEST_CASE("HTTPClient empty response")
{
TestHTTPServer server;
server.setResponseBody(""); // Empty body
server.setHeader("Content-Length", "0");
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server, "/empty", completed, result_status, result_data, result_error);
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == 200);
CHECK(result_data.empty());
}
TEST_CASE("HTTPClient different status codes")
{
std::vector<unsigned int> status_codes = {200, 404, 500};
for (auto status : status_codes)
{
TestHTTPServer server;
server.setStatusCode(status);
server.setResponseBody("Status " + std::to_string(status));
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server,
"/status",
completed,
result_status,
result_data,
result_error);
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == static_cast<int>(status));
}
}

View File

@@ -0,0 +1,21 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>

View File

@@ -877,7 +877,7 @@ private:
verify(
lock_guard const&,
Json::Value& list,
std::string const& manifest,
Manifest manifest,
std::string const& blob,
std::string const& signature);

View File

@@ -1149,21 +1149,33 @@ ValidatorList::applyList(
Json::Value list;
auto const& manifest = localManifest ? *localManifest : globalManifest;
auto [result, pubKeyOpt] = verify(lock, list, manifest, blob, signature);
auto m = deserializeManifest(base64_decode(manifest));
if (!m)
{
JLOG(j_.warn()) << "UNL manifest cannot be deserialized";
return PublisherListStats{ListDisposition::invalid};
}
auto [result, pubKeyOpt] =
verify(lock, list, std::move(*m), blob, signature);
if (!pubKeyOpt)
{
JLOG(j_.info()) << "ValidatorList::applyList unable to retrieve the "
"master public key from the verify function\n";
JLOG(j_.warn())
<< "UNL manifest is signed with an unrecognized master public key";
return PublisherListStats{result};
}
if (!publicKeyType(*pubKeyOpt))
{
JLOG(j_.info()) << "ValidatorList::applyList Invalid Public Key type"
" retrieved from the verify function\n ";
{ // LCOV_EXCL_START
// This is an impossible situation because we will never load an
// invalid public key type (see checks in `ValidatorList::load`) however
// we can only arrive here if the key used by the manifest matched one of
// the loaded keys
UNREACHABLE(
"ripple::ValidatorList::applyList : invalid public key type");
return PublisherListStats{result};
}
} // LCOV_EXCL_STOP
PublicKey pubKey = *pubKeyOpt;
if (result > ListDisposition::pending)
@@ -1356,19 +1368,17 @@ std::pair<ListDisposition, std::optional<PublicKey>>
ValidatorList::verify(
ValidatorList::lock_guard const& lock,
Json::Value& list,
std::string const& manifest,
Manifest manifest,
std::string const& blob,
std::string const& signature)
{
auto m = deserializeManifest(base64_decode(manifest));
if (!m || !publisherLists_.count(m->masterKey))
if (!publisherLists_.count(manifest.masterKey))
return {ListDisposition::untrusted, {}};
PublicKey masterPubKey = m->masterKey;
auto const revoked = m->revoked();
PublicKey masterPubKey = manifest.masterKey;
auto const revoked = manifest.revoked();
auto const result = publisherManifests_.applyManifest(std::move(*m));
auto const result = publisherManifests_.applyManifest(std::move(manifest));
if (revoked && result == ManifestDisposition::accepted)
{
@@ -1796,7 +1806,7 @@ ValidatorList::getAvailable(
if (!keyBlob || !publicKeyType(makeSlice(*keyBlob)))
{
JLOG(j_.info()) << "Invalid requested validator list publisher key: "
JLOG(j_.warn()) << "Invalid requested validator list publisher key: "
<< pubKey;
return {};
}

View File

@@ -17,6 +17,34 @@
*/
//==============================================================================
/**
*
* TODO: Remove ripple::basic_semaphore (and this file) and use
* std::counting_semaphore.
*
* Background:
* - PR: https://github.com/XRPLF/rippled/pull/5512/files
* - std::counting_semaphore had a bug fixed in both GCC and Clang:
* * GCC PR 104928: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928
* * LLVM PR 79265: https://github.com/llvm/llvm-project/pull/79265
*
* GCC:
* According to GCC Bugzilla PR104928
* (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928#c15), the fix is
* scheduled for inclusion in GCC 16.0 (see comment #15, Target
* Milestone: 16.0). It is not included in GCC 14.x or earlier, and there is no
* indication that it will be backported to GCC 13.x or 14.x branches.
*
* Clang:
* The fix for is included in Clang 19.1.0+
*
* Once the minimum compiler version is updated to > GCC 16.0 or Clang 19.1.0,
* we can remove this file.
*
* WARNING: Avoid using std::counting_semaphore until the minimum compiler
* version is updated.
*/
#ifndef RIPPLE_CORE_SEMAPHORE_H_INCLUDED
#define RIPPLE_CORE_SEMAPHORE_H_INCLUDED