Compare commits

..

39 Commits

Author SHA1 Message Date
Ed Hennis
3c31a1dd01 Merge branch 'develop' into ximinez/fix/validator-cache 2026-07-14 20:13:13 -04:00
Ed Hennis
bbfaf85a14 Merge remote-tracking branch 'XRPLF/develop' into ximinez/fix/validator-cache
* XRPLF/develop:
  ci: Run full matrix only on `Ready to merge` or `Full CI build` labeled PRs (7689)
  fix: Strengthen Clawback invariant checks for MPT balances (7285)
  test: Add unit tests for IP address related functions (7744)
  ci: Add Rust to Nix docker image (7571)
  docs: Add more information about pre-commit hooks and how to set them up (7802)
  test: Add JSON array size tests (7592)
  chore: Enable most readability checks (7772)
  ci: Do not run conflict checker when label is applied (7774)
  chore: Run clang_tidy_check with `pass_filenames: false` from pre-commit (7800)
  feat: Add delegate filter param for account_tx RPC (6126)
  refactor: Move `jss.h` `include` out of `Indexes.h` (7799)
  test: Add tests for check doxygen style (7795)
  style: Add pre-commit hook to check doxygen style (7794)
  style: Unify style for all Doxygen comments (7776)
  fix: Improve Number addition/subtraction rounding (7369)
  feat: XLS-68: Sponsor, 5887 continuation  (7350)
2026-07-14 15:02:07 -04:00
Ed Hennis
1c4c998ae1 Merge branch 'develop' into ximinez/fix/validator-cache 2026-07-09 19:50:05 -04:00
Ed Hennis
5d2faef541 Merge remote-tracking branch 'XRPLF/develop' into ximinez/fix/validator-cache
* XRPLF/develop: (158 commits)
  chore: Use std::ranges where possible (7634)
  ci: Use macOS 26 Tahoe with apple-clang 21 (7601)
  build: Mark sec256k1 and mpt-crypto as transitive headers (7658)
  chore: Add a script to nicely format clang-tidy output (7650)
  chore: Enable most bugprone checks (7643)
  feat: Confidential Transfer for MPT (5860)
  fix: Use trustline balance direction to validate IOU PaymentMint/PaymentBurn (7584)
  fix: Unify freeze checks for pseudo-account deposit/withdraw (7382)
  fix: Block delegate tx from being queued (7640)
  chore: Enable groups of clang-tidy checks by default (7637)
  ci: Better determine when we need to run full clang-tidy (7635)
  refactor: Retire NFTokenReserve fix (7367)
  refactor: Retire Clawback amendment (7353)
  refactor: Rename (mostly keylet) functions to more closely match the docs (7059)
  build: Switch to a new conan XRPLF remote, again (7638)
  chore: Revert "build: Switch to a new conan XRPLF remote (7622)" (7623)
  build: Switch to a new conan XRPLF remote (7622)
  build: Align xrpld RPM packaging with DEB package (7529)
  chore: Use clang-tidy v22 new features (7427)
  build: Patch nix binaries in CMake (7539)
  ...
2026-06-30 16:48:26 -04:00
Ed Hennis
5d2be06748 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-19 16:53:46 -04:00
Ed Hennis
2f5b17c2cb Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-19 10:15:25 -04:00
Ed Hennis
a963e4a389 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-19 05:16:02 -04:00
Ed Hennis
962f3ee744 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-15 21:43:29 -04:00
Ed Hennis
2a7499a415 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-14 20:39:19 -04:00
Ed Hennis
ae8c44b606 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-13 22:32:02 -04:00
Ed Hennis
1edd6fe4bf Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-13 12:03:49 -04:00
Ed Hennis
2edf310a6f Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-12 19:18:57 -04:00
Ed Hennis
7af68be818 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-12 16:26:19 -04:00
Ed Hennis
19fc5c48dd Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-11 13:34:22 -04:00
Ed Hennis
2f43a1901d Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-07 18:10:13 -04:00
Ed Hennis
6a0440bf6d Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-07 14:18:39 -04:00
Ed Hennis
cf398d05aa Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-07 13:28:42 -04:00
Ed Hennis
59a8e30a3e Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-06 22:34:33 -04:00
Ed Hennis
8160921783 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-06 14:18:12 -04:00
Ed Hennis
6dc01196b4 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-05 16:46:54 -04:00
Ed Hennis
cdbbbdd27f Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-05 10:50:44 -04:00
Ed Hennis
3d30c09031 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-05 00:14:58 -04:00
Ed Hennis
e59ca23487 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-01 13:59:15 -04:00
Ed Hennis
72c700c3e0 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-28 16:23:38 -04:00
Ed Hennis
4589fcbcfc Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-25 14:45:54 -04:00
Ed Hennis
86d840f53d Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-23 15:56:11 -04:00
Ed Hennis
741b61cdf3 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-22 23:40:28 -04:00
Ed Hennis
6bb0989c9f Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-22 14:49:12 -04:00
Ed Hennis
9120506613 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-22 13:10:44 -04:00
Ed Hennis
3b3de96bd4 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-21 18:48:39 -04:00
Ed Hennis
c9ab6ab25f Merge remote-tracking branch 'XRPLF/develop' into ximinez/fix/validator-cache
* XRPLF/develop:
  chore: Remove empty Taker.h (6984)
  chore: Enable clang-tidy modernize checks (6975)
  ci: Upload clang-tidy git diff (6983)
  fix: Add rounding to Vault invariants (6217) (6955)
