Compare commits

...
Author SHA1 Message Date
Denis Angell 6cb72aa5e0 fix: Rethrow after trimming inner results when an inner Batch apply throws 2026-10-08 04:07:55 +00:00
Denis Angell 70764e9362 fix: Regenerate levelization ordering for Batch_test's RelationalDatabase include 2026-10-08 03:31:07 +00:00
Denis Angell 1f6679938e Local merge develop into dangell7/batch-db 2026-10-08 02:08:05 +00:00
Alex Kremer d343b542f1 refactor: Migrate handlers to rpc-spec (B) (#8484) 2026-10-07 13:46:37 +00:00
Ayaz Salikhov 7acd719bf7 build: Use images with Clang 23 except for clang-tidy (#8530) 2026-10-07 13:08:20 +00:00
Ayaz Salikhov b4564d5301 chore: Update pre-commit hooks and image (#8528) 2026-10-07 10:59:37 +00:00
Shawn Xie 60195e6d37 fix: Fix MPT partial payment overflow (#8302) 2026-10-06 23:16:13 +00:00
Ayaz Salikhov 3d526d456e build: Use LLVM 23 (#8522) 2026-10-06 21:19:57 +00:00
Denis AngellandMayukha Vadari f05c9f7913 refactor: Extract escrow lock helpers and paychan test helpers (#7882)
Co-authored-by: Mayukha Vadari <[email protected]>
2026-10-06 19:58:52 +00:00
Harshit GuptaandMayukha Vadari c2a4bc3aa1 fix: Validate vetoed parameter type in feature RPC (#7583)
Co-authored-by: Mayukha Vadari <[email protected]>
2026-10-06 19:44:56 +00:00
Mayukha Vadari 718185e3b2 refactor: Use CheckEntry everywhere (#8349) 2026-10-06 19:26:35 +00:00
Ayaz Salikhov 1d7783bb86 build: Make conan retry with Conan Center's source backups (#8524) 2026-10-06 19:23:38 +00:00
Mayukha Vadari 9fd2c552f5 refactor: Use AmendmentsEntry everywhere (#8368) 2026-10-06 17:59:44 +00:00
Ayaz Salikhov 70b8fd301b chore: Update docker images; link Conan-built tools with a static runtime (#8520) 2026-10-06 16:47:35 +00:00
Mayukha Vadari e6564f553d refactor: Use NegativeUNLEntry everywhere (#8365) 2026-10-06 14:23:24 +00:00
Ayaz Salikhov ed96e60ce3 build: Make clang-tools custom in Nix, to match what's being built (#8521) 2026-10-06 13:59:12 +00:00
Alex Kremer 2ebd745a1b refactor: Migrate handlers to rpc-spec (A) (#8345) 2026-10-06 13:19:20 +00:00
Mayukha Vadari cfcbe45b60 test: Declare, not define, entry instantiations in SLEBase test (#8357) 2026-10-06 12:12:22 +00:00
yinyiqian1andBart 9cbf78ba99 test: Add more tests for granular permissions (#8459)
Co-authored-by: Bart <[email protected]>
2026-10-05 23:18:58 +00:00
Timur Yalymov b8d8738f81 fix: Enforce that MPT issuance flags are never cleared (#8152) 2026-10-05 23:17:36 +00:00
63c97e719f feat: Allow lending transactions in Batch (#8244)
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Vito Tumas <[email protected]>
2026-10-05 23:17:26 +00:00
3ac26f23c7 feat: Add full support for all objects in ledger_entry (#6319)
Co-authored-by: Timur Yalymov <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Vito Tumas <[email protected]>
Co-authored-by: Ayaz Salikhov <[email protected]>
2026-10-05 23:17:14 +00:00
BartandBart ff9410bc56 build: Define XRPL_ASAN, XRPL_TSAN, and XRPL_UBSAN compile definitions (#8483)
Co-authored-by: Bart <[email protected]>
2026-10-05 23:15:08 +00:00
Mayukha Vadari 1798262d7a refactor: Use LedgerHashesEntry everywhere (#8369) 2026-10-05 18:15:02 +00:00
Mayukha Vadari cd633b9ac9 refactor: Use EscrowEntry everywhere (#8354) 2026-10-05 17:51:18 +00:00
Ayaz Salikhov 5c5f7d315a chore: Update nix flake file (#8479) 2026-10-05 16:58:41 +00:00
Mayukha Vadari 0f7493ce50 refactor: Use FeeSettingsEntry everywhere (#8370) 2026-10-05 16:56:41 +00:00
Harshit Gupta 40f61f828a fix: Add string type validation for channel_id and signature (#7582) 2026-10-05 16:55:14 +00:00
Mayukha Vadari 108277f4b7 test: Migrate three beast suites from beast::unit_test to gtest (#7993) 2026-10-05 15:28:05 +00:00
Denis Angell b3abd3243c Local merge develop into dangell7/batch-db 2026-10-03 18:07:30 +00:00
Denis Angell a99ee034b9 docs: Correct tfOnlyOne's stop condition in the inner_results changelog entry 2026-10-03 12:41:54 +00:00
Denis Angell 2b069f863a fix: Discard partial BatchInnerResult entries when an inner apply throws 2026-10-03 12:41:54 +00:00
Denis Angell d0dbd1527a docs: Note that inner_results only covers evaluated inner transactions 2026-10-03 07:33:12 +00:00
Denis Angell fa5baf2cc4 fix: Scope BatchInnerResults lookup to the transaction's own ledger sequence 2026-10-03 07:33:07 +00:00
Denis Angell 775759eddf docs: Count every inner transaction in the 3.2.0 inner_results changelog entry 2026-10-02 22:32:40 +00:00
Denis Angell 17777928c7 Local merge develop into dangell7/batch-db 2026-10-02 06:09:20 +00:00
Denis Angell 26da18be9f Local merge develop into dangell7/batch-db 2026-10-01 21:12:36 +00:00
Denis Angell b1a9539a6c feat: Record Batch inner results for validated ledgers and return them from tx 2026-09-02 16:00:41 -04:00
139 changed files with 4792 additions and 2503 deletions

No files matched your search

+72
View File
@@ -108,3 +108,75 @@ endfunction()
function(patch_nix_binary target)
endfunction()
function(rpcspec_generate_instantiations)
set(options)
set(oneValueArgs OUT_VAR VALUE_TYPE VIEW_HEADER INCLUDE_DIR)
set(multiValueArgs HANDLERS)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(corrosion_import_crate)
set(options
ALL_FEATURES
NO_DEFAULT_FEATURES
NO_STD
NO_LINKER_OVERRIDE
NO_USES_TERMINAL
LOCKED
FROZEN
)
set(oneValueArgs MANIFEST_PATH PROFILE IMPORTED_CRATES)
set(multiValueArgs
CRATE_TYPES
CRATES
FEATURES
FLAGS
OVERRIDE_CRATE_TYPE
)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(corrosion_set_env_vars target_name env_var)
endfunction()
function(corrosion_add_cxxbridge cxx_target)
set(options)
set(oneValueArgs CRATE)
set(multiValueArgs FILES)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(_unlink_libgcc_s crate)
endfunction()
function(add_xrpl_crate name)
set(options)
set(oneValueArgs CRATE)
set(multiValueArgs FILES)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
+9 -1
View File
@@ -44,4 +44,12 @@ runs:
--conf:all tools.build:jobs=${BUILD_NPROC} \
--conf:all tools.build:verbosity="${LOG_VERBOSITY}" \
--conf:all tools.compilation:verbosity="${LOG_VERBOSITY}" \
.
--format=json \
. >"${RUNNER_TEMP}/conan-graph.json"
# Tools that run during the build may only load glibc from the Nix store,
# as their package ID survives a GCC runtime update.
- name: Check build-context packages for Nix store dependencies (Linux)
if: ${{ runner.os == 'Linux' }}
shell: bash
run: ./bin/check-build-context-runtime.sh "${RUNNER_TEMP}/conan-graph.json"
@@ -75,6 +75,7 @@ test.app > xrpl.json
test.app > xrpl.ledger
test.app > xrpl.nodestore
test.app > xrpl.protocol
test.app > xrpl.rdb
test.app > xrpl.resource
test.app > xrpl.server
test.app > xrpl.shamap
+4 -4
View File
@@ -1,5 +1,5 @@
{
"image_tag": "sha-060957e",
"image_tag": "sha-3d526d4",
"configs": {
"ubuntu": [
{
@@ -74,7 +74,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f"
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-e6055dd"
}
},
{
@@ -86,7 +86,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON -Dassert=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-e6055dd",
"variant": "assert"
}
}
@@ -101,7 +101,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON",
"package": {
"type": "rpm",
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-3a2d19f"
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-e6055dd"
}
}
]
-1
View File
@@ -13,7 +13,6 @@ on:
- "!nix/devshell.nix"
- "!nix/check-tools/**"
- "bin/default-loader-path.sh"
- "bin/install-sanitizer-libs.sh"
pull_request:
paths:
- ".github/workflows/build-nix-images.yml"
@@ -5,7 +5,6 @@ on:
branches:
- develop
paths:
- ".github/workflows/build-packaging-images.yml"
- "bin/install-packaging-tools.sh"
- "package/images/packaging/**"
pull_request:
@@ -5,7 +5,6 @@ on:
branches:
- develop
paths:
- ".github/workflows/build-pre-commit-image.yml"
- "bin/pre-commit/Dockerfile"
- "rust-toolchain.toml"
pull_request:
+1 -1
View File
@@ -34,7 +34,7 @@ permissions:
jobs:
audit:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
permissions:
contents: read
# Needed to open an issue on scheduled failures.
+1 -1
View File
@@ -17,4 +17,4 @@ jobs:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@279ec358f4a1be4088be3e024b07916fa97c75b6
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-473fe44" }'
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-70b8fd3" }'
+1 -1
View File
@@ -41,7 +41,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+1 -1
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-060957e"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-ed96e60"
permissions:
contents: read
issues: write
+3 -3
View File
@@ -28,7 +28,7 @@ permissions:
jobs:
clippy:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -41,7 +41,7 @@ jobs:
coverage:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -70,7 +70,7 @@ jobs:
doc:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+1 -1
View File
@@ -40,7 +40,7 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
+6 -5
View File
@@ -60,7 +60,7 @@ repos:
types_or: [c++, c]
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: e2b496dc2bd8340c2524cb9a2d2a943cde1bb6df # frozen: v23.1.1
rev: a9a8a861f30ed207ead7d5a3b7e8032283ba5da7 # frozen: v23.1.2
hooks:
- id: clang-format
args: [--style=file]
@@ -82,9 +82,10 @@ repos:
files: ^crates/.*\.rs$
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
rev: f1c4833f8cf23c6d952673abc73525411a5719e8 # frozen: 0.29.1
rev: 28010ddd6016e1a0f7bd232acb6536ef996ae897 # frozen: 0.29.2
hooks:
- id: gersemi
args: [-i, --warnings-as-errors]
- repo: https://github.com/rbubley/mirrors-prettier
rev: ef4a397f916211b4a39ccf9d3d9cbb6562157251 # frozen: v3.9.9
@@ -95,19 +96,19 @@ repos:
# Scoped to package/: the rest of the repo's Python has pre-existing findings,
# so widening these is its own change.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: a56c0b927e6465d37cae3e97d35d4d18ab2b96cd # frozen: v0.16.9
rev: f12be1ebaa5351c1fc76472de98db2c3446c8253 # frozen: v0.16.10
hooks:
- id: ruff-check
args: [--fix]
files: ^package/.*\.py$
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 4160603246a6b365d4a2af661c6d71b0a0f50478 # frozen: 26.5.1
rev: 96ae3e5802f3fe2d551e703e18f0a367d1a81ac2 # frozen: 26.10.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 7ff8d35ae36a7d2b968f2f90b4c723e292e594ee # frozen: v2.3.1
rev: 2834ec6639549dd6796205c8f011dedcd587288b # frozen: v2.4.0
hooks:
- id: mypy
args: [--strict]
+12
View File
@@ -29,6 +29,16 @@ Version 3.5.0 is not yet released.
### Additions in 3.5.0
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319))
### Bugfixes in 3.5.0
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
### Bugfixes in 3.5.0
- `feature`: The admin-only `vetoed` field now returns `invalidParams` unless its value is a boolean. [#7583](https://github.com/XRPLF/rippled/pull/7583)
## XRP Ledger server version 3.4.0
@@ -50,6 +60,7 @@ Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
- `noripple_check`: The `transactions` field is no longer included in error responses; it is still returned (possibly as an empty array) whenever `transactions` is `true` and the request succeeds. A malformed `account` is now rejected before the ledger is looked up, so that error response no longer carries the `ledger_hash`, `ledger_index`, and `validated` fields ([#6303](https://github.com/XRPLF/rippled/pull/6303)).
- `transaction_entry`: An object or an array in `tx_hash` now returns `malformedRequest`, like any other value that is not a hex hash, instead of an `internal` error.
## XRP Ledger server version 3.3.0
@@ -71,6 +82,7 @@ This release contains bug fixes only and no API changes.
### Additions in 3.2.0
- `tx`: A validated `Batch` transaction now includes `inner_results`, an array in `RawTransactions` order with one entry per inner transaction that was evaluated, whether or not it was applied: `hash`, `result` (the transaction result code) and `applied` (whether its changes ended up in the ledger; `false` for an inner transaction that failed with a `ter`/`tem`/`tef` code, and for every inner transaction of a `tfAllOrNothing` batch that rolled back). `tfOnlyOne` stops evaluating after the first inner that returns `tesSUCCESS`, not merely applies (an applied `tec` does not stop it), and `tfUntilFailure` stops after the first inner that fails, so later `RawTransactions` entries have no corresponding `inner_results` entry. Inner transactions that fail are not in the ledger, so this is the only record of them. The field is present only when this node built the validated ledger itself and has `[ledger_tx_tables]` enabled; a node that acquired the ledger from the network omits it.
- `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681))
- `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)):
- `TRANSACTION_FORMATS`: Describes the fields and their optionality for each transaction type, including common fields shared across all transactions.
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Fail if a binary of a build-context Conan package loads anything from the Nix
# store other than glibc, or cannot resolve a library at all.
#
# Only binaries linked by the Nix toolchain are checked, i.e. those recording a
# store path as their interpreter or RUNPATH. Prebuilt upstream binaries (such
# as the ones the cmake package ships) use the system loader instead.
#
# Build-context packages provide the tools that run during the build (protoc,
# grpc_cpp_plugin, ...). Their package ID does not change when a Nix toolchain
# update moves the GCC runtime to a new store path, so a cached binary has to
# get by with the pinned glibc alone. See docs/build/nix.md.
#
# Usage: bin/check-build-context-runtime.sh <graph.json>
# <graph.json> is the output of `conan install --format=json`.
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "usage: $0 <graph.json>" >&2
exit 2
fi
if [ "$(uname -s)" != "Linux" ]; then
echo "$0: Linux only" >&2
exit 2
fi
folders="$(jq -r '.graph.nodes[] | select(.context == "build" and .package_folder) | .package_folder' "$1" | sort -u)"
checked=0
failed=0
while IFS= read -r file; do
case "$(file -b "${file}")" in
ELF*) ;;
*) continue ;;
esac
[[ "$(readelf -ldW "${file}")" == */nix/store/* ]] || continue
checked=$((checked + 1))
# `ldd` lists the interpreter and every library as the loader resolves them.
if deps="$(ldd "${file}" 2>&1)"; then
bad="$(printf '%s\n' "${deps}" |
grep -E 'not found|/nix/store/' |
grep -vE '/nix/store/[^/]+-glibc-[^/]+/' || true)"
else
case "${deps}" in
*"not a dynamic executable"*) continue ;;
esac
bad="${deps}"
fi
if [ -n "${bad}" ]; then
failed=$((failed + 1))
echo "::error file=${file}::loads a library from the Nix store other than glibc"
echo "${file}"
echo "${bad}" | sed 's/^/ /'
fi
done < <(
# shellcheck disable=SC2086 # one folder per line, no spaces in Conan paths
[ -z "${folders}" ] || find ${folders} -type f \( -perm -u+x -o -name '*.so*' \)
)
echo "Build-context packages: checked ${checked} binaries, ${failed} failed."
[ "${failed}" -eq 0 ]
+17 -6
View File
@@ -20,9 +20,11 @@
# development setups, but not in the macOS CI environment. They are checked
# everywhere except when running in CI on macOS.
#
# Tools that Nix also exposes under a version-suffixed name (`clang-tidy-22`,
# `g++-15`, ...) are probed under both names: a suffixed name can break while
# the plain one still works (see mkVersionedToolLinks in nix/packages.nix).
# Tools that Nix also exposes under a version-suffixed name
# (`clang-tidy-<v>`, `g++-<v>`, ...) are probed under both names:
# a suffixed name can break while the plain one still works
# (see mkVersionedToolLinks in nix/packages.nix).
# The suffix is the major version of the plain `clang` / `gcc` on PATH.
#
# Tools scoped to a single dev shell rather than to commonPackages are checked
# only in that shell, keyed off XRPL_DEVSHELL.
@@ -34,9 +36,16 @@
set -uo pipefail
# Version suffixes of the Nix tool links, tracking nix/packages.nix.
gcc_version=15
llvm_version=22
# major_version <compiler>
# Major version of a compiler on PATH, or "unknown" when it isn't there.
major_version() {
local version
version="$("$1" -dumpversion 2>/dev/null)" || version=""
version="${version%%.*}"
printf '%s' "${version:-unknown}"
}
llvm_version="$(major_version clang)"
missing=()
checked=0
@@ -112,6 +121,7 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
check ClangBuildAnalyzer
check curl
check file
check jq
check less
check make
# net-tools netstat reports "net-tools X.Y"; macOS ships BSD netstat with no
@@ -180,6 +190,7 @@ fi
if [ "${os}" = "linux" ]; then
echo
echo "GCC toolchain:"
gcc_version="$(major_version gcc)"
check gcc
check "gcc-${gcc_version}"
check g++
+1 -1
View File
@@ -23,7 +23,7 @@ apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
ARG PRE_COMMIT_VERSION=4.6.0
ARG PRE_COMMIT_VERSION=4.6.2
RUN pip install --no-cache --break-system-packages \
pre-commit==${PRE_COMMIT_VERSION}
+20 -7
View File
@@ -26,8 +26,6 @@ import sys
import tempfile
from pathlib import Path
CLANG_TIDY_VERSION = 22
# Extensions run-clang-tidy can analyse: `.cpp` translation units and, thanks to
# the `verify_headers` build option, `.h`/`.hpp` headers (each has its own
# compile_commands.json entry). `.ipp` fragments have no entry and are skipped.
@@ -39,8 +37,21 @@ TIDY_EXTENSIONS = {".cpp", ".h", ".hpp"}
FILEPATH_RE = re.compile(r"^(\s*(?:-\s+)?FilePath:\s*)'((?:[^']|'')*)'\s*$")
def find_tool(name: str) -> str | None:
for candidate in (f"{name}-{CLANG_TIDY_VERSION}", name):
def clang_tidy_major() -> str | None:
"""Major version of the `clang-tidy` on PATH, which run-clang-tidy invokes."""
if not (clang_tidy := shutil.which("clang-tidy")):
return None
output = subprocess.run(
[clang_tidy, "--version"], capture_output=True, text=True
).stdout
m = re.search(r"LLVM version (\d+)", output)
return m.group(1) if m else None
def find_tool(name: str, version: str | None) -> str | None:
"""Prefer `<name>-<version>`, so a host tool of another version can't win."""
candidates = ([f"{name}-{version}"] if version else []) + [name]
for candidate in candidates:
if path := shutil.which(candidate):
return path
return None
@@ -103,8 +114,9 @@ def main():
if not files:
return 0
run_clang_tidy = find_tool("run-clang-tidy")
clang_apply_replacements = find_tool("clang-apply-replacements")
version = clang_tidy_major()
run_clang_tidy = find_tool("run-clang-tidy", version)
clang_apply_replacements = find_tool("clang-apply-replacements", version)
missing = [
name
for name, path in (
@@ -114,9 +126,10 @@ def main():
if not path
]
if missing:
tried = f" (tried the '-{version}' suffix too)" if version else ""
print(
f"clang-tidy check failed: TIDY is enabled but {' and '.join(missing)} "
f"was not found in PATH (tried the '-{CLANG_TIDY_VERSION}' suffix too).",
f"was not found in PATH{tried}.",
file=sys.stderr,
)
return 1
+9
View File
@@ -292,6 +292,14 @@ if(xrpld)
)
target_sources(xrpld PRIVATE ${sources})
rpcspec_generate_instantiations(
OUT_VAR rpcspec_instantiations
VALUE_TYPE "::json::Value"
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
HANDLERS book_changes ledger transaction_entry
)
target_sources(xrpld PRIVATE ${rpcspec_instantiations})
if(tests)
file(
GLOB_RECURSE sources
@@ -322,6 +330,7 @@ if(xrpld)
# antithesis_instrumentation.h, which is not exported as INTERFACE
target_include_directories(
xrpld
SYSTEM
PRIVATE ${CMAKE_SOURCE_DIR}/external/antithesis-sdk
)
endif()
+6 -1
View File
@@ -77,19 +77,24 @@ if(is_clang)
message(STATUS " Ignorelist: ${ignorelist_path}")
endif()
# Define SANITIZERS macro for BuildInfo.cpp
# Define the SANITIZERS macro for BuildInfo.cpp, plus one of XRPL_ASAN,
# XRPL_TSAN and XRPL_UBSAN per active sanitizer, so that code can test for a
# specific one with #ifdef instead of parsing the dot-joined SANITIZERS string.
set(sanitizers_list)
if(SANITIZERS MATCHES "address")
set(enable_asan ON)
list(APPEND sanitizers_list "ASAN")
target_compile_definitions(common INTERFACE XRPL_ASAN)
endif()
if(SANITIZERS MATCHES "thread")
set(enable_tsan ON)
list(APPEND sanitizers_list "TSAN")
target_compile_definitions(common INTERFACE XRPL_TSAN)
endif()
if(SANITIZERS MATCHES "undefinedbehavior")
set(enable_ubsan ON)
list(APPEND sanitizers_list "UBSAN")
target_compile_definitions(common INTERFACE XRPL_UBSAN)
endif()
if(sanitizers_list)
+6 -2
View File
@@ -3,7 +3,7 @@
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"xrpl-rpc-spec/0.1.19#870b2d3abcfbbf13b61c2d3c69495060%1790348286.187549",
"xrpl-rpc-spec/0.1.21#d536f87a2ae7d313452746cfa3ac4404%1790869005.122975",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
@@ -20,7 +20,7 @@
"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#b87796a4269034856cbc1a2522db16eb%1788275071.530512",
"grpc/1.81.1#aaa93ab6cda2f2baa6a84490582c8adf%1791284951.826256",
"fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1785888854.601666",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
@@ -34,11 +34,15 @@
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1782395692.540639",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162",
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
"m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659",
"grpc/1.81.1#aaa93ab6cda2f2baa6a84490582c8adf%1791284951.826256",
"cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654",
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
+9 -1
View File
@@ -3,5 +3,13 @@
core:non_interactive=True
core.download:parallel={{ os.cpu_count() }}
core.upload:parallel={{ os.cpu_count() }}
tools.files.download:retry=5
# Fall back to Conan Center's source backups when a recipe's upstream URL is down
# (e.g. the GNU FTP mirrors), see
# https://github.com/conan-io/conan-center-index/issues/28147#issuecomment-3183544772
# The backups are only tried once every upstream URL has used up its retries,
# so keep the retries low.
core.sources:download_urls=["origin", "https://c3i.jfrog.io/artifactory/conan-center-backup-sources"]
tools.files.download:retry=1
tools.files.download:retry_wait=10
# Fail fast on unreachable hosts (default connect timeout is 30s), keep the 60s read timeout.
core.net.http:timeout=(5, 60)
+24 -3
View File
@@ -29,9 +29,9 @@ os.version={{ min_macos_version }}
[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 its fallback probes a version-suffixed binary (e.g. `g++-<major>`) before plain `g++`. #}
{# Inside the Nix shell the wrapper only provides `g++`/`gcc` (no `-<major>` suffix), #}
{# so on a host that also has a system `g++-<major>` 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) #}
@@ -49,6 +49,17 @@ tools.build:compiler_executables={'c':'{{ cc_exe }}','cpp':'{{ cxx_exe }}'}
user.package:cppstd_version=23
tools.info.package_id:confs+=["user.package:cppstd_version"]
{% if os == "Linux" and context == "build" %}
{# Build-context executables (protoc, grpc_cpp_plugin, build tools) run during the build #}
{# and would otherwise load libstdc++/libgcc from a Nix store path #}
{# that might change with a Nix toolchain update. #}
{# --as-needed drops the ones they link but don't use, #}
{# such as libatomic for grpc_cpp_plugin on arm64. #}
{% set static_runtime_flags = ["-static-libstdc++", "-static-libgcc", "-Wl,--as-needed"] %}
tools.build:exelinkflags+={{ static_runtime_flags }}
tools.info.package_id:confs+=["tools.build:exelinkflags"]
{% endif %}
{% if os == "Macos" %}
[buildenv]
{# os.version adds -mmacosx-version-min to compiler command lines, #}
@@ -58,3 +69,13 @@ tools.info.package_id:confs+=["user.package:cppstd_version"]
{# Scoped to boost/* since it is the only gap. #}
boost/*:MACOSX_DEPLOYMENT_TARGET={{ min_macos_version }}
{% endif %}
{% if os == "Linux" and context == "build" %}
{# b2 links itself with its own script, which ignores exelinkflags #}
{# and only takes CXXFLAGS when use_cxx_env is set. #}
[options]
b2/*:use_cxx_env=True
[buildenv]
b2/*:CXXFLAGS={{ static_runtime_flags | join(" ") }}
{% endif %}
+3
View File
@@ -5,6 +5,9 @@ include(default)
{% if not sanitizers %}
{# Sanitizers not configured; no additional settings needed #}
{% elif context == "build" %}
{# Build-context packages are tools we run, not code we test, #}
{# so don't instrument them #}
{% else %}
{% if compiler == "msvc" %}
+7 -2
View File
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
"nudb/2.0.9",
"openssl/3.6.3",
"soci/4.0.3",
"xrpl-rpc-spec/0.1.19",
"xrpl-rpc-spec/0.1.21",
"zlib/1.3.2",
]
@@ -49,7 +49,8 @@ class Xrpl(ConanFile):
]
tool_requires = [
"protobuf/6.33.5",
"grpc/<host_version>",
"protobuf/<host_version>",
]
default_options = {
@@ -123,6 +124,10 @@ class Xrpl(ConanFile):
"xxhash/*:shared": False,
}
# default_options only reach the host context;
# give tool_requires (and their dependencies) the same dependency options.
default_build_options = {k: v for k, v in default_options.items() if "/" in k}
def set_version(self):
self.version = self.version or DEV_VERSION
+5 -6
View File
@@ -80,13 +80,12 @@ function(add_xrpl_crate name)
# `cc` picks its runtime flag from `crt-static` alone, so it compiles a
# crate's C++ with `-MT`; Debug needs `-MTd` (to match cmake/XrplCompiler.cmake).
if(is_msvc)
corrosion_set_env_vars(
${ARG_CRATE}
"$<$<CONFIG:Debug>:CXXFLAGS=-MTd>"
)
corrosion_set_env_vars(${ARG_CRATE} "$<$<CONFIG:Debug>:CXXFLAGS=-MTd>")
endif()
corrosion_add_cxxbridge(${name}_cxxbridge CRATE ${ARG_CRATE} FILES
${ARG_FILES}
corrosion_add_cxxbridge(
${name}_cxxbridge
CRATE ${ARG_CRATE}
FILES ${ARG_FILES}
)
# Generated cxxbridge headers don't exist at configure time; CMake 3.28+
# validates INTERFACE_SOURCES on consuming targets. Clear it to skip the
+9 -7
View File
@@ -10,14 +10,15 @@ This document explains how to set one up.
support it — see [compiler support for C++23][cpp23-support].
The versions currently tested in CI are:
| Compiler | Version |
| ----------- | ------------------ |
| GCC | 15.2 |
| Clang | 22 |
| Apple Clang | 21 |
| MSVC | Visual Studio 2026 |
| Compiler | Version |
| ----------- | ------------------------------- |
| GCC | `gccVersion` in [packages.nix] |
| Clang | `llvmVersion` in [packages.nix] |
| Apple Clang | 21 |
| MSVC | Visual Studio 2026 |
LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22.
LLVM tools (`clang-tidy` and `clang-format`)
come from the same LLVM release as Clang.
### Older compilers
@@ -156,3 +157,4 @@ version out of the box — run it via `run-clang-tidy`. No separate installation
is needed.
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23
[packages.nix]: ../../nix/packages.nix
+16 -4
View File
@@ -194,10 +194,22 @@ bin/check-nix-store-refs.sh ~/.conan2-nix
```
It works on Linux too, but asserts something narrower there: the toolchain always
writes the store into `PT_INTERP` and `RUNPATH`, and CI builds inside an image
whose store is fixed for its lifetime, so that is fine. Only the binaries
[`PatchNixBinary.cmake`](../../cmake/PatchNixBinary.cmake) retargets to the
system loader have to be clean, and those are what CI checks:
writes the store into `PT_INTERP` and `RUNPATH`. That is fine for the pinned
glibc, whose path does not move, but not for the GCC runtime, which moves with
every GCC update. So [`conan/profiles/default`](../../conan/profiles/default)
links build-context packages, whose executables run during the build, with
`-static-libstdc++ -static-libgcc -Wl,--as-needed`, and
[`conan/profiles/sanitizers`](../../conan/profiles/sanitizers) does not
instrument them. CI checks that they load nothing from the store but glibc, from
the graph `conan install --format=json` writes:
```bash
bin/check-build-context-runtime.sh graph.json
```
Only the binaries [`PatchNixBinary.cmake`](../../cmake/PatchNixBinary.cmake)
retargets to the system loader have to be fully clean, and those are what CI
checks:
```bash
bin/check-nix-store-refs.sh build/xrpld
+1 -1
View File
@@ -6,7 +6,7 @@ project(antithesis-sdk-cpp VERSION 0.4.4 LANGUAGES CXX)
add_library(antithesis-sdk-cpp INTERFACE antithesis_sdk.h)
# Note, both sections below created by xrpld project
target_include_directories(antithesis-sdk-cpp INTERFACE
target_include_directories(antithesis-sdk-cpp SYSTEM INTERFACE
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)
Generated
+6 -6
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1781173989,
"narHash": "sha256-fnzKKPvS+oieI/pTzotA5tkoM47EB1NpaBcgk4R97hE=",
"lastModified": 1791130267,
"narHash": "sha256-1sjwcQMcgAbmiip/VivBSiK8p2bTkQGOvCuO7vnwfx4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8c91a71d13451abc40eb9dae8910f972f979852f",
"rev": "9013764fcc0ea99fa16cf7aa7decf8a5c3889dd2",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
]
},
"locked": {
"lastModified": 1784611586,
"narHash": "sha256-OfqgY+0hp/zseZB7uyH0U8kIDPS4scZZCyAurEplvG0=",
"lastModified": 1791189933,
"narHash": "sha256-Y8eN7ki0Urx4Ojf4w1xSpWg8uk/3lJqK+E0Oth77rH0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "14f58845249f3552a89b07772626b8d3c632fa86",
"rev": "e60029353d0c48d216bc4b065168ccd4079c166f",
"type": "github"
},
"original": {
@@ -17,7 +17,7 @@
namespace xrpl {
template <typename Key>
static std::size_t
std::size_t
extract(Key const& key)
{
return key;
+1 -1
View File
@@ -20,7 +20,7 @@ namespace beast::unit_test {
namespace detail {
template <class String>
static std::string
std::string
makeReason(String const& reason, char const* file, int line)
{
std::string s(reason);
+20
View File
@@ -7,6 +7,7 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/CachedView.h>
#include <xrpl/ledger/RawView.h>
#include <xrpl/protocol/BatchInnerResult.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerHeader.h>
@@ -325,6 +326,24 @@ public:
bool
addSLE(SLE const& sle);
/**
* @brief Inner Batch outcomes observed while this node built the ledger.
*
* Empty for a ledger acquired from the network or loaded from the database. Not part of
* the ledger hash; persisted to the transaction tables when the ledger is saved.
*/
void
setBatchInnerResults(std::vector<BatchInnerResult> results)
{
batchInnerResults_ = std::move(results);
}
std::vector<BatchInnerResult> const&
batchInnerResults() const
{
return batchInnerResults_;
}
//--------------------------------------------------------------------------
void
@@ -433,6 +452,7 @@ private:
Rules rules_;
LedgerHeader header_;
beast::Journal j_;
std::vector<BatchInnerResult> batchInnerResults_;
};
/**
@@ -1,11 +1,17 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <map>
#include <set>
namespace xrpl {
@@ -25,6 +31,52 @@ public:
: Base(keylet::amendments(), view, j)
{
}
/**
* Returns the set of amendments this entry reports as enabled.
*
* @return the set of enabled amendments.
*/
[[nodiscard]] std::set<UInt256>
enabledAmendments() const
{
std::set<UInt256> amendments;
if (this->exists() && (*this)->isFieldPresent(sfAmendments))
{
auto const& v = (*this)->getFieldV256(sfAmendments);
amendments.insert_range(v);
}
return amendments;
}
/**
* Returns a map of amendments that have achieved majority, to the time
* majority was reached.
*
* @return a map of amendment to the time majority was reached.
*/
[[nodiscard]] std::map<UInt256, NetClock::time_point>
majorityAmendments() const
{
std::map<UInt256, NetClock::time_point> ret;
if (this->exists() && (*this)->isFieldPresent(sfMajorities))
{
using TimePoint = NetClock::time_point;
using Duration = TimePoint::duration;
auto const majorities = (*this)->getFieldArray(sfMajorities);
for (auto const& m : majorities)
{
ret[m.getFieldH256(sfAmendment)] = TimePoint(Duration(m.getFieldU32(sfCloseTime)));
}
}
return ret;
}
};
using AmendmentsEntryR = AmendmentsEntry<ReadView>;
@@ -1,11 +1,17 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STVector256.h>
#include <cstddef>
#include <optional>
namespace xrpl {
@@ -25,6 +31,31 @@ public:
: Base(keylet::skip(), view, j)
{
}
/**
* Looks up a hash `diff` slots back from the most recent entry in the
* sfHashes vector (diff == 0 is the most recent entry).
*
* Callers decide which skip list entry to read and how to translate a
* target ledger sequence into `diff`; this only does the bounds check
* and vector indexing shared by both the recent (stride 1) and distant
* (stride 256) skip lists.
*
* @param diff how many slots back from the most recent hash to look up;
* 0 is the most recent hash.
* @return the hash at that slot, or std::nullopt if the entry does not
* exist or `diff` is out of range.
*/
[[nodiscard]] std::optional<UInt256>
hashAt(std::size_t diff) const
{
if (!this->exists())
return std::nullopt;
STVector256 const vec = (*this)->getFieldV256(sfHashes);
if (vec.size() > diff)
return vec[vec.size() - diff - 1];
return std::nullopt;
}
};
using LedgerHashesEntryR = LedgerHashesEntry<ReadView>;
+291 -3
View File
@@ -2,8 +2,10 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -27,6 +29,291 @@
namespace xrpl {
/**
* Validate that @p account may lock @p amount of a token for later delivery
* to @p dest.
*
* The lock-side counterpart of escrowUnlockPreclaimHelper: every issuer
* control (locking opt-in, authorization, freeze/lock, transferability,
* spendable balance) that gates locking token value lives here, so any
* transactor that locks funds applies the same rules. The signature is
* view-based rather than PreclaimContext-based so it can also run from
* doApply.
*/
template <ValidIssueType T>
TER
escrowLockPreclaimHelper(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j);
template <>
inline TER
escrowLockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
auto const& issue = amount.get<Issue>();
auto const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::kZero && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(view, account, issue))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(view, dest, issue))
return tecFROZEN;
STAmount const spendableAmount =
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
inline TER
escrowLockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(view, account, *sleIssuance))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(view, dest, *sleIssuance))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
return ter;
STAmount const spendableAmount = accountHolds(
view,
account,
amount.get<MPTIssue>(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
template <ValidIssueType T>
TER
escrowLockApplyHelper(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal);
template <>
inline TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter =
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
inline TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = lockEscrowMPT(view, sender, amount, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <ValidIssueType T>
TER
escrowUnlockPreclaimHelper(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze = true);
template <>
inline TER
escrowUnlockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (checkFreeze &&
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
template <>
inline TER
escrowUnlockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (checkFreeze && isFrozen(view, account, *sleIssuance))
return tecLOCKED;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
template <ValidIssueType T>
TER
escrowUnlockApplyHelper(
@@ -55,9 +342,6 @@ escrowUnlockApplyHelper<Issue>(
bool createAsset,
beast::Journal journal)
{
auto const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
bool const recvLow = issuer > receiver;
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
@@ -67,6 +351,10 @@ escrowUnlockApplyHelper<Issue>(
if (receiverIssuer)
return tesSUCCESS;
auto const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
bool const recvLow = issuer > receiver;
if (!ctx.view.exists(trustLineKey) && createAsset)
{
// Can the account cover the trust line's reserve?
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
namespace xrpl {
/**
* @brief Outcome of one inner transaction of a Batch, recorded while the outer Batch was
* applied.
*
* Inner transactions that fail with a ter/tem/tef result are never applied and leave no
* metadata in the ledger, and a tfAllOrNothing rollback discards the ones that did apply.
* This record is the only place those outcomes survive. It is node-local and not part of
* the ledger hash.
*/
struct BatchInnerResult
{
uint256 parentBatchId;
uint256 innerTxId;
/**
* @brief Position in the outer transaction's RawTransactions.
*/
std::uint32_t index = 0;
TER ter = tesSUCCESS;
/**
* @brief True if the inner transaction's changes ended up in the ledger.
*/
bool applied = false;
};
} // namespace xrpl
-28
View File
@@ -1,28 +0,0 @@
#pragma once
namespace xrpl {
/**
* @brief Enumeration of ledger shortcuts for specifying which ledger to use.
*
* These shortcuts provide a convenient way to reference commonly used ledgers
* without needing to specify their exact hash or sequence number.
*/
enum class LedgerShortcut {
/**
* The current working ledger (open, not yet closed)
*/
Current,
/**
* The most recently closed ledger (may not be validated)
*/
Closed,
/**
* The most recently validated ledger
*/
Validated
};
} // namespace xrpl
+1
View File
@@ -300,6 +300,7 @@ JSS(in); // out: OverlayImpl
JSS(inLedger); // out: tx/Transaction
JSS(inbound); // out: PeerImp
JSS(index); // in: LedgerEntry
JSS(inner_results); // out: Tx
// out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
JSS(initial_sync_duration_us); //
+16 -1
View File
@@ -72,7 +72,7 @@ inline constexpr std::array<char const*, 5> kLgrDbInit{
// Transaction database holds transactions and public keys
inline constexpr auto kTxDbName{"transaction.db"};
inline constexpr std::array<char const*, 8> kTxDbInit{
inline constexpr std::array<char const*, 10> kTxDbInit{
{"BEGIN TRANSACTION;",
"CREATE TABLE IF NOT EXISTS Transactions ( \
@@ -101,6 +101,21 @@ inline constexpr std::array<char const*, 8> kTxDbInit{
"CREATE INDEX IF NOT EXISTS AcctLgrIndex ON \
AccountTransactions(LedgerSeq, Account, TransID);",
// Outcome of each inner transaction of a Batch this node applied while building a
// validated ledger. Inner transactions that failed are not in the ledger, so this is
// the only record of them. See BatchInnerResult.
"CREATE TABLE IF NOT EXISTS BatchInnerResults ( \
ParentBatchID CHARACTER(64) NOT NULL, \
InnerTxnID CHARACTER(64) NOT NULL, \
TxnIndex INTEGER NOT NULL, \
LedgerSeq BIGINT UNSIGNED NOT NULL, \
TERResult INTEGER NOT NULL, \
Applied INTEGER NOT NULL, \
PRIMARY KEY (ParentBatchID, InnerTxnID) \
);",
"CREATE INDEX IF NOT EXISTS BatchInnerLgrIndex ON \
BatchInnerResults(LedgerSeq);",
"END TRANSACTION;"}};
////////////////////////////////////////////////////////////////////////////////
+13 -16
View File
@@ -7,9 +7,9 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/BatchInnerResult.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/LedgerShortcut.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/TxSearched.h>
@@ -105,21 +105,6 @@ public:
using TxnMetaLedgerType = std::tuple<Blob, Blob, std::uint32_t>;
using MetaTxsList = std::vector<TxnMetaLedgerType>;
using LedgerSequence = uint32_t;
using LedgerHash = UInt256;
using LedgerSpecifier = std::variant<LedgerRange, LedgerShortcut, LedgerSequence, LedgerHash>;
struct AccountTxArgs
{
AccountID account;
std::optional<LedgerSpecifier> ledger;
bool binary = false;
bool forward = false;
uint32_t limit = 0;
std::optional<AccountTxMarker> marker;
std::optional<DelegateFilter> delegate;
};
struct AccountTxResult
{
std::variant<AccountTxs, MetaTxsList> transactions;
@@ -228,6 +213,18 @@ public:
virtual std::optional<LedgerIndex>
getAccountTransactionsMinLedgerSeq() = 0;
/**
* @brief getBatchInnerResults Returns the recorded inner-transaction outcomes of a
* Batch this node applied while building a validated ledger.
* @param parentBatchId Hash of the outer Batch transaction.
* @param ledgerSeq Ledger sequence the outer Batch transaction is validated in; rows
* recorded for any other sequence are not this ledger's and are excluded.
* @return Outcomes in RawTransactions order; empty if the tx tables are disabled or
* nothing was recorded (a ledger acquired from the network records nothing).
*/
virtual std::vector<BatchInnerResult>
getBatchInnerResults(uint256 const& parentBatchId, LedgerIndex ledgerSeq) = 0;
/**
* @brief deleteTransactionByLedgerSeq Deletes transactions from the ledger
* with the given sequence.
+5 -1
View File
@@ -4,12 +4,14 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/BatchInnerResult.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/tx/applySteps.h>
#include <string>
#include <utility>
#include <vector>
namespace xrpl {
@@ -142,6 +144,7 @@ enum class ApplyTransactionResult {
* Provides more detailed logging and decodes the
* correct behavior based on the `TER` type
*
* @param innerResults If set, receives one entry per inner transaction the outer Batch ran.
* @see ApplyTransactionResult
*/
ApplyTransactionResult
@@ -151,6 +154,7 @@ applyTransaction(
STTx const& tx,
bool retryAssured,
ApplyFlags flags,
beast::Journal journal);
beast::Journal journal,
std::vector<BatchInnerResult>* innerResults = nullptr);
} // namespace xrpl
@@ -42,6 +42,14 @@ class ValidMPTIssuance
*/
bool referenceHoldingMutated_ = false;
/**
* Flags cleared on an existing MPTokenIssuance, except lsfMPTLocked,
* which tfMPTUnlock clears legitimately. Every other issuance flag is
* fixed at creation or set-once via MPTokenIssuanceSet, so any bit
* accumulated here is a bug. Enforced post-fixCleanup3_5_0.
*/
std::uint32_t issuanceFlagsCleared_ = 0;
/**
* MPTokens and RippleStates deleted during apply. finalize() checks each
* holder's AccountRoot to detect vault pseudo-account holdings deleted
@@ -57,6 +57,12 @@ public:
ReadView const& view,
beast::Journal const& j) override;
/**
* Inner transaction types that preflight rejects before LendingProtocolV1_2.
*
* Once the amendment is enabled the list only matters when replaying older
* ledgers. Remove it when LendingProtocolV1_2 is retired.
*/
static constexpr auto kDisabledTxTypes = std::to_array<TxType>({
ttVAULT_CREATE,
ttVAULT_SET,
+83 -80
View File
@@ -2,145 +2,148 @@ Detected OS: macos (Darwin arm64)
Core build tools:
✅ cmake
cmake version 4.1.2
/nix/store/gvabsb4yqb5xsqzqph54rijnn4zpihnp-cmake-4.1.2/bin/cmake
cmake version 4.4.3
/nix/store/q85csxf4s4shx89zif097h1ql4ax1rrz-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.28.1
/nix/store/9jiyxmkpwmn6dcqs0765s83riw3l5ail-conan-2.28.1/bin/conan
Conan version 2.33.0
/nix/store/rp0zf22rx3iwnhxp4d53ncirn4lzg68j-conan-2.33.0/bin/conan
✅ git
git version 2.54.0
/nix/store/a14yxcqvv9x2l9mllgpirzhvz93pgprg-git-2.54.0/bin/git
git version 2.55.0
/nix/store/gw7c7m0dwca5lg4152x9lpmxpj171bzw-git-2.55.0/bin/git
✅ python3
Python 3.13.13
/nix/store/ygxqin6ydzjfawywqpp5pal8wv6sf5bh-python3-3.13.13/bin/python3.13
Python 3.14.7
/nix/store/0kcws5blwbdyxqxjd1w8hjipz61xny3k-python3-3.14.7/bin/python3.14
Development tooling:
✅ ccache
ccache version 4.13.6
/nix/store/57davyvs6p6dkrl3svzwg1ph18wsy4cz-ccache-4.13.6/bin/ccache
/nix/store/ydr5nlzxp1djb5256y0zzqa87vz55gsk-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.7
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/rbap7zqq7mw00fyqa02p5rj7gqjp4w5i-clang-22/bin/clang-22
clang version 23.1.0
/nix/store/2c16z6akvv65znb8330sl5qpklaf3194-clang-wrapper-23.1.0/bin/clang
✅ clang-23
clang version 23.1.0
/nix/store/vfd0q2395vyn0bsjsh1b4068icy1x9ph-clang-23/bin/clang-23
✅ clang++
clang version 22.1.7
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/v9haf787f7bcz0mq1sad4bpyx21pj6li-clang++-22/bin/clang++-22
clang version 23.1.0
/nix/store/2c16z6akvv65znb8330sl5qpklaf3194-clang-wrapper-23.1.0/bin/clang++
✅ clang++-23
clang version 23.1.0
/nix/store/0i6kfnif2v4sqyv5fzfg8a9n09rw9lkp-clang++-23/bin/clang++-23
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/4l50ds9fa2mkvh7wg8qzrlbmjs12sb8l-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
/nix/store/c7jjnw29ra78jdlzjww2m374izhy05n8-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
✅ curl
curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
/nix/store/kclq0czaxvsgh4ym9ld7b6iwy50l1snk-curl-8.20.0-bin/bin/curl
curl 8.22.0 (aarch64-apple-darwin25.6.0) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
/nix/store/pq1h17abqy6q5y6k5dn9qj0fwb89flba-curl-8.22.0-bin/bin/curl
✅ file
file-5.47
/nix/store/dax63li7wwcbqxxkkgzc4g2rx7d4w86x-file-5.47/bin/file
file-5.48
/nix/store/y44vpziwbzfm8020dzlqqbybyyy7lb6g-file-5.48/bin/file
✅ jq
jq-1.8.2
/nix/store/wm7v59a2lhj1w2k8jz53spcs1iizm9lv-jq-1.8.2-bin/bin/jq
✅ less
less 692 (PCRE2 regular expressions)
/nix/store/lvr16y75r1pdxpdv0aph5ak2yd0hkvqm-less-692/bin/less
less 710 (PCRE2 regular expressions)
/nix/store/3yn3dx5fwg5i0inqqzwnxq7f6lwhpjqa-less-710/bin/less
✅ make
GNU Make 4.4.1
/nix/store/8wwiw8pwyhrkzyq28hqzxfl4z84lks81-gnumake-4.4.1/bin/make
/nix/store/8l4wgxmhdxfvc9g18y76bnxl1ghgcp7n-gnumake-4.4.1/bin/make
✅ netstat
present
/nix/store/qsd1kzqb0ahrk433vmyl245gp623j19s-network_cmds-730.80.3/bin/netstat
/nix/store/57fqvgdav8lrmzndkwm7qrgksl01yj23-network_cmds-741.100.2/bin/netstat
✅ ninja
1.13.2
/nix/store/bqykhrblarkj4fl0hz2mf8ngwfv6x6bz-ninja-1.13.2/bin/ninja
/nix/store/infm4vlx71q83bqz0k559w1gn4b0rq4i-ninja-1.13.2/bin/ninja
✅ perl
v5.42.0
/nix/store/js13ri9fvm0ajk1fpd3acigys2a9whdv-perl-5.42.0/bin/perl
v5.42.3
/nix/store/7rvr6qjfzk2ybzn1imjw6c6qq1n1a9cr-perl-5.42.3/bin/perl
✅ pkg-config
0.29.2
/nix/store/lzrwr375jqhhbca116kja96xf1md83l8-pkg-config-wrapper-0.29.2/bin/pkg-config
/nix/store/nhgylvj98klqggy54h7gbc3b2ia4a00y-pkg-config-wrapper-0.29.2/bin/pkg-config
✅ vim
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
/nix/store/6vbkykg92w603c0sw3mkk7p7mfaawbns-vim-9.2.0389/bin/vim
/nix/store/akaa9kqm3b8rlkv61kvsqr2l2j70rvxb-vim-9.2.1001/bin/vim
✅ zip
Zip 3.0
/nix/store/z6ph729vcakbvz3wh8ln1wk6mi06w487-zip-3.0/bin/zip
/nix/store/dvawr36npnad4xa6dxi2ss0fw44aa82r-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.7
/nix/store/m3ii69rca4077lf4wlk7m3jcag1fs577-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 23.1.0
/nix/store/mva6ngy45m9in3m2lsfccj1y0z6n1f5s-clang-tools-23.1.0/bin/clang-apply-replacements
✅ clang-apply-replacements-23
clang-apply-replacements version 23.1.0
/nix/store/872k02mqr21hxwcpfwaqkxljfj60aak0-clang-apply-replacements-23/bin/clang-apply-replacements-23
✅ clang-format
clang-format version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-format
✅ clang-format-22
clang-format version 22.1.7
/nix/store/4fawqy6ngqcsqd2ygyyzm93q0xy3f5gs-clang-format-22/bin/clang-format-22
clang-format version 23.1.0
/nix/store/mva6ngy45m9in3m2lsfccj1y0z6n1f5s-clang-tools-23.1.0/bin/clang-format
✅ clang-format-23
clang-format version 23.1.0
/nix/store/wf30x6lcya44n2y1fagd6q7x7p4gyl6f-clang-format-23/bin/clang-format-23
✅ clang-tidy
LLVM version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/jqw4280saixaxxihwdba9ldm2fsm6dr3-clang-tidy-22/bin/clang-tidy-22
LLVM version 23.1.0
/nix/store/mva6ngy45m9in3m2lsfccj1y0z6n1f5s-clang-tools-23.1.0/bin/clang-tidy
✅ clang-tidy-23
LLVM version 23.1.0
/nix/store/kgx00aass66g02p1s4gqqf65kq39m4ci-clang-tidy-23/bin/clang-tidy-23
✅ dot
dot - graphviz version 12.2.1 (0)
/nix/store/ijb4fbnqa6wzlpqnhb6q9knqpf7qqn5z-graphviz-12.2.1/bin/dot
dot - graphviz version 15.1.1 (0)
/nix/store/mc99a6bpbk2waym2cnw6ifmxli4ndb4x-graphviz-15.1.1/bin/dot
✅ doxygen
1.16.1
/nix/store/kbryjdpq9jizjb0ws0nzbf2h2ymbdiwm-doxygen-1.16.1/bin/doxygen
1.17.0
/nix/store/kz43yaj0wg6fx6cbrcmi2442z1ramy0c-doxygen-1.17.0/bin/doxygen
✅ gcovr
gcovr 8.4
/nix/store/wn8jiyh9p0bybs96s4163qp3k8vfmczx-python3.13-gcovr-8.4/bin/gcovr
/nix/store/czj1vxsizrbba280gg22cys40qxkqjgr-python3.14-gcovr-8.4/bin/gcovr
✅ gh
gh version 2.94.0 (nixpkgs)
/nix/store/fhnpw0hs0gjms1ha6ap02jq7rx13gkbp-gh-2.94.0/bin/gh
gh version 2.102.0 (2026-09-30)
/nix/store/s8bahl6zssvry93ihivfmk1l6c58byrr-gh-2.102.0/bin/gh
✅ git-cliff
git-cliff 2.13.1
/nix/store/cy0wwhgxa7yvrz97zydbq6sqmixc90fq-git-cliff-2.13.1/bin/git-cliff
git-cliff 2.14.2
/nix/store/k4m5yk0qb7hfslwsq1zjajz1d94j12bj-git-cliff-2.14.2/bin/git-cliff
✅ git-lfs
git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3)
/nix/store/k9r7zjfjplqa4d5s71cqvf2iv73jd9mc-git-lfs-3.7.1/bin/git-lfs
git-lfs/3.8.0 (3.8.0; darwin arm64; go 1.26.8)
/nix/store/wzixjr3yb40a13m2q654hm3bdjn6sal3-git-lfs-3.8.0/bin/git-lfs
✅ gpg
gpg (GnuPG) 2.4.9
/nix/store/cgh6iwzz5jgx9z5whka4vgj210i6npc6-gnupg-2.4.9/bin/gpg
/nix/store/0ldfwnwx385i5kh7cyvxx4mkvgjhs5rh-gnupg-2.4.9/bin/gpg
✅ pre-commit
pre-commit 4.5.1
/nix/store/z3cca68620w0w10f090szgzdnmh1waf2-pre-commit-4.5.1/bin/pre-commit
pre-commit 4.6.2
/nix/store/ivh54xypg9qvd3ha9lis0i8p4nkf8n7a-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/4x28x911z2f9y7adqlh3qspp4a16dig7-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
/nix/store/zjbzr154xan5xg9pmjiv0zkwklyb94wv-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-23
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/x8iymrh76sk5q91ryg5pa7i32s6gfh34-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/svhfrpgx2j581brsqmkpav9b286v22yy-run-clang-tidy-23/bin/run-clang-tidy-23
Rust toolchain:
✅ cargo
cargo 1.97.1 (c980f4866 2026-06-30)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/cargo
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/cargo
✅ cargo-audit
cargo-audit-audit 0.22.1
/nix/store/snwkga2f5gyf404h7mmp9wriwxb8v65f-cargo-audit-0.22.1/bin/cargo-audit
cargo-audit-audit 0.22.2
/nix/store/7jgrwj557l91940aj0ag4i924ima1cqn-cargo-audit-0.22.2/bin/cargo-audit
✅ cargo-llvm-cov
cargo-llvm-cov 0.8.5
/nix/store/fpiqdh91gwyxalqp409ynm0s0g086w7w-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
cargo-llvm-cov 0.9.0
/nix/store/5q1h285z8zzkw2qxig9a0b1avcsk1cf0-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
✅ cargo-nextest
cargo-nextest 0.9.137
/nix/store/ylz7m947mhkgsp6i7611id3s3gcd58nq-cargo-nextest-0.9.137/bin/cargo-nextest
cargo-nextest 0.9.146
/nix/store/69v3hx3k597hcxd2zy7yad7fg0aw5yxd-cargo-nextest-0.9.146/bin/cargo-nextest
✅ clippy-driver
clippy 0.1.97 (8bab26f4f6 2026-07-14)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/clippy-driver
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/clippy-driver
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f4 2026-07-14)
/nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
/nix/store/30rbyk8gx7v53fxqhs4pp0fvxfjycspr-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly
rustc 1.101.0-nightly (282215592 2026-10-04)
/nix/store/a9552rf3pylcnkijc1z0hf1lcnv50djq-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/rustc
✅ rustfmt
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
/nix/store/5ymwgr9jqjz7zzbmj0j5vqbwcd3kp0vm-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
/nix/store/0jmdipyn8266j9kqx2xxm2qkxlrlrjfc-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 45 checked tools are present and runnable.
✅ All 46 checked tools are present and runnable.
+99 -96
View File
@@ -2,173 +2,176 @@ Detected OS: linux (Linux x86_64)
Core build tools:
✅ cmake
cmake version 4.1.2
/nix/store/r9941n32g4wyvggz2703dlplbdq8a6rd-cmake-4.1.2/bin/cmake
cmake version 4.4.3
/nix/store/jcvvpih1046akxcwh9hdiak5q24jqcf0-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.28.1
/nix/store/lxny9y4jvjdws7hgz1mygvb7hjrpmna5-conan-2.28.1/bin/conan
Conan version 2.33.0
/nix/store/b4lnzrqc60glhl9al1nakfc0vd20kdna-conan-2.33.0/bin/conan
✅ git
git version 2.54.0
/nix/store/bcnisk3ydfgv26v2gw3zlky24g00yww2-git-2.54.0/bin/git
git version 2.55.0
/nix/store/lg46w7hrjx7kylsh6645c9l975i8h43b-git-2.55.0/bin/git
✅ python3
Python 3.13.13
/nix/store/60m4rxhg2fldqaak400c0lry96ijrzqn-python3-3.13.13/bin/python3.13
Python 3.14.7
/nix/store/lb41b0anx1f98y9y5s9mdv97gjgsq740-python3-3.14.7/bin/python3.14
Development tooling:
✅ ccache
ccache version 4.13.6
/nix/store/c9wwl7s5i6rsfwvf4v0xbbmzx5m6jgfr-ccache-4.13.6/bin/ccache
/nix/store/p1a1s700dqsw1b3b8d2ba64nqw68zgpy-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.7
/nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/dagc2rq44gfbr7w7yvvqca3yqpc9gqbq-clang-22/bin/clang-22
clang version 23.1.0
/nix/store/mlqh1xrp0zi3i5g9b0zjcz6zlbbzdgb8-clang-wrapper-23.1.0/bin/clang
✅ clang-23
clang version 23.1.0
/nix/store/xgi7q1bady9dk89zvjmiicjzzp1gvm7x-clang-23/bin/clang-23
✅ clang++
clang version 22.1.7
/nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/l5m8clin1npl605wdkd8mr18ggxww3z4-clang++-22/bin/clang++-22
clang version 23.1.0
/nix/store/mlqh1xrp0zi3i5g9b0zjcz6zlbbzdgb8-clang-wrapper-23.1.0/bin/clang++
✅ clang++-23
clang version 23.1.0
/nix/store/4slsaz29v72xgc4g36cp94f0p41a81pv-clang++-23/bin/clang++-23
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/bshlmn8fqw55nsnm581xqlfbahfkykxx-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
/nix/store/gvx8im1c89vspwb8ixa91lr1dagbi241-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
✅ curl
curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
/nix/store/zbwymrp4lcfjc4kkk0n4779v0kjjz58z-curl-8.20.0-bin/bin/curl
curl 8.22.0 (x86_64-pc-linux-gnu) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
/nix/store/305w9ipyg497mz9dq31abdf61qk58k29-curl-8.22.0-bin/bin/curl
✅ file
file-5.47
/nix/store/bizyfqdw0h67wzqmp10knmf9s2pqahdb-file-5.47/bin/file
file-5.48
/nix/store/g0fbnccaan8q0rxlw43xnbyb90i94vlc-file-5.48/bin/file
✅ jq
jq-1.8.2
/nix/store/m3y7hqmy20nd1ps6qy8410a326255bf0-jq-1.8.2-bin/bin/jq
✅ less
less 692 (PCRE2 regular expressions)
/nix/store/c6bacbn93qg4a7g9n4czww8rg24dvysr-less-692/bin/less
less 710 (PCRE2 regular expressions)
/nix/store/zi4d0awnc6crz18s177bv9y2yz9al3lq-less-710/bin/less
✅ make
GNU Make 4.4.1
/nix/store/d3bwqm6bymhy3pdgbvf7vxjqfp31m3j1-gnumake-4.4.1/bin/make
/nix/store/z565rgmn9vccv2h0g87z725pw610jmm8-gnumake-4.4.1/bin/make
✅ netstat
net-tools 2.10
/nix/store/jmyzqvgflnswmws7rnxx6g3zbj680xvd-net-tools-2.10/bin/netstat
/nix/store/9i6j9lrsliyz1ww2f8xx18z36wc9n38a-net-tools-2.10/bin/netstat
✅ ninja
1.13.2
/nix/store/7a235m7crqbb4h49sak20fqxpw3n7hr0-ninja-1.13.2/bin/ninja
/nix/store/4nsdla28c4177ssp0hlyv5l2yzj8wcba-ninja-1.13.2/bin/ninja
✅ perl
v5.42.0
/nix/store/6plwsm6pkq79yjv4xvy8csk2pd4hzr67-perl-5.42.0/bin/perl
v5.42.3
/nix/store/8g4dmf7l8q1mf9w4wfmlcl8gvdgmhb8v-perl-5.42.3/bin/perl
✅ pkg-config
0.29.2
/nix/store/1m05k7xgfnw6jc21xxk5681ni3ar97wf-pkg-config-wrapper-0.29.2/bin/pkg-config
/nix/store/b9izmrfjp0qnnyahdwwrzdwi87s1h179-pkg-config-wrapper-0.29.2/bin/pkg-config
✅ vim
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
/nix/store/hvyqx52g4g2fxhgpans3fksjj6lmlyaw-vim-9.2.0389/bin/vim
/nix/store/9swnd5pf8p8w1gwcbhdpm2yx705kbp38-vim-9.2.1001/bin/vim
✅ zip
Zip 3.0
/nix/store/qnd2ag67hrjj0b6vbmisdshf50r6s72n-zip-3.0/bin/zip
/nix/store/5ypvkry3qra47dylc9kfk36r1q9vha80-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.7
/nix/store/py2wihg0a96qcppv4hjmww547xabr0fb-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 23.1.0
/nix/store/ipnj8b3s6f00ngi26fi7p5h9lxv6a3s3-clang-tools-23.1.0/bin/clang-apply-replacements
✅ clang-apply-replacements-23
clang-apply-replacements version 23.1.0
/nix/store/lbyc9hrkkymybah7b6n8c7sijlgldfz0-clang-apply-replacements-23/bin/clang-apply-replacements-23
✅ clang-format
clang-format version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-format
✅ clang-format-22
clang-format version 22.1.7
/nix/store/kz820ccifjlwqnwqjsx7kbiajrgsmbrh-clang-format-22/bin/clang-format-22
clang-format version 23.1.0
/nix/store/ipnj8b3s6f00ngi26fi7p5h9lxv6a3s3-clang-tools-23.1.0/bin/clang-format
✅ clang-format-23
clang-format version 23.1.0
/nix/store/40w351b7i7aqhpl5wmicfnp2ij8k12g9-clang-format-23/bin/clang-format-23
✅ clang-tidy
LLVM version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/gdrkvpw846lkyzh8y9p3zx50g6ml2v84-clang-tidy-22/bin/clang-tidy-22
LLVM version 23.1.0
/nix/store/ipnj8b3s6f00ngi26fi7p5h9lxv6a3s3-clang-tools-23.1.0/bin/clang-tidy
✅ clang-tidy-23
LLVM version 23.1.0
/nix/store/40rj38k5ygq7sawkiv0cdaljclbspwz9-clang-tidy-23/bin/clang-tidy-23
✅ dot
dot - graphviz version 12.2.1 (0)
/nix/store/12rgns2296s4qcja778gvcbx61z77rc4-graphviz-12.2.1/bin/dot
dot - graphviz version 15.1.1 (0)
/nix/store/f7r47nc0d13xa4c2xaaxa6dp3pzm9sp1-graphviz-15.1.1/bin/dot
✅ doxygen
1.16.1
/nix/store/k0vzr5lvgq1byraknzwvk51wcgpnsrkh-doxygen-1.16.1/bin/doxygen
1.17.0
/nix/store/q6yx2nsimj9xmckmgw4br2cnapbgnqpk-doxygen-1.17.0/bin/doxygen
✅ gcovr
gcovr 8.4
/nix/store/iyzi7fpyclqrha054adnizvif02lg49x-python3.13-gcovr-8.4/bin/gcovr
/nix/store/shx7mfl8n4kx11icvwyj5x7cmf6z5ahq-python3.14-gcovr-8.4/bin/gcovr
✅ gh
gh version 2.94.0 (nixpkgs)
/nix/store/pidh15szlsb1vc41xdsa3xbdghdazvby-gh-2.94.0/bin/gh
gh version 2.102.0 (2026-09-30)
/nix/store/66h5dr9v94x04myh2kabx06rq9763jfc-gh-2.102.0/bin/gh
✅ git-cliff
git-cliff 2.13.1
/nix/store/1q851fs62shgjhc03fxxdkpzxdjg7k11-git-cliff-2.13.1/bin/git-cliff
git-cliff 2.14.2
/nix/store/sl19b42d69b3w2vb9pxp6jbqk841z8vj-git-cliff-2.14.2/bin/git-cliff
✅ git-lfs
git-lfs/3.7.1 (3.7.1; linux amd64; go 1.26.3)
/nix/store/6ljwpal7b1756708m33vj0crpral7mvl-git-lfs-3.7.1/bin/git-lfs
git-lfs/3.8.0 (3.8.0; linux amd64; go 1.26.8)
/nix/store/rp9ld1f6h83xmf2rb5fpib5wmr46fy9f-git-lfs-3.8.0/bin/git-lfs
✅ gpg
gpg (GnuPG) 2.4.9
/nix/store/wx7vk8babxkgy813r70yc67vcwnmagbx-gnupg-2.4.9/bin/gpg
/nix/store/8gm9aq3sspqmvvh7c0x7djcivj2x7yvn-gnupg-2.4.9/bin/gpg
✅ pre-commit
pre-commit 4.5.1
/nix/store/bj6i9vl34cij5h0r165y40hrjqak0bmz-pre-commit-4.5.1/bin/pre-commit
pre-commit 4.6.2
/nix/store/6blf72f7sdmlrqjggxvk5ij4cxb7fk4c-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/sbg911hs9dbclrzlp04br3iyfpgnaj6r-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
/nix/store/y7isn3dr8bildnp78vk5nhcav63g80dz-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-23
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/n8yak1ap308gvi7gmrniw0ybsx80fjws-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/pm5a8465hs9zb5i116b39i0y3xfpw6n3-run-clang-tidy-23/bin/run-clang-tidy-23
Rust toolchain:
✅ cargo
cargo 1.97.1 (c980f4866 2026-06-30)
/nix/store/88abzp43ywyzql1rhf8jh5aj5n5j7xzr-cargo-1.97.1-x86_64-unknown-linux-gnu/bin/cargo
/nix/store/60b0cz400q34wj2gzav5a41i7y6jzlw3-cargo-1.97.1-x86_64-unknown-linux-gnu/bin/cargo
✅ cargo-audit
cargo-audit-audit 0.22.1
/nix/store/2w9if868piw98xz057sz97jnjvf7hnvf-cargo-audit-0.22.1/bin/cargo-audit
cargo-audit-audit 0.22.2
/nix/store/kksgynfwir3pfii9qkbj3gcc61an0d4s-cargo-audit-0.22.2/bin/cargo-audit
✅ cargo-llvm-cov
cargo-llvm-cov 0.8.5
/nix/store/jjpdf1l6izz6607a346ykra9sndzaw7h-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
cargo-llvm-cov 0.9.0
/nix/store/4y8qxx0mlm22hnxlw2cmbcwpxispw02h-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
✅ cargo-nextest
cargo-nextest 0.9.137
/nix/store/jhkr7gwyrchkml33gyns9cy0yn7b57qc-cargo-nextest-0.9.137/bin/cargo-nextest
cargo-nextest 0.9.146
/nix/store/q0anzqrazmdr8n1049ca28098q6lv2y8-cargo-nextest-0.9.146/bin/cargo-nextest
✅ clippy-driver
clippy 0.1.97 (8bab26f4f6 2026-07-14)
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/clippy-driver
/nix/store/kk0kqdc4f7am21yp30rr8jcrnib5xqzy-rust-minimal-1.97.1/bin/clippy-driver
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
/nix/store/lr3m97p3hx1k22a7c44pb0wa7rbayhfi-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer
/nix/store/4mglk7qw9jqbv5nwlx9ahdnpcn5sz70r-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/j7kf7a5h4xypzp6x1skg4dsdx2k4fwb3-rust-nightly/bin/rust-nightly
rustc 1.101.0-nightly (282215592 2026-10-04)
/nix/store/qwyzkvba3f9569wjj45b4iyb7nz7qbnr-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc
/nix/store/kk0kqdc4f7am21yp30rr8jcrnib5xqzy-rust-minimal-1.97.1/bin/rustc
✅ rustfmt
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
/nix/store/6f1icmb2za20kxn30pgmbv5jq9fnbf4z-rustfmt-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rustfmt
/nix/store/y5gf8a3hix7pnwzdwcfpf3acqhzv3xy6-rustfmt-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rustfmt
GCC toolchain:
✅ gcc
gcc (GCC) 15.2.0
/nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/gcc
gcc (GCC) 15.3.0
/nix/store/bslmkbaiymxxdc6lwcdb1qyw745nds5j-gcc-wrapper-15.3.0/bin/gcc
✅ gcc-15
gcc (GCC) 15.2.0
/nix/store/d6iri2s6bzqq5ac3fg25j6hgnn1lz44f-gcc-15/bin/gcc-15
gcc (GCC) 15.3.0
/nix/store/h6f2qc8kgwf7r81gwb10csvpjqfc2404-gcc-15/bin/gcc-15
✅ g++
g++ (GCC) 15.2.0
/nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/g++
g++ (GCC) 15.3.0
/nix/store/bslmkbaiymxxdc6lwcdb1qyw745nds5j-gcc-wrapper-15.3.0/bin/g++
✅ g++-15
g++ (GCC) 15.2.0
/nix/store/gm3msmmxq055lm9gprkfjj9d2gdz1mpg-g++-15/bin/g++-15
g++ (GCC) 15.3.0
/nix/store/jwncv76v61ld9l99v0b1dkmf810if65s-g++-15/bin/g++-15
✅ cpp
cpp (GCC) 15.2.0
/nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/cpp
cpp (GCC) 15.3.0
/nix/store/bslmkbaiymxxdc6lwcdb1qyw745nds5j-gcc-wrapper-15.3.0/bin/cpp
✅ cpp-15
cpp (GCC) 15.2.0
/nix/store/bn3gmn0m7g4gn2i0yml46fljc7mghiq5-cpp-15/bin/cpp-15
cpp (GCC) 15.3.0
/nix/store/jmi558xj8v7lz2fm6wrradh40dp48zx9-cpp-15/bin/cpp-15
✅ gcov
gcov (GCC) 15.2.0
/nix/store/xvv5sm5i8x0ks6ypfkzl7c4j9srnxz7k-gcc-15.2.0/bin/gcov
gcov (GCC) 15.3.0
/nix/store/n2ab7kf7mm84hna35x463mizcy52pf1h-gcc-15.3.0/bin/gcov
Mold:
✅ mold
mold 2.41.0 (compatible with GNU ld)
/nix/store/2w6fpgxjzzyqmd25wzplm23dfa49a0p2-mold-unwrapped-wrapper-2.41.0/bin/mold
mold 2.42.1 (compatible with GNU ld)
/nix/store/9dcddrfxlfl9n3wfibi5jkmp22bjkvkh-mold-unwrapped-wrapper-2.42.1/bin/mold
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 53 checked tools are present and runnable.
✅ All 54 checked tools are present and runnable.
+99 -96
View File
@@ -2,173 +2,176 @@ Detected OS: linux (Linux aarch64)
Core build tools:
✅ cmake
cmake version 4.1.2
/nix/store/nkcpxjifkambzlrwh27a8igvhnbchibg-cmake-4.1.2/bin/cmake
cmake version 4.4.3
/nix/store/39cq2g3d3flq2lmb6hc09grxs4xaax8p-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.28.1
/nix/store/8i2gyqgc00xvxg9xm6y7n0ilncdv8imw-conan-2.28.1/bin/conan
Conan version 2.33.0
/nix/store/grz6a77mllqzzmg198qagx1r9zhvfijw-conan-2.33.0/bin/conan
✅ git
git version 2.54.0
/nix/store/ixp98f9avf8ikpdrmp40cj33g0dazyp9-git-2.54.0/bin/git
git version 2.55.0
/nix/store/kj6aff4gmz3snpp35rl3gysbl9srd81a-git-2.55.0/bin/git
✅ python3
Python 3.13.13
/nix/store/lqn6mbgzzdrqq2qkwddcmxj9z6amdd86-python3-3.13.13/bin/python3.13
Python 3.14.7
/nix/store/81ag9nxz1flw3xjkv1vvqg8y2ipx2ins-python3-3.14.7/bin/python3.14
Development tooling:
✅ ccache
ccache version 4.13.6
/nix/store/2q39xi2kbi04ibga7635f2sl148d1mzv-ccache-4.13.6/bin/ccache
/nix/store/nzb1lir9sw06fn4635kfx055myqanaz8-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.7
/nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/vcf6ilfwn57828hwzyp6zlyr24j9j6yw-clang-22/bin/clang-22
clang version 23.1.0
/nix/store/g8sc13kz6jarzip643hk01832x40v8ms-clang-wrapper-23.1.0/bin/clang
✅ clang-23
clang version 23.1.0
/nix/store/j8qvcg9150s5n09qg8p62q5z3ci02hd9-clang-23/bin/clang-23
✅ clang++
clang version 22.1.7
/nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/xby0f6gamr7m27zp5cndsvghbp9lgb3c-clang++-22/bin/clang++-22
clang version 23.1.0
/nix/store/g8sc13kz6jarzip643hk01832x40v8ms-clang-wrapper-23.1.0/bin/clang++
✅ clang++-23
clang version 23.1.0
/nix/store/n62cn5w7fgbslamy3dgm8zhl5yly7zq0-clang++-23/bin/clang++-23
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/h893hd4q1bb6ily2lby5dzyfrrzd2nvj-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
/nix/store/ml2991pbdhh0qcxqj9331lhpsg449vky-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
✅ curl
curl 8.20.0 (aarch64-unknown-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
/nix/store/i1s0lqwlrmjd2dxzgy2p84cxqqsb0bmk-curl-8.20.0-bin/bin/curl
curl 8.22.0 (aarch64-unknown-linux-gnu) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
/nix/store/94c2x2bl3xb7mx43sg1bs1hd7pnw83ac-curl-8.22.0-bin/bin/curl
✅ file
file-5.47
/nix/store/dx973zg9km2w9albsib2vw9wyvacfrlw-file-5.47/bin/file
file-5.48
/nix/store/66sd1x1hls0yijibn00iz08jh4fnas9z-file-5.48/bin/file
✅ jq
jq-1.8.2
/nix/store/kr5py0dcqc9f3l9148qd1axh0z77lld3-jq-1.8.2-bin/bin/jq
✅ less
less 692 (PCRE2 regular expressions)
/nix/store/1blb3s7hhsr77wqi598m6k1qkfp3ms0w-less-692/bin/less
less 710 (PCRE2 regular expressions)
/nix/store/n6z9dv3yjlgvfx1nmrx6qzyv8vzim1l2-less-710/bin/less
✅ make
GNU Make 4.4.1
/nix/store/9ngw1ippk25jjj5fjxv36xbp6iq7rxdx-gnumake-4.4.1/bin/make
/nix/store/q07jxvck60qgrg4p1ywypddh5zz6i9s2-gnumake-4.4.1/bin/make
✅ netstat
net-tools 2.10
/nix/store/7vdsz21f0s499s5yyqzp5s4676q4yxdd-net-tools-2.10/bin/netstat
/nix/store/rfmhd3f8drg3wqyr2mhyrlkd7vyp2pxv-net-tools-2.10/bin/netstat
✅ ninja
1.13.2
/nix/store/8ksx98gsbn5lmlizcmw57yd4sg0k2p58-ninja-1.13.2/bin/ninja
/nix/store/lfkdyggn1ffslfd830l35jrvapr5lzri-ninja-1.13.2/bin/ninja
✅ perl
v5.42.0
/nix/store/5wnly69vv1i3y97al4v3xrqymf9hlzgq-perl-5.42.0/bin/perl
v5.42.3
/nix/store/nyp5q4d2cb0pghi4i7nscb97bqkb6kp3-perl-5.42.3/bin/perl
✅ pkg-config
0.29.2
/nix/store/c7vwy0gl1q0agl2h22gi0m9dg7xxad2l-pkg-config-wrapper-0.29.2/bin/pkg-config
/nix/store/h82n3d0wdrgskdc3yvmz5l3c50h2snvp-pkg-config-wrapper-0.29.2/bin/pkg-config
✅ vim
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
/nix/store/v8c7pvx26irvy9k5sbwd183cyvckzzb3-vim-9.2.0389/bin/vim
/nix/store/h39h0qq4i80sp3yd7sxjn2wf497v5rln-vim-9.2.1001/bin/vim
✅ zip
Zip 3.0
/nix/store/5mh19mvbv9ym2sm9vymyyaac5l2cj2jq-zip-3.0/bin/zip
/nix/store/lfwhq17wfcmjy823fldhlhxj1hjchdzc-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.7
/nix/store/bg4kn8z81hk7b9284rjqvr51wpfjqc24-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 23.1.0
/nix/store/jh9xhz6gwxyz3rfx7vy5g4j5d71w940r-clang-tools-23.1.0/bin/clang-apply-replacements
✅ clang-apply-replacements-23
clang-apply-replacements version 23.1.0
/nix/store/1ajzrfr62m3cianfh9i97r30a7vp1xmb-clang-apply-replacements-23/bin/clang-apply-replacements-23
✅ clang-format
clang-format version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-format
✅ clang-format-22
clang-format version 22.1.7
/nix/store/79v57mzcw8ng8kl7p961ck08ymhp31v7-clang-format-22/bin/clang-format-22
clang-format version 23.1.0
/nix/store/jh9xhz6gwxyz3rfx7vy5g4j5d71w940r-clang-tools-23.1.0/bin/clang-format
✅ clang-format-23
clang-format version 23.1.0
/nix/store/5nfddq7xgbp4zmj8jn7xk1pxyyn5wiwl-clang-format-23/bin/clang-format-23
✅ clang-tidy
LLVM version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/wdyd6cb9z1lyi37lbzvwldgcc7yv1n5c-clang-tidy-22/bin/clang-tidy-22
LLVM version 23.1.0
/nix/store/jh9xhz6gwxyz3rfx7vy5g4j5d71w940r-clang-tools-23.1.0/bin/clang-tidy
✅ clang-tidy-23
LLVM version 23.1.0
/nix/store/cawfgsx0vsslc1f007cavmy8md9drb1r-clang-tidy-23/bin/clang-tidy-23
✅ dot
dot - graphviz version 12.2.1 (0)
/nix/store/58rrk4yzwpmyxvl8cqm18h3dhv24zf00-graphviz-12.2.1/bin/dot
dot - graphviz version 15.1.1 (0)
/nix/store/vz4zb8d1rfigzzdn3i0hd4kff28vk02l-graphviz-15.1.1/bin/dot
✅ doxygen
1.16.1
/nix/store/hq32kzwpl89wgr49iq0gmqn9r5n072zq-doxygen-1.16.1/bin/doxygen
1.17.0
/nix/store/6q4z332wc9jnvad71wjj87lgjxidhgzl-doxygen-1.17.0/bin/doxygen
✅ gcovr
gcovr 8.4
/nix/store/sml3xbbfhhlhk6h7jnlg19pdbx9b764b-python3.13-gcovr-8.4/bin/gcovr
/nix/store/v548fjz51k2nmr257vjnd0zp3bxpc2r1-python3.14-gcovr-8.4/bin/gcovr
✅ gh
gh version 2.94.0 (nixpkgs)
/nix/store/7hh2qi0gj2ifbxbl56cjzbiyfc379bji-gh-2.94.0/bin/gh
gh version 2.102.0 (2026-09-30)
/nix/store/vgg6gnq74h76b4r2ac2q9366f2s5qxwx-gh-2.102.0/bin/gh
✅ git-cliff
git-cliff 2.13.1
/nix/store/bidn3pz53yd6qlg711917xx0q10hqmqv-git-cliff-2.13.1/bin/git-cliff
git-cliff 2.14.2
/nix/store/hrlgliq7l3fl2qrdlc197mvscs4n03rd-git-cliff-2.14.2/bin/git-cliff
✅ git-lfs
git-lfs/3.7.1 (3.7.1; linux arm64; go 1.26.3)
/nix/store/4rsklvkbac5bayy0zv12kxyvspi4sshd-git-lfs-3.7.1/bin/git-lfs
git-lfs/3.8.0 (3.8.0; linux arm64; go 1.26.8)
/nix/store/pa2g4xdvr4a73m6mv4cqs0pik1ka1xp5-git-lfs-3.8.0/bin/git-lfs
✅ gpg
gpg (GnuPG) 2.4.9
/nix/store/ka4i8zz5ni3rzqnzcxbfvwr95fk8pn6q-gnupg-2.4.9/bin/gpg
/nix/store/3qc90b7qkxz7na3xnpd2qb8yvcq8zdp6-gnupg-2.4.9/bin/gpg
✅ pre-commit
pre-commit 4.5.1
/nix/store/n981w6hjfar2l81kxbxs2wxl64vwa5kj-pre-commit-4.5.1/bin/pre-commit
pre-commit 4.6.2
/nix/store/5f3av75nc7n7xbf2hlvlagzpp2g8g562-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/4z2fyklg78klallr7x9j02kz92hnxp4m-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
/nix/store/nlg35d9ccslsq5qny7raxyyyrxgk7vsq-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-23
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/f8m0p9ad40brp9ahy4i0h27kqjkya1j9-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/a50nf8hp8p8l8pgcf9fs0j25hij5acw3-run-clang-tidy-23/bin/run-clang-tidy-23
Rust toolchain:
✅ cargo
cargo 1.97.1 (c980f4866 2026-06-30)
/nix/store/6hch2qrr86n2sa2m90lrpxrfxxwbkayl-cargo-1.97.1-aarch64-unknown-linux-gnu/bin/cargo
/nix/store/nci0zxxj2fiq0dlkpqzbnqfh27wz4sq4-cargo-1.97.1-aarch64-unknown-linux-gnu/bin/cargo
✅ cargo-audit
cargo-audit-audit 0.22.1
/nix/store/9rxbrn9aa2r1z96186s69pc7vzizyfch-cargo-audit-0.22.1/bin/cargo-audit
cargo-audit-audit 0.22.2
/nix/store/zldaj03mfz9f21yiby5yymq7v8vqygd7-cargo-audit-0.22.2/bin/cargo-audit
✅ cargo-llvm-cov
cargo-llvm-cov 0.8.5
/nix/store/vwjsi159n89szrx4yh5pc3jlf2gp4fld-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
cargo-llvm-cov 0.9.0
/nix/store/iwpcfjah31fgn99ph32ajzz7i22j6al5-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
✅ cargo-nextest
cargo-nextest 0.9.137
/nix/store/qb6bcg2fjvm3r9s9j98nmffmf9xwh45s-cargo-nextest-0.9.137/bin/cargo-nextest
cargo-nextest 0.9.146
/nix/store/p5svqr8x1rr7b1i8595pr8qafsr7s6fm-cargo-nextest-0.9.146/bin/cargo-nextest
✅ clippy-driver
clippy 0.1.97 (8bab26f4f6 2026-07-14)
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/clippy-driver
/nix/store/ny64zks16ms5d70jk1qsiwsk0fr33ipp-rust-minimal-1.97.1/bin/clippy-driver
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
/nix/store/262830dlw2517lnagfx7i7agqgl4fmsd-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer
/nix/store/d9s5skhvi1qxj940mylwym2rjicnc2kq-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/c59pxk1yikdlf129qwyg4fplmxcrha0k-rust-nightly/bin/rust-nightly
rustc 1.101.0-nightly (282215592 2026-10-04)
/nix/store/rbsf1gr6f9ikv2izlyh8im117w6hs7h5-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc
/nix/store/ny64zks16ms5d70jk1qsiwsk0fr33ipp-rust-minimal-1.97.1/bin/rustc
✅ rustfmt
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
/nix/store/nd8g81wv1smnvdpy4whpcyv2siwjmaan-rustfmt-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rustfmt
/nix/store/dplvgjikmv8909rk0dkqr90ryjg7xn0j-rustfmt-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rustfmt
GCC toolchain:
✅ gcc
gcc (GCC) 15.2.0
/nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/gcc
gcc (GCC) 15.3.0
/nix/store/g7z555pl99vnwif4jxa05nk1qdc33i1n-gcc-wrapper-15.3.0/bin/gcc
✅ gcc-15
gcc (GCC) 15.2.0
/nix/store/h489d1rmjisfbxh5kmsb0a7c35j8qsdf-gcc-15/bin/gcc-15
gcc (GCC) 15.3.0
/nix/store/vbdsfjk3j1xy5z329sl2d8vp6mq6x09i-gcc-15/bin/gcc-15
✅ g++
g++ (GCC) 15.2.0
/nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/g++
g++ (GCC) 15.3.0
/nix/store/g7z555pl99vnwif4jxa05nk1qdc33i1n-gcc-wrapper-15.3.0/bin/g++
✅ g++-15
g++ (GCC) 15.2.0
/nix/store/9ywmhz8bmzknrn3pn84g46z8hj3vrmw5-g++-15/bin/g++-15
g++ (GCC) 15.3.0
/nix/store/9ghl998ry4cxlsg6hnq5rn2iipnlkccm-g++-15/bin/g++-15
✅ cpp
cpp (GCC) 15.2.0
/nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/cpp
cpp (GCC) 15.3.0
/nix/store/g7z555pl99vnwif4jxa05nk1qdc33i1n-gcc-wrapper-15.3.0/bin/cpp
✅ cpp-15
cpp (GCC) 15.2.0
/nix/store/vmjilh1b830qz9yh0a1jj5ads0jxizdk-cpp-15/bin/cpp-15
cpp (GCC) 15.3.0
/nix/store/wj0mv2fv2lhglvpjzhscqpbigkvik6br-cpp-15/bin/cpp-15
✅ gcov
gcov (GCC) 15.2.0
/nix/store/rmwf5hpi1y2m1wpnfvlxmrhksm4djk2j-gcc-15.2.0/bin/gcov
gcov (GCC) 15.3.0
/nix/store/mszfsj00qnv9ygqcqz3w02qzxxskhvlb-gcc-15.3.0/bin/gcov
Mold:
✅ mold
mold 2.41.0 (compatible with GNU ld)
/nix/store/f5qh5a0bx1dslmnf5n5gx0s6aljbswq3-mold-unwrapped-wrapper-2.41.0/bin/mold
mold 2.42.1 (compatible with GNU ld)
/nix/store/gbq334xbibaz4gn184j7gs58m2spgwi7-mold-unwrapped-wrapper-2.42.1/bin/mold
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 53 checked tools are present and runnable.
✅ All 54 checked tools are present and runnable.
+3 -2
View File
@@ -7,7 +7,7 @@
...
}:
let
inherit (import ./packages.nix { inherit pkgs; }) commonPackages;
inherit (import ./packages.nix { inherit pkgs customGlibc; }) commonPackages;
# Each forces something absent on the other platform, so both stay lazy.
linux = import ./linux.nix { inherit pkgs customGlibc; };
@@ -21,7 +21,8 @@ let
)
);
toolchain = if pkgs.stdenv.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
toolchain =
if pkgs.stdenv.hostPlatform.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
in
{
default = pkgs.buildEnv {
+21 -18
View File
@@ -20,15 +20,19 @@ let
# Custom-glibc stdenvs, matching the CI environment. darwin has no custom
# glibc, so there they fall back to the plain nixpkgs stdenvs.
customGccStdenv = if pkgs.stdenv.isLinux then linux.gccStdenv else plainGccStdenv;
customClangStdenv = if pkgs.stdenv.isLinux then linux.clangStdenv else plainClangStdenv;
customGccStdenv = if pkgs.stdenv.hostPlatform.isLinux then linux.gccStdenv else plainGccStdenv;
customClangStdenv =
if pkgs.stdenv.hostPlatform.isLinux then linux.clangStdenv else plainClangStdenv;
# gcov matching each gcc shell, so `-Dcoverage=ON` builds work in the shell.
plainGcov = mkGcov {
name = "plain";
cc = gccPackage.cc;
};
customGccGcov = if pkgs.stdenv.isLinux then linux.gcov else plainGcov;
customGccGcov = if pkgs.stdenv.hostPlatform.isLinux then linux.gcov else plainGcov;
# commonPackages whose clang tools parse with the custom toolchain's headers.
customCommonPackages = (import ./packages.nix { inherit pkgs customGlibc; }).commonPackages;
# Whole directory: init.sh locates the profiles relative to itself.
conanDir = ../conan;
@@ -51,7 +55,7 @@ let
# Not sdkEnv: a shell's stdenv already sets that up. Prepended so the stub
# beats the nixpkgs libresolv this shell's tooling drags in.
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.isDarwin (
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isDarwin (
pkgs.lib.concatLines (
pkgs.lib.mapAttrsToList (
name: value: ''export ${name}="${value} ''${${name}:-}"''
@@ -85,11 +89,11 @@ let
version ? null,
versionedTools ? [ ],
extraPackages ? [ ],
warningHook ? "",
# Opt out of PatchNixBinary.cmake retargeting binaries to the system
# loader. The plain toolchain links a newer glibc, so it must not be
# patched; the custom toolchain patches by default.
noPatchNixBinary ? false,
# The stock nixpkgs toolchain: warn that it doesn't match CI, keep the
# clang tools off the custom toolchain, and opt out of PatchNixBinary.cmake
# retargeting binaries to the system loader (the plain toolchain links a
# newer glibc, so it must not be patched).
plain ? false,
}:
let
compilerVersionHook =
@@ -109,7 +113,8 @@ let
in
(pkgs.mkShell.override { inherit stdenv; }) (
{
packages = commonPackages ++ versionedLinks ++ extraPackages;
packages =
(if plain then commonPackages else customCommonPackages) ++ versionedLinks ++ extraPackages;
# Marks a managed dev shell, so the build (XrplSanity.cmake) can tell an
# intentional Nix toolchain from one leaked into a bare shell.
XRPL_DEVSHELL = shellName;
@@ -118,15 +123,15 @@ let
${compilerVersionHook}
${darwinLibresolvHook}
${conanHook}
${warningHook}
${pkgs.lib.optionalString plain plainWarningHook}
'';
}
// pkgs.lib.optionalAttrs noPatchNixBinary { XRPLD_NO_PATCH_NIX_BINARY = "1"; }
// pkgs.lib.optionalAttrs plain { XRPLD_NO_PATCH_NIX_BINARY = "1"; }
);
in
rec {
# macOS: Nix Clang. Linux: Nix GCC.
default = if pkgs.stdenv.isDarwin then clang else gcc;
default = if pkgs.stdenv.hostPlatform.isDarwin then clang else gcc;
# gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no
# custom glibc, so they fall back to the plain nixpkgs toolchain.
@@ -171,7 +176,7 @@ rec {
# The *-plain shells (stock nixpkgs toolchain) exist only on Linux: on darwin
# gcc/clang are already plain, so these would be redundant and are omitted, which
# makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc.
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
// pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
gcc-plain = makeShell {
shellName = "gcc-plain";
stdenv = plainGccStdenv;
@@ -179,8 +184,7 @@ rec {
version = gccVersion;
versionedTools = gccVersionedTools;
extraPackages = [ plainGcov ];
warningHook = plainWarningHook;
noPatchNixBinary = true;
plain = true;
};
clang-plain = makeShell {
@@ -189,7 +193,6 @@ rec {
compilerName = "clang";
version = llvmVersion;
versionedTools = clangVersionedTools;
warningHook = plainWarningHook;
noPatchNixBinary = true;
plain = true;
};
}
+1
View File
@@ -146,6 +146,7 @@ in
})
];
clang = customClang;
gccStdenv = customStdenv;
clangStdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv customClang;
gcov = customGcov;
+38 -7
View File
@@ -1,14 +1,28 @@
{ pkgs }:
{
pkgs,
# With the custom glibc, the clang tools (clang-tidy, ...) parse code with the
# Linux custom toolchain's headers, i.e. the same glibc and libstdc++ as the
# build. Without it, they use the nixpkgs default compiler's.
customGlibc ? null,
}:
let
# Compiler versions used across the dev shell and the CI environment.
# Docs link here and scripts read the version off the tools;
# only the clang-format rev in .pre-commit-config.yaml
# has to be bumped alongside llvmVersion.
gccVersion = 15;
llvmVersion = 22;
llvmVersion = 23;
gccPackage = pkgs."gcc${toString gccVersion}";
llvmPackages = pkgs."llvmPackages_${toString llvmVersion}";
# Bound explicitly so it tracks llvmPackages above, not the `with pkgs` default.
clangTools = llvmPackages.clang-tools;
# isLinux first: darwin must not evaluate the custom glibc.
clangTools = llvmPackages.clang-tools.override (
pkgs.lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux && customGlibc != null) {
inherit (import ./linux.nix { inherit pkgs customGlibc; }) clang;
}
);
# In LLVM 22, run-clang-tidy.py moved from share/clang/ to bin/, so nixpkgs
# clang-tools no longer links it. Wrap it manually.
@@ -16,6 +30,21 @@ let
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
'';
# Conan 2.33 is the first release whose settings.yml accepts clang 23.
# TODO: drop once nixpkgs ships it.
conan = pkgs.conan.overridePythonAttrs (old: {
version = "2.33.0";
src = pkgs.fetchFromGitHub {
owner = "conan-io";
repo = "conan";
tag = "2.33.0";
hash = "sha256-FDJjesqvPiAUAEjeqQt088Vnet/KXHbUyEPytP9MyG4=";
};
dependencies = old.dependencies ++ [ pkgs.python3Packages.truststore ];
# The upstream test suite takes long and needs network access.
doCheck = false;
});
rust = import ./rust.nix { inherit pkgs; };
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
@@ -25,10 +54,11 @@ let
# - 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.
# On a host that also has the matching system binary
# (e.g. Ubuntu's `/usr/bin/g++-<v>` or `clang-tidy-<v>`),
# 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
@@ -112,6 +142,7 @@ in
gnumake
gnupg # needed for signing commits & codecov/codecov-action
graphviz
jq
less # needed for git diff
mold
nettools # provides netstat, used to debug failures in CI
+7 -4
View File
@@ -11,6 +11,8 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/LedgerTiming.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
#include <xrpl/ledger/entries/NegativeUNLEntry.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
@@ -560,7 +562,7 @@ Ledger::setup()
try
{
if (auto const sle = read(keylet::feeSettings()))
if (auto const sle = FeeSettingsEntryR(*this))
{
bool oldFees = false;
bool newFees = false;
@@ -639,7 +641,7 @@ HashSet<PublicKey>
Ledger::negativeUNL() const
{
HashSet<PublicKey> negUnl;
if (auto sle = read(keylet::negativeUNL()); sle && sle->isFieldPresent(sfDisabledValidators))
if (auto const sle = NegativeUNLEntryR(*this); sle && sle->isFieldPresent(sfDisabledValidators))
{
auto const& nUnlData = sle->getFieldArray(sfDisabledValidators);
for (auto const& n : nUnlData)
@@ -663,7 +665,7 @@ Ledger::negativeUNL() const
std::optional<PublicKey>
Ledger::validatorToDisable() const
{
if (auto sle = read(keylet::negativeUNL()); sle && sle->isFieldPresent(sfValidatorToDisable))
if (auto const sle = NegativeUNLEntryR(*this); sle && sle->isFieldPresent(sfValidatorToDisable))
{
auto d = sle->getFieldVL(sfValidatorToDisable);
auto s = makeSlice(d);
@@ -677,7 +679,8 @@ Ledger::validatorToDisable() const
std::optional<PublicKey>
Ledger::validatorToReEnable() const
{
if (auto sle = read(keylet::negativeUNL()); sle && sle->isFieldPresent(sfValidatorToReEnable))
if (auto const sle = NegativeUNLEntryR(*this);
sle && sle->isFieldPresent(sfValidatorToReEnable))
{
auto d = sle->getFieldVL(sfValidatorToReEnable);
auto s = makeSlice(d);
+3 -3
View File
@@ -3,6 +3,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/ledger/detail/ReadViewFwdRange.h>
#include <xrpl/ledger/entries/AmendmentsEntry.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/Rules.h>
@@ -72,9 +73,8 @@ makeRulesGivenLedger(
std::optional const digest = ledger.digest(k.key);
if (digest)
{
auto const sle = ledger.read(k);
if (sle)
return Rules(presets, digest, sle->getFieldV256(sfAmendments));
if (AmendmentsEntryR const entry(k, ledger); entry)
return Rules(presets, digest, entry->getFieldV256(sfAmendments));
}
return Rules(presets);
}
+11 -39
View File
@@ -9,6 +9,8 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AmendmentsEntry.h>
#include <xrpl/ledger/entries/LedgerHashesEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
@@ -308,40 +310,13 @@ areCompatible(
std::set<UInt256>
getEnabledAmendments(ReadView const& view)
{
std::set<UInt256> amendments;
if (auto const sle = view.read(keylet::amendments()))
{
if (sle->isFieldPresent(sfAmendments))
{
auto const& v = sle->getFieldV256(sfAmendments);
amendments.insert(v.begin(), v.end());
}
}
return amendments;
return AmendmentsEntryR(view).enabledAmendments();
}
MajorityAmendmentsT
getMajorityAmendments(ReadView const& view)
{
MajorityAmendmentsT ret;
if (auto const sle = view.read(keylet::amendments()))
{
if (sle->isFieldPresent(sfMajorities))
{
using Tp = NetClock::time_point;
using D = Tp::duration;
auto const majorities = sle->getFieldArray(sfMajorities);
for (auto const& m : majorities)
ret[m.getFieldH256(sfAmendment)] = Tp(D(m.getFieldU32(sfCloseTime)));
}
}
return ret;
return AmendmentsEntryR(view).majorityAmendments();
}
std::optional<UInt256>
@@ -361,17 +336,16 @@ hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal)
if (int const diff = ledger.seq() - seq; diff <= 256)
{
// Within 256...
auto const hashIndex = ledger.read(keylet::skip());
LedgerHashesEntryR const hashIndex(ledger, journal);
if (hashIndex)
{
XRPL_ASSERT(
hashIndex->getFieldU32(sfLastLedgerSequence) == (ledger.seq() - 1),
"xrpl::hashOfSeq : matching ledger sequence");
STVector256 vec = hashIndex->getFieldV256(sfHashes);
if (vec.size() >= diff)
return vec[vec.size() - diff];
if (auto const hash = hashIndex.hashAt(diff - 1))
return hash;
JLOG(journal.warn()) << "Ledger " << ledger.seq() << " missing hash for " << seq << " ("
<< vec.size() << "," << diff << ")";
<< hashIndex->getFieldV256(sfHashes).size() << "," << diff << ")";
}
else
{
@@ -387,16 +361,14 @@ hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal)
}
// in skiplist
auto const hashIndex = ledger.read(keylet::skip(seq));
LedgerHashesEntryR const hashIndex(keylet::skip(seq), ledger, journal);
if (hashIndex)
{
auto const lastSeq = hashIndex->getFieldU32(sfLastLedgerSequence);
XRPL_ASSERT(lastSeq >= seq, "xrpl::hashOfSeq : minimum last ledger");
XRPL_ASSERT((lastSeq & 0xff) == 0, "xrpl::hashOfSeq : valid last ledger");
auto const diff = (lastSeq - seq) >> 8;
STVector256 vec = hashIndex->getFieldV256(sfHashes);
if (vec.size() > diff)
return vec[vec.size() - diff - 1];
if (auto const hash = hashIndex.hashAt((lastSeq - seq) >> 8))
return hash;
}
JLOG(journal.warn()) << "Can't get seq " << seq << " from " << ledger.seq() << " error";
return std::nullopt;
+39 -6
View File
@@ -18,8 +18,10 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -1337,9 +1339,26 @@ directSendNoLimitMPT(
}
// Sending 3rd party MPTs: transit.
saActual = (waiveFee == WaiveTransferFee::Yes)
? saAmount
: multiply(saAmount, transferRate(view, saAmount.get<MPTIssue>().getMptID()));
if (waiveFee == WaiveTransferFee::Yes)
{
saActual = saAmount;
}
else
{
auto const rate = transferRate(view, saAmount.get<MPTIssue>().getMptID());
if (view.rules().enabled(fixCleanup3_5_0))
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the cost
// exactly and round it up, matching the payment engine.
auto const cost = mulRatio(saAmount.mpt(), rate.value, QUALITY_ONE, true);
saActual = STAmount(saAmount.asset(), cost.value());
}
else
{
saActual = multiply(saAmount, rate);
}
}
JLOG(j.debug()) << "directSendNoLimitMPT> " << to_string(uSenderID) << " - > "
<< to_string(uReceiverID) << " : deliver=" << saAmount.getFullText()
@@ -1448,9 +1467,23 @@ directSendNoLimitMultiMPT(
}
// Sending 3rd party MPTs: transit.
STAmount const actualSend = (waiveFee == WaiveTransferFee::Yes)
? amount
: multiply(amount, transferRate(view, amount.get<MPTIssue>().getMptID()));
STAmount actualSend = amount;
if (waiveFee != WaiveTransferFee::Yes)
{
auto const rate = transferRate(view, amount.get<MPTIssue>().getMptID());
if (view.rules().enabled(fixCleanup3_5_0))
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the
// cost exactly and round it up, matching the payment engine.
auto const cost = mulRatio(amount.mpt(), rate.value, QUALITY_ONE, true);
actualSend = STAmount(amount.asset(), cost.value());
}
else
{
actualSend = multiply(amount, rate);
}
}
actual += actualSend;
takeFromSender += actualSend;
+3 -1
View File
@@ -1642,7 +1642,9 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
bool const resultNegative = (num.negative() != den.negative());
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
// fixCleanup3_5_0: the legacy path below overflows on large MPT amounts.
if (asset.holds<MPTIssue>() &&
(isFeatureEnabled(featureMPTokensV2, true) || isFeatureEnabled(fixCleanup3_5_0, true)))
{
// Match the multiply path above: Number performs the rounded
// operation, then STAmount materializes the final MPT amount using the
+60 -18
View File
@@ -8,6 +8,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/BatchInnerResult.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
@@ -18,9 +19,11 @@
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/applySteps.h>
#include <cstdint>
#include <exception>
#include <string>
#include <utility>
#include <vector>
namespace xrpl {
@@ -203,7 +206,8 @@ applyBatchTransactions(
ServiceRegistry& registry,
OpenView& batchView,
STTx const& batchTxn,
beast::Journal j)
beast::Journal j,
std::vector<BatchInnerResult>* innerResults)
{
XRPL_ASSERT(
batchTxn.getTxnType() == ttBATCH && !batchTxn.getFieldArray(sfRawTransactions).empty(),
@@ -211,8 +215,9 @@ applyBatchTransactions(
auto const parentBatchId = batchTxn.getTransactionID();
auto const mode = batchTxn.getFlags();
std::uint32_t index = 0;
auto applyOneTransaction = [&registry, &j, &parentBatchId, &batchView](STTx const& tx) {
auto applyOneTransaction = [&](STTx const& tx) {
OpenView perTxBatchView(kBatchView, batchView);
auto const ret = apply(registry, perTxBatchView, parentBatchId, tx, TapBatch, j);
@@ -223,6 +228,17 @@ applyBatchTransactions(
JLOG(j.debug()) << "BatchTrace[" << parentBatchId << "]: " << tx.getTransactionID() << " "
<< (ret.applied ? "applied" : "failure") << ": " << transToken(ret.ter);
if (innerResults != nullptr)
{
innerResults->push_back(
{.parentBatchId = parentBatchId,
.innerTxId = tx.getTransactionID(),
.index = index,
.ter = ret.ter,
.applied = ret.applied});
}
++index;
// If the transaction should be applied push its changes to the
// whole-batch view.
// NOTE: each inner tx is individually capped at kOversizeMetaDataCap;
@@ -235,28 +251,53 @@ applyBatchTransactions(
};
int applied = 0;
auto const firstResult = innerResults != nullptr ? innerResults->size() : 0;
for (auto const& stx : batchTxn.getBatchTransactions())
{
auto const result = applyOneTransaction(*stx);
XRPL_ASSERT(
result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)),
"Outer Batch failure, inner transaction should not be applied");
if (result.applied)
++applied;
if (!isTesSuccess(result.ter))
try
{
if ((mode & tfAllOrNothing) != 0u)
return false;
auto const result = applyOneTransaction(*stx);
XRPL_ASSERT(
result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)),
"Outer Batch failure, inner transaction should not be applied");
if ((mode & tfUntilFailure) != 0u)
if (result.applied)
++applied;
if (!isTesSuccess(result.ter))
{
if ((mode & tfAllOrNothing) != 0u)
{
// The whole-batch view is discarded, so nothing recorded so far reached the
// ledger.
if (innerResults != nullptr)
{
for (auto i = firstResult; i < innerResults->size(); ++i)
{
(*innerResults)[i].applied = false;
}
}
return false;
}
if ((mode & tfUntilFailure) != 0u)
break;
}
else if ((mode & tfOnlyOne) != 0u)
{
break;
}
}
else if ((mode & tfOnlyOne) != 0u)
catch (std::exception const& ex)
{
break;
// Drop this batch's results before the exception unwinds: the whole-batch
// view never reaches the ledger and the outer Batch is reported as failed.
JLOG(j.warn()) << "BatchTrace[" << parentBatchId << "]: " << stx->getTransactionID()
<< " throws: " << ex.what();
if (innerResults != nullptr)
innerResults->resize(firstResult);
throw;
}
}
@@ -270,7 +311,8 @@ applyTransaction(
STTx const& txn,
bool retryAssured,
ApplyFlags flags,
beast::Journal j)
beast::Journal j,
std::vector<BatchInnerResult>* innerResults)
{
// Returns false if the transaction has need not be retried.
if (retryAssured)
@@ -292,7 +334,7 @@ applyTransaction(
{
OpenView wholeBatchView(kBatchView, view);
if (applyBatchTransactions(registry, wholeBatchView, txn, j))
if (applyBatchTransactions(registry, wholeBatchView, txn, j, innerResults))
wholeBatchView.apply(view);
}
+26 -9
View File
@@ -89,17 +89,24 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
if (fix320Enabled && after->isFieldPresent(sfReferenceHolding))
referenceHoldingSetOnCreate_ = true;
}
else if (fix320Enabled)
else
{
// Modified issuance: detect any change to sfReferenceHolding.
bool const beforePresent = before->isFieldPresent(sfReferenceHolding);
bool const afterPresent = after->isFieldPresent(sfReferenceHolding);
if (beforePresent != afterPresent ||
(afterPresent &&
before->getFieldH256(sfReferenceHolding) !=
after->getFieldH256(sfReferenceHolding)))
// lsfMPTLocked is the only issuance flag with a legal clear path
// (tfMPTUnlock); the rest are fixed at creation or set-once.
issuanceFlagsCleared_ |= before->getFlags() & ~after->getFlags() & ~lsfMPTLocked;
if (fix320Enabled)
{
referenceHoldingMutated_ = true;
// Modified issuance: detect any change to sfReferenceHolding.
bool const beforePresent = before->isFieldPresent(sfReferenceHolding);
bool const afterPresent = after->isFieldPresent(sfReferenceHolding);
if (beforePresent != afterPresent ||
(afterPresent &&
before->getFieldH256(sfReferenceHolding) !=
after->getFieldH256(sfReferenceHolding)))
{
referenceHoldingMutated_ = true;
}
}
}
}
@@ -199,6 +206,16 @@ ValidMPTIssuance::finalize(
return false;
}
// Post-fixCleanup3_5_0: no transaction may clear an issuance flag other
// than lsfMPTLocked, so downstream code can trust set-once flags such as
// lsfMPTCanTransfer.
if (rules.enabled(fixCleanup3_5_0) && issuanceFlagsCleared_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: immutable MPTokenIssuance flag cleared: "
<< issuanceFlagsCleared_;
return false;
}
// Deleting an MPToken with a non-zero MPTAmount is rejected.
if (rules.enabled(fixCleanup3_5_0))
{
-23
View File
@@ -13,7 +13,6 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
@@ -100,28 +99,6 @@ AMMLiquidity<TIn, TOut>::generateFibSeqOffer(TAmounts<TIn, TOut> const& balances
}
namespace {
template <typename T>
constexpr T
maxAmount()
{
if constexpr (std::is_same_v<T, XRPAmount>)
{
return XRPAmount(STAmount::kMaxNative);
}
else if constexpr (std::is_same_v<T, IOUAmount>)
{
return IOUAmount(STAmount::kMaxValue / 2, STAmount::kMaxOffset);
}
else if constexpr (std::is_same_v<T, STAmount>)
{
return STAmount(STAmount::kMaxValue / 2, STAmount::kMaxOffset);
}
else if constexpr (std::is_same_v<T, MPTAmount>)
{
return MPTAmount(kMaxMpTokenAmount);
}
}
template <typename T>
T
maxOut(T const& out, Asset const& asset)
@@ -3,7 +3,9 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/CheckEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -30,7 +32,7 @@ CheckCancel::preflight(PreflightContext const& ctx)
TER
CheckCancel::preclaim(PreclaimContext const& ctx)
{
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
CheckEntryR const sleCheck(ctx.tx[sfCheckID], ctx.view);
if (!sleCheck)
{
JLOG(ctx.j.warn()) << "Check does not exist.";
@@ -59,7 +61,7 @@ CheckCancel::preclaim(PreclaimContext const& ctx)
TER
CheckCancel::doApply()
{
auto const sleCheck = view().peek(keylet::check(ctx_.tx[sfCheckID]));
CheckEntryW sleCheck(ctx_.tx[sfCheckID], view(), j_);
if (!sleCheck)
{
// Error should have been caught in preclaim.
@@ -96,10 +98,10 @@ CheckCancel::doApply()
}
// If we succeeded, update the check owner's reserve.
decreaseOwnerCountForObject(view(), srcId, sleCheck, 1, viewJ);
decreaseOwnerCountForObject(view(), srcId, sleCheck.mutableRawSle(), 1, viewJ);
// Remove check from ledger.
view().erase(sleCheck);
sleCheck.erase();
return tesSUCCESS;
}
+18 -12
View File
@@ -7,6 +7,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/CheckEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -87,7 +88,7 @@ CheckCash::preflight(PreflightContext const& ctx)
TER
CheckCash::preclaim(PreclaimContext const& ctx)
{
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
CheckEntryR const sleCheck(ctx.tx[sfCheckID], ctx.view);
if (!sleCheck)
{
JLOG(ctx.j.warn()) << "Check does not exist.";
@@ -296,8 +297,9 @@ CheckCash::doApply()
// directly on a View.
PaymentSandbox psb(&ctx_.view());
auto sleCheck = psb.peek(keylet::check(ctx_.tx[sfCheckID]));
if (!sleCheck)
std::optional<CheckEntryW> sleCheck;
sleCheck.emplace(ctx_.tx[sfCheckID], psb, j_);
if (!*sleCheck)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Precheck did not verify check's existence.";
@@ -305,7 +307,7 @@ CheckCash::doApply()
// LCOV_EXCL_STOP
}
AccountID const srcId{sleCheck->getAccountID(sfAccount)};
AccountID const srcId{(*sleCheck)->getAccountID(sfAccount)};
if (!psb.exists(keylet::account(srcId)) || !psb.exists(keylet::account(accountID_)))
{
// LCOV_EXCL_START
@@ -314,7 +316,7 @@ CheckCash::doApply()
// LCOV_EXCL_STOP
}
auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck);
auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck->rawSle());
// Preclaim already checked that source has at least the requested
// funds.
@@ -331,7 +333,7 @@ CheckCash::doApply()
if (srcId != accountID_)
{
STAmount const sendMax = sleCheck->at(sfSendMax);
STAmount const sendMax = (*sleCheck)->at(sfSendMax);
// Flow() doesn't do XRP to XRP transfers.
if (sendMax.native())
@@ -569,7 +571,7 @@ CheckCash::doApply()
true, // owner pays transfer fee
OfferCrossing::No,
std::nullopt,
sleCheck->getFieldAmount(sfSendMax),
(*sleCheck)->getFieldAmount(sfSendMax),
std::nullopt, // check does not support domain
viewJ);
@@ -594,7 +596,7 @@ CheckCash::doApply()
// for DeliverMin.
ctx_.deliver(result.actualAmountOut);
sleCheck = psb.peek(keylet::check(ctx_.tx[sfCheckID]));
sleCheck.emplace(ctx_.tx[sfCheckID], psb, j_);
}
}
@@ -602,7 +604,10 @@ CheckCash::doApply()
// check link from destination directory.
if (srcId != accountID_ &&
!psb.dirRemove(
keylet::ownerDir(accountID_), sleCheck->at(sfDestinationNode), sleCheck->key(), true))
keylet::ownerDir(accountID_),
(*sleCheck)->at(sfDestinationNode),
(*sleCheck)->key(),
true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from destination.";
@@ -611,7 +616,8 @@ CheckCash::doApply()
}
// Remove check from check owner's directory.
if (!psb.dirRemove(keylet::ownerDir(srcId), sleCheck->at(sfOwnerNode), sleCheck->key(), true))
if (!psb.dirRemove(
keylet::ownerDir(srcId), (*sleCheck)->at(sfOwnerNode), (*sleCheck)->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from owner.";
@@ -620,10 +626,10 @@ CheckCash::doApply()
}
// If we succeeded, update the check owner's reserve.
decreaseOwnerCountForObject(psb, srcId, sleCheck, 1, viewJ);
decreaseOwnerCountForObject(psb, srcId, sleCheck->mutableRawSle(), 1, viewJ);
// Remove check from ledger.
psb.erase(sleCheck);
sleCheck->erase();
psb.apply(ctx_.rawView());
return tesSUCCESS;
@@ -4,6 +4,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/CheckEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -25,7 +26,6 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <memory>
#include <optional>
namespace xrpl {
@@ -201,8 +201,9 @@ CheckCreate::doApply()
// Note that we use the value from the sequence or ticket as the
// Check sequence. For more explanation see comments in SeqProxy.h.
auto const seq = ctx_.tx.getSeqProxy();
Keylet const checkKeylet = keylet::check(accountID_, seq);
auto sleCheck = std::make_shared<SLE>(checkKeylet);
CheckEntryW sleCheck(accountID_, seq, view(), j_);
Keylet const checkKeylet = sleCheck.keylet();
sleCheck.newSLE();
sleCheck->setAccountID(sfAccount, accountID_);
AccountID const dstAccountId = ctx_.tx[sfDestination];
@@ -218,7 +219,7 @@ CheckCreate::doApply()
if (auto const expiry = ctx_.tx[~sfExpiration])
sleCheck->setFieldU32(sfExpiration, *expiry);
view().insert(sleCheck);
sleCheck.insert();
auto viewJ = ctx_.registry.get().getJournal("View");
// If it's not a self-send (and it shouldn't be), add Check to the
@@ -252,7 +253,7 @@ CheckCreate::doApply()
// If we succeeded, the new entry counts against the creator's reserve.
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, viewJ);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCheck);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCheck.mutableRawSle());
return tesSUCCESS;
}
@@ -3,17 +3,13 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -34,79 +30,24 @@ EscrowCancel::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCancelPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount);
template <>
TER
escrowCancelPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, amount.get<Issue>(), account); !isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
template <>
TER
escrowCancelPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
TER
EscrowCancel::preclaim(PreclaimContext const& ctx)
{
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx.tx[sfOwner], seqProxy);
auto const slep = ctx.view.read(k);
if (!slep)
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
if (!escrow)
return tecNO_TARGET;
AccountID const account = (*slep)[sfAccount];
STAmount const amount = (*slep)[sfAmount];
AccountID const account = (*escrow)[sfAccount];
STAmount const amount = (*escrow)[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCancelPreclaimHelper<T>(ctx, account, amount);
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
},
amount.asset().value());
!isTesSuccess(ret))
@@ -120,9 +61,8 @@ TER
EscrowCancel::doApply()
{
auto const seqProxy = SeqProxy::rawSequence(ctx_.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx_.tx[sfOwner], seqProxy);
auto const slep = ctx_.view().peek(k);
if (!slep)
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
if (!escrow)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -133,19 +73,19 @@ EscrowCancel::doApply()
auto const now = ctx_.view().header().parentCloseTime;
// No cancel time specified: can't execute at all.
if (!(*slep)[~sfCancelAfter])
if (!(*escrow)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (!after(now, (*slep)[sfCancelAfter]))
if (!after(now, (*escrow)[sfCancelAfter]))
return tecNO_PERMISSION;
AccountID const account = (*slep)[sfAccount];
AccountID const account = (*escrow)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
auto const page = (*escrow)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
@@ -155,9 +95,10 @@ EscrowCancel::doApply()
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode]; optPage)
if (auto const optPage = (*escrow)[~sfDestinationNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true))
if (!ctx_.view().dirRemove(
keylet::ownerDir((*escrow)[sfDestination]), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -167,13 +108,13 @@ EscrowCancel::doApply()
}
auto const sle = ctx_.view().peek(keylet::account(account));
STAmount const amount = slep->getFieldAmount(sfAmount);
STAmount const amount = escrow->getFieldAmount(sfAmount);
// The return can re-create a holding the owner deleted while the escrow
// was pending; the removed escrow must not be counted against its reserve.
bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
if (recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
// Transfer amount back to the owner
if (isXRP(amount))
@@ -192,7 +133,7 @@ EscrowCancel::doApply()
return escrowUnlockApplyHelper<T>(
ctx_.getApplyViewContext(),
kParityRate,
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : slep,
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
preFeeBalance_,
amount,
issuer,
@@ -206,9 +147,9 @@ EscrowCancel::doApply()
return ret; // LCOV_EXCL_LINE
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
if (auto const optPage = (*escrow)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -219,10 +160,10 @@ EscrowCancel::doApply()
}
if (!recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
// Remove escrow from ledger
ctx_.view().erase(slep);
escrow.erase();
return tesSUCCESS;
}
@@ -7,10 +7,10 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -18,6 +18,7 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
@@ -184,162 +185,6 @@ EscrowCreate::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCreatePreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowCreatePreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
auto const& issue = amount.get<Issue>();
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = ctx.view.read(keylet::trustLine(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::kZero && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, issue, account); !isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, issue, dest); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(ctx.view, account, issue))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(ctx.view, dest, issue))
return tecFROZEN;
STAmount const spendableAmount = accountHolds(
ctx.view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
TER
escrowCreatePreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(ctx.view, account, *sleIssuance))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, *sleIssuance))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); !isTesSuccess(ter))
return ter;
STAmount const spendableAmount = accountHolds(
ctx.view,
account,
amount.get<MPTIssue>(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
EscrowCreate::preclaim(PreclaimContext const& ctx)
{
@@ -365,7 +210,7 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCreatePreclaimHelper<T>(ctx, account, dest, amount);
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
@@ -374,54 +219,6 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowLockApplyHelper(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal);
template <>
TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter =
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = lockEscrowMPT(view, sender, amount, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
TER
EscrowCreate::doApply()
{
@@ -481,30 +278,31 @@ EscrowCreate::doApply()
// Create escrow in ledger. Note that we use the value from the
// sequence or ticket. For more explanation see comments in SeqProxy.h.
Keylet const escrowKeylet = keylet::escrow(accountID_, ctx_.tx.getSeqProxy());
auto const slep = std::make_shared<SLE>(escrowKeylet);
(*slep)[sfAmount] = amount;
(*slep)[sfAccount] = accountID_;
(*slep)[~sfCondition] = ctx_.tx[~sfCondition];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[sfDestination] = ctx_.tx[sfDestination];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
EscrowEntryW escrow(accountID_, ctx_.tx.getSeqProxy(), ctx_.view(), j_);
Keylet const escrowKeylet = escrow.keylet();
escrow.newSLE();
(*escrow)[sfAmount] = amount;
(*escrow)[sfAccount] = accountID_;
(*escrow)[~sfCondition] = ctx_.tx[~sfCondition];
(*escrow)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*escrow)[sfDestination] = ctx_.tx[sfDestination];
(*escrow)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*escrow)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*escrow)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqProxy().value();
(*escrow)[sfSequence] = ctx_.tx.getSeqProxy().value();
}
if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount))
{
auto const xferRate = transferRate(ctx_.view(), amount);
if (xferRate != kParityRate)
(*slep)[sfTransferRate] = xferRate.value;
(*escrow)[sfTransferRate] = xferRate.value;
}
ctx_.view().insert(slep);
escrow.insert();
// Add escrow to sender's owner directory
{
@@ -512,7 +310,7 @@ EscrowCreate::doApply()
keylet::ownerDir(accountID_), escrowKeylet, describeOwnerDir(accountID_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
(*escrow)[sfOwnerNode] = *page;
}
// If it's not a self-send, add escrow to recipient's owner directory.
@@ -523,7 +321,7 @@ EscrowCreate::doApply()
ctx_.view().dirInsert(keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
(*escrow)[sfDestinationNode] = *page;
}
// IOU escrow objects are added to the issuer's owner directory to help
@@ -536,7 +334,7 @@ EscrowCreate::doApply()
ctx_.view().dirInsert(keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfIssuerNode] = *page;
(*escrow)[sfIssuerNode] = *page;
}
// Deduct owner's balance
@@ -559,7 +357,7 @@ EscrowCreate::doApply()
// increment owner count
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), escrow.mutableRawSle());
ctx_.view().update(sle);
return tesSUCCESS;
}
@@ -9,18 +9,14 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -136,68 +132,6 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
template <ValidIssueType T>
static TER
escrowFinishPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowFinishPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, amount.get<Issue>(), dest); !isTesSuccess(ter))
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (isDeepFrozen(ctx.view, dest, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
template <>
TER
escrowFinishPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the dest, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the destination is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, *sleIssuance))
return tecLOCKED;
return tesSUCCESS;
}
TER
EscrowFinish::preclaim(PreclaimContext const& ctx)
{
@@ -211,19 +145,18 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx.tx[sfOwner], seqProxy);
auto const slep = ctx.view.read(k);
if (!slep)
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
if (!escrow)
return tecNO_TARGET;
AccountID const dest = (*slep)[sfDestination];
STAmount const amount = (*slep)[sfAmount];
AccountID const dest = (*escrow)[sfDestination];
STAmount const amount = (*escrow)[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowFinishPreclaimHelper<T>(ctx, dest, amount);
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
@@ -237,9 +170,8 @@ TER
EscrowFinish::doApply()
{
auto const seqProxy = SeqProxy::rawSequence(ctx_.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx_.tx[sfOwner], seqProxy);
auto const slep = ctx_.view().peek(k);
if (!slep)
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
if (!escrow)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -252,11 +184,11 @@ EscrowFinish::doApply()
auto const now = ctx_.view().header().parentCloseTime;
// Too soon: can't execute before the finish time
if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter]))
if ((*escrow)[~sfFinishAfter] && !after(now, (*escrow)[sfFinishAfter]))
return tecNO_PERMISSION;
// Too late: can't execute after the cancel time
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
if ((*escrow)[~sfCancelAfter] && after(now, (*escrow)[sfCancelAfter]))
return tecNO_PERMISSION;
// Check cryptocondition fulfillment
@@ -296,7 +228,7 @@ EscrowFinish::doApply()
return tecCRYPTOCONDITION_ERROR;
// Check against condition in the ledger entry:
auto const cond = (*slep)[~sfCondition];
auto const cond = (*escrow)[~sfCondition];
// If a condition wasn't specified during creation,
// one shouldn't be included now.
@@ -313,7 +245,7 @@ EscrowFinish::doApply()
}
// NOTE: Escrow payments cannot be used to fund accounts.
AccountID const destID = (*slep)[sfDestination];
AccountID const destID = (*escrow)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (!sled)
return tecNO_DST;
@@ -323,12 +255,12 @@ EscrowFinish::doApply()
!isTesSuccess(err))
return err;
AccountID const account = (*slep)[sfAccount];
AccountID const account = (*escrow)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
auto const page = (*escrow)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
@@ -338,9 +270,9 @@ EscrowFinish::doApply()
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode])
if (auto const optPage = (*escrow)[~sfDestinationNode])
{
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true))
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -355,9 +287,9 @@ EscrowFinish::doApply()
bool const recycleReserve =
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
if (recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
STAmount const amount = slep->getFieldAmount(sfAmount);
STAmount const amount = escrow->getFieldAmount(sfAmount);
// Transfer amount to destination
if (isXRP(amount))
{
@@ -368,8 +300,8 @@ EscrowFinish::doApply()
if (!ctx_.view().rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
Rate lockedRate = slep->isFieldPresent(sfTransferRate)
? xrpl::Rate(slep->getFieldU32(sfTransferRate))
Rate lockedRate = escrow->isFieldPresent(sfTransferRate)
? xrpl::Rate(escrow->getFieldU32(sfTransferRate))
: kParityRate;
auto const issuer = amount.getIssuer();
bool const createAsset = destID == accountID_;
@@ -392,9 +324,9 @@ EscrowFinish::doApply()
return ret;
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
if (auto const optPage = (*escrow)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -407,10 +339,10 @@ EscrowFinish::doApply()
ctx_.view().update(sled);
if (!recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
// Remove escrow from ledger
ctx_.view().erase(slep);
escrow.erase();
return tesSUCCESS;
}
+45 -11
View File
@@ -18,6 +18,7 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/Quality.h>
@@ -715,22 +716,55 @@ Payment::doApply()
// Amount to deliver.
STAmount amountDeliver = dstAmount;
// Factor in the transfer rate.
// No rounding. It'll change once MPT integrated into DEX.
STAmount requiredMaxSourceAmount = multiply(dstAmount, rate);
// True if the sender would pay more than SendMax.
bool exceedsSendMax = false;
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount)
if (view().rules().enabled(fixCleanup3_5_0))
{
requiredMaxSourceAmount = maxSourceAmount;
// The legacy multiply() and divide() can overflow on large MPT
// amounts, and Number rounding can charge more than SendMax.
// MPTs are integral, so use exact integer arithmetic. The cost
// is rounded up, matching the transfer fee that accountSend
// charges. If the cost overflows, it exceeds any SendMax.
auto const requiredMaxSourceAmount =
tryMulRatio(dstAmount.mpt(), rate.value, QUALITY_ONE, true);
exceedsSendMax =
!requiredMaxSourceAmount || maxSourceAmount.mpt() < *requiredMaxSourceAmount;
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && exceedsSendMax)
{
// Round the delivered amount down so that the sender is
// never charged more than SendMax.
auto const delivered =
mulRatio(maxSourceAmount.mpt(), QUALITY_ONE, rate.value, false);
amountDeliver = STAmount(maxSourceAmount.asset(), delivered.value());
if (amountDeliver <= beast::kZero)
return tecPATH_PARTIAL;
exceedsSendMax = false;
}
}
else
{
// Factor in the transfer rate.
// No rounding. It'll change once MPT integrated into DEX.
amountDeliver = divide(maxSourceAmount, rate);
STAmount requiredMaxSourceAmount = multiply(dstAmount, rate);
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount)
{
requiredMaxSourceAmount = maxSourceAmount;
// No rounding. It'll change once MPT integrated into DEX.
amountDeliver = divide(maxSourceAmount, rate);
}
exceedsSendMax = requiredMaxSourceAmount > maxSourceAmount;
}
if (requiredMaxSourceAmount > maxSourceAmount ||
(deliverMin && amountDeliver < *deliverMin))
if (exceedsSendMax || (deliverMin && amountDeliver < *deliverMin))
return tecPATH_PARTIAL;
PaymentSandbox pv(&view());
+7 -4
View File
@@ -9,6 +9,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h> // IWYU pragma: keep
@@ -297,11 +298,13 @@ Batch::preflight(PreflightContext const& ctx)
}
auto const txType = stx.getFieldU16(sfTransactionType);
if (std::ranges::any_of(
kDisabledTxTypes, [txType](auto const& disabled) { return txType == disabled; }))
{
// Pre-LendingProtocolV1_2: SAV and Lending transactions cannot be Batch inners.
// Post-LendingProtocolV1_2: they continue through the normal Batch checks.
bool const rejectedPreV12 = !ctx.rules.enabled(featureLendingProtocolV1_2) &&
std::ranges::any_of(
kDisabledTxTypes, [txType](auto const& disabled) { return txType == disabled; });
if (rejectedPreV12)
return temINVALID_INNER_BATCH;
}
if (!stx.isFlag(tfInnerBatchTxn))
{
+16 -20
View File
@@ -8,8 +8,10 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/AmendmentTable.h>
#include <xrpl/ledger/entries/AmendmentsEntry.h>
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
#include <xrpl/ledger/entries/NegativeUNLEntry.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
@@ -25,7 +27,6 @@
#include <xrpl/tx/Transactor.h>
#include <algorithm>
#include <memory>
namespace xrpl {
@@ -168,14 +169,12 @@ Change::applyAmendment()
{
UInt256 const amendment(ctx_.tx.getFieldH256(sfAmendment));
auto const k = keylet::amendments();
SLE::pointer amendmentObject = view().peek(k);
AmendmentsEntryW amendmentObject(view(), j_);
if (!amendmentObject)
{
amendmentObject = std::make_shared<SLE>(k);
view().insert(amendmentObject);
amendmentObject.newSLE();
amendmentObject.insert();
}
STVector256 amendments = amendmentObject->getFieldV256(sfAmendments);
@@ -252,7 +251,7 @@ Change::applyAmendment()
amendmentObject->setFieldArray(sfMajorities, newMajorities);
}
view().update(amendmentObject);
amendmentObject.update();
return tesSUCCESS;
}
@@ -260,16 +259,14 @@ Change::applyAmendment()
TER
Change::applyFee()
{
auto const k = keylet::feeSettings();
SLE::pointer feeObject = view().peek(k);
FeeSettingsEntryW feeObject(view());
if (!feeObject)
{
feeObject = std::make_shared<SLE>(k);
view().insert(feeObject);
feeObject.newSLE();
feeObject.insert();
}
auto set = [](SLE::pointer& feeObject, STTx const& tx, auto const& field) {
auto set = [](FeeSettingsEntryW& feeObject, STTx const& tx, auto const& field) {
feeObject->at(field) = tx[field];
};
if (view().rules().enabled(featureXRPFees))
@@ -291,7 +288,7 @@ Change::applyFee()
set(feeObject, ctx_.tx, sfReserveIncrement);
}
view().update(feeObject);
feeObject.update();
JLOG(j_.warn()) << "Fees have been changed";
return tesSUCCESS;
@@ -332,12 +329,11 @@ Change::applyUNLModify()
JLOG(j_.info()) << "N-UNL: applyUNLModify, " << (disabling ? "ToDisable" : "ToReEnable")
<< " seq=" << seq << " validator data:" << strHex(validator);
auto const k = keylet::negativeUNL();
SLE::pointer negUnlObject = view().peek(k);
NegativeUNLEntryW negUnlObject(view(), j_);
if (!negUnlObject)
{
negUnlObject = std::make_shared<SLE>(k);
view().insert(negUnlObject);
negUnlObject.newSLE();
negUnlObject.insert();
}
bool const found = [&] {
@@ -410,7 +406,7 @@ Change::applyUNLModify()
negUnlObject->setFieldVL(sfValidatorToReEnable, validator);
}
view().update(negUnlObject);
negUnlObject.update();
return tesSUCCESS;
}
+1
View File
@@ -12,6 +12,7 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/paychan.h>
#include <test/jtx/rate.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
+1
View File
@@ -11,6 +11,7 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/paychan.h>
#include <test/jtx/rate.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
+175 -21
View File
@@ -47,7 +47,6 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
@@ -63,13 +62,13 @@
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/rdb/RelationalDatabase.h>
#include <xrpl/server/NetworkOPs.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/apply.h>
#include <xrpl/tx/transactors/payment/Payment.h>
#include <xrpl/tx/transactors/system/Batch.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <exception>
@@ -120,15 +119,6 @@ class Batch_test : public beast::unit_test::Suite
return env.rpc("json", "ledger", to_string(params));
}
void
validateInnerTxn(jtx::Env& env, std::string const& batchID, TestLedgerData const& ledgerResult)
{
json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result];
BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(jrr[jss::meta][sfTransactionResult.jsonName] == ledgerResult.result);
BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID);
}
void
validateClosedLedger(jtx::Env& env, std::vector<TestLedgerData> const& ledgerResults)
{
@@ -144,7 +134,14 @@ class Batch_test : public beast::unit_test::Suite
BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result);
if (ledgerResult.batchID)
validateInnerTxn(env, *ledgerResult.batchID, ledgerResult);
{
jtx::batch::validateInnerTxn(
env,
*ledgerResult.batchID,
ledgerResult.txHash,
ledgerResult.txType,
ledgerResult.result);
}
}
}
@@ -3140,9 +3137,7 @@ class Batch_test : public beast::unit_test::Suite
{
testcase("loan");
bool const lendingBatchEnabled = !std::ranges::any_of(
Batch::kDisabledTxTypes,
[](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; });
bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
using namespace test::jtx;
@@ -3291,8 +3286,12 @@ class Batch_test : public beast::unit_test::Suite
batch::Sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
BEAST_EXPECT(!env.le(loanKeylet));
auto const brokerSleBefore = env.le(brokerKeylet);
if (!BEAST_EXPECT(brokerSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const coverDepositAmount = asset(100).value();
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
@@ -3311,19 +3310,65 @@ class Batch_test : public beast::unit_test::Suite
Fee(kNone),
Seq(kNone)),
lenderSeq + 1),
batch::Inner(manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 2),
batch::Inner(
loan_broker::coverDeposit(lender, brokerKeylet.key, coverDepositAmount),
lenderSeq + 2),
batch::Sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
if (auto const sleLoan = env.le(loanKeylet);
lendingBatchEnabled ? BEAST_EXPECT(sleLoan) : !BEAST_EXPECT(!sleLoan))
BEAST_EXPECT(static_cast<bool>(env.le(loanKeylet)) == lendingBatchEnabled);
if (auto const brokerSle = env.le(brokerKeylet); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
auto const expectedCover = lendingBatchEnabled
? coverAvailableBefore + coverDepositAmount
: coverAvailableBefore;
BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCover);
}
}
}
void
testLendingAmendment(FeatureBitset features)
{
testcase("lending amendment");
using namespace test::jtx;
// Before LendingProtocolV1_2 a Vault inner transaction rejects the whole batch.
auto const checkVaultBatch = [this](FeatureBitset amendments) {
bool const lendingBatchEnabled = amendments[featureLendingProtocolV1_2];
Env env{*this, amendments};
Account const payer{"payer"};
Account const lender{"lender"};
env.fund(XRP(100'000), payer, lender);
env.close();
Vault const vault{env};
auto [create, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()});
auto const payerSeq = env.seq(payer);
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
submitBatch(
env,
lendingBatchEnabled ? TER{tesSUCCESS} : TER{temINVALID_INNER_BATCH},
batch::outer(payer, payerSeq, batchFee, tfAllOrNothing),
batch::Inner(create, lenderSeq),
batch::Inner(
vault.deposit(
{.depositor = lender, .id = vaultKeylet.key, .amount = XRP(1'000)}),
lenderSeq + 1),
batch::Sig(lender));
env.close();
BEAST_EXPECT(static_cast<bool>(env.le(vaultKeylet)) == lendingBatchEnabled);
};
checkVaultBatch(features - featureLendingProtocolV1_2);
checkVaultBatch(features);
}
void
testObjectCreateSequence(FeatureBitset features)
{
@@ -5896,6 +5941,113 @@ class Batch_test : public beast::unit_test::Suite
}
}
void
testInnerResults(FeatureBitset features)
{
// `tx` on a validated Batch reports each inner transaction's outcome from the
// BatchInnerResults table, including inner transactions that never reached the
// ledger.
testcase("inner results");
using namespace test::jtx;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// tfAllOrNothing with a future-sequence inner: the first inner applies to the batch
// view and is then rolled back, the second fails with terPRE_SEQ. Neither is in the
// ledger; both are recorded, both applied == false.
{
auto const aliceSeq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 10));
env.close();
auto const jrr = env.rpc("tx", batchID)[jss::result];
BEAST_EXPECT(jrr[jss::validated] == true);
if (BEAST_EXPECT(jrr.isMember(jss::inner_results)))
{
auto const& inner = jrr[jss::inner_results];
BEAST_EXPECT(inner.isArray());
if (BEAST_EXPECT(inner.size() == 2))
{
BEAST_EXPECT(inner[0u][jss::hash] == txIDs[0]);
BEAST_EXPECT(inner[0u][jss::result] == "tesSUCCESS");
BEAST_EXPECT(inner[0u][jss::applied] == false);
BEAST_EXPECT(inner[1u][jss::hash] == txIDs[1]);
BEAST_EXPECT(inner[1u][jss::result] == "terPRE_SEQ");
BEAST_EXPECT(inner[1u][jss::applied] == false);
}
}
// The rolled-back inner transaction is not in the ledger.
BEAST_EXPECT(env.rpc("tx", txIDs[0])[jss::result][jss::error] == "txnNotFound");
// getBatchInnerResults is scoped to the Batch's own ledger sequence: the
// rows it just wrote come back for that sequence and for no other, so a
// stale row from a different sequence can never surface through `tx`.
UInt256 parentBatchId;
BEAST_EXPECT(parentBatchId.parseHex(batchID));
auto const batchSeq = env.closed()->seq();
BEAST_EXPECT(
env.app()
.getRelationalDatabase()
.getBatchInnerResults(parentBatchId, batchSeq)
.size() == 2);
BEAST_EXPECT(env.app()
.getRelationalDatabase()
.getBatchInnerResults(parentBatchId, batchSeq + 1)
.empty());
}
// tfIndependent, both succeed: both applied, in RawTransactions order.
{
auto const aliceSeq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2));
env.close();
auto const jrr = env.rpc("tx", batchID)[jss::result];
if (BEAST_EXPECT(jrr.isMember(jss::inner_results)))
{
auto const& inner = jrr[jss::inner_results];
if (BEAST_EXPECT(inner.size() == 2))
{
for (unsigned i = 0; i < 2; ++i)
{
BEAST_EXPECT(inner[i][jss::hash] == txIDs[i]);
BEAST_EXPECT(inner[i][jss::result] == "tesSUCCESS");
BEAST_EXPECT(inner[i][jss::applied] == true);
}
}
}
// The inner transactions themselves carry no inner_results.
BEAST_EXPECT(!env.rpc("tx", txIDs[0])[jss::result].isMember(jss::inner_results));
}
// A non-Batch transaction never has the field.
{
auto const jt = env.jt(pay(alice, bob, XRP(1)));
env(jt);
env.close();
auto const jrr = env.rpc("tx", strHex(jt.stx->getTransactionID()))[jss::result];
BEAST_EXPECT(jrr[jss::validated] == true);
BEAST_EXPECT(!jrr.isMember(jss::inner_results));
}
}
void
testWithFeats(FeatureBitset features)
{
@@ -5915,6 +6067,7 @@ class Batch_test : public beast::unit_test::Suite
testCheckAllSignatures(features);
testAccountSet(features);
testAccountDelete(features);
testLendingAmendment(features);
testLoan(features);
testObjectCreateSequence(features);
testObjectCreateTicket(features);
@@ -5931,6 +6084,7 @@ class Batch_test : public beast::unit_test::Suite
testBatchDelegateConsent(features);
testValidateRPCResponse(features);
testBatchCalculateBaseFee(features);
testInnerResults(features);
testStandaloneInnerBatchFlag(features);
testOuterBinding(features);
testUnsortedBatchSigners(features);
+150
View File
@@ -18,10 +18,12 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/rate.h>
#include <test/jtx/regkey.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
@@ -1035,6 +1037,15 @@ class Delegate_test : public beast::unit_test::Suite
env(delegate::set(gw, bob, {"PaymentMint"}));
env.close();
// Cross-currency sfSendMax without any flag passes the granular
// template and is rejected by the granular semantic check, because
// the sfSendMax asset (XRP) is not the same as the sfAmount asset (USD).
env(pay(gw, alice, usd(500)),
Sendmax(XRP(1001)),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
BEAST_EXPECT(expectOffers(env, carol, 1));
// bob can not send cross currency payment on behalf of the gw,
// even with PaymentMint permission and gw being the issuer.
env(pay(gw, alice, usd(5000)),
@@ -1063,6 +1074,15 @@ class Delegate_test : public beast::unit_test::Suite
env(delegate::set(alice, bob, {"PaymentBurn"}));
env.close();
// Cross-currency sfSendMax without any flag passes the granular
// template and is rejected by the granular semantic check, because
// the sfSendMax asset (XRP) is not the same as the sfAmount asset (USD).
env(pay(alice, gw, usd(500)),
Sendmax(XRP(1001)),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
BEAST_EXPECT(expectOffers(env, bob, 1));
// bob can not send cross currency payment on behalf of alice,
// even with PaymentBurn permission and gw being the issuer.
env(pay(alice, gw, usd(5000)),
@@ -1263,6 +1283,34 @@ class Delegate_test : public beast::unit_test::Suite
Domain(UInt256{1}),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
// sfPaths is not in the PaymentMint or PaymentBurn template.
env(pay(gw, alice, usd(100)),
Path(~XRP),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
env(pay(alice, gw, usd(50)),
Path(~XRP),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
// Valid Payment flags that are not in the PaymentMint or PaymentBurn template.
for (auto const flag : {tfPartialPayment, tfNoRippleDirect, tfLimitQuality})
{
env(pay(gw, alice, usd(100)),
Txflags(flag),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
env(pay(alice, gw, usd(50)),
Txflags(flag),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
}
// The same payments without the extra field or flag succeed.
env(pay(gw, alice, usd(100)), delegate::As(bob));
env(pay(alice, gw, usd(50)), delegate::As(bob));
env.require(Balance(alice, usd(50)));
}
// Delegate account holds no granular permissions for the tx type:
@@ -2266,6 +2314,107 @@ class Delegate_test : public beast::unit_test::Suite
// terNO_DELEGATE_PERMISSION. The set of permitted flags for delegation is defined in
// permissions.macro.
}
// Fields and flags not in permissions.macro template are not permitted for delegation.
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const credIssuer{"credIssuer"};
env.fund(XRP(100000), alice, bob, credIssuer);
env.close();
env(pdomain::setTx(credIssuer, {{.issuer = credIssuer, .credType = "credential"}}));
auto const domainID = pdomain::getNewDomain(env.meta());
env.close();
MPTTester mpt(env, alice, {.fund = false});
mpt.create({.flags = tfMPTCanLock | tfMPTCanTransfer});
env.close();
// bob holds every granular permission defined for MPTokenIssuanceSet
env(delegate::set(alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}));
env.close();
// Any of those fields or flags not defined in permissions.macro are not permitted for
// delegation.
std::vector<MPTSet> const args = {
{.transferFee = 100},
{.metadata = "test"},
{.domainID = domainID},
{.flags = tfMPTSetCanTrade},
{.flags = tfMPTSetCanClawback},
{.immutableFlags = tifMPTTransferFee},
// a permitted flag does not unlock the extra fields
{.flags = tfMPTLock, .domainID = domainID},
};
for (auto arg : args)
{
arg.account = alice;
arg.delegate = bob;
arg.err = terNO_DELEGATE_PERMISSION;
mpt.set(arg);
env.close();
}
}
}
void
testGranularPermissionWithSponsorCommonFields()
{
testcase("test granular permission with sponsor common fields");
using namespace jtx;
Account const alice{"alice"};
Account const bob{"bob"};
Account const gw{"gw"};
Account const sponsor{"sponsor"};
auto const usd = gw["USD"];
auto const feeAmt = XRP(10);
// Sponsor fields are common fields, so they are permitted in every granular template.
// Fee sponsorship is allowed for a granular delegated transaction.
Env env(*this);
env.fund(XRP(10000), alice, bob, gw, sponsor);
env.trust(usd(200), alice);
env.close();
env(delegate::set(gw, bob, {"PaymentMint"}));
env.close();
// Co-signed: the sponsor account pays the fee.
auto const gwBalance = env.balance(gw);
auto const bobBalance = env.balance(bob);
auto const sponsorBalance = env.balance(sponsor);
env(pay(gw, alice, usd(50)),
delegate::As(bob),
Fee(feeAmt),
sponsor::As(sponsor, spfSponsorFee),
Sig(sfSponsorSignature, sponsor));
env.close();
env.require(Balance(alice, usd(50)));
BEAST_EXPECT(env.balance(gw) == gwBalance);
BEAST_EXPECT(env.balance(bob) == bobBalance);
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt);
// Pre-funded: sponsorship(sponsor, bob) pays the fee, bob is sfDelegate.
env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(bob));
env.close();
auto const sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, bob);
env(pay(gw, alice, usd(50)),
delegate::As(bob),
Fee(feeAmt),
sponsor::As(sponsor, spfSponsorFee));
env.close();
env.require(Balance(alice, usd(100)));
BEAST_EXPECT(env.balance(gw) == gwBalance);
BEAST_EXPECT(env.balance(bob) == bobBalance);
BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, bob) == sponsorFee - feeAmt);
// Reserve sponsorship is not tested here: checkSponsor rejects it with temINVALID for any
// delegated transaction before the permission check, regardless of the delegate's
// permissions. It is covered by testDelegateBlockReserveSponsor in Sponsor_test.
}
void
@@ -3002,6 +3151,7 @@ class Delegate_test : public beast::unit_test::Suite
testTrustSetGranular();
testAccountSetGranular();
testMPTokenIssuanceSetGranular();
testGranularPermissionWithSponsorCommonFields();
testSingleSign();
testSingleSignBadSecret();
testMultiSign();
+12 -53
View File
@@ -1,6 +1,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
#include <test/jtx/escrow.h>
@@ -17,7 +18,6 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Dir.h>
#include <xrpl/ledger/OpenView.h>
@@ -25,7 +25,6 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
@@ -46,52 +45,6 @@ namespace xrpl::test {
struct EscrowToken_test : public beast::unit_test::Suite
{
static uint64_t
mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptoken(mpt.mpt(), account));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
static uint64_t
issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptokenIssuance(mpt.mpt()));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
static jtx::PrettyAmount
issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const obligations = result[jss::obligations][to_string(issue.currency)];
if (obligations.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, obligations.asString());
return {amount, account.name()};
}
static jtx::PrettyAmount
issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const locked = result[jss::locked][to_string(issue.currency)];
if (locked.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, locked.asString());
return {amount, account.name()};
}
void
testIOUEnablement(FeatureBitset features)
{
@@ -3856,10 +3809,15 @@ struct EscrowToken_test : public beast::unit_test::Suite
auto const gw = Account("gw");
for (auto const testFeatures :
{features - featureMPTokensV2 - fixCleanup3_4_0,
{features - featureMPTokensV2 - fixCleanup3_4_0 - fixCleanup3_5_0,
features - featureMPTokensV2 - fixCleanup3_4_0,
features - featureMPTokensV2 - fixCleanup3_5_0,
features - featureMPTokensV2,
(features | featureMPTokensV2) - fixCleanup3_4_0 - fixCleanup3_5_0,
(features | featureMPTokensV2) - fixCleanup3_4_0,
features | featureMPTokensV2})
(features | featureMPTokensV2) - fixCleanup3_5_0,
features | featureMPTokensV2,
features | fixCleanup3_5_0})
{
bool const mptV2 = testFeatures[featureMPTokensV2];
bool const tokenEscrowV1 = testFeatures[fixTokenEscrowV1];
@@ -3867,10 +3825,11 @@ struct EscrowToken_test : public beast::unit_test::Suite
// legacy divideRound(amount, lockedRate, ...) path, which runs when
// fixCleanup3_4_0 is disabled. With fixCleanup3_4_0 the split uses
// mulRatio (128-bit intermediate), which cannot overflow. Without
// it, this large amount overflows unless the MPTokensV2 Number path
// is active. So the finish succeeds when either amendment is enabled.
// it, this large amount overflows unless divideRound takes the
// Number path, which MPTokensV2 or fixCleanup3_5_0 enables. So the
// finish succeeds when any of these amendments is enabled.
bool const cleanup340 = testFeatures[fixCleanup3_4_0];
bool const noOverflow = cleanup340 || mptV2;
bool const noOverflow = cleanup340 || mptV2 || testFeatures[fixCleanup3_5_0];
auto const expectedErr = noOverflow ? Ter(tesSUCCESS) : Ter(tefEXCEPTION);
// Finish with a large MPT amount and non-zero transfer fee. When the
+340 -4
View File
@@ -3864,14 +3864,16 @@ class MPToken_test : public beast::unit_test::Suite
// Bob can pay carol
MPT const mptc = mptAlice;
if (!features[featureMPTokensV2])
if (!features[featureMPTokensV2] && !features[fixCleanup3_5_0])
{
mptAlice.pay(bob, carol, 50);
BEAST_EXPECT(env.balance(carol, mptc) == mptc(50));
}
else
{
// The difference is due to the rounding in MPT/DEX.
// The transfer fee (0.05) is rounded up, so delivering 50
// costs 51, which exceeds the implicit SendMax.
mptAlice.pay(bob, carol, 50, tecPATH_PARTIAL);
// 1 MPTC is the transfer fee paid by bob to the issuer.
env(pay(bob, carol, mptAlice(50)), Txflags(tfPartialPayment));
BEAST_EXPECT(env.balance(carol, mptc) == mptc(49));
@@ -7707,6 +7709,318 @@ class MPToken_test : public beast::unit_test::Suite
}
}
void
testPartialPaymentRounding(FeatureBitset features)
{
testcase("Partial payment rounding");
// With fixCleanup3_5_0, the legacy direct-MPT partial payment rounds the
// delivered amount down instead of to nearest and the transfer fee up,
// so the sender is never charged more than SendMax. This matches V2.
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder / sender
Account const carol("carol"); // holder / receiver
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
// Transfer fee is 10%
mptAlice.create(
{.transferFee = 10'000, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 780);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(100)), Sendmax(mpt(90)), Txflags(tfPartialPayment));
if (features[featureMPTokensV2] || features[fixCleanup3_5_0])
{
// 81 to carol, 9 to issuer (90 / 1.1 ~ 81.81 (rounded down) =
// 81, 81 * 1.1 = 89.1 (rounded up) = 90). In V2 the payments are
// executed via the payment engine, which rounds the same way.
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 81));
}
else
{
// 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) =
// 82)
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 82));
}
}
void
testPartialPaymentDivideOverflow(FeatureBitset features)
{
testcase("Partial payment divide overflow");
// Regression: legacy MPT partial-payment divide rounding overflow.
//
// Under MPTokensV1 (V2 disabled), a holder-to-holder partial Payment can
// hit STAmount::divide's unchecked `muldiv(...) + 5` wrap and return a
// zero delivered amount that is nevertheless accepted as tesSUCCESS by
// the legacy direct-MPT branch. fixCleanup3_5_0 computes the
// delivered amount with mulRatio (exact 128-bit arithmetic) instead.
// Under MPTokensV2 the payment is routed through RippleCalc, which uses
// Number.
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder / sender
Account const carol("carol"); // holder / receiver
// The precise sendMax chosen so that
// muldiv(N * 10^17, D) == UINT64_MAX - 2
// where D is the canonicalized rate mantissa for transferFee = 3
// (D == 1'000'030'000'000'000). The subsequent +5 wraps to 2, and
// the resulting STAmount (mantissa=2, offset=-2) canonicalizes to 0
// for an integral MPT asset.
constexpr std::int64_t sendMax = 184'472'974'760'317'629;
// The largest integer k such that k * 1.00003 <= sendMax.
constexpr std::int64_t expectedDelivered = 184'467'440'737'095'516;
bool const fixed = features[featureMPTokensV2] || features[fixCleanup3_5_0];
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, sendMax);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(sendMax)), Sendmax(mpt(sendMax)), Txflags(tfPartialPayment));
auto const bobBal = mptAlice.getBalance(bob);
auto const carolBal = mptAlice.getBalance(carol);
if (!fixed)
{
// divide(sendMax, rate) wraps to MPT(0), accountSendMPT(0)
// short-circuits to tesSUCCESS, and no balances change.
BEAST_EXPECT(bobBal == sendMax);
BEAST_EXPECT(carolBal == 0);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(sendMax));
}
else
{
// The transfer fee is redeemed to the issuer, so
// OutstandingAmount shrinks by that fee.
BEAST_EXPECT(carolBal == expectedDelivered);
BEAST_EXPECT(bobBal == 0);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(bobBal + carolBal));
}
auto const meta = env.meta();
BEAST_EXPECT(meta->isFieldPresent(sfDeliveredAmount));
if (meta->isFieldPresent(sfDeliveredAmount))
{
BEAST_EXPECT(
meta->getFieldAmount(sfDeliveredAmount) ==
mpt(fixed ? expectedDelivered : 0).value());
}
}
// A larger sendMax makes muldiv(N * 10^17, D) itself exceed
// UINT64_MAX, so the legacy divide() throws instead of wrapping.
{
constexpr std::int64_t largeSendMax = 200'000'000'000'000'000;
// The largest integer k such that k * 1.00003 <= largeSendMax.
constexpr std::int64_t largeExpectedDelivered = 199'994'000'179'994'600;
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, largeSendMax);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(largeSendMax)),
Sendmax(mpt(largeSendMax)),
Txflags(tfPartialPayment),
fixed ? Ter(tesSUCCESS) : Ter(tefEXCEPTION));
auto const bobBal = mptAlice.getBalance(bob);
auto const carolBal = mptAlice.getBalance(carol);
if (!fixed)
{
BEAST_EXPECT(bobBal == largeSendMax);
BEAST_EXPECT(carolBal == 0);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(largeSendMax));
}
else
{
BEAST_EXPECT(carolBal == largeExpectedDelivered);
BEAST_EXPECT(bobBal == 0 || bobBal == 1);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(bobBal + carolBal));
}
}
// The cost of delivering kMaxMpTokenAmount with a transfer fee doesn't
// fit in an MPT amount. Without the fix, the legacy
// multiply(Amount, rate) throws. With it, or under MPTokensV2, the
// overflowing cost is treated as exceeding SendMax.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 1'000);
auto const mpt = mptAlice["MPT"];
// Not a partial payment: the cost exceeds SendMax.
env(pay(bob, carol, mpt(kMaxMpTokenAmount)),
Sendmax(mpt(1'000)),
fixed ? Ter(tecPATH_PARTIAL) : Ter(tefEXCEPTION));
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000);
BEAST_EXPECT(mptAlice.getBalance(carol) == 0);
// Partial payment: deliver floor(1000 / 1.00003) = 999 for a cost
// of ceil(999 * 1.00003) = 1000.
env(pay(bob, carol, mpt(kMaxMpTokenAmount)),
Sendmax(mpt(1'000)),
Txflags(tfPartialPayment),
fixed ? Ter(tesSUCCESS) : Ter(tefEXCEPTION));
BEAST_EXPECT(mptAlice.getBalance(bob) == (fixed ? 0 : 1'000));
BEAST_EXPECT(mptAlice.getBalance(carol) == (fixed ? 999 : 0));
}
// With the fix, a partial payment that would deliver zero fails
// instead of succeeding without moving any funds.
if (!features[featureMPTokensV2] && features[fixCleanup3_5_0])
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.transferFee = 3, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 10);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(10)),
Sendmax(mpt(1)),
Txflags(tfPartialPayment),
Ter(tecPATH_PARTIAL));
BEAST_EXPECT(mptAlice.getBalance(bob) == 10);
BEAST_EXPECT(mptAlice.getBalance(carol) == 0);
}
}
void
testPartialPaymentSendMax(FeatureBitset features)
{
testcase("Partial payment does not exceed SendMax");
// A partial payment must not charge bob more than SendMax. Without
// fixCleanup3_5_0, the cost is computed with Number, which rounds large
// amounts when it has only 16 digits (SingleAssetVault and
// LendingProtocol disabled). bob is funded with 1000 extra so that an
// overcharge shows up in his balance.
//
// SendMax = 122'354'741'735'175'090, transfer fee 3 (rate 1.00003):
//
// | Config | bob pays | carol receives |
// |---------------------------------|--------------|------------------|
// | fixCleanup3_5_0 or MPTokensV2 | SendMax | ...998 (floor) |
// | neither, Number with 19 digits | SendMax | ...999 (nearest) |
// | neither, Number with 16 digits | SendMax + 10 | ...999 (nearest) |
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder / sender
Account const carol("carol"); // holder / receiver
bool const fixed = features[featureMPTokensV2] || features[fixCleanup3_5_0];
bool const smallNumber =
!features[featureSingleAssetVault] && !features[featureLendingProtocol];
// Small enough that the legacy divide() doesn't overflow. Larger
// amounts are covered by testPartialPaymentDivideOverflow.
constexpr std::int64_t sendMax = 122'354'741'735'175'090;
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, sendMax + 1'000);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(sendMax)), Sendmax(mpt(sendMax)), Txflags(tfPartialPayment));
if (fixed)
{
// fixCleanup3_5_0 or MPTokensV2: bob pays exactly SendMax, carol
// receives floor(SendMax / rate)
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000);
BEAST_EXPECT(mptAlice.getBalance(carol) == 122'351'071'203'038'998);
}
else if (!smallNumber)
{
// Neither, Number with 19 digits: the Number cost is precise
// enough, so bob pays exactly SendMax
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000);
// The legacy divide() rounds to nearest, so carol receives one
// more than floor(SendMax / rate)
BEAST_EXPECT(mptAlice.getBalance(carol) == 122'351'071'203'038'999);
}
else
{
// Neither, Number with 16 digits: the Number cost is rounded 10
// above SendMax
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000 - 10);
// The legacy divide() rounds to nearest, so carol receives one
// more than floor(SendMax / rate)
BEAST_EXPECT(mptAlice.getBalance(carol) == 122'351'071'203'038'999);
}
}
public:
void
run() override
@@ -7767,8 +8081,8 @@ public:
// Test Direct Payment
testPayment(all);
testPayment(all | featureSingleAssetVault);
testPayment((all | featureSingleAssetVault) - featureMPTokensV2);
testPayment(all - featureMPTokensV2);
testPayment((all | featureSingleAssetVault) - featureMPTokensV2 - fixCleanup3_5_0);
testPayment(all - featureMPTokensV2 - fixCleanup3_5_0);
testDepositPreauth(all);
testDepositPreauth(all - featureCredentials);
@@ -7826,6 +8140,28 @@ public:
testLockedMPTokenDestroyedIssuance(all - fixCleanup3_4_0);
testLockedMPTokenDestroyedIssuance(all - featureSingleAssetVault);
testLockedMPTokenDestroyedIssuance(all - featureSingleAssetVault - fixCleanup3_4_0);
testPartialPaymentRounding(all);
testPartialPaymentRounding(all - featureMPTokensV2);
testPartialPaymentRounding(all - featureMPTokensV2 - fixCleanup3_5_0);
testPartialPaymentDivideOverflow(all);
testPartialPaymentDivideOverflow(all - featureMPTokensV2);
#ifndef XRPL_UBSAN
// Without the fix, the legacy divide() relies on unsigned wraparound,
// which UBSan reports as unsigned-integer-overflow.
testPartialPaymentDivideOverflow(all - featureMPTokensV2 - fixCleanup3_5_0);
#endif
// Number has a 16-digit mantissa without these amendments.
auto const smallNumber =
all - featureMPTokensV2 - featureSingleAssetVault - featureLendingProtocol;
testPartialPaymentRounding(smallNumber);
testPartialPaymentRounding(smallNumber - fixCleanup3_5_0);
testPartialPaymentSendMax(smallNumber);
testPartialPaymentSendMax(smallNumber - fixCleanup3_5_0);
testPartialPaymentSendMax(smallNumber | featureMPTokensV2);
testPartialPaymentSendMax((smallNumber | featureMPTokensV2) - fixCleanup3_5_0);
testPartialPaymentSendMax(all);
testPartialPaymentSendMax(all - featureMPTokensV2 - fixCleanup3_5_0);
}
};
+25 -25
View File
@@ -9,6 +9,7 @@
#include <test/jtx/deposit.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/paychan.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/txflags.h>
@@ -17,7 +18,6 @@
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
@@ -35,13 +35,10 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
@@ -77,27 +74,6 @@ struct PayChan_test : public beast::unit_test::Suite
return {k.key, view.read(k)};
}
static Buffer
signClaimAuth(
PublicKey const& pk,
SecretKey const& sk,
UInt256 const& channel,
STAmount const& authAmt)
{
Serializer msg;
serializePayChanAuthorization(msg, channel, authAmt.xrp());
return sign(pk, sk, msg.slice());
}
static STAmount
channelAmount(ReadView const& view, UInt256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfAmount];
}
static std::optional<std::int64_t>
channelExpiration(ReadView const& view, UInt256 const& chan)
{
@@ -1404,6 +1380,30 @@ struct PayChan_test : public beast::unit_test::Suite
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
}
{
// channel_id is not a string.
json::Value args{json::ValueType::Object};
args[jss::amount] = "2000";
args[jss::channel_id] = 2000;
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
auto const ra =
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(ra[jss::error] == "invalidParams");
}
{
// channel_id is not a string.
json::Value args{json::ValueType::Object};
args[jss::public_key] = chan1PkStr;
args[jss::channel_id] = 2000;
args[jss::amount] = "1000";
args[jss::signature] = sig;
auto const rv =
env.rpc("json", "channel_verify", args.toStyledString())[jss::result];
BEAST_EXPECT(rv[jss::error] == "invalidParams");
}
{
// give an ill formed base 58 public key
auto illFormedPk = chan1PkStr.substr(0, chan1PkStr.size() - 1);
+1
View File
@@ -19,6 +19,7 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/paychan.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
+69 -7
View File
@@ -36,6 +36,7 @@
#include <functional>
#include <initializer_list>
#include <memory>
#include <ranges>
#include <source_location>
#include <string>
#include <tuple>
@@ -1099,6 +1100,62 @@ class InvariantsMPT_test : public InvariantsBase
});
}
// lsfMPTLocked is exempt because tfMPTUnlock clears it legitimately.
// Pre-fixCleanup3_5_0: clearing another issuance flag is allowed.
// Post-fixCleanup3_5_0: clearing another issuance flag trips the
// invariant.
{
std::uint32_t allFlags = 0;
for (auto const flag : std::views::values(getMPTokenIssuanceFlags()))
allFlags |= flag;
allFlags &= ~lsfMPTLocked;
MPTID id{};
Preclose const setup = [&](Account const&, Account const&, Env& env) {
Account const issuer{"issuer"};
env.fund(XRP(10'000), issuer);
env.close();
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create({.flags = allFlags});
id = mptt.issuanceID();
env.close();
return true;
};
STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
for (auto const flag : std::views::values(getMPTokenIssuanceFlags()))
{
if (flag == lsfMPTLocked)
continue;
Precheck const clearFlag = [&, flag](
Account const&, Account const&, ApplyContext& ac) {
auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
if (!sleIssuance)
return false;
sleIssuance->setFieldU32(sfFlags, sleIssuance->getFlags() & ~flag);
ac.view().update(sleIssuance);
return true;
};
doInvariantCheck(
makeEnv(all_ - fixCleanup3_5_0),
{},
clearFlag,
XRPAmount{},
tx,
{tesSUCCESS, tesSUCCESS},
setup);
doInvariantCheck(
makeEnv(all_),
{{"immutable MPTokenIssuance flag cleared"}},
clearFlag,
XRPAmount{},
tx,
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
setup);
}
}
// A vault pseudo-account's MPToken cannot be deleted by anything
// other than a VaultDelete transaction. Set up a vault, then have
// an arbitrary tx erase the pseudo's MPToken in precheck.
@@ -1166,6 +1223,12 @@ class InvariantsMPT_test : public InvariantsBase
0u})
{
MPTID id{};
// Issuance flags cannot be cleared after creation (and
// ValidMPTIssuance now rejects it), so the CanTransfer /
// CanTrade rows create the issuance without the bit
// instead of stripping it in precheck.
std::uint32_t const createFlags =
(flag == 0u || flag == lsfMPTLocked) ? kMptDexFlags : (kMptDexFlags & flag);
auto const isSuccess = !gates.any() || flag == 0 ||
(tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade)) ||
(tx == ttAMM_WITHDRAW &&
@@ -1187,14 +1250,9 @@ class InvariantsMPT_test : public InvariantsBase
auto issuanceSle = ac.view().peek(keylet::mptokenIssuance(id));
if (!issuanceSle)
return false;
auto const flags = issuanceSle->at(sfFlags);
if (flag == lsfMPTLocked)
{
issuanceSle->at(sfFlags) = flags | lsfMPTLocked;
}
else if (flag != 0u)
{
issuanceSle->at(sfFlags) = flags & flag;
issuanceSle->at(sfFlags) = issuanceSle->at(sfFlags) | lsfMPTLocked;
}
issuanceSle->at(sfOutstandingAmount) = 200;
ac.view().update(issuanceSle);
@@ -1215,7 +1273,11 @@ class InvariantsMPT_test : public InvariantsBase
Account const gw("gw");
env.fund(XRP(1'000), gw);
MPTTester const usd(
{.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 100});
{.env = env,
.issuer = gw,
.holders = {a1, a2},
.pay = 100,
.flags = createFlags});
id = usd.issuanceID();
// Either gate enforces, so both must be off to stay
// advisory. Disable after setting up the MPT; the
+857
View File
@@ -0,0 +1,857 @@
#include <test/app/lending/LoanTestBase.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/batch.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/offer.h>
#include <test/jtx/owners.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/Units.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
namespace xrpl::test {
// Vault and Lending transactions used as Batch inners under LendingProtocolV1_2.
class LendingBatch_test : public LoanTestBase
{
private:
// Asserts a loan has been paid off in full: no principal, value, or
// remaining payments left on the loan; no debt left on the broker; and the
// vault's available assets back to `depositValue`.
void
assertLoanFullyRepaid(
jtx::Env const& env,
Keylet const& brokerKeylet,
Keylet const& vaultKeylet,
Keylet const& loanKeylet,
STAmount const& depositValue)
{
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
{
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
BEAST_EXPECT(loanSle->at(sfTotalValueOutstanding) == 0);
BEAST_EXPECT(loanSle->at(sfPaymentRemaining) == 0);
}
if (auto const brokerSle = env.le(brokerKeylet); BEAST_EXPECT(brokerSle))
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == depositValue);
}
// LoanSet as a batch inner: the counterparty signs the outer batch, so
// this inner carries no signature of its own. Pass paymentTotal >= 2
// wherever the same batch also submits an early tfLoanFullPayment:
// computeFullPayment refuses it with tecKILLED when only the final
// scheduled payment is left.
static json::Value
loanSetInner(
jtx::Env& env,
jtx::Account const& account,
uint256 const& brokerID,
jtx::Account const& counterparty,
STAmount const& principal,
std::uint32_t paymentTotal)
{
using namespace jtx;
using namespace jtx::loan;
return env.json(
set(account, brokerID, principal.value()),
kCounterparty(counterparty.id()),
kPaymentTotal(paymentTotal),
Sig(kNone),
Fee(kNone),
Seq(kNone));
}
// Runs VaultCreate -> Deposit -> Withdraw -> Delete in one tfAllOrNothing
// batch and checks the vault is gone, the owner paid only the batch fee,
// and both owner count and sequence are back where they belong.
void
runVaultLifecycleBatch(jtx::Env& env, jtx::Account const& owner, json::Value const& createTx)
{
using namespace jtx;
Vault const vault{env};
auto const seq = env.seq(owner);
auto const balanceBefore = env.balance(owner);
auto const ownerCountBefore = env.ownerCount(owner);
// The outer Batch consumes owner's current sequence, so the first
// inner (VaultCreate) lands on seq + 1.
auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(seq + 1));
auto const depositAmount = XRP(1'000);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
env(batch::outer(owner, seq, batchFee, tfAllOrNothing),
batch::Inner(createTx, seq + 1),
batch::Inner(
vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = depositAmount}),
seq + 2),
batch::Inner(
vault.withdraw(
{.depositor = owner, .id = vaultKeylet.key, .amount = depositAmount}),
seq + 3),
batch::Inner(vault.del({.owner = owner, .id = vaultKeylet.key}), seq + 4),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(vaultKeylet));
BEAST_EXPECT(env.balance(owner) == balanceBefore - batchFee);
BEAST_EXPECT(env.ownerCount(owner) == ownerCountBefore);
BEAST_EXPECT(env.seq(owner) == seq + 5);
}
void
testVaultLifecycle(FeatureBitset features)
{
// A single tfAllOrNothing batch runs an open-ended XRP vault through
// its whole lifecycle: create, deposit, withdraw everything back out,
// and delete.
testcase("vault lifecycle in a batch");
using namespace jtx;
Env env(*this, features);
Account const owner{"owner"};
env.fund(XRP(100'000), owner);
env.close();
auto const createTx = std::get<0>(Vault{env}.create({.owner = owner, .asset = xrpIssue()}));
runVaultLifecycleBatch(env, owner, createTx);
}
void
testClosedEndedVaultLifecycle(FeatureBitset features)
{
// Same lifecycle as testVaultLifecycle, but on a closed-ended vault
// submitted while it is still in the Subscription phase, where
// LendingProtocolV1_1 allows both deposit and withdraw.
testcase("vault lifecycle in a batch, closed-ended vault, subscription phase");
using namespace jtx;
Env env(*this, features);
Account const owner{"owner"};
env.fund(XRP(100'000), owner);
env.close();
auto const createTx = std::get<0>(Vault{env}.createClosedEnded(
{.owner = owner, .asset = xrpIssue(), .subscriptionOffset = std::chrono::seconds{60}}));
runVaultLifecycleBatch(env, owner, createTx);
}
void
testLoanLifecycleOpenEndedVault(FeatureBitset features)
{
// With LendingProtocolV1_1 off, an open-ended vault can host a broker,
// so vault creation, deposit, broker setup, loan origination, and an
// early full payoff all fit in one atomic batch. The borrower signs the
// outer as counterparty to the LoanSet inner and as the account behind
// the LoanPay inner.
testcase("loan lifecycle in a batch, open-ended vault");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault const vault{env};
auto const lenderSeq = env.seq(lender);
auto const borrowerSeq = env.seq(borrower);
auto const lenderBalanceBefore = env.balance(lender);
auto const borrowerBalanceBefore = env.balance(borrower);
auto const depositAmount = asset(50'000);
auto const principal = asset(1'000);
auto const vaultKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(lenderSeq + 1));
auto const brokerKeylet =
keylet::loanBroker(lender.id(), SeqProxy::rawSequence(lenderSeq + 3));
auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
auto const createTx = std::get<0>(vault.create({.owner = lender, .asset = xrpIssue()}));
// CoverRateMinimum/CoverRateLiquidation are left unset on the
// LoanBrokerSet inner below, which defaults both to zero, so this
// loan needs no first-loss cover deposit from the broker.
auto const loanSetTx = loanSetInner(env, lender, brokerKeylet.key, borrower, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batchFee = batch::calcBatchFee(env, 1, 5);
env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(createTx, lenderSeq + 1),
batch::Inner(
vault.deposit(
{.depositor = lender, .id = vaultKeylet.key, .amount = depositAmount.value()}),
lenderSeq + 2),
batch::Inner(loan_broker::set(lender, vaultKeylet.key), lenderSeq + 3),
batch::Inner(loanSetTx, lenderSeq + 4),
batch::Inner(loanPayTx, borrowerSeq),
batch::Sig(borrower),
Ter(tesSUCCESS));
env.close();
assertLoanFullyRepaid(env, brokerKeylet, vaultKeylet, loanKeylet, depositAmount.value());
// The borrower borrowed the principal and repaid it in full at zero
// interest and fees, so their balance nets to zero; the batch fee is
// paid entirely by the lender, who also funded the vault deposit.
BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore);
BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batchFee - depositAmount.value());
}
void
testLoanLifecycleClosedEndedVault(FeatureBitset features)
{
// Under LendingProtocolV1_1 a broker can attach only to a closed-ended
// vault, and LoanSet is refused outside the Investment phase, so the
// vault/broker setup and the loan/payoff run as two batches split at
// the Subscription -> Investment boundary.
testcase("loan lifecycle in a batch, closed-ended vault");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault const vault{env};
auto const lenderSeq1 = env.seq(lender);
auto const depositAmount = asset(50'000);
auto const vaultKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(lenderSeq1 + 1));
auto const brokerKeylet =
keylet::loanBroker(lender.id(), SeqProxy::rawSequence(lenderSeq1 + 3));
auto const created = vault.createClosedEnded(
{.owner = lender, .asset = xrpIssue(), .subscriptionOffset = std::chrono::seconds{60}});
auto const createTx = std::get<0>(created);
auto const subscriptionDate = std::get<2>(created);
// LoanBrokerSet checks only the vault's kind, not its phase, so setting
// up the broker is allowed here in the same batch, while the vault is
// still in the Subscription phase.
auto const batch1Fee = batch::calcBatchFee(env, 0, 3);
env(batch::outer(lender, lenderSeq1, batch1Fee, tfAllOrNothing),
batch::Inner(createTx, lenderSeq1 + 1),
batch::Inner(
vault.deposit(
{.depositor = lender, .id = vaultKeylet.key, .amount = depositAmount.value()}),
lenderSeq1 + 2),
batch::Inner(loan_broker::set(lender, vaultKeylet.key), lenderSeq1 + 3),
Ter(tesSUCCESS));
env.close();
if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == depositAmount.value());
BEAST_EXPECT(env.le(brokerKeylet));
vault.closePastSubscription(subscriptionDate);
auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
auto const principal = asset(1'000);
auto const lenderSeq2 = env.seq(lender);
auto const borrowerSeq = env.seq(borrower);
auto const lenderBalanceBefore = env.balance(lender);
auto const borrowerBalanceBefore = env.balance(borrower);
auto const loanSetTx = loanSetInner(env, lender, brokerKeylet.key, borrower, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batch2Fee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(lender, lenderSeq2, batch2Fee, tfAllOrNothing),
batch::Inner(loanSetTx, lenderSeq2 + 1),
batch::Inner(loanPayTx, borrowerSeq),
batch::Sig(borrower),
Ter(tesSUCCESS));
env.close();
assertLoanFullyRepaid(env, brokerKeylet, vaultKeylet, loanKeylet, depositAmount.value());
BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore);
// The deposit already left the lender's balance in batch 1; batch 2
// only costs the batch fee, since LoanSet/LoanPay net to zero XRP.
BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batch2Fee);
}
// Handles shared by testArbitrage and testArbitrageRollback.
struct ArbitrageSetup
{
jtx::PrettyAsset iou;
jtx::Account issuer;
jtx::Account lender;
jtx::Account borrower;
jtx::Account mm1;
jtx::Account mm2;
BrokerInfo broker;
};
// Broker with zero cover rates and no debt cap, so LoanSet needs no cover
// deposit. The borrower holds a trust line but no IOU, so the profit is
// exact. mm1 rests an offer selling 400 XRP for 1000 IOU; mm2 is funded
// but places its offer only inside testArbitrage's batch.
ArbitrageSetup
setupArbitrage(jtx::Env& env)
{
using namespace jtx;
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
Account const mm1{"mm1"};
Account const mm2{"mm2"};
env.fund(XRP(1'000'000), issuer, lender, borrower, mm1, mm2);
env(fset(issuer, asfDefaultRipple));
env.close();
PrettyAsset const iou = issuer[iouCurrency_];
STAmount const iouLimit = iou(1'000'000'000);
env(trust(lender, iouLimit));
env(trust(borrower, iouLimit));
env(trust(mm1, iouLimit));
env(trust(mm2, iouLimit));
env.close();
BrokerParameters params = BrokerParameters::defaults();
params.vaultDeposit = 100'000;
params.coverRateMin = TenthBips32{0};
params.coverDeposit = 0;
params.debtMax = 0;
params.coverRateLiquidation = TenthBips32{0};
env(pay(issuer, lender, iou(params.vaultDeposit)));
env(pay(issuer, mm2, iou(20'000)));
env.close();
BrokerInfo const broker{createVaultAndBroker(env, iou, lender, params)};
env(offer(mm1, iou(1'000), XRP(400)));
env.close();
return {
.iou = iou,
.issuer = issuer,
.lender = lender,
.borrower = borrower,
.mm1 = mm1,
.mm2 = mm2,
.broker = broker};
}
void
testArbitrage(FeatureBitset features)
{
// A borrower profits risk-free from a loan and two crossing DEX
// offers in one atomic batch. Crossed offers execute the moment both
// rest on the book, so mm2's offer is itself a batch inner, placed
// right after the borrower's buy leg consumes mm1.
testcase("arbitrage across a loan and two crossing offers in a batch");
using namespace jtx;
Env env(*this, features);
auto const setup = setupArbitrage(env);
auto const& iou = setup.iou;
auto const& lender = setup.lender;
auto const& borrower = setup.borrower;
auto const& broker = setup.broker;
auto const& mm2 = setup.mm2;
auto const loanKeylet = nextLoanKeylet(env, broker);
auto const principal = iou(1'000);
BEAST_EXPECT(env.balance(borrower, iou) == iou(0));
auto const borrowerSeq = env.seq(borrower);
auto const mm2Seq = env.seq(mm2);
auto const loanSetTx = loanSetInner(env, borrower, broker.brokerID, lender, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batchFee = batch::calcBatchFee(env, 2, 5);
env(batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::Inner(loanSetTx, borrowerSeq + 1),
batch::Inner(
offer(borrower, XRP(400), principal.value(), tfFillOrKill), borrowerSeq + 2),
batch::Inner(offer(mm2, XRP(400), iou(1'008)), mm2Seq),
batch::Inner(
offer(borrower, iou(1'008).value(), XRP(400), tfFillOrKill), borrowerSeq + 3),
batch::Inner(loanPayTx, borrowerSeq + 4),
batch::Sig(lender, mm2),
Ter(tesSUCCESS));
env.close();
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
BEAST_EXPECT(env.balance(borrower, iou) == iou(8));
env.require(Offers(setup.mm1, 0));
env.require(Offers(mm2, 0));
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
{
BEAST_EXPECT(
vaultSle->at(sfAssetsAvailable) == iou(broker.params.vaultDeposit).value());
}
}
void
testArbitrageRollback(FeatureBitset features)
{
// Without a second market maker willing to buy XRP back at the
// higher price, the sell leg's tfFillOrKill offer cannot fill and
// returns tecKILLED. tfAllOrNothing then rolls back every inner,
// including the loan origination; the outer batch itself still
// returns tesSUCCESS.
testcase("arbitrage batch rolls back when the sell offer cannot fill");
using namespace jtx;
Env env(*this, features);
auto const setup = setupArbitrage(env);
auto const& iou = setup.iou;
auto const& lender = setup.lender;
auto const& borrower = setup.borrower;
auto const& broker = setup.broker;
auto const brokerSleBefore = env.le(broker.brokerKeylet());
auto const vaultSleBefore = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
return;
auto const loanSequenceBefore = brokerSleBefore->at(sfLoanSequence);
auto const assetsAvailableBefore = vaultSleBefore->at(sfAssetsAvailable);
auto const loanKeylet = nextLoanKeylet(env, broker);
auto const principal = iou(1'000);
auto const borrowerSeq = env.seq(borrower);
auto const borrowerBalanceBefore = env.balance(borrower);
auto const loanSetTx = loanSetInner(env, borrower, broker.brokerID, lender, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batchFee = batch::calcBatchFee(env, 1, 4);
env(batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::Inner(loanSetTx, borrowerSeq + 1),
batch::Inner(
offer(borrower, XRP(400), principal.value(), tfFillOrKill), borrowerSeq + 2),
batch::Inner(
offer(borrower, iou(1'008).value(), XRP(400), tfFillOrKill), borrowerSeq + 3),
batch::Inner(loanPayTx, borrowerSeq + 4),
batch::Sig(lender),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(loanKeylet));
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(brokerSle->at(sfLoanSequence) == loanSequenceBefore);
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
}
BEAST_EXPECT(env.balance(borrower, iou) == iou(0));
BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore - batchFee);
env.require(Offers(setup.mm1, 1));
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == assetsAvailableBefore);
}
// Originates a loan outside any batch: one payment due in a day, a day of
// grace, zero interest. PaymentTotal 1 is fine because callers only
// impair or default the loan and never pay it off early.
static void
originateLoan(
jtx::Env& env,
BrokerInfo const& broker,
jtx::Account const& lender,
jtx::Account const& borrower,
STAmount const& principal)
{
using namespace jtx;
using namespace jtx::loan;
env(set(lender, broker.brokerID, principal),
kCounterparty(borrower.id()),
kPaymentTotal(1),
kPaymentInterval(86400),
kGracePeriod(86400),
Sig(sfCounterpartySignature, borrower),
Fee(env.current()->fees().base * 2));
env.close();
}
void
testImpairAndCoverDeposit(FeatureBitset features)
{
// Impairing a loan records its exposure as the vault's unrealized
// loss; the broker tops up first-loss cover in the same batch. Under
// fixCleanup3_4_0 LoanManage refuses tfLoanImpair with tecTOO_SOON
// until a payment is late, so the impairment cannot share a batch
// with the LoanSet that creates the loan.
testcase("loan impairment and cover deposit in a batch");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(100'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
auto const broker = createVaultAndBroker(env, asset, lender);
auto const loanKeylet = nextLoanKeylet(env, broker);
originateLoan(env, broker, lender, borrower, asset(1'000).value());
auto const loanSleBefore = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSleBefore))
return;
// Advance past NextPaymentDueDate so the payment is late.
using D = NetClock::duration;
using Tp = NetClock::time_point;
env.close(Tp{D{loanSleBefore->at(sfNextPaymentDueDate) + 1}});
auto const brokerSleBefore = env.le(broker.brokerKeylet());
auto const vaultSleBefore = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const lossUnrealizedBefore = vaultSleBefore->at(sfLossUnrealized);
auto const additionalCover = asset(500).value();
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(loan::manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 1),
batch::Inner(
loan_broker::coverDeposit(lender, broker.brokerID, additionalCover), lenderSeq + 2),
Ter(tesSUCCESS));
env.close();
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
BEAST_EXPECT(loanSle->isFlag(lsfLoanImpaired));
// Zero interest and fees make the exposure equal the outstanding
// principal under both accounting modes.
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
{
BEAST_EXPECT(
vaultSle->at(sfLossUnrealized) ==
lossUnrealizedBefore + loanSleBefore->at(sfPrincipalOutstanding));
}
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == coverAvailableBefore + additionalCover);
}
void
testDefaultAndCoverWithdraw(FeatureBitset features)
{
// A defaulted loan burns first-loss cover per CoverRateLiquidation;
// the remainder can be withdrawn once DebtTotal drops to zero. The
// default runs in its own batch: LoanManage refuses tfLoanDefault
// with tecTOO_SOON until the grace period elapses, so it cannot share
// a batch with the LoanSet that creates the loan.
testcase("loan default and cover withdraw in a batch");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(100'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
auto const broker = createVaultAndBroker(env, asset, lender);
auto const loanKeylet = nextLoanKeylet(env, broker);
originateLoan(env, broker, lender, borrower, asset(1'000).value());
auto const loanSleBefore = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSleBefore))
return;
// Advance past NextPaymentDueDate + GracePeriod so the default is no
// longer refused with tecTOO_SOON.
auto const dueDate = loanSleBefore->at(sfNextPaymentDueDate);
auto const gracePeriod = loanSleBefore->at(sfGracePeriod);
using D = NetClock::duration;
using Tp = NetClock::time_point;
env.close(Tp{D{dueDate + gracePeriod + 1}});
auto const brokerSleBefore = env.le(broker.brokerKeylet());
auto const vaultSleBefore = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const assetsAvailableBefore = vaultSleBefore->at(sfAssetsAvailable);
auto const assetsTotalBefore = vaultSleBefore->at(sfAssetsTotal);
TenthBips32 const coverRateMin{brokerSleBefore->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{brokerSleBefore->at(sfCoverRateLiquidation)};
Number const totalDefaultAmount{loanSleBefore->at(sfPrincipalOutstanding)};
// DefaultCovered = min(MinimumCover x CoverRateLiquidation,
// DefaultAmount, CoverAvailable), matching LoanManage::defaultLoan.
// Zero interest and fees make DefaultAmount equal the outstanding
// principal under both accounting modes, so no
// featureLendingProtocolV1_1 branch is needed here.
Number defaultCovered;
{
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
auto const minimumCover =
tenthBipsOfValue(Number{brokerSleBefore->at(sfDebtTotal)}, coverRateMin);
defaultCovered = std::min(
roundToAsset(
asset.raw(),
std::min(
tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
loanSleBefore->at(sfLoanScale)),
Number{coverAvailableBefore});
}
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
auto const lenderSeq = env.seq(lender);
auto const lenderBalanceBefore = env.balance(lender);
auto const withdrawAmount = STAmount{asset.raw(), coverAvailableBefore - defaultCovered};
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(loan::manage(lender, loanKeylet.key, tfLoanDefault), lenderSeq + 1),
batch::Inner(
loan_broker::coverWithdraw(lender, broker.brokerID, withdrawAmount), lenderSeq + 2),
Ter(tesSUCCESS));
env.close();
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
{
BEAST_EXPECT(loanSle->isFlag(lsfLoanDefault));
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
}
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == 0);
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
}
BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batchFee + withdrawAmount);
// The default moves defaultCovered from the broker's cover to the
// vault; the cover withdraw pays out of the broker's own
// pseudo-account and does not touch the vault again.
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
{
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == assetsAvailableBefore + defaultCovered);
BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == assetsTotalBefore - vaultDefaultAmount);
}
}
void
testIndependentVaultChain(FeatureBitset features)
{
// tfIndependent runs every inner regardless of earlier failures.
// VaultWithdraw fails before anything is deposited: the empty vault
// converts the amount to zero shares (tecPRECISION_LOSS). VaultDelete fails
// once the vault holds a deposit (tecHAS_OBLIGATIONS); both are
// tec-class results, so neither stops the inners that follow.
testcase("independent batch runs every inner despite failing inners");
using namespace jtx;
Env env(*this, features);
Account const owner{"owner"};
env.fund(XRP(100'000), owner);
env.close();
Vault const vault{env};
auto const seq = env.seq(owner);
auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(seq + 1));
auto const createTx = std::get<0>(vault.create({.owner = owner, .asset = xrpIssue()}));
auto const amount = XRP(1'000);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const batchTx = env.jt(
batch::outer(owner, seq, batchFee, tfIndependent),
batch::Inner(createTx, seq + 1),
batch::Inner(
vault.withdraw({.depositor = owner, .id = vaultKeylet.key, .amount = amount}),
seq + 2),
batch::Inner(
vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = amount}),
seq + 3),
batch::Inner(vault.del({.owner = owner, .id = vaultKeylet.key}), seq + 4));
env(batchTx, Ter(tesSUCCESS));
env.close();
auto const batchID = strHex(batchTx.stx->getTransactionID());
auto const& innerIDs = batchTx.stx->getBatchTransactionIDs();
if (!BEAST_EXPECT(innerIDs.size() == 4))
return;
batch::validateInnerTxn(env, batchID, strHex(innerIDs[0]), "VaultCreate", "tesSUCCESS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[1]), "VaultWithdraw", "tecPRECISION_LOSS");
batch::validateInnerTxn(env, batchID, strHex(innerIDs[2]), "VaultDeposit", "tesSUCCESS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[3]), "VaultDelete", "tecHAS_OBLIGATIONS");
if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == amount.value());
BEAST_EXPECT(env.seq(owner) == seq + 5);
}
void
testIndependentLoanChain(FeatureBitset features)
{
// Same claim as testIndependentVaultChain, on a loan chain. The
// LoanSet inner leaves the borrower holding only the 1,000 principal,
// so a LoanPay for 2,000 fails preclaim's balance check
// (tecINSUFFICIENT_FUNDS); the cover deposit submitted after it
// still lands.
testcase("independent batch runs every loan inner despite a failing payment");
using namespace jtx;
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), issuer, lender, borrower);
env(fset(issuer, asfDefaultRipple));
env.close();
PrettyAsset const iou = issuer[iouCurrency_];
STAmount const iouLimit = iou(1'000'000'000);
env(trust(lender, iouLimit));
env(trust(borrower, iouLimit));
env.close();
env(pay(issuer, lender, iou(2'000'000)));
env.close();
auto const broker = createVaultAndBroker(env, iou, lender);
auto const loanKeylet = nextLoanKeylet(env, broker);
auto const principal = iou(1'000);
auto const brokerSleBefore = env.le(broker.brokerKeylet());
if (!BEAST_EXPECT(brokerSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const lenderSeq = env.seq(lender);
auto const borrowerSeq = env.seq(borrower);
auto const loanSetTx = loanSetInner(env, lender, broker.brokerID, borrower, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, iou(2'000).value(), tfLoanFullPayment);
auto const additionalCover = iou(500);
auto const batchFee = batch::calcBatchFee(env, 1, 3);
auto const batchTx = env.jt(
batch::outer(lender, lenderSeq, batchFee, tfIndependent),
batch::Inner(loanSetTx, lenderSeq + 1),
batch::Inner(loanPayTx, borrowerSeq),
batch::Inner(
loan_broker::coverDeposit(lender, broker.brokerID, additionalCover), lenderSeq + 2),
batch::Sig(borrower));
env(batchTx, Ter(tesSUCCESS));
env.close();
auto const batchID = strHex(batchTx.stx->getTransactionID());
auto const& innerIDs = batchTx.stx->getBatchTransactionIDs();
if (!BEAST_EXPECT(innerIDs.size() == 3))
return;
batch::validateInnerTxn(env, batchID, strHex(innerIDs[0]), "LoanSet", "tesSUCCESS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[1]), "LoanPay", "tecINSUFFICIENT_FUNDS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[2]), "LoanBrokerCoverDeposit", "tesSUCCESS");
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == principal.value());
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(
brokerSle->at(sfCoverAvailable) == coverAvailableBefore + additionalCover.value());
}
}
public:
void
run() override
{
// Batch rejects Vault and Lending inners before LendingProtocolV1_2.
FeatureBitset const lendingBatch{all_ | featureLendingProtocolV1_2};
for (auto const& features : {lendingBatch, lendingBatch | featureLendingProtocolV1_1})
{
testVaultLifecycle(features);
testArbitrage(features);
testArbitrageRollback(features);
testImpairAndCoverDeposit(features);
testDefaultAndCoverWithdraw(features);
testIndependentVaultChain(features);
testIndependentLoanChain(features);
}
testClosedEndedVaultLifecycle(lendingBatch | featureLendingProtocolV1_1);
testLoanLifecycleOpenEndedVault(lendingBatch);
testLoanLifecycleClosedEndedVault(lendingBatch | featureLendingProtocolV1_1);
}
};
BEAST_DEFINE_TESTSUITE(LendingBatch, tx, xrpl);
} // namespace xrpl::test
+1 -5
View File
@@ -30,11 +30,8 @@
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/transactors/system/Batch.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
@@ -498,8 +495,7 @@ private:
// From FIND-001
testcase << "Batch Bypass Counterparty";
bool const lendingBatchEnabled = !std::ranges::any_of(
Batch::kDisabledTxTypes, [](auto const& disabled) { return disabled == ttLOAN_SET; });
bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
using namespace jtx;
using namespace std::chrono_literals;
-90
View File
@@ -1,90 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <string>
namespace beast {
class Journal_test : public unit_test::Suite
{
public:
class TestSink : public Journal::Sink
{
private:
int count_{0};
public:
TestSink() : Sink(Severity::Warning, false)
{
}
[[nodiscard]] int
count() const
{
return count_;
}
void
reset()
{
count_ = 0;
}
void
write(Severity level, std::string const&) override
{
if (level >= threshold())
++count_;
}
void
writeAlways(Severity level, std::string const&) override
{
++count_;
}
};
void
run() override
{
TestSink sink;
sink.threshold(Severity::Info);
Journal const j(sink);
j.trace() << " ";
BEAST_EXPECT(sink.count() == 0);
j.debug() << " ";
BEAST_EXPECT(sink.count() == 0);
j.info() << " ";
BEAST_EXPECT(sink.count() == 1);
j.warn() << " ";
BEAST_EXPECT(sink.count() == 2);
j.error() << " ";
BEAST_EXPECT(sink.count() == 3);
j.fatal() << " ";
BEAST_EXPECT(sink.count() == 4);
sink.reset();
sink.threshold(Severity::Debug);
j.trace() << " ";
BEAST_EXPECT(sink.count() == 0);
j.debug() << " ";
BEAST_EXPECT(sink.count() == 1);
j.info() << " ";
BEAST_EXPECT(sink.count() == 2);
j.warn() << " ";
BEAST_EXPECT(sink.count() == 3);
j.error() << " ";
BEAST_EXPECT(sink.count() == 4);
j.fatal() << " ";
BEAST_EXPECT(sink.count() == 5);
}
};
BEAST_DEFINE_TESTSUITE(Journal, beast, beast);
} // namespace beast
@@ -1,19 +0,0 @@
#include <xrpl/beast/clock/basic_seconds_clock.h>
#include <xrpl/beast/unit_test/suite.h>
namespace beast {
class BasicSecondsClock_test : public unit_test::Suite
{
public:
void
run() override
{
BasicSecondsClock::now();
pass();
}
};
BEAST_DEFINE_TESTSUITE(BasicSecondsClock, beast, beast);
} // namespace beast
-196
View File
@@ -1,196 +0,0 @@
#include <xrpl/beast/hash/xxhasher.h>
#include <xrpl/beast/unit_test/suite.h>
#include <cstdint>
namespace beast {
class XXHasher_test : public unit_test::Suite
{
public:
void
testWithoutSeed()
{
testcase("Without seed");
Xxhasher hasher{};
std::string objectToHash{"Hello, xxHash!"};
hasher(objectToHash.data(), objectToHash.size());
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 16042857369214894119ULL);
}
void
testWithSeed()
{
testcase("With seed");
Xxhasher hasher{static_cast<std::uint32_t>(102)};
std::string objectToHash{"Hello, xxHash!"};
hasher(objectToHash.data(), objectToHash.size());
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 14440132435660934800ULL);
}
void
testWithTwoSeeds()
{
testcase("With two seeds");
Xxhasher hasher{static_cast<std::uint32_t>(102), static_cast<std::uint32_t>(103)};
std::string objectToHash{"Hello, xxHash!"};
hasher(objectToHash.data(), objectToHash.size());
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 14440132435660934800ULL);
}
void
testBigObjectWithMultipleSmallUpdatesWithoutSeed()
{
testcase("Big object with multiple small updates without seed");
Xxhasher hasher{};
std::string objectToHash{"Hello, xxHash!"};
for (int i = 0; i < 100; i++)
{
hasher(objectToHash.data(), objectToHash.size());
}
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 15296278154063476002ULL);
}
void
testBigObjectWithMultipleSmallUpdatesWithSeed()
{
testcase("Big object with multiple small updates with seed");
Xxhasher hasher{static_cast<std::uint32_t>(103)};
std::string objectToHash{"Hello, xxHash!"};
for (int i = 0; i < 100; i++)
{
hasher(objectToHash.data(), objectToHash.size());
}
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 17285302196561698791ULL);
}
void
testBigObjectWithSmallAndBigUpdatesWithoutSeed()
{
testcase("Big object with small and big updates without seed");
Xxhasher hasher{};
std::string objectToHash{"Hello, xxHash!"};
std::string bigObject;
for (int i = 0; i < 20; i++)
{
bigObject += "Hello, xxHash!";
}
hasher(objectToHash.data(), objectToHash.size());
hasher(bigObject.data(), bigObject.size());
hasher(objectToHash.data(), objectToHash.size());
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 1865045178324729219ULL);
}
void
testBigObjectWithSmallAndBigUpdatesWithSeed()
{
testcase("Big object with small and big updates with seed");
Xxhasher hasher{static_cast<std::uint32_t>(103)};
std::string objectToHash{"Hello, xxHash!"};
std::string bigObject;
for (int i = 0; i < 20; i++)
{
bigObject += "Hello, xxHash!";
}
hasher(objectToHash.data(), objectToHash.size());
hasher(bigObject.data(), bigObject.size());
hasher(objectToHash.data(), objectToHash.size());
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 16189862915636005281ULL);
}
void
testBigObjectWithOneUpdateWithoutSeed()
{
testcase("Big object with one update without seed");
Xxhasher hasher{};
std::string objectToHash;
for (int i = 0; i < 100; i++)
{
objectToHash += "Hello, xxHash!";
}
hasher(objectToHash.data(), objectToHash.size());
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 15296278154063476002ULL);
}
void
testBigObjectWithOneUpdateWithSeed()
{
testcase("Big object with one update with seed");
Xxhasher hasher{static_cast<std::uint32_t>(103)};
std::string objectToHash;
for (int i = 0; i < 100; i++)
{
objectToHash += "Hello, xxHash!";
}
hasher(objectToHash.data(), objectToHash.size());
BEAST_EXPECT(static_cast<Xxhasher::result_type>(hasher) == 17285302196561698791ULL);
}
void
testOperatorResultTypeDoesNotChangeInternalState()
{
testcase("Operator result type doesn't change the internal state");
{
Xxhasher hasher;
std::string object{"Hello xxhash"};
hasher(object.data(), object.size());
auto xxhashResult1 = static_cast<Xxhasher::result_type>(hasher);
auto xxhashResult2 = static_cast<Xxhasher::result_type>(hasher);
BEAST_EXPECT(xxhashResult1 == xxhashResult2);
}
{
Xxhasher hasher;
std::string object;
for (int i = 0; i < 100; i++)
{
object += "Hello, xxHash!";
}
hasher(object.data(), object.size());
auto xxhashResult1 = hasher.operator Xxhasher::result_type();
auto xxhashResult2 = hasher.operator Xxhasher::result_type();
BEAST_EXPECT(xxhashResult1 == xxhashResult2);
}
}
void
run() override
{
testWithoutSeed();
testWithSeed();
testWithTwoSeeds();
testBigObjectWithMultipleSmallUpdatesWithoutSeed();
testBigObjectWithMultipleSmallUpdatesWithSeed();
testBigObjectWithSmallAndBigUpdatesWithoutSeed();
testBigObjectWithSmallAndBigUpdatesWithSeed();
testBigObjectWithOneUpdateWithoutSeed();
testBigObjectWithOneUpdateWithSeed();
testOperatorResultTypeDoesNotChangeInternalState();
}
};
BEAST_DEFINE_TESTSUITE(XXHasher, beast_core, beast);
} // namespace beast
+15 -61
View File
@@ -19,8 +19,8 @@
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -367,6 +367,20 @@ checkArraySize(json::Value const& val, unsigned int size);
std::uint32_t
ownerCount(test::jtx::Env const& env, test::jtx::Account const& account);
/* Token (IOU/MPT) Locking */
/******************************************************************************/
uint64_t
mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt);
uint64_t
issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt);
jtx::PrettyAmount
issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue);
jtx::PrettyAmount
issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue);
// Helper function that returns the sponsored owner count on an account.
std::uint32_t
sponsoredOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account);
@@ -583,66 +597,6 @@ accountBalance(Env& env, Account const& acct);
[[nodiscard]] bool
expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedValue);
/* Payment Channel */
/******************************************************************************/
namespace paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt);
inline json::Value
create(
Account const& account,
Account const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt)
{
return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag);
}
json::Value
fund(
AccountID const& account,
UInt256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration = std::nullopt);
json::Value
claim(
AccountID const& account,
UInt256 const& channel,
std::optional<STAmount> const& balance = std::nullopt,
std::optional<STAmount> const& amount = std::nullopt,
std::optional<Slice> const& signature = std::nullopt,
std::optional<PublicKey> const& pk = std::nullopt);
UInt256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue);
inline UInt256
channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue)
{
return channel(account.id(), dst.id(), seqProxyValue);
}
STAmount
channelBalance(ReadView const& view, UInt256 const& chan);
bool
channelExists(ReadView const& view, UInt256 const& chan);
} // namespace paychan
/* Crossing Limits */
/******************************************************************************/
+25
View File
@@ -15,6 +15,8 @@
#include <concepts>
#include <cstdint>
#include <optional>
#include <source_location>
#include <string>
#include <utility>
#include <vector>
@@ -59,6 +61,29 @@ calcConfidentialBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32
json::Value
outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags);
/**
* @brief Expect an inner Batch transaction to be recorded with the given
* type and result, and with ParentBatchID pointing at its outer Batch.
*
* Looks the inner transaction up with the `tx` RPC, so the ledger holding the
* batch must already be closed. Failures are reported at the caller's line.
*
* @param env The test environment.
* @param batchID Hash of the outer Batch transaction.
* @param txHash Hash of the inner transaction.
* @param txType Expected TransactionType, e.g. "Payment".
* @param result Expected TransactionResult, e.g. "tesSUCCESS".
* @param loc Call site to report failures at.
*/
void
validateInnerTxn(
Env& env,
std::string const& batchID,
std::string const& txHash,
std::string const& txType,
std::string const& result,
std::source_location const& loc = std::source_location::current());
/**
* @brief Adds an inner Batch transaction to a JTx and autofills it.
*/
+48 -101
View File
@@ -16,18 +16,15 @@
#include <xrpld/rpc/Role.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
@@ -37,13 +34,11 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
@@ -53,7 +48,6 @@
#include <xrpl/tx/paths/detail/Steps.h>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
@@ -95,6 +89,54 @@ ownerCount(Env const& env, Account const& account)
return env.ownerCount(account);
}
/* Token (IOU/MPT) Locking */
/******************************************************************************/
uint64_t
mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptoken(mpt.mpt(), account));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
uint64_t
issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptokenIssuance(mpt.mpt()));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
jtx::PrettyAmount
issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const obligations = result[jss::obligations][to_string(issue.currency)];
if (obligations.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, obligations.asString());
return {amount, account.name()};
}
jtx::PrettyAmount
issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const locked = result[jss::locked][to_string(issue.currency)];
if (locked.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, locked.asString());
return {amount, account.name()};
}
std::uint32_t
sponsoredOwnerCount(Env const& env, Account const& account)
{
@@ -500,101 +542,6 @@ expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedVal
return accountBalance(env, acct) == to_string(expectedValue.xrp());
}
/* Payment Channel */
/******************************************************************************/
namespace paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter,
std::optional<std::uint32_t> const& dstTag)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Account] = to_string(account);
jv[jss::Destination] = to_string(to);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::SettleDelay] = settleDelay.count();
jv[sfPublicKey.fieldName] = strHex(pk.slice());
if (cancelAfter)
jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count();
if (dstTag)
jv[sfDestinationTag.fieldName] = *dstTag;
return jv;
}
json::Value
fund(
AccountID const& account,
UInt256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelFund;
jv[jss::Account] = to_string(account);
jv[sfChannel.fieldName] = to_string(channel);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
if (expiration)
jv[sfExpiration.fieldName] = expiration->time_since_epoch().count();
return jv;
}
json::Value
claim(
AccountID const& account,
UInt256 const& channel,
std::optional<STAmount> const& balance,
std::optional<STAmount> const& amount,
std::optional<Slice> const& signature,
std::optional<PublicKey> const& pk)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Account] = to_string(account);
jv["Channel"] = to_string(channel);
if (amount)
jv[jss::Amount] = amount->getJson(JsonOptions::Values::None);
if (balance)
jv["Balance"] = balance->getJson(JsonOptions::Values::None);
if (signature)
jv["Signature"] = strHex(*signature);
if (pk)
jv["PublicKey"] = strHex(pk->slice());
return jv;
}
UInt256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue)
{
auto const seqProxy = SeqProxy::rawSequence(seqProxyValue);
auto const k = keylet::payChannel(account, dst, seqProxy);
return k.key;
}
STAmount
channelBalance(ReadView const& view, UInt256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfBalance];
}
bool
channelExists(ReadView const& view, UInt256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
return bool(slep);
}
} // namespace paychan
/* Crossing Limits */
/******************************************************************************/
+26
View File
@@ -27,6 +27,8 @@
#include <cstdint>
#include <optional>
#include <ostream>
#include <source_location>
#include <string>
#include <utility>
namespace xrpl::test::jtx::batch {
@@ -62,6 +64,30 @@ outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint3
return jv;
}
void
validateInnerTxn(
Env& env,
std::string const& batchID,
std::string const& txHash,
std::string const& txType,
std::string const& result,
std::source_location const& loc)
{
json::Value const jrr = env.rpc("tx", txHash)[jss::result];
env.test.expect(
jrr[sfTransactionType.jsonName] == txType, "TransactionType", loc.file_name(), loc.line());
env.test.expect(
jrr[jss::meta][sfTransactionResult.jsonName] == result,
"TransactionResult",
loc.file_name(),
loc.line());
env.test.expect(
jrr[jss::meta][sfParentBatchID.jsonName] == batchID,
"ParentBatchID",
loc.file_name(),
loc.line());
}
void
Inner::operator()(Env& env, JTx& jt) const
{
+140
View File
@@ -0,0 +1,140 @@
#include <test/jtx/paychan.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <optional>
/**
* Paychan operations.
*/
namespace xrpl::test::jtx::paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter,
std::optional<std::uint32_t> const& dstTag)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Account] = to_string(account);
jv[jss::Destination] = to_string(to);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::SettleDelay] = settleDelay.count();
jv[sfPublicKey.fieldName] = strHex(pk.slice());
if (cancelAfter)
jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count();
if (dstTag)
jv[sfDestinationTag.fieldName] = *dstTag;
return jv;
}
json::Value
fund(
AccountID const& account,
uint256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelFund;
jv[jss::Account] = to_string(account);
jv[sfChannel.fieldName] = to_string(channel);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
if (expiration)
jv[sfExpiration.fieldName] = expiration->time_since_epoch().count();
return jv;
}
json::Value
claim(
AccountID const& account,
uint256 const& channel,
std::optional<STAmount> const& balance,
std::optional<STAmount> const& amount,
std::optional<Slice> const& signature,
std::optional<PublicKey> const& pk)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Account] = to_string(account);
jv["Channel"] = to_string(channel);
if (amount)
jv[jss::Amount] = amount->getJson(JsonOptions::Values::None);
if (balance)
jv["Balance"] = balance->getJson(JsonOptions::Values::None);
if (signature)
jv["Signature"] = strHex(*signature);
if (pk)
jv["PublicKey"] = strHex(pk->slice());
return jv;
}
uint256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue)
{
auto const seqProxy = SeqProxy::rawSequence(seqProxyValue);
auto const k = keylet::payChannel(account, dst, seqProxy);
return k.key;
}
STAmount
channelBalance(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfBalance];
}
STAmount
channelAmount(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfAmount];
}
bool
channelExists(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
return bool(slep);
}
Buffer
signClaimAuth(
PublicKey const& pk,
SecretKey const& sk,
uint256 const& channel,
STAmount const& authAmt)
{
Serializer msg;
serializePayChanAuthorization(msg, channel, authAmt.xrp());
return sign(pk, sk, msg.slice());
}
} // namespace xrpl::test::jtx::paychan
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <test/jtx/Account.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <cstdint>
#include <optional>
/**
* Paychan operations.
*/
namespace xrpl::test::jtx::paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt);
inline json::Value
create(
Account const& account,
Account const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt)
{
return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag);
}
json::Value
fund(
AccountID const& account,
uint256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration = std::nullopt);
json::Value
claim(
AccountID const& account,
uint256 const& channel,
std::optional<STAmount> const& balance = std::nullopt,
std::optional<STAmount> const& amount = std::nullopt,
std::optional<Slice> const& signature = std::nullopt,
std::optional<PublicKey> const& pk = std::nullopt);
uint256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue);
inline uint256
channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue)
{
return channel(account.id(), dst.id(), seqProxyValue);
}
STAmount
channelBalance(ReadView const& view, uint256 const& chan);
STAmount
channelAmount(ReadView const& view, uint256 const& chan);
bool
channelExists(ReadView const& view, uint256 const& chan);
Buffer
signClaimAuth(
PublicKey const& pk,
SecretKey const& sk,
uint256 const& channel,
STAmount const& authAmt);
} // namespace xrpl::test::jtx::paychan
+16
View File
@@ -319,6 +319,22 @@ class AccountTx_test : public beast::unit_test::Suite
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotFound));
}
{
json::Value p{jParams};
p[jss::ledger_index] = "validated";
BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
p[jss::ledger_index] = "closed";
BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
p[jss::ledger_index] = "current";
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotValidated));
p[jss::ledger_index] = "";
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotValidated));
}
// Ledger Hash
{
json::Value p{jParams};
+11
View File
@@ -506,7 +506,18 @@ class Feature_test : public beast::unit_test::Suite
feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == kFeatureName, "name");
BEAST_EXPECTS(feature[jss::vetoed].isBool() && !feature[jss::vetoed].asBool(), "vetoed");
auto testInvalidVetoed = [&](auto const& vetoed) {
json::Value params;
params[jss::feature] = kFeatureName;
params[jss::vetoed] = vetoed;
auto const result = env.rpc("json", "feature", to_string(params))[jss::result];
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
};
testInvalidVetoed("false");
testInvalidVetoed(json::Value(json::ValueType::Null));
// anything other than accept or reject is an error
jrr = env.rpc("feature", kFeatureName, "maybe");
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
+168 -8
View File
@@ -92,6 +92,7 @@ std::vector<std::pair<json::StaticString, FieldType>> gMappings{
{jss::authorized, FieldType::AccountField},
{jss::credential_type, FieldType::BlobField},
{jss::currency, FieldType::CurrencyField},
{jss::destination, FieldType::AccountField},
{jss::issuer, FieldType::AccountField},
{jss::oracle_document_id, FieldType::UInt32Field},
{jss::owner, FieldType::AccountField},
@@ -813,7 +814,8 @@ class LedgerEntry_test : public beast::unit_test::Suite
env.fund(XRP(10000), alice);
env.close();
auto const checkId = keylet::check(env.master, SeqProxy::rawSequence(env.seq(env.master)));
auto const checkSeq = env.seq(env.master);
auto const checkId = keylet::check(env.master, SeqProxy::rawSequence(checkSeq));
env(check::create(env.master, alice, XRP(100)));
env.close();
@@ -829,6 +831,30 @@ class LedgerEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
}
{
// Request a check by account and seq.
json::Value jvParams;
jvParams[jss::check] = json::ValueType::Object;
jvParams[jss::check][jss::account] = env.master.human();
jvParams[jss::check][jss::seq] = checkSeq;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
BEAST_EXPECT(jrr[jss::index] == to_string(checkId.key));
}
{
// Request a non-existent check by account and seq.
json::Value jvParams;
jvParams[jss::check] = json::ValueType::Object;
jvParams[jss::check][jss::account] = env.master.human();
jvParams[jss::check][jss::seq] = checkSeq + 1000;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Request an index that is not a check. We'll use alice's
// account root index.
@@ -849,7 +875,13 @@ class LedgerEntry_test : public beast::unit_test::Suite
}
{
// Check malformed cases
runLedgerEntryTest(env, jss::check);
runLedgerEntryTest(
env,
jss::check,
{
{.fieldName = jss::account, .malformedErrorMsg = "malformedAddress"},
{.fieldName = jss::seq, .malformedErrorMsg = "malformedRequest"},
});
}
}
@@ -1533,8 +1565,8 @@ class LedgerEntry_test : public beast::unit_test::Suite
UInt256 const nftokenID0 = token::getNextID(env, issuer, 0, tfTransferable);
env(token::mint(issuer, 0), Txflags(tfTransferable));
env.close();
UInt256 const offerID =
keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key;
std::uint32_t const offerSeq = env.seq(issuer);
UInt256 const offerID = keylet::nftokenOffer(issuer, SeqProxy::rawSequence(offerSeq)).key;
env(token::createOffer(issuer, nftokenID0, drops(1)),
token::Destination(buyer),
Txflags(tfSellNFToken));
@@ -1549,9 +1581,39 @@ class LedgerEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0));
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "1");
}
{
// Request by owner and seq.
json::Value jvParams;
jvParams[jss::nft_offer] = json::ValueType::Object;
jvParams[jss::nft_offer][jss::owner] = issuer.human();
jvParams[jss::nft_offer][jss::seq] = offerSeq;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NFTokenOffer);
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == issuer.human());
BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0));
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "1");
BEAST_EXPECT(jrr[jss::index] == to_string(offerID));
}
{
// Request a non-existent offer by owner and seq.
json::Value jvParams;
jvParams[jss::nft_offer] = json::ValueType::Object;
jvParams[jss::nft_offer][jss::owner] = issuer.human();
jvParams[jss::nft_offer][jss::seq] = offerSeq + 1000;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
// negative tests
runLedgerEntryTest(env, jss::nft_offer);
runLedgerEntryTest(
env,
jss::nft_offer,
{
{.fieldName = jss::owner, .malformedErrorMsg = "malformedOwner"},
{.fieldName = jss::seq, .malformedErrorMsg = "malformedRequest"},
});
}
void
@@ -1713,13 +1775,14 @@ class LedgerEntry_test : public beast::unit_test::Suite
return jv;
};
auto const payChanSeq = env.seq(alice);
env(payChanCreate(alice, env.master, XRP(57), 18s, alice.pk()));
env.close();
std::string const ledgerHash{to_string(env.closed()->header().hash)};
UInt256 const payChanIndex{
keylet::payChannel(alice, env.master, SeqProxy::rawSequence(env.seq(alice) - 1)).key};
keylet::payChannel(alice, env.master, SeqProxy::rawSequence(payChanSeq)).key};
{
// Request the payment channel using its index.
json::Value jvParams;
@@ -1731,6 +1794,33 @@ class LedgerEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0");
BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18);
}
{
// Request the payment channel by account, destination, and seq.
json::Value jvParams;
jvParams[jss::payment_channel] = json::ValueType::Object;
jvParams[jss::payment_channel][jss::account] = alice.human();
jvParams[jss::payment_channel][jss::destination] = env.master.human();
jvParams[jss::payment_channel][jss::seq] = payChanSeq;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "57000000");
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0");
BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18);
BEAST_EXPECT(jrr[jss::index] == to_string(payChanIndex));
}
{
// Request a non-existent payment channel by account, destination, and seq.
json::Value jvParams;
jvParams[jss::payment_channel] = json::ValueType::Object;
jvParams[jss::payment_channel][jss::account] = alice.human();
jvParams[jss::payment_channel][jss::destination] = env.master.human();
jvParams[jss::payment_channel][jss::seq] = payChanSeq + 1000;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Request an index that is not a payment channel.
json::Value jvParams;
@@ -1743,7 +1833,14 @@ class LedgerEntry_test : public beast::unit_test::Suite
{
// Malformed paychan field
runLedgerEntryTest(env, jss::payment_channel);
runLedgerEntryTest(
env,
jss::payment_channel,
{
{.fieldName = jss::account, .malformedErrorMsg = "malformedAddress"},
{.fieldName = jss::destination, .malformedErrorMsg = "malformedAddress"},
{.fieldName = jss::seq, .malformedErrorMsg = "malformedRequest"},
});
}
}
@@ -1892,7 +1989,70 @@ class LedgerEntry_test : public beast::unit_test::Suite
testcase("Signer List");
using namespace test::jtx;
Env env{*this};
runLedgerEntryTest(env, jss::signer_list);
Account const alice{"alice"};
Account const bogie{"bogie"};
Account const demon{"demon"};
env.fund(XRP(10000), alice);
env.close();
// Attach phantom signers to alice.
env(signers(alice, 1, {{bogie, 1}, {demon, 1}}));
env.close();
std::string const ledgerHash{to_string(env.closed()->header().hash)};
auto const signerListIndex = keylet::signerList(alice).key;
{
// Request by hash.
json::Value jvParams;
jvParams[jss::signer_list] = to_string(signerListIndex);
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::SignerList);
BEAST_EXPECT(jrr[jss::node][sfSignerQuorum.jsonName] == 1);
}
{
// Request by account.
json::Value jvParams;
jvParams[jss::signer_list] = json::ValueType::Object;
jvParams[jss::signer_list][jss::account] = alice.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::SignerList);
BEAST_EXPECT(jrr[jss::node][sfSignerQuorum.jsonName] == 1);
BEAST_EXPECT(jrr[jss::index] == to_string(signerListIndex));
}
{
// Request a non-existent signer list by account.
json::Value jvParams;
jvParams[jss::signer_list] = json::ValueType::Object;
jvParams[jss::signer_list][jss::account] = env.master.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Request an index that is not a signer list.
json::Value jvParams;
jvParams[jss::signer_list] = ledgerHash;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Malformed signer_list fields
runLedgerEntryTest(
env,
jss::signer_list,
{
{.fieldName = jss::account, .malformedErrorMsg = "malformedAddress"},
});
}
}
void
+13
View File
@@ -103,6 +103,19 @@ class LedgerRPC_test : public beast::unit_test::Suite
jrr[jss::ledger][jss::ledger_index] == std::to_string(env.current()->header().seq));
BEAST_EXPECT(jrr[jss::ledger_current_index] == env.current()->header().seq);
}
{
auto const jrr = env.rpc("json", "ledger", "{}")[jss::result];
BEAST_EXPECT(!jrr.isMember(jss::ledger));
BEAST_EXPECT(jrr[jss::closed][jss::ledger][jss::closed] == true);
BEAST_EXPECT(
jrr[jss::closed][jss::ledger][jss::ledger_index] ==
std::to_string(env.closed()->header().seq));
BEAST_EXPECT(jrr[jss::open][jss::ledger][jss::closed] == false);
BEAST_EXPECT(
jrr[jss::open][jss::ledger][jss::ledger_index] ==
std::to_string(env.current()->header().seq));
}
}
void
+9 -9
View File
@@ -1,4 +1,3 @@
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/beast/unit_test/suite.h>
@@ -24,49 +23,50 @@ public:
// Test no type.
json::Value tx = json::ValueType::Object;
auto result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == 0);
// Test empty type.
tx[jss::type] = "";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type'."));
BEAST_EXPECT(result.second == 0);
// Test type using canonical name in mixedcase.
tx[jss::type] = "MPTokenIssuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE);
// Test type using canonical name in lowercase.
tx[jss::type] = "mptokenissuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE);
// Test type using RPC name with exact match.
tx[jss::type] = "mpt_issuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE);
// Test type using RPC name with inexact match.
tx[jss::type] = "MPT_Issuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type'."));
BEAST_EXPECT(result.second == 0);
// Test invalid type.
tx[jss::type] = 1234;
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(
result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type', not string."));
BEAST_EXPECT(result.second == 0);
// Test unknown type.
tx[jss::type] = "unknown";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type'."));
BEAST_EXPECT(result.second == 0);
}
-207
View File
@@ -1,207 +0,0 @@
#include <xrpld/rpc/Status.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <cstddef>
#include <exception>
#include <string>
namespace xrpl::rpc {
class codeString_test : public beast::unit_test::Suite
{
private:
template <typename Type>
std::string
codeString(Type t)
{
return Status(t).codeString();
}
void
testOk()
{
testcase("OK");
{
auto s = codeString(Status());
expect(s.empty(), "String for OK status");
}
{
auto s = codeString(Status::kOK);
expect(s.empty(), "String for OK status");
}
{
auto s = codeString(0);
expect(s.empty(), "String for 0 status");
}
{
auto s = codeString(tesSUCCESS);
expect(s.empty(), "String for tesSUCCESS");
}
{
auto s = codeString(RpcSuccess);
expect(s.empty(), "String for rpcSUCCESS");
}
}
void
testError()
{
testcase("error");
{
auto s = codeString(23);
expect(s == "23", s);
}
{
auto s = codeString(temBAD_AMOUNT);
expect(s == "temBAD_AMOUNT: Malformed: Bad amount.", s);
}
{
auto s = codeString(RpcBadSyntax);
expect(s == "badSyntax: Syntax error.", s);
}
}
public:
void
run() override
{
testOk();
testError();
}
};
BEAST_DEFINE_TESTSUITE(codeString, rpc, RPC);
class fillJson_test : public beast::unit_test::Suite
{
private:
json::Value value_;
template <typename Type>
void
fillJson(Type t)
{
value_.clear();
Status(t).fillJson(value_);
}
void
testOk()
{
testcase("OK");
fillJson(Status());
expect(!value_, "Value for empty status");
fillJson(0);
expect(!value_, "Value for 0 status");
fillJson(Status::kOK);
expect(!value_, "Value for OK status");
fillJson(tesSUCCESS);
expect(!value_, "Value for tesSUCCESS");
fillJson(RpcSuccess);
expect(!value_, "Value for rpcSUCCESS");
}
template <typename Type>
void
expectFill(
std::string const& label,
Type status,
Status::Strings messages,
std::string const& message)
{
value_.clear();
fillJson(Status(status, messages));
auto prefix = label + ": ";
expect(bool(value_), prefix + "No value");
auto error = value_[jss::error];
expect(bool(error), prefix + "No error.");
auto code = error[jss::code].asInt();
expect(
status == code,
prefix + "Wrong status " + std::to_string(code) + " != " + std::to_string(status));
auto m = error[jss::message].asString();
expect(m == message, m + " != " + message);
auto d = error[jss::data];
size_t const s1 = d.size();
size_t const s2 = messages.size();
expect(
s1 == s2,
prefix + "Data sizes differ " + std::to_string(s1) + " != " + std::to_string(s2));
for (auto i = 0; i < std::min(s1, s2); ++i)
{
auto ds = d[i].asString();
expect(ds == messages[i], prefix + ds + " != " + messages[i]);
}
}
void
testError()
{
testcase("error");
expectFill("temBAD_AMOUNT", temBAD_AMOUNT, {}, "temBAD_AMOUNT: Malformed: Bad amount.");
expectFill(
"RpcBadSyntax",
RpcBadSyntax,
{"An error.", "Another error."},
"badSyntax: Syntax error.");
expectFill("integer message", 23, {"Stuff."}, "23");
}
void
testThrow()
{
testcase("throw");
try
{
Throw<Status>(Status(temBAD_PATH, {"path=sdcdfd"}));
}
catch (Status const& s)
{
expect(s.toTER() == temBAD_PATH, "temBAD_PATH wasn't thrown");
auto msgs = s.messages();
expect(msgs.size() == 1, "Wrong number of messages");
expect(msgs[0] == "path=sdcdfd", msgs[0]);
}
catch (std::exception const&)
{
expect(false, "Didn't catch a Status");
}
}
public:
void
run() override
{
testOk();
testError();
testThrow();
}
};
BEAST_DEFINE_TESTSUITE(fillJson, rpc, xrpl);
} // namespace xrpl::rpc
+23
View File
@@ -59,6 +59,19 @@ class TransactionEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(result[jss::status] == "error");
}
{
// No ledger selects the current one, not any ledger.
json::Value params{json::ValueType::Object};
params[jss::tx_hash] =
"E2FE8D4AF3FCC3944DDF6CD8CDDC5E3F0AD50863EF8919AFEF10CB6408CD4D05";
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "notYetImplemented");
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(result.isMember(jss::ledger_current_index));
BEAST_EXPECT(!result.isMember(jss::ledger_hash));
BEAST_EXPECT(result[jss::validated] == false);
}
{
json::Value params{json::ValueType::Object};
params[jss::ledger] = "closed";
@@ -69,6 +82,16 @@ class TransactionEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(result[jss::status] == "error");
}
for (auto const type : {json::ValueType::Object, json::ValueType::Array})
{
json::Value params{json::ValueType::Object};
params[jss::ledger] = "closed";
params[jss::tx_hash] = json::Value{type};
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "malformedRequest");
BEAST_EXPECT(result[jss::status] == "error");
}
std::string const txHash{
"E2FE8D4AF3FCC3944DDF6CD8CDDC5E3F0AD50863EF8919AFEF10CB6408CD4D05"};
+103
View File
@@ -0,0 +1,103 @@
#include <xrpl/beast/utility/Journal.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <string>
namespace beast {
namespace {
// Counts the messages that actually reach the sink, so the tests can assert on
// which severities the journal let through.
class CountingSink : public Journal::Sink
{
std::size_t count_{0};
public:
CountingSink() : Sink(Severity::Warning, false)
{
}
[[nodiscard]] std::size_t
count() const
{
return count_;
}
void
write(Severity level, std::string const&) override
{
if (level >= threshold())
++count_;
}
void
writeAlways(Severity, std::string const&) override
{
++count_;
}
};
} // namespace
TEST(Journal, info_threshold)
{
CountingSink sink;
sink.threshold(Severity::Info);
Journal const j(sink);
j.trace() << " ";
EXPECT_EQ(sink.count(), 0uz);
j.debug() << " ";
EXPECT_EQ(sink.count(), 0uz);
j.info() << " ";
EXPECT_EQ(sink.count(), 1uz);
j.warn() << " ";
EXPECT_EQ(sink.count(), 2uz);
j.error() << " ";
EXPECT_EQ(sink.count(), 3uz);
j.fatal() << " ";
EXPECT_EQ(sink.count(), 4uz);
}
TEST(Journal, debug_threshold)
{
CountingSink sink;
sink.threshold(Severity::Debug);
Journal const j(sink);
j.trace() << " ";
EXPECT_EQ(sink.count(), 0uz);
j.debug() << " ";
EXPECT_EQ(sink.count(), 1uz);
j.info() << " ";
EXPECT_EQ(sink.count(), 2uz);
j.warn() << " ";
EXPECT_EQ(sink.count(), 3uz);
j.error() << " ";
EXPECT_EQ(sink.count(), 4uz);
j.fatal() << " ";
EXPECT_EQ(sink.count(), 5uz);
}
// A Journal holds a reference to its sink rather than a copy of the threshold,
// so lowering the threshold has to take effect on a Journal that already exists.
// The two tests above each build the Journal after setting the threshold, so
// neither of them would notice if that stopped working.
TEST(Journal, threshold_change_applies_to_an_existing_journal)
{
CountingSink sink;
sink.threshold(Severity::Info);
Journal const j(sink);
j.debug() << " ";
EXPECT_EQ(sink.count(), 0uz);
sink.threshold(Severity::Debug);
j.debug() << " ";
EXPECT_EQ(sink.count(), 1uz);
}
} // namespace beast
@@ -0,0 +1,14 @@
#include <xrpl/beast/clock/basic_seconds_clock.h>
#include <gtest/gtest.h>
namespace beast {
// The clock keeps a lazily started background thread; this only asserts that
// asking it for the time does not throw or crash.
TEST(BasicSecondsClock, now)
{
EXPECT_NO_THROW({ BasicSecondsClock::now(); });
}
} // namespace beast
+147
View File
@@ -0,0 +1,147 @@
#include <xrpl/beast/hash/xxhasher.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
namespace beast {
namespace {
constexpr std::string_view kInput{"Hello, xxHash!"};
constexpr std::uint32_t kSeed{102};
constexpr std::uint32_t kOtherSeed{103};
// The multiple-update and one-update cases hash the same kRepeatCount copies of
// kInput, so their expected digests must agree.
constexpr std::size_t kRepeatCount{100};
constexpr std::size_t kBigObjectRepeat{20};
// Returns text repeated the given number of times.
//
// Used by the cases that hash a large object in one update, so that their
// expected digest can be compared against the cases that feed the same bytes
// in as many small updates.
std::string
repeat(std::string_view text, std::size_t times)
{
std::string out;
out.reserve(text.size() * times);
for (std::size_t i = 0; i < times; ++i)
out += text;
return out;
}
} // namespace
TEST(XXHasher, without_seed)
{
Xxhasher hasher{};
hasher(kInput.data(), kInput.size());
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 16042857369214894119ULL);
}
TEST(XXHasher, with_seed)
{
Xxhasher hasher{kSeed};
hasher(kInput.data(), kInput.size());
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 14440132435660934800ULL);
}
TEST(XXHasher, with_two_seeds)
{
Xxhasher hasher{kSeed, kOtherSeed};
hasher(kInput.data(), kInput.size());
// The second seed is ignored, so this matches the single-seed result.
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 14440132435660934800ULL);
}
// Feeds the bytes in as kRepeatCount small updates, never materializing the full object.
// big_object_with_one_update_without_seed below hashes the same bytes in a single
// update and must agree, which is what exercises xxHash's internal buffering.
TEST(XXHasher, big_object_with_multiple_small_updates_without_seed)
{
Xxhasher hasher{};
for (std::size_t i = 0; i < kRepeatCount; ++i)
hasher(kInput.data(), kInput.size());
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 15296278154063476002ULL);
}
TEST(XXHasher, big_object_with_multiple_small_updates_with_seed)
{
Xxhasher hasher{kOtherSeed};
for (std::size_t i = 0; i < kRepeatCount; ++i)
hasher(kInput.data(), kInput.size());
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 17285302196561698791ULL);
}
TEST(XXHasher, big_object_with_small_and_big_updates_without_seed)
{
Xxhasher hasher{};
std::string const bigObject = repeat(kInput, kBigObjectRepeat);
hasher(kInput.data(), kInput.size());
hasher(bigObject.data(), bigObject.size());
hasher(kInput.data(), kInput.size());
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 1865045178324729219ULL);
}
TEST(XXHasher, big_object_with_small_and_big_updates_with_seed)
{
Xxhasher hasher{kOtherSeed};
std::string const bigObject = repeat(kInput, kBigObjectRepeat);
hasher(kInput.data(), kInput.size());
hasher(bigObject.data(), bigObject.size());
hasher(kInput.data(), kInput.size());
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 16189862915636005281ULL);
}
TEST(XXHasher, big_object_with_one_update_without_seed)
{
Xxhasher hasher{};
std::string const object = repeat(kInput, kRepeatCount);
hasher(object.data(), object.size());
// Hashing the whole object at once must match hashing it in kRepeatCount pieces.
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 15296278154063476002ULL);
}
TEST(XXHasher, big_object_with_one_update_with_seed)
{
Xxhasher hasher{kOtherSeed};
std::string const object = repeat(kInput, kRepeatCount);
hasher(object.data(), object.size());
EXPECT_EQ(static_cast<Xxhasher::result_type>(hasher), 17285302196561698791ULL);
}
TEST(XXHasher, operator_result_type_does_not_change_internal_state)
{
{
Xxhasher hasher;
std::string const object{"Hello xxhash"};
hasher(object.data(), object.size());
EXPECT_EQ(
static_cast<Xxhasher::result_type>(hasher), static_cast<Xxhasher::result_type>(hasher));
}
{
Xxhasher hasher;
std::string const object = repeat(kInput, kRepeatCount);
hasher(object.data(), object.size());
EXPECT_EQ(hasher.operator Xxhasher::result_type(), hasher.operator Xxhasher::result_type());
}
}
} // namespace beast
+19 -8
View File
@@ -38,6 +38,7 @@
#include <xrpl/ledger/entries/XChainOwnedClaimIDEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/XChainOwnedCreateAccountClaimIDEntry.h> // IWYU pragma: keep
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -55,11 +56,11 @@
namespace xrpl {
// The entry classes have no consumers yet, and an un-instantiated class
// template is barely type-checked. Instantiate every one explicitly so the
// compiler actually checks them. Keep this block even once real call sites
// exist: it is what catches a new ledger entry type being added without its
// wrapper class, or the wrapper class existing but never actually being used.
// An un-instantiated class template is barely type-checked. Name every entry
// class in a static_assert below so its class definition is instantiated and
// type-checked; the per-type suites check the member bodies. Keep this block
// even once real call sites exist: it is what catches a new ledger entry type
// being added without its wrapper class.
//
// Driving this off ledger_entries.macro keeps it exhaustive by construction:
// adding a ledger entry type without adding its entry class stops compiling
@@ -76,12 +77,22 @@ template class SLEBase<ApplyView>;
#pragma push_macro("LEDGER_ENTRY")
#undef LEDGER_ENTRY
// No explicit instantiation here, neither a definition nor a declaration.
// A definition would instantiate an entry a second time once its member
// bodies live in its own .cpp (MSVC rejects that with C4661 under /WX), and a
// declaration (`extern template`) stops clang from emitting the inline
// constructors of the header-only entries at -O0, which leaves them undefined
// at link time in the debug builds. Naming the class in a static_assert is
// enough to instantiate its definition, which is all this exhaustiveness
// check needs; the per-type suites exercise the members.
#define LEDGER_ENTRY(tag, value, name, ...) \
template class name##Entry<ReadView>; \
template class name##Entry<ApplyView>; \
static_assert( \
name##Entry<ReadView>::kEntryType == tag && name##Entry<ApplyView>::kEntryType == tag, \
#name "Entry must be bound to " #tag);
#name "Entry must be bound to " #tag); \
static_assert( \
std::is_constructible_v<name##Entry<ReadView>, Keylet const&, ReadView const&> && \
std::is_constructible_v<name##Entry<ApplyView>, Keylet const&, ApplyView&>, \
#name "Entry must be constructible from a Keylet and a view");
#include <xrpl/protocol/detail/ledger_entries.macro>
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff. Show more