Compare commits

..

31 Commits

Author SHA1 Message Date
Mayukha Vadari
84c47bb55e fix clang-tidy issues 2026-07-22 18:54:59 -04:00
Mayukha Vadari
5c64d9f7c0 Merge branch 'develop' into mvadari/error-message-ter 2026-07-22 18:16:54 -04:00
Mayukha Vadari
b765e78e94 add test 2026-07-22 18:14:55 -04:00
Mayukha Vadari
2c69d58f26 export error reason via submit/simulate 2026-07-22 18:09:19 -04:00
Mayukha Vadari
445574089f migrate preflight errors 2026-07-22 17:57:55 -04:00
Mayukha Vadari
b5bd12dec0 migrate preflights 2026-07-22 17:45:10 -04:00
Mayukha Vadari
2da94043f2 migrate transactors 2026-07-22 17:07:57 -04:00
Mayukha Vadari
0621841912 make TER changes 2026-07-22 17:07:18 -04:00
Mayukha Vadari
12ed506565 chore: Clean up grammar in PR template (#7846) 2026-07-22 18:50:10 +00:00
Ayaz Salikhov
3122de86bf build: Create versioned compiler/tooling symlinks in nix environments (#7844) 2026-07-22 17:04:02 +00:00
Sergey Kuznetsov
7edf39e622 ci: Use rust-overlay to bring Rust into nix (#7837) 2026-07-22 14:00:17 +00:00
dependabot[bot]
ba01b05f33 ci: [DEPENDABOT] bump actions/setup-python from 6.3.0 to 7.0.0 (#7830)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 16:04:45 +00:00
Sergey Kuznetsov
c414e1877d ci: Update pre-commit image (#7838) 2026-07-21 15:37:47 +00:00
Sergey Kuznetsov
f56b79f00d ci: Add cargo to pre-commit image (#7835) 2026-07-21 13:39:37 +00:00
Ayaz Salikhov
4918f3bb20 chore: Add doxygen in pre-commit image (#7836) 2026-07-21 13:38:26 +00:00
Mayukha Vadari
ee0a3dfad7 ci: Improve test debuggability (#7619)
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-07-21 13:14:40 +00:00
Bart
042c9660cd chore: Add CODEOWNERS for CI-related changes (#7832)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-07-20 21:06:24 +00:00
Ayaz Salikhov
60b49f3fe0 ci: Use in-house image for pre-commit (#7610) 2026-07-20 19:11:09 +00:00
Ayaz Salikhov
4c869ee16c ci: Build separate pre-commit docker image (#7831) 2026-07-20 17:40:38 +00:00
Ayaz Salikhov
b2f45813e0 chore: Workaround boost compiler resolution inside Nix environment (#7826) 2026-07-20 14:04:08 +00:00
Vito Tumas
5ce0b1c2c7 refactor: Restructure LendingHelpers to improve readability (#7807) 2026-07-16 15:47:56 +00:00
Andrzej Budzanowski
701311f27e test: Add google benchmark dependency and migrate nodestore timing test as a benchmark (#7317)
Co-authored-by: Marek Foss <marek.foss@neti-soft.com>
Co-authored-by: Alex Kremer <akremer@ripple.com>
2026-07-16 15:44:43 +00:00
Sergey Kuznetsov
b1a670c46e ci: Add Rust to CI (#7808)
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-07-16 15:28:04 +00:00
Bart
18e311e1e2 chore: Bump version to 3.3.0-rc1 (#7806)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-07-16 13:54:12 +00:00
Denis Angell
69b70d7a0d fix: Refactor Batch Transaction IDs (#7736)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
2026-07-16 13:15:59 +00:00
Mayukha Vadari
b42cde3e85 refactor: Remove redundant enable checks in ConfidentialMPT txs (#7809) 2026-07-16 09:51:19 +00:00
Peter Chen
cd38c0e800 chore: Update mpt-crypto-lib to 0.4.0-rc4 (#7813) 2026-07-15 23:47:22 +00:00
Vlad
433e5f6896 fix: Reject zero CheckID in CheckCancel and CheckCash (#7685) 2026-07-15 22:08:45 +00:00
Ayaz Salikhov
781ab723af ci: Fix workflow launch on matrix-unrelated labels (#7812) 2026-07-15 18:24:31 +00:00
Ed Hennis
a24e543af3 fix: Allocate TaggedCache::getKeys() memory outside of lock (#7567)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-07-15 13:30:20 +00:00
Sophia Xie
a0fd1cce54 fix: Re-store nodes missing from both backends during online_delete rotation (#7763)
Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com>
2026-07-14 23:42:40 +00:00
151 changed files with 3121 additions and 2735 deletions

View File

@@ -216,6 +216,7 @@ words:
- Nyffenegger
- onlatest
- ostr
- oxalica
- pargs
- partitioner
- paychan
@@ -262,6 +263,8 @@ words:
- Rohrs
- roundings
- rustc
- rustfmt
- rustup
- sahyadri
- Satoshi
- scons
@@ -305,6 +308,7 @@ words:
- takerpays
- ters
- TMEndpointv2
- toolchain
- tparam
- trixie
- tx
@@ -337,6 +341,7 @@ words:
- unsquelch
- unsquelched
- unsquelching
- unsuffixed
- unvalidated
- unveto
- unvetoed

View File

@@ -11,6 +11,9 @@ endfunction()
function(create_symbolic_link target link)
endfunction()
function(xrpl_add_benchmark name)
endfunction()
macro(exclude_from_default target_)
endmacro()

View File

@@ -1,10 +1,10 @@
<!--
This PR template helps you to write a good pull request description.
This PR template helps you write a good pull request description.
Please feel free to include additional useful information even beyond what is requested below.
If your branch is on a personal fork and has a name that allows it to
run CI build/test jobs (e.g. "ci/foo"), remember to rename it BEFORE
opening the PR. This avoids unnecessary redundant test runs. Renaming
opening the PR. This avoids redundant test runs. Renaming
the branch after opening the PR will close the PR.
https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/renaming-a-branch
-->
@@ -15,7 +15,7 @@ https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-
Please include a summary of the changes.
This may be a direct input to the release notes.
If too broad, please consider splitting into multiple PRs.
If a relevant task or issue, please link it here.
If there is a relevant task or issue, please link it here.
-->
### Context of Change
@@ -65,5 +65,5 @@ This section may not be needed if your change includes thoroughly commented unit
<!--
## Future Tasks
For future tasks related to PR.
For future tasks related to this PR.
-->

View File

@@ -1,3 +1,6 @@
benchmarks.libxrpl > xrpl.basics
benchmarks.libxrpl > xrpl.config
benchmarks.libxrpl > xrpl.nodestore
libxrpl.basics > xrpl.basics
libxrpl.conditions > xrpl.basics
libxrpl.conditions > xrpl.conditions

View File

@@ -1,5 +1,5 @@
{
"image_tag": "sha-e29b523",
"image_tag": "sha-2e25435",
"configs": {
"ubuntu": [
{

View File

@@ -1,4 +1,4 @@
name: Build Nix Docker images
name: Build `nix` Docker images
on:
push:
@@ -8,6 +8,7 @@ on:
- ".github/workflows/build-nix-images.yml"
- "flake.nix"
- "flake.lock"
- "rust-toolchain.toml"
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
@@ -18,6 +19,7 @@ on:
- ".github/workflows/build-nix-images.yml"
- "flake.nix"
- "flake.lock"
- "rust-toolchain.toml"
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
@@ -36,7 +38,7 @@ defaults:
jobs:
build-merge:
name: Build and push nix-${{ matrix.distro.name }}
name: Build and push `nix-${{ matrix.distro.name }}` image
permissions:
contents: read
packages: write

View File

@@ -1,4 +1,4 @@
name: Build packaging Docker images
name: Build `packaging` Docker images
on:
push:
@@ -26,7 +26,7 @@ defaults:
jobs:
build-merge:
name: Build and push packaging-${{ matrix.distro.name }}
name: Build and push `packaging-${{ matrix.distro.name }}` image
permissions:
contents: read
packages: write

View File

@@ -0,0 +1,38 @@
name: Build `pre-commit` Docker image
on:
push:
branches:
- develop
paths:
- ".github/workflows/build-pre-commit-image.yml"
- "bin/pre-commit/Dockerfile"
- "rust-toolchain.toml"
pull_request:
paths:
- ".github/workflows/build-pre-commit-image.yml"
- "bin/pre-commit/Dockerfile"
- "rust-toolchain.toml"
workflow_dispatch:
concurrency:
# Read `on-trigger.yml` for the rationale behind this concurrency group name.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
build-merge:
name: Build and push `pre-commit` image
permissions:
contents: read
packages: write
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@ee03d31bcc4501d7599dc1b1ecd7a34af582ad1c
with:
image_name: xrpld/pre-commit
dockerfile: bin/pre-commit/Dockerfile
base_image: ubuntu:26.04
push: ${{ github.event_name == 'push' }}

View File

@@ -25,18 +25,11 @@ on:
- unlabeled
concurrency:
# Use a per-ref group so a newer run (a push, or a change to a label below)
# supersedes the in-progress one for that ref. Label events we don't act on get
# their own unique group (per run id) instead, keeping them out of the shared
# group so real builds keep running. Keep this list in sync with `should-run`.
group: >-
${{ github.workflow }}-${{ github.ref }}${{
((github.event.action == 'labeled' || github.event.action == 'unlabeled')
&& github.event.label.name != 'Ready to merge'
&& github.event.label.name != 'DraftRunCI'
&& github.event.label.name != 'Full CI build')
&& format('-{0}', github.run_id) || ''
}}
# A single per-ref group with cancel-in-progress means any newer run (a push
# or a label change) supersedes the in-progress one for that ref. Keeping
# exactly one authoritative run per ref ensures a fast do-nothing run can never
# mask a real build's checks.
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
@@ -44,21 +37,17 @@ defaults:
shell: bash
jobs:
# This job determines whether the rest of the workflow should run. It runs
# when the PR is not a draft (which should also cover merge-group) or has the
# 'DraftRunCI' or 'Full CI build' label. For label events it only runs when the
# label added or removed is one we act on ('Ready to merge', 'DraftRunCI' or
# 'Full CI build'), so unrelated label changes do not trigger a redundant run.
# This job determines whether the rest of the workflow should run at all,
# based on the current set of labels: it runs when the PR is not a draft
# (which should also cover merge-group) or has the 'DraftRunCI' or
# 'Full CI build' label. Whether a build then happens, and whether it is the
# minimal or full matrix, is decided further below and in the strategy matrix.
should-run:
if: >-
${{
((github.event.action != 'labeled' && github.event.action != 'unlabeled')
|| github.event.label.name == 'Ready to merge'
|| github.event.label.name == 'DraftRunCI'
|| github.event.label.name == 'Full CI build')
&& (!github.event.pull_request.draft
|| contains(github.event.pull_request.labels.*.name, 'DraftRunCI')
|| contains(github.event.pull_request.labels.*.name, 'Full CI build'))
!github.event.pull_request.draft
|| contains(github.event.pull_request.labels.*.name, 'DraftRunCI')
|| contains(github.event.pull_request.labels.*.name, 'Full CI build')
}}
runs-on: ubuntu-latest
steps:

View File

@@ -17,4 +17,4 @@ jobs:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@1bde119a1ab71305ba5d3716e7a82cea1c7bdede
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-f56b79f" }'

View File

@@ -41,13 +41,13 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-2e25435
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
with:
enable_ccache: false

View File

@@ -113,7 +113,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
with:
enable_ccache: ${{ inputs.ccache_enabled }}
@@ -223,11 +223,13 @@ jobs:
BUILD_TYPE: ${{ inputs.build_type }}
CMAKE_TARGET: ${{ inputs.cmake_target }}
run: |
set -o pipefail
cmake \
--build . \
--config "${BUILD_TYPE}" \
--parallel "${BUILD_NPROC}" \
--target "${CMAKE_TARGET}"
--target "${CMAKE_TARGET}" \
2>&1 | tee "${GITHUB_WORKSPACE}/build.log"
- name: Show ccache statistics
if: ${{ inputs.ccache_enabled }}
@@ -322,27 +324,46 @@ jobs:
PRELOAD=""
fi
LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee "${GITHUB_WORKSPACE}/unittest.log"
- name: Show test failure summary
if: ${{ failure() && !inputs.build_only }}
env:
WORKING_DIR: ${{ runner.os == 'Windows' && format('{0}\{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
# Smoke-run every benchmark module with a single repetition to confirm the
# benchmarks still build and execute. This is a correctness check, not a
# performance measurement, so it is skipped for instrumented builds
# (sanitizers/coverage/voidstar), where it would be slow and meaningless,
# and on Windows, where the `install` target does not build them.
- name: Run the benchmarks
if: ${{ !inputs.build_only && runner.os != 'Windows' && env.SANITIZERS_ENABLED == 'false' && env.COVERAGE_ENABLED != 'true' && env.VOIDSTAR_ENABLED != 'true' }}
working-directory: ${{ env.BUILD_DIR }}
run: |
if [ ! -d "${WORKING_DIR}" ]; then
echo "Working directory '${WORKING_DIR}' does not exist."
exit 0
fi
rc=0
while IFS= read -r bench; do
echo "::group::${bench}"
"./${bench}" --benchmark_repetitions=1 || rc=1
echo "::endgroup::"
done < <(find src/benchmarks -type f -perm -u+x -name 'xrpl.bench.*')
exit "${rc}"
cd "${WORKING_DIR}"
- name: Show build/test failure summary
if: ${{ failure() }}
run: |
cd "${GITHUB_WORKSPACE}"
if [ ! -f unittest.log ]; then
echo "unittest.log not found; embedded tests may not have run."
exit 0
fi
if ! grep -E "failed" unittest.log; then
echo "Log present but no failure lines found in unittest.log."
if [ -f unittest.log ]; then
if ! grep -E "failed" unittest.log | grep -vE "^I[0-9]|^[0-9]+> (ERR:|FTL:)"; then
echo "unittest.log present but no failure lines found."
fi
elif [ -f build.log ]; then
# GCC/Clang emit "error:" (covers "fatal error:"); MSVC emits
# "error C####:", "error LNK####:", and "fatal error LNK####:".
# -A6 prints the lines that follow each match (source line, caret,
# notes, and the "N errors generated" tally) to capture the whole
# diagnostic block.
if ! grep -E -A6 "error:|error C[0-9]{4}|error LNK[0-9]{4}|fatal error" build.log; then
echo "build.log present but no compile errors found."
fi
else
echo "unittest.log/build.log not found; something went wrong."
exit 1
fi
- name: Debug failure (Linux)
if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }}

View File

@@ -34,7 +34,7 @@ jobs:
needs: [determine-files]
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-e29b523"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-2e25435"
permissions:
contents: read
issues: write
@@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
with:
enable_ccache: false

View File

@@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"

View File

@@ -40,7 +40,7 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-2e25435
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}

View File

@@ -68,7 +68,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
with:
enable_ccache: false

3
.gitignore vendored
View File

@@ -81,6 +81,9 @@ DerivedData
# Python
__pycache__
# Rust build artifacts.
target/
# Direnv's directory
/.direnv

View File

@@ -55,7 +55,7 @@ repos:
types_or: [c++, c]
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: dd18dad857d6133e90bbe478f4f2f22ec0030269 # frozen: v22.1.5
rev: f4d7745e17a28aad7eed2f4874ca8d1568c11c4c # frozen: v22.1.8
hooks:
- id: clang-format
args: [--style=file]
@@ -68,7 +68,7 @@ repos:
- id: gersemi
- repo: https://github.com/rbubley/mirrors-prettier
rev: 39e2973981e6d2f9b6c543b0086a2d2393abdc89 # frozen: v3.9.4
rev: 9337a74165b178ae2c766f60bee7252a0f06f3e8 # frozen: v3.9.5
hooks:
- id: prettier
args: [--end-of-line=auto]

View File

@@ -131,6 +131,10 @@ else()
endif()
target_link_libraries(xrpl_libs INTERFACE ${nudb})
if(benchmark)
find_package(benchmark REQUIRED)
endif()
if(coverage)
include(XrplCov)
endif()
@@ -145,3 +149,7 @@ if(tests)
include(CTest)
add_subdirectory(src/tests/libxrpl)
endif()
if(benchmark)
add_subdirectory(src/benchmarks/libxrpl)
endif()

4
CODEOWNERS Normal file
View File

@@ -0,0 +1,4 @@
# By default, anyone can review changes.
# The CI tooling team should review changes to the CI configuration.
/.github/ @XRPLF/ci-tooling

View File

@@ -83,6 +83,7 @@ If you create new source files, they must be organized as follows:
`src/libxrpl`.
- All other non-test files must go under `src/xrpld`.
- All test source files must go under `src/test`.
- All benchmark source files must go under `src/benchmarks`.
The source must be formatted according to the style guide below. The easiest
way to satisfy this is to install the [`pre-commit`](#pre-commit-hooks) hooks,

55
bin/pre-commit/Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
ARG BASE_IMAGE=ubuntu:26.04
FROM ${BASE_IMAGE}
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
ENTRYPOINT ["/bin/bash"]
ARG DEBIAN_FRONTEND=noninteractive
RUN <<EOF
pkgs=()
pkgs+=(curl) # Required to install nix.
pkgs+=(doxygen) # Needed for Clio's check-doxygen-docs.sh.
pkgs+=(git) # Required for prepare-runner.
pkgs+=(libatomic1) # Required to run pre-commit provided `node`.
pkgs+=(python3) # Python 3 interpreter.
pkgs+=(python3-pip) # Package manager for Python applications.
pkgs+=(xz-utils) # Required to install nix
apt-get update
apt-get install -y --no-install-recommends "${pkgs[@]}"
apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
ARG PRE_COMMIT_VERSION=4.6.0
RUN pip install --no-cache --break-system-packages \
pre-commit==${PRE_COMMIT_VERSION}
RUN sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon --yes
# Add nix to PATH and set NIX environment variables,
# so nix is available in all shells including non-interactive shells (e.g., GitHub Actions).
ENV PATH="/nix/var/nix/profiles/default/bin:${PATH}"
ENV NIX_PROFILES="/nix/var/nix/profiles/default"
ENV NIX_SSL_CERT_FILE="/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt"
# Verify nix installation
RUN nix --version
ENV RUSTUP_HOME="/opt/rust/rustup"
ENV CARGO_HOME="/opt/rust/cargo"
ENV PATH="/opt/rust/cargo/bin:${PATH}"
WORKDIR /tmp
COPY rust-toolchain.toml /tmp/rust-toolchain.toml
RUN <<EOF
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --no-modify-path --profile minimal --default-toolchain none
rustup toolchain install
rustup show
cargo fmt --version
EOF
WORKDIR /

View File

@@ -0,0 +1,36 @@
include(isolate_headers)
# Define a benchmark executable for the module `name`.
#
# This follows the same general pattern as other build helpers in this repo
# (e.g. `add_module`): create a target and isolate headers, but here the target
# is a benchmark executable and no `add_test(...)` is registered.
#
# `isolate_headers` exposes only `${CMAKE_CURRENT_SOURCE_DIR}/${name}` on the
# include path, rooted at `src`, so a benchmark's own headers are reached as
# `<benchmarks/.../${name}/...>` and nothing else in the tree leaks in.
function(xrpl_add_benchmark name)
set(target ${PROJECT_NAME}.bench.${name})
file(
GLOB_RECURSE sources
CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/${name}/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp"
)
add_executable(${target} ${ARGN} ${sources})
# Benchmark sources register cases through Google Benchmark's static
# registrars (anonymous-namespace lambdas). Merging several such files into
# one unity translation unit collides those internal-linkage entities, so
# keep benchmarks out of the unity build - mirroring xrpl.libpb in
# XrplCore.cmake. Each file compiles fine on its own.
set_target_properties(${target} PROPERTIES UNITY_BUILD OFF)
isolate_headers(
${target}
"${CMAKE_SOURCE_DIR}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/${name}"
PRIVATE
)
endfunction()

View File

@@ -30,6 +30,8 @@ if(tests)
endif()
endif()
option(benchmark "Build benchmarks" ON)
# Enabled by default so every header is compiled on its own as the main file of
# its own compile_commands.json entry - this is what lets clang-tidy (and clangd
# and IDEs) analyse a header's own includes directly. The per-header objects are

View File

@@ -10,22 +10,23 @@
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828",
"mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355",
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
"grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616",
"grpc/1.81.1#f729f6d75992d20f9c72828e9142d62f%1783945160.094135",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1782392402.296732",
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1782392419.475605",
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
"benchmark/1.9.5#b885dc73ad67b40a55d45684d1c88ad1%1782736613.864841",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
],
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
@@ -38,7 +39,7 @@
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
],
"python_requires": [],
"overrides": {

View File

@@ -20,6 +20,22 @@ compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }}
{% endif %}
[conf]
{# The Boost recipe builds with b2, which doesn't use Conan's toolchain files. #}
{# Instead it hand-rolls the compiler for user-config.jam, #}
{# and its fallback probes a version-suffixed binary (e.g. `g++-15`) before plain `g++`. #}
{# Inside the Nix shell the wrapper only provides `g++`/`gcc` (no `-15` suffix), #}
{# so on a host that also has a system `g++-15` the probe escapes Nix #}
{# and picks the system compiler, which is mismatched with the Nix libraries #}
{# and breaks the build (e.g. Boost.Stacktrace link checks fail). #}
{# Pinning the executables here short-circuits that probe so Boost (and the rest of the toolchain) #}
{# resolve the same compiler. #}
{# Not part of the package ID, so binaries stay shareable. #}
{% if os != "Windows" %}
{% set cc_exe = {"gcc": "gcc", "clang": "clang", "apple-clang": "clang"}.get(compiler) %}
{% set cxx_exe = {"gcc": "g++", "clang": "clang++", "apple-clang": "clang++"}.get(compiler) %}
tools.build:compiler_executables={'c':'{{ cc_exe }}','cpp':'{{ cxx_exe }}'}
{% endif %}
{# By default, Conan tries to reuse binaries built with different cppstd versions. #}
{# We want to avoid that to improve reproduceability, so we add the cppstd version to the package ID. #}
{# More info: https://docs.conan.io/2/reference/extensions/binary_compatibility.html #}

View File

@@ -15,6 +15,7 @@ class Xrpl(ConanFile):
settings = "os", "compiler", "build_type", "arch"
options = {
"assertions": [True, False],
"benchmark": [True, False],
"coverage": [True, False],
"fPIC": [True, False],
"jemalloc": [True, False],
@@ -46,6 +47,7 @@ class Xrpl(ConanFile):
default_options = {
"assertions": False,
"benchmark": True,
"coverage": False,
"fPIC": True,
"jemalloc": False,
@@ -129,12 +131,14 @@ class Xrpl(ConanFile):
self.options["boost"].without_cobalt = True
def requirements(self):
if self.options.benchmark:
self.requires("benchmark/1.9.5")
self.requires("boost/1.91.0", force=True, transitive_headers=True)
self.requires("date/3.0.4", transitive_headers=True)
if self.options.jemalloc:
self.requires("jemalloc/5.3.1")
self.requires("lz4/1.10.0", force=True)
self.requires("mpt-crypto/0.4.0-rc2", transitive_headers=True)
self.requires("mpt-crypto/0.4.0-rc4", transitive_headers=True)
self.requires("protobuf/6.33.5", force=True)
if self.options.rocksdb:
self.requires("rocksdb/10.5.1")
@@ -162,6 +166,7 @@ class Xrpl(ConanFile):
def generate(self):
tc = CMakeToolchain(self)
tc.variables["tests"] = self.options.tests
tc.variables["benchmark"] = self.options.benchmark
tc.variables["assert"] = self.options.assertions
tc.variables["coverage"] = self.options.coverage
tc.variables["jemalloc"] = self.options.jemalloc

23
flake.lock generated
View File

@@ -36,7 +36,28 @@
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"nixpkgs-custom-glibc": "nixpkgs-custom-glibc"
"nixpkgs-custom-glibc": "nixpkgs-custom-glibc",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1784611586,
"narHash": "sha256-OfqgY+0hp/zseZB7uyH0U8kIDPS4scZZCyAurEplvG0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "14f58845249f3552a89b07772626b8d3c632fa86",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},

View File

@@ -10,12 +10,25 @@
url = "github:NixOS/nixpkgs/9cd98386a38891d1074fc18036b842dc4416f562";
flake = false;
};
# Pinned Rust toolchains, delivered from the Nix store. Lets the Nix CI
# image and dev shell honour the single `rust-toolchain.toml` pin (shared
# with the rustup-based non-Nix runners) while staying hermetic — the
# toolchain lands in the image's Nix closure and is locked by flake.lock.
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{ nixpkgs, nixpkgs-custom-glibc, ... }:
{
nixpkgs,
nixpkgs-custom-glibc,
rust-overlay,
...
}:
let
forEachSystem = import ./nix/utils.nix { inherit nixpkgs nixpkgs-custom-glibc; };
forEachSystem = import ./nix/utils.nix { inherit nixpkgs nixpkgs-custom-glibc rust-overlay; };
in
{
devShells = forEachSystem (import ./nix/devshell.nix);

View File

@@ -1,117 +0,0 @@
#pragma once
#include <functional>
#include <mutex>
#include <set>
/** RAII class to check if an Item is already being processed on another thread,
* as indicated by it's presence in a Collection.
*
* If the Item is not in the Collection, it will be added under lock in the
* ctor, and removed under lock in the dtor. The object will be considered
* "usable" and evaluate to `true`.
*
* If the Item is in the Collection, no changes will be made to the collection,
* and the CanProcess object will be considered "unusable".
*
* It's up to the caller to decide what "usable" and "unusable" mean. (e.g.
* Process or skip a block of code, or set a flag.)
*
* The current use is to avoid lock contention that would be involved in
* processing something associated with the Item.
*
* Examples:
*
* void IncomingLedgers::acquireAsync(LedgerHash const& hash, ...)
* {
* if (CanProcess check{acquiresMutex_, pendingAcquires_, hash})
* {
* acquire(hash, ...);
* }
* }
*
* bool
* NetworkOPsImp::recvValidation(
* std::shared_ptr<STValidation> const& val,
* std::string const& source)
* {
* CanProcess check(
* validationsMutex_, pendingValidations_, val->getLedgerHash());
* BypassAccept bypassAccept =
* check ? BypassAccept::no : BypassAccept::yes;
* handleNewValidation(app_, val, source, bypassAccept, m_journal);
* }
*
*/
class CanProcess
{
public:
template <class Mutex, class Collection, class Item>
CanProcess(Mutex& mtx, Collection& collection, Item const& item)
: cleanup_(insert(mtx, collection, item))
{
}
~CanProcess()
{
if (cleanup_)
cleanup_();
}
CanProcess(CanProcess const&) = delete;
CanProcess&
operator=(CanProcess const&) = delete;
explicit
operator bool() const
{
return static_cast<bool>(cleanup_);
}
private:
template <bool useIterator, class Mutex, class Collection, class Item>
std::function<void()>
doInsert(Mutex& mtx, Collection& collection, Item const& item)
{
std::unique_lock<Mutex> lock(mtx);
// TODO: Use structured binding once LLVM 16 is the minimum supported
// version. See also: https://github.com/llvm/llvm-project/issues/48582
// https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c
auto const insertResult = collection.insert(item);
auto const it = insertResult.first;
if (!insertResult.second)
return {};
if constexpr (useIterator)
return [&, it]() {
std::unique_lock<Mutex> lock(mtx);
collection.erase(it);
};
else
return [&]() {
std::unique_lock<Mutex> lock(mtx);
collection.erase(item);
};
}
// Generic insert() function doesn't use iterators because they may get
// invalidated
template <class Mutex, class Collection, class Item>
std::function<void()>
insert(Mutex& mtx, Collection& collection, Item const& item)
{
return doInsert<false>(mtx, collection, item);
}
// Specialize insert() for std::set, which does not invalidate iterators for
// insert and erase
template <class Mutex, class Item>
std::function<void()>
insert(Mutex& mtx, std::set<Item>& collection, Item const& item)
{
return doInsert<true>(mtx, collection, item);
}
// If set, then the item is "usable"
std::function<void()> cleanup_;
};

View File

@@ -3,6 +3,9 @@
#include <xrpl/basics/IntrusivePointer.ipp>
#include <xrpl/basics/Log.h> // IWYU pragma: keep
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/scope.h>
#include <algorithm>
namespace xrpl {
@@ -601,8 +604,42 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
std::vector<key_type> v;
{
std::scoped_lock const lock(mutex_);
v.reserve(cache_.size());
// Keep track of how many iterations are needed. Exit the loop if the number of retries gets
// absurd. (Note that if this somehow ever happens, one more allocation will be done under
// lock, which is undesirable, but really should be almost impossible.)
std::size_t allocationIterations = 0;
std::unique_lock lock(mutex_);
for (auto size = cache_.size(); v.capacity() < size && allocationIterations < 20;
size = cache_.size())
{
ScopeUnlock const unlock(lock);
if (allocationIterations > 0)
{
JLOG(journal_.info())
<< "getKeys(): Cache grew beyond allocated capacity after "
<< allocationIterations << " prior attempt(s). Have " << v.capacity()
<< ", need " << size << ". Retrying allocation";
}
// Allocate the current size plus a little extra, in case the cache grows while
// allocating. Each time another allocation is needed, the extra also gets bigger until
// it ultimately doubles the size + 1.
constexpr std::size_t baseShift = 5;
auto const bufferOffset = std::min(allocationIterations, std::size_t{baseShift});
auto const bufferShift = baseShift - bufferOffset;
size += (size >> bufferShift) + 1;
v.reserve(size);
++allocationIterations;
}
if (v.capacity() < cache_.size())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::TaggedCache::getKeys(): failed to allocate sufficient capacity");
v.reserve(cache_.size());
// LCOV_EXCL_STOP
}
XRPL_ASSERT(lock.owns_lock(), "xrpl::TaggedCache::getKeys(): owns lock");
XRPL_ASSERT(
v.capacity() >= cache_.size(), "xrpl::TaggedCache::getKeys(): sufficient capacity");
for (auto const& _ : cache_)
v.push_back(_.first);
}

View File

@@ -218,7 +218,7 @@ public:
/**
* Add a suppression peer and get message's relay status.
* Return pair:
* element 1: true if the key is added.
* element 1: true if the peer is added.
* element 2: optional is seated to the relay time point or
* is unseated if has not relayed yet.
*/

View File

@@ -263,10 +263,11 @@ constructLoanState(
Number const& principalOutstanding,
Number const& managementFeeOutstanding);
// Constructs a valid LoanState object from a Loan object, which always has
// rounded values
// Overload of constructLoanState() that reads the three tracked fields
// directly from a Loan ledger object, which always holds rounded values,
// rather than taking them as separate Number arguments.
LoanState
constructRoundedLoanState(SLE::const_ref loan);
constructLoanState(SLE::const_ref loan);
Number
computeManagementFee(

View File

@@ -41,6 +41,19 @@ public:
std::unique_ptr<NodeStore::Backend>&& newBackend,
std::function<void(std::string const& writableName, std::string const& archiveName)> const&
f) = 0;
/**
* Marks an online-delete rotation as in progress (or completed).
*
* While in flight, a read served by the archive backend is copied
* forward into the writable backend even for ordinary
* (duplicate == false) fetches: the archive is about to be deleted,
* and a node body canonicalized into caches during the rotation
* window would otherwise survive only in RAM once the archive is
* dropped.
*/
virtual void
setRotationInFlight(bool inFlight) = 0;
};
} // namespace xrpl::NodeStore

View File

@@ -9,6 +9,7 @@
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
@@ -69,11 +70,22 @@ public:
void
sweep() override;
void
setRotationInFlight(bool inFlight) override;
private:
std::shared_ptr<Backend> writableBackend_;
std::shared_ptr<Backend> archiveBackend_;
mutable std::mutex mutex_;
// True between SHAMapStore starting the cache-freshen phase and the
// completion of rotate(). While true, archive hits on ordinary
// (duplicate == false) fetches are copied forward into the writable
// backend; copyForwardCount_ tallies them per rotation for the
// summary line logged at swap.
std::atomic<bool> rotationInFlight_{false};
std::atomic<std::uint64_t> copyForwardCount_{0};
std::shared_ptr<NodeObject>
fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate)
override;

View File

@@ -40,8 +40,6 @@ struct LedgerHeader
// If validated is false, it means "not yet validated."
// Once validated is true, it will never be set false at a later time.
// NOTE: If you are accessing this directly, you are probably doing it
// wrong. Use LedgerMaster::isValidated().
// VFALCO TODO Make this not mutable
bool mutable validated = false;
bool accepted = false;

View File

@@ -229,13 +229,6 @@ public:
[[nodiscard]] AccountID
getAccountID(SField const& field) const;
/**
* The account responsible for the authorization: the delegate when
* sfDelegate is present, otherwise the account.
*/
[[nodiscard]] AccountID
getInitiator() const;
[[nodiscard]] Blob
getFieldVL(SField const& field) const;
[[nodiscard]] STAmount const&

View File

@@ -142,9 +142,26 @@ public:
TxnSql status,
std::string const& escapedMetaData) const;
[[nodiscard]] std::vector<uint256> const&
/**
* The IDs of the inner transactions of a Batch.
*/
[[nodiscard]] std::vector<uint256>
getBatchTransactionIDs() const;
/**
* The inner transactions of a Batch, built and validated at construction.
* Always seated for Batch STTx instances (construction throws if oversized).
*/
[[nodiscard]] std::vector<std::shared_ptr<STTx const>> const&
getBatchTransactions() const;
/**
* The account responsible for the authorization: the delegate when
* sfDelegate is present, otherwise the account.
*/
[[nodiscard]] AccountID
getInitiator() const;
[[nodiscard]] AccountID
getFeePayerID() const;
@@ -166,13 +183,16 @@ private:
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchSingleSign(STObject const& batchSigner) const;
checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const;
checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const;
void
buildBatchTxnIds();
buildBatchTxns();
STBase*
copy(std::size_t n, void* buf) const override;
@@ -180,11 +200,11 @@ private:
move(std::size_t n, void* buf) override;
friend class detail::STVar;
std::optional<std::vector<uint256>> batchTxnIds_;
std::optional<std::vector<std::shared_ptr<STTx const>>> batchTxns_;
};
bool
passesLocalChecks(STObject const& st, std::string&);
passesLocalChecks(STTx const& tx, std::string&);
/**
* Sterilize a transaction.

View File

@@ -604,7 +604,7 @@ class CanCvtToNotTEC<TEScodes> : public std::true_type
{
};
using NotTEC = TERSubset<CanCvtToNotTEC>;
using NotTECRaw = TERSubset<CanCvtToNotTEC>;
//------------------------------------------------------------------------------
@@ -639,48 +639,188 @@ class CanCvtToTER<TECcodes> : public std::true_type
{
};
template <>
class CanCvtToTER<NotTECRaw> : public std::true_type
{
};
// TERRaw allows all of the subsets (the plain numeric code, no reason string).
using TERRaw = TERSubset<CanCvtToTER>;
//------------------------------------------------------------------------------
// Forward declaration required by TER constructors below (TER.cpp provides the
// definition). The full public declaration is repeated after the struct.
std::string
transHuman(TERRaw code);
//------------------------------------------------------------------------------
// TERBase<RawType, Trait>: shared implementation for TER and NotTEC.
//
// RawType the underlying plain-integer subset (TERRaw or NotTECRaw).
// Trait the trait class restricting which TE*codes values are accepted.
//
// When no reason is supplied the standard description from transHuman() is used
// automatically, so existing bare return statements compile unchanged:
// return tesSUCCESS; // in a TER-returning function
// return temMALFORMED; // in a NotTEC-returning preflight
//
// To attach a more specific reason at a particular return site write:
// return {tecNO_DST, "destination account has been deleted"};
// return {temMALFORMED, "SignerEntries must be present for a non-zero quorum"};
//
template <typename RawType, template <typename> class Trait>
struct TERBase
{
RawType code;
std::string reason;
// Default-construct to tesSUCCESS.
TERBase() : code(tesSUCCESS), reason(transHuman(TERRaw{code}))
{
}
TERBase(TERBase const&) = default;
TERBase(TERBase&&) = default;
TERBase&
operator=(TERBase const&) = default;
TERBase&
operator=(TERBase&&) = default;
// --- construction from RawType ---
// Without explicit reason: populate from transHuman.
TERBase(RawType c) // NOLINT(google-explicit-constructor)
: code(c), reason(transHuman(TERRaw{code}))
{
}
// With explicit reason.
TERBase(RawType c, std::string r) : code(c), reason(std::move(r))
{
}
// --- construction from any TE*codes enum satisfying Trait ---
// Without explicit reason: populate from transHuman.
template <typename T>
TERBase(T c) // NOLINT(google-explicit-constructor)
requires(Trait<std::remove_cv_t<std::remove_reference_t<T>>>::value)
: code(c), reason(transHuman(TERRaw{code}))
{
}
// With explicit reason.
template <typename T>
TERBase(T c, std::string r)
requires(Trait<std::remove_cv_t<std::remove_reference_t<T>>>::value)
: code(c), reason(std::move(r))
{
}
// --- cross-construction from a compatible TERBase specialization ---
// Carries the reason string across (e.g. NotTEC → TER preserves a
// custom reason set during preflight).
template <typename OtherRaw, template <typename> class OtherTrait>
TERBase(TERBase<OtherRaw, OtherTrait> const& other) // NOLINT(google-explicit-constructor)
requires(Trait<OtherRaw>::value)
: code(other.code), reason(other.reason)
{
}
// Build from a raw integer (used by transCode).
static TERBase
fromInt(int from)
{
return TERBase(RawType::fromInt(from));
}
// Implicit conversion to RawType for backward compatibility.
operator RawType() const // NOLINT(google-explicit-constructor)
{
return code;
}
// True when the code is anything other than tesSUCCESS.
explicit
operator bool() const
{
return static_cast<bool>(code);
}
// Allow assignment to json::Objects without casting.
operator json::Value() const // NOLINT(google-explicit-constructor)
{
return json::Value{TERtoInt(code)};
}
friend std::ostream&
operator<<(std::ostream& os, TERBase const& rhs)
{
return os << TERtoInt(rhs.code);
}
};
// TER full result code for apply-phase transactors (all TE*codes).
using TER = TERBase<TERRaw, CanCvtToTER>;
// NotTEC result code for preflight functions (tel*, tem*, tef*, ter*, tes*;
// excludes tec* to prevent fee charging without a valid signature).
using NotTEC = TERBase<NotTECRaw, CanCvtToNotTEC>;
// Expose underlying integers for the comparison operator templates.
inline TERUnderlyingType
TERtoInt(TER const& v)
{
return TERtoInt(v.code);
}
inline TERUnderlyingType
TERtoInt(NotTEC const& v)
{
return TERtoInt(v.code);
}
// Allow TERRaw (and therefore transHuman/transToken/transResultInfo) to accept
// a NotTEC directly, enabling one-step implicit conversion via TERtoInt(NotTEC).
template <>
class CanCvtToTER<NotTEC> : public std::true_type
{
};
// TER allows all of the subsets.
using TER = TERSubset<CanCvtToTER>;
//------------------------------------------------------------------------------
inline bool
isTelLocal(TER x) noexcept
isTelLocal(TER const& x) noexcept
{
return (x >= telLOCAL_ERROR && x < temMALFORMED);
}
inline bool
isTemMalformed(TER x) noexcept
isTemMalformed(TER const& x) noexcept
{
return (x >= temMALFORMED && x < tefFAILURE);
}
inline bool
isTefFailure(TER x) noexcept
isTefFailure(TER const& x) noexcept
{
return (x >= tefFAILURE && x < terRETRY);
}
inline bool
isTerRetry(TER x) noexcept
isTerRetry(TER const& x) noexcept
{
return (x >= terRETRY && x < tesSUCCESS);
}
inline bool
isTesSuccess(TER x) noexcept
isTesSuccess(TER const& x) noexcept
{
// Makes use of TERSubset::operator bool()
// Makes use of TER::operator bool()
return !x;
}
inline bool
isTecClaim(TER x) noexcept
isTecClaim(TER const& x) noexcept
{
return (x >= tecCLAIM);
}
@@ -689,13 +829,10 @@ std::unordered_map<TERUnderlyingType, std::pair<char const* const, char const* c
transResults();
bool
transResultInfo(TER code, std::string& token, std::string& text);
transResultInfo(TERRaw code, std::string& token, std::string& text);
std::string
transToken(TER code);
std::string
transHuman(TER code);
transToken(TERRaw code);
std::optional<TER>
transCode(std::string const& token);

View File

@@ -239,6 +239,7 @@ JSS(enabled); // out: AmendmentTable
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_reason); // out: NetworkOPs, TransactionSign, Submit
JSS(entire_set); // out: get_aggregate_price
JSS(ephemeral_key); // out: ValidatorInfo
// in/out: Manifest

View File

@@ -196,7 +196,7 @@ public:
virtual bool
isFull() = 0;
virtual void
setMode(OperatingMode om, char const* reason) = 0;
setMode(OperatingMode om) = 0;
virtual bool
isBlocked() = 0;
virtual bool

View File

@@ -100,7 +100,7 @@ public:
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: registry(registry)
, view(view)
, preflightResult(preflightResult)
, preflightResult(std::move(preflightResult))
, flags(flags)
, tx(tx)
, parentBatchId(parentBatchId)

View File

@@ -28,7 +28,7 @@ struct ApplyResult
std::optional<TxMeta> metadata;
ApplyResult(TER t, bool a, std::optional<TxMeta> m = std::nullopt)
: ter(t), applied(a), metadata(std::move(m))
: ter(std::move(t)), applied(a), metadata(std::move(m))
{
}
};

View File

@@ -505,7 +505,7 @@ class FlowException : public std::runtime_error
public:
TER ter;
FlowException(TER t, std::string const& msg) : std::runtime_error(msg), ter(t)
FlowException(TER t, std::string const& msg) : std::runtime_error(msg), ter(std::move(t))
{
}

View File

@@ -12,6 +12,7 @@
#include <array>
#include <cstdint>
#include <optional>
namespace xrpl {
@@ -39,6 +40,9 @@ public:
static NotTEC
checkSign(PreclaimContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
@@ -76,6 +80,10 @@ private:
// only be reached through Batch::checkSign.
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
// nullopt on overflow or oversized signer arrays.
static std::optional<XRPAmount>
calculateBaseFeeImpl(ReadView const& view, STTx const& tx);
};
} // namespace xrpl

View File

@@ -7,8 +7,10 @@ let
inherit (import ./packages.nix { inherit pkgs; })
commonPackages
gccPackage
gccVersion
llvmPackages
llvmVersion
mkVersionedToolLinks
;
# Underlying compiler toolchains to wrap (versions pinned in packages.nix).
@@ -127,6 +129,25 @@ in
customGcov
customClangForCiEnv
customBinutils
(mkVersionedToolLinks {
name = "gcc";
package = customGcc;
version = gccVersion;
tools = [
"gcc"
"g++"
"cpp"
];
})
(mkVersionedToolLinks {
name = "clang";
package = customClang;
version = llvmVersion;
tools = [
"clang"
"clang++"
];
})
# CA certificate bundle so HTTPS clients (git, curl, conan) can verify
# TLS connections without ca-certificates being installed in the system.
pkgs.cacert

View File

@@ -3,7 +3,9 @@ let
inherit (import ./packages.nix { inherit pkgs; })
commonPackages
gccVersion
llvmVersion
llvmPackages
mkVersionedToolLinks
;
# Plain nixpkgs stdenvs — no custom glibc, unlike ci-env.nix.
@@ -15,6 +17,8 @@ let
{
stdenv,
compilerName,
version ? null,
versionedTools ? [ ],
}:
let
compilerVersion =
@@ -25,9 +29,15 @@ let
echo "Compiler: "
${compilerName} --version
'';
versionedLinks = pkgs.lib.optional (version != null) (mkVersionedToolLinks {
name = compilerName;
package = stdenv.cc;
inherit version;
tools = versionedTools;
});
in
(pkgs.mkShell.override { inherit stdenv; }) {
packages = commonPackages;
packages = commonPackages ++ versionedLinks;
shellHook = ''
echo "Welcome to xrpld development shell";
${compilerVersion}
@@ -41,11 +51,22 @@ rec {
gcc = makeShell {
stdenv = gccStdenv;
compilerName = "gcc";
version = gccVersion;
versionedTools = [
"gcc"
"g++"
"cpp"
];
};
clang = makeShell {
stdenv = clangStdenv;
compilerName = "clang";
version = llvmVersion;
versionedTools = [
"clang"
"clang++"
];
};
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).

View File

@@ -12,6 +12,7 @@ COPY nix/packages.nix /tmp/build/nix/packages.nix
COPY nix/utils.nix /tmp/build/nix/utils.nix
COPY flake.nix /tmp/build/
COPY flake.lock /tmp/build/
COPY rust-toolchain.toml /tmp/build/
WORKDIR /tmp/build
FROM builder-source AS builder

View File

@@ -47,7 +47,9 @@ work without `ca-certificates` being installed in the base image.
[`test_files/cpp/sources/`](./test_files/cpp/sources) with both `g++` and
`clang++`, and sanitizers, and
- compiles the Rust test programs in
[`test_files/rust/sources/`](./test_files/rust/sources) with `rustc`.
[`test_files/rust/sources/`](./test_files/rust/sources) with `rustc`, and
builds the [`test_files/rust/proc_macro/`](./test_files/rust/proc_macro)
workspace with `cargo` to exercise proc-macro dylib loading.
3. **`tester`** — Start again from a clean `BASE_IMAGE` (no Nix toolchain),
install only the sanitizer runtime libraries
([`install-sanitizer-libs.sh`](./install-sanitizer-libs.sh)), and run the
@@ -76,20 +78,23 @@ toolchain being present at runtime. Two pieces make that work:
[`loader-path.sh`](./loader-path.sh) reports the expected loader path for the
current architecture, so we can patch the binaries to use the correct loader.
The build then verifies all of this end to end: the C++ test programs in
`test_files/cpp/sources/` (a regular binary plus ASan/TSan/UBSan variants) and
the Rust test programs in `test_files/rust/sources/` (a hello binary plus panic
and overflow-check variants) are compiled in `final`, their `PT_INTERP` is
patched to the target loader, and they are run in the clean `tester` stage to
confirm each emits the expected diagnostic on a stock base image.
The build then verifies all of this end to end, and the C++ and Rust programs
go through the same pipeline: each is compiled in `final`, has its `PT_INTERP`
patched to the target loader, and is then run in the clean `tester` stage to
confirm it emits the expected diagnostic on a stock base image. The C++ programs
are in `test_files/cpp/sources/` (a regular binary plus ASan/TSan/UBSan
variants); the Rust programs are in `test_files/rust/sources/` (a hello binary
plus panic and overflow-check variants), plus the `test_files/rust/proc_macro/`
workspace — a crate whose compilation additionally loads a proc-macro dylib, and
whose resulting binary is patched and run like the others.
## Files
| File | Purpose |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. |
| [`./test_files/rust/`](./test_files/rust) | Rust rustc smoke test: sources + compile/run scripts. |
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |
| File | Purpose |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. |
| [`./test_files/rust/`](./test_files/rust) | Rust smoke test: rustc sources + a cargo proc-macro workspace + compile/run scripts. |
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |

View File

@@ -40,6 +40,29 @@ compile hello
compile panic
compile overflow "-C overflow-checks=on"
function compile_proc_macro() {
local proj="${src_dir}/../proc_macro"
echo "=== Building proc-macro workspace (cargo) ==="
cargo build --manifest-path "${proj}/Cargo.toml" --offline
local built="${proj}/target/debug/test_macro"
if [ ! -f "${built}" ]; then
echo "ERROR: built test_macro binary not found at ${built}" >&2
exit 1
fi
local binary="${dst_dir}/proc_macro"
cp "${built}" "${binary}"
echo "=== Patching ${binary} to use ${loader} as PT_INTERP ==="
patchelf --set-interpreter "${loader}" --remove-rpath "${binary}"
rm -rf "${proj}/target"
}
compile_proc_macro
echo "=== All binaries compiled ==="
ls -la "${dst_dir}"

View File

@@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "echo_macro"
version = "0.0.0"
[[package]]
name = "test_macro"
version = "0.0.0"
dependencies = [
"echo_macro",
]

View File

@@ -0,0 +1,3 @@
[workspace]
resolver = "2"
members = ["echo_macro", "test_macro"]

View File

@@ -0,0 +1,8 @@
[package]
name = "echo_macro"
version = "0.0.0"
edition = "2024"
publish = false
[lib]
proc-macro = true

View File

@@ -0,0 +1,6 @@
use proc_macro::TokenStream;
#[proc_macro]
pub fn define_echo(item: TokenStream) -> TokenStream {
format!("fn echo() -> u32 {{ {item} }}").parse().unwrap()
}

View File

@@ -0,0 +1,8 @@
[package]
name = "test_macro"
version = "0.0.0"
edition = "2024"
publish = false
[dependencies]
echo_macro = { path = "../echo_macro" }

View File

@@ -0,0 +1,9 @@
use echo_macro::define_echo;
define_echo!(42);
fn main() {
let a = echo();
println!("proc-macro answer = {a}");
assert_eq!(a, 42, "proc-macro expansion produced the wrong value");
}

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# Run pre-compiled Rust binaries and confirm each emits its expected diagnostic.
# Binaries must already exist in <bins_dir> as <name> for name in
# {hello,panic,overflow}.
# {hello,panic,overflow,proc_macro}.
set -eo pipefail
@@ -54,12 +54,13 @@ declare -A expect=(
[hello]="Hello from main thread"
[panic]="explicit panic from test"
[overflow]="attempt to add with overflow"
[proc_macro]="proc-macro answer = 42"
)
for name in hello panic overflow; do
for name in hello panic overflow proc_macro; do
binary="${bins_dir}/${name}"
if [ "${name}" = "hello" ]; then
if [ "${name}" = "hello" ] || [ "${name}" = "proc_macro" ]; then
expected_rc=0
else
expected_rc=nonzero

View File

@@ -15,6 +15,55 @@ let
runClangTidy = pkgs.writeShellScriptBin "run-clang-tidy" ''
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
'';
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
# names (gcc, g++, clang-tidy, ...). Several tools probe for a
# version-suffixed name first and fall back to a system binary on the PATH
# when Nix doesn't provide it:
# - Conan's Boost recipe looks up `g++-<major>` before plain `g++`.
# - bin/pre-commit/clang_tidy_check.py looks up `run-clang-tidy-<v>` and
# `clang-apply-replacements-<v>` before the unsuffixed names.
# On a host that also has the matching system binary (e.g. Ubuntu's
# `/usr/bin/g++-15` or `clang-tidy-22`) the probe escapes Nix and mixes a
# system tool into the Nix environment. Generate version-suffixed symlinks
# next to a package's tools so those probes resolve to the Nix ones.
#
# Compiler links must point at whichever compiler is active in a given
# environment (the plain stdenv compiler in the dev shell, the custom-glibc
# wrappers in ci-env.nix), so those callers pass their own `package`; the
# clang tooling is environment-independent and is linked in commonPackages.
mkVersionedToolLinks =
{
name,
package,
version,
tools,
}:
pkgs.linkFarm "${name}-${toString version}-versioned-links" (
map (tool: {
name = "bin/${tool}-${toString version}";
path = "${package}/bin/${tool}";
}) tools
);
clangToolLinks = mkVersionedToolLinks {
name = "clang-tools";
package = clangTools;
version = llvmVersion;
tools = [
"clang-tidy"
"clang-apply-replacements"
"clang-format"
];
};
runClangTidyLink = mkVersionedToolLinks {
name = "run-clang-tidy";
package = runClangTidy;
version = llvmVersion;
tools = [ "run-clang-tidy" ];
};
in
{
inherit
@@ -22,9 +71,12 @@ in
llvmVersion
gccPackage
llvmPackages
mkVersionedToolLinks
;
commonPackages = with pkgs; [
clangToolLinks
runClangTidyLink
ccache
clangbuildanalyzer
clangTools
@@ -63,14 +115,10 @@ in
vim
zip
# Rust packages
cargo
cargo-audit
cargo-llvm-cov
cargo-nextest
clippy
corrosion
rust-analyzer
rustc
rustfmt
rustToolchain
];
}

View File

@@ -1,4 +1,8 @@
{ nixpkgs, nixpkgs-custom-glibc }:
{
nixpkgs,
nixpkgs-custom-glibc,
rust-overlay,
}:
function:
nixpkgs.lib.genAttrs
[
@@ -10,7 +14,12 @@ nixpkgs.lib.genAttrs
(
system:
function {
pkgs = import nixpkgs { inherit system; };
# rust-overlay adds `pkgs.rust-bin`, from which we build the pinned Rust
# toolchain (see packages.nix). Consumed by both the CI image and dev shell.
pkgs = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
# glibc 2.31 — matches the system libc on Ubuntu 20.04 LTS. Sourced
# from the nixpkgs snapshot pinned via the `nixpkgs-custom-glibc`
# flake input, so the build uses the compiler from that snapshot

4
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,4 @@
[toolchain]
channel = "1.95"
components = ["rustfmt", "clippy", "rust-analyzer"]
profile = "minimal"

View File

@@ -0,0 +1,22 @@
include(XrplAddBenchmark)
# Benchmark requirements.
find_package(benchmark REQUIRED)
# Custom target for all benchmarks defined in this file.
add_custom_target(xrpl.benchmarks)
# Common library dependencies for every benchmark module. `benchmark_main`
# supplies a `main()` that parses the standard Google Benchmark CLI flags
# (`--benchmark_filter`, `--benchmark_format`, ...), so no per-module main.cpp
# is needed.
add_library(xrpl.imports.bench INTERFACE)
target_link_libraries(
xrpl.imports.bench
INTERFACE benchmark::benchmark_main xrpl.libxrpl
)
# One benchmark executable for each module.
xrpl_add_benchmark(nodestore)
target_link_libraries(xrpl.bench.nodestore PRIVATE xrpl.imports.bench)
add_dependencies(xrpl.benchmarks xrpl.bench.nodestore)

View File

@@ -0,0 +1,329 @@
#include <xrpl/nodestore/Backend.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Types.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/nodestore/NodeStoreBench.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::NodeStore {
namespace {
constexpr std::size_t kPoolSizes[] = {1000, 10000, 100000};
constexpr int kThreadCounts[] = {1, 4, 8};
constexpr std::size_t kBatchSize = 256;
constexpr std::string_view kNamePrefix = "BM_Backend_";
constexpr std::string_view kNameSeparator = "/";
struct RunState
{
std::unique_ptr<BackendHarness> harness;
Batch present; // prefix-1 objects, eligible to be stored
Batch recent; // prefix-1 objects in the "future" key space
std::vector<uint256> missing; // prefix-2 keys that are never stored
std::vector<std::size_t> shuffle; // [0, poolSize) permutation for random-like access
std::size_t avgPayload = 0; // mean getData().size() over `present`
void
release()
{
harness.reset();
Batch{}.swap(present);
Batch{}.swap(recent);
std::vector<uint256>{}.swap(missing);
std::vector<std::size_t>{}.swap(shuffle);
}
};
struct SetupContext
{
RunState& rs;
Backend& backend;
std::size_t poolSize;
};
struct IterateContext
{
RunState& rs;
Backend& backend;
std::size_t index;
std::size_t poolSize;
};
struct Workload
{
std::string_view name;
std::function<void(SetupContext const&)> setup;
std::function<void(IterateContext const&)> iterate;
bool reportBytes = false; // SetBytesProcessed from rs.avgPayload
bool clobber = true; // ClobberMemory after the loop (false for pure stores)
bool pinToPool = false; // pin iterations to one pool sweep instead of autotuning
};
// One store() per iteration. Iterations are pinned to one pool sweep (per
// thread) so the index never wraps past the pool - otherwise NuDB::doInsert
// swallows key_exists and the workload degenerates into duplicate-detection
// no-ops.
Workload const kInsert{
.name = "Insert",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
backend.store(rs.present[index % poolSize]);
},
.reportBytes = true,
.clobber = false,
.pinToPool = true,
};
// One fetch() of a present key (a hit) per iteration.
Workload const kFetch{
.name = "Fetch",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
prepopulate(ctx.backend, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
backend.fetch(rs.present[index % poolSize]->getHash(), &result);
benchmark::DoNotOptimize(result);
},
.reportBytes = true,
};
// One fetch() of a never-stored key (a miss); the backend is left empty.
Workload const kMissing{
.name = "Missing",
.setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); },
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
backend.fetch(rs.missing[index % poolSize], &result);
benchmark::DoNotOptimize(result);
},
};
// 80% hits / 20% misses. The fetch index comes from a shuffle table so access
// is random-like without per-iteration RNG cost; sequential `index % poolSize`
// would be artificially cache-friendly to RocksDB's block cache.
Workload const kMixed{
.name = "Mixed",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.missing = makeMissingKeys(ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/1);
prepopulate(ctx.backend, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
auto const pick = rs.shuffle[index % poolSize];
if (index % 5 == 0)
{
backend.fetch(rs.missing[pick], &result);
}
else
{
backend.fetch(rs.present[pick]->getHash(), &result);
}
benchmark::DoNotOptimize(result);
},
};
// An xrpld-like cycle: a hit, a maybe-miss recent fetch, and a store. The
// recent fetch uses the shuffle table (not `slot`) so it doesn't fetch the item
// it's about to store this iteration - which would give an all-miss-then-hit
// step instead of a smooth ramp. The store walks sequentially so each recent
// object is stored once.
Workload const kWork{
.name = "Work",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.recent = makePool(1, ctx.poolSize, ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/2);
prepopulate(ctx.backend, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
auto const slot = index % poolSize;
auto const pick = rs.shuffle[slot];
std::shared_ptr<NodeObject> historical;
backend.fetch(rs.present[pick]->getHash(), &historical);
benchmark::DoNotOptimize(historical);
std::shared_ptr<NodeObject> recent;
backend.fetch(rs.recent[pick]->getHash(), &recent);
benchmark::DoNotOptimize(recent);
backend.store(rs.recent[slot]);
},
.clobber = true,
.pinToPool = true,
};
auto
makeRunner(Workload w, std::string cfg, std::shared_ptr<RunState> rs)
{
return [w = std::move(w), cfg = std::move(cfg), rs = std::move(rs)](benchmark::State& state) {
auto const poolSize = static_cast<std::size_t>(state.range(0));
if (state.thread_index() == 0)
{
rs->harness = std::make_unique<BackendHarness>(cfg);
w.setup(
SetupContext{.rs = *rs, .backend = *rs->harness->backend, .poolSize = poolSize});
}
std::size_t index = state.thread_index();
for (auto _ : state)
{
w.iterate(
IterateContext{
.rs = *rs,
.backend = *rs->harness->backend,
.index = index,
.poolSize = poolSize});
index += state.threads();
}
if (w.clobber)
benchmark::ClobberMemory();
state.SetItemsProcessed(state.iterations());
if (w.reportBytes)
state.SetBytesProcessed(static_cast<std::int64_t>(state.iterations() * rs->avgPayload));
if (state.thread_index() == 0)
rs->release();
};
}
// Register workload `w` against backend `bc`, choosing the registration shape
// from `w.pinToPool`.
void
registerWorkload(BackendConfig const& bc, Workload const& w)
{
std::string const cfg = bc.config;
std::string name{kNamePrefix};
name += w.name;
name += kNameSeparator;
name += bc.name;
if (!w.pinToPool)
{
auto rs = std::make_shared<RunState>();
auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs));
b->RangeMultiplier(10)->Range(kPoolSizes[0], kPoolSizes[std::size(kPoolSizes) - 1]);
b->Threads(1)->Threads(4)->Threads(8)->UseRealTime();
return;
}
for (auto const poolSize : kPoolSizes)
{
for (auto const threads : kThreadCounts)
{
if (poolSize % static_cast<std::size_t>(threads) != 0)
continue;
auto rs = std::make_shared<RunState>();
benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs))
->Arg(poolSize)
->Iterations(poolSize / static_cast<std::size_t>(threads))
->Threads(threads)
->UseRealTime();
}
}
}
// One storeBatch() of kBatchSize objects per iteration. Single-threaded:
// Backend::storeBatch must not run concurrently with itself or store().
// Iterations are pinned to the batch count so the index never wraps into
// key_exists no-ops. Kept separate from Workload: batch slicing and the
// per-batch item/byte accounting don't fit the thread-axis mold.
void
registerStoreBatch(BackendConfig const& bc)
{
std::string const cfg = bc.config;
std::string name{kNamePrefix};
name += "StoreBatch";
name += kNameSeparator;
name += bc.name;
for (auto const poolSize : kPoolSizes)
{
auto const numBatches = poolSize / kBatchSize;
if (numBatches == 0)
continue;
auto rs = std::make_shared<RunState>();
benchmark::RegisterBenchmark(
name,
[rs, cfg](benchmark::State& state) {
auto const poolSize = static_cast<std::size_t>(state.range(0));
rs->harness = std::make_unique<BackendHarness>(cfg);
rs->present = makePool(1, poolSize);
rs->avgPayload = averagePayload(rs->present);
std::vector<Batch> const batches = sliceBatches(rs->present, kBatchSize);
if (batches.empty())
{
state.SkipWithError("pool smaller than one batch");
return;
}
std::size_t index = 0;
for (auto _ : state)
{
rs->harness->backend->storeBatch(batches[index % batches.size()]);
++index;
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * kBatchSize));
state.SetBytesProcessed(
static_cast<std::int64_t>(state.iterations() * kBatchSize * rs->avgPayload));
rs->release();
})
->Arg(poolSize)
->Iterations(numBatches);
}
}
[[maybe_unused]] bool const kRegistered = [] {
auto const workloads = std::to_array({&kInsert, &kFetch, &kMissing, &kMixed, &kWork});
for (auto const& bc : backendConfigs())
{
for (auto const* w : workloads)
registerWorkload(bc, *w);
registerStoreBatch(bc);
}
return true;
}();
} // namespace
} // namespace xrpl::NodeStore

View File

@@ -0,0 +1,243 @@
#include <xrpl/nodestore/Database.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Types.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/nodestore/NodeStoreBench.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::NodeStore {
namespace {
// Number of distinct objects pre-generated per run.
constexpr std::size_t kDefaultPoolSize = 100000;
// Async read threads the Database spawns. Unused by the synchronous fetch path
// these benchmarks take; kept fixed so runs are comparable.
constexpr int kReadThreads = 4;
constexpr std::string_view kNamePrefix = "BM_Database_";
constexpr std::string_view kNameSeparator = "/";
struct RunState
{
std::unique_ptr<DatabaseHarness> harness;
Batch present; // prefix-1 objects, eligible to be stored
Batch recent; // prefix-1 objects in the "future" key space
std::vector<uint256> missing; // prefix-2 keys that are never stored
std::vector<std::size_t> shuffle; // [0, poolSize) permutation for random-like access
std::size_t avgPayload = 0; // mean getData().size() over `present`
};
struct SetupContext
{
RunState& rs;
Database& db;
std::size_t poolSize;
};
struct IterateContext
{
RunState& rs;
Database& db;
std::uint32_t seq;
std::size_t index;
std::size_t poolSize;
};
struct Workload
{
std::string_view name;
std::function<void(SetupContext const&)> setup;
std::function<void(IterateContext const&)> iterate;
bool reportBytes = false;
bool pinIterations = false;
};
void
prepopulate(Database& db, Batch const& objects)
{
auto const seq = db.earliestLedgerSeq();
for (auto const& obj : objects)
{
Blob data(obj->getData());
db.store(obj->getType(), std::move(data), obj->getHash(), seq);
}
db.sync();
}
// One store() per iteration; a fresh Blob copy is handed over each time.
Workload const kStore{
.name = "Store",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto const& obj = rs.present[index % poolSize];
Blob data(obj->getData());
db.store(obj->getType(), std::move(data), obj->getHash(), seq);
},
.reportBytes = true,
.pinIterations = true,
};
// One fetchNodeObject() of a stored key (a hit) per iteration.
Workload const kFetch{
.name = "Fetch",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
prepopulate(ctx.db, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto obj = db.fetchNodeObject(rs.present[index % poolSize]->getHash(), seq);
benchmark::DoNotOptimize(obj);
},
.reportBytes = true,
};
// One fetchNodeObject() of a never-stored key (a miss) per iteration.
Workload const kMissing{
.name = "Missing",
.setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); },
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto obj = db.fetchNodeObject(rs.missing[index % poolSize], seq);
benchmark::DoNotOptimize(obj);
},
};
// 80% hits / 20% misses. The fetch index comes from a shuffle table so access
// is random-like without per-iteration RNG cost; sequential `index % poolSize`
// would be artificially cache-friendly.
Workload const kMixed{
.name = "Mixed",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.missing = makeMissingKeys(ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/1);
prepopulate(ctx.db, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto const pick = rs.shuffle[index % poolSize];
std::shared_ptr<NodeObject> obj;
if (index % 5 == 0)
{
obj = db.fetchNodeObject(rs.missing[pick], seq);
}
else
{
obj = db.fetchNodeObject(rs.present[pick]->getHash(), seq);
}
benchmark::DoNotOptimize(obj);
},
};
// An xrpld-like cycle: a hit, a maybe-miss recent fetch, and a store. The
// recent fetch uses the shuffle table (not `slot`) so it doesn't fetch the item
// it's about to store this iteration - which would give an all-miss-then-hit
// step instead of a smooth ramp. The store walks sequentially so each recent
// object is stored once.
Workload const kWork{
.name = "Work",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.recent = makePool(1, ctx.poolSize, ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/2);
prepopulate(ctx.db, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto const slot = index % poolSize;
auto const pick = rs.shuffle[slot];
auto historical = db.fetchNodeObject(rs.present[pick]->getHash(), seq);
benchmark::DoNotOptimize(historical);
auto recent = db.fetchNodeObject(rs.recent[pick]->getHash(), seq);
benchmark::DoNotOptimize(recent);
auto const& obj = rs.recent[slot];
Blob data(obj->getData());
db.store(obj->getType(), std::move(data), obj->getHash(), seq);
},
.pinIterations = true,
};
void
registerWorkload(BackendConfig const& bc, Workload const& w)
{
auto rs = std::make_shared<RunState>();
std::string const cfg = bc.config;
std::string name{kNamePrefix};
name += w.name;
name += kNameSeparator;
name += bc.name;
auto* b = benchmark::RegisterBenchmark(name, [rs, cfg, w](benchmark::State& state) {
auto const poolSize = static_cast<std::size_t>(state.range(0));
rs->harness = std::make_unique<DatabaseHarness>(cfg, kReadThreads);
auto& db = *rs->harness->db;
w.setup(SetupContext{.rs = *rs, .db = db, .poolSize = poolSize});
auto const seq = db.earliestLedgerSeq();
std::size_t index = 0;
for (auto _ : state)
{
w.iterate(
IterateContext{
.rs = *rs, .db = db, .seq = seq, .index = index, .poolSize = poolSize});
++index;
}
benchmark::ClobberMemory();
state.SetItemsProcessed(state.iterations());
if (w.reportBytes)
{
state.SetBytesProcessed(static_cast<std::int64_t>(state.iterations() * rs->avgPayload));
}
rs->harness.reset();
});
b->Arg(kDefaultPoolSize);
if (w.pinIterations)
b->Iterations(kDefaultPoolSize);
}
[[maybe_unused]] bool const kRegistered = [] {
auto const workloads = std::to_array({&kStore, &kFetch, &kMissing, &kMixed, &kWork});
for (auto const& bc : backendConfigs())
{
for (auto const* w : workloads)
registerWorkload(bc, *w);
}
return true;
}();
} // namespace
} // namespace xrpl::NodeStore

View File

@@ -0,0 +1,318 @@
#pragma once
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/nodestore/Backend.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <numeric>
#include <random>
#include <string>
#include <utility>
#include <vector>
// Shared helpers for the NodeStore benchmarks.
//
namespace xrpl::NodeStore {
// Fill `bytes` of memory at `buffer` with random bits drawn from `g`.
template <class Generator>
inline void
rngcpy(void* buffer, std::size_t bytes, Generator& g)
{
using result_type = typename Generator::result_type;
while (bytes >= sizeof(result_type))
{
auto const v = g();
std::memcpy(buffer, &v, sizeof(v));
buffer = reinterpret_cast<std::uint8_t*>(buffer) + sizeof(v);
bytes -= sizeof(v);
}
if (bytes > 0)
{
auto const v = g();
std::memcpy(buffer, &v, bytes);
}
}
/**
* @brief Deterministic generator of a reproducible sequence of random NodeObjects.
*
* Indexing is stable: `obj(n)` and `key(n)` always return the same value for a
* given `n`, regardless of call order, because the engine is reseeded from `n`
* on every call.
*
* Using different prefixes guarantees the two key spaces are disjoint for the fetch-miss
* workloads.
*/
class Sequence
{
private:
static constexpr auto kMinSize = 250;
static constexpr auto kMaxSize = 1250;
beast::xor_shift_engine gen_;
std::uint8_t prefix_;
std::discrete_distribution<std::uint32_t> dType_;
std::uniform_int_distribution<std::uint32_t> dSize_;
public:
explicit Sequence(std::uint8_t prefix)
: prefix_(prefix)
// uniform distribution over hotLEDGER - hotTRANSACTION_NODE
// but exclude hotTRANSACTION = 2 (removed)
, dType_({1, 1, 0, 1, 1})
, dSize_(kMinSize, kMaxSize)
{
}
// Returns the n-th key. Used to generate keys that are never stored.
// The layout mirrors obj()'s: prefix at byte 0, RNG over the rest, so the
// two key spaces stay disjoint by construction (not by coincidence).
uint256
key(std::size_t n)
{
gen_.seed(n + 1);
uint256 result;
auto const data = static_cast<std::uint8_t*>(&*result.begin());
*data = prefix_;
rngcpy(data + 1, result.size() - 1, gen_);
return result;
}
// Returns the n-th complete NodeObject.
std::shared_ptr<NodeObject>
obj(std::size_t n)
{
gen_.seed(n + 1);
uint256 key;
auto const data = static_cast<std::uint8_t*>(&*key.begin());
*data = prefix_;
rngcpy(data + 1, key.size() - 1, gen_);
Blob value(dSize_(gen_));
rngcpy(&value[0], value.size(), gen_);
return NodeObject::createObject(
safeCast<NodeObjectType>(dType_(gen_)), std::move(value), key);
}
// Fills `b` with `size` consecutive NodeObjects starting at index `n`.
void
batch(std::size_t n, Batch& b, std::size_t size)
{
b.clear();
b.reserve(size);
while ((size--) != 0u)
b.push_back(obj(n++));
}
};
// Parse a comma-separated "key=value,key=value" string into a config Section.
inline Section
parseConfig(std::string const& s)
{
Section section;
std::vector<std::string> values;
boost::split(values, s, boost::algorithm::is_any_of(","));
section.append(values);
return section;
}
// Pre-generate `count` distinct objects from key space `prefix`, starting at
// sequence index `start`.
inline Batch
makePool(std::uint8_t prefix, std::size_t count, std::size_t start = 0)
{
Sequence seq(prefix);
Batch pool;
pool.reserve(count);
for (std::size_t i = 0; i < count; ++i)
pool.push_back(seq.obj(start + i));
return pool;
}
// Pre-generate `count` keys disjoint from every `makePool(...)` object, for
// measuring fetches that miss.
inline std::vector<uint256>
makeMissingKeys(std::size_t count)
{
Sequence seq(2);
std::vector<uint256> keys;
keys.reserve(count);
for (std::size_t i = 0; i < count; ++i)
keys.push_back(seq.key(i));
return keys;
}
// Mean payload size across a pool, used for SetBytesProcessed throughput.
inline std::size_t
averagePayload(Batch const& pool)
{
if (pool.empty())
return 0;
std::size_t total = 0;
for (auto const& obj : pool)
total += obj->getData().size();
return total / pool.size();
}
// Store every object and flush, so a following fetch exercises the real read
// path rather than an in-memory write buffer.
//
// We chunk the write at kBatchWriteLimitSize because Types.h documents that as
// the maximum allowed batch size. NuDB happens to tolerate larger batches
// today, but the benchmark should not rely on that.
//
// sync() is a no-op for both NuDB and RocksDB at the moment (NuDB has a small
// internal burst buffer that the timed loop will warm up). That is a contract
// hint, not a guarantee; if either backend ever grows a real flush we get it
// here for free.
inline void
prepopulate(Backend& backend, Batch const& objects)
{
for (std::size_t i = 0; i < objects.size(); i += kBatchWriteLimitSize)
{
auto const end = std::min(i + kBatchWriteLimitSize, objects.size());
backend.storeBatch(Batch(objects.begin() + i, objects.begin() + end));
}
backend.sync();
}
// A deterministic permutation of [0, size). Lets the timed loop visit the
// pre-generated pool in a random-like order with zero RNG cost per iteration -
// the Timing_test workloads it replaces used uniform_int_distribution per
// fetch, and a shuffle table reproduces that access pattern without paying for
// the distribution inside the timed region.
inline std::vector<std::size_t>
makeShuffle(std::size_t size, std::uint64_t seed)
{
std::vector<std::size_t> v(size);
std::iota(v.begin(), v.end(), std::size_t{0});
beast::xor_shift_engine gen(seed);
std::shuffle(v.begin(), v.end(), gen);
return v;
}
// Partition a pool into fixed-size batches. Any trailing remainder shorter than
// `batchSize` is dropped, so every returned batch has exactly `batchSize`.
inline std::vector<Batch>
sliceBatches(Batch const& pool, std::size_t batchSize)
{
std::vector<Batch> batches;
if (batchSize == 0)
return batches;
batches.reserve(pool.size() / batchSize);
for (std::size_t i = 0; i + batchSize <= pool.size(); i += batchSize)
batches.emplace_back(pool.begin() + i, pool.begin() + i + batchSize);
return batches;
}
/**
* @brief RAII owner of a NodeStore Backend opened on a private temporary directory.
*
* Member declaration order matters: `tempDir` is declared first so it is
* destroyed last, after the backend has closed and released its files.
*/
struct BackendHarness
{
beast::TempDir tempDir;
DummyScheduler scheduler;
beast::Journal journal{beast::Journal::getNullSink()};
std::unique_ptr<Backend> backend;
explicit BackendHarness(std::string const& configString)
{
Section config = parseConfig(configString);
// A private, unique path per harness, so concurrent or repeated runs
// never share on-disk state.
config.set("path", tempDir.path());
backend =
Manager::instance().makeBackend(config, megabytes(std::size_t{4}), scheduler, journal);
backend->setDeletePath();
backend->open();
}
~BackendHarness()
{
if (backend)
backend->close();
}
};
/**
* RAII owner of a NodeStore Database - the application-facing wrapper around a
* Backend, which adds fetch/store accounting and the async read-thread pool.
*/
struct DatabaseHarness
{
beast::TempDir tempDir;
DummyScheduler scheduler;
beast::Journal journal{beast::Journal::getNullSink()};
std::unique_ptr<Database> db;
DatabaseHarness(std::string const& configString, int readThreads)
{
Section config = parseConfig(configString);
config.set("path", tempDir.path());
db = Manager::instance().makeDatabase(
megabytes(std::size_t{4}), scheduler, readThreads, config, journal);
}
~DatabaseHarness()
{
if (db)
db->stop();
}
};
// A NodeStore backend to benchmark, named for the --benchmark_filter CLI flag.
struct BackendConfig
{
char const* name; // short label, e.g. "nudb"
char const* config; // parseConfig() string, e.g. "type=nudb"
};
// The backends every workload is registered against.
//
// The in-memory backend is intentionally excluded. It keeps its table in a
// process-global map keyed by path, with no removal API, so building a fresh
// backend per run - as a microbenchmark must - would leak the whole dataset on
// every run. Timing_test, the suite this benchmark replaces, excluded it for
// the same reason. NuDB and RocksDB are the production backends worth timing.
//
// RocksDB is included only when it was compiled in (xrpl.libxrpl carries
// XRPL_ROCKSDB_AVAILABLE transitively).
inline std::vector<BackendConfig> const&
backendConfigs()
{
static std::vector<BackendConfig> const kConfigs = {
{.name = "nudb", .config = "type=nudb"},
#if XRPL_ROCKSDB_AVAILABLE
{.name = "rocksdb",
.config = "type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256,"
"file_size_mb=8,file_size_mult=2"},
#endif
};
return kConfigs;
}
} // namespace xrpl::NodeStore

View File

@@ -312,6 +312,25 @@ computeInterestAndFeeParts(
return std::make_pair(interest - fee, fee);
}
/* Rounds a raw (unrounded) interest amount to the loan's scale, then splits
* the rounded amount into net interest (to the vault) and management fee (to
* the broker).
*
* This is the common "round then split" step shared by late payment, full
* payment, and overpayment interest calculations.
*/
std::pair<Number, Number>
roundAndSplitInterest(
Asset const& asset,
Number const& rawInterest,
TenthBips16 managementFeeRate,
std::int32_t loanScale,
Number::RoundingMode mode = Number::getround())
{
auto const interest = roundToAsset(asset, rawInterest, loanScale, mode);
return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}
/* Calculates penalty interest accrued on overdue payments.
* Returns 0 if payment is not late.
*
@@ -387,22 +406,18 @@ loanAccruedInterest(
*
* This is the core function that updates the Loan ledger object fields based on
* a computed payment.
* The function is templated to work with both direct Number/uint32_t values
* (for testing/simulation) and ValueProxy types (for actual ledger updates).
*/
template <class NumberProxy, class UInt32Proxy, class UInt32OptionalProxy>
LoanPaymentParts
doPayment(
ExtendedPaymentComponents const& payment,
NumberProxy& totalValueOutstandingProxy,
NumberProxy& principalOutstandingProxy,
NumberProxy& managementFeeOutstandingProxy,
UInt32Proxy& paymentRemainingProxy,
UInt32Proxy& prevPaymentDateProxy,
UInt32OptionalProxy& nextDueDateProxy,
std::uint32_t paymentInterval)
doPayment(ExtendedPaymentComponents const& payment, SLE::ref loan)
{
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate);
auto nextDueDateProxy = loan->at(sfNextPaymentDueDate);
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
XRPL_ASSERT_PARTS(nextDueDateProxy, "xrpl::detail::doPayment", "Next due date proxy set");
if (payment.specialCase == PaymentSpecialCase::Final)
@@ -470,16 +485,12 @@ doPayment(
// Principal can never exceed total value (principal is part of total value)
XRPL_ASSERT_PARTS(
// Use an explicit cast because the template parameter can be
// ValueProxy<Number> or Number
static_cast<Number>(principalOutstandingProxy) <=
static_cast<Number>(totalValueOutstandingProxy),
"xrpl::detail::doPayment",
"principal does not exceed total");
XRPL_ASSERT_PARTS(
// Use an explicit cast because the template parameter can be
// ValueProxy<Number> or Number
static_cast<Number>(managementFeeOutstandingProxy) >= beast::kZero,
"xrpl::detail::doPayment",
"fee outstanding stays valid");
@@ -717,22 +728,23 @@ tryOverpayment(
* overpayment would leave the loan in an invalid state, we can reject it
* gracefully without corrupting the ledger data.
*/
template <class NumberProxy>
std::expected<LoanPaymentParts, TER>
doOverpayment(
Rules const& rules,
Asset const& asset,
std::int32_t loanScale,
ExtendedPaymentComponents const& overpaymentComponents,
NumberProxy& totalValueOutstandingProxy,
NumberProxy& principalOutstandingProxy,
NumberProxy& managementFeeOutstandingProxy,
NumberProxy& periodicPaymentProxy,
SLE::ref loan,
Number const& periodicRate,
std::uint32_t const paymentRemaining,
TenthBips16 const managementFeeRate,
beast::Journal j)
{
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
auto const paymentsRemaining = loan->at(sfPaymentRemaining);
auto const loanState = constructLoanState(
totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy);
auto const periodicPayment = periodicPaymentProxy;
@@ -744,7 +756,7 @@ doOverpayment(
<< ", interestPart: " << overpaymentComponents.trackedInterestPart()
<< ", untrackedInterest: " << overpaymentComponents.untrackedInterest
<< ", totalDue: " << overpaymentComponents.totalDue
<< ", payments remaining :" << paymentRemaining;
<< ", payments remaining :" << paymentsRemaining;
// Attempt to re-amortize the loan with the overpayment applied.
// This modifies the temporary copies, leaving the proxies unchanged.
@@ -756,7 +768,7 @@ doOverpayment(
loanState,
periodicPayment,
periodicRate,
paymentRemaining,
paymentsRemaining,
managementFeeRate,
j);
if (!ret)
@@ -864,16 +876,15 @@ std::expected<ExtendedPaymentComponents, TER>
computeLatePayment(
Asset const& asset,
ApplyView const& view,
Number const& principalOutstanding,
std::int32_t nextDueDate,
SLE::const_ref loan,
ExtendedPaymentComponents const& periodic,
TenthBips32 lateInterestRate,
std::int32_t loanScale,
Number const& latePaymentFee,
STAmount const& amount,
TenthBips16 managementFeeRate,
beast::Journal j)
{
std::int32_t const nextDueDate = loan->at(sfNextPaymentDueDate);
std::int32_t const loanScale = loan->at(sfLoanScale);
// Check if the due date has passed. If not, reject the payment as
// being too soon
if (!hasExpired(view, nextDueDate))
@@ -881,15 +892,15 @@ computeLatePayment(
// Calculate the penalty interest based on how long the payment is overdue.
auto const latePaymentInterest = loanLatePaymentInterest(
principalOutstanding, lateInterestRate, view.parentCloseTime(), nextDueDate);
loan->at(sfPrincipalOutstanding),
TenthBips32{loan->at(sfLateInterestRate)},
view.parentCloseTime(),
nextDueDate);
// Round the late interest and split it between the vault (net interest)
// and the broker (management fee portion). This lambda ensures we
// round before splitting to maintain precision.
auto const [roundedLateInterest, roundedLateManagementFee] = [&]() {
auto const interest = roundToAsset(asset, latePaymentInterest, loanScale);
return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}();
// and the broker (management fee portion).
auto const [roundedLateInterest, roundedLateManagementFee] =
roundAndSplitInterest(asset, latePaymentInterest, managementFeeRate, loanScale);
XRPL_ASSERT(roundedLateInterest >= 0, "xrpl::detail::computeLatePayment : valid late interest");
XRPL_ASSERT_PARTS(
@@ -908,7 +919,7 @@ computeLatePayment(
// 1. Regular service fee (from periodic.untrackedManagementFee)
// 2. Late payment fee (fixed penalty)
// 3. Management fee portion of late interest
periodic.untrackedManagementFee + latePaymentFee + roundedLateManagementFee,
periodic.untrackedManagementFee + loan->at(sfLatePaymentFee) + roundedLateManagementFee,
// Untracked interest includes:
// 1. Any untracked interest from the regular payment (usually 0)
@@ -958,22 +969,15 @@ std::expected<ExtendedPaymentComponents, TER>
computeFullPayment(
Asset const& asset,
ApplyView& view,
Number const& principalOutstanding,
Number const& managementFeeOutstanding,
Number const& periodicPayment,
std::uint32_t paymentRemaining,
std::uint32_t prevPaymentDate,
std::uint32_t const startDate,
std::uint32_t const paymentInterval,
TenthBips32 const closeInterestRate,
std::int32_t loanScale,
Number const& totalInterestOutstanding,
SLE::const_ref loan,
Number const& periodicRate,
Number const& closePaymentFee,
STAmount const& amount,
TenthBips16 managementFeeRate,
beast::Journal j)
{
std::uint32_t const paymentRemaining = loan->at(sfPaymentRemaining);
std::int32_t const loanScale = loan->at(sfLoanScale);
// Full payment must be made before the final scheduled payment.
if (paymentRemaining <= 1)
{
@@ -986,7 +990,7 @@ computeFullPayment(
// This theoretical (unrounded) value is used to compute interest and
// penalties accurately.
Number const theoreticalPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
view.rules(), periodicPayment, periodicRate, paymentRemaining);
view.rules(), loan->at(sfPeriodicPayment), periodicRate, paymentRemaining);
// Full payment interest includes both accrued interest (time since last
// payment) and prepayment penalty (for closing early).
@@ -994,18 +998,21 @@ computeFullPayment(
theoreticalPrincipalOutstanding,
periodicRate,
view.parentCloseTime(),
paymentInterval,
prevPaymentDate,
startDate,
closeInterestRate);
loan->at(sfPaymentInterval),
loan->at(sfPreviousPaymentDueDate),
loan->at(sfStartDate),
TenthBips32{loan->at(sfCloseInterestRate)});
// Split the full payment interest into net interest (to vault) and
// management fee (to broker), applying proper rounding.
auto const [roundedFullInterest, roundedFullManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, fullPaymentInterest, loanScale, Number::RoundingMode::Downward);
return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}();
// Split the full payment interest into net interest (to vault) and management fee (to broker),
// applying proper rounding.
auto const [roundedFullInterest, roundedFullManagementFee] = roundAndSplitInterest(
asset, fullPaymentInterest, managementFeeRate, loanScale, Number::RoundingMode::Downward);
LoanState const loanState = constructLoanState(loan);
Number const principalOutstanding = loanState.principalOutstanding;
Number const managementFeeOutstanding = loanState.managementFeeDue;
Number const totalInterestOutstanding = loanState.interestDue;
Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
ExtendedPaymentComponents const full{
PaymentComponents{
@@ -1046,8 +1053,7 @@ computeFullPayment(
"xrpl::detail::computeFullPayment",
"total due is rounded");
JLOG(j.trace()) << "computeFullPayment result: periodicPayment: " << periodicPayment
<< ", periodicRate: " << periodicRate
JLOG(j.trace()) << "computeFullPayment result: periodicRate: " << periodicRate
<< ", paymentRemaining: " << paymentRemaining
<< ", theoreticalPrincipalOutstanding: " << theoreticalPrincipalOutstanding
<< ", fullPaymentInterest: " << fullPaymentInterest
@@ -1298,6 +1304,34 @@ computePaymentComponents(
};
}
/* Thin overload of computePaymentComponents() that unwraps the tracked
* fields directly from the Loan ledger object. `periodicRate` is derived
* rather than stored, and `managementFeeRate` comes from the LoanBroker, not
* the Loan, so both remain explicit parameters. Kept separate from the
* value-based overload above, which is exercised directly by unit tests
* against simulated (non-ledger) loan states.
*/
PaymentComponents
computePaymentComponents(
Rules const& rules,
Asset const& asset,
SLE::ref loan,
Number const& periodicRate,
TenthBips16 managementFeeRate)
{
return computePaymentComponents(
rules,
asset,
loan->at(sfLoanScale),
loan->at(sfTotalValueOutstanding),
loan->at(sfPrincipalOutstanding),
loan->at(sfManagementFeeOutstanding),
loan->at(sfPeriodicPayment),
periodicRate,
loan->at(sfPaymentRemaining),
managementFeeRate);
}
/* Computes payment components for an overpayment scenario.
*
* An overpayment occurs when a borrower pays more than the scheduled periodic
@@ -1342,11 +1376,12 @@ computeOverpaymentComponents(
// This interest doesn't follow the normal amortization schedule - it's
// a one-time charge for paying early.
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, tenthBipsOfValue(overpayment, overpaymentInterestRate), loanScale);
return detail::computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}();
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
roundAndSplitInterest(
asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate),
managementFeeRate,
loanScale);
auto const result = detail::ExtendedPaymentComponents{
// Build the payment components, after fees and penalty
@@ -1373,6 +1408,265 @@ computeOverpaymentComponents(
return result;
}
/* Derives the two rate values every make*Payment() helper needs: the
* broker's management fee rate, and the loan's periodic (per-payment-period)
* interest rate.
*/
std::pair<TenthBips16, Number>
loanRatesFor(SLE::const_ref loan, SLE::const_ref brokerSle)
{
TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
TenthBips32 const interestRate{loan->at(sfInterestRate)};
Number const periodicRate = loanPeriodicRate(interestRate, loan->at(sfPaymentInterval));
XRPL_ASSERT(interestRate == 0 || periodicRate > 0, "xrpl::detail::loanRatesFor : valid rate");
return {managementFeeRate, periodicRate};
}
/* Handles a full (early payoff) payment. Implements the "full payment"
* branch of the make_payment function from the XLS-66 spec, Section
* 3.2.4.4.
*/
std::expected<LoanPaymentParts, TER>
makeFullPayment(
Asset const& asset,
ApplyView& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
beast::Journal j)
{
auto const [managementFeeRate, periodicRate] = loanRatesFor(loan, brokerSle);
auto const fullPaymentComponents =
computeFullPayment(asset, view, loan, periodicRate, amount, managementFeeRate, j);
// computeFullPayment only ever fails with a genuine error TER (never
// tesSUCCESS), so there is no separate "no-op" outcome to handle here.
if (fullPaymentComponents.has_value())
return doPayment(*fullPaymentComponents, loan);
return std::unexpected(fullPaymentComponents.error());
}
/* Handles a late payment (past due date, with the late-payment flag set).
* Implements the "late payment" branch of the make_payment function from
* the XLS-66 spec, Section 3.2.4.4.
*/
std::expected<LoanPaymentParts, TER>
makeLatePayment(
Asset const& asset,
ApplyView const& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
beast::Journal j)
{
auto const [managementFeeRate, periodicRate] = loanRatesFor(loan, brokerSle);
Number const serviceFee = loan->at(sfLoanServiceFee);
ExtendedPaymentComponents const periodic{
computePaymentComponents(view.rules(), asset, loan, periodicRate, managementFeeRate),
serviceFee};
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::detail::makeLatePayment",
"regular payment valid principal");
auto const latePaymentComponents =
computeLatePayment(asset, view, loan, periodic, amount, managementFeeRate, j);
// computeLatePayment only ever fails with a genuine error TER (never
// tesSUCCESS), so there is no separate "no-op" outcome to handle here.
if (latePaymentComponents.has_value())
return doPayment(*latePaymentComponents, loan);
return std::unexpected(latePaymentComponents.error());
}
/* Handles regular scheduled payments, including an optional overpayment tail.
* Implements the "regular" and "overpayment" branches of the make_payment
* function from the XLS-66 spec, Section 3.2.4.4.
*/
std::expected<LoanPaymentParts, TER>
makeRegularPayment(
Asset const& asset,
ApplyView const& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
LoanPaymentType const paymentType,
beast::Journal j)
{
using namespace Lending;
XRPL_ASSERT_PARTS(
paymentType == LoanPaymentType::Regular || paymentType == LoanPaymentType::Overpayment,
"xrpl::detail::makeRegularPayment",
"regular payment type");
auto const [managementFeeRate, periodicRate] = loanRatesFor(loan, brokerSle);
std::int32_t const loanScale = loan->at(sfLoanScale);
Number const serviceFee = loan->at(sfLoanServiceFee);
ExtendedPaymentComponents periodic{
computePaymentComponents(view.rules(), asset, loan, periodicRate, managementFeeRate),
serviceFee};
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::detail::makeRegularPayment",
"regular payment valid principal");
// Keep a running total of the actual parts paid
LoanPaymentParts totalParts;
Number totalPaid = kNumZero;
std::size_t numPayments = 0;
// Cached here (rather than re-looking up loan->at(sfPaymentRemaining) at each use) since it's
// read multiple times below. It's a write-through proxy, so it still reflects doPayment's
// mutations each iteration.
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
while ((amount >= (totalPaid + periodic.totalDue)) && paymentRemainingProxy > 0 &&
numPayments < kLoanMaximumPaymentsPerTransaction)
{
// Try to make more payments
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::detail::makeRegularPayment",
"payment pays non-negative principal");
totalPaid += periodic.totalDue;
totalParts += doPayment(periodic, loan);
++numPayments;
XRPL_ASSERT_PARTS(
(periodic.specialCase == PaymentSpecialCase::Final) == (paymentRemainingProxy == 0),
"xrpl::detail::makeRegularPayment",
"final payment is the final payment");
// Don't compute the next payment if this was the last payment
if (periodic.specialCase == PaymentSpecialCase::Final)
break;
periodic = ExtendedPaymentComponents{
computePaymentComponents(view.rules(), asset, loan, periodicRate, managementFeeRate),
serviceFee};
}
if (numPayments == 0)
{
JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: " << periodic.totalDue
<< ", paid: " << amount;
return std::unexpected(tecINSUFFICIENT_PAYMENT);
}
XRPL_ASSERT_PARTS(
totalParts.principalPaid + totalParts.interestPaid + totalParts.feePaid == totalPaid,
"xrpl::detail::makeRegularPayment",
"payment parts add up");
XRPL_ASSERT_PARTS(
totalParts.valueChange == 0, "xrpl::detail::makeRegularPayment", "no value change");
// -------------------------------------------------------------
// overpayment handling
//
// If the "fixCleanup3_1_3" amendment is enabled, truncate "amount",
// at the loan scale. If the raw value is used, the overpayment
// amount could be meaningless dust. Trying to process such a small
// amount will, at best, waste time when all the result values round
// to zero. At worst, it can cause logical errors with tiny amounts
// of interest that don't add up correctly.
auto const roundedAmount = view.rules().enabled(fixCleanup3_1_3)
? roundToAsset(asset, amount, loanScale, Number::RoundingMode::TowardsZero)
: amount;
bool const overpaymentSupported =
paymentType == LoanPaymentType::Overpayment && loan->isFlag(lsfLoanOverpayment);
bool const overpaymentAllowed = //
paymentRemainingProxy > 0 && //
totalPaid < roundedAmount && //
numPayments < kLoanMaximumPaymentsPerTransaction;
if (overpaymentSupported && overpaymentAllowed)
{
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)};
// It shouldn't be possible for the overpayment to be greater than
// totalValueOutstanding, because that would have been processed as
// another normal payment. But cap it just in case.
Number const overpaymentRaw =
std::min(roundedAmount - totalPaid, *loan->at(sfTotalValueOutstanding));
bool const fixEnabled = view.rules().enabled(fixCleanup3_2_0);
Number const overpayment = fixEnabled
? roundToAsset(asset, overpaymentRaw, loanScale, Number::RoundingMode::Downward)
: overpaymentRaw;
// Post-amendment, the rounded overpayment can be zero; pre-amendment
// it's always positive given the surrounding guards.
if (!fixEnabled || overpayment > 0)
{
ExtendedPaymentComponents const overpaymentComponents = computeOverpaymentComponents(
view.rules(),
asset,
loanScale,
overpayment,
overpaymentInterestRate,
overpaymentFeeRate,
managementFeeRate);
// Don't process an overpayment if the whole amount (or more!)
// gets eaten by fees and interest.
if (overpaymentComponents.trackedPrincipalDelta > 0)
{
XRPL_ASSERT_PARTS(
overpaymentComponents.untrackedInterest >= beast::kZero,
"xrpl::detail::makeRegularPayment",
"overpayment penalty did not reduce value of loan");
if (auto const overResult = doOverpayment(
view.rules(),
asset,
loanScale,
overpaymentComponents,
loan,
periodicRate,
managementFeeRate,
j))
{
totalParts += *overResult;
}
else if (overResult.error())
{
// error() will be the TER returned if a payment is not
// made. It will only evaluate to true if it's unsuccessful.
// Otherwise, tesSUCCESS means nothing was done, so
// continue.
return std::unexpected(overResult.error());
}
}
}
}
// Check the final results are rounded, to double-check that the
// intermediate steps were rounded.
XRPL_ASSERT(
isRounded(asset, totalParts.principalPaid, loanScale) &&
totalParts.principalPaid >= beast::kZero,
"xrpl::detail::makeRegularPayment : total principal paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.interestPaid, loanScale) &&
totalParts.interestPaid >= beast::kZero,
"xrpl::detail::makeRegularPayment : total interest paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.valueChange, loanScale),
"xrpl::detail::makeRegularPayment : loan value change is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.feePaid, loanScale) && totalParts.feePaid >= beast::kZero,
"xrpl::detail::makeRegularPayment : fee paid is valid");
return totalParts;
}
} // namespace detail
detail::LoanStateDeltas
@@ -1632,8 +1926,10 @@ constructLoanState(
}
LoanState
constructRoundedLoanState(SLE::const_ref loan)
constructLoanState(SLE::const_ref loan)
{
XRPL_ASSERT(loan && loan->getType() == ltLOAN, "xrpl::constructLoanState : valid loan SLE");
return constructLoanState(
loan->at(sfTotalValueOutstanding),
loan->at(sfPrincipalOutstanding),
@@ -1790,12 +2086,7 @@ loanMakePayment(
LoanPaymentType const paymentType,
beast::Journal j)
{
using namespace Lending;
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0)
if (loan->at(sfPaymentRemaining) == 0 || loan->at(sfPrincipalOutstanding) == 0)
{
// Loan complete this is already checked in LoanPay::preclaim()
// LCOV_EXCL_START
@@ -1804,9 +2095,6 @@ loanMakePayment(
// LCOV_EXCL_STOP
}
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
// Next payment due date must be set unless the loan is complete
auto nextDueDateProxy = loan->at(sfNextPaymentDueDate);
if (*nextDueDateProxy == 0)
@@ -1815,26 +2103,8 @@ loanMakePayment(
return std::unexpected(tecINTERNAL);
}
std::int32_t const loanScale = loan->at(sfLoanScale);
TenthBips32 const interestRate{loan->at(sfInterestRate)};
Number const serviceFee = loan->at(sfLoanServiceFee);
TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
Number const periodicPayment = loan->at(sfPeriodicPayment);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate);
std::uint32_t const startDate = loan->at(sfStartDate);
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
// Compute the periodic rate that will be used for calculations
// throughout
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
XRPL_ASSERT(interestRate == 0 || periodicRate > 0, "xrpl::loanMakePayment : valid rate");
XRPL_ASSERT(*totalValueOutstandingProxy > 0, "xrpl::loanMakePayment : valid total value");
XRPL_ASSERT(
*loan->at(sfTotalValueOutstanding) > 0, "xrpl::loanMakePayment : valid total value");
view.update(loan);
@@ -1844,311 +2114,29 @@ loanMakePayment(
{
// If the payment is late, and the late flag was not set, it's not
// valid
JLOG(j.warn()) << "Loan payment is overdue. Use the tfLoanLatePayment "
"transaction "
"flag to make a late payment. Loan was created on "
<< startDate << ", prev payment due date is " << prevPaymentDateProxy
<< ", next payment due date is " << nextDueDateProxy << ", ledger time is "
JLOG(j.warn()) << "Loan payment is overdue. Use the tfLoanLatePayment transaction flag to "
"make a late payment. Loan was created on "
<< loan->at(sfStartDate) << ", prev payment due date is "
<< loan->at(sfPreviousPaymentDueDate) << ", next payment due date is "
<< nextDueDateProxy << ", ledger time is "
<< view.parentCloseTime().time_since_epoch().count();
return std::unexpected(tecEXPIRED);
}
// -------------------------------------------------------------
// full payment handling
if (paymentType == LoanPaymentType::Full)
switch (paymentType)
{
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
LoanState const roundedLoanState = constructLoanState(
totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy);
auto const fullPaymentComponents = detail::computeFullPayment(
asset,
view,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPayment,
paymentRemainingProxy,
prevPaymentDateProxy,
startDate,
paymentInterval,
closeInterestRate,
loanScale,
roundedLoanState.interestDue,
periodicRate,
closePaymentFee,
amount,
managementFeeRate,
j);
if (fullPaymentComponents.has_value())
{
return doPayment(
*fullPaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
}
if (fullPaymentComponents.error())
{
// error() will be the TER returned if a payment is not made. It
// will only evaluate to true if it's unsuccessful. Otherwise,
// tesSUCCESS means nothing was done, so continue.
return std::unexpected(fullPaymentComponents.error());
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::loanMakePayment : invalid full payment result");
JLOG(j.error()) << "Full payment computation failed unexpectedly.";
return std::unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
case LoanPaymentType::Full:
return detail::makeFullPayment(asset, view, loan, brokerSle, amount, j);
case LoanPaymentType::Late:
return detail::makeLatePayment(asset, view, loan, brokerSle, amount, j);
case LoanPaymentType::Regular:
case LoanPaymentType::Overpayment:
return detail::makeRegularPayment(asset, view, loan, brokerSle, amount, paymentType, j);
}
// -------------------------------------------------------------
// compute the periodic payment info that will be needed whether the
// payment is late or regular
detail::ExtendedPaymentComponents periodic{
detail::computePaymentComponents(
view.rules(),
asset,
loanScale,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPayment,
periodicRate,
paymentRemainingProxy,
managementFeeRate),
serviceFee};
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::loanMakePayment",
"regular payment valid principal");
// -------------------------------------------------------------
// late payment handling
if (paymentType == LoanPaymentType::Late)
{
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
Number const latePaymentFee = loan->at(sfLatePaymentFee);
auto const latePaymentComponents = detail::computeLatePayment(
asset,
view,
principalOutstandingProxy,
nextDueDateProxy,
periodic,
lateInterestRate,
loanScale,
latePaymentFee,
amount,
managementFeeRate,
j);
if (latePaymentComponents.has_value())
{
return doPayment(
*latePaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
}
if (latePaymentComponents.error())
{
// error() will be the TER returned if a payment is not made. It
// will only evaluate to true if it's unsuccessful.
return std::unexpected(latePaymentComponents.error());
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::loanMakePayment : invalid late payment result");
JLOG(j.error()) << "Late payment computation failed unexpectedly.";
return std::unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
}
// -------------------------------------------------------------
// regular periodic payment handling
XRPL_ASSERT_PARTS(
paymentType == LoanPaymentType::Regular || paymentType == LoanPaymentType::Overpayment,
"xrpl::loanMakePayment",
"regular payment type");
// Keep a running total of the actual parts paid
LoanPaymentParts totalParts;
Number totalPaid;
std::size_t numPayments = 0;
while ((amount >= (totalPaid + periodic.totalDue)) && paymentRemainingProxy > 0 &&
numPayments < kLoanMaximumPaymentsPerTransaction)
{
// Try to make more payments
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::loanMakePayment",
"payment pays non-negative principal");
totalPaid += periodic.totalDue;
totalParts += detail::doPayment(
periodic,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
++numPayments;
XRPL_ASSERT_PARTS(
(periodic.specialCase == detail::PaymentSpecialCase::Final) ==
(paymentRemainingProxy == 0),
"xrpl::loanMakePayment",
"final payment is the final payment");
// Don't compute the next payment if this was the last payment
if (periodic.specialCase == detail::PaymentSpecialCase::Final)
break;
periodic = detail::ExtendedPaymentComponents{
detail::computePaymentComponents(
view.rules(),
asset,
loanScale,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPayment,
periodicRate,
paymentRemainingProxy,
managementFeeRate),
serviceFee};
}
if (numPayments == 0)
{
JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: " << periodic.totalDue
<< ", paid: " << amount;
return std::unexpected(tecINSUFFICIENT_PAYMENT);
}
XRPL_ASSERT_PARTS(
totalParts.principalPaid + totalParts.interestPaid + totalParts.feePaid == totalPaid,
"xrpl::loanMakePayment",
"payment parts add up");
XRPL_ASSERT_PARTS(totalParts.valueChange == 0, "xrpl::loanMakePayment", "no value change");
// -------------------------------------------------------------
// overpayment handling
//
// If the "fixCleanup3_1_3" amendment is enabled, truncate "amount",
// at the loan scale. If the raw value is used, the overpayment
// amount could be meaningless dust. Trying to process such a small
// amount will, at best, waste time when all the result values round
// to zero. At worst, it can cause logical errors with tiny amounts
// of interest that don't add up correctly.
auto const roundedAmount = view.rules().enabled(fixCleanup3_1_3)
? roundToAsset(asset, amount, loanScale, Number::RoundingMode::TowardsZero)
: amount;
if (paymentType == LoanPaymentType::Overpayment && loan->isFlag(lsfLoanOverpayment) &&
paymentRemainingProxy > 0 && totalPaid < roundedAmount &&
numPayments < kLoanMaximumPaymentsPerTransaction)
{
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)};
// It shouldn't be possible for the overpayment to be greater than
// totalValueOutstanding, because that would have been processed as
// another normal payment. But cap it just in case.
Number const overpaymentRaw =
std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
bool const fixEnabled = view.rules().enabled(fixCleanup3_2_0);
Number const overpayment = fixEnabled
? roundToAsset(asset, overpaymentRaw, loanScale, Number::RoundingMode::Downward)
: overpaymentRaw;
// Post-amendment, the rounded overpayment can be zero; pre-amendment
// it's always positive given the surrounding guards.
if (!fixEnabled || overpayment > 0)
{
detail::ExtendedPaymentComponents const overpaymentComponents =
detail::computeOverpaymentComponents(
view.rules(),
asset,
loanScale,
overpayment,
overpaymentInterestRate,
overpaymentFeeRate,
managementFeeRate);
// Don't process an overpayment if the whole amount (or more!)
// gets eaten by fees and interest.
if (overpaymentComponents.trackedPrincipalDelta > 0)
{
XRPL_ASSERT_PARTS(
overpaymentComponents.untrackedInterest >= beast::kZero,
"xrpl::loanMakePayment",
"overpayment penalty did not reduce value of loan");
// Can't just use `periodicPayment` here, because it might
// change
auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
if (auto const overResult = detail::doOverpayment(
view.rules(),
asset,
loanScale,
overpaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPaymentProxy,
periodicRate,
paymentRemainingProxy,
managementFeeRate,
j))
{
totalParts += *overResult;
}
else if (overResult.error())
{
// error() will be the TER returned if a payment is not
// made. It will only evaluate to true if it's unsuccessful.
// Otherwise, tesSUCCESS means nothing was done, so
// continue.
return std::unexpected(overResult.error());
}
}
}
}
// Check the final results are rounded, to double-check that the
// intermediate steps were rounded.
XRPL_ASSERT(
isRounded(asset, totalParts.principalPaid, loanScale) &&
totalParts.principalPaid >= beast::kZero,
"xrpl::loanMakePayment : total principal paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.interestPaid, loanScale) &&
totalParts.interestPaid >= beast::kZero,
"xrpl::loanMakePayment : total interest paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.valueChange, loanScale),
"xrpl::loanMakePayment : loan value change is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.feePaid, loanScale) && totalParts.feePaid >= beast::kZero,
"xrpl::loanMakePayment : fee paid is valid");
return totalParts;
// LCOV_EXCL_START
UNREACHABLE("xrpl::loanMakePayment : invalid payment type");
return std::unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
}
} // namespace xrpl

View File

@@ -13,6 +13,7 @@
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <atomic>
#include <cstdint>
#include <exception>
#include <functional>
@@ -52,6 +53,7 @@ DatabaseRotatingImp::rotate(
// callback finishes. Only then will the archive directory be
// deleted.
std::shared_ptr<NodeStore::Backend> oldArchiveBackend;
std::uint64_t copyForwards = 0;
{
std::scoped_lock const lock(mutex_);
@@ -62,11 +64,28 @@ DatabaseRotatingImp::rotate(
newArchiveBackendName = archiveBackend_->getName();
writableBackend_ = std::move(newBackend);
copyForwards = copyForwardCount_.exchange(0, std::memory_order_relaxed);
}
if (copyForwards > 0)
{
JLOG(j_.warn()) << "Rotating: copied forward " << copyForwards
<< " archive-served reads into the writable backend "
"during the rotation window";
}
f(newWritableBackendName, newArchiveBackendName);
}
void
DatabaseRotatingImp::setRotationInFlight(bool inFlight)
{
rotationInFlight_.store(inFlight, std::memory_order_release);
JLOG(j_.debug()) << "Rotating: copy-forward on archive reads "
<< (inFlight ? "enabled" : "disabled");
}
std::string
DatabaseRotatingImp::getName() const
{
@@ -177,9 +196,18 @@ DatabaseRotatingImp::fetchNodeObject(
writable = writableBackend_;
}
// Update writable backend with data from the archive backend
if (duplicate)
// Update writable backend with data from the archive backend.
// While a rotation is in flight, ordinary (duplicate == false)
// reads served by the archive are copied forward too: the
// archive is about to be deleted, and a body canonicalized
// into the cache after the freshen getKeys() snapshot would
// otherwise survive only in RAM once the archive is dropped.
if (duplicate || rotationInFlight_.load(std::memory_order_acquire))
{
if (!duplicate)
copyForwardCount_.fetch_add(1, std::memory_order_relaxed);
writable->store(nodeObject);
}
}
}

View File

@@ -23,7 +23,7 @@ namespace {
//------------------------------------------------------------------------------
// clang-format off
// NOLINTNEXTLINE(readability-identifier-naming)
char const* const versionString = "3.3.0-b1"
char const* const versionString = "3.3.0-rc1"
// clang-format on
;

View File

@@ -633,20 +633,6 @@ STObject::getAccountID(SField const& field) const
return getFieldByValue<STAccount>(field);
}
AccountID
STObject::getInitiator() const
{
// If sfDelegate is present, the delegate account is the initiator
// note: if a delegate is specified, its authorization to act on behalf of the account is
// enforced in `Transactor::invokeCheckPermission`
// cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`)
if (isFieldPresent(sfDelegate))
return getAccountID(sfDelegate);
// Default initiator
return getAccountID(sfAccount);
}
Blob
STObject::getFieldVL(SField const& field) const
{

View File

@@ -72,7 +72,7 @@ STTx::STTx(STObject&& object)
{
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // may throw
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxnIds();
buildBatchTxns();
}
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
@@ -89,7 +89,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // May throw
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxnIds();
buildBatchTxns();
}
STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfTransaction)
@@ -110,7 +110,7 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfT
logicError("Transaction type was mutated during assembly");
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxnIds();
buildBatchTxns();
}
STBase*
@@ -279,12 +279,9 @@ STTx::checkSign(Rules const& rules) const
return std::unexpected("Sponsor: " + ret.error());
}
// Verify the batch signer signatures here too, so they are cached with the
// rest of signature checking (checkValidity / SF_SIGGOOD) and stay out of
// the transaction engine. Gated on a batch (batchTxnIds_ seated) that
// actually carries signers; a batch whose inners are all from the outer
// account has no sfBatchSigners and needs no signer crypto.
if (batchTxnIds_ && isFieldPresent(sfBatchSigners))
// Verify batch signer signatures here so they are cached with the rest
// of signature checking.
if (isFieldPresent(sfBatchSigners))
{
if (auto const ret = checkBatchSign(rules); !ret)
return ret;
@@ -307,11 +304,28 @@ STTx::checkBatchSign(Rules const& rules) const
if (!isFieldPresent(sfBatchSigners))
return std::unexpected("Missing BatchSigners field."); // LCOV_EXCL_LINE
STArray const& signers{getFieldArray(sfBatchSigners)};
// Bound signature verification to the protocol cap. This runs in
// checkValidity (via checkSign) at relay / submit time, BEFORE preflight
// and passesLocalChecks enforce the cap. Without this guard a malicious
// peer could put an oversized sfBatchSigners array in a 1 MB blob and
// force one signature verification per entry before any of those checks
// (or the fee charge) runs.
if (signers.size() > kMaxBatchSigners)
return std::unexpected("BatchSigners array exceeds max entries.");
// Defensive.
if (!batchTxns_)
{
// LCOV_EXCL_START
UNREACHABLE("STTx::checkBatchSign : batch transactions not built");
return std::unexpected("Missing inner transactions.");
// LCOV_EXCL_STOP
}
auto const txIds = getBatchTransactionIDs();
for (auto const& signer : signers)
{
Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules)
: checkBatchSingleSign(signer);
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules, txIds)
: checkBatchSingleSign(signer, txIds);
if (!result)
return result;
@@ -441,12 +455,11 @@ STTx::checkSingleSign(STObject const& sigObject) const
}
std::expected<void, std::string>
STTx::checkBatchSingleSign(STObject const& batchSigner) const
STTx::checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction");
Serializer msg;
serializeBatch(
msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
serializeBatch(msg, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
return singleSignHelper(batchSigner, msg.slice());
}
@@ -529,7 +542,10 @@ multiSignHelper(
}
std::expected<void, std::string>
STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const
STTx::checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction");
// We can ease the computational load inside the loop a bit by
@@ -537,8 +553,7 @@ STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const
// with the stuff that stays constant from signature to signature.
auto const batchSignerAccount = batchSigner.getAccountID(sfAccount);
Serializer dataStart;
serializeBatch(
dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
serializeBatch(dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
dataStart.addBitString(batchSignerAccount);
return multiSignHelper(
batchSigner,
@@ -577,38 +592,79 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
}
void
STTx::buildBatchTxnIds()
STTx::buildBatchTxns()
{
// Precondition: the template must have been applied first, so the fields
// (including sfRawTransactions) are canonical before the inner txns are
// hashed. The constructors call this immediately after applying the
// template; isFree() being false confirms a template is set.
XRPL_ASSERT(!isFree(), "STTx::buildBatchTxnIds : template applied");
if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions))
XRPL_ASSERT(!isFree(), "STTx::buildBatchTxns : template applied");
if (getTxnType() != ttBATCH)
return;
// A Batch always seats its inner transactions here, so every downstream
// consumer can rely on them. sfRawTransactions is required by the format
// (applyTemplate rejects a Batch without it); this guards a future change
// that made it optional.
if (!isFieldPresent(sfRawTransactions))
{
// LCOV_EXCL_START
UNREACHABLE("STTx::buildBatchTxns : missing RawTransactions");
Throw<std::runtime_error>("Batch has no RawTransactions.");
// LCOV_EXCL_STOP
}
auto const& raw = getFieldArray(sfRawTransactions);
if (raw.size() > kMaxBatchTxCount)
Throw<std::runtime_error>("Batch has too many inner transactions.");
// Seated for any batch with raw transactions. The count is validated in
// preflight and at the relay boundary, so build every id here; this keeps
// the invariant batchTxnIds_->size() == rawTransactions.size().
auto& ids = batchTxnIds_.emplace();
ids.reserve(raw.size());
// Build and validate each inner as an STTx once. A malformed inner throws;
// a nested batch is rejected before building it (a batch cannot contain a
// batch, and building one would recurse).
auto& txns = batchTxns_.emplace();
txns.reserve(raw.size());
for (STObject const& rb : raw)
ids.push_back(rb.getHash(HashPrefix::TransactionId));
{
if (rb.getFieldU16(sfTransactionType) == ttBATCH)
Throw<std::runtime_error>("Batch inner transaction cannot be a Batch.");
txns.push_back(std::make_shared<STTx const>(STObject{rb}));
}
}
std::vector<uint256> const&
std::vector<uint256>
STTx::getBatchTransactionIDs() const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : batch transaction");
auto const& txns = getBatchTransactions();
std::vector<uint256> ids;
ids.reserve(txns.size());
for (auto const& stx : txns)
ids.push_back(stx->getTransactionID());
return ids;
}
std::vector<std::shared_ptr<STTx const>> const&
STTx::getBatchTransactions() const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactions : batch transaction");
XRPL_ASSERT(batchTxns_.has_value(), "STTx::getBatchTransactions : batch transactions built");
XRPL_ASSERT(
batchTxnIds_.has_value(), "STTx::getBatchTransactionIDs : batch transaction IDs built");
XRPL_ASSERT(
batchTxnIds_->size() == getFieldArray(sfRawTransactions).size(),
"STTx::getBatchTransactionIDs : batch transaction IDs size mismatch");
// NOLINTNEXTLINE(bugprone-unchecked-optional-access): guarded by assert above
return *batchTxnIds_;
batchTxns_->size() == getFieldArray(sfRawTransactions).size(),
"STTx::getBatchTransactions : batch transactions size mismatch");
return *batchTxns_;
}
AccountID
STTx::getInitiator() const
{
// If sfDelegate is present, the delegate account is the initiator
// note: if a delegate is specified, its authorization to act on behalf of the account is
// enforced in `Transactor::invokeCheckPermission`
// cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`)
if (isFieldPresent(sfDelegate))
return getAccountID(sfDelegate);
// Default initiator
return getAccountID(sfAccount);
}
AccountID
@@ -754,86 +810,62 @@ invalidMPTAmountInTx(STObject const& tx)
}
static bool
isBatchRawTransactionOkay(STObject const& st, std::string& reason)
isBatchRawTransactionOkay(STTx const& tx, std::string& reason)
{
if (!st.isFieldPresent(sfRawTransactions))
if (!tx.isFieldPresent(sfRawTransactions))
return true;
// sfRawTransactions only appears on a Batch. passesLocalChecks runs on
// unverified user and peer input, so reject (rather than assert) a non-batch
// transaction that carries it.
if (st.getFieldU16(sfTransactionType) != ttBATCH)
if (tx.getTxnType() != ttBATCH)
{
reason = "Only Batch transactions may contain raw transactions.";
return false;
}
if (st.isFieldPresent(sfBatchSigners) &&
st.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
if (tx.isFieldPresent(sfBatchSigners) &&
tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
{
reason = "Batch Signers array exceeds max entries.";
reason = "BatchSigners array exceeds max entries.";
return false;
}
auto const& rawTxns = st.getFieldArray(sfRawTransactions);
if (rawTxns.size() > kMaxBatchTxCount)
// Inner structure (type, template, no nesting, count) is validated when the
// batch STTx is constructed; here we only run each inner's local checks.
for (auto const& inner : tx.getBatchTransactions())
{
reason = "Raw Transactions array exceeds max entries.";
return false;
}
for (STObject raw : rawTxns)
{
try
{
auto const tt = safeCast<TxType>(raw.getFieldU16(sfTransactionType));
if (tt == ttBATCH)
{
reason = "Raw Transactions may not contain batch transactions.";
return false;
}
raw.applyTemplate(getTxFormat(tt)->getSOTemplate());
// passesLocalChecks recurses back into isBatchRawTransactionOkay,
// but an inner can never be a batch (rejected above), so the
// recursion terminates at depth 1.
if (!passesLocalChecks(raw, reason))
return false;
}
catch (std::exception const& e)
{
reason = e.what();
if (!passesLocalChecks(*inner, reason))
return false;
}
}
return true;
}
bool
passesLocalChecks(STObject const& st, std::string& reason)
passesLocalChecks(STTx const& tx, std::string& reason)
{
if (!isMemoOkay(st, reason))
if (!isMemoOkay(tx, reason))
return false;
if (!isAccountFieldOkay(st))
if (!isAccountFieldOkay(tx))
{
reason = "An account field is invalid.";
return false;
}
if (isPseudoTx(st))
if (isPseudoTx(tx))
{
reason = "Cannot submit pseudo transactions.";
return false;
}
if (invalidMPTAmountInTx(st))
if (invalidMPTAmountInTx(tx))
{
reason = "Amount can not be MPT.";
return false;
}
if (!isBatchRawTransactionOkay(st, reason))
if (!isBatchRawTransactionOkay(tx, reason))
return false;
return true;

View File

@@ -233,7 +233,7 @@ transResults()
}
bool
transResultInfo(TER code, std::string& token, std::string& text)
transResultInfo(TERRaw code, std::string& token, std::string& text)
{
auto& results = transResults();
@@ -248,7 +248,7 @@ transResultInfo(TER code, std::string& token, std::string& text)
}
std::string
transToken(TER code)
transToken(TERRaw code)
{
std::string token;
std::string text;
@@ -257,7 +257,7 @@ transToken(TER code)
}
std::string
transHuman(TER code)
transHuman(TERRaw code)
{
std::string token;
std::string text;

View File

@@ -64,9 +64,7 @@ preflight0(PreflightContext const& ctx, std::uint32_t flagMask)
{
if (isPseudoTx(ctx.tx) && ctx.tx.isFlag(tfInnerBatchTxn))
{
JLOG(ctx.j.warn()) << "Pseudo transactions cannot contain the "
"tfInnerBatchTxn flag.";
return temINVALID_FLAG;
return {temINVALID_FLAG, "Pseudo transactions cannot contain the tfInnerBatchTxn flag."};
}
if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID))
@@ -97,8 +95,7 @@ preflight0(PreflightContext const& ctx, std::uint32_t flagMask)
if (txID == beast::kZero)
{
JLOG(ctx.j.warn()) << "applyTransaction: transaction id may not be zero";
return temINVALID;
return {temINVALID, "applyTransaction: transaction id may not be zero"};
}
if ((ctx.tx.getFlags() & flagMask) != 0u)
@@ -124,8 +121,7 @@ preflightCheckSigningKey(STObject const& sigObject, beast::Journal j)
if (auto const spk = sigObject.getFieldVL(sfSigningPubKey);
!spk.empty() && !publicKeyType(makeSlice(spk)))
{
JLOG(j.debug()) << "preflightCheckSigningKey: invalid signing key";
return temBAD_SIGNATURE;
return {temBAD_SIGNATURE, "preflightCheckSigningKey: invalid signing key"};
}
return tesSUCCESS;
}
@@ -184,13 +180,11 @@ preflight1Sponsor(PreflightContext const& ctx)
if (hasSponsor != hasSponsorFlags)
{
JLOG(ctx.j.debug()) << "preflight1: sponsor and sponsor flags mismatch";
return temINVALID_FLAG;
return {temINVALID_FLAG, "preflight1: sponsor and sponsor flags mismatch"};
}
if (hasSponsorSig && (!hasSponsor || !hasSponsorFlags))
{
JLOG(ctx.j.debug()) << "preflight1: sponsor signature without sponsor definition";
return temMALFORMED;
return {temMALFORMED, "preflight1: sponsor signature without sponsor definition"};
}
if (hasSponsorFlags)
@@ -198,8 +192,7 @@ preflight1Sponsor(PreflightContext const& ctx)
auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags);
if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0)
{
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
return temINVALID_FLAG;
return {temINVALID_FLAG, "preflight1: invalid sponsor flags"};
}
// Reserve sponsorship is only permitted for an explicit allow-list of
@@ -208,17 +201,16 @@ preflight1Sponsor(PreflightContext const& ctx)
{
if (!isReserveSponsorAllowed(ctx.tx.getTxnType()))
{
JLOG(ctx.j.debug())
<< "preflight1: spfSponsorReserve not allowed for this transaction type";
return temINVALID_FLAG;
return {
temINVALID_FLAG,
"preflight1: spfSponsorReserve not allowed for this transaction type"};
}
}
}
if (hasSponsor && ctx.tx.getAccountID(sfSponsor) == ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.debug()) << "preflight1: Sponsor account cannot be the same as the account";
return temMALFORMED;
return {temMALFORMED, "preflight1: Sponsor account cannot be the same as the account"};
}
return tesSUCCESS;
@@ -255,16 +247,14 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
auto const id = ctx.tx.getAccountID(sfAccount);
if (id == beast::kZero)
{
JLOG(ctx.j.warn()) << "preflight1: bad account id";
return temBAD_SRC_ACCOUNT;
return {temBAD_SRC_ACCOUNT, "preflight1: bad account id"};
}
// No point in going any further if the transaction fee is malformed.
auto const fee = ctx.tx.getFieldAmount(sfFee);
if (!fee.native() || fee.negative() || !isLegalAmount(fee.xrp()))
{
JLOG(ctx.j.debug()) << "preflight1: invalid fee";
return temBAD_FEE;
return {temBAD_FEE, "preflight1: invalid fee"};
}
if (auto const ret = detail::preflightCheckSigningKey(ctx.tx, ctx.j))
@@ -525,8 +515,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
if (feePaid == beast::kZero)
return tesSUCCESS;
JLOG(ctx.j.trace()) << "Batch: Fee must be zero.";
return temBAD_FEE; // LCOV_EXCL_LINE
return {temBAD_FEE, "Batch: Fee must be zero."}; // LCOV_EXCL_LINE
}
if (!isLegalAmount(feePaid) || feePaid < beast::kZero)
@@ -715,9 +704,9 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
{
if (tx.isFieldPresent(sfTicketSequence))
{
JLOG(j.trace()) << "applyTransaction: has both a TicketSequence "
"and a non-zero Sequence number";
return temSEQ_AND_TICKET;
return {
temSEQ_AND_TICKET,
"applyTransaction: has both a TicketSequence and a non-zero Sequence number"};
}
if (tSeqProx != aSeq)
{
@@ -817,8 +806,7 @@ Transactor::ticketDelete(
if (!sleTicket)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Ticket disappeared from ledger.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Ticket disappeared from ledger."};
// LCOV_EXCL_STOP
}
@@ -826,8 +814,7 @@ Transactor::ticketDelete(
if (!view.dirRemove(keylet::ownerDir(account), page, ticketIndex, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Ticket from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Ticket from owner."};
// LCOV_EXCL_STOP
}
@@ -837,8 +824,7 @@ Transactor::ticketDelete(
if (!sleAccount)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not find Ticket owner account root.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Could not find Ticket owner account root."};
// LCOV_EXCL_STOP
}
@@ -856,8 +842,7 @@ Transactor::ticketDelete(
else
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "TicketCount field missing from account root.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "TicketCount field missing from account root."};
// LCOV_EXCL_STOP
}
@@ -983,8 +968,9 @@ Transactor::checkSign(
if (!publicKeyType(makeSlice(pkSigner)))
{
JLOG(j.trace()) << "checkSign: signing public key type is unknown";
return tefBAD_AUTH; // FIXME: should be better error!
return {
tefBAD_AUTH,
"checkSign: signing public key type is unknown"}; // FIXME: should be better error!
}
// Look up the account.
@@ -1058,8 +1044,7 @@ Transactor::checkMultiSign(
// If the signer list doesn't exist the account is not multi-signing.
if (!sleAccountSigners)
{
JLOG(j.trace()) << "applyTransaction: Invalid: Not a multi-signing account.";
return tefNOT_MULTI_SIGNING;
return {tefNOT_MULTI_SIGNING, "applyTransaction: Invalid: Not a multi-signing account."};
}
// We have plans to support multiple SignerLists in the future. The
@@ -1095,15 +1080,13 @@ Transactor::checkMultiSign(
{
if (++iter == accountSigners->end())
{
JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account.";
return tefBAD_SIGNATURE;
return {tefBAD_SIGNATURE, "applyTransaction: Invalid SigningAccount.Account."};
}
}
if (iter->account != txSignerAcctID)
{
// The SigningAccount is not in the SignerEntries.
JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account.";
return tefBAD_SIGNATURE;
return {tefBAD_SIGNATURE, "applyTransaction: Invalid SigningAccount.Account."};
}
// We found the SigningAccount in the list of valid signers. Now we
@@ -1115,8 +1098,7 @@ Transactor::checkMultiSign(
// STTx::checkMultiSign
if (!spk.empty() && !publicKeyType(makeSlice(spk)))
{
JLOG(j.trace()) << "checkMultiSign: signing public key type is unknown";
return tefBAD_SIGNATURE;
return {tefBAD_SIGNATURE, "checkMultiSign: signing public key type is unknown"};
}
XRPL_ASSERT(
@@ -1163,8 +1145,8 @@ Transactor::checkMultiSign(
if ((signerAccountFlags & lsfDisableMaster) != 0u)
{
JLOG(j.trace()) << "applyTransaction: Signer:Account lsfDisableMaster.";
return tefMASTER_DISABLED;
return {
tefMASTER_DISABLED, "applyTransaction: Signer:Account lsfDisableMaster."};
}
}
}
@@ -1174,20 +1156,17 @@ Transactor::checkMultiSign(
// Public key must hash to the account's regular key.
if (!sleTxSignerRoot)
{
JLOG(j.trace()) << "applyTransaction: Non-phantom signer "
"lacks account root.";
return tefBAD_SIGNATURE;
return {
tefBAD_SIGNATURE, "applyTransaction: Non-phantom signer lacks account root."};
}
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
{
JLOG(j.trace()) << "applyTransaction: Account lacks RegularKey.";
return tefBAD_SIGNATURE;
return {tefBAD_SIGNATURE, "applyTransaction: Account lacks RegularKey."};
}
if (signingAcctIDFromPubKey != sleTxSignerRoot->getAccountID(sfRegularKey))
{
JLOG(j.trace()) << "applyTransaction: Account doesn't match RegularKey.";
return tefBAD_SIGNATURE;
return {tefBAD_SIGNATURE, "applyTransaction: Account doesn't match RegularKey."};
}
}
// The signer is legitimate. Add their weight toward the quorum.
@@ -1197,8 +1176,7 @@ Transactor::checkMultiSign(
// Cannot perform transaction if quorum is not met.
if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum))
{
JLOG(j.trace()) << "applyTransaction: Signers failed to meet quorum.";
return tefBAD_QUORUM;
return {tefBAD_QUORUM, "applyTransaction: Signers failed to meet quorum."};
}
// Met the quorum. Continue.

View File

@@ -177,9 +177,9 @@ applyBatchTransactions(
int applied = 0;
for (STObject rb : batchTxn.getFieldArray(sfRawTransactions))
for (auto const& stx : batchTxn.getBatchTransactions())
{
auto const result = applyOneTransaction(STTx{std::move(rb)});
auto const result = applyOneTransaction(*stx);
XRPL_ASSERT(
result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)),
"Outer Batch failure, inner transaction should not be applied");

View File

@@ -825,14 +825,12 @@ DirectStepI<TDerived>::check(StrandContext const& ctx) const
// The following checks apply for both payments and offer crossing.
if (!src_ || !dst_)
{
JLOG(j_.debug()) << "DirectStepI: specified bad account.";
return temBAD_PATH;
return {temBAD_PATH, "DirectStepI: specified bad account."};
}
if (src_ == dst_)
{
JLOG(j_.debug()) << "DirectStepI: same src and dst.";
return temBAD_PATH;
return {temBAD_PATH, "DirectStepI: same src and dst."};
}
auto const sleSrc = ctx.view.read(keylet::account(src_));

View File

@@ -823,14 +823,12 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
// The following checks apply for both payments and offer crossing.
if (!src_ || !dst_)
{
JLOG(j_.debug()) << "MPTEndpointStep: specified bad account.";
return temBAD_PATH;
return {temBAD_PATH, "MPTEndpointStep: specified bad account."};
}
if (src_ == dst_)
{
JLOG(j_.debug()) << "MPTEndpointStep: same src and dst.";
return temBAD_PATH;
return {temBAD_PATH, "MPTEndpointStep: same src and dst."};
}
auto const sleSrc = ctx.view.read(keylet::account(src_));
@@ -885,15 +883,13 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
// MPT can only be an endpoint
if (!ctx.isLast && !ctx.isFirst)
{
JLOG(j_.warn()) << "MPTEndpointStep: MPT can only be an endpoint";
return temBAD_PATH;
return {temBAD_PATH, "MPTEndpointStep: MPT can only be an endpoint"};
}
auto const& issuer = mptIssue_.getIssuer();
if ((src_ != issuer && dst_ != issuer) || (src_ == issuer && dst_ == issuer))
{
JLOG(j_.warn()) << "MPTEndpointStep: invalid src/dst";
return temBAD_PATH;
return {temBAD_PATH, "MPTEndpointStep: invalid src/dst"};
}
return static_cast<TDerived const*>(this)->check(ctx, sleSrc);

View File

@@ -338,8 +338,7 @@ XRPEndpointStep<TDerived>::check(StrandContext const& ctx) const
{
if (!acc_)
{
JLOG(j_.debug()) << "XRPEndpointStep: specified bad account.";
return temBAD_PATH;
return {temBAD_PATH, "XRPEndpointStep: specified bad account."};
}
auto sleAcc = ctx.view.read(keylet::account(acc_));

View File

@@ -63,15 +63,13 @@ NotTEC
AccountSet::preflight(PreflightContext const& ctx)
{
auto& tx = ctx.tx;
auto& j = ctx.j;
std::uint32_t const uSetFlag = tx.getFieldU32(sfSetFlag);
std::uint32_t const uClearFlag = tx.getFieldU32(sfClearFlag);
if ((uSetFlag != 0) && (uSetFlag == uClearFlag))
{
JLOG(j.trace()) << "Malformed transaction: Set and clear same flag.";
return temINVALID_FLAG;
return {temINVALID_FLAG, "Malformed transaction: Set and clear same flag."};
}
//
@@ -82,8 +80,7 @@ AccountSet::preflight(PreflightContext const& ctx)
if (bSetRequireAuth && bClearRequireAuth)
{
JLOG(j.trace()) << "Malformed transaction: Contradictory flags set.";
return temINVALID_FLAG;
return {temINVALID_FLAG, "Malformed transaction: Contradictory flags set."};
}
//
@@ -94,8 +91,7 @@ AccountSet::preflight(PreflightContext const& ctx)
if (bSetRequireDest && bClearRequireDest)
{
JLOG(j.trace()) << "Malformed transaction: Contradictory flags set.";
return temINVALID_FLAG;
return {temINVALID_FLAG, "Malformed transaction: Contradictory flags set."};
}
//
@@ -106,8 +102,7 @@ AccountSet::preflight(PreflightContext const& ctx)
if (bSetDisallowXRP && bClearDisallowXRP)
{
JLOG(j.trace()) << "Malformed transaction: Contradictory flags set.";
return temINVALID_FLAG;
return {temINVALID_FLAG, "Malformed transaction: Contradictory flags set."};
}
// TransferRate
@@ -117,14 +112,12 @@ AccountSet::preflight(PreflightContext const& ctx)
if ((uRate != 0u) && (uRate < QUALITY_ONE))
{
JLOG(j.trace()) << "Malformed transaction: Transfer rate too small.";
return temBAD_TRANSFER_RATE;
return {temBAD_TRANSFER_RATE, "Malformed transaction: Transfer rate too small."};
}
if (uRate > 2 * QUALITY_ONE)
{
JLOG(j.trace()) << "Malformed transaction: Transfer rate too large.";
return temBAD_TRANSFER_RATE;
return {temBAD_TRANSFER_RATE, "Malformed transaction: Transfer rate too large."};
}
}
@@ -135,8 +128,7 @@ AccountSet::preflight(PreflightContext const& ctx)
if ((uTickSize != 0u) &&
((uTickSize < Quality::kMinTickSize) || (uTickSize > Quality::kMaxTickSize)))
{
JLOG(j.trace()) << "Malformed transaction: Bad tick size.";
return temBAD_TICK_SIZE;
return {temBAD_TICK_SIZE, "Malformed transaction: Bad tick size."};
}
}
@@ -144,15 +136,13 @@ AccountSet::preflight(PreflightContext const& ctx)
{
if (!mk->empty() && !publicKeyType({mk->data(), mk->size()}))
{
JLOG(j.trace()) << "Invalid message key specified.";
return telBAD_PUBLIC_KEY;
return {telBAD_PUBLIC_KEY, "Invalid message key specified."};
}
}
if (auto const domain = tx[~sfDomain]; domain && domain->size() > kMaxDomainLength)
{
JLOG(j.trace()) << "domain too long";
return telBAD_DOMAIN;
return {telBAD_DOMAIN, "domain too long"};
}
// Configure authorized minting account:
@@ -198,14 +188,12 @@ AccountSet::preclaim(PreclaimContext const& ctx)
{
if (sle->isFlag(lsfNoFreeze))
{
JLOG(ctx.j.trace()) << "Can't set Clawback if NoFreeze is set";
return tecNO_PERMISSION;
return {tecNO_PERMISSION, "Can't set Clawback if NoFreeze is set"};
}
if (!dirIsEmpty(ctx.view, keylet::ownerDir(id)))
{
JLOG(ctx.j.trace()) << "Owner directory not empty.";
return tecOWNERS;
return {tecOWNERS, "Owner directory not empty."};
}
}
else if (uSetFlag == asfNoFreeze)
@@ -213,8 +201,7 @@ AccountSet::preclaim(PreclaimContext const& ctx)
// Cannot set NoFreeze if clawback is enabled
if (sle->isFlag(lsfAllowTrustLineClawback))
{
JLOG(ctx.j.trace()) << "Can't set NoFreeze if clawback is enabled";
return tecNO_PERMISSION;
return {tecNO_PERMISSION, "Can't set NoFreeze if clawback is enabled"};
}
}
@@ -308,8 +295,7 @@ AccountSet::doApply()
{
if (!sigWithMaster)
{
JLOG(j_.trace()) << "Must use master key to disable master key.";
return tecNEED_MASTER_KEY;
return {tecNEED_MASTER_KEY, "Must use master key to disable master key."};
}
if ((!sle->isFieldPresent(sfRegularKey)) && (!view().peek(keylet::signerList(accountID_))))
@@ -349,8 +335,7 @@ AccountSet::doApply()
{
if (!sigWithMaster && !sle->isFlag(lsfDisableMaster))
{
JLOG(j_.trace()) << "Must use master key to set NoFreeze.";
return tecNEED_MASTER_KEY;
return {tecNEED_MASTER_KEY, "Must use master key to set NoFreeze."};
}
JLOG(j_.trace()) << "Set NoFreeze flag";

View File

@@ -89,8 +89,7 @@ SignerListSet::preflight(PreflightContext const& ctx)
if (std::get<3>(result) == Operation::Unknown)
{
// Neither a set nor a destroy. Malformed.
JLOG(ctx.j.trace()) << "Malformed transaction: Invalid signer set list format.";
return temMALFORMED;
return {temMALFORMED, "Malformed transaction: Invalid signer set list format."};
}
if (std::get<3>(result) == Operation::Set)
@@ -208,8 +207,7 @@ removeSignersFromLedger(
if (!view.dirRemove(ownerDirKeylet, hint, signerListKeylet.key, false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete SignerList from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete SignerList from owner."};
// LCOV_EXCL_STOP
}
@@ -249,8 +247,7 @@ SignerListSet::validateQuorumAndSignerEntries(
std::size_t const signerCount = signers.size();
if (signerCount < STTx::kMinMultiSigners || signerCount > STTx::kMaxMultiSigners)
{
JLOG(j.trace()) << "Too many or too few signers in signer list.";
return temMALFORMED;
return {temMALFORMED, "Too many or too few signers in signer list."};
}
}
@@ -261,8 +258,7 @@ SignerListSet::validateQuorumAndSignerEntries(
"signers");
if (std::ranges::adjacent_find(signers) != signers.end())
{
JLOG(j.trace()) << "Duplicate signers in signer list";
return temBAD_SIGNER;
return {temBAD_SIGNER, "Duplicate signers in signer list"};
}
// Make sure no signers reference this account. Also make sure the
@@ -273,24 +269,21 @@ SignerListSet::validateQuorumAndSignerEntries(
std::uint32_t const weight = signer.weight;
if (weight <= 0)
{
JLOG(j.trace()) << "Every signer must have a positive weight.";
return temBAD_WEIGHT;
return {temBAD_WEIGHT, "Every signer must have a positive weight."};
}
allSignersWeight += signer.weight;
if (signer.account == account)
{
JLOG(j.trace()) << "A signer may not self reference account.";
return temBAD_SIGNER;
return {temBAD_SIGNER, "A signer may not self reference account."};
}
// Don't verify that the signer accounts exist. Non-existent accounts
// may be phantom accounts (which are permitted).
}
if ((quorum <= 0) || (allSignersWeight < quorum))
{
JLOG(j.trace()) << "Quorum is unreachable";
return temBAD_QUORUM;
return {temBAD_QUORUM, "Quorum is unreachable"};
}
return tesSUCCESS;
}

View File

@@ -133,9 +133,9 @@ checkAttestationPublicKey(
// master key
if (sleAttestationSigningAccount->isFlag(lsfDisableMaster))
{
JLOG(j.trace()) << "Attempt to add an attestation with "
"disabled master key.";
return tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR;
return {
tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR,
"Attempt to add an attestation with disabled master key."};
}
}
else
@@ -165,9 +165,10 @@ checkAttestationPublicKey(
// account does not exist.
if (calcAccountID(pk) != attestationSignerAccount)
{
JLOG(j.trace()) << "Attempt to add an attestation with non-existant account "
"and mismatched pk/account pair.";
return tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR;
return {
tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR,
"Attempt to add an attestation with non-existant account and mismatched pk/account "
"pair."};
}
}
@@ -465,8 +466,7 @@ transferHelper(
}
if (amt < psb.fees().reserve)
{
JLOG(j.trace()) << "Insufficient payment to create account.";
return tecNO_DST_INSUF_XRP;
return {tecNO_DST_INSUF_XRP, "Insufficient payment to create account."};
}
// Create the account.

View File

@@ -1,10 +1,11 @@
#include <xrpl/tx/transactors/check/CheckCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -19,6 +20,9 @@ namespace xrpl {
NotTEC
CheckCancel::preflight(PreflightContext const& ctx)
{
if (ctx.rules.enabled(fixCleanup3_3_0) && ctx.tx[sfCheckID] == beast::kZero)
return temMALFORMED;
return tesSUCCESS;
}
@@ -28,8 +32,7 @@ CheckCancel::preclaim(PreclaimContext const& ctx)
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
if (!sleCheck)
{
JLOG(ctx.j.warn()) << "Check does not exist.";
return tecNO_ENTRY;
return {tecNO_ENTRY, "Check does not exist."};
}
// Expiration is defined in terms of the close time of the parent
@@ -43,9 +46,9 @@ CheckCancel::preclaim(PreclaimContext const& ctx)
AccountID const acctId{ctx.tx[sfAccount]};
if (acctId != (*sleCheck)[sfAccount] && acctId != (*sleCheck)[sfDestination])
{
JLOG(ctx.j.warn()) << "Check is not expired and canceler is "
"neither check source nor destination.";
return tecNO_PERMISSION;
return {
tecNO_PERMISSION,
"Check is not expired and canceler is neither check source nor destination."};
}
}
return tesSUCCESS;
@@ -58,8 +61,7 @@ CheckCancel::doApply()
if (!sleCheck)
{
// Error should have been caught in preclaim.
JLOG(j_.warn()) << "Check does not exist.";
return tecNO_ENTRY;
return {tecNO_ENTRY, "Check does not exist."};
}
AccountID const srcId{sleCheck->getAccountID(sfAccount)};
@@ -74,8 +76,7 @@ CheckCancel::doApply()
if (!view().dirRemove(keylet::ownerDir(dstId), page, sleCheck->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from destination.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete check from destination."};
// LCOV_EXCL_STOP
}
}
@@ -84,8 +85,7 @@ CheckCancel::doApply()
if (!view().dirRemove(keylet::ownerDir(srcId), page, sleCheck->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete check from owner."};
// LCOV_EXCL_STOP
}
}

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/scope.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
@@ -51,15 +52,18 @@ CheckCash::checkExtraFeatures(xrpl::PreflightContext const& ctx)
NotTEC
CheckCash::preflight(PreflightContext const& ctx)
{
if (ctx.rules.enabled(fixCleanup3_3_0) && ctx.tx[sfCheckID] == beast::kZero)
return temMALFORMED;
// Exactly one of Amount or DeliverMin must be present.
auto const optAmount = ctx.tx[~sfAmount];
auto const optDeliverMin = ctx.tx[~sfDeliverMin];
if (static_cast<bool>(optAmount) == static_cast<bool>(optDeliverMin))
{
JLOG(ctx.j.warn()) << "Malformed transaction: "
"does not specify exactly one of Amount and DeliverMin.";
return temMALFORMED;
return {
temMALFORMED,
"Malformed transaction: does not specify exactly one of Amount and DeliverMin."};
}
// Make sure the amount is valid.
@@ -72,8 +76,7 @@ CheckCash::preflight(PreflightContext const& ctx)
if (badAsset() == value.asset())
{
JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency.";
return temBAD_CURRENCY;
return {temBAD_CURRENCY, "Malformed transaction: Bad currency."};
}
return tesSUCCESS;
@@ -85,16 +88,14 @@ CheckCash::preclaim(PreclaimContext const& ctx)
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
if (!sleCheck)
{
JLOG(ctx.j.warn()) << "Check does not exist.";
return tecNO_ENTRY;
return {tecNO_ENTRY, "Check does not exist."};
}
// Only cash a check with this account as the destination.
AccountID const dstId = sleCheck->at(sfDestination);
if (ctx.tx[sfAccount] != dstId)
{
JLOG(ctx.j.warn()) << "Cashing a check with wrong Destination.";
return tecNO_PERMISSION;
return {tecNO_PERMISSION, "Cashing a check with wrong Destination."};
}
AccountID const srcId = sleCheck->at(sfAccount);
if (srcId == dstId)
@@ -102,8 +103,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
// They wrote a check to themselves. This should be caught when
// the check is created, but better late than never.
// LCOV_EXCL_START
JLOG(ctx.j.error()) << "Malformed transaction: Cashing check to self.";
return tecINTERNAL;
return {tecINTERNAL, "Malformed transaction: Cashing check to self."};
// LCOV_EXCL_STOP
}
{
@@ -112,23 +112,20 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (!sleSrc || !sleDst)
{
// If the check exists this should never occur.
JLOG(ctx.j.warn()) << "Malformed transaction: source or destination not in ledger";
return tecNO_ENTRY;
return {tecNO_ENTRY, "Malformed transaction: source or destination not in ledger"};
}
if (sleDst->isFlag(lsfRequireDestTag) && !sleCheck->isFieldPresent(sfDestinationTag))
{
// The tag is basically account-specific information we don't
// understand, but we can require someone to fill it in.
JLOG(ctx.j.warn()) << "Malformed transaction: DestinationTag required in check.";
return tecDST_TAG_NEEDED;
return {tecDST_TAG_NEEDED, "Malformed transaction: DestinationTag required in check."};
}
}
if (hasExpired(ctx.view, sleCheck->at(~sfExpiration)))
{
JLOG(ctx.j.warn()) << "Cashing a check that has already expired.";
return tecEXPIRED;
return {tecEXPIRED, "Cashing a check that has already expired."};
}
{
@@ -147,19 +144,16 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (!equalTokens(value.asset(), sendMax.asset()))
{
JLOG(ctx.j.warn()) << "Check cash does not match check currency.";
return temMALFORMED;
return {temMALFORMED, "Check cash does not match check currency."};
}
AccountID const issuerId{value.getIssuer()};
if (issuerId != sendMax.getIssuer())
{
JLOG(ctx.j.warn()) << "Check cash does not match check issuer.";
return temMALFORMED;
return {temMALFORMED, "Check cash does not match check issuer."};
}
if (value > sendMax)
{
JLOG(ctx.j.warn()) << "Check cashed for more than check sendMax.";
return tecPATH_PARTIAL;
return {tecPATH_PARTIAL, "Check cashed for more than check sendMax."};
}
// Make sure the check owner holds at least value. If they have
@@ -182,8 +176,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (value > availableFunds)
{
JLOG(ctx.j.warn()) << "Check cashed for more than owner's balance.";
return tecPATH_PARTIAL;
return {tecPATH_PARTIAL, "Check cashed for more than owner's balance."};
}
}
@@ -226,9 +219,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (!isAuthorized)
{
JLOG(ctx.j.warn()) << "Can't receive IOUs from "
"issuer without auth.";
return tecNO_AUTH;
return {tecNO_AUTH, "Can't receive IOUs from issuer without auth."};
}
}
@@ -241,8 +232,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
// not be frozen.
if (isFrozen(ctx.view, dstId, currency, issuerId))
{
JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline.";
return tecFROZEN;
return {tecFROZEN, "Cashing a check to a frozen trustline."};
}
return tesSUCCESS;
@@ -266,8 +256,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (isFrozen(ctx.view, dstId, issue))
{
JLOG(ctx.j.warn()) << "Cashing a check to a frozen MPT.";
return tecLOCKED;
return {tecLOCKED, "Cashing a check to a frozen MPT."};
}
if (auto const err = canTransfer(ctx.view, issue, srcId, dstId);
@@ -295,8 +284,7 @@ CheckCash::doApply()
if (!sleCheck)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Precheck did not verify check's existence.";
return tecFAILED_PROCESSING;
return {tecFAILED_PROCESSING, "Precheck did not verify check's existence."};
// LCOV_EXCL_STOP
}
@@ -304,8 +292,7 @@ CheckCash::doApply()
if (!psb.exists(keylet::account(srcId)) || !psb.exists(keylet::account(accountID_)))
{
// LCOV_EXCL_START
JLOG(ctx_.journal.fatal()) << "Precheck did not verify source or destination's existence.";
return tecFAILED_PROCESSING;
return {tecFAILED_PROCESSING, "Precheck did not verify source or destination's existence."};
// LCOV_EXCL_STOP
}
@@ -562,8 +549,7 @@ CheckCash::doApply()
{
if (result.actualAmountOut < *optDeliverMin)
{
JLOG(ctx_.journal.warn()) << "flow did not produce DeliverMin.";
return tecPATH_PARTIAL;
return {tecPATH_PARTIAL, "flow did not produce DeliverMin."};
}
ctx_.deliver(result.actualAmountOut);
}
@@ -583,8 +569,7 @@ CheckCash::doApply()
keylet::ownerDir(accountID_), sleCheck->at(sfDestinationNode), sleCheck->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from destination.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete check from destination."};
// LCOV_EXCL_STOP
}
@@ -592,8 +577,7 @@ CheckCash::doApply()
if (!psb.dirRemove(keylet::ownerDir(srcId), sleCheck->at(sfOwnerNode), sleCheck->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete check from owner."};
// LCOV_EXCL_STOP
}

View File

@@ -43,8 +43,7 @@ CheckCreate::preflight(PreflightContext const& ctx)
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
{
// They wrote a check to themselves.
JLOG(ctx.j.warn()) << "Malformed transaction: Check to self.";
return temREDUNDANT;
return {temREDUNDANT, "Malformed transaction: Check to self."};
}
{
@@ -58,8 +57,7 @@ CheckCreate::preflight(PreflightContext const& ctx)
if (badAsset() == sendMax.asset())
{
JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency.";
return temBAD_CURRENCY;
return {temBAD_CURRENCY, "Malformed transaction: Bad currency."};
}
}
@@ -67,8 +65,7 @@ CheckCreate::preflight(PreflightContext const& ctx)
{
if (*optExpiry == 0)
{
JLOG(ctx.j.warn()) << "Malformed transaction: bad expiration";
return temBAD_EXPIRATION;
return {temBAD_EXPIRATION, "Malformed transaction: bad expiration"};
}
}
@@ -83,8 +80,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
auto const sleDst = ctx.view.read(keylet::account(dstId));
if (!sleDst)
{
JLOG(ctx.j.warn()) << "Destination account does not exist.";
return tecNO_DST;
return {tecNO_DST, "Destination account does not exist."};
}
// Check if the destination has disallowed incoming checks
@@ -102,8 +98,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
{
// The tag is basically account-specific information we don't
// understand, but we can require someone to fill it in.
JLOG(ctx.j.warn()) << "Malformed transaction: DestinationTag required.";
return tecDST_TAG_NEEDED;
return {tecDST_TAG_NEEDED, "Malformed transaction: DestinationTag required."};
}
{
@@ -114,8 +109,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
AccountID const& issuerId{sendMax.getIssuer()};
if (auto const ter = checkGlobalFrozen(ctx.view, sendMax.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.warn()) << "Creating a check for frozen or locked asset";
return ter;
return {ter, "Creating a check for frozen or locked asset"};
}
auto const err = sendMax.asset().visit(
[&](Issue const& issue) -> std::optional<TER> {
@@ -132,8 +126,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
if (sleTrust &&
sleTrust->isFlag((issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze))
{
JLOG(ctx.j.warn()) << "Creating a check for frozen trustline.";
return tecFROZEN;
return TER{tecFROZEN, "Creating a check for frozen trustline."};
}
}
if (issuerId != dstId)
@@ -144,9 +137,8 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
if (sleTrust &&
sleTrust->isFlag((dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze))
{
JLOG(ctx.j.warn()) << "Creating a check for "
"destination frozen trustline.";
return tecFROZEN;
return TER{
tecFROZEN, "Creating a check for destination frozen trustline."};
}
}
@@ -155,19 +147,16 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
[&](MPTIssue const& issue) -> std::optional<TER> {
if (srcId != issuerId && isFrozen(ctx.view, srcId, issue))
{
JLOG(ctx.j.warn()) << "Creating a check for locked MPT.";
return tecLOCKED;
return TER{tecLOCKED, "Creating a check for locked MPT."};
}
if (dstId != issuerId && isFrozen(ctx.view, dstId, issue))
{
JLOG(ctx.j.warn()) << "Creating a check for locked MPT.";
return tecLOCKED;
return TER{tecLOCKED, "Creating a check for locked MPT."};
}
if (auto const ter = canTransfer(ctx.view, issue, srcId, dstId);
!isTesSuccess(ter))
{
JLOG(ctx.j.warn()) << "MPT transfer is disabled.";
return ter;
return TER{ter, "MPT transfer is disabled."};
}
return std::nullopt;
@@ -178,8 +167,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
}
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
{
JLOG(ctx.j.warn()) << "Creating a check that has already expired.";
return tecEXPIRED;
return {tecEXPIRED, "Creating a check that has already expired."};
}
return tesSUCCESS;

View File

@@ -36,15 +36,13 @@ CredentialAccept::preflight(PreflightContext const& ctx)
{
if (!ctx.tx[sfIssuer])
{
JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed.";
return temINVALID_ACCOUNT_ID;
return {temINVALID_ACCOUNT_ID, "Malformed transaction: Issuer field zeroed."};
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > kMaxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
return {temMALFORMED, "Malformed transaction: invalid size of CredentialType."};
}
return tesSUCCESS;

View File

@@ -53,26 +53,22 @@ NotTEC
CredentialCreate::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
auto& j = ctx.j;
if (!tx[sfSubject])
{
JLOG(j.trace()) << "Malformed transaction: Invalid Subject";
return temMALFORMED;
return {temMALFORMED, "Malformed transaction: Invalid Subject"};
}
auto const uri = tx[~sfURI];
if (uri && (uri->empty() || (uri->size() > kMaxCredentialUriLength)))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of URI.";
return temMALFORMED;
return {temMALFORMED, "Malformed transaction: invalid size of URI."};
}
auto const credType = tx[sfCredentialType];
if (credType.empty() || (credType.size() > kMaxCredentialTypeLength))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
return {temMALFORMED, "Malformed transaction: invalid size of CredentialType."};
}
return tesSUCCESS;
@@ -86,14 +82,12 @@ CredentialCreate::preclaim(PreclaimContext const& ctx)
if (!ctx.view.exists(keylet::account(subject)))
{
JLOG(ctx.j.trace()) << "Subject doesn't exist.";
return tecNO_TARGET;
return {tecNO_TARGET, "Subject doesn't exist."};
}
if (ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType)))
{
JLOG(ctx.j.trace()) << "Credential already exists.";
return tecDUPLICATE;
return {tecDUPLICATE, "Credential already exists."};
}
return tesSUCCESS;
@@ -118,9 +112,7 @@ CredentialCreate::doApply()
if (closeTime > *optExp)
{
JLOG(j_.trace()) << "Malformed transaction: "
"Expiration time is in the past.";
return tecEXPIRED;
return {tecEXPIRED, "Malformed transaction: Expiration time is in the past."};
}
sleCred->setFieldU32(sfExpiration, *optExp);

View File

@@ -1,6 +1,5 @@
#include <xrpl/tx/transactors/credentials/CredentialDelete.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -36,24 +35,19 @@ CredentialDelete::preflight(PreflightContext const& ctx)
if (!subject && !issuer)
{
// Neither field is present, the transaction is malformed.
JLOG(ctx.j.trace()) << "Malformed transaction: "
"No Subject or Issuer fields.";
return temMALFORMED;
return {temMALFORMED, "Malformed transaction: No Subject or Issuer fields."};
}
// Make sure that the passed account is valid.
if ((subject && subject->isZero()) || (issuer && issuer->isZero()))
{
JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer "
"field zeroed.";
return temINVALID_ACCOUNT_ID;
return {temINVALID_ACCOUNT_ID, "Malformed transaction: Subject or Issuer field zeroed."};
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > kMaxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
return {temMALFORMED, "Malformed transaction: invalid size of CredentialType."};
}
return tesSUCCESS;
@@ -87,8 +81,7 @@ CredentialDelete::doApply()
if ((subject != accountID_) && (issuer != accountID_) &&
!checkExpired(*sleCred, ctx_.view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Can't delete non-expired credential.";
return tecNO_PERMISSION;
return {tecNO_PERMISSION, "Can't delete non-expired credential."};
}
return deleteSLE(view(), sleCred, j_);

View File

@@ -1,6 +1,5 @@
#include <xrpl/tx/transactors/delegate/DelegateSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
@@ -152,8 +151,7 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::ref sle, beast::Journal j)
if (!view.dirRemove(keylet::ownerDir(delegator), (*sle)[sfOwnerNode], sle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Delegate from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Delegate from owner."};
// LCOV_EXCL_STOP
}
@@ -163,8 +161,7 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::ref sle, beast::Journal j)
if (!view.dirRemove(keylet::ownerDir(delegatee), *optPage, sle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Delegate from authorized account.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Delegate from authorized account."};
// LCOV_EXCL_STOP
}
}

View File

@@ -78,8 +78,7 @@ AMMBid::preflight(PreflightContext const& ctx)
auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts);
if (authAccounts.size() > kAuctionSlotMaxAuthAccounts)
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts.";
return temMALFORMED;
return {temMALFORMED, "AMM Bid: Invalid number of AuthAccounts."};
}
if (ctx.rules.enabled(fixAMMv1_3))
{
@@ -90,8 +89,7 @@ AMMBid::preflight(PreflightContext const& ctx)
auto authAccount = obj[sfAccount];
if (authAccount == account || unique.contains(authAccount))
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account.";
return temMALFORMED;
return {temMALFORMED, "AMM Bid: Invalid auth.account."};
}
unique.insert(authAccount);
}
@@ -107,8 +105,7 @@ AMMBid::preclaim(PreclaimContext const& ctx)
auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
if (!ammSle)
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
return terNO_AMM;
return {terNO_AMM, "AMM Bid: Invalid asset pair."};
}
auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];
@@ -121,8 +118,7 @@ AMMBid::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.read(keylet::account(account[sfAccount])))
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid Account.";
return terNO_ACCOUNT;
return {terNO_ACCOUNT, "AMM Bid: Invalid Account."};
}
}
}
@@ -131,8 +127,7 @@ AMMBid::preclaim(PreclaimContext const& ctx)
// Not LP
if (lpTokens == beast::kZero)
{
JLOG(ctx.j.debug()) << "AMM Bid: account is not LP.";
return tecAMM_INVALID_TOKENS;
return {tecAMM_INVALID_TOKENS, "AMM Bid: account is not LP."};
}
auto const bidMin = ctx.tx[~sfBidMin];
@@ -141,13 +136,11 @@ AMMBid::preclaim(PreclaimContext const& ctx)
{
if (bidMin->asset() != lpTokens.asset())
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Bid: Invalid LPToken."};
}
if (*bidMin > lpTokens || *bidMin >= lpTokensBalance)
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
return tecAMM_INVALID_TOKENS;
return {tecAMM_INVALID_TOKENS, "AMM Bid: Invalid Tokens."};
}
}
@@ -156,20 +149,17 @@ AMMBid::preclaim(PreclaimContext const& ctx)
{
if (bidMax->asset() != lpTokens.asset())
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Bid: Invalid LPToken."};
}
if (*bidMax > lpTokens || *bidMax >= lpTokensBalance)
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
return tecAMM_INVALID_TOKENS;
return {tecAMM_INVALID_TOKENS, "AMM Bid: Invalid Tokens."};
}
}
if (bidMin && bidMax && bidMin > bidMax)
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid Max/MinSlotPrice.";
return tecAMM_INVALID_TOKENS;
return {tecAMM_INVALID_TOKENS, "AMM Bid: Invalid Max/MinSlotPrice."};
}
return tesSUCCESS;

View File

@@ -59,8 +59,7 @@ AMMClawback::preflight(PreflightContext const& ctx)
if (issuer == holder)
{
JLOG(ctx.j.trace()) << "AMMClawback: holder cannot be the same as issuer.";
return temMALFORMED;
return {temMALFORMED, "AMMClawback: holder cannot be the same as issuer."};
}
std::optional<STAmount> const clawAmount = ctx.tx[~sfAmount];
@@ -72,23 +71,20 @@ AMMClawback::preflight(PreflightContext const& ctx)
if (ctx.tx.isFlag(tfClawTwoAssets) && asset.getIssuer() != asset2.getIssuer())
{
JLOG(ctx.j.trace()) << "AMMClawback: tfClawTwoAssets can only be enabled when two "
"assets in the AMM pool are both issued by the issuer";
return temINVALID_FLAG;
return {
temINVALID_FLAG,
"AMMClawback: tfClawTwoAssets can only be enabled when two assets in the AMM pool are "
"both issued by the issuer"};
}
if (asset.getIssuer() != issuer)
{
JLOG(ctx.j.trace()) << "AMMClawback: Asset's account does not "
"match Account field.";
return temMALFORMED;
return {temMALFORMED, "AMMClawback: Asset's account does not match Account field."};
}
if (clawAmount && clawAmount->asset() != asset)
{
JLOG(ctx.j.trace()) << "AMMClawback: Amount's asset subfield "
"does not match Asset field";
return temBAD_AMOUNT;
return {temBAD_AMOUNT, "AMMClawback: Amount's asset subfield does not match Asset field"};
}
if (clawAmount && *clawAmount <= beast::kZero)
@@ -112,8 +108,7 @@ AMMClawback::preclaim(PreclaimContext const& ctx)
auto const ammSle = ctx.view.read(keylet::amm(asset, asset2));
if (!ammSle)
{
JLOG(ctx.j.debug()) << "AMM Clawback: Invalid asset pair.";
return terNO_AMM;
return {terNO_AMM, "AMM Clawback: Invalid asset pair."};
}
if (!ctx.view.rules().enabled(featureMPTokensV2))

View File

@@ -59,8 +59,7 @@ AMMCreate::preflight(PreflightContext const& ctx)
if (amount.asset() == amount2.asset())
{
JLOG(ctx.j.debug()) << "AMM Instance: tokens can not have the same asset.";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Instance: tokens can not have the same asset."};
}
if (auto const err = invalidAMMAmount(amount))
@@ -77,8 +76,7 @@ AMMCreate::preflight(PreflightContext const& ctx)
if (ctx.tx[sfTradingFee] > kTradingFeeThreshold)
{
JLOG(ctx.j.debug()) << "AMM Instance: invalid trading fee.";
return temBAD_FEE;
return {temBAD_FEE, "AMM Instance: invalid trading fee."};
}
return tesSUCCESS;
@@ -102,8 +100,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
if (auto const ammKeylet = keylet::amm(amount.asset(), amount2.asset());
ctx.view.read(ammKeylet))
{
JLOG(ctx.j.debug()) << "AMM Instance: ltAMM already exists.";
return tecDUPLICATE;
return {tecDUPLICATE, "AMM Instance: ltAMM already exists."};
}
if (auto const ter = requireAuth(ctx.view, amount.asset(), accountID); !isTesSuccess(ter))
@@ -122,13 +119,11 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
if (auto const ter = checkFrozen(ctx.view, accountID, amount.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Instance: involves frozen or locked asset.";
return ter;
return {ter, "AMM Instance: involves frozen or locked asset."};
}
if (auto const ter = checkFrozen(ctx.view, accountID, amount2.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Instance: involves frozen or locked asset.";
return ter;
return {ter, "AMM Instance: involves frozen or locked asset."};
}
auto noDefaultRipple = [](ReadView const& view, Asset const& asset) {
@@ -143,8 +138,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
if (noDefaultRipple(ctx.view, amount.asset()) || noDefaultRipple(ctx.view, amount2.asset()))
{
JLOG(ctx.j.debug()) << "AMM Instance: DefaultRipple not set";
return terNO_RIPPLE;
return {terNO_RIPPLE, "AMM Instance: DefaultRipple not set"};
}
// Check the reserve for LPToken trustline
@@ -152,8 +146,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
// Insufficient reserve
if (xrpBalance <= beast::kZero)
{
JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves";
return tecINSUF_RESERVE_LINE;
return {tecINSUF_RESERVE_LINE, "AMM Instance: insufficient reserves"};
}
auto insufficientBalance = [&](STAmount const& amount) {

View File

@@ -1,6 +1,5 @@
#include <xrpl/tx/transactors/dex/AMMDelete.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
@@ -39,8 +38,7 @@ AMMDelete::preclaim(PreclaimContext const& ctx)
auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
if (!ammSle)
{
JLOG(ctx.j.debug()) << "AMM Delete: Invalid asset pair.";
return terNO_AMM;
return {terNO_AMM, "AMM Delete: Invalid asset pair."};
}
auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];

View File

@@ -70,8 +70,7 @@ AMMDeposit::preflight(PreflightContext const& ctx)
// tfLimitLPToken: Amount and EPrice
if (std::popcount(flags & tfDepositSubTx) != 1)
{
JLOG(ctx.j.debug()) << "AMM Deposit: invalid flags.";
return temMALFORMED;
return {temMALFORMED, "AMM Deposit: invalid flags."};
}
if (ctx.tx.isFlag(tfLPToken))
{
@@ -124,8 +123,7 @@ AMMDeposit::preflight(PreflightContext const& ctx)
if (lpTokens && *lpTokens <= beast::kZero)
{
JLOG(ctx.j.debug()) << "AMM Deposit: invalid LPTokens";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Deposit: invalid LPTokens"};
}
if (amount)
@@ -166,8 +164,7 @@ AMMDeposit::preflight(PreflightContext const& ctx)
if (tradingFee > kTradingFeeThreshold)
{
JLOG(ctx.j.debug()) << "AMM Deposit: invalid trading fee.";
return temBAD_FEE;
return {temBAD_FEE, "AMM Deposit: invalid trading fee."};
}
return tesSUCCESS;
@@ -181,8 +178,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
if (!ammSle)
{
JLOG(ctx.j.debug()) << "AMM Deposit: Invalid asset pair.";
return terNO_AMM;
return {terNO_AMM, "AMM Deposit: Invalid asset pair."};
}
auto const expected = ammHolds(
@@ -203,8 +199,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
if (amountBalance != beast::kZero || amount2Balance != beast::kZero)
{
// LCOV_EXCL_START
JLOG(ctx.j.debug()) << "AMM Deposit: tokens balance is not zero.";
return tecINTERNAL;
return {tecINTERNAL, "AMM Deposit: tokens balance is not zero."};
// LCOV_EXCL_STOP
}
}
@@ -216,8 +211,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
lptAMMBalance < beast::kZero)
{
// LCOV_EXCL_START
JLOG(ctx.j.debug()) << "AMM Deposit: reserves or tokens balance is zero.";
return tecINTERNAL;
return {tecINTERNAL, "AMM Deposit: reserves or tokens balance is zero."};
// LCOV_EXCL_STOP
}
}
@@ -381,8 +375,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
if (auto const lpTokens = ctx.tx[~sfLPTokenOut];
lpTokens && lpTokens->asset() != lptAMMBalance.asset())
{
JLOG(ctx.j.debug()) << "AMM Deposit: invalid LPTokens.";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Deposit: invalid LPTokens."};
}
// Check the reserve for LPToken trustline if not LP.
@@ -393,8 +386,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
// Insufficient reserve
if (xrpBalance <= beast::kZero)
{
JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves";
return tecINSUF_RESERVE_LINE;
return {tecINSUF_RESERVE_LINE, "AMM Instance: insufficient reserves"};
}
}

View File

@@ -48,8 +48,7 @@ AMMVote::preflight(PreflightContext const& ctx)
if (ctx.tx[sfTradingFee] > kTradingFeeThreshold)
{
JLOG(ctx.j.debug()) << "AMM Vote: invalid trading fee.";
return temBAD_FEE;
return {temBAD_FEE, "AMM Vote: invalid trading fee."};
}
return tesSUCCESS;
@@ -61,8 +60,7 @@ AMMVote::preclaim(PreclaimContext const& ctx)
auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
if (!ammSle)
{
JLOG(ctx.j.debug()) << "AMM Vote: Invalid asset pair.";
return terNO_AMM;
return {terNO_AMM, "AMM Vote: Invalid asset pair."};
}
if (ammSle->getFieldAmount(sfLPTokenBalance) == beast::kZero)
{
@@ -71,8 +69,7 @@ AMMVote::preclaim(PreclaimContext const& ctx)
if (auto const lpTokensNew = ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j);
lpTokensNew == beast::kZero)
{
JLOG(ctx.j.debug()) << "AMM Vote: account is not LP.";
return tecAMM_INVALID_TOKENS;
return {tecAMM_INVALID_TOKENS, "AMM Vote: account is not LP."};
}
return tesSUCCESS;

View File

@@ -78,8 +78,7 @@ AMMWithdraw::preflight(PreflightContext const& ctx)
// Amount and EPrice
if (std::popcount(flags & tfWithdrawSubTx) != 1)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid flags.";
return temMALFORMED;
return {temMALFORMED, "AMM Withdraw: invalid flags."};
}
if (ctx.tx.isFlag(tfLPToken))
{
@@ -129,8 +128,7 @@ AMMWithdraw::preflight(PreflightContext const& ctx)
if (lpTokens && *lpTokens <= beast::kZero)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens.";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Withdraw: invalid tokens."};
}
if (amount)
@@ -186,8 +184,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
if (!ammSle)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid asset pair.";
return terNO_AMM;
return {terNO_AMM, "AMM Withdraw: Invalid asset pair."};
}
auto const amount = ctx.tx[~sfAmount];
@@ -210,8 +207,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
lptAMMBalance < beast::kZero)
{
// LCOV_EXCL_START
JLOG(ctx.j.debug()) << "AMM Withdraw: reserves or tokens balance is zero.";
return tecINTERNAL;
return {tecINTERNAL, "AMM Withdraw: reserves or tokens balance is zero."};
// LCOV_EXCL_STOP
}
@@ -280,26 +276,22 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
if (lpTokens <= beast::kZero)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: tokens balance is zero.";
return tecAMM_BALANCE;
return {tecAMM_BALANCE, "AMM Withdraw: tokens balance is zero."};
}
if (lpTokensWithdraw && lpTokensWithdraw->asset() != lpTokens.asset())
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid LPTokens.";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Withdraw: invalid LPTokens."};
}
if (lpTokensWithdraw && *lpTokensWithdraw > lpTokens)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens.";
return tecAMM_INVALID_TOKENS;
return {tecAMM_INVALID_TOKENS, "AMM Withdraw: invalid tokens."};
}
if (auto const ePrice = ctx.tx[~sfEPrice]; ePrice && ePrice->asset() != lpTokens.asset())
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid EPrice.";
return temBAD_AMM_TOKENS;
return {temBAD_AMM_TOKENS, "AMM Withdraw: invalid EPrice."};
}
if ((ctx.tx.getFlags() & (tfLPToken | tfWithdrawAll)) != 0u)

View File

@@ -17,8 +17,7 @@ OfferCancel::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfOfferSequence] == 0u)
{
JLOG(ctx.j.trace()) << "OfferCancel::preflight: missing sequence";
return temBAD_SEQUENCE;
return {temBAD_SEQUENCE, "OfferCancel::preflight: missing sequence"};
}
return tesSUCCESS;

View File

@@ -90,7 +90,6 @@ NotTEC
OfferCreate::preflight(PreflightContext const& ctx)
{
auto& tx = ctx.tx;
auto& j = ctx.j;
if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID))
return temINVALID_FLAG;
@@ -106,22 +105,19 @@ OfferCreate::preflight(PreflightContext const& ctx)
if (bImmediateOrCancel && bFillOrKill)
{
JLOG(j.debug()) << "Malformed transaction: both IoC and FoK set.";
return temINVALID_FLAG;
return {temINVALID_FLAG, "Malformed transaction: both IoC and FoK set."};
}
bool const bHaveExpiration(tx.isFieldPresent(sfExpiration));
if (bHaveExpiration && (tx.getFieldU32(sfExpiration) == 0))
{
JLOG(j.debug()) << "Malformed offer: bad expiration";
return temBAD_EXPIRATION;
return {temBAD_EXPIRATION, "Malformed offer: bad expiration"};
}
if (auto const cancelSequence = tx[~sfOfferSequence]; cancelSequence && *cancelSequence == 0)
{
JLOG(j.debug()) << "Malformed offer: bad cancel sequence";
return temBAD_SEQUENCE;
return {temBAD_SEQUENCE, "Malformed offer: bad cancel sequence"};
}
STAmount const saTakerPays = tx[sfTakerPays];
@@ -132,13 +128,11 @@ OfferCreate::preflight(PreflightContext const& ctx)
if (saTakerPays.native() && saTakerGets.native())
{
JLOG(j.debug()) << "Malformed offer: redundant (XRP for XRP)";
return temBAD_OFFER;
return {temBAD_OFFER, "Malformed offer: redundant (XRP for XRP)"};
}
if (saTakerPays <= beast::kZero || saTakerGets <= beast::kZero)
{
JLOG(j.debug()) << "Malformed offer: bad amount";
return temBAD_OFFER;
return {temBAD_OFFER, "Malformed offer: bad amount"};
}
auto const& uPaysIssuerID = saTakerPays.getIssuer();
@@ -149,20 +143,17 @@ OfferCreate::preflight(PreflightContext const& ctx)
if (uPaysAsset == uGetsAsset)
{
JLOG(j.debug()) << "Malformed offer: redundant (IOU for IOU)";
return temREDUNDANT;
return {temREDUNDANT, "Malformed offer: redundant (IOU for IOU)"};
}
// We don't allow a non-native currency to use the currency code XRP.
if (badAsset() == uPaysAsset || badAsset() == uGetsAsset)
{
JLOG(j.debug()) << "Malformed offer: bad currency";
return temBAD_CURRENCY;
return {temBAD_CURRENCY, "Malformed offer: bad currency"};
}
if (saTakerPays.native() != !uPaysIssuerID || saTakerGets.native() != !uGetsIssuerID)
{
JLOG(j.debug()) << "Malformed offer: bad issuer";
return temBAD_ISSUER;
return {temBAD_ISSUER, "Malformed offer: bad issuer"};
}
return tesSUCCESS;
@@ -190,13 +181,11 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
if (auto const ter = checkGlobalFrozen(ctx.view, saTakerPays.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "Offer involves frozen or locked asset";
return ter;
return {ter, "Offer involves frozen or locked asset"};
}
if (auto const ter = checkGlobalFrozen(ctx.view, saTakerGets.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "Offer involves frozen or locked asset";
return ter;
return {ter, "Offer involves frozen or locked asset"};
}
// Allow unfunded MPT for issuer (OutstandingAmount >= MaximumAmount)
@@ -209,8 +198,7 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
AuthHandling::ZeroIfUnauthorized,
viewJ) <= beast::kZero)
{
JLOG(ctx.j.debug()) << "delay: Offers must be at least partially funded.";
return tecUNFUNDED_OFFER;
return {tecUNFUNDED_OFFER, "delay: Offers must be at least partially funded."};
}
// This can probably be simplified to make sure that you cancel sequences
@@ -587,8 +575,9 @@ OfferCreate::applyHybrid(
if (!bookNode)
{
JLOG(j_.debug()) << "final result: failed to add hybrid offer to open book";
return tecDIR_FULL; // LCOV_EXCL_LINE
return {
tecDIR_FULL,
"final result: failed to add hybrid offer to open book"}; // LCOV_EXCL_LINE
}
STArray bookArr(sfAdditionalBooks, 1);

View File

@@ -1,6 +1,5 @@
#include <xrpl/tx/transactors/did/DIDDelete.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
@@ -41,8 +40,7 @@ DIDDelete::deleteSLE(ApplyView& view, SLE::pointer sle, AccountID const owner, b
if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete DID from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete DID from owner."};
// LCOV_EXCL_STOP
}

View File

@@ -1,6 +1,5 @@
#include <xrpl/tx/transactors/escrow/EscrowCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
@@ -145,8 +144,7 @@ EscrowCancel::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Escrow from owner."};
// LCOV_EXCL_STOP
}
}
@@ -157,8 +155,7 @@ EscrowCancel::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
// LCOV_EXCL_STOP
}
}
@@ -202,8 +199,7 @@ EscrowCancel::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
// LCOV_EXCL_STOP
}
}

View File

@@ -1,6 +1,5 @@
#include <xrpl/tx/transactors/escrow/EscrowFinish.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/conditions/Condition.h>
@@ -322,8 +321,7 @@ EscrowFinish::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Escrow from owner."};
// LCOV_EXCL_STOP
}
}
@@ -334,8 +332,7 @@ EscrowFinish::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
// LCOV_EXCL_STOP
}
}
@@ -390,8 +387,7 @@ EscrowFinish::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
// LCOV_EXCL_STOP
}
}