2026-04-21 18:46:58 -04:00
Ed Hennis
fb0605cfd3 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-20 17:49:47 -04:00
Ed Hennis
156553bb5e Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-20 15:45:04 -04:00
Ed Hennis
781b56849b Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-20 11:39:06 -04:00
Ed Hennis
278c02bebb Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-17 18:11:31 -04:00
Ed Hennis
1d6fedf9a2 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-16 13:44:37 -04:00
Ed Hennis
2e8de499aa Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-15 19:06:29 -04:00
Ed Hennis
0bce3639a6 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-15 14:28:55 -04:00
Ed Hennis
8f329e3bc6 Use Validator List (VL) cache files in more scenarios
- If any [validator_list_keys] are not available after all
  [validator_list_sites] have had a chance to be queried, then fall
  back to loading cache files. Currently, cache files are only used if
  no sites are defined, or the request to one of them has an error. It
  does not include cases where not enough sites are defined, or if a
  site returns an invalid VL (or something else entirely).
- Resolves #5320
2026-04-13 19:46:06 -04:00
137 changed files with 2359 additions and 3040 deletions

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
<!--
This PR template helps you write a good pull request description.
This PR template helps you to 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 redundant test runs. Renaming
opening the PR. This avoids unnecessary 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 there is a relevant task or issue, please link it here.
If 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 this PR.
For future tasks related to PR.
-->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,11 +25,18 @@ on:
- unlabeled
concurrency:
# 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 }}
# 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) || ''
}}
cancel-in-progress: true
defaults:
@@ -37,17 +44,21 @@ defaults:
shell: bash
jobs:
# 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.
# 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.
should-run:
if: >-
${{
!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.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'))
}}
runs-on: ubuntu-latest
steps:

View File

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

View File

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

View File

@@ -113,7 +113,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
with:
enable_ccache: ${{ inputs.ccache_enabled }}
@@ -223,13 +223,11 @@ 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}" \
2>&1 | tee "${GITHUB_WORKSPACE}/build.log"
--target "${CMAKE_TARGET}"
- name: Show ccache statistics
if: ${{ inputs.ccache_enabled }}
@@ -324,46 +322,27 @@ jobs:
PRELOAD=""
fi
LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee "${GITHUB_WORKSPACE}/unittest.log"
LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
# 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 }}
- 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 }}
run: |
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}"
if [ ! -d "${WORKING_DIR}" ]; then
echo "Working directory '${WORKING_DIR}' does not exist."
exit 0
fi
- name: Show build/test failure summary
if: ${{ failure() }}
run: |
cd "${GITHUB_WORKSPACE}"
cd "${WORKING_DIR}"
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
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."
fi
- name: Debug failure (Linux)
if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }}

View File

@@ -34,7 +34,7 @@ jobs:
needs: [determine-files]
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-2e25435"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-e29b523"
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@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
with:
enable_ccache: false

View File

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

View File

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

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -83,7 +83,6 @@ 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,

View File

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

View File

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

View File

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

View File

@@ -10,23 +10,22 @@
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355",
"mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828",
"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#f729f6d75992d20f9c72828e9142d62f%1783945160.094135",
"grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616",
"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",
"benchmark/1.9.5#b885dc73ad67b40a55d45684d1c88ad1%1782736613.864841",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
@@ -39,7 +38,7 @@
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"python_requires": [],
"overrides": {

View File

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

View File

@@ -15,7 +15,6 @@ 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],
@@ -47,7 +46,6 @@ class Xrpl(ConanFile):
default_options = {
"assertions": False,
"benchmark": True,
"coverage": False,
"fPIC": True,
"jemalloc": False,
@@ -131,14 +129,12 @@ 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-rc4", transitive_headers=True)
self.requires("mpt-crypto/0.4.0-rc2", transitive_headers=True)
self.requires("protobuf/6.33.5", force=True)
if self.options.rocksdb:
self.requires("rocksdb/10.5.1")
@@ -166,7 +162,6 @@ class Xrpl(ConanFile):
def generate(self):
tc = CMakeToolchain(self)
tc.variables["tests"] = self.options.tests
tc.variables["benchmark"] = self.options.benchmark
tc.variables["assert"] = self.options.assertions
tc.variables["coverage"] = self.options.coverage
tc.variables["jemalloc"] = self.options.jemalloc

23
flake.lock generated
View File

@@ -36,28 +36,7 @@
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"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"
"nixpkgs-custom-glibc": "nixpkgs-custom-glibc"
}
}
},

View File

