mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 07:00:21 +00:00
Compare commits
31 Commits
ximinez/ac
...
mvadari/er
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84c47bb55e | ||
|
|
5c64d9f7c0 | ||
|
|
b765e78e94 | ||
|
|
2c69d58f26 | ||
|
|
445574089f | ||
|
|
b5bd12dec0 | ||
|
|
2da94043f2 | ||
|
|
0621841912 | ||
|
|
12ed506565 | ||
|
|
3122de86bf | ||
|
|
7edf39e622 | ||
|
|
ba01b05f33 | ||
|
|
c414e1877d | ||
|
|
f56b79f00d | ||
|
|
4918f3bb20 | ||
|
|
ee0a3dfad7 | ||
|
|
042c9660cd | ||
|
|
60b49f3fe0 | ||
|
|
4c869ee16c | ||
|
|
b2f45813e0 | ||
|
|
5ce0b1c2c7 | ||
|
|
701311f27e | ||
|
|
b1a670c46e | ||
|
|
18e311e1e2 | ||
|
|
69b70d7a0d | ||
|
|
b42cde3e85 | ||
|
|
cd38c0e800 | ||
|
|
433e5f6896 | ||
|
|
781ab723af | ||
|
|
a24e543af3 | ||
|
|
a0fd1cce54 |
@@ -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
|
||||
|
||||
@@ -11,6 +11,9 @@ endfunction()
|
||||
function(create_symbolic_link target link)
|
||||
endfunction()
|
||||
|
||||
function(xrpl_add_benchmark name)
|
||||
endfunction()
|
||||
|
||||
macro(exclude_from_default target_)
|
||||
endmacro()
|
||||
|
||||
|
||||
8
.github/pull_request_template.md
vendored
8
.github/pull_request_template.md
vendored
@@ -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.
|
||||
-->
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/scripts/strategy-matrix/linux.json
vendored
2
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-e29b523",
|
||||
"image_tag": "sha-2e25435",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
|
||||
6
.github/workflows/build-nix-images.yml
vendored
6
.github/workflows/build-nix-images.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/build-packaging-images.yml
vendored
4
.github/workflows/build-packaging-images.yml
vendored
@@ -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
|
||||
|
||||
38
.github/workflows/build-pre-commit-image.yml
vendored
Normal file
38
.github/workflows/build-pre-commit-image.yml
vendored
Normal 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' }}
|
||||
37
.github/workflows/on-pr.yml
vendored
37
.github/workflows/on-pr.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -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" }'
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -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
|
||||
|
||||
|
||||
59
.github/workflows/reusable-build-test-config.yml
vendored
59
.github/workflows/reusable-build-test-config.yml
vendored
@@ -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 }}
|
||||
|
||||
4
.github/workflows/reusable-clang-tidy.yml
vendored
4
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/reusable-package.yml
vendored
2
.github/workflows/reusable-package.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
2
.github/workflows/reusable-upload-recipe.yml
vendored
2
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -81,6 +81,9 @@ DerivedData
|
||||
# Python
|
||||
__pycache__
|
||||
|
||||
# Rust build artifacts.
|
||||
target/
|
||||
|
||||
# Direnv's directory
|
||||
/.direnv
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
4
CODEOWNERS
Normal 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
|
||||
@@ -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
55
bin/pre-commit/Dockerfile
Normal 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 /
|
||||
36
cmake/XrplAddBenchmark.cmake
Normal file
36
cmake/XrplAddBenchmark.cmake
Normal 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()
|
||||
@@ -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
|
||||
|
||||
11
conan.lock
11
conan.lock
@@ -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": {
|
||||
|
||||
@@ -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 #}
|
||||
|
||||
@@ -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
23
flake.lock
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
17
flake.nix
17
flake.nix
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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&
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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}"
|
||||
|
||||
14
nix/docker/test_files/rust/proc_macro/Cargo.lock
generated
Normal file
14
nix/docker/test_files/rust/proc_macro/Cargo.lock
generated
Normal 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",
|
||||
]
|
||||
3
nix/docker/test_files/rust/proc_macro/Cargo.toml
Normal file
3
nix/docker/test_files/rust/proc_macro/Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["echo_macro", "test_macro"]
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "echo_macro"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -0,0 +1,6 @@
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn define_echo(item: TokenStream) -> TokenStream {
|
||||
format!("fn echo() -> u32 {{ {item} }}").parse().unwrap()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "test_macro"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
echo_macro = { path = "../echo_macro" }
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
4
rust-toolchain.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.95"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||
profile = "minimal"
|
||||
22
src/benchmarks/libxrpl/CMakeLists.txt
Normal file
22
src/benchmarks/libxrpl/CMakeLists.txt
Normal 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)
|
||||
329
src/benchmarks/libxrpl/nodestore/Backend.cpp
Normal file
329
src/benchmarks/libxrpl/nodestore/Backend.cpp
Normal 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
|
||||
243
src/benchmarks/libxrpl/nodestore/Database.cpp
Normal file
243
src/benchmarks/libxrpl/nodestore/Database.cpp
Normal 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
|
||||
318
src/benchmarks/libxrpl/nodestore/NodeStoreBench.h
Normal file
318
src/benchmarks/libxrpl/nodestore/NodeStoreBench.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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_));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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"};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user