View File

@@ -248,8 +248,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
return {tecNO_ENTRY, "LoanBroker does not exist."};
}
auto const brokerPseudoAccountID = sleBroker->at(sfAccount);
@@ -267,16 +266,14 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
if (vaultAsset.native())
{
JLOG(ctx.j.warn()) << "Cannot clawback native asset.";
return tecNO_PERMISSION;
return {tecNO_PERMISSION, "Cannot clawback native asset."};
}
// Only the issuer of the vault asset can claw it back from the broker's
// cover funds.
if (vaultAsset.getIssuer() != account)
{
JLOG(ctx.j.warn()) << "Account is not the issuer of the vault asset.";
return tecNO_PERMISSION;
return {tecNO_PERMISSION, "Account is not the issuer of the vault asset."};
}
if (amount)
@@ -287,9 +284,10 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
auto const txAsset = *findAsset;
if (txAsset != vaultAsset)
{
JLOG(ctx.j.warn()) << "Account is the correct issuer, but trying "
"to clawback the wrong asset from LoanBroker";
return tecWRONG_ASSET;
return {
tecWRONG_ASSET,
"Account is the correct issuer, but trying to clawback the wrong asset from "
"LoanBroker"};
}
}
@@ -323,8 +321,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
if (!sleIssuer)
{
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "Issuer account does not exist.";
return tefBAD_LEDGER;
return {tefBAD_LEDGER, "Issuer account does not exist."};
// LCOV_EXCL_STOP
}

View File

@@ -54,13 +54,11 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
return {tecNO_ENTRY, "LoanBroker does not exist."};
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
return {tecNO_PERMISSION, "Account is not the owner of the LoanBroker."};
}
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)

Some files were not shown because too many files have changed in this diff Show More