mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-11 14:18:07 +00:00
Compare commits
160 Commits
dangell7/m
...
a1q123456/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7bc9d890a | ||
|
|
53c281d292 | ||
|
|
291b9d3a28 | ||
|
|
b94b6cc605 | ||
|
|
9fb3d78932 | ||
|
|
4bff29e39b | ||
|
|
29ff8282ba | ||
|
|
af321df56e | ||
|
|
d26e61b516 | ||
|
|
651bb207b4 | ||
|
|
c45363fd8b | ||
|
|
f1744cb76e | ||
|
|
bcbaa4df07 | ||
|
|
3cd357949c | ||
|
|
a9027bb997 | ||
|
|
0a6da4de74 | ||
|
|
6a05339c6c | ||
|
|
1b864895f9 | ||
|
|
f3ad8f8373 | ||
|
|
f75e317fe6 | ||
|
|
0271003597 | ||
|
|
1dadfd7405 | ||
|
|
5cd5241dee | ||
|
|
0b8336bde8 | ||
|
|
9f7993f7d0 | ||
|
|
3a0b7b69da | ||
|
|
0c41a87604 | ||
|
|
f5938e4097 | ||
|
|
c7d10c1b60 | ||
|
|
84e2a155b9 | ||
|
|
cbade49976 | ||
|
|
b712b95f2b | ||
|
|
f93a25e0ff | ||
|
|
65c4bae372 | ||
|
|
3127e2c9d3 | ||
|
|
afa0cc8c6c | ||
|
|
a680ec2581 | ||
|
|
e5cc51096c | ||
|
|
052bfc7310 | ||
|
|
565f3e2787 | ||
|
|
5f964d74f9 | ||
|
|
c905e3948f | ||
|
|
2930bab1c3 | ||
|
|
244b132f8d | ||
|
|
e7b03d9b81 | ||
|
|
06383eaeba | ||
|
|
21031eb05a | ||
|
|
d8b3f54713 | ||
|
|
69e5a8b92e | ||
|
|
4db0997329 | ||
|
|
f2e5f58518 | ||
|
|
c7fbe3aa9f | ||
|
|
ef780fe257 | ||
|
|
bf906c52e8 | ||
|
|
2fa2239cb2 | ||
|
|
a20b7d55f4 | ||
|
|
c09725af11 | ||
|
|
5b273138f5 | ||
|
|
f6290acf6d | ||
|
|
9b3512707b | ||
|
|
8968c892bf | ||
|
|
fd8af944f0 | ||
|
|
2517b5b983 | ||
|
|
14039dce06 | ||
|
|
b21b700951 | ||
|
|
0d0260460b | ||
|
|
a335d6d9fc | ||
|
|
2fbe48d2a6 | ||
|
|
49d3763295 | ||
|
|
f307a9bccc | ||
|
|
1f0915bd46 | ||
|
|
e5fb5afa5e | ||
|
|
af9a2efc18 | ||
|
|
9fc908aa34 | ||
|
|
8098f6de1d | ||
|
|
1c8322853e | ||
|
|
a5a112f646 | ||
|
|
97fbea23cc | ||
|
|
e6055ddbe1 | ||
|
|
624798f7b1 | ||
|
|
e98f90d726 | ||
|
|
0a78cfb4d3 | ||
|
|
6ec28706f1 | ||
|
|
1ed4c3855d | ||
|
|
96c35383ac | ||
|
|
1b45e6bf76 | ||
|
|
84f5e205b1 | ||
|
|
4e2dc7a63b | ||
|
|
7005ca4815 | ||
|
|
734cd81190 | ||
|
|
4fe4ac530a | ||
|
|
9083442c02 | ||
|
|
425fec2f51 | ||
|
|
0ed495b92c | ||
|
|
c1ad8b5295 | ||
|
|
cd396fce37 | ||
|
|
6078cae6ad | ||
|
|
bf1f49940c | ||
|
|
2658200d13 | ||
|
|
086a0c1a35 | ||
|
|
6fd5ece5c3 | ||
|
|
69e45b901c | ||
|
|
69447e8519 | ||
|
|
18e66bb98d | ||
|
|
ed5ec1e554 | ||
|
|
3728d76c68 | ||
|
|
301675ac13 | ||
|
|
0496a80094 | ||
|
|
db8f6cf189 | ||
|
|
1b562f2c9f | ||
|
|
db79ce5d45 | ||
|
|
7a0229c9f5 | ||
|
|
c29cbaec9d | ||
|
|
acdb754728 | ||
|
|
ade43a4b9e | ||
|
|
47294b5ae8 | ||
|
|
6775bacabe | ||
|
|
9b79472c53 | ||
|
|
1ebe5078fa | ||
|
|
f0eacbad31 | ||
|
|
7450b3d299 | ||
|
|
4a5257443c | ||
|
|
bba00533c9 | ||
|
|
5416782cc9 | ||
|
|
6d54abf904 | ||
|
|
c32202999a | ||
|
|
7a38dd8869 | ||
|
|
9161ddbc6d | ||
|
|
56d1d59586 | ||
|
|
6a5b12bc2d | ||
|
|
92726e1b10 | ||
|
|
5ec8f37bb7 | ||
|
|
1da9ca310a | ||
|
|
3be766c2b6 | ||
|
|
a60b5596fe | ||
|
|
a58a692383 | ||
|
|
533dfdddbd | ||
|
|
a5bf7c08fd | ||
|
|
57562b4768 | ||
|
|
3d1d9d707a | ||
|
|
25cb86143a | ||
|
|
b32aca2e59 | ||
|
|
01ab57caee | ||
|
|
bd31dbcc48 | ||
|
|
cf714bbe1f | ||
|
|
d7e23b425d | ||
|
|
928c05e7e6 | ||
|
|
5b5d4c2bdb | ||
|
|
294b127425 | ||
|
|
20f206e024 | ||
|
|
19519c6b19 | ||
|
|
4e83215457 | ||
|
|
5157a2866b | ||
|
|
663229cfdf | ||
|
|
89028a2ec6 | ||
|
|
bb820683cb | ||
|
|
5e0e36f87d | ||
|
|
9ae863d89e | ||
|
|
b17c737e74 | ||
|
|
71d271ebed |
@@ -255,6 +255,7 @@ words:
|
||||
- queuable
|
||||
- Raphson
|
||||
- rcflags
|
||||
- reencrypted
|
||||
- replayer
|
||||
- repodata
|
||||
- repomd
|
||||
|
||||
4
.envrc
4
.envrc
@@ -8,3 +8,7 @@ watch_file rust-toolchain.toml
|
||||
watch_dir conan
|
||||
|
||||
use flake
|
||||
|
||||
# Optional, untracked local overrides. To use a different shell, put e.g.
|
||||
# `use flake .#formal-verification` in .envrc.local.
|
||||
source_env_if_exists .envrc.local
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
# This file is sorted in reverse chronological order, with the most recent commits at the top.
|
||||
# The commits listed here are ignored by git blame, which is useful for formatting-only commits that would otherwise obscure the history of changes to a file.
|
||||
|
||||
# chore: CamelCase for typedef/using in `clang-tidy` (#8177)
|
||||
97a1824537d20d82d25bf151a37a7a4532ebf085
|
||||
# chore: Rename CamelCase namespaces to snake_case (#7933)
|
||||
06488c1318d96f56d0536251bee08ac85fa7fdd3
|
||||
# style: Unify style for all Doxygen comments (#7776)
|
||||
73b6852a122854140336e6e6bc30a3a4b41aa5fd
|
||||
# style: More clang-tidy identifier renaming (#7290)
|
||||
a830ab10efed8d3e59ef2fc15d66efdf9c6bb0d8
|
||||
# refactor: Rename static constants (#7120)
|
||||
5b6e8b6f93b19c1e3f6a3467a25639031d9d9a53
|
||||
# chore: More fixes for bad renames (#7092)
|
||||
7afdd71a54d562b32a50b29a5aa00bb997dc9053
|
||||
# refactor: Enable clang-tidy `readability-identifier-naming` check (#6571)
|
||||
8995564ed6b9e453e144bb663303072a3c1ba305
|
||||
# refactor: Enable remaining clang-tidy `cppcoreguidelines` checks (#6538)
|
||||
|
||||
17
.github/actions/release-info/action.yml
vendored
17
.github/actions/release-info/action.yml
vendored
@@ -15,30 +15,25 @@ outputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# A tag names its own version. Anything else takes it from BuildInfo.cpp and
|
||||
# appends the commit hash as build metadata, joined with a plus sign because a
|
||||
# Conan version cannot contain two hyphens.
|
||||
# A tag names its own version. Anything else is a development build named by
|
||||
# its commit hash, matching what cmake/XrplVersion.cmake derives: the head of
|
||||
# a pull request rather than the merge commit GitHub creates for it.
|
||||
- name: Determine version
|
||||
id: version
|
||||
shell: bash
|
||||
env:
|
||||
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
SHA: ${{ github.sha }}
|
||||
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: |
|
||||
if [[ "${IS_TAG}" == "true" ]]; then
|
||||
version="${REF_NAME}"
|
||||
else
|
||||
version="$(awk -F'"' '/versionString =/ { print $2 }' src/libxrpl/protocol/BuildInfo.cpp)"
|
||||
if [[ -z "${version}" ]]; then
|
||||
echo "Unable to read versionString from BuildInfo.cpp." >&2
|
||||
exit 1
|
||||
fi
|
||||
version="${version}+${SHA:0:7}"
|
||||
version="0.0.0-dev+${SHA:0:7}"
|
||||
fi
|
||||
|
||||
echo "version=${version}" | tee -a "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Determine release channel and package release
|
||||
id: release_info
|
||||
uses: XRPLF/actions/release-info@ebcf6cea14eee258697308a51fea55cad777581b
|
||||
uses: XRPLF/actions/release-info@a9f2eeca6fb3980ba3a84cf68566f1c69ad30674
|
||||
|
||||
40
.github/scripts/releasing/check-merge-back-commits.sh
vendored
Executable file
40
.github/scripts/releasing/check-merge-back-commits.sh
vendored
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
|
||||
# This script fails if <head> merges a release back into <base>,
|
||||
# but also adds commits that aren't on a release or staging branch, see RELEASING.md.
|
||||
# Merge commits are allowed.
|
||||
# Usage: .github/scripts/releasing/check-merge-back-commits.sh <base> <head>
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <base> <head>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE=$1
|
||||
HEAD=$2
|
||||
|
||||
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
||||
# shellcheck source=.github/scripts/releasing/common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
load_release_branches
|
||||
|
||||
# A PR is a merge-back if some of its commits are on a release or staging branch.
|
||||
PR_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}")
|
||||
NEW_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}" --not "${BRANCHES[@]}")
|
||||
if ((NEW_COUNT == PR_COUNT)); then
|
||||
echo "This PR doesn't merge a release back."
|
||||
exit 0
|
||||
fi
|
||||
if ((NEW_COUNT == 0)); then
|
||||
echo "This merge-back adds no commits of its own."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "This PR merges a release back, but also adds commits that aren't on a release or staging branch:"
|
||||
git log --no-merges --format=' %h %s' "${BASE}..${HEAD}" --not "${BRANCHES[@]}"
|
||||
echo
|
||||
echo "Make these changes in a separate PR, see RELEASING.md."
|
||||
exit 1
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
@@ -21,11 +21,10 @@ patch_ids() {
|
||||
git log --no-merges --patch --no-color --no-ext-diff "$@" | git patch-id --stable | sort
|
||||
}
|
||||
|
||||
mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
|
||||
if [ "${#BRANCHES[@]}" -eq 0 ]; then
|
||||
echo "Error: No release or staging branches found."
|
||||
exit 1
|
||||
fi
|
||||
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
||||
# shellcheck source=.github/scripts/releasing/common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
load_release_branches
|
||||
|
||||
# Each line is "<patch-id> <commit>".
|
||||
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
|
||||
|
||||
38
.github/scripts/releasing/check-no-merge-back.sh
vendored
Executable file
38
.github/scripts/releasing/check-no-merge-back.sh
vendored
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
|
||||
# This script fails if <head> contains release or staging commits that <base> does not,
|
||||
# i.e. if <head> merges a release back into <base>.
|
||||
# Used in the merge queue, which squashes PRs and would drop the merge commit, see RELEASING.md.
|
||||
# Usage: .github/scripts/releasing/check-no-merge-back.sh <base> <head>
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <base> <head>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE=$1
|
||||
HEAD=$2
|
||||
|
||||
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
||||
# shellcheck source=.github/scripts/releasing/common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
load_release_branches
|
||||
|
||||
RELEASE_COMMITS=$(git rev-list "${BRANCHES[@]}" --not "${BASE}")
|
||||
HEAD_COMMITS=$(git rev-list "${BASE}..${HEAD}")
|
||||
# The release commits in <head>, newest first.
|
||||
MERGED=$(grep -xF -f <(echo "${RELEASE_COMMITS}") <<<"${HEAD_COMMITS}" || true)
|
||||
if [ -z "${MERGED}" ]; then
|
||||
echo "No release commits are merged back."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "This PR merges $(wc -l <<<"${MERGED}" | tr -d ' ') release commits back, e.g.:"
|
||||
head -5 <<<"${MERGED}" | xargs git log --no-walk --format=' %h %s'
|
||||
echo
|
||||
echo "Merge-backs must not go through the merge queue, which squashes them."
|
||||
echo "Fast-forward develop to the PR branch instead, see RELEASING.md."
|
||||
exit 1
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit the script as soon as an error occurs.
|
||||
set -euo pipefail
|
||||
|
||||
12
.github/scripts/releasing/common.sh
vendored
Normal file
12
.github/scripts/releasing/common.sh
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# shellcheck shell=bash
|
||||
|
||||
# Helpers shared by the release checks in this directory, see RELEASING.md.
|
||||
|
||||
# Sets BRANCHES to all release and staging branches, and fails if there are none.
|
||||
load_release_branches() {
|
||||
mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
|
||||
if [ "${#BRANCHES[@]}" -eq 0 ]; then
|
||||
echo "Error: No release or staging branches found."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
6
.github/scripts/strategy-matrix/linux.json
vendored
6
.github/scripts/strategy-matrix/linux.json
vendored
@@ -74,7 +74,7 @@
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON",
|
||||
"package": {
|
||||
"type": "deb",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10"
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,7 +86,7 @@
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON -Dassert=ON",
|
||||
"package": {
|
||||
"type": "deb",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f",
|
||||
"variant": "assert"
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON",
|
||||
"package": {
|
||||
"type": "rpm",
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-49cdc10"
|
||||
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-3a2d19f"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
6
.github/workflows/build-packaging-images.yml
vendored
6
.github/workflows/build-packaging-images.yml
vendored
@@ -7,12 +7,12 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/build-packaging-images.yml"
|
||||
- "bin/install-packaging-tools.sh"
|
||||
- "package/docker/**"
|
||||
- "package/images/packaging/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/build-packaging-images.yml"
|
||||
- "bin/install-packaging-tools.sh"
|
||||
- "package/docker/**"
|
||||
- "package/images/packaging/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -44,6 +44,6 @@ jobs:
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@696384b292577293292daed06af0306d1b83bd7d
|
||||
with:
|
||||
image_name: xrpld/packaging-${{ matrix.distro.name }}
|
||||
dockerfile: package/docker/Dockerfile
|
||||
dockerfile: package/images/packaging/Dockerfile
|
||||
base_image: ${{ matrix.distro.base_image }}
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
|
||||
25
.github/workflows/on-pr.yml
vendored
25
.github/workflows/on-pr.yml
vendored
@@ -166,10 +166,31 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Check for copied release commits
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
env:
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
|
||||
BASE: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha }}
|
||||
run: .github/scripts/releasing/check-no-copied-release-commits.sh "${BASE}" "${HEAD}"
|
||||
# Runs even if the previous check fails, so that both problems are reported at once.
|
||||
- name: Check merge-back has no new commits
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
env:
|
||||
BASE: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha }}
|
||||
run: .github/scripts/releasing/check-merge-back-commits.sh "${BASE}" "${HEAD}"
|
||||
# The queue squashes PRs, so check the PR's own branch, named in the queue branch.
|
||||
- name: Check the merge queue doesn't merge a release back
|
||||
if: ${{ github.event_name == 'merge_group' }}
|
||||
env:
|
||||
BASE: ${{ github.event.merge_group.base_sha }}
|
||||
HEAD_REF: ${{ github.event.merge_group.head_ref }}
|
||||
run: |
|
||||
if ! [[ "${HEAD_REF}" =~ /pr-([0-9]+)-[0-9a-f]+$ ]]; then
|
||||
echo "Error: Can't find the PR number in '${HEAD_REF}'."
|
||||
exit 1
|
||||
fi
|
||||
git fetch --no-tags origin "refs/pull/${BASH_REMATCH[1]}/head"
|
||||
.github/scripts/releasing/check-no-merge-back.sh "${BASE}" FETCH_HEAD
|
||||
|
||||
clang-tidy:
|
||||
needs: should-run
|
||||
|
||||
9
.github/workflows/on-tag.yml
vendored
9
.github/workflows/on-tag.yml
vendored
@@ -1,9 +1,12 @@
|
||||
# When a versioned tag is pushed, this workflow:
|
||||
#
|
||||
# - uploads the libxrpl recipe to the Conan remote
|
||||
# - builds and tests the release binaries
|
||||
# - uploads the libxrpl recipe to the Conan remote
|
||||
# - builds the DEB and RPM packages
|
||||
# - publishes those packages to the XRPLF package repositories
|
||||
#
|
||||
# Nothing is published unless the build passes, which is also where CMake
|
||||
# rejects a tag that is not a valid version, e.g. 3.2.01.
|
||||
name: Tag
|
||||
|
||||
on:
|
||||
@@ -22,6 +25,7 @@ defaults:
|
||||
jobs:
|
||||
upload-recipe:
|
||||
if: ${{ github.repository == 'XRPLF/rippled' }}
|
||||
needs: build-test
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
@@ -51,3 +55,6 @@ jobs:
|
||||
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
|
||||
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}
|
||||
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
antithesis_docker_host: ${{ secrets.ANTITHESIS_DOCKER_HOST }}
|
||||
antithesis_docker_path: ${{ secrets.ANTITHESIS_DOCKER_PATH }}
|
||||
antithesis_docker_credentials: ${{ secrets.ANTITHESIS_DOCKER_CREDENTIALS }}
|
||||
|
||||
3
.github/workflows/on-trigger.yml
vendored
3
.github/workflows/on-trigger.yml
vendored
@@ -130,3 +130,6 @@ jobs:
|
||||
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
|
||||
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}
|
||||
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
antithesis_docker_host: ${{ secrets.ANTITHESIS_DOCKER_HOST }}
|
||||
antithesis_docker_path: ${{ secrets.ANTITHESIS_DOCKER_PATH }}
|
||||
antithesis_docker_credentials: ${{ secrets.ANTITHESIS_DOCKER_CREDENTIALS }}
|
||||
|
||||
13
.github/workflows/reusable-build-test-config.yml
vendored
13
.github/workflows/reusable-build-test-config.yml
vendored
@@ -196,6 +196,19 @@ jobs:
|
||||
env:
|
||||
BUILD_TYPE: ${{ inputs.build_type }}
|
||||
CMAKE_ARGS: ${{ inputs.cmake_args }}
|
||||
# GitHub creates a merge commit for a PR
|
||||
# https://www.kenmuse.com/blog/the-many-shas-of-a-github-pull-request/
|
||||
#
|
||||
# We:
|
||||
# - explicitly provide branch name
|
||||
# - use `github.event.pull_request.head.sha` to get the SHA of last commit in the PR branch
|
||||
#
|
||||
# This way it works both for PRs and pushes to branches.
|
||||
GITHUB_BRANCH_NAME: "${{ github.head_ref || github.ref_name }}"
|
||||
GITHUB_HEAD_SHA: "${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
#
|
||||
# If tag is being pushed, we use that version.
|
||||
FORCE_XRPLD_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }}
|
||||
run: |
|
||||
cmake \
|
||||
-G '${{ runner.os == 'Windows' && 'Visual Studio 18 2026' || 'Ninja' }}' \
|
||||
|
||||
71
.github/workflows/reusable-package.yml
vendored
71
.github/workflows/reusable-package.yml
vendored
@@ -9,8 +9,9 @@
|
||||
# never reaches Nexus
|
||||
# - 'publish' uploads with the image's publish_pkg.py, doing a --dry-run
|
||||
# unless 'publish: true'
|
||||
# - 'docker' builds an Ubuntu image from the tested DEB, pushing it to Docker
|
||||
# Hub only with 'publish: true'
|
||||
# - 'docker' builds an Ubuntu image from the tested DEB, and one with the
|
||||
# voidstar binary for Antithesis, pushing them to Docker Hub and to the
|
||||
# Antithesis registry only with 'publish: true'
|
||||
#
|
||||
# Only linux/amd64 is supported; the runner is hardcoded in the jobs below.
|
||||
name: Package
|
||||
@@ -37,10 +38,19 @@ on:
|
||||
description: "The password or token for that Nexus account."
|
||||
required: false
|
||||
signing_key:
|
||||
description: "Armoured PGP private key used to sign the RPMs. Required when publishing."
|
||||
description: "Armoured PGP private key used to sign the RPMs."
|
||||
required: false
|
||||
dockerhub_token:
|
||||
description: "A Docker Hub organization access token for xrplf, with push access to xrplf/xrpld. Required when publishing."
|
||||
description: "A Docker Hub organization access token for xrplf, with push access to xrplf/xrpld."
|
||||
required: false
|
||||
antithesis_docker_host:
|
||||
description: "The host of the Antithesis container registry, e.g. us-central1-docker.pkg.dev."
|
||||
required: false
|
||||
antithesis_docker_path:
|
||||
description: "The repository path in that registry, the image name excluded."
|
||||
required: false
|
||||
antithesis_docker_credentials:
|
||||
description: "The JSON key of a service account with push access to that repository."
|
||||
required: false
|
||||
|
||||
defaults:
|
||||
@@ -247,18 +257,35 @@ jobs:
|
||||
|
||||
docker:
|
||||
needs: [test-install-deb, test-install-rpm]
|
||||
name: "docker${{ !inputs.publish && ' (dry run)' || '' }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [xrpld, voidstar]
|
||||
name: "docker ${{ matrix.target }}${{ !inputs.publish && ' (dry run)' || '' }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
IMAGE: xrplf/xrpld:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
|
||||
CONTEXT: image-context
|
||||
IMAGE: ${{ matrix.target == 'voidstar' && 'xrpld-voidstar' || 'xrplf/xrpld' }}:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Determine release info
|
||||
id: release_info
|
||||
uses: ./.github/actions/release-info
|
||||
|
||||
# Docker Hub is public, so it only gets builds whose packages are public:
|
||||
# those of a public codebase, and stable releases.
|
||||
# The Antithesis registry is private, so it gets every build.
|
||||
- name: Decide whether to push
|
||||
env:
|
||||
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public' || steps.release_info.outputs.channel == 'stable') }}
|
||||
run: echo "PUSH=${PUSH}" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
@@ -266,13 +293,20 @@ jobs:
|
||||
merge-multiple: true
|
||||
path: ${{ env.PACKAGE_DIR }}
|
||||
|
||||
- name: Download voidstar binary
|
||||
if: ${{ matrix.target == 'voidstar' }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: xrpld-ubuntu-clang-debug-amd64-voidstar
|
||||
path: ${{ env.CONTEXT }}
|
||||
|
||||
- name: Build image
|
||||
env:
|
||||
CONTEXT: image-context
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
mkdir -p "${CONTEXT}"
|
||||
find "${PACKAGE_DIR}" -type f -name 'xrpld_[0-9]*.deb' -exec cp {} "${CONTEXT}/" \;
|
||||
docker build --pull --file package/image/Dockerfile --tag "${IMAGE}" "${CONTEXT}"
|
||||
docker build --pull --file package/images/xrpld/Dockerfile --target "${TARGET}" --tag "${IMAGE}" "${CONTEXT}"
|
||||
|
||||
- name: Start the server
|
||||
run: |
|
||||
@@ -290,14 +324,25 @@ jobs:
|
||||
docker logs "${container}"
|
||||
exit 1
|
||||
|
||||
# Docker Hub is public, so a private build never reaches it.
|
||||
- name: Log in to Docker Hub
|
||||
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
|
||||
if: ${{ env.PUSH == 'true' && matrix.target == 'xrpld' }}
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: xrplf
|
||||
password: ${{ secrets.dockerhub_token }}
|
||||
|
||||
- name: Log in to the Antithesis registry
|
||||
if: ${{ env.PUSH == 'true' && matrix.target == 'voidstar' }}
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ secrets.antithesis_docker_host }}
|
||||
username: _json_key
|
||||
password: ${{ secrets.antithesis_docker_credentials }}
|
||||
|
||||
- name: Push image
|
||||
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
|
||||
run: docker push "${IMAGE}"
|
||||
if: ${{ env.PUSH == 'true' }}
|
||||
env:
|
||||
REGISTRY: ${{ matrix.target == 'voidstar' && format('{0}/{1}/', secrets.antithesis_docker_host, secrets.antithesis_docker_path) || '' }}
|
||||
run: |
|
||||
docker tag "${IMAGE}" "${REGISTRY}${IMAGE}"
|
||||
docker push "${REGISTRY}${IMAGE}"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -92,6 +92,9 @@ target/
|
||||
# Direnv's directory
|
||||
/.direnv
|
||||
|
||||
# Direnv's local, per-developer overrides
|
||||
/.envrc.local
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
|
||||
|
||||
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
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))
|
||||
|
||||
## XRP Ledger server version 3.4.0
|
||||
|
||||
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
|
||||
|
||||
14
BUILD.md
14
BUILD.md
@@ -41,15 +41,19 @@ branch.
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
For a release candidate, choose the relevant release branch, e.g.
|
||||
`release/3.2.x`.
|
||||
For a release or release candidate, check out its [tag](https://github.com/XRPLF/rippled/releases), e.g.:
|
||||
|
||||
```bash
|
||||
git checkout release/3.2.x
|
||||
git checkout 3.4.0
|
||||
```
|
||||
|
||||
For a stable release, choose one of the [tagged
|
||||
releases](https://github.com/XRPLF/rippled/releases).
|
||||
See [RELEASING.md](./RELEASING.md) for how branches and releases are organized.
|
||||
|
||||
A build reports `0.0.0-dev` with its short commit hash as build metadata, e.g.
|
||||
`0.0.0-dev+0123abc`. Only builds of a release, from a tag in CI or a versioned
|
||||
Conan reference, report a release version. To use another version, set the
|
||||
`FORCE_XRPLD_VERSION` environment variable when running CMake, e.g.
|
||||
`FORCE_XRPLD_VERSION=3.4.0`.
|
||||
|
||||
### Set Up Conan
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ endif()
|
||||
include(PatchNixBinary)
|
||||
|
||||
include(XrplSanity)
|
||||
include(XrplVersion)
|
||||
include(XrplSettings)
|
||||
# this check has to remain in the top-level cmake because of the early return statement
|
||||
if(packages_only)
|
||||
|
||||
606
CONTRIBUTING.md
606
CONTRIBUTING.md
@@ -14,9 +14,13 @@ The following branches exist in the main project repository:
|
||||
|
||||
- `develop`: The latest set of unreleased features, and the most common
|
||||
starting point for contributions.
|
||||
- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line,
|
||||
holding the latest release candidate, or stable release for that line.
|
||||
Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
|
||||
- `staging/*` (e.g. `staging/3.4.x`): Staging branches, one per release line,
|
||||
where fixes for that line are developed.
|
||||
- `release/*` (e.g. `release/3.4.x`): Release branches, one per release line,
|
||||
holding the latest public release candidate or release for that line.
|
||||
Releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
|
||||
|
||||
See [RELEASING.md](./RELEASING.md) for how these branches are used.
|
||||
|
||||
The tip of each branch must be signed. In order for GitHub to sign a
|
||||
squashed commit that it builds from your pull request, GitHub must know
|
||||
@@ -145,8 +149,8 @@ tl;dr
|
||||
|
||||
In general, pull requests use `develop` as the base branch.
|
||||
|
||||
The exceptions are fixes, improvements, and hotfixes for an existing release,
|
||||
which use that release's branch (e.g. `release/3.2.x`) as the base.
|
||||
The exceptions are fixes for an existing release line,
|
||||
which use that line's staging branch (e.g. `staging/3.4.x`) as the base.
|
||||
|
||||
If your changes are not quite ready, but you want to make it easily available
|
||||
for preliminary examination or review, you can create a "Draft" pull request.
|
||||
@@ -591,15 +595,16 @@ the suggested commit message, or modify it as needed.
|
||||
|
||||
#### Slightly more complicated pull requests
|
||||
|
||||
Some pull requests need to be pushed to `develop` as more than one
|
||||
commit. A PR author may _request_ to merge as separate commits. They
|
||||
Some pull requests need to be pushed to their base branch (usually `develop`)
|
||||
as more than one commit.
|
||||
A PR author may _request_ to merge as separate commits. They
|
||||
must _justify_ why separate commits are needed, and _specify_ how they
|
||||
would like the commits to be merged. If you disagree with the author,
|
||||
discuss it with them directly.
|
||||
|
||||
If the process is reasonable, follow it. The simplest option is to do a
|
||||
fast forward only merge (`--ff-only`) on the command line and push to
|
||||
`develop`.
|
||||
fast forward only merge (`--ff-only`) on the command line
|
||||
and push to the base branch.
|
||||
|
||||
Some examples of when separate commits are worthwhile are:
|
||||
|
||||
@@ -612,9 +617,10 @@ Some examples of when separate commits are worthwhile are:
|
||||
|
||||
Either way, check that:
|
||||
|
||||
- The commits are based on the current tip of `develop`.
|
||||
- The commits are clean: No merge commits (except when reverse
|
||||
merging), no "[FOLD]" or "fixup!" messages.
|
||||
- The commits are based on the current tip of the base branch.
|
||||
- The commits are clean:
|
||||
No merge commits (except when merging a release, see [RELEASING.md](./RELEASING.md)),
|
||||
no "[FOLD]" or "fixup!" messages.
|
||||
- All commits are signed. If the commits are not signed by the author, use
|
||||
`git commit --amend -S` to sign them yourself.
|
||||
- At least one (but preferably all) of the commits has the PR number
|
||||
@@ -626,578 +632,8 @@ use them!**
|
||||
|
||||
### Releases
|
||||
|
||||
All releases, including release candidates and betas, are handled
|
||||
differently from typical PRs. Most importantly, never use
|
||||
the Github UI to merge a release.
|
||||
|
||||
Xrpld uses a linear workflow model that can be summarized as:
|
||||
|
||||
1. In between releases, developers work against the `develop` branch.
|
||||
2. Periodically, a maintainer will build and tag a beta version from
|
||||
`develop`, which is pushed to `release`.
|
||||
- Betas are usually released every two to three weeks, though that
|
||||
schedule can vary depending on progress, availability, and other
|
||||
factors.
|
||||
3. When the changes in `develop` are considered stable and mature enough
|
||||
to be ready to release, a release candidate (RC) is built and tagged
|
||||
from `develop`, and merged to `release`.
|
||||
- Further development for that release (primarily fixes) then
|
||||
continues against `release`, while other development continues on
|
||||
`develop`. Effectively, `release` is forked from `develop`. Changes
|
||||
to `release` must be reverse merged to `develop`.
|
||||
4. When the candidate has passed testing and is ready for release, the
|
||||
final release is merged to `master`.
|
||||
5. If any issues are found post-release, a hotfix / point release may be
|
||||
created, which is merged to `master`, and then reverse merged to
|
||||
`develop`.
|
||||
|
||||
#### Betas, and the first release candidate
|
||||
|
||||
##### Preparing the `develop` branch
|
||||
|
||||
1. Optimally, the `develop` branch will be ready to go, with all
|
||||
relevant PRs already merged.
|
||||
2. If there are any PRs pending, merge them **BEFORE** preparing the beta.
|
||||
1. If only one or two PRs need to be merged, merge those PRs [as
|
||||
normal](#when-and-how-to-merge-pull-requests), updating the second
|
||||
one, and waiting for CI to finish in between.
|
||||
2. If there are several pending PRs, do not use the Github UI,
|
||||
because the delays waiting for CI in between each merge will be
|
||||
unnecessarily onerous. (Incidentally, this process can also be
|
||||
used to merge if the Github UI has issues.) Merge each PR branch
|
||||
directly to a `release-next` on your local machine and create a single
|
||||
PR, then push your branch to `develop`.
|
||||
1. Squash the changes from each PR, one commit each (unless more
|
||||
are needed), being sure to sign each commit and update the
|
||||
commit message to include the PR number. You may be able to use
|
||||
a fast-forward merge for the first PR.
|
||||
2. Push your branch.
|
||||
3. Continue to [Making the release](#making-the-release) to update
|
||||
the version number, etc.
|
||||
|
||||
The workflow may look something like:
|
||||
|
||||
```
|
||||
git fetch --multiple upstreams user1 user2 user3 [...]
|
||||
git checkout -B release-next --no-track upstream/develop
|
||||
|
||||
# Only do an ff-only merge if pr-branch1 is either already
|
||||
# squashed, or needs to be merged with separate commits,
|
||||
# and has no merge commits.
|
||||
# Use -S on the ff-only merge if pr-branch1 isn't signed.
|
||||
git merge [-S] --ff-only user1/pr-branch1
|
||||
|
||||
git merge --squash user2/pr-branch2
|
||||
git commit -S # Use the commit message provided on the PR
|
||||
|
||||
git merge --squash user3/pr-branch3
|
||||
git commit -S # Use the commit message provided on the PR
|
||||
|
||||
[...]
|
||||
|
||||
# Make sure the commits look right
|
||||
git log --show-signature "upstream/develop..HEAD"
|
||||
|
||||
git push --set-upstream origin
|
||||
|
||||
# Continue to "Making the release" to update the version number, so
|
||||
# everything can be done in one PR.
|
||||
```
|
||||
|
||||
You can also use the [squash-branches] script.
|
||||
|
||||
You may also need to manually close the open PRs after the changes are
|
||||
merged to `develop`. Be sure to include the commit ID.
|
||||
|
||||
##### Making the release
|
||||
|
||||
This includes, betas, and the first release candidate (RC).
|
||||
|
||||
1. If you didn't create one [preparing the `develop`
|
||||
branch](#preparing-the-develop-branch), Ensure there is no old
|
||||
`release-next` branch hanging around. Then make a `release-next`
|
||||
branch that only changes the version number. e.g.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
git checkout --no-track -B release-next upstream/develop
|
||||
|
||||
v="A.B.C-bD"
|
||||
build=$( find -name BuildInfo.cpp )
|
||||
sed 's/\(^.*versionString =\).*$/\1 "'${v}'"/' ${build} > version.cpp && mv -vi version.cpp ${build}
|
||||
|
||||
git diff
|
||||
|
||||
git add ${build}
|
||||
|
||||
git commit -S -m "Set version to ${v}"
|
||||
|
||||
# You could use your "origin" repo, but some CI tests work better on upstream.
|
||||
git push upstream-push
|
||||
git fetch upstreams
|
||||
git branch --set-upstream-to=upstream/release-next
|
||||
```
|
||||
|
||||
You can also use the [update-version] script. 2. Create a Pull Request for `release-next` with **`develop`** as
|
||||
the base branch.
|
||||
|
||||
1. Use the title "[TRIVIAL] Set version to X.X.X-bX".
|
||||
2. Instead of the default description template, use the following:
|
||||
|
||||
```
|
||||
## High Level Overview of Change
|
||||
|
||||
This PR only changes the version number. It will be merged as
|
||||
soon as Github CI actions successfully complete.
|
||||
```
|
||||
|
||||
3. Wait for CI to successfully complete, and get someone to approve
|
||||
the PR. (It is safe to ignore known CI issues.)
|
||||
4. Push the updated `develop` branch using your `release-next`
|
||||
branch. **Do not use the Github UI. It's important to preserve
|
||||
commit IDs.**
|
||||
|
||||
```
|
||||
git push upstream-push release-next:develop
|
||||
```
|
||||
|
||||
5. In the unlikely event that the push fails because someone has merged
|
||||
something else in the meantime, rebase your branch onto the updated
|
||||
`develop` branch, push again, and go back to step 3.
|
||||
6. Ensure that your PR against `develop` is closed. Github should do it
|
||||
automatically.
|
||||
7. Once this is done, forward progress on `develop` can continue
|
||||
(other PRs may be merged).
|
||||
8. Now create a Pull Request for `release-next` with **`release`** as
|
||||
the base branch. Instead of the default template, reuse and update
|
||||
the message from the previous release. Include the following verbiage
|
||||
somewhere in the description:
|
||||
|
||||
```
|
||||
The base branch is `release`. [All releases (including
|
||||
betas)](https://github.com/XRPLF/rippled/blob/develop/CONTRIBUTING.md#before-you-start)
|
||||
go in `release`. This PR branch will be pushed directly to `release` (not
|
||||
squashed or rebased, and not using the GitHub UI).
|
||||
```
|
||||
|
||||
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
|
||||
offline, but at least one approval will be needed on the PR.
|
||||
- If issues are discovered during testing, simply abandon the
|
||||
release. It's easy to start a new release, it should be easy to
|
||||
abandon one. **DO NOT REUSE THE VERSION NUMBER.** e.g. If you
|
||||
abandon 2.4.0-b1, the next attempt will be 2.4.0-b2.
|
||||
8. Once everything is ready to go, push to `release`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Just to be safe, do a dry run first:
|
||||
git push --dry-run upstream-push release-next:release
|
||||
|
||||
# If everything looks right, push the branch
|
||||
git push upstream-push release-next:release
|
||||
|
||||
# Check that all of the branches are updated
|
||||
git fetch upstreams
|
||||
git log -1 --oneline
|
||||
# The output should look like:
|
||||
# 0123456789 (HEAD -> upstream/release-next, upstream/release,
|
||||
# upstream/develop) Set version to 2.4.0-b1
|
||||
# Note that upstream/develop may not be on this commit, but
|
||||
# upstream/release must be.
|
||||
# Other branches, including some from upstream-push, may also be
|
||||
# present.
|
||||
```
|
||||
|
||||
9. Tag the release, too.
|
||||
|
||||
```
|
||||
git tag <version number>
|
||||
git push upstream-push <version number>
|
||||
```
|
||||
|
||||
10. Delete the `release-next` branch on the repo. Use the Github UI or:
|
||||
|
||||
```
|
||||
git push --delete upstream-push release-next
|
||||
```
|
||||
|
||||
11. Finally [create a new release on
|
||||
Github](https://github.com/XRPLF/rippled/releases).
|
||||
|
||||
#### Release candidates after the first
|
||||
|
||||
Once the first release candidate is [merged into
|
||||
release](#making-the-release), then `release` and `develop` _are allowed
|
||||
to diverge_.
|
||||
|
||||
If a bug or issue is discovered in a version that has a release
|
||||
candidate being tested, any fix and new version will need to be applied
|
||||
against `release`, then reverse-merged to `develop`. This helps keep git
|
||||
history as linear as possible.
|
||||
|
||||
A `release-next` branch will be created from `release`, and any further
|
||||
work for that release must be based on `release-next`. Specifically,
|
||||
PRs must use `release-next` as the base, and those PRs will be merged
|
||||
directly to `release-next` when approved. Changes should be restricted
|
||||
to bug fixes, but other changes may be necessary from time to time.
|
||||
|
||||
1. Open any PRs for the pending release using `release-next` as the base,
|
||||
so they can be merged directly in to it. Unlike `develop`, though,
|
||||
`release-next` can be thrown away and recreated if necessary.
|
||||
2. Once a new release candidate is ready, create a version commit as in
|
||||
step 1 [above](#making-the-release) on `release-next`. You can use
|
||||
the [update-version] script for this, too.
|
||||
3. Jump to step 8 ("Now create a Pull Request for `release-next` with
|
||||
**`release`** as the base") from the process
|
||||
[above](#making-the-release) to merge `release-next` into `release`.
|
||||
|
||||
##### Follow up: reverse merge
|
||||
|
||||
Once the RC is merged and tagged, it needs to be reverse merged into
|
||||
`develop` as soon as possible.
|
||||
|
||||
1. Create a branch, based on `upstream/develop`.
|
||||
The branch name is not important, but could include "mergeNNNrcN".
|
||||
E.g. For release A.B.C-rcD, use `mergeABCrcD`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
git checkout --no-track -b mergeABCrcD upstream/develop
|
||||
```
|
||||
|
||||
2. Merge `release` into your branch.
|
||||
|
||||
```
|
||||
# I like the "--edit --log --verbose" parameters, but they are
|
||||
# not required.
|
||||
git merge upstream/release
|
||||
```
|
||||
|
||||
3. `BuildInfo.cpp` will have a conflict with the version number.
|
||||
Resolve it with the version from `develop` - the higher version.
|
||||
4. Push your branch to your repo (or `upstream` if you have permission),
|
||||
and open a normal PR against `develop`. The "High level overview" can
|
||||
simply indicate that this is a merge of the RC. The "Context" should
|
||||
summarize the changes from the RC. Include the following text
|
||||
prominently:
|
||||
|
||||
```
|
||||
This PR must be merged manually using a push. Do not use the Github UI.
|
||||
```
|
||||
|
||||
5. Depending on the complexity of the changes, and/or merge conflicts,
|
||||
the PR may need a thorough review, or just a sign-off that the
|
||||
merge was done correctly.
|
||||
6. If `develop` is updated before this PR is merged, do not merge
|
||||
`develop` back into your branch. Instead rebase preserving merges,
|
||||
or do the merge again. (See also the `rerere` git config setting.)
|
||||
|
||||
```
|
||||
git rebase --rebase-merges upstream/develop
|
||||
# OR
|
||||
git reset --hard upstream/develop
|
||||
git merge upstream/release
|
||||
```
|
||||
|
||||
7. When the PR is ready, push it to `develop`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Make sure the commits look right
|
||||
git log --show-signature "upstream/develop^..HEAD"
|
||||
|
||||
git push upstream-push mergeABCrcD:develop
|
||||
|
||||
git fetch upstreams
|
||||
```
|
||||
|
||||
Development on `develop` can proceed as normal.
|
||||
|
||||
#### Final releases
|
||||
|
||||
A final release is any release that is not a beta or RC, such as 2.2.0.
|
||||
|
||||
Only code that has already been tested and vetted across all three
|
||||
platforms should be included in a final release. Most of the time, that
|
||||
means that the commit immediately preceding the commit setting the
|
||||
version number will be an RC. Occasionally, there may be last-minute bug
|
||||
fixes included as well. If so, those bug fixes must have been tested
|
||||
internally as if they were RCs (at minimum, ensuring unit tests pass,
|
||||
and the app starts, syncs, and stops cleanly across all three
|
||||
platforms.)
|
||||
|
||||
_If in doubt, make an RC first._
|
||||
|
||||
The process for building a final release is very similar to [the process
|
||||
for building a beta](#making-the-release), except the code will be
|
||||
moving from `release` to `master` instead of from `develop` to
|
||||
`release`, and both branches will be pushed at the same time.
|
||||
|
||||
1. Ensure there is no old `master-next` branch hanging around.
|
||||
Then make a `master-next` branch that only changes the version
|
||||
number. As above, or using the
|
||||
[update-version] script.
|
||||
2. Create a Pull Request for `master-next` with **`master`** as
|
||||
the base branch. Instead of the default template, reuse and update
|
||||
the message from the previous final release. Include the following verbiage
|
||||
somewhere in the description:
|
||||
|
||||
```
|
||||
The base branch is `master`. This PR branch will be pushed directly to
|
||||
`release` and `master` (not squashed or rebased, and not using the
|
||||
GitHub UI).
|
||||
```
|
||||
|
||||
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
|
||||
offline, but at least one approval will be needed on the PR.
|
||||
- If issues are discovered during testing, close the PR, delete
|
||||
`master-next`, and move development back to `release`, [issuing
|
||||
more RCs as necessary](#release-candidates-after-the-first)
|
||||
8. Once everything is ready to go, push to `release` and `master`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Just to be safe, do dry runs first:
|
||||
git push --dry-run upstream-push master-next:release
|
||||
git push --dry-run upstream-push master-next:master
|
||||
|
||||
# If everything looks right, push the branch
|
||||
git push upstream-push master-next:release
|
||||
git push upstream-push master-next:master
|
||||
|
||||
# Check that all of the branches are updated
|
||||
git fetch upstreams
|
||||
git log -1 --oneline
|
||||
# The output should look like:
|
||||
# 0123456789 (HEAD -> upstream/master-next, upstream/master,
|
||||
# upstream/release) Set version to A.B.0
|
||||
# Note that both upstream/release and upstream/master must be on this
|
||||
# commit.
|
||||
# Other branches, including some from upstream-push, may also be
|
||||
# present.
|
||||
```
|
||||
|
||||
9. Tag the release, too.
|
||||
|
||||
```
|
||||
git tag <version number>
|
||||
git push upstream-push <version number>
|
||||
```
|
||||
|
||||
10. Delete the `master-next` branch on the repo. Use the Github UI or:
|
||||
|
||||
```
|
||||
git push --delete upstream-push master-next
|
||||
```
|
||||
|
||||
11. [Create a new release on
|
||||
Github](https://github.com/XRPLF/rippled/releases). Be sure that
|
||||
"Set as the latest release" is checked.
|
||||
12. Open a PR to update the [API-CHANGELOG](API-CHANGELOG.md) and `API-VERSION-[n].md` with the changes for this release (if any are missing).
|
||||
13. Finally, [reverse merge the release into `develop`](#follow-up-reverse-merge).
|
||||
|
||||
#### Special cases: point releases, hotfixes, etc.
|
||||
|
||||
On occasion, a bug or issue is discovered in a version that already
|
||||
had a final release. Most of the time, development will have started
|
||||
on the next version, and will usually have changes in `develop`
|
||||
and often in `release`.
|
||||
|
||||
Because git history is kept as linear as possible, any fix and new
|
||||
version will need to be applied against `master`.
|
||||
|
||||
The process for building a hotfix release is very similar to [the
|
||||
process for building release candidates after the
|
||||
first](#release-candidates-after-the-first) and [for building a final
|
||||
release](#final-releases), except the changes will be done against
|
||||
`master` instead of `release`.
|
||||
|
||||
If there is only a single issue for the hotfix, the work can be done in
|
||||
any branch. When it's ready to merge, jump to step 3 using your branch
|
||||
instead of `master-next`.
|
||||
|
||||
1. Create a `master-next` branch from `master`.
|
||||
|
||||
```
|
||||
git checkout --no-track -b master-next upstream/master
|
||||
git push upstream-push
|
||||
git fetch upstreams
|
||||
```
|
||||
|
||||
2. Open any PRs for the pending hotfix using `master-next` as the base,
|
||||
so they can be merged directly in to it. Unlike `develop`, though,
|
||||
`master-next` can be thrown away and recreated if necessary.
|
||||
3. Once the hotfix is ready, create a version commit using the same
|
||||
steps as above, or use the
|
||||
[update-version] script.
|
||||
4. Create a Pull Request for `master-next` with **`master`** as
|
||||
the base branch. Instead of the default template, reuse and update
|
||||
the message from the previous final release. Include the following verbiage
|
||||
somewhere in the description:
|
||||
|
||||
```
|
||||
The base branch is `master`. This PR branch will be pushed directly to
|
||||
`master` (not squashed or rebased, and not using the GitHub UI).
|
||||
```
|
||||
|
||||
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
|
||||
offline, but at least one approval will be needed on the PR.
|
||||
- If issues are discovered during testing, update `master-next` as
|
||||
needed, but ensure that the changes are properly squashed, and the
|
||||
version setting commit remains last
|
||||
8. Once everything is ready to go, push to `master` **only**.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Just to be safe, do a dry run first:
|
||||
git push --dry-run upstream-push master-next:master
|
||||
|
||||
# If everything looks right, push the branch
|
||||
git push upstream-push master-next:master
|
||||
|
||||
# Check that all of the branches are updated
|
||||
git fetch upstreams
|
||||
git log -1 --oneline
|
||||
# The output should look like:
|
||||
# 0123456789 (HEAD -> upstream/master-next, upstream/master) Set version
|
||||
# to 2.4.1
|
||||
# Note that upstream/master must be on this commit. upstream/release and
|
||||
# upstream/develop should not.
|
||||
# Other branches, including some from upstream-push, may also be
|
||||
# present.
|
||||
```
|
||||
|
||||
9. Tag the release, too.
|
||||
|
||||
```
|
||||
git tag <version number>
|
||||
git push upstream-push <version number>
|
||||
```
|
||||
|
||||
9. Delete the `master-next` branch on the repo.
|
||||
|
||||
```
|
||||
git push --delete upstream-push master-next
|
||||
```
|
||||
|
||||
10. [Create a new release on
|
||||
Github](https://github.com/XRPLF/rippled/releases). Be sure that
|
||||
"Set as the latest release" is checked.
|
||||
|
||||
Once the hotfix is released, it needs to be reverse merged into
|
||||
`develop` as soon as possible. It may also need to be merged into
|
||||
`release` if a release candidate is under development.
|
||||
|
||||
1. Create a branch in your own repo, based on `upstream/develop`.
|
||||
The branch name is not important, but could include "mergeNNN".
|
||||
E.g. For release 2.2.3, use `merge223`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
git checkout --no-track -b merge223 upstream/develop
|
||||
```
|
||||
|
||||
2. Merge master into your branch.
|
||||
|
||||
```
|
||||
# I like the "--edit --log --verbose" parameters, but they are
|
||||
# not required.
|
||||
git merge upstream/master
|
||||
```
|
||||
|
||||
3. `BuildInfo.cpp` will have a conflict with the version number.
|
||||
Resolve it with the version from `develop` - the higher version.
|
||||
4. Push your branch to your repo, and open a normal PR against
|
||||
`develop`. The "High level overview" can simply indicate that this
|
||||
is a merge of the hotfix version. The "Context" should summarize
|
||||
the changes from the hotfix. Include the following text
|
||||
prominently:
|
||||
|
||||
```
|
||||
This PR must be merged manually using a --ff-only merge. Do not use the Github UI.
|
||||
```
|
||||
|
||||
5. Depending on the complexity of the hotfix, and/or merge conflicts,
|
||||
the PR may need a thorough review, or just a sign-off that the
|
||||
merge was done correctly.
|
||||
6. If `develop` is updated before this PR is merged, do not merge
|
||||
`develop` back into your branch. Instead rebase preserving merges,
|
||||
or do the merge again. (See also the `rerere` git config setting.)
|
||||
|
||||
```
|
||||
git rebase --rebase-merges upstream/develop
|
||||
# OR
|
||||
git reset --hard upstream/develop
|
||||
git merge upstream/master
|
||||
```
|
||||
|
||||
7. When the PR is ready, push it to `develop`.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Make sure the commits look right
|
||||
git log --show-signature "upstream/develop..HEAD"
|
||||
|
||||
git push upstream-push HEAD:develop
|
||||
```
|
||||
|
||||
Development on `develop` can proceed as normal. It is recommended to
|
||||
create a beta (or RC) immediately to ensure that everything worked as
|
||||
expected.
|
||||
|
||||
##### An even rarer scenario: A hotfix on an old release
|
||||
|
||||
Historically, once a final release is tagged and packages are released,
|
||||
versions older than the latest final release are no longer supported.
|
||||
However, there is a possibility that a very high severity bug may occur
|
||||
in a non-amendment blocked version that is still being run by
|
||||
a significant fraction of users, which would necessitate a hotfix / point
|
||||
release to that version as well as any later versions.
|
||||
|
||||
This scenario would follow the same basic procedure as above,
|
||||
except that _none_ of `develop`, `release`, or `master`
|
||||
would be touched during the release process.
|
||||
|
||||
In this example, consider if version 2.1.1 needed to be patched.
|
||||
|
||||
1. Create two branches in the main (`upstream`) repo.
|
||||
|
||||
```
|
||||
git fetch upstreams
|
||||
|
||||
# Create a base branch off the tag
|
||||
git checkout --no-track -b master-2.1.2 2.1.1
|
||||
git push upstream-push
|
||||
|
||||
# Create a working branch
|
||||
git checkout --no-track -b master212-next master-2.1.2
|
||||
git push upstream-push
|
||||
|
||||
git fetch upstreams
|
||||
```
|
||||
|
||||
2. Work continues as above, except using `master-2.1.2`as
|
||||
the base branch for any merging, packaging, etc.
|
||||
3. After the release is tagged and packages are built, you could
|
||||
potentially delete both branches, e.g. `master-2.1.2` and
|
||||
`master212-next`. However, it may be useful to keep `master-2.1.2`
|
||||
around indefinitely for reference.
|
||||
4. Assuming that a hotfix is also released for the latest
|
||||
version in parallel with this one, or if the issue is
|
||||
already fixed in the latest version, do no do any
|
||||
reverse merges. However, if it is not, it probably makes
|
||||
sense to reverse merge `master-2.1.2` into `master`,
|
||||
release a hotfix for _that_ version, then reverse merge
|
||||
from `master` to `develop`. (Please don't do this unless absolutely
|
||||
necessary.)
|
||||
Releases, release branches, and merging releases back into `develop`
|
||||
are described in [RELEASING.md](./RELEASING.md).
|
||||
|
||||
[contrib]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
|
||||
[squash]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits
|
||||
@@ -1205,5 +641,3 @@ git fetch upstreams
|
||||
[xrpld]: https://github.com/XRPLF/rippled
|
||||
[signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification
|
||||
[setup-upstreams]: ./bin/git/setup-upstreams.sh
|
||||
[squash-branches]: ./bin/git/squash-branches.sh
|
||||
[update-version]: ./bin/git/update-version.sh
|
||||
|
||||
152
RELEASING.md
Normal file
152
RELEASING.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Branching and Release Management
|
||||
|
||||
This document describes how we branch, release, and merge releases back into `develop`.
|
||||
It does not define version naming
|
||||
(e.g., what constitutes a major-minor, patch, or beta release).
|
||||
|
||||
Examples use `X.Y` for a release line:
|
||||
`X` and `Y` are placeholders, while the trailing `x` is literal,
|
||||
e.g., `release/X.Y.x` is `release/3.4.x` for the `3.4` line.
|
||||
|
||||
## Branches
|
||||
|
||||
| Branch | Purpose |
|
||||
| :-------------- | :---------------------------------------------------------------------------------------- |
|
||||
| `develop` | Main development branch. Betas and the first RC of a major-minor release are tagged here. |
|
||||
| `staging/X.Y.x` | Where fixes for the `X.Y` line are developed and RCs are prepared. |
|
||||
| `release/X.Y.x` | The last public RC or release of the `X.Y` line. |
|
||||
|
||||
A release line is named `X.Y.x`
|
||||
because one branch serves every patch release of that line (`X.Y.0`, `X.Y.1`, `X.Y.2`, ...).
|
||||
The `/` groups branches hierarchically,
|
||||
so tools can filter them and protection rules can target `release/*` and `staging/*`.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Releases are merged back into `develop`, never cherry-picked or rebased onto it.**
|
||||
Cherry-picked and rebased commits get new hashes,
|
||||
so Git can't tell that `develop` already has them.
|
||||
Future merges then replay them and produce artificial conflicts,
|
||||
and it's hard to verify that every fix actually reached `develop`.
|
||||
Merging keeps a single history:
|
||||
Git knows exactly which release commits `develop` contains,
|
||||
and no fix is left behind.
|
||||
- **Branches only move forward.**
|
||||
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
|
||||
- **Cherry-picking only goes from `develop` to a staging branch**,
|
||||
for fixes that must get into a release after the code freeze
|
||||
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
|
||||
- **Security fixes are prepared privately and published with the release that contains them**,
|
||||
so vulnerabilities are not disclosed prematurely.
|
||||
|
||||
## Release Lifecycle
|
||||
|
||||
This diagram shows a major-minor release and its first patch release.
|
||||
The staging and release branches are drawn as one line.
|
||||
|
||||
```text
|
||||
develop staging/X.Y.x & release/X.Y.x
|
||||
│
|
||||
├── Tag: X.Y.0-b1
|
||||
├── Tag: X.Y.0-bN
|
||||
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
|
||||
│ │
|
||||
│ (development continues) ├── Fixes
|
||||
│ ├── Tag: X.Y.0-rcN
|
||||
│ ├── Tag: X.Y.0 (final)
|
||||
◀──── (merge) ────────────────────┤
|
||||
│ ├── Fixes
|
||||
│ ├── Tag: X.Y.1-rcN
|
||||
│ ├── Tag: X.Y.1 (final)
|
||||
◀──── (merge) ────────────────────┤
|
||||
│
|
||||
▼
|
||||
```
|
||||
|
||||
### Betas, First RC & Branching
|
||||
|
||||
> [!NOTE]
|
||||
> This phase applies only to a new major-minor release (e.g., `X.Y.0`).
|
||||
> Patch releases work on the existing branches of the line.
|
||||
|
||||
1. **Betas:** All beta versions (e.g., `X.Y.0-b1`) are built and tagged directly on `develop`.
|
||||
2. **First RC:** We release the first RC (`X.Y.0-rc1`)
|
||||
once everything that should be included in the release has been merged.
|
||||
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
|
||||
at the commit tagged `X.Y.0-rc1`.
|
||||
The first RC also kicks off the QE process.
|
||||
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
|
||||
into `staging/X.Y.x` or `release/X.Y.x`.
|
||||
Only critical stabilization fixes go into the line.
|
||||
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
|
||||
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
|
||||
are not merged into `develop`,
|
||||
so that fixes on the line and the merge back don't run into conflicts.
|
||||
|
||||
### Release Candidates
|
||||
|
||||
1. Fixes are developed against `staging/X.Y.x`.
|
||||
2. When ready, a new RC is created on `staging/X.Y.x`,
|
||||
and `release/X.Y.x` is fast-forwarded to it.
|
||||
|
||||
RCs that contain unpublished security fixes are not published,
|
||||
and don't touch the public branches.
|
||||
|
||||
RCs are not merged back into `develop`:
|
||||
`X.Y.0-rc1` is tagged on `develop` itself,
|
||||
and all later changes reach `develop` with the [final release](#final-release).
|
||||
|
||||
### Final Release
|
||||
|
||||
Security fixes become public as soon as they reach the public repo,
|
||||
so these steps happen only once the release is ready to be published,
|
||||
one right after the other.
|
||||
|
||||
1. Unpublished security fixes, if any, are merged into `staging/X.Y.x`.
|
||||
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
|
||||
and the release is tagged on it.
|
||||
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
|
||||
For a major-minor release, this lifts the freeze on large changes in `develop`.
|
||||
|
||||
### Merging Back Into `develop`
|
||||
|
||||
The merge back is a regular PR into `develop`
|
||||
whose branch contains a real merge commit of the release tag:
|
||||
|
||||
1. Create a branch from `develop`, run `git merge --no-ff <tag>`, and resolve any conflicts.
|
||||
2. Once the PR is approved, `develop` is fast-forwarded to the PR branch,
|
||||
so the merge commit lands as it is.
|
||||
Never squash or rebase it.
|
||||
Never add it to the merge queue either: the queue squashes PRs, which would drop the merge commit.
|
||||
3. If `develop` moves while the PR is open, redo the merge on top of the new `develop`.
|
||||
|
||||
After the merge, `git log develop..<tag>` must be empty.
|
||||
|
||||
## Special Cases
|
||||
|
||||
### Emergency Fixes From `develop`
|
||||
|
||||
If a commit was merged to `develop`
|
||||
and needs to be included in a release after the code freeze:
|
||||
|
||||
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
|
||||
2. Leave `develop` as it is, with no reverts.
|
||||
3. Follow the [release candidates](#release-candidates) process as usual.
|
||||
|
||||
When the release is later merged back into `develop`,
|
||||
both sides already contain the same change,
|
||||
so the merge usually resolves it cleanly or with a trivial conflict.
|
||||
From Git's perspective, the cherry-picked commit then becomes part of `develop` too.
|
||||
|
||||
### Several Supported Lines
|
||||
|
||||
When a fix must ship in more than one supported line (e.g., `X.Y` and `X.(Y+1)`),
|
||||
the lines are merged upwards rather than cherry-picked between:
|
||||
|
||||
1. The fix goes into the oldest line first.
|
||||
2. After that line's release,
|
||||
its `release/X.Y.x` is merged into the staging branch of the next newer line,
|
||||
and so on up to the newest line.
|
||||
3. The newest line is merged back into `develop` as usual.
|
||||
|
||||
This way every newer line, and eventually `develop`, contains the history of the older lines.
|
||||
@@ -24,9 +24,13 @@
|
||||
# `g++-15`, ...) are probed under both names: a suffixed name can break while
|
||||
# the plain one still works (see mkVersionedToolLinks in nix/packages.nix).
|
||||
#
|
||||
# Tools scoped to a single dev shell rather than to commonPackages are checked
|
||||
# only in that shell, keyed off XRPL_DEVSHELL.
|
||||
#
|
||||
# Environment variables:
|
||||
# CI if set, skip the tools above when on macOS.
|
||||
# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check.
|
||||
# XRPL_DEVSHELL active dev shell; selects shell-specific tools.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
@@ -163,6 +167,14 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
|
||||
check rustfmt
|
||||
fi
|
||||
|
||||
# Lean4 is in the formal-verification shell only, not in commonPackages.
|
||||
if [ "${XRPL_DEVSHELL:-}" = "formal-verification" ]; then
|
||||
echo
|
||||
echo "Formal verification toolchain:"
|
||||
check lean
|
||||
check lake
|
||||
fi
|
||||
|
||||
# GCC is the default compiler on Linux. macOS uses the system Apple Clang
|
||||
# instead, so GCC/g++/gcov are not expected there.
|
||||
if [ "${os}" = "linux" ]; then
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $# -ne 3 || "$1" == "--help" || "$1" = "-h" ]]; then
|
||||
name=$(basename $0)
|
||||
cat <<-USAGE
|
||||
Usage: $name workbranch base/branch version
|
||||
|
||||
* workbranch will be created locally from base/branch. If it exists,
|
||||
it will be reused, so make sure you don't overwrite any work.
|
||||
* base/branch may be specified as user:branch to allow easy copying
|
||||
from Github PRs.
|
||||
USAGE
|
||||
exit 0
|
||||
fi
|
||||
|
||||
work="$1"
|
||||
shift
|
||||
|
||||
base=$(echo "$1" | sed "s/:/\//")
|
||||
shift
|
||||
|
||||
version=$1
|
||||
shift
|
||||
|
||||
set -e
|
||||
|
||||
git fetch upstreams
|
||||
|
||||
git checkout -B "${work}" --no-track "${base}"
|
||||
|
||||
push=$(git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \
|
||||
2>/dev/null) || true
|
||||
if [[ "${push}" != "" ]]; then
|
||||
echo "Warning: ${push} may already exist."
|
||||
fi
|
||||
|
||||
build=$(find -name BuildInfo.cpp)
|
||||
sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} >version.cpp &&
|
||||
diff "${build}" version.cpp && exit 1 ||
|
||||
mv -vi version.cpp ${build}
|
||||
|
||||
git diff
|
||||
|
||||
git add ${build}
|
||||
|
||||
git commit -S -m "Set version to ${version}"
|
||||
|
||||
git log --oneline --first-parent ${base}^..
|
||||
|
||||
cat <<PUSH
|
||||
|
||||
-------------------------------------------------------------------
|
||||
This script will not push. Verify everything is correct, then push
|
||||
to your repo, and create a PR as described in CONTRIBUTING.md.
|
||||
-------------------------------------------------------------------
|
||||
PUSH
|
||||
@@ -1,28 +0,0 @@
|
||||
include_guard()
|
||||
|
||||
set(GIT_BUILD_BRANCH "")
|
||||
set(GIT_COMMIT_HASH "")
|
||||
|
||||
find_package(Git)
|
||||
if(NOT Git_FOUND)
|
||||
message(WARNING "Git not found. Git branch and commit hash will be empty.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(GIT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.git)
|
||||
|
||||
execute_process(
|
||||
COMMAND
|
||||
${GIT_EXECUTABLE} --git-dir=${GIT_DIRECTORY} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
OUTPUT_VARIABLE GIT_BUILD_BRANCH
|
||||
)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} --git-dir=${GIT_DIRECTORY} rev-parse HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
)
|
||||
|
||||
message(STATUS "Git branch: ${GIT_BUILD_BRANCH}")
|
||||
message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
|
||||
@@ -81,7 +81,7 @@ include(target_link_modules)
|
||||
add_module(xrpl beast)
|
||||
target_link_libraries(xrpl.libxrpl.beast PUBLIC xrpl.imports.main)
|
||||
|
||||
include(GitInfo)
|
||||
include(XrplVersion)
|
||||
add_module(xrpl git)
|
||||
target_compile_definitions(
|
||||
xrpl.libxrpl.git
|
||||
@@ -111,6 +111,11 @@ target_link_libraries(
|
||||
xrpl.libxrpl.protocol
|
||||
PUBLIC xrpl.libxrpl.crypto xrpl.libxrpl.git xrpl.libxrpl.json
|
||||
)
|
||||
# Only on BuildInfo.cpp, so a new version does not rebuild the whole module.
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/libxrpl/protocol/BuildInfo.cpp
|
||||
PROPERTIES COMPILE_DEFINITIONS XRPLD_VERSION="${XRPLD_VERSION}"
|
||||
)
|
||||
|
||||
# Level 05
|
||||
add_module(xrpl protocol_autogen)
|
||||
|
||||
@@ -1,15 +1,75 @@
|
||||
#[===================================================================[
|
||||
read version from source
|
||||
#]===================================================================]
|
||||
find_package(Git)
|
||||
|
||||
file(STRINGS src/libxrpl/protocol/BuildInfo.cpp BUILD_INFO)
|
||||
foreach(line_ ${BUILD_INFO})
|
||||
if(line_ MATCHES "versionString[ ]*=[ ]*\"(.+)\"")
|
||||
set(xrpld_version ${CMAKE_MATCH_1})
|
||||
endif()
|
||||
endforeach()
|
||||
if(xrpld_version)
|
||||
message(STATUS "xrpld version: ${xrpld_version}")
|
||||
else()
|
||||
message(FATAL_ERROR "unable to determine xrpld version")
|
||||
set(GIT_BUILD_BRANCH "")
|
||||
set(GIT_COMMIT_HASH "")
|
||||
|
||||
if(DEFINED ENV{GITHUB_BRANCH_NAME})
|
||||
set(GIT_BUILD_BRANCH $ENV{GITHUB_BRANCH_NAME})
|
||||
set(GIT_COMMIT_HASH $ENV{GITHUB_HEAD_SHA})
|
||||
elseif(Git_FOUND AND EXISTS "${CMAKE_CURRENT_LIST_DIR}/../.git")
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/..
|
||||
OUTPUT_VARIABLE GIT_BUILD_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/..
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
endif()
|
||||
|
||||
message(STATUS "Git branch: ${GIT_BUILD_BRANCH}")
|
||||
message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
|
||||
|
||||
if(
|
||||
DEFINED ENV{FORCE_XRPLD_VERSION}
|
||||
AND NOT "$ENV{FORCE_XRPLD_VERSION}" STREQUAL ""
|
||||
)
|
||||
message(
|
||||
STATUS
|
||||
"Using explicitly provided '$ENV{FORCE_XRPLD_VERSION}' as xrpld version"
|
||||
)
|
||||
|
||||
set(XRPLD_VERSION "$ENV{FORCE_XRPLD_VERSION}")
|
||||
|
||||
# The rules beast::SemanticVersion::parse applies, so that an invalid version
|
||||
# fails here rather than when xrpld starts. It reads each number as an int.
|
||||
set(SEMVER_NUMBER "(0|[1-9][0-9]*)")
|
||||
set(SEMVER_PRE_RELEASE "[A-Za-z1-9-][A-Za-z0-9-]*")
|
||||
set(SEMVER_METADATA "[A-Za-z0-9-]+")
|
||||
set(SEMVER_NUMBER_MAX 2147483647)
|
||||
if(
|
||||
NOT XRPLD_VERSION
|
||||
MATCHES
|
||||
"^${SEMVER_NUMBER}\\.${SEMVER_NUMBER}\\.${SEMVER_NUMBER}(-${SEMVER_PRE_RELEASE}(\\.${SEMVER_PRE_RELEASE})*)?(\\+${SEMVER_METADATA}(\\.${SEMVER_METADATA})*)?$"
|
||||
OR CMAKE_MATCH_1 GREATER SEMVER_NUMBER_MAX
|
||||
OR CMAKE_MATCH_2 GREATER SEMVER_NUMBER_MAX
|
||||
OR CMAKE_MATCH_3 GREATER SEMVER_NUMBER_MAX
|
||||
)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"FORCE_XRPLD_VERSION '${XRPLD_VERSION}' is not a semantic version xrpld accepts, see https://semver.org"
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Using '0.0.0-dev+<git short rev>' as xrpld version")
|
||||
|
||||
if(GIT_COMMIT_HASH STREQUAL "")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Unable to determine xrpld version without git, set FORCE_XRPLD_VERSION"
|
||||
)
|
||||
endif()
|
||||
|
||||
string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH_SHORT)
|
||||
|
||||
set(XRPLD_VERSION "0.0.0-dev+${GIT_COMMIT_HASH_SHORT}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Build version: ${XRPLD_VERSION}")
|
||||
|
||||
23
conanfile.py
23
conanfile.py
@@ -1,9 +1,13 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
|
||||
from conan.tools.env import Environment
|
||||
|
||||
from conan import ConanFile
|
||||
|
||||
DEV_VERSION = "0.0.0-dev"
|
||||
|
||||
|
||||
class Xrpl(ConanFile):
|
||||
name = "xrpl"
|
||||
@@ -120,13 +124,7 @@ class Xrpl(ConanFile):
|
||||
}
|
||||
|
||||
def set_version(self):
|
||||
if self.version is None:
|
||||
path = f"{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp"
|
||||
regex = r"versionString\s?=\s?\"(.*)\""
|
||||
with open(path, encoding="utf-8") as file:
|
||||
matches = (re.search(regex, line) for line in file)
|
||||
match = next(m for m in matches if m)
|
||||
self.version = match.group(1)
|
||||
self.version = self.version or DEV_VERSION
|
||||
|
||||
def configure(self):
|
||||
if self.settings.compiler == "apple-clang":
|
||||
@@ -169,6 +167,17 @@ class Xrpl(ConanFile):
|
||||
generators = "CMakeDeps"
|
||||
|
||||
def generate(self):
|
||||
# The sources in the Conan cache have no git history, so the version
|
||||
# comes from the reference, unless it is not one, like 'develop'.
|
||||
if not os.path.exists(os.path.join(self.source_folder, ".git")):
|
||||
version = str(self.version)
|
||||
env = Environment()
|
||||
env.define(
|
||||
"FORCE_XRPLD_VERSION",
|
||||
version if re.match(r"\d+\.\d+\.\d+", version) else DEV_VERSION,
|
||||
)
|
||||
env.vars(self).save_script("xrpld_version")
|
||||
|
||||
tc = CMakeToolchain(self)
|
||||
tc.variables["tests"] = self.options.tests
|
||||
tc.variables["benchmark"] = self.options.benchmark
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
the `xrpld` DEB package installed on Ubuntu 26.04, running as the `xrpld` user.
|
||||
Each release is tagged with its version, `xrplf/xrpld:<version>`, and
|
||||
`xrplf/xrpld:develop` follows the `develop` branch.
|
||||
See [`package/README.md`](../package/README.md#docker-image) for how it is built
|
||||
See [`package/README.md`](../package/README.md#docker-images) for how it is built
|
||||
and tagged.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -110,10 +110,10 @@ static_assert(
|
||||
*
|
||||
* However, it does not have sufficient precision to represent the full integer
|
||||
* range of int64_t values (-2^63 to 2^63-1), which are needed for XRP and MPT
|
||||
* values. The implementation of SingleAssetVault, and LendingProtocol need to
|
||||
* represent those integer values accurately and precisely, both for the
|
||||
* STNumber field type, and for internal calculations. That necessitated the
|
||||
* "large" scale.
|
||||
* values. The implementation of SingleAssetVault, LendingProtocol, and
|
||||
* MPTokensV2 need to represent those integer values accurately and precisely,
|
||||
* both for the STNumber field type, and for internal calculations. That
|
||||
* necessitated the "large" scale.
|
||||
*
|
||||
* The "Large" scales are intended to represent all values that can be represented
|
||||
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
|
||||
@@ -134,8 +134,8 @@ struct MantissaRange final
|
||||
// NOLINTBEGIN(readability-enum-initial-value)
|
||||
// The values don't matter, except for Large
|
||||
enum class MantissaScale {
|
||||
// Small can be removed when either featureSingleAssetVault or featureLendingProtocol are
|
||||
// retired
|
||||
// Small can be removed when any of featureSingleAssetVault, featureLendingProtocol, or
|
||||
// featureMPTokensV2 are retired
|
||||
Small,
|
||||
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
|
||||
LargeLegacy,
|
||||
@@ -311,10 +311,10 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
|
||||
*
|
||||
* The mantissa range may be changed at runtime via setMantissaScale(). The
|
||||
* default mantissa range is "large". The range is updated whenever transaction
|
||||
* processing begins, based on whether SingleAssetVault or LendingProtocol are
|
||||
* enabled. If either is enabled, the mantissa range is set to "large". If not,
|
||||
* it is set to "small", preserving backward compatibility and correct
|
||||
* "amendment-gating".
|
||||
* processing begins, based on whether SingleAssetVault, LendingProtocol, or
|
||||
* MPTokensV2 are enabled. If any is enabled, the mantissa range is set to
|
||||
* "large". If not, it is set to "small", preserving backward compatibility and
|
||||
* correct "amendment-gating".
|
||||
*
|
||||
* It is extremely unlikely that any more calls to setMantissaScale() will be
|
||||
* needed outside of unit tests.
|
||||
@@ -344,8 +344,8 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
|
||||
* set/getMantissaScale() functions may be most appropriate. However, if the
|
||||
* test has anything to do with transaction processing, it should enable or
|
||||
* disable the amendments that control the mantissa range choice
|
||||
* (SingleAssetVault and LendingProtocol), and/or check if either of those
|
||||
* amendments are enabled to determine which result to expect.
|
||||
* (SingleAssetVault, LendingProtocol, and MPTokensV2), and/or check if any of
|
||||
* those amendments are enabled to determine which result to expect.
|
||||
*/
|
||||
class Number final
|
||||
{
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -31,9 +31,9 @@ namespace xrpl {
|
||||
/**
|
||||
* Broker cover preclaim precision guard (fixCleanup3_2_0).
|
||||
*
|
||||
* Prevents a "silent sub-ULP no-op" where a deposit, withdrawal, or clawback
|
||||
* amount is so small that it rounds to zero at `sfCoverAvailable`'s scale.
|
||||
* Without this guard, both the pseudo trust-line and `sfCoverAvailable` would
|
||||
* Prevents a silent sub-ULP no-op where a deposit, withdrawal, or clawback
|
||||
* amount is so small that it rounds to zero at sfCoverAvailable's scale.
|
||||
* Without this guard, both the pseudo trust-line and sfCoverAvailable would
|
||||
* identically absorb the rounded zero, resulting in a successful transaction
|
||||
* (tesSUCCESS) where no funds actually moved.
|
||||
*
|
||||
@@ -44,8 +44,8 @@ namespace xrpl {
|
||||
* @param j Journal for logging.
|
||||
* @param logPrefix Transactor name for log diagnostics.
|
||||
*
|
||||
* @return `tecPRECISION_LOSS` if the request rounds to zero at cover scale.
|
||||
* `tesSUCCESS` if the amendment is disabled or the request is safely supra-ULP.
|
||||
* @return tecPRECISION_LOSS if the request rounds to zero at cover scale.
|
||||
* tesSUCCESS if the amendment is disabled or the request is safely supra-ULP.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canApplyToBrokerCover(
|
||||
@@ -56,6 +56,82 @@ canApplyToBrokerCover(
|
||||
beast::Journal j,
|
||||
std::string_view logPrefix);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return a LoanBroker's posterior live cover exponent after applying an
|
||||
* unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta);
|
||||
|
||||
/**
|
||||
* Round a cover outflow delta (cover withdraw, cover clawback, the
|
||||
* broker-side decrease of a default's cover credit) at the LoanBroker's
|
||||
* posterior live exponent. Safe to round the delta directly for an outflow:
|
||||
* amount on the posterior grid of CoverAvailable always leaves CoverAvailable
|
||||
* minus amount representable, since the posterior grid is exactly the grid
|
||||
* CoverAvailable itself will canonicalize to after the subtraction.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a cover debit (cover withdraw, cover clawback, the broker-side
|
||||
* decrease of a default's cover credit): amount is the non-negative
|
||||
* magnitude leaving CoverAvailable. Negates so the posterior is computed for
|
||||
* CoverAvailable minus amount, then negates the result back to a
|
||||
* non-negative magnitude, per roundToPosteriorBrokerCoverScale's outflow
|
||||
* rationale above. Magnitude in, magnitude out -- unlike the vault-side
|
||||
* outflow dispatcher (clampVaultOutflow), which takes and returns a negative
|
||||
* delta; callers on each side already hold the value in that side's native
|
||||
* form.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
debitToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Round a cover inflow (cover deposit, LoanPay's fee redirect into cover)
|
||||
* into CoverAvailable by flooring the sum rather than the delta:
|
||||
* credit = floor16(CoverAvailable + raw) - CoverAvailable, never finer
|
||||
* than the Vault's base exponent (-Scale). See
|
||||
* creditToPosteriorAvailableScale's doc for why the sum, not the delta,
|
||||
* must be floored; the same reasoning applies to CoverAvailable.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional cover inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. A LoanBroker attached to a
|
||||
* FixedPrecision Vault must remain at the Vault's base scale after applying
|
||||
* the rounded amount, and its posterior CoverAvailable must stay within the
|
||||
* Open zone.
|
||||
*
|
||||
* LoanPay's fee redirect into cover is a mandatory inflow and does not call
|
||||
* this check, so it can push CoverAvailable past the Open zone limit this
|
||||
* check enforces. Once that happens, every subsequent LoanBrokerCoverDeposit
|
||||
* returns tecLIMIT_EXCEEDED until the owner withdraws cover back under the
|
||||
* limit; LoanBrokerCoverWithdraw and LoanBrokerCoverClawback are unaffected.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount);
|
||||
|
||||
// Lending protocol has dependencies, so capture them here.
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
|
||||
@@ -64,13 +140,13 @@ checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
|
||||
* The accounts and asset that LoanManage::defaultLoan's fixCleanup3_4_0
|
||||
* freeze/lock exemption applies to.
|
||||
*
|
||||
* `defaultLoan` moves funds from the LoanBroker pseudo-account to the Vault
|
||||
* pseudo-account via `accountSend`. Since neither is the vault asset's
|
||||
* defaultLoan moves funds from the LoanBroker pseudo-account to the Vault
|
||||
* pseudo-account via accountSend. Since neither is the vault asset's
|
||||
* issuer, this is a third-party transfer that transits through the issuer in
|
||||
* two hops (broker -> issuer, issuer -> vault; see
|
||||
* `directSendNoLimitIOU`/`directSendNoLimitMPT`), so the exemption must cover
|
||||
* directSendNoLimitIOU/directSendNoLimitMPT), so the exemption must cover
|
||||
* both the issuer/broker and issuer/vault pairs, not a direct broker/vault
|
||||
* pair. `asset` scopes it further to the vault's own currency/MPT issuance,
|
||||
* pair. asset scopes it further to the vault's own currency/MPT issuance,
|
||||
* so an unrelated one the same accounts happen to hold is still protected.
|
||||
*/
|
||||
struct LoanDefaultFreezeExemptAccounts
|
||||
@@ -88,10 +164,10 @@ struct LoanDefaultFreezeExemptAccounts
|
||||
* @param view Ledger view used to resolve the Loan -> LoanBroker -> Vault
|
||||
* chain.
|
||||
* @param tx The transaction under invariant review.
|
||||
* @return The exempt accounts and asset if `tx` is a `ttLOAN_MANAGE`
|
||||
* transaction with the `tfLoanDefault` flag set, `fixCleanup3_4_0` is
|
||||
* @return The exempt accounts and asset if tx is a ttLOAN_MANAGE
|
||||
* transaction with the tfLoanDefault flag set, fixCleanup3_4_0 is
|
||||
* enabled, and the loan/broker/vault objects it references can all be
|
||||
* resolved; `std::nullopt` otherwise.
|
||||
* resolved; std::nullopt otherwise.
|
||||
*/
|
||||
[[nodiscard]] std::optional<LoanDefaultFreezeExemptAccounts>
|
||||
getLoanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx);
|
||||
@@ -254,28 +330,38 @@ adjustImpreciseNumber(
|
||||
value = 0;
|
||||
}
|
||||
|
||||
inline int
|
||||
getAssetsTotalScale(SLE::ConstRef vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::kMinExponent - 1; // LCOV_EXCL_LINE
|
||||
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
|
||||
}
|
||||
/**
|
||||
* Apply a signed delta to a LoanBroker's DebtTotal: on FixedPrecision Vaults,
|
||||
* add the exact delta (DebtTotal moves by exactly the same amount as the
|
||||
* Vault's AssetsDeployed, never re-rounded); on Legacy/CashBasis Vaults,
|
||||
* round through adjustImpreciseNumber at vaultScale. Shared by LoanSet
|
||||
* (origination) and LoanPay (payment, where delta is negative).
|
||||
*
|
||||
* @param brokerSle The LoanBroker whose DebtTotal is adjusted.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @param delta The signed change to DebtTotal.
|
||||
* @param vaultScale The Vault scale the caller captured before changing
|
||||
* AssetsTotal. Legacy/CashBasis round DebtTotal at that scale, not at
|
||||
* the scale after this transaction's AssetsTotal change. Unused on
|
||||
* FixedPrecision Vaults.
|
||||
*/
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::Ref brokerSle,
|
||||
SLE::ConstRef vaultSle,
|
||||
Number const& delta,
|
||||
int vaultScale);
|
||||
|
||||
// Compute the minimum required broker cover, rounded consistently.
|
||||
// DebtTotal is a broker-level aggregate maintained at vault scale, so the
|
||||
// rounding must also use vault scale — never an individual loan's scale.
|
||||
inline Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getAssetsTotalScale(vaultSle));
|
||||
}
|
||||
/**
|
||||
* Minimum required broker cover, rounded up.
|
||||
*
|
||||
* DebtTotal is a broker-level aggregate, never rounded at an individual
|
||||
* loan's scale. Legacy and CashBasis Vaults round at the live AssetsTotal
|
||||
* exponent. FixedPrecision Vaults round at the Vault's base exponent
|
||||
* (-Scale, or 0 for integral assets).
|
||||
*/
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle);
|
||||
|
||||
TER
|
||||
checkLoanGuards(
|
||||
@@ -379,10 +465,10 @@ loanPaymentDeltas(LoanPaymentParts const& parts);
|
||||
|
||||
} // namespace cash_basis
|
||||
|
||||
// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is
|
||||
// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is
|
||||
// VaultVersion::CashBasis, else instant_recognition::. These are the only entry points
|
||||
// transactors call.
|
||||
// Public dispatchers: pick cash_basis:: if the Vault's LEVersion
|
||||
// (VaultHelpers::getVaultVersion) is CashBasis or later (CashBasis or
|
||||
// FixedPrecision), else instant_recognition::. These are the only entry
|
||||
// points transactors call.
|
||||
AccountingDeltas
|
||||
loanOriginationDeltas(
|
||||
SLE::ConstRef vaultSle,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
@@ -17,6 +20,217 @@ namespace xrpl {
|
||||
|
||||
class STTx;
|
||||
|
||||
/**
|
||||
* @brief Return the Vault's current assets total.
|
||||
*
|
||||
* FixedPrecision Vaults return AssetsAvailable plus AssetsDeployed, with no
|
||||
* rounding to the asset's 16-digit precision: the sum is exact at Number's
|
||||
* active 19-digit mantissa width (guaranteed once fixCleanup3_2_0 is
|
||||
* enabled, which FixedPrecision requires), rounds Downward regardless of the
|
||||
* caller's ambient mode beyond that, and is not rounded to match the cached
|
||||
* sfAssetsTotal field. Legacy and CashBasis Vaults return the stored
|
||||
* AssetsTotal.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @return The current assets total.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* @brief A change to a FixedPrecision Vault's balances.
|
||||
*/
|
||||
struct VaultBalanceChange
|
||||
{
|
||||
/**
|
||||
* Signed change to AssetsAvailable. Must be the exact amount moved on the
|
||||
* Vault pseudo-account in the same transaction. Absent means no cash
|
||||
* moved; adjustVaultBalances then uses zero of the Vault's own asset.
|
||||
*/
|
||||
std::optional<STAmount> cash = std::nullopt;
|
||||
|
||||
/**
|
||||
* Signed change to AssetsDeployed: open Loan principal, exact.
|
||||
*/
|
||||
Number deployed = 0;
|
||||
|
||||
/**
|
||||
* Signed change to scheduled, unpaid interest (YieldUnrealized), exact.
|
||||
*/
|
||||
Number yield = 0;
|
||||
|
||||
/**
|
||||
* Signed change to impaired principal (LossUnrealized), exact.
|
||||
*/
|
||||
Number loss = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Apply a balance change to a FixedPrecision Vault. The only writer of
|
||||
* AssetsAvailable, AssetsDeployed, AssetsTotal, YieldUnrealized and
|
||||
* LossUnrealized on these Vaults after creation.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param change The balance change to apply.
|
||||
* @param j Journal for logging the fatal (tefBAD_LEDGER) and warning
|
||||
* (YieldUnrealized clamp) cases.
|
||||
*
|
||||
* @return tesSUCCESS; tecLIMIT_EXCEEDED if LossUnrealized would exceed
|
||||
* AssetsDeployed; tefBAD_LEDGER if AssetsAvailable, AssetsDeployed or
|
||||
* LossUnrealized would become negative.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Return the Vault's current live exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
|
||||
* Vaults use max(base exponent, exponent of AssetsTotal): the grid is never
|
||||
* finer than -Scale and coarsens once AssetsTotal outgrows 16 digits there.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Return the Vault's base exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use their current live exponent. FixedPrecision
|
||||
* Vaults use -Scale, or 0 for integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Round an amount at the Vault's posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return the Vault's posterior live exponent after applying an unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Shared grid-floor core of getVaultScale / getPosteriorVaultScale /
|
||||
* getPosteriorBrokerCoverScale: baseScale when reference is zero, otherwise
|
||||
* max(baseScale, scale(reference, asset)), rounded Downward regardless of
|
||||
* the caller's ambient rounding mode.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale);
|
||||
|
||||
/**
|
||||
* Shared core of getPosteriorVaultScale and getPosteriorBrokerCoverScale:
|
||||
* the exponent reference + delta would have under version's live-scale rule
|
||||
* (floored at baseScale for FixedPrecision, unbounded for Legacy/CashBasis).
|
||||
*
|
||||
* @param version The Vault's LEVersion.
|
||||
* @param asset The Vault's underlying asset.
|
||||
* @param baseScale The Vault's base exponent; unused outside the
|
||||
* FixedPrecision case.
|
||||
* @param reference The balance the delta is applied to (AssetsAvailable,
|
||||
* AssetsTotal, or a LoanBroker's CoverAvailable).
|
||||
* @param delta The unrounded change to reference.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta);
|
||||
|
||||
/**
|
||||
* Shared core of creditToPosteriorAvailableScale and
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at scale and
|
||||
* returns the difference from reference, so the delta applied is exactly
|
||||
* what moves reference onto the floored sum. See
|
||||
* creditToPosteriorAvailableScale's doc for why the sum, not raw alone,
|
||||
* must be floored. raw is the exact amount; it is not rounded on its own
|
||||
* before being added to reference.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a FixedPrecision cash inflow (LoanPay's credit, LoanManage's
|
||||
* default cover credit) into AssetsAvailable, never AssetsTotal, which is
|
||||
* a derived cache, by flooring the sum rather than the delta:
|
||||
* credit equals floor16(AssetsAvailable + raw) minus AssetsAvailable, never
|
||||
* finer than the Vault's base exponent (-Scale).
|
||||
*
|
||||
* Flooring only the delta at its own posterior grid is not enough: the sum
|
||||
* can still need a 17th digit when AssetsAvailable is off the coarser grid
|
||||
* right after crossing a power of ten. Example at Scale 6: AssetsAvailable
|
||||
* equals 9999999999.999999 (16 digits already) and a credit of 0.000011
|
||||
* floors to 0.00001 on its own (finer) grid, but 9999999999.999999 plus
|
||||
* 0.00001 equals 10000000000.000009, which still needs 17 digits. Flooring
|
||||
* the sum instead, floor16(9999999999.999999 + 0.000011) minus
|
||||
* 9999999999.999999, is exact by construction, since STAmount's own
|
||||
* 16-digit canonical form of the sum is what gets subtracted from.
|
||||
*
|
||||
* raw is taken as an exact Number: rounding it to 16 digits before adding
|
||||
* it to AssetsAvailable could drop a tail that moves the floored sum.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Open-zone capacity ceiling: 9 * 10^(15 + baseScale).
|
||||
*
|
||||
* Defined only for FixedPrecision Vaults, where this is 9 * 10^(15 - P).
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. FixedPrecision Vaults must
|
||||
* remain at their base scale after applying the rounded amount, and the
|
||||
* posterior capacity (AssetsTotal + YieldUnrealized + rounded amount) must
|
||||
* stay within the Open zone.
|
||||
*
|
||||
* The amount is rounded toward zero at the posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Open-zone capacity after adding roundedAmount: AssetsTotal + YieldUnrealized
|
||||
* + roundedAmount, computed TowardsZero regardless of the caller's ambient
|
||||
* rounding mode. Defined only for FixedPrecision Vaults.
|
||||
*
|
||||
* Shared by checkOptionalVaultInflow and LoanSet's loan-origination capacity
|
||||
* check, which pair this formula with different scale checks (posterior vs.
|
||||
* current), so only the capacity formula itself -- not the whole check -- is
|
||||
* shared here.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param roundedAmount The amount to add, already rounded to the relevant
|
||||
* posterior or base scale by the caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to give
|
||||
* depositor when they offer a fixed amount of assets. Note, since shares are
|
||||
@@ -46,31 +260,127 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& shares);
|
||||
|
||||
/**
|
||||
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
|
||||
* Adjusts a requested asset change (delta) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures sfAssetsTotal, sfAssetsAvailable,
|
||||
* and the actual asset transfer change by the exact same representable amount.
|
||||
*
|
||||
* Rounding strategy:
|
||||
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
|
||||
* paying out more than requested.
|
||||
* - Credits (deposits): Floors the resulting total asset balance and returns the
|
||||
* difference from the current total. This prevents crediting the vault with
|
||||
* more assets than the user deposited.
|
||||
* - Legacy/CashBasis debits (withdrawals): rounds down the magnitude of delta
|
||||
* on the new scale to prevent paying out more than requested.
|
||||
* - Legacy/CashBasis credits: floors the resulting total asset balance and
|
||||
* returns the difference from the current total.
|
||||
* - FixedPrecision credits only (VaultDeposit): rounds the delta toward zero
|
||||
* at the scale getAssetsTotal (AssetsAvailable + AssetsDeployed, derived,
|
||||
* not the stored AssetsTotal cache) would have after applying delta.
|
||||
* FixedPrecision outflows (VaultWithdraw, VaultClawback) never call this;
|
||||
* they use clampVaultOutflow instead.
|
||||
*
|
||||
* Key rules:
|
||||
* - The returned magnitude never exceeds `|delta|`.
|
||||
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
|
||||
* - The returned magnitude never exceeds the magnitude of delta.
|
||||
* - Returns tecPRECISION_LOSS if the change is smaller than 1 ULP of the target scale
|
||||
* (prevents share operations when totals cannot change).
|
||||
* - For integer assets (XRP, MPT), rounding is a no-op.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed change to sfAssetsTotal.
|
||||
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Outflow clamp dispatcher for VaultWithdraw and VaultClawback: rounds a
|
||||
* negative delta to the scale appropriate for the Vault's version -- for
|
||||
* FixedPrecision, at AssetsAvailable's own posterior scale (the balance the
|
||||
* cash actually leaves); for Legacy/CashBasis, clampToAssetsTotalScale --
|
||||
* so both call sites share one dispatch point. Same tecPRECISION_LOSS and
|
||||
* integral-asset and magnitude-never-exceeds-delta rules as
|
||||
* clampToAssetsTotalScale.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed outflow (negative).
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Returns the Vault's base exponent for asset at scale, before a Vault
|
||||
* object exists to read it from: 0 for an integral asset, -scale otherwise.
|
||||
* Used by VaultCreate::preclaim, which only has the proposed Scale field,
|
||||
* not yet a Vault SLE; getVaultBaseScale is the post-creation equivalent.
|
||||
*
|
||||
* @param asset The (prospective) Vault's underlying asset.
|
||||
* @param scale The (prospective) Vault's sfScale value, ignored for
|
||||
* integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale);
|
||||
|
||||
/**
|
||||
* Returns true iff value is exactly representable both as an STAmount of
|
||||
* asset and on the base grid at baseScale (no precision lost rounding
|
||||
* TowardsZero at baseScale). Shared representability check for
|
||||
* VaultCreate::preclaim, VaultSet::preclaim and LoanBrokerSet::preclaim's
|
||||
* AssetsMaximum / DebtMaximum / cover-rate field checks.
|
||||
*
|
||||
* @param asset The vault's underlying asset.
|
||||
* @param value The value to check.
|
||||
* @param baseScale The vault's base exponent (see vaultBaseScale /
|
||||
* getVaultBaseScale).
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
|
||||
|
||||
/**
|
||||
* Checks that a requested AssetsMaximum is exactly representable on the
|
||||
* Vault's base grid, otherwise associateAsset would silently round the cap
|
||||
* the owner asked for. Used by VaultSet::preclaim on an existing Vault.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param amount The requested AssetsMaximum.
|
||||
* @return tesSUCCESS, or tecPRECISION_LOSS if amount is not representable.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount);
|
||||
|
||||
/**
|
||||
* Returns the early-exit fee rate that applies to a withdrawal from `vault`.
|
||||
* The rate is zero when no fee applies: before featureLendingProtocolV1_2,
|
||||
* on a vault without sfEarlyExitFeeRate, or outside the Investment phase.
|
||||
* The caller must not charge the fee on a withdrawal that burns every
|
||||
* outstanding share, since the retained fee would be left behind in a vault
|
||||
* with no shares.
|
||||
*
|
||||
* @param view The ledger view whose parent close time is used as the clock.
|
||||
* @param vault The vault SLE.
|
||||
*/
|
||||
[[nodiscard]] TenthBips32
|
||||
getEarlyExitFeeRate(ReadView const& view, SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Computes the early-exit fee charged on a withdrawal from a closed-ended
|
||||
* vault during its Investment phase: `amount * rate`, rounded up on the grid
|
||||
* the post-fee payout leaves AssetsAvailable on (see clampVaultOutflow). The
|
||||
* payout, `amount` minus the fee, therefore needs no further rounding. A
|
||||
* non-zero rate always keeps at least one unit behind (one drop for XRP, one
|
||||
* unit for MPT). A zero rate or zero amount yields zero; a 100% rate yields
|
||||
* `amount`. A withdrawal so small that the post-fee payout rounds to zero is
|
||||
* retained in full, so the fee equals `amount` and the caller pays nothing
|
||||
* out, just as at 100%.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param amount The pre-fee withdrawal amount, already clamped by
|
||||
* clampVaultOutflow.
|
||||
* @param rate The vault's sfEarlyExitFeeRate, in 1/10 bips.
|
||||
*
|
||||
* @return The fee, never greater than `amount`.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
calculateEarlyExitFee(SLE::ConstRef vault, STAmount const& amount, TenthBips32 rate);
|
||||
|
||||
/**
|
||||
* Controls whether to truncate shares instead of rounding.
|
||||
*/
|
||||
@@ -98,9 +408,9 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
|
||||
/**
|
||||
* Returns true if debiting `amount` from `total` (the current value of a
|
||||
* Returns true if debiting amount from total (the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
|
||||
* same STAmount value. This happens when `amount` is non-zero but too small
|
||||
* same STAmount value. This happens when amount is non-zero but too small
|
||||
* to change the stored total at STAmount's precision. Shares would still
|
||||
* move, so the ValidVault invariant would fail after apply; callers use
|
||||
* this to reject the transaction upfront instead.
|
||||
@@ -114,6 +424,20 @@ assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
/**
|
||||
* Returns the reference balance a cash outflow's dust check
|
||||
* (debitIsNonZeroDust) should compare against: FixedPrecision Vaults use
|
||||
* AssetsAvailable, the balance the cash actually leaves; Legacy/CashBasis
|
||||
* Vaults use the given AssetsTotal. Shared by VaultWithdraw and
|
||||
* VaultClawback.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param assetsTotal The vault's current AssetsTotal, as already read by the
|
||||
* caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to demand from
|
||||
* the depositor when they ask to withdraw a fixed amount of assets. Since
|
||||
@@ -158,7 +482,7 @@ sharesToAssetsWithdraw(
|
||||
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
|
||||
|
||||
/**
|
||||
* Returns true iff `account` holds all of the vault's outstanding shares —
|
||||
* Returns true iff account holds all of the vault's outstanding shares,
|
||||
* i.e. is the sole remaining shareholder. Returns false if the account
|
||||
* holds no shares or fewer than the total outstanding.
|
||||
*
|
||||
@@ -171,11 +495,13 @@ sharesToAssetsWithdraw(
|
||||
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef issuance);
|
||||
|
||||
/**
|
||||
* Resolves a Vault's LEVersion, the single point every accounting touch
|
||||
* point should call to determine which recognition model (instant interest
|
||||
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
|
||||
* activated never have sfLEVersion set, which resolves here to
|
||||
* VaultVersion::Legacy.
|
||||
* Resolves a Vault's LEVersion.
|
||||
*
|
||||
* LEVersion is the single point every accounting and rounding helper
|
||||
* should call to decide which protocol a Vault follows. It is written
|
||||
* at VaultCreate and is not updated afterwards, so a Vault created
|
||||
* under an older amendment keeps that behaviour after later amendments
|
||||
* activate. Absent sfLEVersion resolves to VaultVersion::Legacy.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
*
|
||||
@@ -185,6 +511,34 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Decodes an already-extracted sfLEVersion value with the same range check as
|
||||
* getVaultVersion. Usable from contexts, such as an invariant's ledger-entry
|
||||
* snapshot, that keep the field value but not the owning SLE.
|
||||
*
|
||||
* @param leVersion The value of sfLEVersion, or nullopt if absent.
|
||||
*
|
||||
* @return The decoded LEVersion, or VaultVersion::Legacy if absent.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion);
|
||||
|
||||
/**
|
||||
* Resolves which LEVersion a newly-created Vault should get under the
|
||||
* currently active amendments. This is the only place that decides Vault
|
||||
* version policy; VaultCreate calls it instead of checking amendments
|
||||
* directly.
|
||||
*
|
||||
* @param rules The active ledger rules.
|
||||
*
|
||||
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_1,
|
||||
* featureLendingProtocolV1_2 and fixCleanup3_4_0 are all enabled;
|
||||
* VaultVersion::CashBasis
|
||||
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
vaultVersionFor(Rules const& rules);
|
||||
|
||||
/**
|
||||
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
|
||||
* sfVaultKind is present and equal to that value; anything else (including an
|
||||
|
||||
@@ -311,24 +311,55 @@ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
|
||||
* Default IOU scale factor for a Vault
|
||||
*/
|
||||
constexpr std::uint8_t kVaultDefaultIouScale = 6;
|
||||
/**
|
||||
* Maximum scale factor for a Vault. The number is chosen to ensure that
|
||||
* 1 IOU can be always converted to shares.
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumIouScale = 18;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions. Assigned to newly created
|
||||
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
|
||||
* activation are left without LEVersion (implicit legacy version 0,
|
||||
* instant interest recognition).
|
||||
* Maximum Scale for a Vault created before featureLendingProtocolV1_2.
|
||||
* Chosen so 1 IOU can always convert to shares:
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumLegacyIouScale = 18;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumFixedPrecisionIouScale = 10;
|
||||
|
||||
/**
|
||||
* The maximum early-exit fee rate of a closed-ended vault in 1/10 bips.
|
||||
*
|
||||
* Valid values are between 0 and 100% inclusive. At 100% an early exit burns
|
||||
* shares and transfers no assets.
|
||||
*/
|
||||
constexpr TenthBips32 kMaxEarlyExitFeeRate = percentageToTenthBips(100);
|
||||
static_assert(kMaxEarlyExitFeeRate == TenthBips32(100'000u));
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions, persisted as sfLEVersion.
|
||||
*
|
||||
* LEVersion records which protocol a Vault was created under so later
|
||||
* amendments can change the rules for new Vaults without rewriting
|
||||
* existing ones. VaultCreate writes it from the then-active lending
|
||||
* amendments; later transactions do not update it. A Vault created
|
||||
* under an older amendment keeps that amendment's behaviour for its
|
||||
* lifetime, even after a newer lending amendment activates.
|
||||
*
|
||||
* Absent sfLEVersion is implicit Legacy (version 0): instant interest
|
||||
* recognition and a dynamic AssetsTotal scale. CashBasis (V1.1) uses
|
||||
* cash-basis recognition on the same dynamic scale. FixedPrecision
|
||||
* (V1.2) keeps cash-basis recognition and adds the lifetime base grid.
|
||||
*/
|
||||
enum class VaultVersion : uint8_t {
|
||||
Legacy = 0,
|
||||
CashBasis,
|
||||
FixedPrecision,
|
||||
};
|
||||
|
||||
// Code compares VaultVersion values with < and >= (e.g. "CashBasis or later"),
|
||||
// so each later version must stay numerically larger than the one before.
|
||||
static_assert(
|
||||
VaultVersion::Legacy < VaultVersion::CashBasis &&
|
||||
VaultVersion::CashBasis < VaultVersion::FixedPrecision);
|
||||
|
||||
/**
|
||||
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
|
||||
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
|
||||
|
||||
@@ -238,6 +238,12 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
|
||||
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
|
||||
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(ConfidentialMPTHolderKeyUpdate, \
|
||||
TF_FLAG(tfHolderKeyRotation, 0x00010000) \
|
||||
TF_FLAG(tfHolderKeyRecovery, 0x00020000) \
|
||||
TF_FLAG(tfCancelRecovery, 0x00040000), \
|
||||
MASK_ADJ(0))
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <flat_set>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -115,6 +117,9 @@ public:
|
||||
parentBatchID_ = id;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::flat_set<MPTID>
|
||||
getAffectedMPTs() const;
|
||||
|
||||
private:
|
||||
UInt256 transactionID_;
|
||||
std::uint32_t ledgerSeq_;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
// Requires LendingProtocolV1_1. New vaults take FixedPrecision plus cash-basis.
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -439,6 +439,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks Oracle
|
||||
@@ -513,6 +514,8 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfAssetsAvailable, SoeDefault},
|
||||
{sfAssetsMaximum, SoeDefault},
|
||||
{sfLossUnrealized, SoeDefault},
|
||||
{sfYieldUnrealized, SoeDefault},
|
||||
{sfAssetsDeployed, SoeDefault},
|
||||
{sfShareMPTID, SoeRequired},
|
||||
{sfWithdrawalPolicy, SoeRequired},
|
||||
{sfScale, SoeDefault},
|
||||
@@ -520,6 +523,7 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfVaultKind, SoeDefault},
|
||||
{sfSubscriptionDate, SoeOptional},
|
||||
{sfRedemptionDate, SoeOptional},
|
||||
{sfEarlyExitFeeRate, SoeOptional},
|
||||
// no SharesTotal ever (use MPTIssuance.sfOutstandingAmount)
|
||||
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
|
||||
}))
|
||||
|
||||
@@ -128,6 +128,7 @@ TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82)
|
||||
TYPED_SFIELD(sfGasPrice, UINT32, 83)
|
||||
TYPED_SFIELD(sfGas, UINT32, 84)
|
||||
TYPED_SFIELD(sfGasUsed, UINT32, 85)
|
||||
TYPED_SFIELD(sfEarlyExitFeeRate, UINT32, 86) // 1/10 basis points (bips)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -240,6 +241,8 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
|
||||
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
|
||||
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfYieldUnrealized, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfAssetsDeployed, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
@@ -330,6 +333,7 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
|
||||
TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
TYPED_SFIELD(sfRecoveryKey, VL, 48)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -788,6 +788,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate,
|
||||
{sfVaultKind, SoeOptional},
|
||||
{sfSubscriptionDate, SoeOptional},
|
||||
{sfRedemptionDate, SoeOptional},
|
||||
{sfEarlyExitFeeRate, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction updates a single asset vault. */
|
||||
@@ -1147,12 +1148,27 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
|
||||
{sfZKProof, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This transaction rotates or recovers a confidential MPT holder's ElGamal encryption key,
|
||||
or cancels a recovery. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, 93, ConfidentialMPTHolderKeyUpdate,
|
||||
({.amendment = featureConfidentialMPTKeyRotation}),
|
||||
({
|
||||
{sfMPTokenIssuanceID, SoeRequired},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfConfidentialBalanceSpending, SoeOptional},
|
||||
{sfConfidentialBalanceInbox, SoeOptional},
|
||||
{sfZKProof, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction posts an unsigned transaction on-ledger as a
|
||||
TransactionProposal, pending multi-signature collection. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
|
||||
#endif
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
|
||||
({.amendment = featureCosign}),
|
||||
({
|
||||
{sfProposedTransaction, SoeRequired},
|
||||
|
||||
@@ -411,6 +411,7 @@ JSS(minimum_level); // out: TxQ
|
||||
JSS(missingCommand); // error
|
||||
JSS(mpt_amount); // out: mpt_holders
|
||||
JSS(mpt_issuance_id); // in: Payment, mpt_holders
|
||||
JSS(mpt_issuances); // in: Subscribe, Unsubscribe
|
||||
JSS(mptoken_index); // out: mpt_holders
|
||||
JSS(mpt_issuance_id_a); // out: BookChanges
|
||||
JSS(mpt_issuance_id_b); // out: BookChanges
|
||||
|
||||
@@ -339,6 +339,30 @@ public:
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfRecoveryKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getRecoveryKey() const
|
||||
{
|
||||
if (hasRecoveryKey())
|
||||
return this->sle_->at(sfRecoveryKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfRecoveryKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasRecoveryKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfRecoveryKey);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -552,6 +576,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfRecoveryKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setRecoveryKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfRecoveryKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed MPToken wrapper.
|
||||
* @param index The ledger entry index.
|
||||
|
||||
@@ -242,6 +242,54 @@ public:
|
||||
return this->sle_->isFieldPresent(sfLossUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfYieldUnrealized (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getYieldUnrealized() const
|
||||
{
|
||||
if (hasYieldUnrealized())
|
||||
return this->sle_->at(sfYieldUnrealized);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfYieldUnrealized is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasYieldUnrealized() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfYieldUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAssetsDeployed (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getAssetsDeployed() const
|
||||
{
|
||||
if (hasAssetsDeployed())
|
||||
return this->sle_->at(sfAssetsDeployed);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAssetsDeployed is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAssetsDeployed() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfShareMPTID (SoeRequired)
|
||||
* @return The field value.
|
||||
@@ -383,6 +431,30 @@ public:
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfRedemptionDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfEarlyExitFeeRate (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getEarlyExitFeeRate() const
|
||||
{
|
||||
if (hasEarlyExitFeeRate())
|
||||
return this->sle_->at(sfEarlyExitFeeRate);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfEarlyExitFeeRate is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasEarlyExitFeeRate() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfEarlyExitFeeRate);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -571,6 +643,28 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfYieldUnrealized (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setYieldUnrealized(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfYieldUnrealized] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAssetsDeployed (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setAssetsDeployed(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAssetsDeployed] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfShareMPTID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
@@ -648,6 +742,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfEarlyExitFeeRate (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setEarlyExitFeeRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfEarlyExitFeeRate] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed Vault wrapper.
|
||||
* @param index The ledger entry index.
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class ConfidentialMPTHolderKeyUpdateBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: ConfidentialMPTHolderKeyUpdate
|
||||
*
|
||||
* Type: ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE (93)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureConfidentialMPTKeyRotation
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use ConfidentialMPTHolderKeyUpdateBuilder to construct new transactions.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdate : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE;
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTHolderKeyUpdate transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit ConfidentialMPTHolderKeyUpdate(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdate");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfMPTokenIssuanceID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT192::type::value_type
|
||||
getMPTokenIssuanceID() const
|
||||
{
|
||||
return this->tx_->at(sfMPTokenIssuanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolderEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getHolderEncryptionKey() const
|
||||
{
|
||||
if (hasHolderEncryptionKey())
|
||||
{
|
||||
return this->tx_->at(sfHolderEncryptionKey);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfHolderEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasHolderEncryptionKey() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfHolderEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialBalanceSpending (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getConfidentialBalanceSpending() const
|
||||
{
|
||||
if (hasConfidentialBalanceSpending())
|
||||
{
|
||||
return this->tx_->at(sfConfidentialBalanceSpending);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfConfidentialBalanceSpending is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasConfidentialBalanceSpending() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfConfidentialBalanceSpending);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialBalanceInbox (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getConfidentialBalanceInbox() const
|
||||
{
|
||||
if (hasConfidentialBalanceInbox())
|
||||
{
|
||||
return this->tx_->at(sfConfidentialBalanceInbox);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfConfidentialBalanceInbox is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasConfidentialBalanceInbox() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfConfidentialBalanceInbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfZKProof (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getZKProof() const
|
||||
{
|
||||
if (hasZKProof())
|
||||
{
|
||||
return this->tx_->at(sfZKProof);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfZKProof is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasZKProof() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfZKProof);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for ConfidentialMPTHolderKeyUpdate transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConfidentialMPTHolderKeyUpdateBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, account, sequence, fee)
|
||||
{
|
||||
setMPTokenIssuanceID(mPTokenIssuanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ConfidentialMPTHolderKeyUpdateBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdateBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfMPTokenIssuanceID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMPTokenIssuanceID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfHolderEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfConfidentialBalanceSpending] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfConfidentialBalanceInbox] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfZKProof (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdateBuilder&
|
||||
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfZKProof] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the ConfidentialMPTHolderKeyUpdate wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
ConfidentialMPTHolderKeyUpdate
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return ConfidentialMPTHolderKeyUpdate{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
|
||||
/**
|
||||
* @brief Transaction: TransactionProposalCreate
|
||||
*
|
||||
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
|
||||
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureCosign
|
||||
* Privileges: Privilege::NoPriv
|
||||
|
||||
@@ -292,6 +292,32 @@ public:
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfRedemptionDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfEarlyExitFeeRate (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getEarlyExitFeeRate() const
|
||||
{
|
||||
if (hasEarlyExitFeeRate())
|
||||
{
|
||||
return this->tx_->at(sfEarlyExitFeeRate);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfEarlyExitFeeRate is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasEarlyExitFeeRate() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfEarlyExitFeeRate);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -449,6 +475,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfEarlyExitFeeRate (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultCreateBuilder&
|
||||
setEarlyExitFeeRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfEarlyExitFeeRate] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the VaultCreate wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
@@ -26,11 +27,11 @@ namespace xrpl {
|
||||
/**
|
||||
* Maximum number of subscriptions a single client connection may hold at once.
|
||||
*
|
||||
* Applies to the account, real-time account, and account-history subscriptions
|
||||
* tracked on one InfoSub (the sets counted by totalSubscriptionCount), bounding
|
||||
* the disconnect-time cleanup of those sets. Book subscriptions are tracked
|
||||
* separately (OrderBookDB) and are not counted here. Generous enough for
|
||||
* legitimate power users such as block explorers.
|
||||
* Applies to the account, real-time account, account-history, and MPT issuance
|
||||
* subscriptions tracked on one InfoSub (the sets counted by
|
||||
* totalSubscriptionCount), bounding the disconnect-time cleanup of those sets.
|
||||
* Book subscriptions are tracked separately (OrderBookDB) and are not counted
|
||||
* here. Generous enough for legitimate power users such as block explorers.
|
||||
*/
|
||||
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
|
||||
|
||||
@@ -151,6 +152,21 @@ public:
|
||||
AccountID const& account,
|
||||
bool historyOnly) = 0;
|
||||
|
||||
/**
|
||||
* Remove an MPT issuance subscription during InfoSub teardown.
|
||||
*
|
||||
* Removes only the server-side entry from subMPT_. Does NOT touch
|
||||
* InfoSub::mptSubscriptions_ because the InfoSub is being destroyed.
|
||||
* Called by ~InfoSub() for each issuance in mptSubscriptions_.
|
||||
*
|
||||
* @param uListener The sequence number of the subscriber being torn down.
|
||||
* @param mptID The MPT issuance to remove.
|
||||
*
|
||||
* @note Thread-safety: acquires mptLock_ internally.
|
||||
*/
|
||||
virtual void
|
||||
unsubMPTInternal(std::uint64_t uListener, MPTID const& mptID) = 0;
|
||||
|
||||
/**
|
||||
* Schedule the server-side teardown of a disconnecting connection's
|
||||
* account subscriptions off the destructor thread.
|
||||
@@ -270,6 +286,11 @@ public:
|
||||
virtual bool
|
||||
unsubConsensus(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual void
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
virtual void
|
||||
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
|
||||
// VFALCO TODO Remove
|
||||
// This was added for one particular partner, it
|
||||
// "pushes" subscription data to a particular URL.
|
||||
@@ -309,9 +330,9 @@ public:
|
||||
* Return the number of subscriptions currently tracked on this
|
||||
* connection.
|
||||
*
|
||||
* The combined size of the per-connection account, real-time account, and
|
||||
* account-history subscription sets. `doSubscribe` reads this to enforce
|
||||
* the per-connection subscription cap before admitting more.
|
||||
* The combined size of the per-connection account, real-time account,
|
||||
* account-history, and MPT issuance subscription sets. `doSubscribe` reads
|
||||
* this to enforce the per-connection subscription cap before admitting more.
|
||||
*
|
||||
* @return The total tracked subscription count for this connection.
|
||||
*
|
||||
@@ -342,6 +363,25 @@ public:
|
||||
HashSet<AccountID> const& normalAccounts,
|
||||
std::size_t cap);
|
||||
|
||||
/**
|
||||
* Enforce the cap and reserve a request's net-new MPT issuances, atomically.
|
||||
*
|
||||
* The MPT analogue of tryReserveAccountSubscriptions: under one hold of
|
||||
* `lock_`, count the net-new issuances, check the total against @p cap, and
|
||||
* insert them only if it fits. All-or-nothing, so a rejected request records
|
||||
* nothing. Doing check and insert together stops two concurrent requests
|
||||
* sharing an InfoSub (the admin subscribe-by-url path) from both passing the
|
||||
* check before either records its issuances. The server-side map is
|
||||
* populated afterwards by subMPT, whose re-insert is a no-op.
|
||||
*
|
||||
* @param mptIDs The MPT issuance ids to reserve.
|
||||
* @param cap The effective per-connection cap.
|
||||
* @return true if reserved; false if the request must be rejected.
|
||||
* @note Thread-safe: takes `lock_`.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap);
|
||||
|
||||
/**
|
||||
* Whether this connection already tracks an account-history for @p account.
|
||||
*
|
||||
@@ -413,12 +453,29 @@ public:
|
||||
[[nodiscard]] unsigned int
|
||||
getApiVersion() const noexcept;
|
||||
|
||||
void
|
||||
insertSubMPTInfo(MPTID const& mptID);
|
||||
|
||||
void
|
||||
deleteSubMPTInfo(MPTID const& mptID);
|
||||
|
||||
protected:
|
||||
// Mutable so the read-only totalSubscriptionCount() accessor can lock it
|
||||
// from a const method; locking semantics are otherwise unchanged.
|
||||
mutable std::mutex lock_;
|
||||
|
||||
private:
|
||||
// The lock type guarding this instance's subscription sets.
|
||||
using ScopedLock = std::scoped_lock<decltype(lock_)>;
|
||||
|
||||
/**
|
||||
* The combined tally the per-connection cap is enforced against.
|
||||
*
|
||||
* @param lock Proof that `lock_` is held; unused otherwise.
|
||||
*/
|
||||
[[nodiscard]] std::size_t
|
||||
subscriptionCount(ScopedLock const& lock) const;
|
||||
|
||||
Consumer consumer_;
|
||||
Source& source_;
|
||||
HashSet<AccountID> realTimeSubscriptions_;
|
||||
@@ -427,6 +484,7 @@ private:
|
||||
std::uint64_t seq_;
|
||||
HashSet<AccountID> accountHistorySubscriptions_;
|
||||
HashSet<Book> bookSubscriptions_;
|
||||
HashSet<MPTID> mptSubscriptions_;
|
||||
unsigned int apiVersion_ = 0;
|
||||
|
||||
static int
|
||||
|
||||
@@ -46,6 +46,9 @@ class ValidMPTIssuance
|
||||
* MPTokens and RippleStates deleted during apply. finalize() checks each
|
||||
* holder's AccountRoot to detect vault pseudo-account holdings deleted
|
||||
* outside VaultDelete. All these checks are gated on fixCleanup3_2_0.
|
||||
*
|
||||
* Under fixCleanup3_5_0, finalize() also rejects any MPToken erased with
|
||||
* a non-zero sfMPTAmount.
|
||||
*/
|
||||
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
|
||||
|
||||
@@ -138,8 +141,9 @@ public:
|
||||
* - Convert/ConvertBack symmetry:
|
||||
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
|
||||
* - Cannot delete MPToken with non-zero confidential state:
|
||||
* Cannot delete if sfIssuerEncryptedBalance exists
|
||||
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
|
||||
* Cannot delete if any of sfConfidentialBalanceSpending, sfConfidentialBalanceInbox,
|
||||
* sfIssuerEncryptedBalance or sfAuditorEncryptedBalance is present, and the issuance's
|
||||
* sfConfidentialOutstandingAmount is non-zero. Mirrors MPTokenAuthorize::preclaim.
|
||||
* - Privacy flag consistency:
|
||||
* MPToken confidential balance fields can only be created or changed if
|
||||
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
|
||||
@@ -162,6 +166,8 @@ class ValidConfidentialMPToken
|
||||
std::int64_t outstandingDelta = 0;
|
||||
SLE::const_pointer issuance;
|
||||
bool deletedWithEncrypted = false;
|
||||
// True when an erased MPToken had a non-zero pre-tx public balance.
|
||||
bool deletedWithBalanceBefore = false;
|
||||
bool badConsistency = false;
|
||||
bool badCOA = false;
|
||||
bool changesConfidentialFields = false;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -29,7 +30,7 @@ namespace xrpl {
|
||||
* - vault must have MPTokenIssuance for shares
|
||||
* - vault without shares outstanding must have no shares
|
||||
* - loss unrealized does not exceed the difference between assets total and
|
||||
* assets available
|
||||
* assets available (Legacy/CashBasis), or AssetsDeployed (FixedPrecision)
|
||||
* - assets available do not exceed assets total
|
||||
* - vault deposit increases assets and share issuance, and adds to:
|
||||
* total assets, assets available, shares outstanding
|
||||
@@ -46,6 +47,14 @@ namespace xrpl {
|
||||
* - vault withdrawal may not succeed when the vault phase is Investment
|
||||
* - closed-ended loan origination (ttLOAN_SET) may only succeed when the
|
||||
* vault phase is Investment
|
||||
* - FixedPrecision only: AssetsDeployed is non-negative and exactly
|
||||
* representable at the asset's precision; AssetsDeployed changes only for
|
||||
* ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE; AssetsDeployed is zero right
|
||||
* after ttVAULT_CREATE and whenever shares outstanding return to zero; the
|
||||
* stored AssetsTotal cache equals AssetsAvailable + AssetsDeployed, rounded
|
||||
* Downward to the asset's precision
|
||||
* - FixedPrecision only: YieldUnrealized is non-negative; YieldUnrealized
|
||||
* changes only for ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE
|
||||
*
|
||||
* Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced
|
||||
* by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). From
|
||||
@@ -68,9 +77,14 @@ class ValidVault
|
||||
Number assetsAvailable = 0;
|
||||
Number assetsMaximum = 0;
|
||||
Number lossUnrealized = 0;
|
||||
Number assetsDeployed = 0;
|
||||
Number yieldUnrealized = 0;
|
||||
VaultVersion version = VaultVersion::Legacy;
|
||||
std::optional<std::uint8_t> vaultKind;
|
||||
std::optional<std::uint32_t> subscriptionDate;
|
||||
std::optional<std::uint32_t> redemptionDate;
|
||||
std::optional<std::uint32_t> earlyExitFeeRate;
|
||||
std::optional<std::uint8_t> leVersion;
|
||||
|
||||
Vault static make(SLE const&);
|
||||
};
|
||||
@@ -217,6 +231,39 @@ private:
|
||||
[[nodiscard]] bool
|
||||
finalizeLoanSet(ReadView const& view, beast::Journal const& j) const;
|
||||
|
||||
/**
|
||||
* @brief Check that a vault's AssetsTotal and AssetsAvailable deltas add
|
||||
* up against the real transfer, for deposit/withdraw/clawback.
|
||||
*
|
||||
* Shared body of the ttVAULT_DEPOSIT, ttVAULT_WITHDRAW and
|
||||
* ttVAULT_CLAWBACK "assets outstanding"/"assets available" add-up checks,
|
||||
* which differ only in the rounded/exact deltas plugged in and the verb
|
||||
* used in the log message.
|
||||
*
|
||||
* @param afterVault Snapshot of the vault after the transaction.
|
||||
* @param beforeVault Snapshot of the vault before the transaction.
|
||||
* @param vaultAsset The vault's underlying asset.
|
||||
* @param fix340Enabled Whether fixCleanup3_4_0 is enabled.
|
||||
* @param minScale The posterior scale computed by computeVaultMinScale.
|
||||
* @param roundedVaultDelta The real transfer's delta, rounded to minScale.
|
||||
* @param exactVaultDelta The real transfer's exact (unrounded) delta.
|
||||
* @param verb The transaction name used in the log message ("deposit",
|
||||
* "withdrawal" or "clawback").
|
||||
* @param j Journal for logging.
|
||||
* @return false if either add-up check fails.
|
||||
*/
|
||||
[[nodiscard]] static bool
|
||||
checkTotalsAddUp(
|
||||
Vault const& afterVault,
|
||||
Vault const& beforeVault,
|
||||
Asset const& vaultAsset,
|
||||
bool fix340Enabled,
|
||||
std::int32_t minScale,
|
||||
Number const& roundedVaultDelta,
|
||||
Number const& exactVaultDelta,
|
||||
char const* verb,
|
||||
beast::Journal const& j);
|
||||
|
||||
public:
|
||||
// Compute the coarsest scale required to represent all numbers
|
||||
[[nodiscard]] static std::int32_t
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -45,9 +46,29 @@ public:
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Compute the First-Loss Capital cover amount for a defaulted Loan on a FixedPrecision
|
||||
* Vault, capped at the LoanBroker's CoverAvailable and floored so that AssetsAvailable + cover
|
||||
* is exactly on its posterior grid.
|
||||
*
|
||||
* The LoanBroker absorbs the rounding. When CoverAvailable - cover needs finer digits than the
|
||||
* broker's posterior grid, it rounds to nearest, as the broker's trust line does, so
|
||||
* CoverAvailable can move by up to half a posterior ulp more or less than the cover. When the
|
||||
* two grids differ, no cover is exact on both sides; the Vault side is kept exact.
|
||||
*
|
||||
* The one exception: when AssetsAvailable has digits finer than the 16-digit cover can carry,
|
||||
* no cover cancels them, and the balance writer rounds AssetsAvailable + cover to nearest,
|
||||
* by at most half a posterior ulp. VaultDeposit and LoanPay credits have the same limit.
|
||||
* @param loanSle The Loan being defaulted.
|
||||
* @param brokerSle The Loan's LoanBroker.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @return The cover amount, in the Vault's Asset.
|
||||
*/
|
||||
static STAmount
|
||||
calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle);
|
||||
|
||||
/**
|
||||
* Helper function that might be needed by other transactors
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Allows a confidential MPT holder to rotate or recover their ElGamal
|
||||
* encryption key, or to cancel a pending recovery.
|
||||
*
|
||||
* Submitted by the holder. Exactly one of three modes must be selected via
|
||||
* the transaction flags:
|
||||
*
|
||||
* - Rotation (tfHolderKeyRotation): the holder still has their current
|
||||
* ElGamal private key. They provide a new public key along with their
|
||||
* current spending/inbox balances re-encrypted under that new key. The
|
||||
* holder's encryption key and confidential balances are updated
|
||||
* immediately.
|
||||
*
|
||||
* - Recovery (tfHolderKeyRecovery): the holder has lost their current
|
||||
* private key. They provide a new public key but cannot provide
|
||||
* re-encrypted balances. The new key is recorded as a pending
|
||||
* sfRecoveryKey; the confidential balances are left untouched. Completing
|
||||
* the recovery (rewriting the balances) is done separately by the issuer
|
||||
* via ConfidentialMPTRecoverBalance.
|
||||
*
|
||||
* - Cancel (tfCancelRecovery): the holder revokes a pending recovery
|
||||
* authorization created by a prior Recovery-mode transaction. No key,
|
||||
* balances, or proof are carried; authorization is via the holder's
|
||||
* ordinary XRPL signature. sfRecoveryKey is removed and everything else
|
||||
* is left untouched. Fails if no recovery is pending.
|
||||
*
|
||||
* @note Zero-knowledge proof verification for Rotation and Recovery is
|
||||
* deferred to a follow-up once the mpt-crypto constructions are finalized.
|
||||
*/
|
||||
class ConfidentialMPTHolderKeyUpdate : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit ConfidentialMPTHolderKeyUpdate(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static std::uint32_t
|
||||
getFlagsMask(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -26,6 +26,10 @@ The store paths carry their derivation hash, so they change whenever a tool is
|
||||
rebuilt — a `flake.lock` update generally rewrites most of them even when no
|
||||
version moves. That is deliberate: it makes tooling changes visible in review.
|
||||
|
||||
Tools scoped to a single dev shell rather than to `commonPackages` do not appear
|
||||
here: `check-tools.sh` keys those off `XRPL_DEVSHELL`, and none of the three
|
||||
environments above is such a shell. Adding to one needs no snapshot update.
|
||||
|
||||
## Regenerating
|
||||
|
||||
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible
|
||||
|
||||
@@ -147,6 +147,19 @@ rec {
|
||||
versionedTools = clangVersionedTools;
|
||||
};
|
||||
|
||||
# The gcc shell plus the Lean4 formal verification toolchain
|
||||
formal-verification = makeShell {
|
||||
shellName = "formal-verification";
|
||||
stdenv = customGccStdenv;
|
||||
compilerName = "gcc";
|
||||
version = gccVersion;
|
||||
versionedTools = gccVersionedTools;
|
||||
extraPackages = [
|
||||
customGccGcov
|
||||
pkgs.lean4
|
||||
];
|
||||
};
|
||||
|
||||
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
|
||||
no-compiler = makeShell {
|
||||
shellName = "no-compiler";
|
||||
|
||||
@@ -10,11 +10,12 @@ a build configured with `-Dvalidator_keys=ON`.
|
||||
package/
|
||||
build_pkg.py Staging and build script (called by the CMake `package` target and CI)
|
||||
sign_rpm.py Signs the built RPMs (called by CI when publishing)
|
||||
docker/
|
||||
Dockerfile Packaging image, built by `build-packaging-images.yml`; installs its tooling with `bin/install-packaging-tools.sh`
|
||||
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
|
||||
image/
|
||||
Dockerfile The xrpld Docker image, installing the built DEB on Ubuntu (see "Docker image")
|
||||
images/
|
||||
packaging/
|
||||
Dockerfile Packaging image, built by `build-packaging-images.yml`; installs its tooling with `bin/install-packaging-tools.sh`
|
||||
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
|
||||
xrpld/
|
||||
Dockerfile The xrpld Docker images, installing the built DEB on Ubuntu (see "Docker images")
|
||||
rpm/
|
||||
xrpld.spec RPM spec
|
||||
debian/ Debian control files (control.in, lintian-overrides.in, rules, copyright, docs, links, source/format).
|
||||
@@ -119,8 +120,8 @@ Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
|
||||
in the container of every distro that format targets and running the binaries
|
||||
there, so one that cannot be installed never reaches Nexus.
|
||||
3. `publish` uploads both artifacts, or lists what it would upload.
|
||||
4. `docker` builds the [Docker image](#docker-image) from the tested DEB, and
|
||||
pushes it when publishing.
|
||||
4. `docker` builds the [Docker images](#docker-images) from the tested DEB, and
|
||||
pushes them when publishing.
|
||||
|
||||
The packaging script derives the package version from the downloaded binary's
|
||||
`xrpld --version` output; no CMake configure or build step is needed inside the
|
||||
@@ -209,32 +210,37 @@ rejects request bodies over 100 MB, so uploads use the DNS-only host instead.
|
||||
The `release-info` action decides the channel from the event, and
|
||||
`publish_pkg.py` maps that channel to its repositories:
|
||||
|
||||
| Event | Version | Channel | DEB repository | RPM upload repository |
|
||||
| ------------------------ | ----------------- | --------- | -------------- | --------------------- |
|
||||
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
|
||||
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
|
||||
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
|
||||
| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` |
|
||||
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
|
||||
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
|
||||
| Event | Version | Channel | DEB repository | RPM upload repository |
|
||||
| ------------------------ | ------------------ | --------- | -------------- | --------------------- |
|
||||
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
|
||||
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
|
||||
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
|
||||
| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` |
|
||||
| push to `develop` | `0.0.0-dev+<hash>` | `develop` | `deb-develop` | `rpm-develop-hosted` |
|
||||
| tag, non-public codebase | _any but `X.Y.Z`_ | `private` | `deb-private` | `rpm-private-hosted` |
|
||||
|
||||
A variant is published to the same channel under its own name, so
|
||||
`xrpld-assert` never overwrites `xrpld`.
|
||||
|
||||
Only a tag names a channel — do not extend that to `develop`, where
|
||||
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
|
||||
version during a release cycle, which would send develop builds into `stable`.
|
||||
Versions sort in row order, so moving to a more mature channel never downgrades.
|
||||
A tag matching none of the release patterns, such as `X.Y.Z-hotfix1`, publishes
|
||||
to `custom`, which sits outside that order.
|
||||
Only a tag picks a release channel. A final release, `X.Y.Z`, goes to `stable`
|
||||
even from a non-public codebase. Versions sort in row order, so moving to a
|
||||
more mature channel never downgrades. A tag matching none of the release
|
||||
patterns, such as `X.Y.Z-hotfix1`, publishes to `custom`, which sits outside
|
||||
that order.
|
||||
|
||||
The action decides the package release number on the same split: a tag's version
|
||||
is unique, so its packages are release 1, while develop repeats the same version
|
||||
and takes `<run number>.<commit date>git<commit hash>`, e.g.
|
||||
`857.20260826gitb6a8995` — the leading run number keeps each push superseding
|
||||
the last, and the date and hash say which commit a package on
|
||||
`packages.xrplf.org` came from. Both reach the packaging scripts as arguments,
|
||||
so neither script derives anything itself.
|
||||
Every untagged build reports `0.0.0-dev+<hash>`, which the `develop` channel
|
||||
packages as `0.0.0~dev`, below every release.
|
||||
|
||||
The action also picks the package release number:
|
||||
|
||||
- A tag: `1`, since a tag's version is never reused.
|
||||
- Anything else: `<run number>.<commit date>git<commit hash>`, e.g.
|
||||
`857.20260826gitb6a8995`. Develop packages all share `0.0.0~dev`, so the run
|
||||
number orders them, and the date and hash name the commit a package on
|
||||
`packages.xrplf.org` came from.
|
||||
|
||||
Both reach the packaging scripts as arguments, so neither script derives
|
||||
anything itself.
|
||||
|
||||
Publishing is its own job, gated behind the install tests, uploading from the same
|
||||
image that built the packages with the `publish_pkg.py` shipped in it — the
|
||||
@@ -271,14 +277,23 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
|
||||
installs it at `/usr/local/bin/publish_pkg.py` for other XRPLF repositories that
|
||||
build their packages elsewhere.
|
||||
|
||||
## Docker image
|
||||
## Docker images
|
||||
|
||||
The `docker` job installs the tested `xrpld` DEB on `ubuntu:26.04` using
|
||||
[`image/Dockerfile`](image/Dockerfile), checks that the server starts, and,
|
||||
with `publish: true`, pushes it to `xrplf/xrpld` on Docker Hub using the
|
||||
`DOCKERHUB_TOKEN` secret, an organization access token for `xrplf`. A tag's
|
||||
image is tagged with the tag name, `xrplf/xrpld:<version>`, and a develop image
|
||||
as `xrplf/xrpld:develop`. Private builds are never pushed.
|
||||
[`images/xrpld/Dockerfile`](images/xrpld/Dockerfile), once per target, and
|
||||
checks that the server starts in each image. A tag's images are tagged with the
|
||||
tag name, a develop image as `develop`. With `publish: true`:
|
||||
|
||||
- `xrpld` is pushed to `xrplf/xrpld` on Docker Hub using the `DOCKERHUB_TOKEN`
|
||||
secret, an organization access token for `xrplf`. Builds of a non-public
|
||||
codebase are pushed there only for `stable` releases, whose packages are
|
||||
public too.
|
||||
- `voidstar` replaces `/usr/bin/xrpld` with the binary of the `voidstar` build
|
||||
config, adds `libvoidstar.so` and links the binary into `/symbols`, as
|
||||
Antithesis expects. It is pushed as `xrpld-voidstar` to the Antithesis
|
||||
registry, `${ANTITHESIS_DOCKER_HOST}/${ANTITHESIS_DOCKER_PATH}`, logging in
|
||||
with the `ANTITHESIS_DOCKER_CREDENTIALS` service account key. The registry is
|
||||
private, so private builds are pushed too.
|
||||
|
||||
## How `build_pkg.py` works
|
||||
|
||||
@@ -307,26 +322,30 @@ values; DEB writes them as `${pkg_version}-${PKG_RELEASE}` in
|
||||
|
||||
With `PKG_RELEASE=1`, the package metadata becomes:
|
||||
|
||||
| Input version | RPM version/release | Debian version |
|
||||
| ------------------ | ---------------------------- | -------------------- |
|
||||
| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` |
|
||||
| `3.2.0-b0+abc1234` | `3.2.0~b0+abc1234-1%{?dist}` | `3.2.0~b0+abc1234-1` |
|
||||
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
|
||||
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
|
||||
| `3.2.0-custom-1` | `3.2.0~custom.1-1%{?dist}` | `3.2.0~custom.1-1` |
|
||||
| Input version | RPM version/release | Debian version |
|
||||
| ------------------- | -------------------------- | ------------------ |
|
||||
| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` |
|
||||
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
|
||||
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
|
||||
| `3.2.0-custom-1` | `3.2.0~custom.1-1%{?dist}` | `3.2.0~custom.1-1` |
|
||||
| `0.0.0-dev+abc1234` | `0.0.0~dev-1%{?dist}` | `0.0.0~dev-1` |
|
||||
|
||||
`build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it
|
||||
from the build host, so the RHEL image can track a newer release without
|
||||
changing what the packages claim to target.
|
||||
|
||||
The Debian changelog entry carries the channel passed as `--channel`, which
|
||||
only accepts the channels in the table above plus `UNRELEASED`, the Debian
|
||||
convention for a build that targets no channel at all — what local and CMake
|
||||
builds pass, since nothing publishes them. An unsupported pre-release, and
|
||||
build metadata on a final release such as `3.2.0+abc123`, are both rejected,
|
||||
except in the `custom` and `private` channels, which accept any version and
|
||||
switch each `-` inside the pre-release or build metadata to `.`, so
|
||||
`3.4.0-custom-1` packages as `3.4.0~custom.1`.
|
||||
The Debian changelog entry carries the channel passed as `--channel`: one of
|
||||
the channels in the table above, or `UNRELEASED`, the Debian convention for a
|
||||
build that targets no channel, which local and CMake builds pass. Each channel
|
||||
accepts:
|
||||
|
||||
- `stable`, `rc`, `beta`: `X.Y.Z`, or a `bN`/`rcN` pre-release, the only kind
|
||||
that may carry build metadata. `3.2.0-b1` packages as `3.2.0~b1`.
|
||||
- `custom`, `private`: any version, with each `-` inside the pre-release or
|
||||
build metadata switched to `.`. `3.4.0-custom-1` packages as `3.4.0~custom.1`.
|
||||
- `develop`: only `0.0.0-dev`, without its build metadata. `0.0.0-dev+abc1234`
|
||||
packages as `0.0.0~dev`.
|
||||
- `UNRELEASED`: `0.0.0-dev` as `develop` does, anything else as `stable` does.
|
||||
|
||||
The RPM path intentionally uses `~` in `Version`, matching the Debian
|
||||
pre-release ordering convention, so RPM filenames/NVRs begin with forms like
|
||||
|
||||
@@ -24,6 +24,12 @@ PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)$")
|
||||
# Channels a tag of any version is published to, rather than only bN/rcN.
|
||||
ANY_VERSION_CHANNELS = ("custom", "private")
|
||||
|
||||
# The version of any untagged build, which is all the develop channel publishes.
|
||||
DEV_VERSION = "0.0.0-dev"
|
||||
|
||||
# Channels an untagged build is packaged for: develop, or none for a local build.
|
||||
DEV_VERSION_CHANNELS = ("develop", "UNRELEASED")
|
||||
|
||||
# The package name a variant suffixes, and the name every variant keeps for its
|
||||
# on-disk paths (/usr/bin/xrpld, /etc/xrpld, xrpld.service).
|
||||
BASE_NAME = "xrpld"
|
||||
@@ -74,13 +80,23 @@ def capture(*command: object) -> str:
|
||||
def package_version(reported: str, channel: str) -> str:
|
||||
"""Normalise a reported version into one the package formats accept.
|
||||
|
||||
A pre-release switches to '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before
|
||||
the final 3.2.0; a no-op for a final release. The custom and private
|
||||
channels accept any pre-release and build metadata, with any '-' inside
|
||||
either switched to '.' (3.4.0-custom-1 -> 3.4.0~custom.1).
|
||||
- Release: unchanged (3.2.0 -> 3.2.0).
|
||||
- bN/rcN pre-release: '-' becomes '~', to sort before the release
|
||||
(3.2.0-b1 -> 3.2.0~b1).
|
||||
- custom, private: any version, '-' inside the pre-release and build
|
||||
metadata becomes '.' (3.4.0-custom-1 -> 3.4.0~custom.1).
|
||||
- develop, UNRELEASED: 0.0.0-dev without build metadata, so builds sort by
|
||||
package release, not commit hash (0.0.0-dev+abc1234 -> 0.0.0~dev).
|
||||
develop accepts nothing else.
|
||||
"""
|
||||
# Metadata first, as it may contain '-' too.
|
||||
release, plus, metadata = reported.partition("+")
|
||||
if release == DEV_VERSION and channel in DEV_VERSION_CHANNELS:
|
||||
return DEV_VERSION.replace("-", "~")
|
||||
assert channel != "develop", (
|
||||
f"unsupported version {reported!r}: "
|
||||
f"the develop channel only accepts {DEV_VERSION}."
|
||||
)
|
||||
base, _, pre_release = release.partition("-")
|
||||
if channel in ANY_VERSION_CHANNELS:
|
||||
pre_release = pre_release.replace("-", ".")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
ARG BASE_IMAGE=debian:trixie
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# Bind-mounted rather than copied in, so the installer never lands in a layer.
|
||||
RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
|
||||
/install-packaging-tools.sh
|
||||
|
||||
# See ../README.md, "Publishing from other repositories".
|
||||
COPY package/docker/publish_pkg.py /usr/local/bin/publish_pkg.py
|
||||
@@ -1,15 +0,0 @@
|
||||
FROM ubuntu:26.04
|
||||
|
||||
RUN --mount=type=bind,target=/tmp/package \
|
||||
apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends /tmp/package/*.deb \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -sf /dev/stdout /var/log/xrpld/debug.log
|
||||
|
||||
USER xrpld
|
||||
WORKDIR /var/lib/xrpld
|
||||
|
||||
EXPOSE 2459 5005 6006 50051
|
||||
|
||||
ENTRYPOINT ["/usr/bin/xrpld"]
|
||||
CMD ["--net", "--silent", "--conf", "/etc/xrpld/xrpld.cfg"]
|
||||
9
package/images/packaging/Dockerfile
Normal file
9
package/images/packaging/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
ARG BASE_IMAGE=debian:trixie
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
|
||||
/install-packaging-tools.sh
|
||||
|
||||
# See package/README.md, "Publishing from other repositories".
|
||||
COPY package/images/packaging/publish_pkg.py /usr/local/bin/publish_pkg.py
|
||||
31
package/images/xrpld/Dockerfile
Normal file
31
package/images/xrpld/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM ubuntu:26.04 AS xrpld
|
||||
|
||||
RUN --mount=type=bind,target=/tmp/package \
|
||||
apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends /tmp/package/*.deb \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -sf /dev/stdout /var/log/xrpld/debug.log
|
||||
|
||||
USER xrpld
|
||||
WORKDIR /var/lib/xrpld
|
||||
|
||||
EXPOSE 2459 5005 6006 50051
|
||||
|
||||
ENTRYPOINT ["/usr/bin/xrpld"]
|
||||
CMD ["--net", "--silent", "--conf", "/etc/xrpld/xrpld.cfg"]
|
||||
|
||||
# The same image with the Antithesis-instrumented binary, which loads
|
||||
# libvoidstar.so; Antithesis reads the debug symbols from /symbols.
|
||||
FROM xrpld AS voidstar
|
||||
|
||||
USER root
|
||||
|
||||
ADD --chmod=644 --checksum=sha256:d080cc85f1d8d96452bdaf0021a10fa3e4cc3c319840f1fadd2d33904e607095 \
|
||||
https://antithesis.com/assets/instrumentation/libvoidstar.so /usr/lib/libvoidstar.so
|
||||
|
||||
RUN --mount=type=bind,source=xrpld,target=/tmp/xrpld \
|
||||
install -m 755 /tmp/xrpld /usr/bin/xrpld \
|
||||
&& mkdir /symbols \
|
||||
&& ln -s /usr/bin/xrpld /symbols/xrpld
|
||||
|
||||
USER xrpld
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
@@ -99,12 +100,13 @@ uniqueRandomPath(
|
||||
if (ec)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Unable to check path '" + candidate.string() + "': " + ec.message());
|
||||
std::format("Unable to check path '{}': {}", candidate.string(), ec.message()));
|
||||
}
|
||||
if (!exists)
|
||||
return candidate;
|
||||
}
|
||||
Throw<std::runtime_error>("Unable to generate a unique path under '" + base.string() + "'");
|
||||
Throw<std::runtime_error>(
|
||||
std::format("Unable to generate a unique path under '{}'", base.string()));
|
||||
}
|
||||
|
||||
TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -264,7 +265,7 @@ OpenView::rawTxInsert(
|
||||
auto const result = txs_.emplace(
|
||||
std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData));
|
||||
if (!result.second)
|
||||
Throw<std::logic_error>("rawTxInsert: duplicate TX id: " + to_string(key));
|
||||
Throw<std::logic_error>(std::format("rawTxInsert: duplicate TX id: {}", to_string(key)));
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -66,6 +66,161 @@ canApplyToBrokerCover(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::detail::getPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
|
||||
return posteriorAssetScale(
|
||||
getVaultVersion(vault),
|
||||
vault->at(sfAsset),
|
||||
getVaultBaseScale(vault),
|
||||
broker->at(sfCoverAvailable),
|
||||
delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : delta and Vault asset match");
|
||||
if (delta.integral())
|
||||
return delta;
|
||||
return roundToScale(delta, getPosteriorBrokerCoverScale(vault, broker, delta), roundingMode);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::creditToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = broker->at(sfCoverAvailable);
|
||||
int const scale = detail::getPosteriorBrokerCoverScale(vault, broker, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
debitToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::debitToPosteriorBrokerCoverScale : non-negative amount");
|
||||
return -detail::roundToPosteriorBrokerCoverScale(vault, broker, -amount, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalBrokerCoverInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalBrokerCoverInflow : non-negative amount");
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
// amount is the effective credit (already floored on the posterior
|
||||
// CoverAvailable grid by creditToPosteriorBrokerCoverScale); rounding it
|
||||
// again as a standalone delta could hide a crossing.
|
||||
STAmount const& rounded = amount;
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though the Open-limit capacity check below rejects
|
||||
// every coarsening transition too. The protocol defines both conditions
|
||||
// independently.
|
||||
if (detail::getPosteriorBrokerCoverScale(vault, broker, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return broker->at(sfCoverAvailable) + rounded;
|
||||
}();
|
||||
if (posterior > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::Ref brokerSle,
|
||||
SLE::ConstRef vaultSle,
|
||||
Number const& delta,
|
||||
int vaultScale)
|
||||
{
|
||||
// On FixedPrecision Vaults, LoanSet adds principal to DebtTotal exactly, LoanPay and default
|
||||
// subtract exact amounts. Thus DebtTotal must not be rounded coarser than the base scale.
|
||||
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
brokerSle->at(sfDebtTotal) += delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), delta, vaultSle->at(sfAsset), vaultScale);
|
||||
}
|
||||
}
|
||||
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getVaultBaseScale(vaultSle));
|
||||
}
|
||||
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx)
|
||||
{
|
||||
@@ -248,14 +403,13 @@ loanPaymentDeltas(LoanPaymentParts const& parts)
|
||||
|
||||
namespace {
|
||||
|
||||
// Cash-basis accounting applies only when featureLendingProtocolV1_1 is
|
||||
// enabled AND the specific Vault was created under it (LEVersion ==
|
||||
// VaultVersion::CashBasis). Vaults created before activation keep instant
|
||||
// interest recognition forever, even after the amendment later turns on.
|
||||
// Cash-basis accounting applies to Vaults created under
|
||||
// featureLendingProtocolV1_1 or a later version. Vaults created before
|
||||
// activation keep instant interest recognition forever.
|
||||
bool
|
||||
cashBasisEnabled(SLE::ConstRef vaultSle)
|
||||
{
|
||||
return getVaultVersion(vaultSle) == VaultVersion::CashBasis;
|
||||
return getVaultVersion(vaultSle) >= VaultVersion::CashBasis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
@@ -394,9 +395,11 @@ removeToken(ApplyView& view, AccountID const& owner, UInt256 const& nftokenID, S
|
||||
|
||||
if (!page2)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"page " + to_string(page1->key()) + " has a broken " + field.getName() +
|
||||
" field pointing to " + to_string(*id));
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"page {} has a broken {} field pointing to {}",
|
||||
to_string(page1->key()),
|
||||
field.getName(),
|
||||
to_string(*id)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,7 +600,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
|
||||
else
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
|
||||
std::format("Offer {} cannot be deleted!", to_string(offerIndexes[i])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,9 +737,9 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
|
||||
if (!newPrev)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
Throw<std::runtime_error>(
|
||||
"NFTokenPage directory for " + to_string(owner) +
|
||||
" cannot be repaired. Unexpected link problem.");
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"NFTokenPage directory for {} cannot be repaired. Unexpected link problem.",
|
||||
to_string(owner)));
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
newPrev->at(sfNextPageMin) = nextPage->key();
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -16,7 +20,9 @@
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
@@ -24,6 +30,406 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int
|
||||
fixedBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
// sfScale is never written for an integral asset (VaultCreate only sets
|
||||
// it when scale != 0, and integral assets always get scale 0), so it
|
||||
// must not be read here; vaultBaseScale ignores the scale argument for
|
||||
// an integral asset anyway.
|
||||
if (asset.integral())
|
||||
return vaultBaseScale(asset, 0);
|
||||
return vaultBaseScale(asset, vault->at(sfScale));
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
|
||||
[[nodiscard]] int
|
||||
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
|
||||
{
|
||||
return detail::posteriorAssetScale(
|
||||
getVaultVersion(vault), vault->at(sfAsset), fixedBaseScale(vault), reference, delta);
|
||||
}
|
||||
|
||||
// FixedPrecision-only counterpart to clampToAssetsTotalScale, for cash
|
||||
// outflows (VaultWithdraw, VaultClawback): rounds the magnitude of delta
|
||||
// toward zero at AssetsAvailable's own posterior scale, the balance the cash
|
||||
// actually leaves, instead of the derived AssetsTotal cache. Same
|
||||
// tecPRECISION_LOSS and integral-asset and magnitude-never-exceeds-delta
|
||||
// rules as clampToAssetsTotalScale. Not part of the public interface; reached
|
||||
// only through clampVaultOutflow.
|
||||
//
|
||||
// For an outflow it is safe to round the delta directly, unlike the credit
|
||||
// case: amount on the posterior grid of AssetsAvailable always leaves
|
||||
// AssetsAvailable minus amount representable, since the posterior grid is
|
||||
// exactly the grid AssetsAvailable itself will canonicalize to after the
|
||||
// subtraction.
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAvailableScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::clampToAvailableScale : delta and vault asset match");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::clampToAvailableScale : FixedPrecision Vault");
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampToAvailableScale : outflow delta is negative");
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount magnitude = delta.negative() ? -delta : delta;
|
||||
if (asset.integral())
|
||||
return magnitude;
|
||||
|
||||
STAmount const rounded = roundToScale(
|
||||
delta,
|
||||
posteriorScale(vault, vault->at(sfAssetsAvailable), delta),
|
||||
Number::RoundingMode::TowardsZero);
|
||||
STAmount actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
|
||||
XRPL_ASSERT(
|
||||
abs(actualDelta) <= abs(delta),
|
||||
"xrpl::clampToAvailableScale : actual delta smaller or equal to calculated delta");
|
||||
|
||||
// Reject changes below scale precision (1 ULP) to prevent share balance changes
|
||||
// without corresponding asset movements.
|
||||
if (actualDelta <= beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
// Write cached AssetsTotal from getAssetsTotal, as the exact total rounded
|
||||
// Downward to the Vault asset's 16-digit STAmount representation. Below
|
||||
// coarsening this equals the exact total; above it, the cache is a
|
||||
// deliberate floor of the exact value. Internal to adjustVaultBalances,
|
||||
// the only writer of AssetsAvailable, AssetsDeployed, AssetsTotal,
|
||||
// YieldUnrealized and LossUnrealized on a FixedPrecision Vault after
|
||||
// creation; not part of the public interface.
|
||||
void
|
||||
syncAssetsTotal(SLE::Ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::syncAssetsTotal : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
|
||||
// adjustVaultBalances writes AssetsAvailable as an already-canonical
|
||||
// STAmount before calling this, so the associateAsset pass a transactor
|
||||
// runs afterwards is always a no-op. If this fires, AssetsAvailable was
|
||||
// written some other way.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, vault->at(sfAssetsAvailable)} == vault->at(sfAssetsAvailable)),
|
||||
"xrpl::syncAssetsTotal : AssetsAvailable is a 16-digit STAmount value");
|
||||
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale)
|
||||
{
|
||||
if (reference == beast::kZero)
|
||||
return baseScale;
|
||||
// Round Downward, not the ambient mode: the live exponent must match the
|
||||
// exponent syncAssetsTotal actually stores (also Downward), regardless of
|
||||
// what rounding mode the caller's own arithmetic is using.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return std::max(baseScale, scale(reference, asset));
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta)
|
||||
{
|
||||
// ToNearest for the sum and for scale()'s canonicalization of it, as the
|
||||
// Legacy/CashBasis clamp always did; liveScale sets its own mode.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
Number const posterior = reference + delta;
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(posterior, asset);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(posterior, asset, baseScale);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::detail::posteriorAssetScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw, at atScale. See
|
||||
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
|
||||
// can still leave a 17-digit sum once the reference has crossed a power
|
||||
// of ten.
|
||||
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
|
||||
// flooredSum - reference can carry 17 significant digits (the sum is on
|
||||
// the posterior grid, the reference on the finer one); build the
|
||||
// STAmount under roundingMode, not the caller's ambient mode, so the
|
||||
// credit this returns never exceeds raw.
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, flooredSum - reference};
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::getPosteriorVaultScale : delta and Vault asset match");
|
||||
return posteriorScale(vault, getAssetsTotal(vault), delta);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getAssetsTotal : valid Vault sle");
|
||||
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return vault->at(sfAssetsTotal);
|
||||
|
||||
// AssetsAvailable is at -Scale (or coarser) and AssetsDeployed is always at
|
||||
// -Scale, so the sum is exact while it fits 19 digits. Fix the mode so a
|
||||
// sum that does not fit rounds the same way for every caller.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return vault->at(sfAssetsAvailable) + vault->at(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::adjustVaultBalances : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::adjustVaultBalances : FixedPrecision Vault");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount const cash = change.cash.value_or(STAmount{asset, 0});
|
||||
XRPL_ASSERT(cash.asset() == asset, "xrpl::adjustVaultBalances : cash and Vault asset match");
|
||||
// This is the mode the ledger's own transfers (trust line / MPT balance
|
||||
// updates) normalize in; computing AA' with any other ambient mode would
|
||||
// not reproduce what the real transfer just did to the backing balance.
|
||||
XRPL_ASSERT(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::adjustVaultBalances : ambient rounding mode is ToNearest");
|
||||
|
||||
// AA' is the exact trust-line/MPT arithmetic the ledger uses for the
|
||||
// transfer itself: a Number sum re-canonicalized to the asset's 16-digit
|
||||
// STAmount precision. MPT and XRP sums are already integral, so this is
|
||||
// a no-op rounding for them.
|
||||
//
|
||||
// AssetsDeployed, LossUnrealized and YieldUnrealized move no cash, so they stay
|
||||
// exact Number sums; the asserts below check each fits 16 digits, rather
|
||||
// than letting an STAmount conversion round it silently.
|
||||
STAmount const availableAfter{asset, Number(vault->at(sfAssetsAvailable)) + Number(cash)};
|
||||
Number const assetsDeployedAfter = Number(vault->at(sfAssetsDeployed)) + change.deployed;
|
||||
Number const lossUnrealizedAfter = Number(vault->at(sfLossUnrealized)) + change.loss;
|
||||
Number yieldUnrealizedAfter = Number(vault->at(sfYieldUnrealized)) + change.yield;
|
||||
|
||||
if (availableAfter < beast::kZero || assetsDeployedAfter < beast::kZero ||
|
||||
lossUnrealizedAfter < beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "adjustVaultBalances: a balance would become negative."
|
||||
<< " AssetsAvailable: " << Number(availableAfter)
|
||||
<< ", AssetsDeployed: " << assetsDeployedAfter
|
||||
<< ", LossUnrealized: " << lossUnrealizedAfter;
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The impairment rule: a Loan can only be impaired for as much as it
|
||||
// still owes. Reachable only through LoanManage's impair.
|
||||
if (lossUnrealizedAfter > assetsDeployedAfter)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
if (yieldUnrealizedAfter < beast::kZero)
|
||||
{
|
||||
JLOG(j.warn()) << "adjustVaultBalances: YieldUnrealized would become negative: "
|
||||
<< yieldUnrealizedAfter << "; clamping to zero.";
|
||||
yieldUnrealizedAfter = kNumZero;
|
||||
}
|
||||
|
||||
// Every caller derives principal from a Loan's own PrincipalOutstanding so the new total must
|
||||
// stay representable at the asset's own 16-digit precision too.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, assetsDeployedAfter} == assetsDeployedAfter),
|
||||
"xrpl::adjustVaultBalances : AssetsDeployed is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, lossUnrealizedAfter} == lossUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : LossUnrealized is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, yieldUnrealizedAfter} == yieldUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : YieldUnrealized is a 16-digit STAmount value");
|
||||
|
||||
vault->at(sfAssetsAvailable) = availableAfter;
|
||||
vault->at(sfAssetsDeployed) = assetsDeployedAfter;
|
||||
vault->at(sfLossUnrealized) = lossUnrealizedAfter;
|
||||
vault->at(sfYieldUnrealized) = yieldUnrealizedAfter;
|
||||
syncAssetsTotal(vault);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
|
||||
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(assetsTotal, vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return detail::liveScale(assetsTotal, vault->at(sfAsset), fixedBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultBaseScale : valid Vault sle");
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return getVaultScale(vault);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return fixedBaseScale(vault);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultBaseScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::roundToPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::roundToPosteriorVaultScale : amount and Vault asset match");
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
return roundToScale(amount, detail::getPosteriorVaultScale(vault, amount), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorAvailableScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
int const scale = posteriorScale(vault, reference, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultOpenLimit : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::getVaultOpenLimit : FixedPrecision Vault");
|
||||
return Number{9, 15 + getVaultBaseScale(vault)};
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::checkOptionalVaultInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalVaultInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalVaultInflow : non-negative amount");
|
||||
if (amount.negative())
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero);
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though a non-negative YieldUnrealized makes the
|
||||
// Open-zone capacity ceiling reject every coarsening transition too. The
|
||||
// protocol defines both conditions independently.
|
||||
if (detail::getPosteriorVaultScale(vault, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
if (vaultOpenZoneCapacity(vault, rounded) > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::vaultOpenZoneCapacity : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::vaultOpenZoneCapacity : FixedPrecision Vault");
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
|
||||
{
|
||||
@@ -34,7 +440,7 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (assets.negative() || assets.asset() != vault->at(sfAsset))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
STAmount shares{vault->at(sfShareMPTID)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -58,7 +464,7 @@ sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
STAmount assets{vault->at(sfAsset)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -85,37 +491,52 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
return magnitude;
|
||||
}
|
||||
Number const assetsTotal = vault->at(sfAssetsTotal);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
STAmount actualDelta;
|
||||
if (delta.negative())
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
// FixedPrecision only reaches this branch for credits (VaultDeposit);
|
||||
// outflows go through clampVaultOutflow -> clampToAvailableScale,
|
||||
// which rounds at AssetsAvailable's own posterior scale instead.
|
||||
XRPL_ASSERT(
|
||||
!delta.negative(), "xrpl::clampToAssetsTotalScale : FixedPrecision credit only");
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero);
|
||||
actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
if (delta.negative())
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
@@ -130,10 +551,120 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] TenthBips32
|
||||
getEarlyExitFeeRate(ReadView const& view, SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::getEarlyExitFeeRate : valid Vault sle");
|
||||
|
||||
if (!view.rules().enabled(featureLendingProtocolV1_2))
|
||||
return TenthBips32{0};
|
||||
|
||||
auto const feeRate = vault->at(~sfEarlyExitFeeRate);
|
||||
if (!feeRate || getVaultPhase(view, vault) != VaultPhase::Investment)
|
||||
return TenthBips32{0};
|
||||
|
||||
return TenthBips32{*feeRate};
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
calculateEarlyExitFee(SLE::ConstRef vault, STAmount const& amount, TenthBips32 rate)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::calculateEarlyExitFee : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::calculateEarlyExitFee : assets and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::calculateEarlyExitFee : non-negative assets");
|
||||
XRPL_ASSERT(rate <= kMaxEarlyExitFeeRate, "xrpl::calculateEarlyExitFee : valid fee rate");
|
||||
|
||||
if (rate == TenthBips32{0} || amount == beast::kZero)
|
||||
return STAmount{amount.asset()};
|
||||
|
||||
// At 100% the whole withdrawal is retained and nothing leaves the vault.
|
||||
if (rate == kMaxEarlyExitFeeRate)
|
||||
return amount;
|
||||
|
||||
// Round the post-fee payout down on the grid AssetsAvailable will land on.
|
||||
STAmount const payout = [&] {
|
||||
// amount * rate can need more digits than Number keeps, so round the
|
||||
// fee up first: rounding it down would lift the payout above its exact
|
||||
// value, and the clamp below cannot always pull it back onto the grid.
|
||||
Number const feeRoundedUp = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Upward);
|
||||
return tenthBipsOfValue(Number{amount}, rate);
|
||||
}();
|
||||
// Subtract and convert under Downward so the 16-digit STAmount can
|
||||
// never nudge the payout above the exact value.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return STAmount{amount.asset(), Number{amount} - feeRoundedUp};
|
||||
}();
|
||||
|
||||
// A withdrawal too small to leave anything behind at the asset's own
|
||||
// precision is retained in full, the same outcome as a 100% rate. Return
|
||||
// before the clamp, which requires a strictly negative delta.
|
||||
if (payout == beast::kZero)
|
||||
return amount;
|
||||
|
||||
// The clamp rejects a payout that rounds to zero on the AssetsAvailable
|
||||
// grid, which is the only way it can fail. That too means the fee retains
|
||||
// the whole withdrawal.
|
||||
auto const clampedPayout = clampVaultOutflow(vault, -payout);
|
||||
if (!clampedPayout)
|
||||
return amount;
|
||||
|
||||
// The clamped payout lies below the exact payout and strictly below
|
||||
// amount, so the fee is at least one unit and never exceeds amount.
|
||||
STAmount fee = amount - *clampedPayout;
|
||||
XRPL_ASSERT(
|
||||
fee > beast::kZero && fee < amount,
|
||||
"xrpl::calculateEarlyExitFee : fee is positive and below amount");
|
||||
|
||||
// Release builds skip the assert, so keep the fee within [0, amount] even
|
||||
// if the clamp ever returns a payout outside its contract.
|
||||
if (fee > amount)
|
||||
return amount; // LCOV_EXCL_LINE
|
||||
if (fee < beast::kZero)
|
||||
return STAmount{amount.asset()}; // LCOV_EXCL_LINE
|
||||
return fee;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampVaultOutflow : outflow delta is negative");
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? clampToAvailableScale(vault, delta)
|
||||
: clampToAssetsTotalScale(vault, delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale)
|
||||
{
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return -static_cast<int>(scale);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
|
||||
{
|
||||
return STAmount{asset, value} == value &&
|
||||
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount)
|
||||
{
|
||||
return isOnVaultBaseGrid(vault->at(sfAsset), amount, getVaultBaseScale(vault))
|
||||
? TER{tesSUCCESS}
|
||||
: TER{tecPRECISION_LOSS};
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
|
||||
{
|
||||
Number assetTotal = vault->at(sfAssetsTotal);
|
||||
Number assetTotal = getAssetsTotal(vault);
|
||||
if (waive == WaiveUnrealizedLoss::No)
|
||||
assetTotal -= vault->at(sfLossUnrealized);
|
||||
return assetTotal;
|
||||
@@ -147,6 +678,14 @@ debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal)
|
||||
{
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? Number(vault->at(sfAssetsAvailable))
|
||||
: assetsTotal;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesWithdraw(
|
||||
SLE::ConstRef vault,
|
||||
@@ -218,34 +757,40 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault)
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
|
||||
if (!vault->isFieldPresent(sfLEVersion))
|
||||
if (!leVersion)
|
||||
return VaultVersion::Legacy;
|
||||
|
||||
auto const version = vault->at(sfLEVersion);
|
||||
if (version > std::to_underlying(VaultVersion::CashBasis))
|
||||
if (*leVersion > std::to_underlying(VaultVersion::FixedPrecision))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
|
||||
UNREACHABLE("xrpl::decodeVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(version);
|
||||
return static_cast<VaultVersion>(*leVersion);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
|
||||
return decodeVaultVersion(vault->at(~sfLEVersion));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
[[nodiscard]] VaultVersion
|
||||
vaultVersionFor(Rules const& rules)
|
||||
{
|
||||
// FixedPrecision requires featureLendingProtocolV1_1 (closed-ended Vaults and
|
||||
// cash-basis accounting) and fixCleanup3_4_0, so a FixedPrecision Vault always
|
||||
// sees both enabled. fixCleanup3_2_0 is already enabled on the network.
|
||||
if (rules.enabled(featureLendingProtocolV1_1) && rules.enabled(featureLendingProtocolV1_2) &&
|
||||
rules.enabled(fixCleanup3_4_0))
|
||||
return VaultVersion::FixedPrecision;
|
||||
if (rules.enabled(featureLendingProtocolV1_1))
|
||||
return VaultVersion::CashBasis;
|
||||
return VaultVersion::Legacy;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
getVaultKind(SLE::ConstRef vault)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
@@ -193,7 +194,7 @@ public:
|
||||
if (!s.ok())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Unable to set RocksDB bbt_options: ") + s.ToString());
|
||||
std::format("Unable to set RocksDB bbt_options: {}", s.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +207,7 @@ public:
|
||||
if (!s.ok())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Unable to set RocksDB options: ") + s.ToString());
|
||||
std::format("Unable to set RocksDB options: {}", s.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +242,7 @@ public:
|
||||
if (!status.ok() || (localDb == nullptr))
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Unable to open/create RocksDB: ") + status.ToString());
|
||||
std::format("Unable to open/create RocksDB: {}", status.ToString()));
|
||||
}
|
||||
db.reset(localDb);
|
||||
}
|
||||
@@ -356,7 +357,7 @@ public:
|
||||
auto ret = db->Write(options, &wb);
|
||||
|
||||
if (!ret.ok())
|
||||
Throw<std::runtime_error>("storeBatch failed: " + ret.ToString());
|
||||
Throw<std::runtime_error>(std::format("storeBatch failed: {}", ret.ToString()));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -13,34 +13,29 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifndef XRPLD_VERSION
|
||||
#error "XRPLD_VERSION must be defined"
|
||||
#endif
|
||||
|
||||
namespace xrpl::build_info {
|
||||
|
||||
namespace {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// The build version number. You must edit this for each release
|
||||
// and follow the format described at http://semver.org/
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.5.0-b0"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
//
|
||||
// Don't touch anything below this line
|
||||
//
|
||||
// Set by cmake/XrplVersion.cmake, see http://semver.org/
|
||||
constexpr char kVersionString[] = XRPLD_VERSION;
|
||||
|
||||
std::string
|
||||
buildVersionString()
|
||||
{
|
||||
std::string version = versionString;
|
||||
std::string version = kVersionString;
|
||||
|
||||
#if defined(DEBUG) || defined(SANITIZERS)
|
||||
// A version derived from git already names its commit in the metadata.
|
||||
bool const hasMetadata = version.contains('+');
|
||||
std::string metadata;
|
||||
|
||||
std::string const& commitHash = xrpl::git::getCommitHash();
|
||||
if (!commitHash.empty())
|
||||
if (!hasMetadata && !commitHash.empty())
|
||||
metadata += commitHash + ".";
|
||||
|
||||
#ifdef DEBUG
|
||||
@@ -56,7 +51,7 @@ buildVersionString()
|
||||
#endif
|
||||
|
||||
if (!metadata.empty())
|
||||
version += "+" + metadata;
|
||||
version += (hasMetadata ? "." : "+") + metadata;
|
||||
#endif
|
||||
|
||||
return version;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
@@ -105,8 +106,8 @@ Permission::Permission()
|
||||
if (type <= UINT16_MAX)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
Throw<std::logic_error>(
|
||||
"Granular permission value must exceed the maximum uint16_t value: " + name);
|
||||
Throw<std::logic_error>(std::format(
|
||||
"Granular permission value must exceed the maximum uint16_t value: {}", name));
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,9 @@ setCurrentTransactionRules(std::optional<Rules> r)
|
||||
auto const range = [&r]() {
|
||||
// If any new conditions with new amendments are added to "enableLargeNumbers", those
|
||||
// amendments must also be added to useRulesGuards.
|
||||
bool const enableLargeNumbers =
|
||||
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
|
||||
bool const enableLargeNumbers = !r ||
|
||||
(r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol) ||
|
||||
r->enabled(featureMPTokensV2));
|
||||
// If enableLargeNumbers is true, then useRulesGuards must also return true.
|
||||
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
|
||||
// even though large numbers are _not_ used.
|
||||
@@ -84,7 +85,8 @@ useRulesGuards(Rules const& rules)
|
||||
// with createGuards, and any other callers, and the first set of guards can be created directly
|
||||
// at the call site, without using optional.
|
||||
return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) ||
|
||||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0);
|
||||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0) ||
|
||||
rules.enabled(featureMPTokensV2);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <format>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
@@ -1193,8 +1194,7 @@ muldiv(std::uint64_t multiplier, std::uint64_t multiplicand, std::uint64_t divis
|
||||
if (ret > std::numeric_limits<std::uint64_t>::max())
|
||||
{
|
||||
Throw<std::overflow_error>(
|
||||
"overflow: (" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
|
||||
") / " + std::to_string(divisor));
|
||||
std::format("overflow: ({} * {}) / {}", multiplier, multiplicand, divisor));
|
||||
}
|
||||
|
||||
return static_cast<uint64_t>(ret);
|
||||
@@ -1215,9 +1215,8 @@ muldivRound(
|
||||
|
||||
if (ret > std::numeric_limits<std::uint64_t>::max())
|
||||
{
|
||||
Throw<std::overflow_error>(
|
||||
"overflow: ((" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
|
||||
") + " + std::to_string(rounding) + ") / " + std::to_string(divisor));
|
||||
Throw<std::overflow_error>(std::format(
|
||||
"overflow: (({} * {}) + {}) / {}", multiplier, multiplicand, rounding, divisor));
|
||||
}
|
||||
|
||||
return static_cast<uint64_t>(ret);
|
||||
|
||||
@@ -35,9 +35,8 @@ STLedgerEntry::STLedgerEntry(Keylet const& k) : STObject(sfLedgerEntry), key_(k.
|
||||
|
||||
if (format == nullptr)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Attempt to create a SLE of unknown type " +
|
||||
std::to_string(safeCast<std::uint16_t>(k.type)));
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Attempt to create a SLE of unknown type {}", safeCast<std::uint16_t>(k.type)));
|
||||
}
|
||||
|
||||
set(format->getSOTemplate());
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
@@ -170,7 +171,7 @@ partsFromString(std::string const& number)
|
||||
boost::smatch match;
|
||||
|
||||
if (!boost::regex_match(number, match, kReNumber))
|
||||
Throw<std::runtime_error>("'" + number + "' is not a number");
|
||||
Throw<std::runtime_error>(std::format("'{}' is not a number", number));
|
||||
|
||||
// Match fields:
|
||||
// 0 = whole input
|
||||
|
||||
@@ -59,9 +59,8 @@ getTxFormat(TxType type)
|
||||
|
||||
if (format == nullptr)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid transaction type " +
|
||||
std::to_string(safeCast<std::underlying_type_t<TxType>>(type)));
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Invalid transaction type {}", safeCast<std::underlying_type_t<TxType>>(type)));
|
||||
}
|
||||
|
||||
return format;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -22,7 +22,7 @@ STVector256::STVector256(SerialIter& sit, SField const& name) : STBase(name)
|
||||
if (slice.size() % UInt256::size() != 0)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Bad serialization for STVector256: " + std::to_string(slice.size()));
|
||||
std::format("Bad serialization for STVector256: {}", slice.size()));
|
||||
}
|
||||
|
||||
auto const cnt = slice.size() / UInt256::size();
|
||||
|
||||
@@ -70,7 +70,8 @@ STXChainBridge::STXChainBridge(SField const& name, json::Value const& v) : STBas
|
||||
std::string const name = it.memberName();
|
||||
if (!kBridgeJson.isMember(name))
|
||||
{
|
||||
Throw<std::runtime_error>("STXChainBridge extra field detected: " + name);
|
||||
Throw<std::runtime_error>(
|
||||
std::format("STXChainBridge extra field detected: {}", name));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -456,7 +456,7 @@ SerialIter::getFieldID(int& type, int& name)
|
||||
// uncommon type
|
||||
type = get8();
|
||||
if (type < 16)
|
||||
Throw<std::runtime_error>("gFID: uncommon type out of range " + std::to_string(type));
|
||||
Throw<std::runtime_error>(std::format("gFID: uncommon type out of range {}", type));
|
||||
}
|
||||
|
||||
if (name == 0)
|
||||
@@ -464,7 +464,7 @@ SerialIter::getFieldID(int& type, int& name)
|
||||
// uncommon name
|
||||
name = get8();
|
||||
if (name < 16)
|
||||
Throw<std::runtime_error>("gFID: uncommon name out of range " + std::to_string(name));
|
||||
Throw<std::runtime_error>(std::format("gFID: uncommon name out of range {}", name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -13,10 +15,12 @@
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <flat_set>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -145,6 +149,52 @@ TxMeta::getAffectedAccounts() const
|
||||
return list;
|
||||
}
|
||||
|
||||
std::flat_set<MPTID>
|
||||
TxMeta::getAffectedMPTs() const
|
||||
{
|
||||
std::flat_set<MPTID> list;
|
||||
|
||||
for (auto const& it : nodes_)
|
||||
{
|
||||
int const index =
|
||||
it.getFieldIndex((it.getFName() == sfCreatedNode) ? sfNewFields : sfFinalFields);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
auto inner = dynamic_cast<STObject const*>(&it.peekAtIndex(index));
|
||||
XRPL_ASSERT(inner, "xrpl::getAffectedMPTs : STObject type cast succeeded");
|
||||
if (inner != nullptr)
|
||||
{
|
||||
// An MPTokenIssuance entry does not store its own issuance id;
|
||||
// the id is derived from the issuer and the sequence that
|
||||
// created the issuance.
|
||||
if (it.getFieldU16(sfLedgerEntryType) == ltMPTOKEN_ISSUANCE)
|
||||
{
|
||||
list.insert(
|
||||
makeMptID(inner->getFieldU32(sfSequence), inner->getAccountID(sfIssuer)));
|
||||
}
|
||||
|
||||
for (auto const& field : *inner)
|
||||
{
|
||||
if (auto mptID = dynamic_cast<STBitString<192> const*>(&field);
|
||||
field.getFName() == sfMPTokenIssuanceID && (mptID != nullptr))
|
||||
{
|
||||
list.insert(mptID->value());
|
||||
}
|
||||
else if (
|
||||
auto amount = dynamic_cast<STAmount const*>(&field);
|
||||
(amount != nullptr) && amount->holds<MPTIssue>())
|
||||
{
|
||||
list.insert(amount->get<MPTIssue>().getMptID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
STObject&
|
||||
TxMeta::getAffectedNode(SLE::Ref node, SField const& type)
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include <soci/sqlite3/soci-sqlite3.h> // IWYU pragma: keep
|
||||
|
||||
#include <format>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -40,8 +41,8 @@ getSociSqliteInit(std::string const& name, std::string const& dir, std::string c
|
||||
{
|
||||
if (name.empty())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Sqlite databases must specify a dir and a name. Name: " + name + " Dir: " + dir);
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Sqlite databases must specify a dir and a name. Name: {} Dir: {}", name, dir));
|
||||
}
|
||||
std::filesystem::path file(dir);
|
||||
if (std::filesystem::is_directory(file))
|
||||
@@ -56,7 +57,7 @@ getSociInit(BasicConfig const& config, std::string const& dbName)
|
||||
auto const backendName = get(section, Keys::kBackend, "sqlite");
|
||||
|
||||
if (backendName != "sqlite")
|
||||
Throw<std::runtime_error>("Unsupported soci backend: " + backendName);
|
||||
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", backendName));
|
||||
|
||||
auto const path = config.legacy(Sections::kDatabasePath);
|
||||
auto const ext = dbName == "validators" || dbName == "peerfinder" ? ".sqlite" : ".db";
|
||||
@@ -101,7 +102,7 @@ open(soci::session& s, std::string const& beName, std::string const& connectionS
|
||||
}
|
||||
else
|
||||
{
|
||||
Throw<std::runtime_error>("Unsupported soci backend: " + beName);
|
||||
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", beName));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -42,6 +44,22 @@ safeUnsub(std::uint64_t seq, F&& f, beast::Journal j) noexcept
|
||||
}
|
||||
}
|
||||
|
||||
// Number of requested entries not already tracked in `existing`. Only these are
|
||||
// charged against the cap, so re-subscribing entries a connection already holds
|
||||
// is free.
|
||||
template <typename T>
|
||||
[[nodiscard]] std::size_t
|
||||
countNew(HashSet<T> const& requested, HashSet<T> const& existing)
|
||||
{
|
||||
std::size_t fresh = 0;
|
||||
for (auto const& entry : requested)
|
||||
{
|
||||
if (!existing.contains(entry))
|
||||
++fresh;
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// This is the primary interface into the "client" portion of the program.
|
||||
@@ -84,6 +102,16 @@ InfoSub::~InfoSub()
|
||||
safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j);
|
||||
|
||||
// MPT subscriptions are torn down inline here, keyed on seq_, like books
|
||||
// below. The set is capped; each unsubMPTInternal takes mptLock_ for a
|
||||
// single O(1) erase and releases it, so a competing MPT publish can
|
||||
// interleave between erases. Use the internal variant so it does not write
|
||||
// back to mptSubscriptions_ on this partially-destroyed object.
|
||||
for (auto const& mptID : mptSubscriptions_)
|
||||
{
|
||||
safeUnsub(seq_, [&] { source_.unsubMPTInternal(seq_, mptID); }, j);
|
||||
}
|
||||
|
||||
// Book subscriptions are torn down inline here, keyed on seq_, rather than
|
||||
// through the chunked account cleanup below. The book set is not capped, so
|
||||
// it can be large; but each unsubBookInternal takes bookLock_ for a single
|
||||
@@ -145,16 +173,21 @@ InfoSub::onSendEmpty()
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
InfoSub::subscriptionCount(ScopedLock const&) const
|
||||
{
|
||||
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size() + mptSubscriptions_.size();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
InfoSub::totalSubscriptionCount() const
|
||||
{
|
||||
// Hold lock_ for the whole read so the three sets cannot be mutated
|
||||
// Hold lock_ for the whole read so the counted sets cannot be mutated
|
||||
// mid-count by a concurrent (un)subscribe on this connection.
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
// Combined tally the per-connection cap is enforced against.
|
||||
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size();
|
||||
return subscriptionCount(sl);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -166,25 +199,10 @@ InfoSub::tryReserveAccountSubscriptions(
|
||||
// One lock hold covers the count, the check and the insert.
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
// Entries not already tracked; re-subscribing held accounts is not charged.
|
||||
auto const countNew = [](HashSet<AccountID> const& requested,
|
||||
HashSet<AccountID> const& existing) {
|
||||
std::size_t fresh = 0;
|
||||
for (auto const& account : requested)
|
||||
{
|
||||
if (!existing.contains(account))
|
||||
++fresh;
|
||||
}
|
||||
return fresh;
|
||||
};
|
||||
|
||||
std::size_t const additional = countNew(proposedAccounts, realTimeSubscriptions_) +
|
||||
countNew(normalAccounts, normalSubscriptions_);
|
||||
|
||||
std::size_t const current = normalSubscriptions_.size() + realTimeSubscriptions_.size() +
|
||||
accountHistorySubscriptions_.size();
|
||||
|
||||
if (exceedsSubscriptionCap(current, additional, cap))
|
||||
if (exceedsSubscriptionCap(subscriptionCount(sl), additional, cap))
|
||||
return false;
|
||||
|
||||
realTimeSubscriptions_.insert(proposedAccounts.begin(), proposedAccounts.end());
|
||||
@@ -192,6 +210,19 @@ InfoSub::tryReserveAccountSubscriptions(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
InfoSub::tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap)
|
||||
{
|
||||
// One lock hold covers the count, the check and the insert.
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
if (exceedsSubscriptionCap(subscriptionCount(sl), countNew(mptIDs, mptSubscriptions_), cap))
|
||||
return false;
|
||||
|
||||
mptSubscriptions_.insert(mptIDs.begin(), mptIDs.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::insertSubAccountInfo(AccountID const& account, bool rt)
|
||||
{
|
||||
@@ -288,4 +319,20 @@ InfoSub::getApiVersion() const noexcept
|
||||
return apiVersion_;
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::insertSubMPTInfo(MPTID const& mptID)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
mptSubscriptions_.insert(mptID);
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::deleteSubMPTInfo(MPTID const& mptID)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
mptSubscriptions_.erase(mptID);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
@@ -48,7 +49,7 @@ to_string(Manifest const& m)
|
||||
return "Revocation Manifest " + mk;
|
||||
|
||||
if (!m.signingKey)
|
||||
Throw<std::runtime_error>("No SigningKey in manifest " + mk);
|
||||
Throw<std::runtime_error>(std::format("No SigningKey in manifest {}", mk));
|
||||
|
||||
return "Manifest " + mk + " (" + std::to_string(m.sequence) + ": " +
|
||||
toBase58(TokenType::NodePublic, *m.signingKey) + ")";
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -107,10 +108,10 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
|
||||
depth_ <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::getChildNodeID : maximum leaf depth");
|
||||
|
||||
if (depth_ >= SHAMap::kLeafDepth)
|
||||
Throw<std::logic_error>("Request for child node ID of " + to_string(*this));
|
||||
Throw<std::logic_error>(std::format("Request for child node ID of {}", to_string(*this)));
|
||||
|
||||
if (!isPrefixOf(id_))
|
||||
Throw<std::logic_error>("Incorrect mask for " + to_string(*this));
|
||||
Throw<std::logic_error>(std::format("Incorrect mask for {}", to_string(*this)));
|
||||
|
||||
SHAMapNodeID node{depth_ + 1, id_};
|
||||
node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? branch : (branch << 4);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <xrpl/shamap/SHAMapTxPlusMetaLeafNode.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
@@ -30,9 +31,8 @@ SHAMapTreeNode::makeTransaction(Slice data, SHAMapHash const& hash, bool hashVal
|
||||
{
|
||||
if (data.size() < kMinShaMapItemBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Short TXN node: " + std::to_string(data.size()) + " bytes (minimum " +
|
||||
std::to_string(kMinShaMapItemBytes) + " required)");
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short TXN node: {} bytes (minimum {} required)", data.size(), kMinShaMapItemBytes));
|
||||
}
|
||||
|
||||
auto item = makeShamapitem(sha512Half(HashPrefix::TransactionId, data), data);
|
||||
@@ -52,27 +52,25 @@ SHAMapTreeNode::makeTransactionWithMeta(Slice data, SHAMapHash const& hash, bool
|
||||
|
||||
if (s.size() < tag.kBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Short TXN+MD node: " + std::to_string(s.size()) + " bytes (minimum " +
|
||||
std::to_string(tag.kBytes) + " required for tag)");
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short TXN+MD node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
|
||||
}
|
||||
|
||||
// FIXME: improve this interface so that the above check isn't needed
|
||||
if (!s.getBitString(tag, s.size() - tag.kBytes))
|
||||
{
|
||||
Throw<std::out_of_range>(
|
||||
"Short TXN+MD node: failed to read tag at offset " +
|
||||
std::to_string(s.size() - tag.kBytes));
|
||||
Throw<std::out_of_range>(std::format(
|
||||
"Short TXN+MD node: failed to read tag at offset {}", s.size() - tag.kBytes));
|
||||
}
|
||||
|
||||
s.chop(tag.kBytes);
|
||||
|
||||
if (s.size() < kMinShaMapItemBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Short TXN+MD node: " + std::to_string(s.size()) +
|
||||
" bytes after tag removal (minimum " + std::to_string(kMinShaMapItemBytes) +
|
||||
" required)");
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short TXN+MD node: {} bytes after tag removal (minimum {} required)",
|
||||
s.size(),
|
||||
kMinShaMapItemBytes));
|
||||
}
|
||||
|
||||
auto item = makeShamapitem(tag, s.slice());
|
||||
@@ -92,16 +90,15 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
|
||||
|
||||
if (s.size() < tag.kBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Short AS node: " + std::to_string(s.size()) + " bytes (minimum " +
|
||||
std::to_string(tag.kBytes) + " required for tag)");
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short AS node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
|
||||
}
|
||||
|
||||
// FIXME: improve this interface so that the above check isn't needed
|
||||
if (!s.getBitString(tag, s.size() - tag.kBytes))
|
||||
{
|
||||
Throw<std::out_of_range>(
|
||||
"Short AS node: failed to read tag at offset " + std::to_string(s.size() - tag.kBytes));
|
||||
std::format("Short AS node: failed to read tag at offset {}", s.size() - tag.kBytes));
|
||||
}
|
||||
|
||||
s.chop(tag.kBytes);
|
||||
@@ -111,9 +108,10 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
|
||||
|
||||
if (s.size() < kMinShaMapItemBytes)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Short AS node: " + std::to_string(s.size()) + " bytes after tag removal (minimum " +
|
||||
std::to_string(kMinShaMapItemBytes) + " required)");
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"Short AS node: {} bytes after tag removal (minimum {} required)",
|
||||
s.size(),
|
||||
kMinShaMapItemBytes));
|
||||
}
|
||||
|
||||
auto item = makeShamapitem(tag, s.slice());
|
||||
@@ -152,7 +150,7 @@ SHAMapTreeNode::makeFromWire(Slice rawNode)
|
||||
if (type == kWireTypeTransactionWithMeta)
|
||||
return makeTransactionWithMeta(rawNode, hash, hashValid);
|
||||
|
||||
Throw<std::runtime_error>("wire: Unknown type (" + std::to_string(type) + ")");
|
||||
Throw<std::runtime_error>(std::format("wire: Unknown type ({})", type));
|
||||
}
|
||||
|
||||
SHAMapTreeNodePtr
|
||||
@@ -183,9 +181,8 @@ SHAMapTreeNode::makeFromPrefix(Slice rawNode, SHAMapHash const& hash)
|
||||
if (type == HashPrefix::TxNode)
|
||||
return makeTransactionWithMeta(rawNode, hash, hashValid);
|
||||
|
||||
Throw<std::runtime_error>(
|
||||
"prefix: unknown type (" +
|
||||
std::to_string(safeCast<std::underlying_type_t<HashPrefix>>(type)) + ")");
|
||||
Throw<std::runtime_error>(std::format(
|
||||
"prefix: unknown type ({})", safeCast<std::underlying_type_t<HashPrefix>>(type)));
|
||||
}
|
||||
|
||||
std::string
|
||||
|
||||
@@ -1229,6 +1229,10 @@ NoModifiedUnmodifiableFields::finalize(
|
||||
kFieldChanged(before, after, sfAccount) ||
|
||||
kFieldChanged(before, after, sfShareMPTID);
|
||||
}
|
||||
// sfEarlyExitFeeRate is set only by VaultCreate: an existing value keeps it, an
|
||||
// absent field stays absent.
|
||||
if (view.rules().enabled(featureLendingProtocolV1_2))
|
||||
bad = bad || kFieldChanged(before, after, sfEarlyExitFeeRate);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -160,7 +160,7 @@ ValidLoanBroker::finalize(
|
||||
roundToAsset(
|
||||
Asset{vault->at(sfAsset)},
|
||||
debtTotal,
|
||||
getAssetsTotalScale(vault),
|
||||
getVaultScale(vault),
|
||||
Number::RoundingMode::TowardsZero) != beast::kZero)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
|
||||
@@ -41,6 +41,7 @@ constexpr auto kConfidentialMptTxTypes = std::to_array<TxType>({
|
||||
ttCONFIDENTIAL_MPT_MERGE_INBOX,
|
||||
ttCONFIDENTIAL_MPT_CLAWBACK,
|
||||
ttCONFIDENTIAL_MPT_MIRROR_UPDATE,
|
||||
ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE,
|
||||
});
|
||||
|
||||
// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant
|
||||
@@ -73,7 +74,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
|
||||
// only meaningful post-fixCleanup3_2_0 (the field is never set
|
||||
// pre-amendment, and the holding-deletion rule does not apply).
|
||||
// Skip both blocks when the amendment is off so we avoid wasted work
|
||||
// on the hot path.
|
||||
// on the hot path, except where noted for fixCleanup3_5_0 below.
|
||||
bool const fix320Enabled = isFeatureEnabled(fixCleanup3_2_0);
|
||||
|
||||
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
|
||||
@@ -108,7 +109,10 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
|
||||
if (isDelete)
|
||||
{
|
||||
mptokensDeleted_++;
|
||||
if (fix320Enabled)
|
||||
// deletedHoldings_ also feeds finalize()'s erase-time public
|
||||
// balance check, gated on fixCleanup3_5_0 independently of
|
||||
// fixCleanup3_2_0.
|
||||
if (fix320Enabled || isFeatureEnabled(fixCleanup3_5_0))
|
||||
deletedHoldings_.push_back(after);
|
||||
}
|
||||
else if (!before)
|
||||
@@ -195,6 +199,19 @@ ValidMPTIssuance::finalize(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deleting an MPToken with a non-zero MPTAmount is rejected.
|
||||
if (rules.enabled(fixCleanup3_5_0))
|
||||
{
|
||||
for (auto const& sleHolding : deletedHoldings_)
|
||||
{
|
||||
if (sleHolding->getType() == ltMPTOKEN && sleHolding->getFieldU64(sfMPTAmount) > 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPToken deleted with non-zero balance";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isTesSuccess(result) || (mptV2Enabled && result == tecINCOMPLETE))
|
||||
{
|
||||
[[maybe_unused]]
|
||||
@@ -579,16 +596,27 @@ ValidConfidentialMPToken::visitEntry(
|
||||
change.mptAmountDelta =
|
||||
subtractMPTAmountDelta(change.mptAmountDelta, before->getFieldU64(sfMPTAmount));
|
||||
|
||||
// Cannot delete MPToken with non-zero confidential state or non-zero public amount
|
||||
// Cannot delete MPToken with non-zero confidential state.
|
||||
if (isDelete)
|
||||
{
|
||||
bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
|
||||
bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
// changes_ is keyed by issuance, so sibling holders erased by the
|
||||
// same transaction share this entry. Only ever set these flags,
|
||||
// never clear them, or an empty sibling visited later would mask
|
||||
// a funded MPToken.
|
||||
|
||||
// Retired by fixCleanup3_5_0, which moved the public balance
|
||||
// check to ValidMPTIssuance::finalize. Kept pre-amendment for
|
||||
// consensus safety: a non-zero public balance used to feed the
|
||||
// confidential gate below, rejecting the erase whenever the
|
||||
// issuance's COA was non-zero, and already-validated ledgers
|
||||
// depend on that.
|
||||
if (!isFeatureEnabled(fixCleanup3_5_0) && before->getFieldU64(sfMPTAmount) > 0)
|
||||
changes_[id].deletedWithBalanceBefore = true;
|
||||
|
||||
if (before->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
before->isFieldPresent(sfConfidentialBalanceInbox) ||
|
||||
before->isFieldPresent(sfIssuerEncryptedBalance) ||
|
||||
before->isFieldPresent(sfAuditorEncryptedBalance);
|
||||
|
||||
if (hasPublicBalance || hasEncryptedFields)
|
||||
before->isFieldPresent(sfAuditorEncryptedBalance))
|
||||
changes_[id].deletedWithEncrypted = true;
|
||||
}
|
||||
}
|
||||
@@ -694,6 +722,8 @@ ValidConfidentialMPToken::finalize(
|
||||
if (result != tesSUCCESS)
|
||||
return true;
|
||||
|
||||
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
|
||||
|
||||
for (auto const& [id, checks] : changes_)
|
||||
{
|
||||
// Find the MPTokenIssuance
|
||||
@@ -708,8 +738,20 @@ ValidConfidentialMPToken::finalize(
|
||||
if (!issuance)
|
||||
continue;
|
||||
|
||||
// Cannot delete MPToken with non-zero confidential state
|
||||
if (checks.deletedWithEncrypted)
|
||||
// Cannot delete MPToken with non-zero confidential state.
|
||||
//
|
||||
// Before fixCleanup3_5_0 this gate also absorbed the pre-transaction
|
||||
// public balance, so any drain-then-erase of an MPToken -- an
|
||||
// AMMWithdraw of the whole pool, a LoanBrokerDelete returning cover --
|
||||
// was rejected whenever some unrelated holder of the same issuance
|
||||
// held a confidential balance. The COA gate itself is correct for
|
||||
// ciphertext and mirrors MPTokenAuthorize::preclaim; only the public
|
||||
// balance leg was misplaced.
|
||||
bool const deletedWithEncrypted = fix350Enabled
|
||||
? checks.deletedWithEncrypted
|
||||
: (checks.deletedWithEncrypted || checks.deletedWithBalanceBefore);
|
||||
|
||||
if (deletedWithEncrypted)
|
||||
{
|
||||
if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
|
||||
{
|
||||
@@ -784,10 +826,10 @@ ValidConfidentialMPToken::finalize(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Among confidential MPT transactions, only ConfidentialMPTSend and
|
||||
// ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT
|
||||
// transaction reaches here, it must be one of these two types, neither of which will
|
||||
// modify sfOutstandingAmount
|
||||
// Reaching here means this confidential MPT transaction left coaDelta
|
||||
// unmodified (e.g. ConfidentialMPTSend, ConfidentialMPTMergeInbox, or
|
||||
// ConfidentialMPTHolderKeyUpdate/ConfidentialMPTMirrorUpdate, none of which touch
|
||||
// sfConfidentialOutstandingAmount), so it must not modify sfOutstandingAmount either.
|
||||
if (checks.outstandingDelta != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed "
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
@@ -63,9 +64,14 @@ ValidVault::Vault::make(SLE const& from)
|
||||
self.assetsAvailable = from.at(sfAssetsAvailable);
|
||||
self.assetsMaximum = from.at(sfAssetsMaximum);
|
||||
self.lossUnrealized = from.at(sfLossUnrealized);
|
||||
self.assetsDeployed = from[~sfAssetsDeployed].value_or(Number{0});
|
||||
self.yieldUnrealized = from[~sfYieldUnrealized].value_or(Number{0});
|
||||
self.version = decodeVaultVersion(from[~sfLEVersion]);
|
||||
self.vaultKind = from[~sfVaultKind];
|
||||
self.subscriptionDate = from[~sfSubscriptionDate];
|
||||
self.redemptionDate = from[~sfRedemptionDate];
|
||||
self.earlyExitFeeRate = from[~sfEarlyExitFeeRate];
|
||||
self.leVersion = from[~sfLEVersion];
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -408,6 +414,70 @@ ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules
|
||||
return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
|
||||
}
|
||||
|
||||
bool
|
||||
ValidVault::checkTotalsAddUp(
|
||||
Vault const& afterVault,
|
||||
Vault const& beforeVault,
|
||||
Asset const& vaultAsset,
|
||||
bool fix340Enabled,
|
||||
std::int32_t minScale,
|
||||
Number const& roundedVaultDelta,
|
||||
Number const& exactVaultDelta,
|
||||
char const* verb,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
// FixedPrecision: the stored AssetsTotal is a Downward-floored cache
|
||||
// (AA + AD), not an exact running total, so comparing its delta against
|
||||
// the real transfer can be off by more than one unit purely from
|
||||
// independent floor-rounding of the two snapshots -- not an accounting
|
||||
// bug. The stored-cache-equals-derived-total check elsewhere in finalize,
|
||||
// plus the AssetsDeployed-only-changed-by-lending-transactors check,
|
||||
// already cover AssetsTotal; only compare it here on Legacy/CashBasis,
|
||||
// where AssetsTotal is written exactly alongside AA.
|
||||
if (afterVault.version != VaultVersion::FixedPrecision)
|
||||
{
|
||||
auto const assetTotalDelta =
|
||||
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(assetTotalDelta, roundedVaultDelta, vaultAsset, minScale)
|
||||
: assetTotalDelta == roundedVaultDelta;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << verb
|
||||
<< " and assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
// FixedPrecision: AssetsAvailable is the exact, single writer
|
||||
// (adjustVaultBalances)-managed balance, so its delta must match the real
|
||||
// transfer exactly, with no rounding tolerance -- compare the exact
|
||||
// deltas, not the values rounded to the posterior AssetsTotal scale.
|
||||
bool const availableAddsUp = [&] {
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
return (afterVault.assetsAvailable - beforeVault.assetsAvailable) == exactVaultDelta;
|
||||
}
|
||||
if (fix340Enabled)
|
||||
{
|
||||
return agreesWithinOneUnit(
|
||||
assetAvailableDelta, roundedVaultDelta, vaultAsset, minScale);
|
||||
}
|
||||
return assetAvailableDelta == roundedVaultDelta;
|
||||
}();
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << verb << " and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidVault::finalize(
|
||||
STTx const& tx,
|
||||
@@ -418,6 +488,7 @@ ValidVault::finalize(
|
||||
{
|
||||
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
|
||||
bool const lendingV12Enabled = view.rules().enabled(featureLendingProtocolV1_2);
|
||||
|
||||
if (!isTesSuccess(ret))
|
||||
return true; // Do not perform checks
|
||||
@@ -585,6 +656,13 @@ ValidVault::finalize(
|
||||
"vault must have no assets available";
|
||||
result = false;
|
||||
}
|
||||
if (afterVault.version == VaultVersion::FixedPrecision &&
|
||||
afterVault.assetsDeployed != kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
|
||||
"vault must have no AssetsDeployed";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
|
||||
{
|
||||
@@ -609,6 +687,14 @@ ValidVault::finalize(
|
||||
else
|
||||
{
|
||||
bool const gapExceeded = [&] {
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// FixedPrecision: both LossUnrealized and AssetsDeployed are exact
|
||||
// (never rounded), so compare them directly. AssetsTotal is
|
||||
// just a rounded cache of AssetsAvailable + AssetsDeployed and
|
||||
// is not the reference this check needs.
|
||||
return afterVault.lossUnrealized > afterVault.assetsDeployed;
|
||||
}
|
||||
if (!fix340Enabled)
|
||||
{
|
||||
return afterVault.lossUnrealized >
|
||||
@@ -631,12 +717,67 @@ ValidVault::finalize(
|
||||
}
|
||||
}
|
||||
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (afterVault.assetsDeployed < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: AssetsDeployed must not be negative";
|
||||
result = false;
|
||||
}
|
||||
else if (
|
||||
Number(STAmount{afterVault.asset, afterVault.assetsDeployed}) !=
|
||||
afterVault.assetsDeployed)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: AssetsDeployed must be exactly representable "
|
||||
"at the vault asset's precision";
|
||||
result = false;
|
||||
}
|
||||
|
||||
Number const expectedAssetsTotal = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return Number(
|
||||
STAmount{afterVault.asset, afterVault.assetsAvailable + afterVault.assetsDeployed});
|
||||
}();
|
||||
if (afterVault.assetsTotal != expectedAssetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: stored AssetsTotal must equal AssetsAvailable "
|
||||
"plus AssetsDeployed, rounded Downward";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.yieldUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: YieldUnrealized must not be negative";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fix340Enabled && afterVault.lossUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (lendingV12Enabled && afterVault.earlyExitFeeRate)
|
||||
{
|
||||
bool const hasVersion = afterVault.leVersion &&
|
||||
*afterVault.leVersion >= std::to_underlying(VaultVersion::CashBasis);
|
||||
if (!isClosedEnded(afterVault.vaultKind) || !hasVersion)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: early-exit fee rate only allowed on a "
|
||||
"closed-ended vault with LEVersion >= CashBasis";
|
||||
result = false;
|
||||
}
|
||||
if (TenthBips32{*afterVault.earlyExitFeeRate} > kMaxEarlyExitFeeRate)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: early-exit fee rate must not exceed "
|
||||
"MAX_EARLY_EXIT_FEE_RATE";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (afterVault.assetsTotal < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: assets outstanding must not be negative";
|
||||
@@ -659,6 +800,18 @@ ValidVault::finalize(
|
||||
return !enforce; // That's all we can do here
|
||||
}
|
||||
|
||||
// ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE are the only transactors allowed to
|
||||
// write AssetsDeployed and YieldUnrealized; any new transactor that writes
|
||||
// either field must be added to both allow-lists below.
|
||||
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
|
||||
afterVault.assetsDeployed != beforeVault_[0].assetsDeployed && txnType != ttLOAN_SET &&
|
||||
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: vault transaction must not change AssetsDeployed";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
|
||||
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
|
||||
{
|
||||
@@ -668,6 +821,16 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
|
||||
afterVault.yieldUnrealized != beforeVault_[0].yieldUnrealized && txnType != ttLOAN_SET &&
|
||||
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: vault transaction must not change yield "
|
||||
"unrealized";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
|
||||
// NoModifiedUnmodifiableFields in InvariantCheck.cpp.
|
||||
|
||||
@@ -716,7 +879,9 @@ ValidVault::finalize(
|
||||
}
|
||||
|
||||
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
|
||||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
|
||||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0 ||
|
||||
(afterVault.version == VaultVersion::FixedPrecision &&
|
||||
(afterVault.assetsDeployed != kZero || afterVault.yieldUnrealized != kZero)))
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: created vault must be empty";
|
||||
@@ -880,7 +1045,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (vaultDeltaAssets <= kZero)
|
||||
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
|
||||
// delta is exact; rounding either one down to the posterior
|
||||
// AssetsTotal scale before the sign check can turn a legitimate
|
||||
// sub-unit deposit into a false zero. Compare the exact deltas
|
||||
// instead; Legacy/CashBasis keep the rounded comparison.
|
||||
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const exactVaultDeltaAssets = maybeVaultDeltaAssets->delta;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? exactVaultDeltaAssets : vaultDeltaAssets;
|
||||
|
||||
if (signCheckDeltaAssets <= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must increase vault balance";
|
||||
@@ -976,29 +1151,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: deposit and assets outstanding must add up";
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultDeltaAssets,
|
||||
exactVaultDeltaAssets,
|
||||
"deposit",
|
||||
j))
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1011,8 +1174,11 @@ ValidVault::finalize(
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
// Withdrawal from a closed-ended vault is not allowed during the Investment phase
|
||||
// (strictly past SubscriptionDate, before RedemptionDate).
|
||||
if (getVaultPhase(
|
||||
// (strictly past SubscriptionDate, before RedemptionDate), unless the vault was
|
||||
// created with an early-exit fee.
|
||||
bool const earlyExitAllowed = lendingV12Enabled && afterVault.earlyExitFeeRate;
|
||||
if (!earlyExitAllowed &&
|
||||
getVaultPhase(
|
||||
view,
|
||||
afterVault.vaultKind,
|
||||
afterVault.subscriptionDate,
|
||||
@@ -1036,10 +1202,16 @@ ValidVault::finalize(
|
||||
// value merely rounds down to zero, so a missing delta while
|
||||
// the pool still held positive effective value indicates a
|
||||
// real accounting bug, not this exception.
|
||||
bool const zeroDeltaIsLegitimate = fix340Enabled && !maybeVaultDeltaAssets &&
|
||||
beforeVault.assetsTotal == beforeVault.lossUnrealized;
|
||||
//
|
||||
// Post-featureLendingProtocolV1_2: the bounds are relaxed to "the vault balance
|
||||
// must not increase" and "the destination balance must not decrease", so a zero
|
||||
// payout is legitimate in any vault. An early exit at a 100% fee burns shares and
|
||||
// pays out nothing.
|
||||
bool const allowZeroVaultDelta = !maybeVaultDeltaAssets &&
|
||||
(lendingV12Enabled ||
|
||||
(fix340Enabled && beforeVault.assetsTotal == beforeVault.lossUnrealized));
|
||||
|
||||
if (!maybeVaultDeltaAssets && !zeroDeltaIsLegitimate)
|
||||
if (!maybeVaultDeltaAssets && !allowZeroVaultDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault balance";
|
||||
return false; // That's all we can do
|
||||
@@ -1054,7 +1226,19 @@ ValidVault::finalize(
|
||||
auto const vaultPseudoDeltaAssets =
|
||||
roundToAsset(vaultAsset, vaultDeltaAssets.delta, minScale);
|
||||
|
||||
if (!zeroDeltaIsLegitimate && vaultPseudoDeltaAssets >= kZero)
|
||||
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
|
||||
// delta is exact; rounding either one down to the posterior
|
||||
// AssetsTotal scale before the sign check can turn a legitimate
|
||||
// sub-unit withdrawal into a false zero and reject it. Compare
|
||||
// the exact deltas instead; Legacy/CashBasis keep the rounded
|
||||
// comparison.
|
||||
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? vaultDeltaAssets.delta : vaultPseudoDeltaAssets;
|
||||
|
||||
bool const vaultBalanceInvalid =
|
||||
isFixedPrecision ? signCheckDeltaAssets > kZero : signCheckDeltaAssets >= kZero;
|
||||
if (!allowZeroVaultDelta && vaultBalanceInvalid)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
|
||||
result = false;
|
||||
@@ -1103,7 +1287,7 @@ ValidVault::finalize(
|
||||
// A legitimate zero-value withdrawal moves nothing to
|
||||
// the recipient either; there is nothing left to
|
||||
// cross-check.
|
||||
if (!zeroDeltaIsLegitimate)
|
||||
if (!allowZeroVaultDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change one destination balance";
|
||||
@@ -1132,7 +1316,7 @@ ValidVault::finalize(
|
||||
// XRP and MPT remain strict for rounding artifacts.
|
||||
bool const tolerateZeroDelta =
|
||||
view.rules().enabled(fixCleanup3_2_0) && !vaultAsset.integral();
|
||||
auto const invalidBalanceChange = tolerateZeroDelta
|
||||
auto const invalidBalanceChange = (lendingV12Enabled || tolerateZeroDelta)
|
||||
? roundedDestinationDelta < kZero
|
||||
: roundedDestinationDelta <= kZero;
|
||||
if (invalidBalanceChange)
|
||||
@@ -1205,33 +1389,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
// Note, vaultBalance is negative (see check above)
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultPseudoDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets outstanding must add up";
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultPseudoDeltaAssets,
|
||||
vaultDeltaAssets.delta,
|
||||
"withdrawal",
|
||||
j))
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultPseudoDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1263,39 +1431,33 @@ ValidVault::finalize(
|
||||
computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
|
||||
auto const vaultDeltaAssets =
|
||||
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
if (vaultDeltaAssets >= kZero)
|
||||
// FixedPrecision: AssetsAvailable is exact and the
|
||||
// pseudo-account delta is exact; rounding either one down
|
||||
// to the posterior AssetsTotal scale before the sign
|
||||
// check can turn a legitimate sub-unit clawback into a
|
||||
// false zero and reject it. Compare the exact deltas
|
||||
// instead; Legacy/CashBasis keep the rounded comparison.
|
||||
bool const isFixedPrecision =
|
||||
afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? maybeVaultDeltaAssets->delta : vaultDeltaAssets;
|
||||
if (signCheckDeltaAssets >= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetsTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets outstanding must add up";
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultDeltaAssets,
|
||||
maybeVaultDeltaAssets->delta,
|
||||
"clawback",
|
||||
j))
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
minScale);
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (!isVaultEmpty(beforeVault))
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
@@ -158,7 +159,7 @@ determineAsset(
|
||||
|
||||
std::expected<STAmount, TER>
|
||||
determineClawAmount(
|
||||
SLE const& sleBroker,
|
||||
SLE::ConstRef sleBroker,
|
||||
Asset const& vaultAsset,
|
||||
std::optional<STAmount> const& amount,
|
||||
SLE::ConstRef vaultSle,
|
||||
@@ -169,17 +170,19 @@ determineClawAmount(
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
return minimumBrokerCover(
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]), vaultSle);
|
||||
sleBroker->at(sfDebtTotal),
|
||||
TenthBips32(sleBroker->at(sfCoverRateMinimum)),
|
||||
vaultSle);
|
||||
}
|
||||
|
||||
// Always round the minimum required up
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return tenthBipsOfValue(
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
|
||||
sleBroker->at(sfDebtTotal), TenthBips32(sleBroker->at(sfCoverRateMinimum)));
|
||||
}();
|
||||
// The subtraction probably won't round, but round down if it does.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
return sleBroker[sfCoverAvailable] - minRequiredCover;
|
||||
return sleBroker->at(sfCoverAvailable) - minRequiredCover;
|
||||
}();
|
||||
if (maxClawAmount <= beast::kZero)
|
||||
return std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
@@ -187,12 +190,23 @@ determineClawAmount(
|
||||
// Use the vaultAsset here, because it will be the right type in all
|
||||
// circumstances. The amount may be an IOU indicating the pseudo-account's
|
||||
// asset, which is correct, but not what is needed here.
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
STAmount requested = [&] {
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
}();
|
||||
|
||||
if (getVaultVersion(vaultSle) != VaultVersion::FixedPrecision)
|
||||
return requested;
|
||||
|
||||
STAmount rounded = debitToPosteriorBrokerCoverScale(
|
||||
vaultSle, sleBroker, requested, Number::RoundingMode::TowardsZero);
|
||||
if (rounded == beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
return rounded;
|
||||
}
|
||||
|
||||
template <ValidIssueType T>
|
||||
@@ -294,7 +308,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
if (!findClawAmount)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
|
||||
@@ -302,9 +316,15 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
// FixedPrecision outflows already rounded at the posterior exponent; the
|
||||
// live CoverAvailable scale used by canApplyToBrokerCover would reject a
|
||||
// re-fining clawback as sub-ULP.
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Explicitly check the balance of the trust line / MPT to make sure the
|
||||
// balance is actually there. It should always match `sfCoverAvailable`, so
|
||||
@@ -357,7 +377,7 @@ LoanBrokerCoverClawback::doApply()
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
if (!findClawAmount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -104,22 +106,35 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// here in preclaim lets us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of
|
||||
// failing only in doApply.
|
||||
auto const roundedAmount = [&]() -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Cover deposit is an inflow to CoverAvailable, so it must floor
|
||||
// the sum, not just the delta.
|
||||
return creditToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, amount, Number::RoundingMode::Downward);
|
||||
}
|
||||
if (!fix320Enabled)
|
||||
return tx[sfAmount];
|
||||
return STAmount{amount};
|
||||
|
||||
return roundToScale(
|
||||
tx[sfAmount],
|
||||
amount,
|
||||
scale(sleBroker->at(sfCoverAvailable), vaultAsset),
|
||||
Number::RoundingMode::Downward);
|
||||
}();
|
||||
|
||||
if (fix320Enabled && roundedAmount == beast::kZero)
|
||||
// FixedPrecision vaults require fixCleanup3_2_0, so this always covers the
|
||||
// FixedPrecision zero-credit case too.
|
||||
if (fix320Enabled && roundedAmount <= beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
|
||||
<< " is zero at loan broker scale";
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << amount
|
||||
<< " is zero or negative at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
if (auto const ter = checkOptionalBrokerCoverInflow(vault, sleBroker, roundedAmount);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
account,
|
||||
@@ -155,6 +170,13 @@ LoanBrokerCoverDeposit::doApply()
|
||||
// see the rationale comment in preclaim.
|
||||
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
|
||||
auto const amount = [&]() -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Cover deposit is an inflow to CoverAvailable, so it must floor
|
||||
// the sum, not just the delta.
|
||||
return creditToPosteriorBrokerCoverScale(
|
||||
vault, broker, tx[sfAmount], Number::RoundingMode::Downward);
|
||||
}
|
||||
if (!fix320Enabled)
|
||||
return tx[sfAmount];
|
||||
|
||||
@@ -164,12 +186,12 @@ LoanBrokerCoverDeposit::doApply()
|
||||
Number::RoundingMode::Downward);
|
||||
}();
|
||||
|
||||
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
|
||||
if (amount == beast::kZero)
|
||||
// We validated zero-or-negative amount in preclaim, if we ended up with one now, fail hard.
|
||||
if (amount <= beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
|
||||
<< " is zero";
|
||||
<< " is zero or negative";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -103,10 +104,28 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (amount.asset() != vaultAsset)
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
// Helper handles both IOU and MPT correctly without explicit branching.
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
STAmount roundedAmount{amount};
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
roundedAmount = debitToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, amount, Number::RoundingMode::TowardsZero);
|
||||
if (roundedAmount == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverWithdraw: withdraw amount: " << amount
|
||||
<< " is zero at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// FixedPrecision outflows are already rounded at the posterior
|
||||
// exponent above; the live CoverAvailable scale used by
|
||||
// canApplyToBrokerCover would reject a re-fining withdrawal as
|
||||
// sub-ULP.
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, roundedAmount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The broker's pseudo-account is the source of funds.
|
||||
auto const pseudoAccountID = sleBroker->at(sfAccount);
|
||||
@@ -129,7 +148,15 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
AuthType authType = AuthType::WeakAuth;
|
||||
if (account != dstAcct)
|
||||
{
|
||||
if (auto const ret = canWithdraw(ctx.view, tx))
|
||||
// Check the amount actually sent: on FixedPrecision it is the rounded
|
||||
// amount, not sfAmount.
|
||||
if (auto const ret = canWithdraw(
|
||||
ctx.view,
|
||||
account,
|
||||
dstAcct,
|
||||
roundedAmount,
|
||||
tx.isFieldPresent(sfDestinationTag),
|
||||
tx[~sfCredentialIDs]))
|
||||
return ret;
|
||||
|
||||
// The destination account must have consented to receive the asset by
|
||||
@@ -184,9 +211,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
|
||||
scale(currentDebtTotal, vaultAsset));
|
||||
}();
|
||||
if (coverAvail < amount)
|
||||
if (coverAvail < roundedAmount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
if ((coverAvail - amount) < minimumCover)
|
||||
if ((coverAvail - roundedAmount) < minimumCover)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer()
|
||||
@@ -199,7 +226,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
vaultAsset,
|
||||
freezeHandling,
|
||||
AuthHandling::ZeroIfUnauthorized,
|
||||
ctx.j) < amount)
|
||||
ctx.j) < roundedAmount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -211,7 +238,7 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
auto const amount = tx[sfAmount];
|
||||
auto const requestedAmount = tx[sfAmount];
|
||||
auto const dstAcct = tx[~sfDestination].value_or(accountID_);
|
||||
|
||||
auto broker = view().peek(keylet::loanBroker(brokerID));
|
||||
@@ -223,6 +250,10 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
auto const amount = getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? debitToPosteriorBrokerCoverScale(
|
||||
vault, broker, requestedAmount, Number::RoundingMode::TowardsZero)
|
||||
: requestedAmount;
|
||||
|
||||
auto const brokerPseudoID = *broker->at(sfAccount);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -77,7 +78,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Any remaining debt should have been wiped out by the last Loan
|
||||
// Delete. This check is purely defensive.
|
||||
auto const scale = getAssetsTotalScale(vault);
|
||||
auto const scale = getVaultScale(vault);
|
||||
|
||||
auto const rounded =
|
||||
roundToAsset(asset, debtTotal, scale, Number::RoundingMode::TowardsZero);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
@@ -174,7 +175,10 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
|
||||
// type. This is mostly only relevant for integral (non-IOU) types
|
||||
for (auto const& field : getValueFields())
|
||||
{
|
||||
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
|
||||
if (auto const value = tx[field]; value &&
|
||||
(STAmount{asset, *value} != *value ||
|
||||
(getVaultVersion(sleVault) == VaultVersion::FixedPrecision &&
|
||||
!isOnVaultBaseGrid(asset, *value, getVaultBaseScale(sleVault)))))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
|
||||
<< ") can not be represented as a(n) " << to_string(asset) << ".";
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h> // IWYU pragma: keep
|
||||
@@ -120,7 +121,7 @@ LoanDelete::doApply()
|
||||
roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
debtTotalProxy,
|
||||
getAssetsTotalScale(vaultSle),
|
||||
getVaultScale(vaultSle),
|
||||
Number::RoundingMode::TowardsZero) == beast::kZero,
|
||||
"xrpl::LoanDelete::doApply",
|
||||
"last loan, remaining debt rounds to zero");
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -131,15 +133,126 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
STAmount
|
||||
LoanManage::calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle)
|
||||
{
|
||||
auto const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
|
||||
|
||||
Number const rawCover = [&]() {
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
// Compute minimum cover
|
||||
Number const minimumCover =
|
||||
tenthBipsOfValue(brokerSle->at(sfDebtTotal).value(), coverRateMinimum);
|
||||
|
||||
// Apply coverRateLiquidation
|
||||
return std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), principalOutstanding);
|
||||
}();
|
||||
|
||||
STAmount cappedCover = [&]() -> STAmount {
|
||||
// rawCover is a product of multiplication and can carry more digits than STAmount's;
|
||||
// fix the mode so the STAmount conversion truncates
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero);
|
||||
return STAmount{vaultAsset, std::min(rawCover, *brokerSle->at(sfCoverAvailable))};
|
||||
}();
|
||||
|
||||
if (cappedCover.integral())
|
||||
return cappedCover;
|
||||
|
||||
// Floor AssetsAvailable + cover at its posterior grid so the Vault side is exact, unless
|
||||
// AssetsAvailable has digits finer than the 16-digit cover can carry. The LoanBroker absorbs
|
||||
// the rounding: CoverAvailable - cover may need finer digits than its posterior grid and
|
||||
// rounds to nearest, as the broker's trust line does. Downward also keeps the 19-digit
|
||||
// AssetsAvailable + cover sum from rounding up past cappedCover.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
return creditToPosteriorAvailableScale(vaultSle, cappedCover, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Defaults a Loan on a FixedPrecision Vault. Cover is the raw XLS-66
|
||||
// First-Loss Capital amount, capped at the LoanBroker's CoverAvailable and
|
||||
// floored so AssetsAvailable + cover lands exactly on its posterior grid (a
|
||||
// no-op for integral assets). AssetsAvailable rises by cover through the cash writer;
|
||||
// AssetsDeployed drops by the Loan's full PrincipalOutstanding exactly, and
|
||||
// AssetsTotal is re-derived from AssetsAvailable + AssetsDeployed by the
|
||||
// writer's sync -- this function never touches sfAssetsTotal directly.
|
||||
TER
|
||||
defaultLoanFixedPrecision(
|
||||
ApplyView& view,
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
beast::Journal j)
|
||||
{
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
Number const scheduledInterest = loanSle->at(sfTotalValueOutstanding) - principalOutstanding -
|
||||
loanSle->at(sfManagementFeeOutstanding);
|
||||
|
||||
STAmount const coverAmount = LoanManage::calculateDefaultCover(loanSle, brokerSle, vaultSle);
|
||||
|
||||
// The broker's CoverAvailable decrease and the broker-to-vault transfer
|
||||
// both use this amount. AssetsAvailable + cover is exact unless
|
||||
// AssetsAvailable has digits finer than the 16-digit cover can carry; then
|
||||
// adjustVaultBalances rounds the sum. CoverAvailable - cover may need finer
|
||||
// digits than the broker's posterior grid; it rounds to nearest, as the
|
||||
// broker's trust line does.
|
||||
|
||||
Number const loss = loanSle->isFlag(lsfLoanImpaired) ? -principalOutstanding : 0;
|
||||
if (auto const ter = adjustVaultBalances(
|
||||
vaultSle,
|
||||
{.cash = coverAmount,
|
||||
.deployed = -principalOutstanding,
|
||||
.yield = -scheduledInterest,
|
||||
.loss = loss},
|
||||
j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
view.update(vaultSle);
|
||||
|
||||
brokerSle->at(sfDebtTotal) -= principalOutstanding;
|
||||
brokerSle->at(sfCoverAvailable) -= coverAmount;
|
||||
view.update(brokerSle);
|
||||
|
||||
loanSle->setFlag(lsfLoanDefault);
|
||||
loanSle->at(sfTotalValueOutstanding) = 0;
|
||||
loanSle->at(sfPaymentRemaining) = 0;
|
||||
loanSle->at(sfPrincipalOutstanding) = 0;
|
||||
loanSle->at(sfManagementFeeOutstanding) = 0;
|
||||
loanSle->at(sfNextPaymentDueDate) = 0;
|
||||
view.update(loanSle);
|
||||
|
||||
if (coverAmount == beast::kZero)
|
||||
return tesSUCCESS;
|
||||
|
||||
return accountSend(
|
||||
view,
|
||||
brokerSle->at(sfAccount),
|
||||
vaultSle->at(sfAccount),
|
||||
coverAmount,
|
||||
j,
|
||||
{},
|
||||
WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TER
|
||||
LoanManage::defaultLoan(
|
||||
ApplyView& view,
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
return defaultLoanFixedPrecision(view, loanSle, brokerSle, vaultSle, j);
|
||||
|
||||
auto const vaultAsset = *vaultSle->at(sfAsset);
|
||||
// Calculate the amount of the Default that First-Loss Capital covers:
|
||||
|
||||
std::int32_t const loanScale = loanSle->at(sfLoanScale);
|
||||
@@ -176,7 +289,7 @@ LoanManage::defaultLoan(
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
|
||||
{
|
||||
// Decrease the Total Value of the Vault:
|
||||
@@ -299,22 +412,37 @@ LoanManage::impairLoan(
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle);
|
||||
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(set "paper loss")
|
||||
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
|
||||
if (vaultLossUnrealizedProxy > vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
// Update the Vault object (set "paper loss")
|
||||
if (fixedPrecision)
|
||||
{
|
||||
// Having a loss greater than the vault's unavailable assets
|
||||
// will leave the vault in an invalid / inconsistent state.
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
// adjustVaultBalances's own LU' > AD' check covers the comparison
|
||||
// against AssetsDeployed.
|
||||
if (auto const ter = adjustVaultBalances(vaultSle, {.loss = lossUnrealized}, j);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
|
||||
if (vaultLossUnrealizedProxy >
|
||||
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
{
|
||||
// Having a loss greater than the vault's unavailable assets
|
||||
// will leave the vault in an invalid / inconsistent state.
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
}
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -343,23 +471,32 @@ LoanManage::unimpairLoan(
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(clear "paper loss")
|
||||
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
// Update the Vault object (clear "paper loss")
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
Number const lossReversed = loanVaultExposure(vaultSle, loanSle);
|
||||
if (vaultLossUnrealizedProxy < lossReversed)
|
||||
if (fixedPrecision)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
// adjustVaultBalances's own LU' < 0 check covers the comparison.
|
||||
if (auto const ter = adjustVaultBalances(vaultSle, {.loss = -lossReversed}, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
if (vaultLossUnrealizedProxy < lossReversed)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
}
|
||||
// Reverse the "paper loss"
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -414,7 +551,7 @@ LoanManage::doApply()
|
||||
// Valid flag combinations are checked in preflight. No flags is valid -
|
||||
// just a noop.
|
||||
if (tx.isFlag(tfLoanDefault))
|
||||
return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
|
||||
return defaultLoan(view, loanSle, brokerSle, vaultSle, j_);
|
||||
if (tx.isFlag(tfLoanImpair))
|
||||
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
|
||||
if (tx.isFlag(tfLoanUnimpair))
|
||||
@@ -427,9 +564,27 @@ LoanManage::doApply()
|
||||
// path. Post-amendment, we call associateAsset on all successful paths.
|
||||
if (view.rules().enabled(fixCleanup3_1_3) && isTesSuccess(result))
|
||||
{
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
[[maybe_unused]] bool const assetsEqualBeforeStore =
|
||||
fixedPrecision && vaultSle->at(sfAssetsAvailable) == vaultSle->at(sfAssetsTotal);
|
||||
|
||||
associateAsset(*loanSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
|
||||
if (fixedPrecision)
|
||||
{
|
||||
[[maybe_unused]] Number const assetsAvailableAfter = *vaultSle->at(sfAssetsAvailable);
|
||||
[[maybe_unused]] Number const assetsTotalAfter = *vaultSle->at(sfAssetsTotal);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
"xrpl::LoanManage::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
XRPL_ASSERT_PARTS(
|
||||
!assetsEqualBeforeStore || assetsAvailableAfter == assetsTotalAfter,
|
||||
"xrpl::LoanManage::doApply",
|
||||
"equal assets remain equal after storage");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -356,11 +357,11 @@ LoanPay::doApply()
|
||||
auto const asset = *vaultSle->at(sfAsset);
|
||||
|
||||
// Determine where to send the broker's fee
|
||||
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
|
||||
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
|
||||
// Send the broker fee to the owner if they have sufficient cover available,
|
||||
// _and_ if the owner can receive funds
|
||||
@@ -386,7 +387,8 @@ LoanPay::doApply()
|
||||
return roundToAsset(
|
||||
asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale);
|
||||
}();
|
||||
return coverAvailableProxy >= minCover && !isDeepFrozen(view, brokerOwner, asset) &&
|
||||
return brokerSle->at(sfCoverAvailable) >= minCover &&
|
||||
!isDeepFrozen(view, brokerOwner, asset) &&
|
||||
!requireAuth(view, asset, brokerOwner, AuthType::StrongAuth);
|
||||
}();
|
||||
|
||||
@@ -419,6 +421,12 @@ LoanPay::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const scheduledInterest = [&loanSle] {
|
||||
return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding) -
|
||||
loanSle->at(sfManagementFeeOutstanding);
|
||||
};
|
||||
Number const scheduledInterestBefore = fixedPrecision ? scheduledInterest() : kNumZero;
|
||||
|
||||
LoanPaymentType const paymentType = [&tx]() {
|
||||
// preflight already checked that at most one flag is set.
|
||||
if (tx.isFlag(tfLoanLatePayment))
|
||||
@@ -432,7 +440,6 @@ LoanPay::doApply()
|
||||
|
||||
std::expected<LoanPaymentParts, TER> const paymentParts =
|
||||
loanMakePayment(asset, view, loanSle, brokerSle, amount, paymentType, j_);
|
||||
|
||||
if (!paymentParts)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -440,10 +447,12 @@ LoanPay::doApply()
|
||||
return paymentParts.error();
|
||||
}
|
||||
|
||||
// If the payment computation completed without error, the loanSle object
|
||||
// has been modified.
|
||||
// If the payment computation completed without error, the loanSle object has been modified.
|
||||
view.update(loanSle);
|
||||
|
||||
Number const scheduledInterestDelta =
|
||||
fixedPrecision ? scheduledInterest() - scheduledInterestBefore : kNumZero;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
// It is possible to pay 0 principal
|
||||
paymentParts->principalPaid >= 0,
|
||||
@@ -470,35 +479,49 @@ LoanPay::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const [assetsTotalDelta, debtTotalDelta] = loanPaymentDeltas(vaultSle, *paymentParts);
|
||||
//------------------------------------------------------
|
||||
// LoanBroker object state changes
|
||||
view.update(brokerSle);
|
||||
|
||||
Number const assetsAvailableBefore = *vaultSle->at(sfAssetsAvailable);
|
||||
Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
auto const deltas = loanPaymentDeltas(vaultSle, *paymentParts);
|
||||
// FixedPrecision never writes sfAssetsTotal directly, so assetsTotalDelta
|
||||
// (cash_basis's interestPaid) is unused here and only meaningful for
|
||||
// Legacy/CashBasis below.
|
||||
Number const assetsTotalDelta = fixedPrecision ? kNumZero : deltas.assetsTotalDelta;
|
||||
Number const debtTotalDelta = deltas.debtTotalDelta;
|
||||
STAmount const totalPaidToVaultRounded = fixedPrecision
|
||||
// Pass the exact Number sum: rounding it to 16 digits first could drop
|
||||
// a tail that moves floor16(AssetsAvailable + sum).
|
||||
? creditToPosteriorAvailableScale(
|
||||
vaultSle, totalPaidToVaultRaw, Number::RoundingMode::Downward)
|
||||
: STAmount{
|
||||
asset,
|
||||
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward)};
|
||||
XRPL_ASSERT_PARTS(
|
||||
!asset.integral() || totalPaidToVaultRaw == Number(totalPaidToVaultRounded),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"rounding does nothing for integral asset");
|
||||
Number const totalPaidToBrokerRaw = paymentParts->feePaid;
|
||||
// Fee redirect into cover is an inflow to CoverAvailable, so it must
|
||||
// floor the sum (see creditToPosteriorBrokerCoverScale's doc), not just
|
||||
// the delta. Legacy/CashBasis keeps the fee as the original, unrounded
|
||||
// Number: only the FixedPrecision path is 16-digit-rounded via STAmount.
|
||||
Number const totalPaidToBroker = fixedPrecision && !sendBrokerFeeToOwner
|
||||
// The exact fee, for the same reason as totalPaidToVaultRounded.
|
||||
? Number(creditToPosteriorBrokerCoverScale(
|
||||
vaultSle, brokerSle, totalPaidToBrokerRaw, Number::RoundingMode::Downward))
|
||||
: totalPaidToBrokerRaw;
|
||||
|
||||
JLOG(j_.debug()) << "Loan Pay: principal paid: " << paymentParts->principalPaid
|
||||
<< ", interest paid: " << paymentParts->interestPaid
|
||||
<< ", fee paid: " << paymentParts->feePaid
|
||||
<< ", assets total delta: " << assetsTotalDelta
|
||||
<< ", debt total delta: " << debtTotalDelta;
|
||||
|
||||
//------------------------------------------------------
|
||||
// LoanBroker object state changes
|
||||
view.update(brokerSle);
|
||||
|
||||
auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
auto const totalPaidToVaultRounded =
|
||||
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward);
|
||||
XRPL_ASSERT_PARTS(
|
||||
!asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"rounding does nothing for integral asset");
|
||||
auto const totalPaidToBroker = paymentParts->feePaid;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
(totalPaidToVaultRaw + totalPaidToBroker) ==
|
||||
(paymentParts->principalPaid + paymentParts->interestPaid + paymentParts->feePaid),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"payments add up");
|
||||
<< ", debt total delta: " << debtTotalDelta
|
||||
<< ", scheduled interest delta: " << scheduledInterestDelta;
|
||||
|
||||
// Decrease LoanBroker Debt by the amount paid, add the Loan value change
|
||||
// (which might be negative). debtTotalDelta may be negative, increasing the
|
||||
@@ -508,45 +531,33 @@ LoanPay::doApply()
|
||||
"xrpl::LoanPay::doApply",
|
||||
"debtTotalDelta rounding good");
|
||||
// Despite our best efforts, it's possible for rounding errors to accumulate
|
||||
// in the loan broker's debt total. This is because the broker may have more
|
||||
// than one loan with significantly different scales.
|
||||
adjustImpreciseNumber(debtTotalProxy, -debtTotalDelta, asset, vaultScale);
|
||||
// in the loan broker's debt total on Legacy/CashBasis Vaults. This is because
|
||||
// the broker may have more than one loan with significantly different scales.
|
||||
adjustBrokerDebtTotal(brokerSle, vaultSle, -debtTotalDelta, vaultScale);
|
||||
|
||||
//------------------------------------------------------
|
||||
// Vault object state changes
|
||||
view.update(vaultSle);
|
||||
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||
#if !NDEBUG
|
||||
if (fixedPrecision)
|
||||
{
|
||||
Number const pseudoAccountBalanceBefore = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableBefore == pseudoAccountBalanceBefore,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees before");
|
||||
// AssetsDeployed drops by exactly the principal paid this payment,
|
||||
// taken from the Loan's own payment split rather than the
|
||||
// broker-side debtTotalDelta.
|
||||
if (auto const ter = adjustVaultBalances(
|
||||
vaultSle,
|
||||
{.cash = totalPaidToVaultRounded,
|
||||
.deployed = -paymentParts->principalPaid,
|
||||
.yield = scheduledInterestDelta},
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
vaultSle->at(sfAssetsAvailable) += totalPaidToVaultRounded;
|
||||
vaultSle->at(sfAssetsTotal) += assetsTotalDelta;
|
||||
}
|
||||
#endif
|
||||
|
||||
assetsAvailableProxy += totalPaidToVaultRounded;
|
||||
assetsTotalProxy += assetsTotalDelta;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
*assetsAvailableProxy <= *assetsTotalProxy,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
|
||||
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
|
||||
<< ", total paid to vault rounded: " << totalPaidToVaultRounded
|
||||
<< ", total paid to broker: " << totalPaidToBroker
|
||||
<< ", amount from transaction: " << amount;
|
||||
|
||||
// Move funds
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -557,10 +568,11 @@ LoanPay::doApply()
|
||||
if (!sendBrokerFeeToOwner)
|
||||
{
|
||||
// If there is not enough first-loss capital, add the fee to First Loss
|
||||
// Cover Pool. Note that this moves the entire fee - it does not attempt
|
||||
// to split it. The broker can Withdraw it later if they want, or leave
|
||||
// it for future needs.
|
||||
coverAvailableProxy += totalPaidToBroker;
|
||||
// Cover Pool. FixedPrecision rounds the redirected fee at the
|
||||
// posterior cover scale; any sub-unit remainder is forgiven. The
|
||||
// broker can Withdraw the credited amount later or leave it for future
|
||||
// needs.
|
||||
brokerSle->at(sfCoverAvailable) += totalPaidToBroker;
|
||||
}
|
||||
|
||||
associateAsset(*loanSle, asset);
|
||||
@@ -568,28 +580,46 @@ LoanPay::doApply()
|
||||
associateAsset(*vaultSle, asset);
|
||||
|
||||
// Duplicate some checks after rounding
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const assetsTotalAfter = *assetsTotalProxy;
|
||||
Number const assetsAvailableAfter = vaultSle->at(sfAssetsAvailable);
|
||||
Number const assetsTotalAfter = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
if (assetsAvailableAfter == assetsAvailableBefore)
|
||||
{
|
||||
// An unchanged assetsAvailable indicates that the amount paid to the
|
||||
// vault was zero, or rounded to zero. That should be impossible, but I
|
||||
// can't rule it out for extreme edge cases, so fail gracefully if it
|
||||
// happens.
|
||||
//
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||
<< "Before: " << assetsAvailableBefore //
|
||||
<< ", After: " << assetsAvailableAfter;
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
// Credit rounded to zero. On FixedPrecision, if this payment also
|
||||
// closes the Loan (XLS-66 section 3.2.8's terminal exception), let
|
||||
// it through: AssetsDeployed (vault) and DebtTotal (broker) already
|
||||
// dropped by the exact remaining principal, and YieldUnrealized was already
|
||||
// reduced by this payment's scheduled interest, above. No cash moves for the sub-unit
|
||||
// remainder; the Vault forgives it. A non-terminal zero-credit payment is still rejected:
|
||||
// letting it through would change the Loan's schedule with no cash movement at all.
|
||||
if (fixedPrecision && loanSle->at(sfPaymentRemaining) == 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "LoanPay: terminal payment closed the Loan with a zero cash "
|
||||
"credit; the sub-unit remainder is forgiven. Principal: "
|
||||
<< paymentParts->principalPaid
|
||||
<< ", Interest: " << paymentParts->interestPaid;
|
||||
|
||||
if (totalPaidToVaultRounded == beast::kZero && totalPaidToBroker == beast::kZero)
|
||||
{
|
||||
// Nothing moves for this terminal close: the sub-unit remainder is forgiven, and no
|
||||
// fee is owed either. Return before the transfer and its conservation checks, since
|
||||
// they assume some transfer happens.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||
<< "Before: " << assetsAvailableBefore //
|
||||
<< ", After: " << assetsAvailableAfter //
|
||||
<< ", Credit: " << totalPaidToVaultRounded //
|
||||
<< ", Principal: " << paymentParts->principalPaid //
|
||||
<< ", Interest: " << paymentParts->interestPaid;
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
if (assetsTotalDelta != beast::kZero && assetsTotalAfter == assetsTotalBefore)
|
||||
if (!fixedPrecision && assetsTotalDelta != beast::kZero &&
|
||||
assetsTotalAfter == assetsTotalBefore)
|
||||
{
|
||||
// Non-zero assetsTotalDelta with an unchanged assetsTotal indicates that
|
||||
// the actual value change rounded to zero. That should be impossible, but
|
||||
@@ -605,7 +635,8 @@ LoanPay::doApply()
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (assetsTotalDelta == beast::kZero && assetsTotalAfter != assetsTotalBefore)
|
||||
if (!fixedPrecision && assetsTotalDelta == beast::kZero &&
|
||||
assetsTotalAfter != assetsTotalBefore)
|
||||
{
|
||||
// A change in assetsTotal when there was no assetsTotalDelta indicates
|
||||
// that something really weird happened. That should be flat out
|
||||
@@ -684,22 +715,6 @@ LoanPay::doApply()
|
||||
WaiveTransferFee::Yes))
|
||||
return ter;
|
||||
|
||||
#if !NDEBUG
|
||||
{
|
||||
Number const pseudoAccountBalanceAfter = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees after");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check that funds are conserved
|
||||
auto const accountBalanceAfter = conservationBalance(view, accountID_, asset, j_);
|
||||
auto const vaultBalanceAfter = accountID_ == vaultPseudoAccount
|
||||
@@ -748,10 +763,6 @@ LoanPay::doApply()
|
||||
}();
|
||||
|
||||
// No object changes are made below this point
|
||||
XRPL_ASSERT_PARTS(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"Number rounding ToNearest");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::ToNearest);
|
||||
|
||||
auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
|
||||
|
||||
@@ -320,6 +320,8 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
auto const vaultVersion = getVaultVersion(vault);
|
||||
|
||||
if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
|
||||
{
|
||||
auto const phase = getVaultPhase(ctx.view, vault);
|
||||
@@ -351,13 +353,24 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
// already at AssetsMaximum cannot take another loan. Cash-basis origination
|
||||
// does not change AssetsTotal (see cash_basis::loanOriginationDeltas), so
|
||||
// this leftover instant-recognition gate must not apply there.
|
||||
if (getVaultVersion(vault) != VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
|
||||
if (vaultVersion < VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
|
||||
vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Vault at maximum assets limit. Can't add another loan.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
if (vaultVersion == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Reject origination if the Vault is already coarsened.
|
||||
if (getVaultScale(vault) != getVaultBaseScale(vault))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "FixedPrecision Vault is already coarsened; no further loans can "
|
||||
"be originated until it returns to its base scale.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
}
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
|
||||
auto const vaultPseudo = vault->at(sfAccount);
|
||||
@@ -387,7 +400,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
// tecDUPLICATE, which doApply ignores, so run the check only when the
|
||||
// borrower lacks a holding, or the origination fee is nonzero and the
|
||||
// broker owner lacks one.
|
||||
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
|
||||
auto const originationFee = tx[~sfLoanOriginationFee].value_or(0);
|
||||
if (!ctx.view.rules().enabled(fixCleanup3_4_0) || !holdingExists(ctx.view, borrower, asset) ||
|
||||
(originationFee != beast::kZero && !holdingExists(ctx.view, brokerOwner, asset)))
|
||||
{
|
||||
@@ -452,6 +465,7 @@ LoanSet::doApply()
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
auto const vaultPseudo = vaultSle->at(sfAccount);
|
||||
Asset const vaultAsset = vaultSle->at(sfAsset);
|
||||
auto const vaultVersion = getVaultVersion(vaultSle);
|
||||
|
||||
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
|
||||
auto const borrower = counterparty == brokerOwner ? accountID_ : counterparty;
|
||||
@@ -471,7 +485,8 @@ LoanSet::doApply()
|
||||
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
// For Legacy/CashBasis, getVaultBaseScale falls through to getVaultScale.
|
||||
auto const vaultScale = getVaultBaseScale(vaultSle);
|
||||
if (vaultAvailableProxy < principalRequested)
|
||||
{
|
||||
JLOG(j_.warn()) << "Insufficient assets available in the Vault to fund the loan.";
|
||||
@@ -499,8 +514,7 @@ LoanSet::doApply()
|
||||
properties.loanState.managementFeeDue);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultSle->at(sfAssetsMaximum) == 0 ||
|
||||
getVaultVersion(vaultSle) == VaultVersion::CashBasis ||
|
||||
*vaultSle->at(sfAssetsMaximum) == 0 || vaultVersion >= VaultVersion::CashBasis ||
|
||||
*vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy,
|
||||
"xrpl::LoanSet::doApply",
|
||||
"instant-recognition vault is below maximum limit");
|
||||
@@ -547,6 +561,20 @@ LoanSet::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (vaultVersion == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Reject origination if this loan's interest would grow the Vault past its Open-zone
|
||||
// capacity.
|
||||
if (vaultOpenZoneCapacity(vaultSle, state.interestDue) > getVaultOpenLimit(vaultSle))
|
||||
{
|
||||
JLOG(j_.warn()) << "Loan interest would exceed the FixedPrecision Vault's Open zone.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
XRPL_ASSERT(
|
||||
properties.loanScale == getVaultBaseScale(vaultSle),
|
||||
"xrpl::LoanSet::doApply : FixedPrecision loan uses Vault base scale");
|
||||
}
|
||||
|
||||
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
|
||||
|
||||
auto const loanAssetsToBorrower = principalRequested - originationFee;
|
||||
@@ -696,8 +724,22 @@ LoanSet::doApply()
|
||||
view.insert(loan);
|
||||
|
||||
// Update the balances in the vault
|
||||
vaultAvailableProxy -= principalRequested;
|
||||
vaultTotalProxy += assetsTotalDelta;
|
||||
if (vaultVersion == VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ter = adjustVaultBalances(
|
||||
vaultSle,
|
||||
{.cash = -STAmount{vaultAsset, principalRequested},
|
||||
.deployed = debtTotalDelta,
|
||||
.yield = state.interestDue},
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
vaultAvailableProxy -= principalRequested;
|
||||
vaultTotalProxy += assetsTotalDelta;
|
||||
}
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultAvailableProxy <= *vaultTotalProxy,
|
||||
"xrpl::LoanSet::doApply",
|
||||
@@ -705,7 +747,7 @@ LoanSet::doApply()
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the balances in the loan broker
|
||||
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), debtTotalDelta, vaultAsset, vaultScale);
|
||||
adjustBrokerDebtTotal(brokerSle, vaultSle, debtTotalDelta, vaultScale);
|
||||
adjustLoanBrokerOwnerCount(view, brokerSle, 1, j_);
|
||||
loanSequenceProxy += 1;
|
||||
// The sequence should be extremely unlikely to roll over, but fail if it
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
ConfidentialMPTHolderKeyUpdate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
// Holder key update is only meaningful when confidential transfers are enabled.
|
||||
return ctx.rules.enabled(featureConfidentialTransfer);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
ConfidentialMPTHolderKeyUpdate::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfConfidentialMPTHolderKeyUpdateMask;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTHolderKeyUpdate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
bool const rotation = ctx.tx.isFlag(tfHolderKeyRotation);
|
||||
bool const recovery = ctx.tx.isFlag(tfHolderKeyRecovery);
|
||||
bool const cancel = ctx.tx.isFlag(tfCancelRecovery);
|
||||
|
||||
// Exactly one of the three mode flags must be set.
|
||||
static constexpr auto modeFlags = tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery;
|
||||
if (std::popcount(ctx.tx.getFlags() & modeFlags) != 1)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// The issuer cannot hold confidential balances.
|
||||
if (ctx.tx[sfAccount] == MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
|
||||
return temMALFORMED;
|
||||
|
||||
bool const hasHolderKey = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
|
||||
bool const hasSpending = ctx.tx.isFieldPresent(sfConfidentialBalanceSpending);
|
||||
bool const hasInbox = ctx.tx.isFieldPresent(sfConfidentialBalanceInbox);
|
||||
bool const hasProof = ctx.tx.isFieldPresent(sfZKProof);
|
||||
|
||||
if (cancel)
|
||||
{
|
||||
// Cancel mode only revokes a pending recovery authorization; it
|
||||
// carries no key material, balances, or proof.
|
||||
if (hasHolderKey || hasSpending || hasInbox || hasProof)
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Rotation and Recovery both require a holder key and a proof.
|
||||
if (!hasHolderKey || !hasProof)
|
||||
return temMALFORMED;
|
||||
|
||||
// Rotation mode requires re-encrypted balances; Recovery mode must not
|
||||
// provide them since the holder cannot decrypt the current ones.
|
||||
if (rotation && (!hasSpending || !hasInbox))
|
||||
return temMALFORMED;
|
||||
|
||||
if (recovery && (hasSpending || hasInbox))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasSpending &&
|
||||
ctx.tx[sfConfidentialBalanceSpending].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasInbox && ctx.tx[sfConfidentialBalanceInbox].length() != kEcGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasSpending && !isValidCiphertext(ctx.tx[sfConfidentialBalanceSpending]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasInbox && !isValidCiphertext(ctx.tx[sfConfidentialBalanceInbox]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
ConfidentialMPTHolderKeyUpdate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTHolderKeyUpdate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
|
||||
if (!sleMptoken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) ||
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFlag(tfCancelRecovery))
|
||||
{
|
||||
// Nothing to cancel if no recovery is pending.
|
||||
if (!sleMptoken->isFieldPresent(sfRecoveryKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (ctx.tx[sfHolderEncryptionKey] == (*sleMptoken)[sfHolderEncryptionKey])
|
||||
return tecDUPLICATE;
|
||||
|
||||
// Recovery mode: reject if a recovery is already pending
|
||||
if (ctx.tx.isFlag(tfHolderKeyRecovery) && sleMptoken->isFieldPresent(sfRecoveryKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTHolderKeyUpdate::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
|
||||
if (!sleMptoken)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::ConfidentialMPTHolderKeyUpdate::doApply : preclaim already validated the "
|
||||
"MPToken exists");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFlag(tfCancelRecovery))
|
||||
{
|
||||
// The holder revokes their pending recovery authorization; the
|
||||
// current key and balances are left untouched.
|
||||
sleMptoken->makeFieldAbsent(sfRecoveryKey);
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const newPubKey = ctx_.tx[sfHolderEncryptionKey];
|
||||
|
||||
if (ctx_.tx.isFlag(tfHolderKeyRotation))
|
||||
{
|
||||
// Replace the key and balances in rotation mode. Rotation proves the
|
||||
// holder still has the old key, so any pending recovery is cancelled.
|
||||
(*sleMptoken)[sfHolderEncryptionKey] = newPubKey;
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = ctx_.tx[sfConfidentialBalanceSpending];
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = ctx_.tx[sfConfidentialBalanceInbox];
|
||||
sleMptoken->makeFieldAbsent(sfRecoveryKey);
|
||||
incrementConfidentialVersion(*sleMptoken);
|
||||
}
|
||||
else if (ctx_.tx.isFlag(tfHolderKeyRecovery))
|
||||
{
|
||||
// Recovery mode: the holder cannot decrypt their current balances,
|
||||
// so only the pending recovery key is recorded. The balances are
|
||||
// rewritten separately by the issuer via ConfidentialMPTRecoverBalance.
|
||||
(*sleMptoken)[sfRecoveryKey] = newPubKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::ConfidentialMPTHolderKeyUpdate::doApply : invalid mode");
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
ConfidentialMPTHolderKeyUpdate::visitInvariantEntry(bool, SLE::ConstRef, SLE::ConstRef)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
ConfidentialMPTHolderKeyUpdate::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -269,11 +270,11 @@ VaultClawback::assetsToClawback(
|
||||
STAmount sharesDestroyed;
|
||||
STAmount assetsRecovered;
|
||||
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large.
|
||||
try
|
||||
{
|
||||
// Do not discount a sole holder's shares: clawing back AssetsAvailable
|
||||
// at the discounted rate can burn every share while loan assets remain.
|
||||
// Do not discount a sole holder's shares: clawing back AssetsAvailable at the discounted
|
||||
// rate can burn every share while loan assets remain.
|
||||
auto const waiveUnrealizedLoss =
|
||||
fix340Enabled && isSoleShareholder(view(), holder, sleShareIssuance)
|
||||
? WaiveUnrealizedLoss::Yes
|
||||
@@ -281,12 +282,11 @@ VaultClawback::assetsToClawback(
|
||||
|
||||
if (clawbackAmount == beast::kZero)
|
||||
{
|
||||
// Zero amount means clawback all shares the holder has; derive the corresponding asset
|
||||
// amount from the share balance.
|
||||
// isSoleShareholder already established that the holder owns the
|
||||
// entire outstanding share supply whenever the waiver applies, so
|
||||
// sfOutstandingAmount gives sharesDestroyed directly, avoiding a
|
||||
// redundant MPToken read via accountHolds.
|
||||
// Zero amount clawbacks all shares of the holder; derive the corresponding asset
|
||||
// amount from the share balance. isSoleShareholder already established that the holder
|
||||
// owns the entire outstanding share supply whenever the waiver applies, so
|
||||
// sfOutstandingAmount gives sharesDestroyed directly, avoiding a redundant MPToken read
|
||||
// via accountHolds.
|
||||
sharesDestroyed = waiveUnrealizedLoss == WaiveUnrealizedLoss::Yes
|
||||
? STAmount{share, sleShareIssuance->at(sfOutstandingAmount)}
|
||||
: accountHolds(
|
||||
@@ -305,11 +305,10 @@ VaultClawback::assetsToClawback(
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-fixCleanup3_4_0: shares were rounded to nearest, so the
|
||||
// round-trip back to assets could exceed clawbackAmount.
|
||||
// Post-amendment: truncate shares so assetsRecovered <=
|
||||
// clawbackAmount by construction (matches the clamp branch
|
||||
// below).
|
||||
// Pre-fixCleanup3_4_0: shares were rounded to nearest, so the round-trip back to assets
|
||||
// could exceed clawbackAmount.
|
||||
// Post-fixCleanup3_4_0: truncate shares so assetsRecovered <= clawbackAmount by
|
||||
// construction.
|
||||
auto const truncate = fix340Enabled ? TruncateShares::Yes : TruncateShares::No;
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault, sleShareIssuance, clawbackAmount, truncate, waiveUnrealizedLoss);
|
||||
@@ -323,8 +322,7 @@ VaultClawback::assetsToClawback(
|
||||
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
}
|
||||
// Clamp assetsRecovered to sfAssetsAvailable, then re-derive shares and assets so the pair
|
||||
// stays consistent.
|
||||
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
assetsRecovered = *assetsAvailable;
|
||||
@@ -356,13 +354,28 @@ VaultClawback::assetsToClawback(
|
||||
}
|
||||
}
|
||||
|
||||
// Post-fixCleanup3_4_0: round the recovery down at the posterior sfAssetsTotal scale so all
|
||||
// rails change by the same representable delta. sharesDestroyed is intentionally NOT
|
||||
// re-derived here: the holder's shares are burned for their pre-clamp value, so any
|
||||
// sub-ULP trimmed off stays in the vault for the remaining shareholders.
|
||||
if (ctx_.view().rules().enabled(fixCleanup3_4_0) && assetsRecovered > beast::kZero)
|
||||
// On a coarsened Vault, an AssetsDeployed below half a unit of AssetsAvailable's grid
|
||||
// rounds away: the conversion equals AssetsAvailable exactly, the clamp above never runs,
|
||||
// and every share would be burned while AssetsDeployed is still outstanding.
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision &&
|
||||
sharesDestroyed == STAmount{share, sleShareIssuance->at(sfOutstandingAmount)} &&
|
||||
Number(vault->at(sfAssetsDeployed)) != beast::kZero)
|
||||
{
|
||||
auto const maybeClamped = clampToAssetsTotalScale(vault, -assetsRecovered);
|
||||
JLOG(j_.debug())
|
||||
<< "VaultClawback: cannot burn all shares while AssetsDeployed is non-zero";
|
||||
return std::unexpected(tecHAS_OBLIGATIONS);
|
||||
}
|
||||
|
||||
// Post-fixCleanup3_4_0: round the recovery down at the posterior scale of the balance
|
||||
// - AssetsAvailable on FixedPrecision Vault
|
||||
// - AssetsTotal on Legacy/CashBasis Vault
|
||||
// All rails change by the same representable delta.
|
||||
// sharesDestroyed is intentionally NOT re-derived here: the holder's shares are burned for
|
||||
// their pre-clamp value, so any sub-ULP trimmed off stays in the vault for the remaining
|
||||
// shareholders.
|
||||
if (fix340Enabled && assetsRecovered > beast::kZero)
|
||||
{
|
||||
auto const maybeClamped = clampVaultOutflow(vault, -assetsRecovered);
|
||||
if (!maybeClamped)
|
||||
return std::unexpected(maybeClamped.error());
|
||||
assetsRecovered = *maybeClamped;
|
||||
@@ -408,8 +421,7 @@ VaultClawback::doApply()
|
||||
Asset const vaultAsset = vault->at(sfAsset);
|
||||
STAmount const amount = clawbackAmount(vault, tx[~sfAmount], accountID_);
|
||||
|
||||
auto assetsAvailable = vault->at(sfAssetsAvailable);
|
||||
auto assetsTotal = vault->at(sfAssetsTotal);
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
|
||||
AccountID const holder = tx[sfHolder];
|
||||
STAmount sharesDestroyed = {share};
|
||||
@@ -443,9 +455,12 @@ VaultClawback::doApply()
|
||||
{
|
||||
try
|
||||
{
|
||||
// A non-zero recovery can be too small to change the stored sfAssetsTotal at
|
||||
// A non-zero recovery can be too small to change the stored balance at
|
||||
// STAmount's precision. Shares would still be burned, reject it instead.
|
||||
if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsRecovered))
|
||||
// FixedPrecision measures this against AssetsAvailable, the balance the
|
||||
// cash actually leaves, not the derived AssetsTotal cache.
|
||||
Number const dustReference = vaultDebitDustReference(vault, assetsTotal);
|
||||
if (debitIsNonZeroDust(vaultAsset, dustReference, assetsRecovered))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.debug())
|
||||
@@ -473,8 +488,17 @@ VaultClawback::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
assetsTotal -= assetsRecovered;
|
||||
assetsAvailable -= assetsRecovered;
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ter = adjustVaultBalances(vault, {.cash = -assetsRecovered}, j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
vault->at(sfAssetsTotal) -= assetsRecovered;
|
||||
vault->at(sfAssetsAvailable) -= assetsRecovered;
|
||||
}
|
||||
view().update(vault);
|
||||
|
||||
auto const& vaultAccount = vault->at(sfAccount);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <xrpl/tx/transactors/vault/VaultCreate.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
@@ -24,6 +25,7 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/transactors/token/MPTokenIssuanceCreate.h>
|
||||
@@ -49,6 +51,9 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfRedemptionDate)))
|
||||
return false;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfEarlyExitFeeRate) && !ctx.rules.enabled(featureLendingProtocolV1_2))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -101,7 +106,10 @@ VaultCreate::preflight(PreflightContext const& ctx)
|
||||
if (vaultAsset.holds<MPTIssue>() || vaultAsset.native())
|
||||
return temMALFORMED;
|
||||
|
||||
if (scale > kVaultMaximumIouScale)
|
||||
auto const maximumScale = vaultVersionFor(ctx.rules) == VaultVersion::FixedPrecision
|
||||
? kVaultMaximumFixedPrecisionIouScale
|
||||
: kVaultMaximumLegacyIouScale;
|
||||
if (scale > maximumScale)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
@@ -121,6 +129,16 @@ VaultCreate::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// An early-exit fee only lifts the Investment-phase withdrawal gate, which
|
||||
// exists only on closed-ended vaults. A rate of zero is still a rate.
|
||||
if (auto const feeRate = ctx.tx[~sfEarlyExitFeeRate])
|
||||
{
|
||||
if (!isClosedEnded)
|
||||
return temMALFORMED;
|
||||
if (TenthBips32{*feeRate} > kMaxEarlyExitFeeRate)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -168,6 +186,21 @@ VaultCreate::preclaim(PreclaimContext const& ctx)
|
||||
hasExpired(ctx.view, ctx.tx[~sfRedemptionDate]))
|
||||
return tecEXPIRED;
|
||||
|
||||
// FixedPrecision: AssetsMaximum must be exactly representable on the Vault's base grid,
|
||||
// otherwise associateAsset would silently round the cap the owner asked for.
|
||||
if (auto const assetMax = ctx.tx[~sfAssetsMaximum];
|
||||
assetMax && ctx.view.rules().enabled(featureLendingProtocolV1_2))
|
||||
{
|
||||
int const baseScale =
|
||||
vaultBaseScale(vaultAsset, ctx.tx[~sfScale].value_or(kVaultDefaultIouScale));
|
||||
if (!isOnVaultBaseGrid(vaultAsset, *assetMax, baseScale))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultCreate: AssetsMaximum " << *assetMax
|
||||
<< " is not representable at the Vault scale.";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -273,16 +306,30 @@ VaultCreate::doApply()
|
||||
}
|
||||
if (scale != 0u)
|
||||
vault->at(sfScale) = scale;
|
||||
if (view().rules().enabled(featureLendingProtocolV1_1))
|
||||
VaultVersion const version = vaultVersionFor(view().rules());
|
||||
if (version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::FixedPrecision);
|
||||
vault->at(sfYieldUnrealized) = Number(0);
|
||||
vault->at(sfAssetsDeployed) = Number(0);
|
||||
}
|
||||
else if (version == VaultVersion::CashBasis)
|
||||
{
|
||||
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::CashBasis);
|
||||
}
|
||||
|
||||
if (version != VaultVersion::Legacy)
|
||||
{
|
||||
auto const kind = getVaultKind(tx);
|
||||
vault->at(sfVaultKind) = std::to_underlying(kind);
|
||||
if (kind == VaultKind::ClosedEnded)
|
||||
{
|
||||
vault->at(sfSubscriptionDate) = tx[sfSubscriptionDate];
|
||||
vault->at(sfRedemptionDate) = tx[sfRedemptionDate];
|
||||
// Stored as submitted, including zero: an absent field forbids early
|
||||
// exit, while zero permits it free of charge.
|
||||
if (auto const feeRate = tx[~sfEarlyExitFeeRate])
|
||||
vault->at(sfEarlyExitFeeRate) = *feeRate;
|
||||
}
|
||||
}
|
||||
view().insert(vault);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -59,7 +60,7 @@ VaultDelete::preclaim(PreclaimContext const& ctx)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
}
|
||||
|
||||
if (vault->at(sfAssetsTotal) != 0)
|
||||
if (getAssetsTotal(vault) != 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultDelete: nonzero assets total.";
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
@@ -31,24 +31,6 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
[[nodiscard]]
|
||||
static STAmount
|
||||
roundToVaultScale(STAmount const& amount, SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::roundToVaultScale : valid vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset), "xrpl::roundToVaultScale : valid vault asset");
|
||||
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
|
||||
int const postScale = [&]() {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(vault->at(sfAssetsTotal) + amount, vault->at(sfAsset));
|
||||
}();
|
||||
return roundToScale(amount, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
// True if debiting `assets` would leave the depositor's balance where it started, so the deposit
|
||||
// would mint shares against a transfer that never happened. Asking the balance directly whether it
|
||||
// notices the debit avoids having to infer the rounding step: it has to be the stored balance that
|
||||
@@ -187,7 +169,9 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
if (auto const ter = requireAuth(ctx.view, vaultAsset, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
auto const roundedAmount = fix320Enabled ? roundToVaultScale(amount, vault) : amount;
|
||||
auto const roundedAmount = fix320Enabled
|
||||
? roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero)
|
||||
: amount;
|
||||
|
||||
if (fix320Enabled && roundedAmount == beast::kZero)
|
||||
{
|
||||
@@ -237,10 +221,11 @@ VaultDeposit::doApply()
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
// Post-amendment IOU only: round Downward to the AssetsTotal precision so
|
||||
// Post-amendment IOU only: round toward zero to the AssetsTotal precision so
|
||||
// a sub-ULP tail can't be silently absorbed by one rail and not the other.
|
||||
auto const amount =
|
||||
fix320Enabled ? roundToVaultScale(ctx_.tx[sfAmount], vault) : ctx_.tx[sfAmount];
|
||||
auto const amount = fix320Enabled
|
||||
? roundToPosteriorVaultScale(vault, ctx_.tx[sfAmount], Number::RoundingMode::TowardsZero)
|
||||
: ctx_.tx[sfAmount];
|
||||
|
||||
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
|
||||
if (amount == beast::kZero)
|
||||
@@ -374,17 +359,29 @@ VaultDeposit::doApply()
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
|
||||
if (auto const ter = checkOptionalVaultInflow(vault, assetsDeposited); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
XRPL_ASSERT(
|
||||
sharesCreated.asset() != assetsDeposited.asset(),
|
||||
"xrpl::VaultDeposit::doApply : assets are not shares");
|
||||
|
||||
vault->at(sfAssetsTotal) += assetsDeposited;
|
||||
vault->at(sfAssetsAvailable) += assetsDeposited;
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ter = adjustVaultBalances(vault, {.cash = assetsDeposited}, j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
vault->at(sfAssetsTotal) += assetsDeposited;
|
||||
vault->at(sfAssetsAvailable) += assetsDeposited;
|
||||
}
|
||||
view().update(vault);
|
||||
|
||||
// A deposit must not push the vault over its limit.
|
||||
auto const maximum = *vault->at(sfAssetsMaximum);
|
||||
if (maximum != 0 && *vault->at(sfAssetsTotal) > maximum)
|
||||
if (maximum != 0 && getAssetsTotal(vault) > maximum)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
// Transfer assets from depositor to vault.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include <xrpl/tx/transactors/vault/VaultSet.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
@@ -110,6 +112,21 @@ VaultSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// With featureLendingProtocolV1_2, AssetsMaximum must be exactly
|
||||
// representable at the Vault's scale (the base grid on FixedPrecision
|
||||
// Vaults, the live scale on existing Legacy/CashBasis Vaults), otherwise
|
||||
// associateAsset would silently round the cap the owner asked for.
|
||||
if (auto const assetMax = ctx.tx[~sfAssetsMaximum];
|
||||
assetMax && ctx.view.rules().enabled(featureLendingProtocolV1_2))
|
||||
{
|
||||
if (auto const ter = checkAssetsMaximum(vault, *assetMax); !isTesSuccess(ter))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultSet: AssetsMaximum " << *assetMax
|
||||
<< " is not representable at the Vault scale.";
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -144,7 +161,7 @@ VaultSet::doApply()
|
||||
vault->at(sfData) = tx[sfData];
|
||||
if (tx.isFieldPresent(sfAssetsMaximum))
|
||||
{
|
||||
if (tx[sfAssetsMaximum] != 0 && tx[sfAssetsMaximum] < *vault->at(sfAssetsTotal))
|
||||
if (tx[sfAssetsMaximum] != 0 && tx[sfAssetsMaximum] < getAssetsTotal(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
vault->at(sfAssetsMaximum) = tx[sfAssetsMaximum];
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user