Compare commits

..

13 Commits

Author SHA1 Message Date
Bart
1bb53ee0a9 Fix secret name, consolidate action and workflow 2026-01-20 13:42:43 -05:00
Bart
49b96cd307 Review feedback 2026-01-20 09:48:24 -05:00
Bart
08e9f439ff Fix comment 2026-01-16 15:38:30 -05:00
Bart
c6dc700673 Merge branch 'develop' into bthomee/upload 2026-01-16 15:05:10 -05:00
Bart
12c0d67ff6 ci: remove 'master' branch as a trigger (#6234)
This change removes the `master` branch as a trigger for the CI pipelines, and updates comments accordingly. It also fixes the pre-commit workflow, so it will run on all release branches.
2026-01-16 15:01:53 -05:00
Bart
72f0a30dfe Can't use secrets in if-statement 2026-01-16 14:04:54 -05:00
Bart
9ac4e95ec2 Can't use secrets in if-statement 2026-01-16 14:03:50 -05:00
Bart
7b1c83e6e1 Merge branch 'develop' into bthomee/upload 2026-01-16 13:58:07 -05:00
Bart
a43c4ce441 Shorten commit hash 2026-01-16 13:57:30 -05:00
Bart
aae623eb67 ci: Upload Conan recipe for merges into develop and commits to release 2026-01-16 13:45:07 -05:00
Ed Hennis
00d3cee6cc Improve ledger_entry lookups for fee, amendments, NUNL, and hashes (#5644)
These "fixed location" objects can be found in multiple ways:

1. The lookup parameters use the same format as other ledger objects, but the only valid value is true or the valid index of the object: 
  - Amendments: "amendments" : true
  - FeeSettings: "fee" : true
  - NegativeUNL: "nunl" : true
  - LedgerHashes: "hashes" : true (For the "short" list. See below.)

2. With RPC API >= 3, using special case values to "index", such as "index" : "amendments". Uses the same names as above. Note that for "hashes", this option will only return the recent ledger hashes / "short" skip list.

3. LedgerHashes has two types: "short", which stores recent ledger hashes, and "long", which stores the flag ledger hashes for a particular ledger range.
  - To find a "long" LedgerHashes object, request '"hashes" : <ledger sequence>'. <ledger sequence> must be a number that evaluates to an unsigned integer.
  - To find the "short" LedgerHashes object, request "hashes": true as with the other fixed objects.

The following queries are all functionally equivalent:

  - "amendments" : true
  - "index" : "amendments" (API >=3 only)
  - "amendments" : "7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4"
  - "index" : "7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4"

Finally, whether the object is found or not, if a valid index is computed, that index will be returned. This can be used to confirm the query was valid, or to save the index for future use.
2026-01-16 12:26:30 -05:00
Bart
857a4eea9a Add shell: bash to each composite action step 2026-01-15 14:23:30 -05:00
Bart
24177fdca0 ci: Move Conan package versioning and uploading into action 2026-01-15 14:19:34 -05:00
17 changed files with 875 additions and 498 deletions

View File

@@ -0,0 +1,32 @@
name: Generate build version number
description: "Generate build version number."
outputs:
version:
description: "The generated build version number."
value: ${{ steps.version.outputs.version }}
runs:
using: composite
steps:
- name: Generate version
id: version
shell: bash
env:
# Append a shortened commit hash for the develop branch. For releases
# the version is used as-is.
COMMIT_HASH: ${{ github.ref == 'refs/heads/develop' && github.sha || '' }}
run: |
echo 'Extracting version from BuildInfo.cpp.'
VERSION="$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')"
if [[ -z "${VERSION}" ]]; then
echo 'Unable to extract version from BuildInfo.cpp.'
exit 1
fi
if [[ -n "${COMMIT_HASH}" ]]; then
echo 'Appending shortened commit hash to version.'
VERSION="${VERSION}-${COMMIT_HASH:0:7}"
fi
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"

View File

@@ -2,11 +2,11 @@ name: Setup Conan
description: "Set up Conan configuration, profile, and remote."
inputs:
conan_remote_name:
remote_name:
description: "The name of the Conan remote to use."
required: false
default: xrplf
conan_remote_url:
remote_url:
description: "The URL of the Conan endpoint to use."
required: false
default: https://conan.ripplex.io
@@ -36,11 +36,11 @@ runs:
- name: Set up Conan remote
shell: bash
env:
CONAN_REMOTE_NAME: ${{ inputs.conan_remote_name }}
CONAN_REMOTE_URL: ${{ inputs.conan_remote_url }}
REMOTE_NAME: ${{ inputs.remote_name }}
REMOTE_URL: ${{ inputs.remote_url }}
run: |
echo "Adding Conan remote '${CONAN_REMOTE_NAME}' at '${CONAN_REMOTE_URL}'."
conan remote add --index 0 --force "${CONAN_REMOTE_NAME}" "${CONAN_REMOTE_URL}"
echo "Adding Conan remote '${REMOTE_NAME}' at '${REMOTE_URL}'."
conan remote add --index 0 --force "${REMOTE_NAME}" "${REMOTE_URL}"
echo 'Listing Conan remotes.'
conan remote list

View File

@@ -20,8 +20,8 @@ class Config:
Generate a strategy matrix for GitHub Actions CI.
On each PR commit we will build a selection of Debian, RHEL, Ubuntu, MacOS, and
Windows configurations, while upon merge into the develop, release, or master
branches, we will build all configurations, and test most of them.
Windows configurations, while upon merge into the develop or release branches,
we will build all configurations, and test most of them.
We will further set additional CMake arguments as follows:
- All builds will have the `tests`, `werr`, and `xrpld` options.

View File

@@ -53,7 +53,6 @@ jobs:
.github/scripts/rename/**
.github/workflows/reusable-check-levelization.yml
.github/workflows/reusable-check-rename.yml
.github/workflows/reusable-notify-clio.yml
.github/workflows/on-pr.yml
# Keep the paths below in sync with those in `on-trigger.yml`.
@@ -66,6 +65,7 @@ jobs:
.github/workflows/reusable-build-test.yml
.github/workflows/reusable-strategy-matrix.yml
.github/workflows/reusable-test.yml
.github/workflows/reusable-upload-recipe.yml
.codecov.yml
cmake/**
conan/**
@@ -121,22 +121,43 @@ jobs:
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
notify-clio:
upload-recipe:
needs:
- should-run
- build-test
if: ${{ needs.should-run.outputs.go == 'true' && (startsWith(github.base_ref, 'release') || github.base_ref == 'master') }}
uses: ./.github/workflows/reusable-notify-clio.yml
# Only run when committing to the release branch in the XRPLF repository. We
# also upload the recipe when merging into the develop branch, but that is
# handled in the on-trigger.yml workflow.
if: ${{ github.repository_owner == 'XRPLF' && needs.should-run.outputs.go == 'true' && startsWith(github.ref, 'refs/heads/release') }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }}
conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
conan_remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
notify-clio:
needs: upload-recipe
runs-on: ubuntu-latest
steps:
# Notify the Clio repository about the newly proposed release version, so
# it can be checked for compatibility before the release is actually made.
- name: Notify Clio
env:
GH_TOKEN: ${{ secrets.CLIO_NOTIFY_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \
/repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \
-F "client_payload[ref]=${{ needs.upload-recipe.outputs.recipe_ref }}" \
-F "client_payload[pr_url]=${PR_URL}"
passed:
if: failure() || cancelled()
needs:
- build-test
- check-levelization
- check-rename
- build-test
- upload-recipe
- notify-clio
runs-on: ubuntu-latest
steps:
- name: Fail

View File

@@ -1,9 +1,8 @@
# This workflow runs all workflows to build the dependencies required for the
# project on various Linux flavors, as well as on MacOS and Windows, on a
# scheduled basis, on merge into the 'develop', 'release', or 'master' branches,
# or manually. The missing commits check is only run when the code is merged
# into the 'develop' or 'release' branches, and the documentation is built when
# the code is merged into the 'develop' branch.
# This workflow runs all workflows to build and test the code on various Linux
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
# the 'develop' or 'release*' branches, or when requested manually. Upon
# successful completion, it also uploads the built libxrpl package to the Conan
# remote.
name: Trigger
on:
@@ -11,7 +10,6 @@ on:
branches:
- "develop"
- "release*"
- "master"
paths:
# These paths are unique to `on-trigger.yml`.
- ".github/workflows/on-trigger.yml"
@@ -26,6 +24,7 @@ on:
- ".github/workflows/reusable-build-test.yml"
- ".github/workflows/reusable-strategy-matrix.yml"
- ".github/workflows/reusable-test.yml"
- ".github/workflows/reusable-upload-recipe.yml"
- ".codecov.yml"
- "cmake/**"
- "conan/**"
@@ -70,11 +69,22 @@ jobs:
with:
# Enable ccache only for events targeting the XRPLF repository, since
# other accounts will not have access to our remote cache storage.
# However, we do not enable ccache for events targeting the master or a
# release branch, to protect against the rare case that the output
# produced by ccache is not identical to a regular compilation.
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !(github.base_ref == 'master' || startsWith(github.base_ref, 'release')) }}
# However, we do not enable ccache for events targeting a release branch,
# to protect against the rare case that the output produced by ccache is
# not identical to a regular compilation.
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
os: ${{ matrix.os }}
strategy_matrix: ${{ github.event_name == 'schedule' && 'all' || 'minimal' }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
upload-recipe:
needs: build-test
# Only run when pushing to the develop branch in the XRPLF repository. We
# also upload the recipe when committing to a release branch, but that is
# handled in the on-pr.yml workflow.
if: ${{ github.repository_owner == 'XRPLF' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}

View File

@@ -3,7 +3,9 @@ name: Run pre-commit hooks
on:
pull_request:
push:
branches: [develop, release, master]
branches:
- "develop"
- "release*"
workflow_dispatch:
jobs:

View File

@@ -1,91 +0,0 @@
# This workflow exports the built libxrpl package to the Conan remote on a
# a channel named after the pull request, and notifies the Clio repository about
# the new version so it can check for compatibility.
name: Notify Clio
# This workflow can only be triggered by other workflows.
on:
workflow_call:
inputs:
conan_remote_name:
description: "The name of the Conan remote to use."
required: false
type: string
default: xrplf
conan_remote_url:
description: "The URL of the Conan endpoint to use."
required: false
type: string
default: https://conan.ripplex.io
secrets:
clio_notify_token:
description: "The GitHub token to notify Clio about new versions."
required: true
conan_remote_username:
description: "The username for logging into the Conan remote."
required: true
conan_remote_password:
description: "The password for logging into the Conan remote."
required: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-clio
cancel-in-progress: true
defaults:
run:
shell: bash
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-sha-5dd7158
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Generate outputs
id: generate
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
echo 'Generating user and channel.'
echo "user=clio" >> "${GITHUB_OUTPUT}"
echo "channel=pr_${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
echo 'Extracting version.'
echo "version=$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')" >> "${GITHUB_OUTPUT}"
- name: Calculate conan reference
id: conan_ref
run: |
echo "conan_ref=${{ steps.generate.outputs.version }}@${{ steps.generate.outputs.user }}/${{ steps.generate.outputs.channel }}" >> "${GITHUB_OUTPUT}"
- name: Set up Conan
uses: ./.github/actions/setup-conan
with:
conan_remote_name: ${{ inputs.conan_remote_name }}
conan_remote_url: ${{ inputs.conan_remote_url }}
- name: Log into Conan remote
env:
CONAN_REMOTE_NAME: ${{ inputs.conan_remote_name }}
run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.conan_remote_username }}" --password "${{ secrets.conan_remote_password }}"
- name: Upload package
env:
CONAN_REMOTE_NAME: ${{ inputs.conan_remote_name }}
run: |
conan export --user=${{ steps.generate.outputs.user }} --channel=${{ steps.generate.outputs.channel }} .
conan upload --confirm --check --remote="${CONAN_REMOTE_NAME}" xrpl/${{ steps.conan_ref.outputs.conan_ref }}
outputs:
conan_ref: ${{ steps.conan_ref.outputs.conan_ref }}
notify:
needs: upload
runs-on: ubuntu-latest
steps:
- name: Notify Clio
env:
GH_TOKEN: ${{ secrets.clio_notify_token }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \
/repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \
-F "client_payload[conan_ref]=${{ needs.upload.outputs.conan_ref }}" \
-F "client_payload[pr_url]=${PR_URL}"

View File

@@ -0,0 +1,78 @@
# This workflow exports the built libxrpl package to the Conan remote.
name: Upload Conan recipe
# This workflow can only be triggered by other workflows.
on:
workflow_call:
inputs:
remote_name:
description: "The name of the Conan remote to use."
required: false
type: string
default: xrplf
remote_url:
description: "The URL of the Conan endpoint to use."
required: false
type: string
default: https://conan.ripplex.io
secrets:
remote_username:
description: "The username for logging into the Conan remote."
required: true
remote_password:
description: "The password for logging into the Conan remote."
required: true
outputs:
recipe_ref:
description: "The Conan recipe reference ('name/version') that was uploaded."
value: ${{ jobs.upload.outputs.ref }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-upload-recipe
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13-sha-5dd7158
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Generate build version number
id: version
uses: ./.github/actions/generate-version
- name: Determine recipe reference
id: ref
run: echo "ref=xrpl/${{ steps.version.outputs.version }}" >> "${GITHUB_OUTPUT}"
- name: Set up Conan
uses: ./.github/actions/setup-conan
with:
remote_name: ${{ inputs.remote_name }}
remote_url: ${{ inputs.remote_url }}
- name: Log into Conan remote
env:
REMOTE_NAME: ${{ inputs.remote_name }}
REMOTE_USERNAME: ${{ inputs.remote_username }}
REMOTE_PASSWORD: ${{ inputs.remote_password }}
run: conan remote login "${REMOTE_NAME}" "${REMOTE_USERNAME}" --password "${REMOTE_PASSWORD}"
- name: Upload Conan recipe
env:
RECIPE_REF: ${{ steps.ref.outputs.ref }}
REMOTE_NAME: ${{ inputs.remote_name }}
run: |
conan export .
conan upload --confirm --check --remote="${REMOTE_NAME}" ${RECIPE_REF}
outputs:
ref: ${{ steps.ref.outputs.ref }}

View File

@@ -86,8 +86,8 @@ jobs:
- name: Setup Conan
uses: ./.github/actions/setup-conan
with:
conan_remote_name: ${{ env.CONAN_REMOTE_NAME }}
conan_remote_url: ${{ env.CONAN_REMOTE_URL }}
remote_name: ${{ env.CONAN_REMOTE_NAME }}
remote_url: ${{ env.CONAN_REMOTE_URL }}
- name: Build dependencies
uses: ./.github/actions/build-deps

View File

@@ -109,10 +109,6 @@ template <class T>
concept Integral64 =
std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>;
template <class STAmount, class Asset>
concept CanUseAsScale = requires(Asset a, Number n) { STAmount(a, n); } &&
requires(STAmount s) { s.exponent(); };
/** Number is a floating point type that can represent a wide range of values.
*
* It can represent all values that can be represented by an STAmount -
@@ -272,26 +268,6 @@ public:
constexpr int
exponent() const noexcept;
/** Get the scale of this Number for the given asset.
*
* "scale" is similar to "exponent", but from the perspective of STAmount,
* which has different rules for determining the exponent than Number.
*
* Because Number does not have access to STAmount or Asset, this function
* is implemented as a template, with the expectation that it will only be
* used by those types. Any types that fit the requirements will work,
* though, if there's a need.
*
* @tparam STAmount The STAmount type.
* @tparam Asset The Asset type.
* @param asset The asset to use for determining the scale.
* @return The scale of this Number for the given asset.
*/
template <class STAmount, class Asset>
int
scale(Asset const& asset) const
requires CanUseAsScale<STAmount, Asset>;
constexpr Number
operator+() const noexcept;
constexpr Number
@@ -626,14 +602,6 @@ Number::exponent() const noexcept
return e;
}
template <class STAmount, class Asset>
int
Number::scale(Asset const& asset) const
requires CanUseAsScale<STAmount, Asset>
{
return STAmount{asset, *this}.exponent();
}
inline constexpr Number
Number::operator+() const noexcept
{

View File

@@ -4,7 +4,6 @@
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpld/app/tx/detail/InvariantCheck.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
@@ -21,9 +20,6 @@
#include <boost/algorithm/string/predicate.hpp>
#include <initializer_list>
#include <string>
namespace xrpl {
namespace test {
@@ -3892,122 +3888,6 @@ class Invariants_test : public beast::unit_test::suite
precloseMpt);
}
void
testVaultComputeMinScale()
{
using namespace jtx;
Account const issuer{"issuer"};
PrettyAsset const vaultAsset = issuer["IOU"];
struct TestCase
{
std::string name;
std::int32_t expectedMinScale;
std::initializer_list<Number const> values;
};
NumberMantissaScaleGuard g{MantissaRange::large};
auto const testCases = std::vector<TestCase>{
{
.name = "No values",
.expectedMinScale = 0,
.values = {},
},
{
.name = "Mixed integer and Number values",
.expectedMinScale = -15,
.values = {1, -1, Number{10, -1}},
},
{
.name = "Mixed scales",
.expectedMinScale = -17,
.values = {Number{1, -2}, Number{5, -3}, Number{3, -2}},
},
{
.name = "Mixed mantissa sizes",
.expectedMinScale = -12,
.values =
{Number{1},
Number{1234, -3},
Number{12345, -6},
Number{123, 1}},
},
};
for (auto const& tc : testCases)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale =
ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) +
", actual: " + std::to_string(actualScale));
for (auto const& num : tc.values)
{
// None of these scales are far enough apart that rounding the
// values would lose information, so check that the rounded
// value matches the original.
auto const actualRounded =
roundToAsset(vaultAsset, num, actualScale);
BEAST_EXPECTS(
actualRounded == num,
"number " + to_string(num) + " rounded to scale " +
std::to_string(actualScale) + " is " +
to_string(actualRounded));
}
}
auto const testCases2 = std::vector<TestCase>{
{
.name = "False equivalence",
.expectedMinScale = -15,
.values =
{
Number{1234567890123456789, -18},
Number{12345, -4},
Number{1},
},
},
};
// Unlike the first set of test cases, the values in these test could
// look equivalent if using the wrong scale.
for (auto const& tc : testCases2)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale =
ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) +
", actual: " + std::to_string(actualScale));
std::optional<Number> first;
Number firstRounded;
for (auto const& num : tc.values)
{
if (!first)
{
first = num;
firstRounded = roundToAsset(vaultAsset, num, actualScale);
continue;
}
auto const numRounded =
roundToAsset(vaultAsset, num, actualScale);
BEAST_EXPECTS(
numRounded != firstRounded,
"at a scale of " + std::to_string(actualScale) + " " +
to_string(num) + " == " + to_string(*first));
}
}
}
public:
void
run() override
@@ -4031,7 +3911,6 @@ public:
testValidPseudoAccounts();
testValidLoanBroker();
testVault();
testVaultComputeMinScale();
}
};

