Compare commits

...

17 Commits

Author SHA1 Message Date
Ayaz Salikhov
1d325c335a Remove clang-tidy patches, they'll soon no longer be needed 2026-10-06 13:49:59 +01:00
Ayaz Salikhov
3074723eba Only check binaries linked by the Nix toolchain 2026-10-06 13:49:34 +01:00
Ayaz Salikhov
75c0e58049 Update lockfile using script 2026-10-06 13:15:00 +01:00
Ayaz Salikhov
7681b72b85 Make b2 static and check-build-context-runtime 2026-10-06 13:12:15 +01:00
Ayaz Salikhov
d79ad12242 Use <host_version> to not repeat tool versions 2026-10-06 12:48:52 +01:00
Ayaz Salikhov
48158adc8c Disable misc-static-assert for now due to 22.1.8 clang-tidy 2026-10-06 12:48:31 +01:00
Ayaz Salikhov
edb341acc9 Make default_build_options use regular build_options 2026-10-06 12:33:40 +01:00
Ayaz Salikhov
92d9fd8aeb Make grpc a build requirement too 2026-10-06 12:03:02 +01:00
Ayaz Salikhov
1a5ce8377c Update docs 2026-10-06 11:53:05 +01:00
Ayaz Salikhov
026efd2606 Fix linker flags 2026-10-06 11:27:33 +01:00
Ayaz Salikhov
2449f951eb Make tool use static runtime; don't sanitize them 2026-10-06 11:23:26 +01:00
Ayaz Salikhov
24400ba70f chore: Update docker images 2026-10-06 10:48:23 +01:00
yinyiqian1
9cbf78ba99 test: Add more tests for granular permissions (#8459)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
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
Timur Yalymov
63c97e719f feat: Allow lending transactions in Batch (#8244)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-10-05 23:17:26 +00:00
Mayukha Vadari
3ac26f23c7 feat: Add full support for all objects in ledger_entry (#6319)
Co-authored-by: Timur Yalymov <36795566+tyalymov@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-10-05 23:17:14 +00:00
Bart
ff9410bc56 build: Define XRPL_ASAN, XRPL_TSAN, and XRPL_UBSAN compile definitions (#8483)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-10-05 23:15:08 +00:00
35 changed files with 1805 additions and 260 deletions

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"

View File

@@ -1,5 +1,5 @@
{
"image_tag": "sha-060957e",
"image_tag": "sha-1798262",
"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"
}
}
]

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"

View File

@@ -5,7 +5,6 @@ on:
branches:
- develop
paths:
- ".github/workflows/build-packaging-images.yml"
- "bin/install-packaging-tools.sh"
- "package/images/packaging/**"
pull_request:

View File

@@ -5,7 +5,6 @@ on:
branches:
- develop
paths:
- ".github/workflows/build-pre-commit-image.yml"
- "bin/pre-commit/Dockerfile"
- "rust-toolchain.toml"
pull_request:

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-1798262
permissions:
contents: read
# Needed to open an issue on scheduled failures.

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-3a2d19f" }'

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-1798262
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.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-1798262"
permissions:
contents: read
issues: write

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-1798262
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-1798262
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-1798262
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.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-1798262
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}

View File

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

View File

@@ -0,0 +1,71 @@
#!/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
# TODO: Add jq to the Nix packages and use it here instead of Python.
folders="$(python3 -c '
import json, sys
for node in json.load(open(sys.argv[1]))["graph"]["nodes"].values():
if node["context"] == "build" and node.get("package_folder"):
print(node["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 ]

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}

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)

View File

@@ -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",

View File

@@ -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 %}

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" %}

View File

@@ -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

20
docs/build/nix.md vendored
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

View File

@@ -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

View File

@@ -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,

View File

@@ -2,172 +2,172 @@ 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.32.0
/nix/store/q0g5grbh7zm8gfhhnq5h6fm1rbs9yv3n-conan-2.32.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 version 22.1.8
/nix/store/dggxva67vs7virgsmd6xkwd6p7zsc5pn-clang-wrapper-22.1.8/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/dagc2rq44gfbr7w7yvvqca3yqpc9gqbq-clang-22/bin/clang-22
clang version 22.1.8
/nix/store/ckz6y10v5dbyswpf9f6hd8kjfv84f920-clang-22/bin/clang-22
✅ clang++
clang version 22.1.7
/nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang++
clang version 22.1.8
/nix/store/dggxva67vs7virgsmd6xkwd6p7zsc5pn-clang-wrapper-22.1.8/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/l5m8clin1npl605wdkd8mr18ggxww3z4-clang++-22/bin/clang++-22
clang version 22.1.8
/nix/store/lsishk8fwbdmlwr5fbwyd3yhhjsm67qr-clang++-22/bin/clang++-22
✅ 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
✅ 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 version 22.1.8
/nix/store/dykdkqyz48al2mllwjpwgdxyxgkwhk9x-clang-tools-22.1.8/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 22.1.8
/nix/store/m0aqc6dbryn1mvr0m2zsxxgpmahcvp6v-clang-apply-replacements-22/bin/clang-apply-replacements-22
✅ clang-format
clang-format version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-format
clang-format version 22.1.8
/nix/store/dykdkqyz48al2mllwjpwgdxyxgkwhk9x-clang-tools-22.1.8/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 22.1.8
/nix/store/grpzqmx46d3xr3ssrz4ibkl9xr7rvjzj-clang-format-22/bin/clang-format-22
✅ clang-tidy
LLVM version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-tidy
LLVM version 22.1.8
/nix/store/dykdkqyz48al2mllwjpwgdxyxgkwhk9x-clang-tools-22.1.8/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/gdrkvpw846lkyzh8y9p3zx50g6ml2v84-clang-tidy-22/bin/clang-tidy-22
LLVM version 22.1.8
/nix/store/v9g20lw1q1qq9ljwvh5k580hgd9hmsv8-clang-tidy-22/bin/clang-tidy-22
✅ 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
/nix/store/a760dc8nldq1715410384bkk83idmm00-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/n8yak1ap308gvi7gmrniw0ybsx80fjws-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/xwh85m9sa5h8fnpnlmqwfnf2sz9l59mm-run-clang-tidy-22/bin/run-clang-tidy-22
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).