@@ -10,25 +10,12 @@
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,
rust-overlay,
...
}:
{ nixpkgs, nixpkgs-custom-glibc, ... }:
let
forEachSystem = import ./nix/utils.nix { inherit nixpkgs nixpkgs-custom-glibc rust-overlay; };
forEachSystem = import ./nix/utils.nix { inherit nixpkgs nixpkgs-custom-glibc; };
in
{
devShells = forEachSystem (import ./nix/devshell.nix);

View File

@@ -3,9 +3,6 @@
#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 {
@@ -604,42 +601,8 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
std::vector<key_type> v;
{
// 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");
std::scoped_lock const lock(mutex_);
v.reserve(cache_.size());
for (auto const& _ : cache_)
v.push_back(_.first);
}

View File

@@ -263,11 +263,10 @@ constructLoanState(
Number const& principalOutstanding,
Number const& managementFeeOutstanding);
// 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.
// Constructs a valid LoanState object from a Loan object, which always has
// rounded values
LoanState
constructLoanState(SLE::const_ref loan);
constructRoundedLoanState(SLE::const_ref loan);
Number
computeManagementFee(

View File

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

View File

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

View File

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

View File

@@ -142,26 +142,9 @@ public:
TxnSql status,
std::string const& escapedMetaData) const;
/**
* The IDs of the inner transactions of a Batch.
*/
[[nodiscard]] std::vector<uint256>
[[nodiscard]] std::vector<uint256> const&
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;
@@ -183,16 +166,13 @@ private:
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
checkBatchSingleSign(STObject const& batchSigner) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const;
checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const;
void
buildBatchTxns();
buildBatchTxnIds();
STBase*
copy(std::size_t n, void* buf) const override;
@@ -200,11 +180,11 @@ private:
move(std::size_t n, void* buf) override;
friend class detail::STVar;
std::optional<std::vector<std::shared_ptr<STTx const>>> batchTxns_;
std::optional<std::vector<uint256>> batchTxnIds_;
};
bool
passesLocalChecks(STTx const& tx, std::string&);
passesLocalChecks(STObject const& st, std::string&);
/**
* Sterilize a transaction.

View File

@@ -604,7 +604,7 @@ class CanCvtToNotTEC<TEScodes> : public std::true_type
{
};
using NotTECRaw = TERSubset<CanCvtToNotTEC>;
using NotTEC = TERSubset<CanCvtToNotTEC>;
//------------------------------------------------------------------------------
@@ -639,188 +639,48 @@ 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 const& x) noexcept
isTelLocal(TER x) noexcept
{
return (x >= telLOCAL_ERROR && x < temMALFORMED);
}
inline bool
isTemMalformed(TER const& x) noexcept
isTemMalformed(TER x) noexcept
{
return (x >= temMALFORMED && x < tefFAILURE);
}
inline bool
isTefFailure(TER const& x) noexcept
isTefFailure(TER x) noexcept
{
return (x >= tefFAILURE && x < terRETRY);
}
inline bool
isTerRetry(TER const& x) noexcept
isTerRetry(TER x) noexcept
{
return (x >= terRETRY && x < tesSUCCESS);
}
inline bool
isTesSuccess(TER const& x) noexcept
isTesSuccess(TER x) noexcept
{
// Makes use of TER::operator bool()
// Makes use of TERSubset::operator bool()
return !x;
}
inline bool
isTecClaim(TER const& x) noexcept
isTecClaim(TER x) noexcept
{
return (x >= tecCLAIM);
}
@@ -829,10 +689,13 @@ std::unordered_map<TERUnderlyingType, std::pair<char const* const, char const* c
transResults();
bool
transResultInfo(TERRaw code, std::string& token, std::string& text);
transResultInfo(TER code, std::string& token, std::string& text);
std::string
transToken(TERRaw code);
transToken(TER code);
std::string
transHuman(TER code);
std::optional<TER>
transCode(std::string const& token);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,9 +47,7 @@ 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`, and
builds the [`test_files/rust/proc_macro/`](./test_files/rust/proc_macro)
workspace with `cargo` to exercise proc-macro dylib loading.
[`test_files/rust/sources/`](./test_files/rust/sources) with `rustc`.
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
@@ -78,23 +76,20 @@ 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, 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.
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.
## 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 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. |
| 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. |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# Run pre-compiled Rust binaries and confirm each emits its expected diagnostic.
# Binaries must already exist in <bins_dir> as <name> for name in
# {hello,panic,overflow,proc_macro}.
# {hello,panic,overflow}.
set -eo pipefail
@@ -54,13 +54,12 @@ 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 proc_macro; do
for name in hello panic overflow; do
binary="${bins_dir}/${name}"
if [ "${name}" = "hello" ] || [ "${name}" = "proc_macro" ]; then
if [ "${name}" = "hello" ]; then
expected_rc=0
else
expected_rc=nonzero

View File

@@ -15,55 +15,6 @@ 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
@@ -71,12 +22,9 @@ in
llvmVersion
gccPackage
llvmPackages
mkVersionedToolLinks
;
commonPackages = with pkgs; [
clangToolLinks
runClangTidyLink
ccache
clangbuildanalyzer
clangTools
@@ -115,10 +63,14 @@ in
vim
zip
# Rust packages
cargo
cargo-audit
cargo-llvm-cov
cargo-nextest
clippy
corrosion
rustToolchain
rust-analyzer
rustc
rustfmt
];
}

View File

@@ -1,8 +1,4 @@
{
nixpkgs,
nixpkgs-custom-glibc,
rust-overlay,
}:
{ nixpkgs, nixpkgs-custom-glibc }:
function:
nixpkgs.lib.genAttrs
[
@@ -14,12 +10,7 @@ nixpkgs.lib.genAttrs
(
system:
function {
# 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) ];
};
pkgs = import nixpkgs { inherit system; };
# 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -312,25 +312,6 @@ 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.
*
@@ -406,18 +387,22 @@ loanAccruedInterest(
*
* This is the core function that updates the Loan ledger object fields based on
* a computed payment.
*/
LoanPaymentParts
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);
* 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)
{
XRPL_ASSERT_PARTS(nextDueDateProxy, "xrpl::detail::doPayment", "Next due date proxy set");
if (payment.specialCase == PaymentSpecialCase::Final)
@@ -485,12 +470,16 @@ doPayment(ExtendedPaymentComponents const& payment, SLE::ref loan)
// 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");
@@ -728,23 +717,22 @@ 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,
SLE::ref loan,
NumberProxy& totalValueOutstandingProxy,
NumberProxy& principalOutstandingProxy,
NumberProxy& managementFeeOutstandingProxy,
NumberProxy& periodicPaymentProxy,
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;
@@ -756,7 +744,7 @@ doOverpayment(
<< ", interestPart: " << overpaymentComponents.trackedInterestPart()
<< ", untrackedInterest: " << overpaymentComponents.untrackedInterest
<< ", totalDue: " << overpaymentComponents.totalDue
<< ", payments remaining :" << paymentsRemaining;
<< ", payments remaining :" << paymentRemaining;
// Attempt to re-amortize the loan with the overpayment applied.
// This modifies the temporary copies, leaving the proxies unchanged.
@@ -768,7 +756,7 @@ doOverpayment(
loanState,
periodicPayment,
periodicRate,
paymentsRemaining,
paymentRemaining,
managementFeeRate,
j);
if (!ret)
@@ -876,15 +864,16 @@ std::expected<ExtendedPaymentComponents, TER>
computeLatePayment(
Asset const& asset,
ApplyView const& view,
SLE::const_ref loan,
Number const& principalOutstanding,
std::int32_t nextDueDate,
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))
@@ -892,15 +881,15 @@ computeLatePayment(
// Calculate the penalty interest based on how long the payment is overdue.
auto const latePaymentInterest = loanLatePaymentInterest(
loan->at(sfPrincipalOutstanding),
TenthBips32{loan->at(sfLateInterestRate)},
view.parentCloseTime(),
nextDueDate);
principalOutstanding, lateInterestRate, view.parentCloseTime(), nextDueDate);
// Round the late interest and split it between the vault (net interest)
// and the broker (management fee portion).
auto const [roundedLateInterest, roundedLateManagementFee] =
roundAndSplitInterest(asset, latePaymentInterest, managementFeeRate, loanScale);
// 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);
}();
XRPL_ASSERT(roundedLateInterest >= 0, "xrpl::detail::computeLatePayment : valid late interest");
XRPL_ASSERT_PARTS(
@@ -919,7 +908,7 @@ computeLatePayment(
// 1. Regular service fee (from periodic.untrackedManagementFee)
// 2. Late payment fee (fixed penalty)
// 3. Management fee portion of late interest
periodic.untrackedManagementFee + loan->at(sfLatePaymentFee) + roundedLateManagementFee,
periodic.untrackedManagementFee + latePaymentFee + roundedLateManagementFee,
// Untracked interest includes:
// 1. Any untracked interest from the regular payment (usually 0)
@@ -969,15 +958,22 @@ std::expected<ExtendedPaymentComponents, TER>
computeFullPayment(
Asset const& asset,
ApplyView& view,
SLE::const_ref loan,
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,
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)
{
@@ -990,7 +986,7 @@ computeFullPayment(
// This theoretical (unrounded) value is used to compute interest and
// penalties accurately.
Number const theoreticalPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
view.rules(), loan->at(sfPeriodicPayment), periodicRate, paymentRemaining);
view.rules(), periodicPayment, periodicRate, paymentRemaining);
// Full payment interest includes both accrued interest (time since last
// payment) and prepayment penalty (for closing early).
@@ -998,21 +994,18 @@ computeFullPayment(
theoreticalPrincipalOutstanding,
periodicRate,
view.parentCloseTime(),
loan->at(sfPaymentInterval),
loan->at(sfPreviousPaymentDueDate),
loan->at(sfStartDate),
TenthBips32{loan->at(sfCloseInterestRate)});
paymentInterval,
prevPaymentDate,
startDate,
closeInterestRate);
// 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);
// 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);
}();
ExtendedPaymentComponents const full{
PaymentComponents{
@@ -1053,7 +1046,8 @@ computeFullPayment(
"xrpl::detail::computeFullPayment",
"total due is rounded");
JLOG(j.trace()) << "computeFullPayment result: periodicRate: " << periodicRate
JLOG(j.trace()) << "computeFullPayment result: periodicPayment: " << periodicPayment
<< ", periodicRate: " << periodicRate
<< ", paymentRemaining: " << paymentRemaining
<< ", theoreticalPrincipalOutstanding: " << theoreticalPrincipalOutstanding
<< ", fullPaymentInterest: " << fullPaymentInterest
@@ -1304,34 +1298,6 @@ 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
@@ -1376,12 +1342,11 @@ 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] =
roundAndSplitInterest(
asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate),
managementFeeRate,
loanScale);
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, tenthBipsOfValue(overpayment, overpaymentInterestRate), loanScale);
return detail::computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}();
auto const result = detail::ExtendedPaymentComponents{
// Build the payment components, after fees and penalty
@@ -1408,265 +1373,6 @@ 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
@@ -1926,10 +1632,8 @@ constructLoanState(
}
LoanState
constructLoanState(SLE::const_ref loan)
constructRoundedLoanState(SLE::const_ref loan)
{
XRPL_ASSERT(loan && loan->getType() == ltLOAN, "xrpl::constructLoanState : valid loan SLE");
return constructLoanState(
loan->at(sfTotalValueOutstanding),
loan->at(sfPrincipalOutstanding),
@@ -2086,7 +1790,12 @@ loanMakePayment(
LoanPaymentType const paymentType,
beast::Journal j)
{
if (loan->at(sfPaymentRemaining) == 0 || loan->at(sfPrincipalOutstanding) == 0)
using namespace Lending;
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0)
{
// Loan complete this is already checked in LoanPay::preclaim()
// LCOV_EXCL_START
@@ -2095,6 +1804,9 @@ 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)
@@ -2103,8 +1815,26 @@ loanMakePayment(
return std::unexpected(tecINTERNAL);
}
XRPL_ASSERT(
*loan->at(sfTotalValueOutstanding) > 0, "xrpl::loanMakePayment : valid total value");
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");
view.update(loan);
@@ -2114,29 +1844,311 @@ 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 "
<< loan->at(sfStartDate) << ", prev payment due date is "
<< loan->at(sfPreviousPaymentDueDate) << ", 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 "
<< startDate << ", prev payment due date is " << prevPaymentDateProxy
<< ", next payment due date is " << nextDueDateProxy << ", ledger time is "
<< view.parentCloseTime().time_since_epoch().count();
return std::unexpected(tecEXPIRED);
}
switch (paymentType)
// -------------------------------------------------------------
// full payment handling
if (paymentType == LoanPaymentType::Full)
{
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);
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
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::loanMakePayment : invalid payment type");
return std::unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
// -------------------------------------------------------------
// 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;
}
} // namespace xrpl