View File

@@ -5,6 +5,8 @@
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>
#include <xrpld/app/tx/apply.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
@@ -30,6 +32,7 @@ enum class FieldType {
CurrencyField,
HashField,
HashOrObjectField,
FixedHashField,
IssueField,
ObjectField,
StringField,
@@ -86,6 +89,7 @@ getTypeName(FieldType typeID)
case FieldType::CurrencyField:
return "Currency";
case FieldType::HashField:
case FieldType::FixedHashField:
return "hex string";
case FieldType::HashOrObjectField:
return "hex string or object";
@@ -202,6 +206,7 @@ class LedgerEntry_test : public beast::unit_test::suite
static auto const& badBlobValues = remove({3, 7, 8, 16});
static auto const& badCurrencyValues = remove({14});
static auto const& badHashValues = remove({2, 3, 7, 8, 16});
static auto const& badFixedHashValues = remove({1, 2, 3, 4, 7, 8, 16});
static auto const& badIndexValues = remove({12, 16, 18, 19});
static auto const& badUInt32Values = remove({2, 3});
static auto const& badUInt64Values = remove({2, 3});
@@ -222,6 +227,8 @@ class LedgerEntry_test : public beast::unit_test::suite
return badHashValues;
case FieldType::HashOrObjectField:
return badIndexValues;
case FieldType::FixedHashField:
return badFixedHashValues;
case FieldType::IssueField:
return badIssueValues;
case FieldType::UInt32Field:
@@ -717,7 +724,12 @@ class LedgerEntry_test : public beast::unit_test::suite
}
// negative tests
runLedgerEntryTest(env, jss::amendments);
testMalformedField(
env,
Json::Value{},
jss::amendments,
FieldType::FixedHashField,
"malformedRequest");
}
void
@@ -1538,7 +1550,12 @@ class LedgerEntry_test : public beast::unit_test::suite
}
// negative tests
runLedgerEntryTest(env, jss::fee);
testMalformedField(
env,
Json::Value{},
jss::fee,
FieldType::FixedHashField,
"malformedRequest");
}
void
@@ -1561,7 +1578,12 @@ class LedgerEntry_test : public beast::unit_test::suite
}
// negative tests
runLedgerEntryTest(env, jss::hashes);
testMalformedField(
env,
Json::Value{},
jss::hashes,
FieldType::FixedHashField,
"malformedRequest");
}
void
@@ -1686,7 +1708,12 @@ class LedgerEntry_test : public beast::unit_test::suite
}
// negative tests
runLedgerEntryTest(env, jss::nunl);
testMalformedField(
env,
Json::Value{},
jss::nunl,
FieldType::FixedHashField,
"malformedRequest");
}
void
@@ -2343,6 +2370,438 @@ class LedgerEntry_test : public beast::unit_test::suite
}
}
/// Test the ledger entry types that don't take parameters
void
testFixed()
{
using namespace test::jtx;
Account const alice{"alice"};
Account const bob{"bob"};
Env env{*this, envconfig([](auto cfg) {
cfg->START_UP = Config::FRESH;
return cfg;
})};
env.close();
/** Verifies that the RPC result has the expected data
*
* @param good: Indicates that the request should have succeeded
* and returned a ledger object of `expectedType` type.
* @param jv: The RPC result Json value
* @param expectedType: The type that the ledger object should
* have if "good".
* @param expectedError: Optional. The expected error if not
* good. Defaults to "entryNotFound".
*/
auto checkResult =
[&](bool good,
Json::Value const& jv,
Json::StaticString const& expectedType,
std::optional<std::string> const& expectedError = {}) {
if (good)
{
BEAST_EXPECTS(
jv.isObject() && jv.isMember(jss::result) &&
!jv[jss::result].isMember(jss::error) &&
jv[jss::result].isMember(jss::node) &&
jv[jss::result][jss::node].isMember(
sfLedgerEntryType.jsonName) &&
jv[jss::result][jss::node]
[sfLedgerEntryType.jsonName] == expectedType,
to_string(jv));
}
else
{
BEAST_EXPECTS(
jv.isObject() && jv.isMember(jss::result) &&
jv[jss::result].isMember(jss::error) &&
!jv[jss::result].isMember(jss::node) &&
jv[jss::result][jss::error] ==
expectedError.value_or("entryNotFound"),
to_string(jv));
}
};
/** Runs a series of tests for a given fixed-position ledger
* entry.
*
* @param field: The Json request field to use.
* @param expectedType: The type that the ledger object should
* have if "good".
* @param expectedKey: The keylet of the fixed object.
* @param good: Indicates whether the object is expected to
* exist.
*/
auto test = [&](Json::StaticString const& field,
Json::StaticString const& expectedType,
Keylet const& expectedKey,
bool good) {
testcase << expectedType.c_str() << (good ? "" : " not")
<< " found";
auto const hexKey = strHex(expectedKey.key);
{
// Test bad values
// "field":null
Json::Value params;
params[jss::ledger_index] = jss::validated;
params[field] = Json::nullValue;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, expectedType, "malformedRequest");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// "field":"string"
params[jss::ledger_index] = jss::validated;
params[field] = "arbitrary string";
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, expectedType, "malformedRequest");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// "field":false
params[jss::ledger_index] = jss::validated;
params[field] = false;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, expectedType, "invalidParams");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// "field":[incorrect index hash]
auto const badKey = strHex(expectedKey.key + uint256{1});
params[jss::ledger_index] = jss::validated;
params[field] = badKey;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, expectedType, "entryNotFound");
BEAST_EXPECTS(
jv[jss::result][jss::index] == badKey, to_string(jv));
}
{
Json::Value params;
// "index":"field" using API 2
params[jss::ledger_index] = jss::validated;
params[jss::index] = field;
params[jss::api_version] = 2;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, expectedType, "malformedRequest");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
std::string const pdIdx = [&]() {
{
Json::Value params;
// Test good values
// Use the "field":true notation
params[jss::ledger_index] = jss::validated;
params[field] = true;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
// Index will always be returned for valid parameters.
std::string const pdIdx =
jv[jss::result][jss::index].asString();
BEAST_EXPECTS(hexKey == pdIdx, to_string(jv));
checkResult(good, jv, expectedType);
return pdIdx;
}
}();
{
Json::Value params;
// "field":"[index hash]"
params[jss::ledger_index] = jss::validated;
params[field] = hexKey;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(good, jv, expectedType);
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
}
{
// Bad value
// Use the "index":"field" notation with API 2
Json::Value params;
params[jss::ledger_index] = jss::validated;
params[jss::index] = field;
params[jss::api_version] = 2;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, expectedType, "malformedRequest");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// Use the "index":"field" notation with API 3
params[jss::ledger_index] = jss::validated;
params[jss::index] = field;
params[jss::api_version] = 3;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
// Index is correct either way
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
checkResult(good, jv, expectedType);
}
{
Json::Value params;
// Use the "index":"[index hash]" notation
params[jss::ledger_index] = jss::validated;
params[jss::index] = pdIdx;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
// Index is correct either way
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
checkResult(good, jv, expectedType);
}
};
test(jss::amendments, jss::Amendments, keylet::amendments(), true);
test(jss::fee, jss::FeeSettings, keylet::fees(), true);
// There won't be an nunl
test(jss::nunl, jss::NegativeUNL, keylet::negativeUNL(), false);
// Can only get the short skip list this way
test(jss::hashes, jss::LedgerHashes, keylet::skip(), true);
}
void
testHashes()
{
using namespace test::jtx;
Account const alice{"alice"};
Account const bob{"bob"};
Env env{*this, envconfig([](auto cfg) {
cfg->START_UP = Config::FRESH;
return cfg;
})};
env.close();
/** Verifies that the RPC result has the expected data
*
* @param good: Indicates that the request should have succeeded
* and returned a ledger object of `expectedType` type.
* @param jv: The RPC result Json value
* @param expectedCount: The number of Hashes expected in the
* object if "good".
* @param expectedError: Optional. The expected error if not
* good. Defaults to "entryNotFound".
*/
auto checkResult =
[&](bool good,
Json::Value const& jv,
int expectedCount,
std::optional<std::string> const& expectedError = {}) {
if (good)
{
BEAST_EXPECTS(
jv.isObject() && jv.isMember(jss::result) &&
!jv[jss::result].isMember(jss::error) &&
jv[jss::result].isMember(jss::node) &&
jv[jss::result][jss::node].isMember(
sfLedgerEntryType.jsonName) &&
jv[jss::result][jss::node]
[sfLedgerEntryType.jsonName] == jss::LedgerHashes,
to_string(jv));
BEAST_EXPECTS(
jv[jss::result].isMember(jss::node) &&
jv[jss::result][jss::node].isMember("Hashes") &&
jv[jss::result][jss::node]["Hashes"].size() ==
expectedCount,
to_string(jv[jss::result][jss::node]["Hashes"].size()));
}
else
{
BEAST_EXPECTS(
jv.isObject() && jv.isMember(jss::result) &&
jv[jss::result].isMember(jss::error) &&
!jv[jss::result].isMember(jss::node) &&
jv[jss::result][jss::error] ==
expectedError.value_or("entryNotFound"),
to_string(jv));
}
};
/** Runs a series of tests for a given ledger index.
*
* @param ledger: The ledger index value of the "hashes" request
* parameter. May not necessarily be a number.
* @param expectedKey: The expected keylet of the object.
* @param good: Indicates whether the object is expected to
* exist.
* @param expectedCount: The number of Hashes expected in the
* object if "good".
*/
auto test = [&](Json::Value ledger,
Keylet const& expectedKey,
bool good,
int expectedCount = 0) {
testcase << "LedgerHashes: seq: " << env.current()->header().seq
<< " \"hashes\":" << to_string(ledger)
<< (good ? "" : " not") << " found";
auto const hexKey = strHex(expectedKey.key);
{
// Test bad values
// "hashes":null
Json::Value params;
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = Json::nullValue;
auto jv = env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, 0, "malformedRequest");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// "hashes":"non-uint string"
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = "arbitrary string";
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, 0, "malformedRequest");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// "hashes":"uint string" is invalid, too
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = "10";
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, 0, "malformedRequest");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// "hashes":false
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = false;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, 0, "invalidParams");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
{
Json::Value params;
// "hashes":-1
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = -1;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, 0, "internal");
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
}
// "hashes":[incorrect index hash]
{
Json::Value params;
auto const badKey = strHex(expectedKey.key + uint256{1});
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = badKey;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(false, jv, 0, "entryNotFound");
BEAST_EXPECT(jv[jss::result][jss::index] == badKey);
}
{
Json::Value params;
// Test good values
// Use the "hashes":ledger notation
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = ledger;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(good, jv, expectedCount);
// Index will always be returned for valid parameters.
std::string const pdIdx =
jv[jss::result][jss::index].asString();
BEAST_EXPECTS(hexKey == pdIdx, strHex(pdIdx));
}
{
Json::Value params;
// "hashes":"[index hash]"
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = hexKey;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(good, jv, expectedCount);
// Index is correct either way
BEAST_EXPECTS(
hexKey == jv[jss::result][jss::index].asString(),
strHex(jv[jss::result][jss::index].asString()));
}
{
Json::Value params;
// Use the "index":"[index hash]" notation
params[jss::ledger_index] = jss::validated;
params[jss::index] = hexKey;
auto const jv =
env.rpc("json", "ledger_entry", to_string(params));
checkResult(good, jv, expectedCount);
// Index is correct either way
BEAST_EXPECTS(
hexKey == jv[jss::result][jss::index].asString(),
strHex(jv[jss::result][jss::index].asString()));
}
};
// short skip list
test(true, keylet::skip(), true, 2);
// long skip list at index 0
test(1, keylet::skip(1), false);
// long skip list at index 1
test(1 << 17, keylet::skip(1 << 17), false);
// Close more ledgers, but stop short of the flag ledger
for (auto i = env.current()->seq(); i <= 250; ++i)
env.close();
// short skip list
test(true, keylet::skip(), true, 249);
// long skip list at index 0
test(1, keylet::skip(1), false);
// long skip list at index 1
test(1 << 17, keylet::skip(1 << 17), false);
// Close a flag ledger so the first "long" skip list is created
for (auto i = env.current()->seq(); i <= 260; ++i)
env.close();
// short skip list
test(true, keylet::skip(), true, 256);
// long skip list at index 0
test(1, keylet::skip(1), true, 1);
// long skip list at index 1
test(1 << 17, keylet::skip(1 << 17), false);
}
void
testCLI()
{
@@ -2400,6 +2859,8 @@ public:
testOracleLedgerEntry();
testMPT();
testPermissionedDomain();
testFixed();
testHashes();
testCLI();
}
};