View File

@@ -2,172 +2,172 @@ 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.32.0
/nix/store/vbr044x1zbq062kkw53zm0sm3slv8g56-conan-2.32.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 version 22.1.8
/nix/store/7z7by1cxa52mk7hjia1pimjvsd6aizhf-clang-wrapper-22.1.8/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/vcf6ilfwn57828hwzyp6zlyr24j9j6yw-clang-22/bin/clang-22
clang version 22.1.8
/nix/store/pqn36q5paasw7v8sjgz5fd90mfpgkkqr-clang-22/bin/clang-22
✅ clang++
clang version 22.1.7
/nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang++
clang version 22.1.8
/nix/store/7z7by1cxa52mk7hjia1pimjvsd6aizhf-clang-wrapper-22.1.8/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/xby0f6gamr7m27zp5cndsvghbp9lgb3c-clang++-22/bin/clang++-22
clang version 22.1.8
/nix/store/m9a3slb0lcixm2csgjz9s0nvj9m5ysnq-clang++-22/bin/clang++-22
✅ 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
✅ 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 version 22.1.8
/nix/store/iv4dxwnzvl0lqj8jfyibwf5ijb8hjw2i-clang-tools-22.1.8/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 22.1.8
/nix/store/7gq05wr155k0pm1b2k73pya3l35hgifl-clang-apply-replacements-22/bin/clang-apply-replacements-22
✅ clang-format
clang-format version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-format
clang-format version 22.1.8
/nix/store/iv4dxwnzvl0lqj8jfyibwf5ijb8hjw2i-clang-tools-22.1.8/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 22.1.8
/nix/store/54pnici9imw8ff0j5m7nig7dwaxrnlhv-clang-format-22/bin/clang-format-22
✅ clang-tidy
LLVM version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-tidy
LLVM version 22.1.8
/nix/store/iv4dxwnzvl0lqj8jfyibwf5ijb8hjw2i-clang-tools-22.1.8/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/wdyd6cb9z1lyi37lbzvwldgcc7yv1n5c-clang-tidy-22/bin/clang-tidy-22
LLVM version 22.1.8
/nix/store/0mmdxscn11irlmm7jvp1c0a32712s026-clang-tidy-22/bin/clang-tidy-22
✅ 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
/nix/store/b1yccjmij9qiha8a8d9yxayjdh5cg2vq-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/f8m0p9ad40brp9ahy4i0h27kqjkya1j9-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/v75dyrp7vwdgx5w2fzwsy00lf94qpz0i-run-clang-tidy-22/bin/run-clang-tidy-22
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).

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))
{

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))
{

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>
@@ -69,7 +68,6 @@
#include <xrpl/tx/transactors/payment/Payment.h>
#include <xrpl/tx/transactors/system/Batch.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <exception>
@@ -120,15 +118,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 +133,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 +3136,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 +3285,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 +3309,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)
{
@@ -5915,6 +5959,7 @@ class Batch_test : public beast::unit_test::Suite
testCheckAllSignatures(features);
testAccountSet(features);
testAccountDelete(features);
testLendingAmendment(features);
testLoan(features);
testObjectCreateSequence(features);
testObjectCreateTicket(features);

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();

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

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

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;

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.
*/

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
{

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

View File

@@ -180,7 +180,23 @@ parseCheck(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const account =
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
if (!account)
return std::unexpected(account.error());
auto const sequence =
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
if (!sequence)
return std::unexpected(sequence.error());
auto const seqProxy = SeqProxy::rawSequence(*sequence);
return keylet::check(*account, seqProxy).key;
}
static std::expected<UInt256, json::Value>
@@ -570,7 +586,23 @@ parseNFTokenOffer(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const owner =
ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
if (!owner)
return std::unexpected(owner.error());
auto const sequence =
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
if (!sequence)
return std::unexpected(sequence.error());
auto const seqProxy = SeqProxy::rawSequence(*sequence);
return keylet::nftokenOffer(*owner, seqProxy).key;
}
static std::expected<UInt256, json::Value>
@@ -638,7 +670,28 @@ parsePayChannel(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const account =
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
if (!account)
return std::unexpected(account.error());
auto const destination =
ledger_entry_helpers::requiredAccountID(params, jss::destination, "malformedAddress");
if (!destination)
return std::unexpected(destination.error());
auto const sequence =
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
if (!sequence)
return std::unexpected(sequence.error());
auto const seqProxy = SeqProxy::rawSequence(*sequence);
return keylet::payChannel(*account, *destination, seqProxy).key;
}
static std::expected<UInt256, json::Value>
@@ -726,7 +779,17 @@ parseSignerList(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const id =
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
if (!id)
return std::unexpected(id.error());
return keylet::signerList(*id).key;
}
static std::expected<UInt256, json::Value>