View File

@@ -13,7 +13,6 @@
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <atomic>
#include <cstdint>
#include <exception>
#include <functional>
@@ -53,7 +52,6 @@ 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_);
@@ -64,28 +62,11 @@ 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
{
@@ -196,18 +177,9 @@ DatabaseRotatingImp::fetchNodeObject(
writable = writableBackend_;
}
// 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);
// Update writable backend with data from the archive backend
if (duplicate)
writable->store(nodeObject);
}
}
}

View File

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

View File

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

View File

@@ -72,7 +72,7 @@ STTx::STTx(STObject&& object)
{
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // may throw
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxns();
buildBatchTxnIds();
}
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);
buildBatchTxns();
buildBatchTxnIds();
}
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);
buildBatchTxns();
buildBatchTxnIds();
}
STBase*
@@ -279,9 +279,12 @@ STTx::checkSign(Rules const& rules) const
return std::unexpected("Sponsor: " + ret.error());
}
// Verify batch signer signatures here so they are cached with the rest
// of signature checking.
if (isFieldPresent(sfBatchSigners))
// 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))
{
if (auto const ret = checkBatchSign(rules); !ret)
return ret;
@@ -304,28 +307,11 @@ 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, txIds)
: checkBatchSingleSign(signer, txIds);
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules)
: checkBatchSingleSign(signer);
if (!result)
return result;
@@ -455,11 +441,12 @@ STTx::checkSingleSign(STObject const& sigObject) const
}
std::expected<void, std::string>
STTx::checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const
STTx::checkBatchSingleSign(STObject const& batchSigner) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction");
Serializer msg;
serializeBatch(msg, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
serializeBatch(
msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
return singleSignHelper(batchSigner, msg.slice());
}
@@ -542,10 +529,7 @@ multiSignHelper(
}
std::expected<void, std::string>
STTx::checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const
STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction");
// We can ease the computational load inside the loop a bit by
@@ -553,7 +537,8 @@ STTx::checkBatchMultiSign(
// with the stuff that stays constant from signature to signature.
auto const batchSignerAccount = batchSigner.getAccountID(sfAccount);
Serializer dataStart;
serializeBatch(dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
serializeBatch(
dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
dataStart.addBitString(batchSignerAccount);
return multiSignHelper(
batchSigner,
@@ -592,79 +577,38 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
}
void
STTx::buildBatchTxns()
STTx::buildBatchTxnIds()
{
// 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::buildBatchTxns : template applied");
if (getTxnType() != ttBATCH)
XRPL_ASSERT(!isFree(), "STTx::buildBatchTxnIds : template applied");
if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions))
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.");
// 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());
// 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());
for (STObject const& rb : raw)
{
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}));
}
ids.push_back(rb.getHash(HashPrefix::TransactionId));
}
std::vector<uint256>
std::vector<uint256> const&
STTx::getBatchTransactionIDs() const
{
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(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : batch transaction");
XRPL_ASSERT(
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);
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_;
}
AccountID
@@ -810,62 +754,86 @@ invalidMPTAmountInTx(STObject const& tx)
}
static bool
isBatchRawTransactionOkay(STTx const& tx, std::string& reason)
isBatchRawTransactionOkay(STObject const& st, std::string& reason)
{
if (!tx.isFieldPresent(sfRawTransactions))
if (!st.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 (tx.getTxnType() != ttBATCH)
if (st.getFieldU16(sfTransactionType) != ttBATCH)
{
reason = "Only Batch transactions may contain raw transactions.";
return false;
}
if (tx.isFieldPresent(sfBatchSigners) &&
tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
if (st.isFieldPresent(sfBatchSigners) &&
st.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
{
reason = "BatchSigners array exceeds max entries.";
reason = "Batch Signers array exceeds max entries.";
return false;
}
// 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())
auto const& rawTxns = st.getFieldArray(sfRawTransactions);
if (rawTxns.size() > kMaxBatchTxCount)
{
if (!passesLocalChecks(*inner, reason))
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();
return false;
}
}
return true;
}
bool
passesLocalChecks(STTx const& tx, std::string& reason)
passesLocalChecks(STObject const& st, std::string& reason)
{
if (!isMemoOkay(tx, reason))
if (!isMemoOkay(st, reason))
return false;
if (!isAccountFieldOkay(tx))
if (!isAccountFieldOkay(st))
{
reason = "An account field is invalid.";
return false;
}
if (isPseudoTx(tx))
if (isPseudoTx(st))
{
reason = "Cannot submit pseudo transactions.";
return false;
}
if (invalidMPTAmountInTx(tx))
if (invalidMPTAmountInTx(st))
{
reason = "Amount can not be MPT.";
return false;
}
if (!isBatchRawTransactionOkay(tx, reason))
if (!isBatchRawTransactionOkay(st, reason))
return false;
return true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
#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>
@@ -52,18 +51,15 @@ 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))
{
return {
temMALFORMED,
"Malformed transaction: does not specify exactly one of Amount and DeliverMin."};
JLOG(ctx.j.warn()) << "Malformed transaction: "
"does not specify exactly one of Amount and DeliverMin.";
return temMALFORMED;
}
// Make sure the amount is valid.
@@ -76,7 +72,8 @@ CheckCash::preflight(PreflightContext const& ctx)
if (badAsset() == value.asset())
{
return {temBAD_CURRENCY, "Malformed transaction: Bad currency."};
JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency.";
return temBAD_CURRENCY;
}
return tesSUCCESS;
@@ -88,14 +85,16 @@ CheckCash::preclaim(PreclaimContext const& ctx)
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
if (!sleCheck)
{
return {tecNO_ENTRY, "Check does not exist."};
JLOG(ctx.j.warn()) << "Check does not exist.";
return tecNO_ENTRY;
}
// Only cash a check with this account as the destination.
AccountID const dstId = sleCheck->at(sfDestination);
if (ctx.tx[sfAccount] != dstId)
{
return {tecNO_PERMISSION, "Cashing a check with wrong Destination."};
JLOG(ctx.j.warn()) << "Cashing a check with wrong Destination.";
return tecNO_PERMISSION;
}
AccountID const srcId = sleCheck->at(sfAccount);
if (srcId == dstId)
@@ -103,7 +102,8 @@ 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
return {tecINTERNAL, "Malformed transaction: Cashing check to self."};
JLOG(ctx.j.error()) << "Malformed transaction: Cashing check to self.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
{
@@ -112,20 +112,23 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (!sleSrc || !sleDst)
{
// If the check exists this should never occur.
return {tecNO_ENTRY, "Malformed transaction: source or destination not in ledger"};
JLOG(ctx.j.warn()) << "Malformed transaction: source or destination not in ledger";
return tecNO_ENTRY;
}
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.
return {tecDST_TAG_NEEDED, "Malformed transaction: DestinationTag required in check."};
JLOG(ctx.j.warn()) << "Malformed transaction: DestinationTag required in check.";
return tecDST_TAG_NEEDED;
}
}
if (hasExpired(ctx.view, sleCheck->at(~sfExpiration)))
{
return {tecEXPIRED, "Cashing a check that has already expired."};
JLOG(ctx.j.warn()) << "Cashing a check that has already expired.";
return tecEXPIRED;
}
{
@@ -144,16 +147,19 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (!equalTokens(value.asset(), sendMax.asset()))
{
return {temMALFORMED, "Check cash does not match check currency."};
JLOG(ctx.j.warn()) << "Check cash does not match check currency.";
return temMALFORMED;
}
AccountID const issuerId{value.getIssuer()};
if (issuerId != sendMax.getIssuer())
{
return {temMALFORMED, "Check cash does not match check issuer."};
JLOG(ctx.j.warn()) << "Check cash does not match check issuer.";
return temMALFORMED;
}
if (value > sendMax)
{
return {tecPATH_PARTIAL, "Check cashed for more than check sendMax."};
JLOG(ctx.j.warn()) << "Check cashed for more than check sendMax.";
return tecPATH_PARTIAL;
}
// Make sure the check owner holds at least value. If they have
@@ -176,7 +182,8 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (value > availableFunds)
{
return {tecPATH_PARTIAL, "Check cashed for more than owner's balance."};
JLOG(ctx.j.warn()) << "Check cashed for more than owner's balance.";
return tecPATH_PARTIAL;
}
}
@@ -219,7 +226,9 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (!isAuthorized)
{
return {tecNO_AUTH, "Can't receive IOUs from issuer without auth."};
JLOG(ctx.j.warn()) << "Can't receive IOUs from "
"issuer without auth.";
return tecNO_AUTH;
}
}
@@ -232,7 +241,8 @@ CheckCash::preclaim(PreclaimContext const& ctx)
// not be frozen.
if (isFrozen(ctx.view, dstId, currency, issuerId))
{
return {tecFROZEN, "Cashing a check to a frozen trustline."};
JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline.";
return tecFROZEN;
}
return tesSUCCESS;
@@ -256,7 +266,8 @@ CheckCash::preclaim(PreclaimContext const& ctx)
if (isFrozen(ctx.view, dstId, issue))
{
return {tecLOCKED, "Cashing a check to a frozen MPT."};
JLOG(ctx.j.warn()) << "Cashing a check to a frozen MPT.";
return tecLOCKED;
}
if (auto const err = canTransfer(ctx.view, issue, srcId, dstId);
@@ -284,7 +295,8 @@ CheckCash::doApply()
if (!sleCheck)
{
// LCOV_EXCL_START
return {tecFAILED_PROCESSING, "Precheck did not verify check's existence."};
JLOG(j_.fatal()) << "Precheck did not verify check's existence.";
return tecFAILED_PROCESSING;
// LCOV_EXCL_STOP
}
@@ -292,7 +304,8 @@ CheckCash::doApply()
if (!psb.exists(keylet::account(srcId)) || !psb.exists(keylet::account(accountID_)))
{
// LCOV_EXCL_START
return {tecFAILED_PROCESSING, "Precheck did not verify source or destination's existence."};
JLOG(ctx_.journal.fatal()) << "Precheck did not verify source or destination's existence.";
return tecFAILED_PROCESSING;
// LCOV_EXCL_STOP
}
@@ -549,7 +562,8 @@ CheckCash::doApply()
{
if (result.actualAmountOut < *optDeliverMin)
{
return {tecPATH_PARTIAL, "flow did not produce DeliverMin."};
JLOG(ctx_.journal.warn()) << "flow did not produce DeliverMin.";
return tecPATH_PARTIAL;
}
ctx_.deliver(result.actualAmountOut);
}
@@ -569,7 +583,8 @@ CheckCash::doApply()
keylet::ownerDir(accountID_), sleCheck->at(sfDestinationNode), sleCheck->key(), true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete check from destination."};
JLOG(j_.fatal()) << "Unable to delete check from destination.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
@@ -577,7 +592,8 @@ CheckCash::doApply()
if (!psb.dirRemove(keylet::ownerDir(srcId), sleCheck->at(sfOwnerNode), sleCheck->key(), true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete check from owner."};
JLOG(j_.fatal()) << "Unable to delete check from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
#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>
@@ -151,7 +152,8 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::ref sle, beast::Journal j)
if (!view.dirRemove(keylet::ownerDir(delegator), (*sle)[sfOwnerNode], sle->key(), false))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Delegate from owner."};
JLOG(j.fatal()) << "Unable to delete Delegate from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
@@ -161,7 +163,8 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::ref sle, beast::Journal j)
if (!view.dirRemove(keylet::ownerDir(delegatee), *optPage, sle->key(), false))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Delegate from authorized account."};
JLOG(j.fatal()) << "Unable to delete Delegate from authorized account.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
#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>
@@ -38,7 +39,8 @@ AMMDelete::preclaim(PreclaimContext const& ctx)
auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
if (!ammSle)
{
return {terNO_AMM, "AMM Delete: Invalid asset pair."};
JLOG(ctx.j.debug()) << "AMM Delete: Invalid asset pair.";
return terNO_AMM;
}
auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
#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>
@@ -40,7 +41,8 @@ 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
return {tefBAD_LEDGER, "Unable to delete DID from owner."};
JLOG(j.fatal()) << "Unable to delete DID from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}

View File

@@ -1,5 +1,6 @@
#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>
@@ -144,7 +145,8 @@ EscrowCancel::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Escrow from owner."};
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
@@ -155,7 +157,8 @@ EscrowCancel::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
@@ -199,7 +202,8 @@ EscrowCancel::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}