View File

@@ -176,7 +176,8 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
{
if (!vaultSle)
return Number::minExponent - 1; // LCOV_EXCL_LINE
return vaultSle->at(sfAssetsTotal).scale<STAmount>(vaultSle->at(sfAsset));
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}
.exponent();
}
TER

View File

@@ -22,12 +22,7 @@
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/nftPageMask.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <optional>
namespace xrpl {
@@ -3201,29 +3196,16 @@ ValidVault::finalize(
"xrpl::ValidVault::finalize : deposit updated a vault");
auto const& beforeVault = beforeVault_[0];
auto const maybeVaultDeltaAssets =
deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault balance";
return false; // That's all we can do
}
// Get the coarsest scale to round calculations to
auto const minScale = computeMinScale(
vaultAsset,
{
*maybeVaultDeltaAssets,
afterVault.assetsTotal - beforeVault.assetsTotal,
afterVault.assetsAvailable -
beforeVault.assetsAvailable,
});
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, *maybeVaultDeltaAssets, minScale);
if (vaultDeltaAssets > tx[sfAmount])
if (*vaultDeltaAssets > tx[sfAmount])
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must not change vault "
@@ -3231,7 +3213,7 @@ ValidVault::finalize(
result = false;
}
if (vaultDeltaAssets <= zero)
if (*vaultDeltaAssets <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance";
@@ -3248,24 +3230,16 @@ ValidVault::finalize(
if (!issuerDeposit)
{
auto const maybeAccDeltaAssets = deltaAssetsTxAccount();
if (!maybeAccDeltaAssets)
auto const accountDeltaAssets = deltaAssetsTxAccount();
if (!accountDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor "
"balance";
return false;
}
auto const localMinScale = std::min(
minScale,
computeMinScale(vaultAsset, {*maybeAccDeltaAssets}));
auto const accountDeltaAssets = roundToAsset(
vaultAsset, *maybeAccDeltaAssets, localMinScale);
auto const localVaultDeltaAssets = roundToAsset(
vaultAsset, vaultDeltaAssets, localMinScale);
if (accountDeltaAssets >= zero)
if (*accountDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must decrease depositor "
@@ -3273,7 +3247,7 @@ ValidVault::finalize(
result = false;
}
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and "
@@ -3291,17 +3265,16 @@ ValidVault::finalize(
result = false;
}
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
if (!maybeAccDeltaShares)
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor "
"shares";
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const accountDeltaShares = *maybeAccDeltaShares;
if (accountDeltaShares <= zero)
if (*accountDeltaShares <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase depositor "
@@ -3309,18 +3282,15 @@ ValidVault::finalize(
result = false;
}
auto const maybeVaultDeltaShares =
deltaShares(afterVault.pseudoId);
if (!maybeVaultDeltaShares || *maybeVaultDeltaShares == zero)
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault shares";
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = *maybeVaultDeltaShares;
if (vaultDeltaShares * -1 != accountDeltaShares)
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor and "
@@ -3328,22 +3298,15 @@ ValidVault::finalize(
result = false;
}
auto const assetTotalDelta = roundToAsset(
vaultAsset,
afterVault.assetsTotal - beforeVault.assetsTotal,
minScale);
if (assetTotalDelta != vaultDeltaAssets)
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"outstanding must add up";
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset,
afterVault.assetsAvailable - beforeVault.assetsAvailable,
minScale);
if (assetAvailableDelta != vaultDeltaAssets)
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"available must add up";
@@ -3361,32 +3324,22 @@ ValidVault::finalize(
"vault");
auto const& beforeVault = beforeVault_[0];
auto const maybeVaultDeltaAssets =
deltaAssets(afterVault.pseudoId);
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
if (!vaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"change vault balance";
return false; // That's all we can do
}
// Get the most coarse scale to round calculations to
auto const minScale = computeMinScale(
vaultAsset,
{*maybeVaultDeltaAssets,
afterVault.assetsTotal - beforeVault.assetsTotal,
afterVault.assetsAvailable - beforeVault.assetsAvailable});
auto const vaultPseudoDeltaAssets =
roundToAsset(vaultAsset, *maybeVaultDeltaAssets, minScale);
if (vaultPseudoDeltaAssets >= zero)
if (*vaultDeltaAssets >= zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"decrease vault balance";
result = false;
}
// Any payments (including withdrawal) going to the issuer
// do not change their balance, but destroy funds instead.
bool const issuerWithdrawal = [&]() -> bool {
@@ -3399,8 +3352,8 @@ ValidVault::finalize(
if (!issuerWithdrawal)
{
auto const maybeAccDelta = deltaAssetsTxAccount();
auto const maybeOtherAccDelta =
auto const accountDeltaAssets = deltaAssetsTxAccount();
auto const otherAccountDelta =
[&]() -> std::optional<Number> {
if (auto const destination = tx[~sfDestination];
destination && *destination != tx[sfAccount])
@@ -3408,8 +3361,8 @@ ValidVault::finalize(
return std::nullopt;
}();
if (maybeAccDelta.has_value() ==
maybeOtherAccDelta.has_value())
if (accountDeltaAssets.has_value() ==
otherAccountDelta.has_value())
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change one "
@@ -3418,18 +3371,10 @@ ValidVault::finalize(
}
auto const destinationDelta = //
maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
accountDeltaAssets ? *accountDeltaAssets
: *otherAccountDelta;
// the scale of destinationDelta can be coarser than
// minScale, so we take that into account when rounding
auto const localMinScale = std::min(
minScale,
computeMinScale(vaultAsset, {destinationDelta}));
auto const roundedDestinationDelta = roundToAsset(
vaultAsset, destinationDelta, localMinScale);
if (roundedDestinationDelta <= zero)
if (destinationDelta <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must increase "
@@ -3437,9 +3382,7 @@ ValidVault::finalize(
result = false;
}
auto const localPseudoDeltaAssets = roundToAsset(
vaultAsset, vaultPseudoDeltaAssets, localMinScale);
if (localPseudoDeltaAssets * -1 != roundedDestinationDelta)
if (*vaultDeltaAssets * -1 != destinationDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault "
@@ -3447,7 +3390,7 @@ ValidVault::finalize(
result = false;
}
}
// We don't need to round shares, they are integral MPT
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
@@ -3464,7 +3407,7 @@ ValidVault::finalize(
"shares";
result = false;
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
@@ -3481,24 +3424,17 @@ ValidVault::finalize(
result = false;
}
auto const assetTotalDelta = roundToAsset(
vaultAsset,
afterVault.assetsTotal - beforeVault.assetsTotal,
minScale);
// Note, vaultBalance is negative (see check above)
if (assetTotalDelta != vaultPseudoDeltaAssets)
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets outstanding must add up";
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset,
afterVault.assetsAvailable - beforeVault.assetsAvailable,
minScale);
if (assetAvailableDelta != vaultPseudoDeltaAssets)
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets available must add up";
@@ -3532,19 +3468,10 @@ ValidVault::finalize(
}
}
auto const maybeVaultDeltaAssets =
deltaAssets(afterVault.pseudoId);
if (maybeVaultDeltaAssets)
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (vaultDeltaAssets)
{
auto const minScale = computeMinScale(
vaultAsset,
{*maybeVaultDeltaAssets,
afterVault.assetsTotal - beforeVault.assetsTotal,
afterVault.assetsAvailable -
beforeVault.assetsAvailable});
auto const vaultDeltaAssets = roundToAsset(
vaultAsset, *maybeVaultDeltaAssets, minScale);
if (vaultDeltaAssets >= zero)
if (*vaultDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease vault "
@@ -3552,11 +3479,8 @@ ValidVault::finalize(
result = false;
}
auto const assetsTotalDelta = roundToAsset(
vaultAsset,
afterVault.assetsTotal - beforeVault.assetsTotal,
minScale);
if (assetsTotalDelta != vaultDeltaAssets)
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
afterVault.assetsTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding "
@@ -3564,12 +3488,8 @@ ValidVault::finalize(
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset,
afterVault.assetsAvailable -
beforeVault.assetsAvailable,
minScale);
if (assetAvailableDelta != vaultDeltaAssets)
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available "
@@ -3584,15 +3504,15 @@ ValidVault::finalize(
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
if (!maybeAccountDeltaShares)
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder shares";
return false; // That's all we can do
}
if (*maybeAccountDeltaShares >= zero)
if (*accountDeltaShares >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease holder "
@@ -3600,7 +3520,6 @@ ValidVault::finalize(
result = false;
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
@@ -3609,7 +3528,7 @@ ValidVault::finalize(
return false; // That's all we can do
}
if (*vaultDeltaShares * -1 != *maybeAccountDeltaShares)
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder and "
@@ -3647,32 +3566,4 @@ ValidVault::finalize(
return true;
}
[[nodiscard]] std::int32_t
ValidVault::computeMinScale(
Asset const& asset,
std::initializer_list<Number const> numbers)
{
if (numbers.size() == 0)
return 0;
// Helper to get the minimal scale of an STAmount by removing trailing
// zeros from its mantissa
auto const getNatScale = [](Asset const& asset,
Number const& value) -> std::int32_t {
if (asset.integral())
return 0;
if (value == beast::zero)
return value.exponent();
return value.scale<STAmount>(asset);
};
std::vector<std::int32_t> natScales;
std::transform(
numbers.begin(),
numbers.end(),
std::back_inserter(natScales),
[&](Number const& n) { return getNatScale(asset, n); });
return *std::max_element(natScales.begin(), natScales.end());
}
} // namespace xrpl

View File

@@ -900,12 +900,6 @@ public:
XRPAmount const,
ReadView const&,
beast::Journal const&);
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
computeMinScale(
Asset const& asset,
std::initializer_list<Number const> numbers);
};
// additional invariant checks can be declared above and then added to this

View File

@@ -125,7 +125,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
tenthBipsOfValue(
currentDebtTotal,
TenthBips32(sleBroker->at(sfCoverRateMinimum))),
currentDebtTotal.scale<STAmount>(vaultAsset));
currentDebtTotal.exponent());
}();
if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS;

View File

@@ -18,6 +18,32 @@
namespace xrpl {
using FunctionType = std::function<Expected<uint256, Json::Value>(
Json::Value const&,
Json::StaticString const,
unsigned const apiVersion)>;
static Expected<uint256, Json::Value>
parseFixed(
Keylet const& keylet,
Json::Value const& params,
Json::StaticString const& fieldName,
unsigned const apiVersion);
// Helper function to return FunctionType for objects that have a fixed
// location. That is, they don't take parameters to compute the index.
// e.g. amendments, fees, negative UNL, etc.
static FunctionType
fixed(Keylet const& keylet)
{
return [keylet](
Json::Value const& params,
Json::StaticString const fieldName,
unsigned const apiVersion) -> Expected<uint256, Json::Value> {
return parseFixed(keylet, params, fieldName, apiVersion);
};
}
static Expected<uint256, Json::Value>
parseObjectID(
Json::Value const& params,
@@ -33,13 +59,33 @@ parseObjectID(
}
static Expected<uint256, Json::Value>
parseIndex(Json::Value const& params, Json::StaticString const fieldName)
parseIndex(
Json::Value const& params,
Json::StaticString const fieldName,
unsigned const apiVersion)
{
if (apiVersion > 2u && params.isString())
{
std::string const index = params.asString();
if (index == jss::amendments.c_str())
return keylet::amendments().key;
if (index == jss::fee.c_str())
return keylet::fees().key;
if (index == jss::nunl)
return keylet::negativeUNL().key;
if (index == jss::hashes)
// Note this only finds the "short" skip list. Use "hashes":index to
// get the long list.
return keylet::skip().key;
}
return parseObjectID(params, fieldName, "hex string");
}
static Expected<uint256, Json::Value>
parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName)
parseAccountRoot(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (auto const account = LedgerEntryHelpers::parse<AccountID>(params))
{
@@ -50,14 +96,13 @@ parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName)
"malformedAddress", fieldName, "AccountID");
}
static Expected<uint256, Json::Value>
parseAmendments(Json::Value const& params, Json::StaticString const fieldName)
{
return parseObjectID(params, fieldName, "hex string");
}
auto const parseAmendments = fixed(keylet::amendments());
static Expected<uint256, Json::Value>
parseAMM(Json::Value const& params, Json::StaticString const fieldName)
parseAMM(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -85,7 +130,10 @@ parseAMM(Json::Value const& params, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseBridge(Json::Value const& params, Json::StaticString const fieldName)
parseBridge(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isMember(jss::bridge))
{
@@ -116,13 +164,19 @@ parseBridge(Json::Value const& params, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseCheck(Json::Value const& params, Json::StaticString const fieldName)
parseCheck(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
}
static Expected<uint256, Json::Value>
parseCredential(Json::Value const& cred, Json::StaticString const fieldName)
parseCredential(
Json::Value const& cred,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!cred.isObject())
{
@@ -153,7 +207,10 @@ parseCredential(Json::Value const& cred, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseDelegate(Json::Value const& params, Json::StaticString const fieldName)
parseDelegate(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -244,7 +301,10 @@ parseAuthorizeCredentials(Json::Value const& jv)
}
static Expected<uint256, Json::Value>
parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName)
parseDepositPreauth(
Json::Value const& dp,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!dp.isObject())
{
@@ -297,7 +357,10 @@ parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseDID(Json::Value const& params, Json::StaticString const fieldName)
parseDID(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
auto const account = LedgerEntryHelpers::parse<AccountID>(params);
if (!account)
@@ -312,7 +375,8 @@ parseDID(Json::Value const& params, Json::StaticString const fieldName)
static Expected<uint256, Json::Value>
parseDirectoryNode(
Json::Value const& params,
Json::StaticString const fieldName)
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -365,7 +429,10 @@ parseDirectoryNode(
}
static Expected<uint256, Json::Value>
parseEscrow(Json::Value const& params, Json::StaticString const fieldName)
parseEscrow(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -384,20 +451,53 @@ parseEscrow(Json::Value const& params, Json::StaticString const fieldName)
return keylet::escrow(*id, *seq).key;
}
auto const parseFeeSettings = fixed(keylet::fees());
static Expected<uint256, Json::Value>
parseFeeSettings(Json::Value const& params, Json::StaticString const fieldName)
parseFixed(
Keylet const& keylet,
Json::Value const& params,
Json::StaticString const& fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isBool())
{
return parseObjectID(params, fieldName, "hex string");
}
if (!params.asBool())
{
return LedgerEntryHelpers::invalidFieldError(
"invalidParams", fieldName, "true");
}
return keylet.key;
}
static Expected<uint256, Json::Value>
parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName)
parseLedgerHashes(
Json::Value const& params,
Json::StaticString const fieldName,
unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (params.isUInt() || params.isInt())
{
// If the index doesn't parse as a UInt, throw
auto const index = params.asUInt();
// Return the "long" skip list for the given ledger index.
auto const keylet = keylet::skip(index);
return keylet.key;
}
// Return the key in `params` or the "short" skip list, which contains
// hashes since the last flag ledger.
return parseFixed(keylet::skip(), params, fieldName, apiVersion);
}
static Expected<uint256, Json::Value>
parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName)
parseLoanBroker(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -417,7 +517,10 @@ parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseLoan(Json::Value const& params, Json::StaticString const fieldName)
parseLoan(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -437,7 +540,10 @@ parseLoan(Json::Value const& params, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseMPToken(Json::Value const& params, Json::StaticString const fieldName)
parseMPToken(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -460,7 +566,8 @@ parseMPToken(Json::Value const& params, Json::StaticString const fieldName)
static Expected<uint256, Json::Value>
parseMPTokenIssuance(
Json::Value const& params,
Json::StaticString const fieldName)
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
auto const mptIssuanceID = LedgerEntryHelpers::parse<uint192>(params);
if (!mptIssuanceID)
@@ -471,25 +578,30 @@ parseMPTokenIssuance(
}
static Expected<uint256, Json::Value>
parseNFTokenOffer(Json::Value const& params, Json::StaticString const fieldName)
parseNFTokenOffer(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
}
static Expected<uint256, Json::Value>
parseNFTokenPage(Json::Value const& params, Json::StaticString const fieldName)
parseNFTokenPage(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
}
static Expected<uint256, Json::Value>
parseNegativeUNL(Json::Value const& params, Json::StaticString const fieldName)
{
return parseObjectID(params, fieldName, "hex string");
}
auto const parseNegativeUNL = fixed(keylet::negativeUNL());
static Expected<uint256, Json::Value>
parseOffer(Json::Value const& params, Json::StaticString const fieldName)
parseOffer(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -510,7 +622,10 @@ parseOffer(Json::Value const& params, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseOracle(Json::Value const& params, Json::StaticString const fieldName)
parseOracle(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -531,7 +646,10 @@ parseOracle(Json::Value const& params, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parsePayChannel(Json::Value const& params, Json::StaticString const fieldName)
parsePayChannel(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
}
@@ -539,7 +657,8 @@ parsePayChannel(Json::Value const& params, Json::StaticString const fieldName)
static Expected<uint256, Json::Value>
parsePermissionedDomain(
Json::Value const& pd,
Json::StaticString const fieldName)
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (pd.isString())
{
@@ -568,7 +687,8 @@ parsePermissionedDomain(
static Expected<uint256, Json::Value>
parseRippleState(
Json::Value const& jvRippleState,
Json::StaticString const fieldName)
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
Currency uCurrency;
@@ -618,13 +738,19 @@ parseRippleState(
}
static Expected<uint256, Json::Value>
parseSignerList(Json::Value const& params, Json::StaticString const fieldName)
parseSignerList(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
}
static Expected<uint256, Json::Value>
parseTicket(Json::Value const& params, Json::StaticString const fieldName)
parseTicket(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -645,7 +771,10 @@ parseTicket(Json::Value const& params, Json::StaticString const fieldName)
}
static Expected<uint256, Json::Value>
parseVault(Json::Value const& params, Json::StaticString const fieldName)
parseVault(
Json::Value const& params,
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
@@ -668,7 +797,8 @@ parseVault(Json::Value const& params, Json::StaticString const fieldName)
static Expected<uint256, Json::Value>
parseXChainOwnedClaimID(
Json::Value const& claim_id,
Json::StaticString const fieldName)
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!claim_id.isObject())
{
@@ -693,7 +823,8 @@ parseXChainOwnedClaimID(
static Expected<uint256, Json::Value>
parseXChainOwnedCreateAccountClaimID(
Json::Value const& claim_id,
Json::StaticString const fieldName)
Json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!claim_id.isObject())
{
@@ -717,10 +848,6 @@ parseXChainOwnedCreateAccountClaimID(
return keylet.key;
}
using FunctionType = Expected<uint256, Json::Value> (*)(
Json::Value const&,
Json::StaticString const);
struct LedgerEntry
{
Json::StaticString fieldName;
@@ -753,7 +880,7 @@ doLedgerEntry(RPC::JsonContext& context)
{jss::ripple_state, parseRippleState, ltRIPPLE_STATE},
});
auto hasMoreThanOneMember = [&]() {
auto const hasMoreThanOneMember = [&]() {
int count = 0;
for (auto const& ledgerEntry : ledgerEntryParsers)
@@ -797,8 +924,8 @@ doLedgerEntry(RPC::JsonContext& context)
Json::Value const& params = ledgerEntry.fieldName == jss::bridge
? context.params
: context.params[ledgerEntry.fieldName];
auto const result =
ledgerEntry.parseFunction(params, ledgerEntry.fieldName);
auto const result = ledgerEntry.parseFunction(
params, ledgerEntry.fieldName, context.apiVersion);
if (!result)
return result.error();
@@ -829,9 +956,13 @@ doLedgerEntry(RPC::JsonContext& context)
throw;
}
// Return the computed index regardless of whether the node exists.
jvResult[jss::index] = to_string(uNodeIndex);
if (uNodeIndex.isZero())
{
return RPC::make_error(rpcENTRY_NOT_FOUND);
RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult);
return jvResult;
}
auto const sleNode = lpLedger->read(keylet::unchecked(uNodeIndex));
@@ -843,12 +974,14 @@ doLedgerEntry(RPC::JsonContext& context)
if (!sleNode)
{
// Not found.
return RPC::make_error(rpcENTRY_NOT_FOUND);
RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult);
return jvResult;
}
if ((expectedType != ltANY) && (expectedType != sleNode->getType()))
{
return RPC::make_error(rpcUNEXPECTED_LEDGER_TYPE);
RPC::inject_error(rpcUNEXPECTED_LEDGER_TYPE, jvResult);
return jvResult;
}
if (bNodeBinary)
@@ -858,12 +991,10 @@ doLedgerEntry(RPC::JsonContext& context)
sleNode->add(s);
jvResult[jss::node_binary] = strHex(s.peekData());
jvResult[jss::index] = to_string(uNodeIndex);
}
else
{
jvResult[jss::node] = sleNode->getJson(JsonOptions::none);
jvResult[jss::index] = to_string(uNodeIndex);
}
return jvResult;