View File

@@ -1,5 +1,6 @@
#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>
@@ -321,7 +322,8 @@ EscrowFinish::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Escrow from owner."};
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
@@ -332,7 +334,8 @@ EscrowFinish::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
@@ -387,7 +390,8 @@ EscrowFinish::doApply()
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Unable to delete Escrow from recipient."};
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}

View File

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

View File

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

View File

@@ -67,16 +67,19 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
if (isPseudoAccount(ctx.view, dstAcct))
{
return {tecPSEUDO_ACCOUNT, "Trying to withdraw into a pseudo-account."};
JLOG(ctx.j.warn()) << "Trying to withdraw into a pseudo-account.";
return tecPSEUDO_ACCOUNT;
}
auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
if (!sleBroker)
{
return {tecNO_ENTRY, "LoanBroker does not exist."};
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (account != sleBroker->at(sfOwner))
{
return {tecNO_PERMISSION, "Account is not the owner of the LoanBroker."};
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)

View File

@@ -46,14 +46,16 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
if (!sleBroker)
{
return {tecNO_ENTRY, "LoanBroker does not exist."};
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
auto const brokerOwner = sleBroker->at(sfOwner);
if (account != brokerOwner)
{
return {tecNO_PERMISSION, "Account is not the owner of the LoanBroker."};
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
if (auto const ownerCount = sleBroker->at(sfOwnerCount); ownerCount != 0)
{
@@ -168,21 +170,18 @@ LoanBrokerDelete::doApply()
// obligations associated with the broker or broker pseudo-account.
if (*brokerPseudoSLE->at(sfBalance))
{
return {
tecHAS_OBLIGATIONS,
"LoanBrokerDelete: Pseudo-account has a balance"}; // LCOV_EXCL_LINE
JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a balance";
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
}
if (brokerPseudoSLE->at(sfOwnerCount) != 0)
{
return {
tecHAS_OBLIGATIONS,
"LoanBrokerDelete: Pseudo-account still owns objects"}; // LCOV_EXCL_LINE
JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account still owns objects";
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
}
if (auto const directory = keylet::ownerDir(brokerPseudoID); view().read(directory))
{
return {
tecHAS_OBLIGATIONS,
"LoanBrokerDelete: Pseudo-account has a directory"}; // LCOV_EXCL_LINE
JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a directory";
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
}
view().erase(brokerPseudoSLE);

View File

@@ -99,13 +99,15 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
auto const sleVault = ctx.view.read(keylet::vault(vaultID));
if (!sleVault)
{
return {tecNO_ENTRY, "Vault does not exist."};
JLOG(ctx.j.warn()) << "Vault does not exist.";
return tecNO_ENTRY;
}
Asset const asset = sleVault->at(sfAsset);
if (account != sleVault->at(sfOwner))
{
return {tecNO_PERMISSION, "Account is not the owner of the Vault."};
JLOG(ctx.j.warn()) << "Account is not the owner of the Vault.";
return tecNO_PERMISSION;
}
if (auto const brokerID = tx[~sfLoanBrokerID])
@@ -115,15 +117,18 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
auto const sleBroker = ctx.view.read(keylet::loanBroker(*brokerID));
if (!sleBroker)
{
return {tecNO_ENTRY, "LoanBroker does not exist."};
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (vaultID != sleBroker->at(sfVaultID))
{
return {tecNO_PERMISSION, "Can not change VaultID on an existing LoanBroker."};
JLOG(ctx.j.warn()) << "Can not change VaultID on an existing LoanBroker.";
return tecNO_PERMISSION;
}
if (account != sleBroker->at(sfOwner))
{
return {tecNO_PERMISSION, "Account is not the owner of the LoanBroker."};
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
if (auto const debtMax = tx[~sfDebtMaximum])
@@ -132,7 +137,8 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
if (*debtMax != 0 && *debtMax < currentDebtTotal)
{
return {tecLIMIT_EXCEEDED, "Cannot reduce DebtMaximum below current DebtTotal."};
JLOG(ctx.j.warn()) << "Cannot reduce DebtMaximum below current DebtTotal.";
return tecLIMIT_EXCEEDED;
}
}
}
@@ -143,7 +149,8 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
if (auto const ter = checkFrozen(ctx.view, sleVault->at(sfAccount), sleVault->at(sfAsset)))
{
return {ter, "Vault pseudo-account is frozen."};
JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen.";
return ter;
}
}
@@ -176,7 +183,8 @@ LoanBrokerSet::doApply()
{
// This should be impossible
// LCOV_EXCL_START
return {tefBAD_LEDGER, "LoanBroker does not exist."};
JLOG(j_.fatal()) << "LoanBroker does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
@@ -204,7 +212,8 @@ LoanBrokerSet::doApply()
{
// This should be impossible
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Vault does not exist."};
JLOG(j_.fatal()) << "Vault does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const vaultPseudoID = sleVault->at(sfAccount);
@@ -216,7 +225,8 @@ LoanBrokerSet::doApply()
{
// This should be impossible
// LCOV_EXCL_START
return {tefBAD_LEDGER, "Account does not exist."};
JLOG(j_.fatal()) << "Account does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto broker = std::make_shared<SLE>(keylet::loanBroker(accountID_, sequence));

View File

@@ -1,5 +1,6 @@
#include <xrpl/tx/transactors/lending/LoanDelete.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h> // IWYU pragma: keep
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
@@ -43,11 +44,13 @@ LoanDelete::preclaim(PreclaimContext const& ctx)
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
return {tecNO_ENTRY, "Loan does not exist."};
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
if (loanSle->at(sfPaymentRemaining) > 0)
{
return {tecHAS_OBLIGATIONS, "Active loan can not be deleted."};
JLOG(ctx.j.warn()) << "Active loan can not be deleted.";
return tecHAS_OBLIGATIONS;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
@@ -59,7 +62,8 @@ LoanDelete::preclaim(PreclaimContext const& ctx)
}
if (loanBrokerSle->at(sfOwner) != account && loanSle->at(sfBorrower) != account)
{
return {tecNO_PERMISSION, "Account is not Loan Broker Owner or Loan Borrower."};
JLOG(ctx.j.warn()) << "Account is not Loan Broker Owner or Loan Borrower.";
return tecNO_PERMISSION;
}
return tesSUCCESS;

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