mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Compare commits
164 Commits
ximinez/le
...
pratik/Add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca9fd5e319 | ||
|
|
5bb30cff15 | ||
|
|
8b761b9ce6 | ||
|
|
1f8fbaedca | ||
|
|
1cdd43f259 | ||
|
|
982d26610d | ||
|
|
1f2f05d320 | ||
|
|
7f09fbe807 | ||
|
|
734f8a9810 | ||
|
|
935e45bcca | ||
|
|
0b612b5c13 | ||
|
|
7617072ebc | ||
|
|
4a61ff3bef | ||
|
|
7097566d9d | ||
|
|
afedb276db | ||
|
|
941775fec4 | ||
|
|
4058ca01ac | ||
|
|
9c5df200b8 | ||
|
|
96e8ee4bdd | ||
|
|
c182d13ab5 | ||
|
|
d942fd9f9c | ||
|
|
064d51b79c | ||
|
|
6202148623 | ||
|
|
954fe68e91 | ||
|
|
1d38504d85 | ||
|
|
9cdabe98e2 | ||
|
|
49f8daf82f | ||
|
|
dd9e554d4a | ||
|
|
facb6c63e3 | ||
|
|
32d129ee5e | ||
|
|
6e32392f93 | ||
|
|
39ace64833 | ||
|
|
4430cb64b8 | ||
|
|
d4b033958e | ||
|
|
1a08b97fb6 | ||
|
|
6407788b0f | ||
|
|
702d94134e | ||
|
|
7d3fc5c396 | ||
|
|
810f5e94db | ||
|
|
d026d0640f | ||
|
|
5befd5d249 | ||
|
|
4f496db291 | ||
|
|
10553fc437 | ||
|
|
087e52ea36 | ||
|
|
cb982e7a8a | ||
|
|
2610e653d6 | ||
|
|
b6c313bb94 | ||
|
|
a3e42a0d11 | ||
|
|
92a6c986b3 | ||
|
|
dd0408ac53 | ||
|
|
0fcfcd059e | ||
|
|
4c0f7a337a | ||
|
|
640f9ff5e2 | ||
|
|
42cab6c826 | ||
|
|
9267756944 | ||
|
|
3b1e82b412 | ||
|
|
7d7c659822 | ||
|
|
a58df41a4d | ||
|
|
d01b1da80e | ||
|
|
216ecf67e1 | ||
|
|
d8c8900fbb | ||
|
|
e2c75f5d60 | ||
|
|
2428c5c196 | ||
|
|
d8ff72b342 | ||
|
|
4fc5c00c24 | ||
|
|
5a9014912c | ||
|
|
8006a1e967 | ||
|
|
6610f469df | ||
|
|
9f026929cf | ||
|
|
a9d134af85 | ||
|
|
18b9b9da19 | ||
|
|
6a89a544fe | ||
|
|
427771775e | ||
|
|
67946f4993 | ||
|
|
8d8a0cb970 | ||
|
|
89f9ede1f5 | ||
|
|
aac1076d83 | ||
|
|
c0da02814b | ||
|
|
013ff18fce | ||
|
|
e1403d56ef | ||
|
|
58b248d9a4 | ||
|
|
f1561c5b48 | ||
|
|
73ab466029 | ||
|
|
4cf4802971 | ||
|
|
8d6d2ec455 | ||
|
|
4248a1fbb6 | ||
|
|
7edba0a856 | ||
|
|
b38ef53c44 | ||
|
|
baee65bd28 | ||
|
|
2e53c5ab80 | ||
|
|
09a6e46c39 | ||
|
|
a93052e765 | ||
|
|
1cc00cddd8 | ||
|
|
5f2a351e3f | ||
|
|
5e89bce8d9 | ||
|
|
9b793cd429 | ||
|
|
b650852fa7 | ||
|
|
1d76de83f6 | ||
|
|
ea4d062e68 | ||
|
|
dcd0553050 | ||
|
|
6355eba88e | ||
|
|
2e5afa7556 | ||
|
|
7d05090d68 | ||
|
|
916f00039c | ||
|
|
e687ab8653 | ||
|
|
b182b6abf7 | ||
|
|
509d388f97 | ||
|
|
fd712770e1 | ||
|
|
695f4a2cc9 | ||
|
|
5ea3b4327d | ||
|
|
a9444d3a42 | ||
|
|
20326a785b | ||
|
|
5ef2ced584 | ||
|
|
a011d29fcb | ||
|
|
9abec17536 | ||
|
|
2bc089a962 | ||
|
|
14f605b2f9 | ||
|
|
f2365543fb | ||
|
|
d9c26bd7a9 | ||
|
|
937dc8740e | ||
|
|
9b92aafe6d | ||
|
|
124b6ca4bd | ||
|
|
28885a4638 | ||
|
|
5478e4fee6 | ||
|
|
72b3a03538 | ||
|
|
18b65fd129 | ||
|
|
ebf917ab15 | ||
|
|
2178fb919f | ||
|
|
e156ed40ba | ||
|
|
db5aa2d277 | ||
|
|
e347da9fac | ||
|
|
6b8d5b57e1 | ||
|
|
0029210926 | ||
|
|
832c32d15c | ||
|
|
39adc5a82b | ||
|
|
1f88697f54 | ||
|
|
4a0fc0f686 | ||
|
|
f5b473ccd1 | ||
|
|
71212677c0 | ||
|
|
abfaac5c64 | ||
|
|
cb5a76589e | ||
|
|
d077141d75 | ||
|
|
1eb3cc4ec7 | ||
|
|
34b10b87e6 | ||
|
|
c0ecb9a0cd | ||
|
|
c15d6399fb | ||
|
|
0b1bd42cc0 | ||
|
|
30f7ef7676 | ||
|
|
e7da05e44c | ||
|
|
1cdd0bf63e | ||
|
|
48a3e5ea31 | ||
|
|
10a422dd69 | ||
|
|
b150feaab6 | ||
|
|
ada523071f | ||
|
|
ff0284e984 | ||
|
|
0be98ac610 | ||
|
|
83eb93f5d7 | ||
|
|
db74cebc8b | ||
|
|
3b32210ef4 | ||
|
|
21b9f6d1af | ||
|
|
977a087bb3 | ||
|
|
e95299dac5 | ||
|
|
7ae3a85f21 | ||
|
|
c38b5aa2d4 |
6
.github/actions/build-deps/action.yml
vendored
6
.github/actions/build-deps/action.yml
vendored
@@ -21,6 +21,10 @@ inputs:
|
||||
description: "The logging verbosity."
|
||||
required: false
|
||||
default: "verbose"
|
||||
sanitizers:
|
||||
description: "The sanitizers to enable ('Address', 'Thread')."
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -33,11 +37,13 @@ runs:
|
||||
BUILD_OPTION: ${{ inputs.force_build == 'true' && '*' || 'missing' }}
|
||||
BUILD_TYPE: ${{ inputs.build_type }}
|
||||
LOG_VERBOSITY: ${{ inputs.log_verbosity }}
|
||||
SANITIZERS: ${{ inputs.sanitizers }}
|
||||
run: |
|
||||
echo 'Installing dependencies.'
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
cd "${BUILD_DIR}"
|
||||
conan install \
|
||||
--profile ci \
|
||||
--output-folder . \
|
||||
--build="${BUILD_OPTION}" \
|
||||
--options:host='&:tests=True' \
|
||||
|
||||
2
.github/actions/setup-conan/action.yml
vendored
2
.github/actions/setup-conan/action.yml
vendored
@@ -28,7 +28,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'Installing profile.'
|
||||
conan config install conan/profiles/default -tf $(conan config home)/profiles/
|
||||
conan config install conan/profiles/ -tf $(conan config home)/profiles/
|
||||
|
||||
echo 'Conan profile:'
|
||||
conan profile show
|
||||
|
||||
163
.github/scripts/strategy-matrix/generate.py
vendored
163
.github/scripts/strategy-matrix/generate.py
vendored
@@ -209,15 +209,17 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
|
||||
):
|
||||
continue
|
||||
|
||||
# Enable code coverage for Debian Bookworm using GCC 15 in Debug and no
|
||||
cxx_flags = ""
|
||||
# Enable code coverage for Debian Bookworm using GCC 14 in Debug and no
|
||||
# Unity on linux/amd64
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-15"
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-14"
|
||||
and build_type == "Debug"
|
||||
and "-Dunity=OFF" in cmake_args
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
cmake_args = f"-Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0 {cmake_args}"
|
||||
cmake_args = f"-Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 {cmake_args}"
|
||||
cxx_flags = f"-O0 {cxx_flags}"
|
||||
|
||||
# Generate a unique name for the configuration, e.g. macos-arm64-debug
|
||||
# or debian-bookworm-gcc-12-amd64-release-unity.
|
||||
@@ -229,7 +231,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
|
||||
if (n := os["compiler_version"]) != "":
|
||||
config_name += f"-{n}"
|
||||
config_name += (
|
||||
f"-{architecture['platform'][architecture['platform'].find('/') + 1 :]}"
|
||||
f"-{architecture['platform'][architecture['platform'].find('/')+1:]}"
|
||||
)
|
||||
config_name += f"-{build_type.lower()}"
|
||||
if "-Dunity=ON" in cmake_args:
|
||||
@@ -238,21 +240,152 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
|
||||
# Add the configuration to the list, with the most unique fields first,
|
||||
# so that they are easier to identify in the GitHub Actions UI, as long
|
||||
# names get truncated.
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name,
|
||||
"cmake_args": cmake_args,
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
}
|
||||
)
|
||||
# Add Address and Thread (both coupled with UB) sanitizers for specific bookworm distros.
|
||||
if os[
|
||||
"distro_version"
|
||||
] == "bookworm" and f"{os['compiler_name']}-{os['compiler_version']}" in {
|
||||
"gcc-15",
|
||||
"clang-20",
|
||||
}:
|
||||
configs = addSanitizerConfigs(architecture, os, cmake_args, cxx_flags)
|
||||
if "asan_ubsan" in configs:
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name + "-asan-ubsan",
|
||||
"cmake_args": configs["asan_ubsan"],
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
"sanitizers": "Address,UndefinedBehavior",
|
||||
}
|
||||
)
|
||||
if "tsan_ubsan" in configs:
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name + "-tsan-ubsan",
|
||||
"cmake_args": configs["tsan_ubsan"],
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
"sanitizers": "Thread,UndefinedBehavior",
|
||||
}
|
||||
)
|
||||
else:
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name,
|
||||
"cmake_args": cmake_args,
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
}
|
||||
)
|
||||
|
||||
return configurations
|
||||
|
||||
|
||||
def addSanitizerConfigs(
|
||||
architecture: dict,
|
||||
os: dict,
|
||||
cmake_args: str,
|
||||
cxx_flags: str,
|
||||
) -> dict:
|
||||
extra_warning_flags = ""
|
||||
linker_relocation_flags = ""
|
||||
linker_flags = ""
|
||||
|
||||
cxx_flags += " -fno-omit-frame-pointer"
|
||||
|
||||
# Use large code model to avoid relocation errors with large binaries
|
||||
# Only for x86-64 (amd64) - ARM64 doesn't support -mcmodel=large
|
||||
if architecture["platform"] == "linux/amd64" and os["compiler_name"] == "gcc":
|
||||
# Add -mcmodel=large to both compiler AND linker flags
|
||||
# This is needed because sanitizers create very large binaries and
|
||||
# large model removes the 2GB limitation that medium model has
|
||||
cxx_flags += " -mcmodel=large -fno-PIC"
|
||||
linker_relocation_flags += " -mcmodel=large -fno-PIC"
|
||||
|
||||
# Create default sanitizer flags
|
||||
sanitizers_flags = "undefined,float-divide-by-zero"
|
||||
|
||||
# There are some differences between GCC and Clang support for sanitizers.
|
||||
# Hence we must use diff. falg combinations for each compiler.
|
||||
# These combination of flags were tested to work with GCC 15 and Clang 20.
|
||||
# If the versions are changed, the flags might need to be updated.
|
||||
|
||||
if os["compiler_name"] == "gcc":
|
||||
# Suppress false positive warnings in GCC with stringop-overflow
|
||||
extra_warning_flags += " -Wno-stringop-overflow"
|
||||
# Disable mold, gold and lld linkers.
|
||||
# Use default linker (bfd/ld) which is more lenient with mixed code models
|
||||
cmake_args += " -Duse_mold=OFF -Duse_gold=OFF -Duse_lld=OFF"
|
||||
# Add linker flags for Sanitizers
|
||||
linker_flags += f" -DCMAKE_EXE_LINKER_FLAGS='{linker_relocation_flags} -fsanitize=address,{sanitizers_flags}'"
|
||||
linker_flags += f" -DCMAKE_SHARED_LINKER_FLAGS='{linker_relocation_flags} -fsanitize=address,{sanitizers_flags}'"
|
||||
elif os["compiler_name"] == "clang":
|
||||
# Note: We use $GITHUB_WORKSPACE environment variable which will be expanded by the shell
|
||||
# before CMake processes it. This ensures the compiler receives an absolute path.
|
||||
# CMAKE_SOURCE_DIR won't work here because it's inside CMAKE_CXX_FLAGS string.
|
||||
# GCC doesn't support ignorelist.
|
||||
cxx_flags += " -fsanitize-ignorelist=${GITHUB_WORKSPACE}/sanitizers/suppressions/sanitizer-ignorelist.txt"
|
||||
sanitizers_flags = f"{sanitizers_flags},unsigned-integer-overflow"
|
||||
linker_flags += (
|
||||
f" -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,{sanitizers_flags}'"
|
||||
)
|
||||
linker_flags += (
|
||||
f" -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address,{sanitizers_flags}'"
|
||||
)
|
||||
|
||||
# Sanitizers recommend minimum of -O1 for reasonable performance
|
||||
cxx_flags += " -O1"
|
||||
|
||||
# First create config for asan
|
||||
cmake_args_flags = f'{cmake_args} -DCMAKE_CXX_FLAGS="-fsanitize=address,{sanitizers_flags} {cxx_flags} {extra_warning_flags}" {linker_flags}'
|
||||
|
||||
# Add config with asan+ubsan
|
||||
configs = {}
|
||||
configs["asan_ubsan"] = cmake_args_flags
|
||||
|
||||
# Since TSAN runs are crashing with seg faults(could be compatibility issues with latest compilers)
|
||||
# We deactivate it for now. But I would keep the code, since it took some effort to find the correct set of config needed to run this.
|
||||
# This will be useful when we decide to activate it again in future.
|
||||
activateTSAN = False
|
||||
if activateTSAN:
|
||||
linker_flags = ""
|
||||
# Update configs for tsan
|
||||
# gcc doesn't supports atomic_thread_fence with tsan. Suppress warnings.
|
||||
# Also tsan doesn't work well with mcmode=large and bfd linker
|
||||
if os["compiler_name"] == "gcc":
|
||||
extra_warning_flags += " -Wno-tsan"
|
||||
cxx_flags = cxx_flags.replace("-mcmodel=large", "-mcmodel=medium")
|
||||
linker_relocation_flags = linker_relocation_flags.replace(
|
||||
"-mcmodel=large", "-mcmodel=medium"
|
||||
)
|
||||
# Add linker flags for Sanitizers
|
||||
linker_flags += f" -DCMAKE_EXE_LINKER_FLAGS='{linker_relocation_flags} -fsanitize=thread,{sanitizers_flags}'"
|
||||
linker_flags += f" -DCMAKE_SHARED_LINKER_FLAGS='{linker_relocation_flags} -fsanitize=thread,{sanitizers_flags}'"
|
||||
elif os["compiler_name"] == "clang":
|
||||
linker_flags += (
|
||||
f" -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=thread,{sanitizers_flags}'"
|
||||
)
|
||||
linker_flags += (
|
||||
f" -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=thread,{sanitizers_flags}'"
|
||||
)
|
||||
|
||||
cmake_args_flags = f"{cmake_args} -DCMAKE_CXX_FLAGS='-fsanitize=thread,{sanitizers_flags} {cxx_flags} {extra_warning_flags}' {linker_flags}"
|
||||
|
||||
# Add config with tsan+ubsan
|
||||
configs["tsan_ubsan"] = cmake_args_flags
|
||||
|
||||
return configs
|
||||
|
||||
|
||||
def read_config(file: Path) -> Config:
|
||||
config = json.loads(file.read_text())
|
||||
if (
|
||||
|
||||
21
.github/workflows/reusable-build-test-config.yml
vendored
21
.github/workflows/reusable-build-test-config.yml
vendored
@@ -50,6 +50,12 @@ on:
|
||||
type: number
|
||||
default: 2
|
||||
|
||||
sanitizers:
|
||||
description: "The sanitizers to enable ('Address+UndefinedBehavior' or 'Thread+UndefinedBehavior')."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: "The Codecov token to use for uploading coverage reports."
|
||||
@@ -68,6 +74,7 @@ jobs:
|
||||
env:
|
||||
ENABLED_VOIDSTAR: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }}
|
||||
ENABLED_COVERAGE: ${{ contains(inputs.cmake_args, '-Dcoverage=ON') }}
|
||||
ENABLED_SANITIZERS: ${{ contains(inputs.cmake_args, '-fsanitize') }}
|
||||
steps:
|
||||
- name: Cleanup workspace (macOS and Windows)
|
||||
if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }}
|
||||
@@ -102,18 +109,18 @@ jobs:
|
||||
# Set the verbosity to "quiet" for Windows to avoid an excessive
|
||||
# amount of logs. For other OSes, the "verbose" logs are more useful.
|
||||
log_verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }}
|
||||
sanitizers: ${{ inputs.sanitizers }}
|
||||
|
||||
- name: Configure CMake
|
||||
working-directory: ${{ inputs.build_dir }}
|
||||
env:
|
||||
BUILD_TYPE: ${{ inputs.build_type }}
|
||||
CMAKE_ARGS: ${{ inputs.cmake_args }}
|
||||
run: |
|
||||
cmake \
|
||||
-G '${{ runner.os == 'Windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \
|
||||
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
|
||||
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
|
||||
${CMAKE_ARGS} \
|
||||
${{ inputs.cmake_args }} \
|
||||
..
|
||||
|
||||
- name: Build the binary
|
||||
@@ -141,7 +148,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Check linking (Linux)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
if: ${{ runner.os == 'Linux' && env.ENABLED_SANITIZERS == 'false' }}
|
||||
working-directory: ${{ inputs.build_dir }}
|
||||
run: |
|
||||
ldd ./rippled
|
||||
@@ -166,6 +173,10 @@ jobs:
|
||||
BUILD_TYPE: ${{ inputs.build_type }}
|
||||
PARALLELISM: ${{ runner.os == 'Windows' && '1' || steps.nproc.outputs.nproc }}
|
||||
run: |
|
||||
export ASAN_OPTIONS="detect_container_overflow=0 suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/asan.supp"
|
||||
export TSAN_OPTIONS="second_deadlock_stack=1 halt_on_error=0 suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/tsan.supp"
|
||||
export UBSAN_OPTIONS="suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/ubsan.supp"
|
||||
export LSAN_OPTIONS="suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/lsan.supp"
|
||||
ctest \
|
||||
--output-on-failure \
|
||||
-C "${BUILD_TYPE}" \
|
||||
@@ -177,6 +188,10 @@ jobs:
|
||||
env:
|
||||
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
|
||||
run: |
|
||||
export ASAN_OPTIONS="detect_container_overflow=0 suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/asan.supp"
|
||||
export TSAN_OPTIONS="second_deadlock_stack=1 halt_on_error=0 suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/tsan.supp"
|
||||
export UBSAN_OPTIONS="suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/ubsan.supp"
|
||||
export LSAN_OPTIONS="suppressions=$GITHUB_WORKSPACE/sanitizers/suppressions/lsan.supp"
|
||||
./rippled --unittest --unittest-jobs "${BUILD_NPROC}"
|
||||
|
||||
- name: Debug failure (Linux)
|
||||
|
||||
1
.github/workflows/reusable-build-test.yml
vendored
1
.github/workflows/reusable-build-test.yml
vendored
@@ -54,5 +54,6 @@ jobs:
|
||||
runs_on: ${{ toJSON(matrix.architecture.runner) }}
|
||||
image: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) || '' }}
|
||||
config_name: ${{ matrix.config_name }}
|
||||
sanitizers: ${{ matrix.sanitizers }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
47
BUILD.md
47
BUILD.md
@@ -141,37 +141,26 @@ Alternatively, you can pull the patched recipes into the repository and use them
|
||||
locally:
|
||||
|
||||
```bash
|
||||
# Extract the version number from the lockfile.
|
||||
function extract_version {
|
||||
version=$(cat conan.lock | sed -nE "s@.+${1}/(.+)#.+@\1@p" | head -n1)
|
||||
echo ${version}
|
||||
}
|
||||
|
||||
# Define which recipes to export.
|
||||
recipes=(ed25519 grpc secp256k1 snappy soci)
|
||||
|
||||
# Selectively check out the recipes from our CCI fork.
|
||||
cd external
|
||||
mkdir -p conan-center-index
|
||||
cd conan-center-index
|
||||
git init
|
||||
git remote add origin git@github.com:XRPLF/conan-center-index.git
|
||||
git sparse-checkout init
|
||||
for recipe in ${recipes[@]}; do
|
||||
echo "Checking out ${recipe}..."
|
||||
git sparse-checkout add recipes/${recipe}/all
|
||||
done
|
||||
git sparse-checkout set recipes/ed25519
|
||||
git sparse-checkout add recipes/grpc
|
||||
git sparse-checkout add recipes/secp256k1
|
||||
git sparse-checkout add recipes/snappy
|
||||
git sparse-checkout add recipes/soci
|
||||
git fetch origin master
|
||||
git checkout master
|
||||
rm -rf .git
|
||||
cd ../..
|
||||
|
||||
# Export the recipes into the local cache.
|
||||
for recipe in ${recipes[@]}; do
|
||||
version=$(extract_version ${recipe})
|
||||
echo "Exporting ${recipe}/${version}..."
|
||||
conan export --version $(extract_version ${recipe}) \
|
||||
external/conan-center-index/recipes/${recipe}/all
|
||||
done
|
||||
conan export --version 2015.03 external/conan-center-index/recipes/ed25519/all
|
||||
conan export --version 1.72.0 external/conan-center-index/recipes/grpc/all
|
||||
conan export --version 0.7.0 external/conan-center-index/recipes/secp256k1/all
|
||||
conan export --version 1.1.10 external/conan-center-index/recipes/snappy/all
|
||||
conan export --version 4.0.3 external/conan-center-index/recipes/soci/all
|
||||
```
|
||||
|
||||
In the case we switch to a newer version of a dependency that still requires a
|
||||
@@ -388,6 +377,20 @@ tools.build:cxxflags=['-DBOOST_ASIO_DISABLE_CONCEPTS']
|
||||
conan install .. --output-folder . --build missing --settings build_type=Debug
|
||||
```
|
||||
|
||||
If you would like to activate `asan`(`Address`) or `tsan`(`Thread`) or `ubsan`(`UndefinedBehavior`) for the build,
|
||||
declare an environment variable as follows(with values: `Address`, `Thread`, `UndefinedBehavior`) and use the `sanitizers`
|
||||
profile in the `conan install` command.
|
||||
|
||||
```
|
||||
SANITIZERS=Address conan install .. --output-folder . --profile:all sanitizers --build missing --settings build_type=Debug
|
||||
# or if you want asan+ubsan
|
||||
SANITIZERS=Address,UndefinedBehavior conan install .. --output-folder . --profile:all sanitizers --build missing --settings build_type=Debug
|
||||
```
|
||||
|
||||
Note: Do not mix Address and Thread, that's incompatible.
|
||||
|
||||
More details here: [sanitizers](./docs/build/sanitizers.md)
|
||||
|
||||
To build Debug, in the next step, be sure to set `-DCMAKE_BUILD_TYPE=Debug`
|
||||
|
||||
For a single-configuration generator, e.g. `Unix Makefiles` or `Ninja`,
|
||||
|
||||
1
conan/profiles/ci
Normal file
1
conan/profiles/ci
Normal file
@@ -0,0 +1 @@
|
||||
include(sanitizers)
|
||||
59
conan/profiles/sanitizers
Normal file
59
conan/profiles/sanitizers
Normal file
@@ -0,0 +1,59 @@
|
||||
include(default)
|
||||
{% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %}
|
||||
{% set sanitizers = os.getenv("SANITIZERS") %}
|
||||
|
||||
[conf]
|
||||
{% if sanitizers %}
|
||||
{% if compiler == "gcc" %}
|
||||
{% if "Address" in sanitizers or "Thread" in sanitizers or "UndefinedBehavior" in sanitizers %}
|
||||
{% set sanitizer_list = [] %}
|
||||
{% set model_code = "" %}
|
||||
{% set extra_cxxflags = "-fno-omit-frame-pointer -fno-PIC -O1 -Wno-stringop-overflow" %}
|
||||
|
||||
{% if "Address" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("address") %}
|
||||
{% set model_code = "-mcmodel=large" %}
|
||||
{% elif "Thread" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("thread") %}
|
||||
{% set model_code = "-mcmodel=medium" %}
|
||||
{% set extra_cxxflags = extra_cxxflags ~ " -Wno-tsan" %}
|
||||
{% endif %}
|
||||
|
||||
{% if "UndefinedBehavior" in sanitizers or "UndefinedBehavior" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("undefined") %}
|
||||
{% set _ = sanitizer_list.append("float-divide-by-zero") %}
|
||||
{% endif %}
|
||||
|
||||
{% set sanitizer_flags = "-fsanitize=" ~ ",".join(sanitizer_list) ~ " " ~ model_code %}
|
||||
|
||||
tools.build:cxxflags+=['{{sanitizer_flags}} {{extra_cxxflags}}']
|
||||
tools.build:sharedlinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:exelinkflags+=['{{sanitizer_flags}}']
|
||||
{% endif %}
|
||||
{% elif compiler == "apple-clang" or compiler == "clang" %}
|
||||
{% if "Address" in sanitizers or "Thread" in sanitizers or "UndefinedBehavior" in sanitizers %}
|
||||
{% set sanitizer_list = [] %}
|
||||
{% set extra_cxxflags = "-fno-omit-frame-pointer -fno-PIC -O1" %}
|
||||
|
||||
{% if "Address" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("address") %}
|
||||
{% elif "Thread" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("thread") %}
|
||||
{% endif %}
|
||||
|
||||
{% if "UndefinedBehavior" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("undefined") %}
|
||||
{% set _ = sanitizer_list.append("float-divide-by-zero") %}
|
||||
{% set _ = sanitizer_list.append("unsigned-integer-overflow") %}
|
||||
{% endif %}
|
||||
|
||||
{% set sanitizer_flags = "-fsanitize=" ~ ",".join(sanitizer_list) %}
|
||||
|
||||
tools.build:cxxflags+=['{{sanitizer_flags}} {{extra_cxxflags}}']
|
||||
tools.build:sharedlinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:exelinkflags+=['{{sanitizer_flags}}']
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags"]
|
||||
183
docs/build/sanitizers.md
vendored
Normal file
183
docs/build/sanitizers.md
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
# Sanitizer Configuration for Rippled
|
||||
|
||||
This document explains how to properly configure and run sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer) with the rippled project.
|
||||
Corresponding suppression files are located in the `sanitizers/suppressions` directory.
|
||||
|
||||
- [Sanitizer Configuration for Rippled](#sanitizer-configuration-for-rippled)
|
||||
- [Building with Sanitizers](#building-with-sanitizers)
|
||||
- [AddressSanitizer (ASan) + UndefinedBehaviorSanitizer (UBSan)](#addresssanitizer-asan--undefinedbehaviorsanitizer-ubsan)
|
||||
- [ThreadSanitizer (TSan) + UndefinedBehaviorSanitizer (UBSan)](#threadsanitizer-tsan--undefinedbehaviorsanitizer-ubsan)
|
||||
- [Just UndefinedBehaviorSanitizer (UBSan)](#just-undefinedbehaviorsanitizer-ubsan)
|
||||
- [Running Tests with Sanitizers](#running-tests-with-sanitizers)
|
||||
- [AddressSanitizer (ASan)](#addresssanitizer-asan)
|
||||
- [ThreadSanitizer (TSan)](#threadsanitizer-tsan)
|
||||
- [LeakSanitizer (LSan)](#leaksanitizer-lsan)
|
||||
- [Suppression Files](#suppression-files)
|
||||
- [asan.supp](#asansupp)
|
||||
- [lsan.supp](#lsansupp)
|
||||
- [ubsan.supp](#ubsansupp)
|
||||
- [tsan.supp](#tsansupp)
|
||||
- [Ignorelist](#sanitizer-ignorelisttxt)
|
||||
- [Known False Positives](#known-false-positives)
|
||||
- [References](#references)
|
||||
|
||||
## Building with Sanitizers
|
||||
|
||||
### Summary
|
||||
|
||||
Follow the same instructions as mentioned in [BUILD.md](../../BUILD.md) but with following changes:
|
||||
|
||||
1. Make sure you have clean build directory.
|
||||
2. Use `--profile sanitizers` to configure build options to include sanitizer flags. [sanitizes](../../conan/profiles/sanitizers) profile contains settings for all sanitizers.
|
||||
3. Set `ASAN_OPTIONS`, `LSAN_OPTIONS` ,`UBSAN_OPTIONS` and `TSAN_OPTIONS` environment variables to configure sanitizer behavior when running executables.
|
||||
|
||||
---
|
||||
|
||||
### Build steps:
|
||||
|
||||
```bash
|
||||
cd /path/to/rippled
|
||||
rm -rf .build
|
||||
mkdir .build
|
||||
cd .build
|
||||
```
|
||||
|
||||
#### AddressSanitizer (ASan) + UndefinedBehaviorSanitizer (UBSan)
|
||||
|
||||
Build with AddressSanitizer+UndefinedBehavior sanitizers (or you can choose just one of them).
|
||||
|
||||
```bash
|
||||
SANITIZERS=Address,UndefinedBehavior conan install .. --output-folder . --profile sanitizers --build missing --settings build_type=Release
|
||||
```
|
||||
|
||||
#### ThreadSanitizer (TSan) + UndefinedBehaviorSanitizer (UBSan)
|
||||
|
||||
```bash
|
||||
# Build dependencies with Thread sanitizer
|
||||
SANITIZERS=Thread,UndefinedBehavior conan install .. --output-folder . --profile sanitizers --build missing --settings build_type=Release
|
||||
```
|
||||
|
||||
#### Just UndefinedBehaviorSanitizer (UBSan)
|
||||
|
||||
```bash
|
||||
# Build dependencies with Thread sanitizer
|
||||
SANITIZERS=UndefinedBehavior conan install .. --output-folder . --profile sanitizers --build missing --settings build_type=Release
|
||||
```
|
||||
|
||||
Use `--profile:all sanitizers` if you would like to build all dependencies and libraries (boost etc.) with sanitizers. This might take long time but you won't see some false-positives on sanitizer reports since whole binary will be instrumented.
|
||||
|
||||
To build with Thread+UndefinedBehavior Sanitizer, replace `SANITIZERS=Address` with `SANITIZERS=Thread`.
|
||||
|
||||
# Configure CMake
|
||||
|
||||
```bash
|
||||
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -Dunity=ON -Dtests=ON -Dxrpld=ON
|
||||
|
||||
# Build
|
||||
cmake --build . --parallel 4
|
||||
```
|
||||
|
||||
## Running Tests with Sanitizers
|
||||
|
||||
### AddressSanitizer (ASan)
|
||||
|
||||
**IMPORTANT**: ASan with Boost produces many false positives. Use these options:
|
||||
|
||||
```bash
|
||||
export ASAN_OPTIONS="detect_container_overflow=0 suppressions=path/to/asan.supp halt_on_error=0 log_path=asan.log"
|
||||
export UBSAN_OPTIONS="suppressions=path/to/ubsan.supp print_stacktrace=1 halt_on_error=0 log_path=ubsan.log"
|
||||
export LSAN_OPTIONS="suppressions=path/to/lsan.supp halt_on_error=0 log_path=lsan.log"
|
||||
|
||||
# Run tests
|
||||
./rippled --unittest --unittest-jobs=5
|
||||
```
|
||||
|
||||
**Why `detect_container_overflow=0`?**
|
||||
|
||||
- Boost intrusive containers (used in `aged_unordered_container`) trigger false positives
|
||||
- Boost context switching (used in `Workers.cpp`) confuses ASan's stack tracking
|
||||
- Since we usually don't build boost(because we don't want to instrument boost and detect issues in boost code) with asan but use boost containers in ASAN instrumented rippled code, it generates false positives.
|
||||
- See: https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow
|
||||
|
||||
### ThreadSanitizer (TSan) + UndefinedBehaviorSanitizer (UBSan)
|
||||
|
||||
```bash
|
||||
export TSAN_OPTIONS="suppressions=path/to/tsan.supp halt_on_error=0 log_path=tsan.log"
|
||||
|
||||
# Run tests
|
||||
./rippled --unittest --unittest-jobs=5
|
||||
```
|
||||
|
||||
### LeakSanitizer (LSan)
|
||||
|
||||
LSan is automatically enabled with ASan. To disable it:
|
||||
|
||||
```bash
|
||||
export ASAN_OPTIONS="detect_leaks=0"
|
||||
```
|
||||
|
||||
## Suppression Files
|
||||
|
||||
### `asan.supp`
|
||||
|
||||
- **Purpose**: Suppress AddressSanitizer (ASan) errors only
|
||||
- **Format**: `interceptor_name:<pattern>` where pattern matches file names. Supported suppression types are:
|
||||
- interceptor_name
|
||||
- interceptor_via_fun
|
||||
- interceptor_via_lib
|
||||
- odr_violation
|
||||
- **More info**: [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer)
|
||||
- **Note**: Cannot suppress stack-buffer-overflow, container-overflow, etc.
|
||||
|
||||
### `lsan.supp`
|
||||
|
||||
- **Purpose**: Suppress LeakSanitizer (LSan) errors only
|
||||
- **Format**: `leak:<pattern>` where pattern matches function/file names
|
||||
- **More info**: [LeakSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer)
|
||||
|
||||
### `ubsan.supp`
|
||||
|
||||
- **Purpose**: Suppress UndefinedBehaviorSanitizer errors
|
||||
- **Format**: `<error_type>:<pattern>` (e.g., `unsigned-integer-overflow:protobuf`)
|
||||
- **Covers**: Intentional overflows in sanitizers/suppressions libraries (protobuf, gRPC, stdlib)
|
||||
|
||||
### `tsan.supp`
|
||||
|
||||
- **Purpose**: Suppress ThreadSanitizer data race warnings
|
||||
- **Format**: `race:<pattern>` where pattern matches function/file names
|
||||
- **More info**: [ThreadSanitizerSuppressions](https://github.com/google/sanitizers/wiki/ThreadSanitizerSuppressions)
|
||||
|
||||
### `sanitizer-ignorelist.txt`
|
||||
|
||||
- **Purpose**: Compile-time ignorelist for all sanitizers
|
||||
- **Usage**: Passed via `-fsanitize-ignorelist=absolute/path/to/sanitizer-ignorelist.txt`
|
||||
- **Format**: `<level>:<pattern>` (e.g., `src:Workers.cpp`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "ASan is ignoring requested \_\_asan_handle_no_return" warnings
|
||||
|
||||
These warnings appear when using Boost context switching and are harmless. They indicate potential false positives.
|
||||
|
||||
### Sanitizer Mismatch Errors
|
||||
|
||||
If you see undefined symbols like `___tsan_atomic_load` when building with ASan:
|
||||
|
||||
**Problem**: Dependencies were built with a different sanitizer than the main project.
|
||||
|
||||
**Solution**: Rebuild everything with the same sanitizer:
|
||||
|
||||
```bash
|
||||
rm -rf .build
|
||||
# Then follow the build instructions above
|
||||
```
|
||||
|
||||
Then review the log files: `asan.log.*`, `ubsan.log.*`, `tsan.log.*`
|
||||
|
||||
## References
|
||||
|
||||
- [AddressSanitizer Wiki](https://github.com/google/sanitizers/wiki/AddressSanitizer)
|
||||
- [AddressSanitizer Flags](https://github.com/google/sanitizers/wiki/AddressSanitizerFlags)
|
||||
- [Container Overflow Detection](https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow)
|
||||
- [UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html)
|
||||
- [ThreadSanitizer](https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual)
|
||||
32
sanitizers/suppressions/asan.supp
Normal file
32
sanitizers/suppressions/asan.supp
Normal file
@@ -0,0 +1,32 @@
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppresions.
|
||||
#
|
||||
# ASAN_OPTIONS="detect_container_overflow=0 suppressions=sanitizers/suppressions/asan.supp halt_on_error=0"
|
||||
#
|
||||
# The detect_container_overflow=0 option disables false positives from:
|
||||
# - Boost intrusive containers (slist_iterator.hpp, hashtable.hpp, aged_unordered_container.h)
|
||||
# - Boost context/coroutine stack switching (Workers.cpp, thread.h)
|
||||
#
|
||||
# See: https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow
|
||||
|
||||
# Suprpress voilations in sanitizers/suppressions code
|
||||
interceptor_name:^sanitizers/suppressions
|
||||
|
||||
# Boost
|
||||
interceptor_name:boost/asio
|
||||
|
||||
# Leaks in Doctest tests: xrpl.test.*
|
||||
interceptor_name:src/libxrpl/net/HTTPClient.cpp
|
||||
interceptor_name:src/libxrpl/net/RegisterSSLCerts.cpp
|
||||
interceptor_name:src/tests/libxrpl/net/HTTPClient.cpp
|
||||
interceptor_name:xrpl/net/AutoSocket.h
|
||||
interceptor_name:xrpl/net/HTTPClient.h
|
||||
interceptor_name:xrpl/net/HTTPClientSSLContext.h
|
||||
interceptor_name:xrpl/net/RegisterSSLCerts.h
|
||||
|
||||
# Suppress false positive stack-buffer errors in thread stack allocation
|
||||
# Related to ASan's __asan_handle_no_return warnings (github.com/google/sanitizers/issues/189)
|
||||
# These occur during multi-threaded test initialization on macOS
|
||||
interceptor_name:memcpy
|
||||
interceptor_name:__bzero
|
||||
interceptor_name:__asan_memset
|
||||
interceptor_name:__asan_memcpy
|
||||
16
sanitizers/suppressions/lsan.supp
Normal file
16
sanitizers/suppressions/lsan.supp
Normal file
@@ -0,0 +1,16 @@
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppresions.
|
||||
|
||||
# Suppress leaks detected by asan in rippled code.
|
||||
leak:src/libxrpl/net/HTTPClient.cpp
|
||||
leak:src/libxrpl/net/RegisterSSLCerts.cpp
|
||||
leak:src/tests/libxrpl/net/HTTPClient.cpp
|
||||
leak:xrpl/net/AutoSocket.h
|
||||
leak:xrpl/net/HTTPClient.h
|
||||
leak:xrpl/net/HTTPClientSSLContext.h
|
||||
leak:xrpl/net/RegisterSSLCerts.h
|
||||
leak:ripple::HTTPClient
|
||||
leak:ripple::HTTPClientImp
|
||||
|
||||
# Suppress leaks detected by asan in boost code.
|
||||
leak:boost::asio
|
||||
leak:boost/asio
|
||||
27
sanitizers/suppressions/sanitizer-ignorelist.txt
Normal file
27
sanitizers/suppressions/sanitizer-ignorelist.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
deadlock:libxrpl/beast/utility/beast_Journal.cpp
|
||||
deadlock:libxrpl/beast/utility/beast_Journal.cpp
|
||||
deadlock:libxrpl/beast/utility/beast_PropertyStream.cpp
|
||||
deadlock:test/beast/beast_PropertyStream_test.cpp
|
||||
deadlock:xrpld/core/detail/Workers.cpp
|
||||
deadlock:xrpld/core/JobQueue.cpp
|
||||
|
||||
race:libxrpl/beast/utility/beast_Journal.cpp
|
||||
race:libxrpl/beast/utility/beast_Journal.cpp
|
||||
race:libxrpl/beast/utility/beast_PropertyStream.cpp
|
||||
race:test/beast/beast_PropertyStream_test.cpp
|
||||
race:xrpld/core/detail/Workers.cpp
|
||||
race:xrpld/core/JobQueue.cpp
|
||||
|
||||
signal:libxrpl/beast/utility/beast_Journal.cpp
|
||||
signal:libxrpl/beast/utility/beast_Journal.cpp
|
||||
signal:libxrpl/beast/utility/beast_PropertyStream.cpp
|
||||
signal:test/beast/beast_PropertyStream_test.cpp
|
||||
signal:xrpld/core/detail/Workers.cpp
|
||||
signal:xrpld/core/JobQueue.cpp
|
||||
|
||||
src:beast/utility/beast_Journal.cpp
|
||||
src:beast/utility/beast_PropertyStream.cpp
|
||||
src:core/detail/Workers.cpp
|
||||
src:core/JobQueue.cpp
|
||||
src:libxrpl/beast/utility/beast_Journal.cpp
|
||||
src:test/beast/beast_PropertyStream_test.cpp
|
||||
102
sanitizers/suppressions/tsan.supp
Normal file
102
sanitizers/suppressions/tsan.supp
Normal file
@@ -0,0 +1,102 @@
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppresions.
|
||||
|
||||
# Suppress race in Boost ASIO scheduler detected by GCC-15
|
||||
# This is a false positive in Boost's internal pipe() synchronization
|
||||
race:boost/asio/
|
||||
race:boost/context/
|
||||
race:boost/asio/executor.hpp
|
||||
race:boost::asio
|
||||
|
||||
# Suppress tsan related issues in rippled code.
|
||||
race:src/libxrpl/basics/make_SSLContext.cpp
|
||||
race:src/libxrpl/basics/Number.cpp
|
||||
race:src/libxrpl/json/json_value.cpp
|
||||
race:src/libxrpl/json/to_string.cpp
|
||||
race:src/libxrpl/ledger/OpenView.cpp
|
||||
race:src/libxrpl/net/HTTPClient.cpp
|
||||
race:src/libxrpl/nodestore/backend/NuDBFactory.cpp
|
||||
race:src/libxrpl/protocol/InnerObjectFormats.cpp
|
||||
race:src/libxrpl/protocol/STParsedJSON.cpp
|
||||
race:src/libxrpl/resource/ResourceManager.cpp
|
||||
race:src/test/app/Flow_test.cpp
|
||||
race:src/test/app/LedgerReplay_test.cpp
|
||||
race:src/test/app/NFToken_test.cpp
|
||||
race:src/test/app/Offer_test.cpp
|
||||
race:src/test/app/ValidatorSite_test.cpp
|
||||
race:src/test/consensus/NegativeUNL_test.cpp
|
||||
race:src/test/jtx/impl/Env.cpp
|
||||
race:src/test/jtx/impl/JSONRPCClient.cpp
|
||||
race:src/test/jtx/impl/pay.cpp
|
||||
race:src/test/jtx/impl/token.cpp
|
||||
race:src/test/rpc/Book_test.cpp
|
||||
race:src/xrpld/app/ledger/detail/InboundTransactions.cpp
|
||||
race:src/xrpld/app/main/Application.cpp
|
||||
race:src/xrpld/app/main/BasicApp.cpp
|
||||
race:src/xrpld/app/main/GRPCServer.cpp
|
||||
race:src/xrpld/app/misc/detail/AmendmentTable.cpp
|
||||
race:src/xrpld/app/misc/FeeVoteImpl.cpp
|
||||
race:src/xrpld/app/rdb/detail/Wallet.cpp
|
||||
race:src/xrpld/overlay/detail/OverlayImpl.cpp
|
||||
race:src/xrpld/peerfinder/detail/PeerfinderManager.cpp
|
||||
race:src/xrpld/peerfinder/detail/SourceStrings.cpp
|
||||
race:src/xrpld/rpc/detail/ServerHandler.cpp
|
||||
race:xrpl/server/detail/Door.h
|
||||
race:xrpl/server/detail/Spawn.h
|
||||
race:xrpl/server/detail/ServerImpl.h
|
||||
race:xrpl/nodestore/detail/DatabaseNodeImp.h
|
||||
race:src/libxrpl/beast/utility/beast_Journal.cpp
|
||||
race:src/test/beast/LexicalCast_test.cpp
|
||||
race:ripple::ServerHandler
|
||||
|
||||
# More suppressions in external library code.
|
||||
race:crtstuff.c
|
||||
race:pipe
|
||||
|
||||
# Deadlock / lock-order-inversion suppressions
|
||||
# Note: GCC's TSAN may not fully support all deadlock suppression patterns
|
||||
deadlock:src/libxrpl/beast/utility/beast_Journal.cpp
|
||||
deadlock:src/libxrpl/beast/utility/beast_PropertyStream.cpp
|
||||
deadlock:src/test/beast/beast_PropertyStream_test.cpp
|
||||
deadlock:src/xrpld/core/detail/Workers.cpp
|
||||
deadlock:src/xrpld/app/misc/detail/Manifest.cpp
|
||||
deadlock:src/xrpld/app/misc/detail/ValidatorList.cpp
|
||||
deadlock:src/xrpld/app/misc/detail/ValidatorSite.cpp
|
||||
|
||||
signal:src/libxrpl/beast/utility/beast_Journal.cpp
|
||||
signal:src/xrpld/core/detail/Workers.cpp
|
||||
signal:src/xrpld/core/JobQueue.cpp
|
||||
signal:ripple::Workers::Worker
|
||||
|
||||
# Aggressive suppressing of deadlock tsan errors
|
||||
deadlock:pthread_create
|
||||
deadlock:pthread_rwlock_rdlock
|
||||
deadlock:boost::asio
|
||||
|
||||
# Suppress SEGV crashes in TSAN itself during stringbuf operations
|
||||
# This appears to be a GCC-15 TSAN instrumentation issue with basic_stringbuf::str()
|
||||
# Commonly triggered in beast::Journal::ScopedStream destructor
|
||||
signal:std::__cxx11::basic_stringbuf
|
||||
signal:basic_stringbuf
|
||||
signal:basic_ostringstream
|
||||
|
||||
called_from_lib:libclang_rt
|
||||
race:ostreambuf_iterator
|
||||
race:basic_ostream
|
||||
|
||||
# Suppress SEGV in Boost ASIO memory allocation with GCC-15 TSAN
|
||||
signal:boost::asio::aligned_new
|
||||
signal:boost::asio::detail::memory
|
||||
|
||||
# Suppress SEGV in execute_native_thread_routine
|
||||
signal:execute_native_thread_routine
|
||||
|
||||
# Suppress data race in Boost Context fiber management
|
||||
# This is a false positive in Boost's exception state management during fiber context switching
|
||||
race:__cxxabiv1::manage_exception_state
|
||||
race:boost::context::fiber::resume
|
||||
race:boost::asio::detail::spawned_fiber_thread
|
||||
race:boost::asio::detail::spawned_fiber_thread::suspend_with
|
||||
race:boost::asio::detail::spawned_fiber_thread::destroy
|
||||
|
||||
# Suppress data race in __tsan_memcpy called from Boost fiber operations
|
||||
race:__tsan_memcpy
|
||||
234
sanitizers/suppressions/ubsan.supp
Normal file
234
sanitizers/suppressions/ubsan.supp
Normal file
@@ -0,0 +1,234 @@
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppresions.
|
||||
|
||||
# Suppress UBSan errors in external code by source file path
|
||||
# This matches any source file under the external/ directory
|
||||
alignment:external
|
||||
bool:external
|
||||
bounds:external
|
||||
cfi:external
|
||||
enum:external
|
||||
float-cast-overflow:external
|
||||
float-divide-by-zero:external
|
||||
function:external
|
||||
implicit-integer-sign-change:external
|
||||
implicit-signed-integer-truncation::external
|
||||
implicit-signed-integer-truncation:external
|
||||
implicit-unsigned-integer-truncation:external
|
||||
integer-divide-by-zero:external
|
||||
invalid-builtin-use:external
|
||||
invalid-objc-cast:external
|
||||
nonnull-attribute:external
|
||||
null:external
|
||||
nullability-arg:external
|
||||
nullability-assign:external
|
||||
nullability-return:external
|
||||
object-size:external
|
||||
pointer-overflow:external
|
||||
return:external
|
||||
returns-nonnull-attribute:external
|
||||
shift-base:external
|
||||
shift-exponent:external
|
||||
signed-integer-overflow:external
|
||||
undefined:external
|
||||
unreachable:external
|
||||
unsigned-integer-overflow:external
|
||||
vla-bound:external
|
||||
vptr_check:external
|
||||
vptr:external
|
||||
|
||||
# Suppress all UBSan errors in Boost libraries
|
||||
# This matches any files containing "boost" in its path or name
|
||||
alignment:boost
|
||||
bool:boost
|
||||
bounds:boost
|
||||
cfi:boost
|
||||
enum:boost
|
||||
float-cast-overflow:boost
|
||||
float-divide-by-zero:boost
|
||||
function:boost
|
||||
implicit-integer-sign-change:boost
|
||||
implicit-signed-integer-truncation:boost
|
||||
implicit-unsigned-integer-truncation:boost
|
||||
integer-divide-by-zero:boost
|
||||
invalid-builtin-use:boost
|
||||
invalid-objc-cast:boost
|
||||
nonnull-attribute:boost
|
||||
null:boost
|
||||
nullability-arg:boost
|
||||
nullability-assign:boost
|
||||
nullability-return:boost
|
||||
object-size:boost
|
||||
pointer-overflow:boost
|
||||
return:boost
|
||||
returns-nonnull-attribute:boost
|
||||
shift-base:boost
|
||||
shift-exponent:boost
|
||||
signed-integer-overflow:boost
|
||||
undefined:boost
|
||||
unreachable:boost
|
||||
unsigned-integer-overflow:boost
|
||||
vla-bound:boost
|
||||
vptr_check:boost
|
||||
vptr:boost
|
||||
|
||||
# Google protobuf
|
||||
undefined:protobuf
|
||||
|
||||
# Suppress UBSan errors in rippled code by source file path
|
||||
undefined:src/libxrpl/basics/base64.cpp
|
||||
undefined:src/libxrpl/basics/Number.cpp
|
||||
undefined:src/libxrpl/beast/utility/beast_Journal.cpp
|
||||
undefined:src/libxrpl/crypto/RFC1751.cpp
|
||||
undefined:src/libxrpl/ledger/ApplyView.cpp
|
||||
undefined:src/libxrpl/ledger/View.cpp
|
||||
undefined:src/libxrpl/protocol/Permissions.cpp
|
||||
undefined:src/libxrpl/protocol/STAmount.cpp
|
||||
undefined:src/libxrpl/protocol/STPathSet.cpp
|
||||
undefined:src/libxrpl/protocol/tokens.cpp
|
||||
undefined:src/libxrpl/shamap/SHAMap.cpp
|
||||
undefined:src/test/app/Batch_test.cpp
|
||||
undefined:src/test/app/Invariants_test.cpp
|
||||
undefined:src/test/app/NFToken_test.cpp
|
||||
undefined:src/test/app/Offer_test.cpp
|
||||
undefined:src/test/app/Path_test.cpp
|
||||
undefined:src/test/basics/XRPAmount_test.cpp
|
||||
undefined:src/test/beast/LexicalCast_test.cpp
|
||||
undefined:src/test/jtx/impl/acctdelete.cpp
|
||||
undefined:src/test/ledger/SkipList_test.cpp
|
||||
undefined:src/test/rpc/Subscribe_test.cpp
|
||||
undefined:src/tests/libxrpl/basics/RangeSet.cpp
|
||||
undefined:src/xrpld/app/main/BasicApp.cpp
|
||||
undefined:src/xrpld/app/main/BasicApp.cpp
|
||||
undefined:src/xrpld/app/misc/detail/AmendmentTable.cpp
|
||||
undefined:src/xrpld/app/misc/NetworkOPs.cpp
|
||||
undefined:src/libxrpl/json/json_value.cpp
|
||||
undefined:src/xrpld/app/paths/detail/StrandFlow.h
|
||||
undefined:src/xrpld/app/tx/detail/NFTokenMint.cpp
|
||||
undefined:src/xrpld/app/tx/detail/SetOracle.cpp
|
||||
undefined:src/xrpld/core/detail/JobQueue.cpp
|
||||
undefined:src/xrpld/core/detail/Workers.cpp
|
||||
undefined:src/xrpld/rpc/detail/Role.cpp
|
||||
undefined:src/xrpld/rpc/handlers/GetAggregatePrice.cpp
|
||||
undefined:xrpl/basics/base_uint.h
|
||||
undefined:xrpl/basics/DecayingSample.h
|
||||
undefined:xrpl/beast/test/yield_to.h
|
||||
undefined:xrpl/beast/xor_shift_engine.h
|
||||
undefined:xrpl/nodestore/detail/varint.h
|
||||
undefined:xrpl/peerfinder/detail/Counts.h
|
||||
undefined:xrpl/protocol/nft.h
|
||||
|
||||
# basic_string.h:483:51: runtime error: unsigned integer overflow
|
||||
unsigned-integer-overflow:basic_string.h
|
||||
unsigned-integer-overflow:bits/chrono.h
|
||||
unsigned-integer-overflow:bits/random.h
|
||||
unsigned-integer-overflow:bits/random.tcc
|
||||
unsigned-integer-overflow:bits/stl_algobase.h
|
||||
unsigned-integer-overflow:bits/uniform_int_dist.h
|
||||
unsigned-integer-overflow:string_view
|
||||
|
||||
# runtime error: unsigned integer overflow: 0 - 1 cannot be represented in type 'std::size_t' (aka 'unsigned long')
|
||||
unsigned-integer-overflow:src/libxrpl/basics/base64.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/basics/Number.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/crypto/RFC1751.cpp
|
||||
unsigned-integer-overflow:rc/libxrpl/json/json_value.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/ledger/ApplyView.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/ledger/View.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/protocol/Permissions.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/protocol/STAmount.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/protocol/STPathSet.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/protocol/tokens.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/shamap/SHAMap.cpp
|
||||
unsigned-integer-overflow:src/test/app/Batch_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/Invariants_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/NFToken_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/Offer_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/Path_test.cpp
|
||||
unsigned-integer-overflow:src/test/basics/XRPAmount_test.cpp
|
||||
unsigned-integer-overflow:src/test/beast/LexicalCast_test.cpp
|
||||
unsigned-integer-overflow:src/test/jtx/impl/acctdelete.cpp
|
||||
unsigned-integer-overflow:src/test/ledger/SkipList_test.cpp
|
||||
unsigned-integer-overflow:src/test/rpc/Subscribe_test.cpp
|
||||
unsigned-integer-overflow:src/tests/libxrpl/basics/RangeSet.cpp
|
||||
unsigned-integer-overflow:src/xrpld/app/main/BasicApp.cpp
|
||||
unsigned-integer-overflow:src/xrpld/app/misc/detail/AmendmentTable.cpp
|
||||
unsigned-integer-overflow:src/xrpld/app/misc/NetworkOPs.cpp
|
||||
unsigned-integer-overflow:src/xrpld/app/paths/detail/StrandFlow.h
|
||||
unsigned-integer-overflow:src/xrpld/app/tx/detail/NFTokenMint.cpp
|
||||
unsigned-integer-overflow:src/xrpld/app/tx/detail/SetOracle.cpp
|
||||
unsigned-integer-overflow:src/xrpld/rpc/detail/Role.cpp
|
||||
unsigned-integer-overflow:src/xrpld/rpc/handlers/GetAggregatePrice.cpp
|
||||
unsigned-integer-overflow:xrpl/basics/base_uint.h
|
||||
unsigned-integer-overflow:xrpl/basics/DecayingSample.h
|
||||
unsigned-integer-overflow:xrpl/beast/test/yield_to.h
|
||||
unsigned-integer-overflow:xrpl/beast/xor_shift_engine.h
|
||||
unsigned-integer-overflow:xrpl/nodestore/detail/varint.h
|
||||
unsigned-integer-overflow:xrpl/peerfinder/detail/Counts.h
|
||||
unsigned-integer-overflow:xrpl/protocol/nft.h
|
||||
|
||||
# Rippled intentional overflows and operations
|
||||
# STAmount uses intentional negation of INT64_MIN and overflow in arithmetic
|
||||
signed-integer-overflow:src/libxrpl/protocol/STAmount.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/protocol/STAmount.cpp
|
||||
|
||||
# XRPAmount test intentional overflows
|
||||
signed-integer-overflow:src/test/basics/XRPAmount_test.cpp
|
||||
|
||||
# Peerfinder intentional overflow in counter arithmetic
|
||||
unsigned-integer-overflow:src/xrpld/peerfinder/detail/Counts.h
|
||||
|
||||
# Signed integer overflow suppressions
|
||||
signed-integer-overflow:src/test/beast/LexicalCast_test.cpp
|
||||
|
||||
# External library suppressions
|
||||
unsigned-integer-overflow:nudb/detail/xxhash.hpp
|
||||
|
||||
# Protobuf intentional overflows in hash functions
|
||||
# Protobuf uses intentional unsigned overflow for hash computation (stringpiece.h:393)
|
||||
unsigned-integer-overflow:google/protobuf/stubs/stringpiece.h
|
||||
|
||||
# gRPC intentional overflows
|
||||
# gRPC uses intentional overflow in timer calculations
|
||||
unsigned-integer-overflow:grpc
|
||||
unsigned-integer-overflow:timer_manager.cc
|
||||
|
||||
# Standard library intentional overflows
|
||||
# These are intentional overflows in random number generation and character conversion
|
||||
unsigned-integer-overflow:__random/seed_seq.h
|
||||
unsigned-integer-overflow:__charconv/traits.h
|
||||
|
||||
|
||||
# Suppress errors in RocksDB
|
||||
# RocksDB uses intentional unsigned integer overflows in hash functions and CRC calculations
|
||||
unsigned-integer-overflow:rocks*/b/src/util/xxhash.h
|
||||
unsigned-integer-overflow:rocks*/b/src/util/xxph3.h
|
||||
unsigned-integer-overflow:rocks*/b/src/util/hash.cc
|
||||
unsigned-integer-overflow:rocks*/b/src/util/crc32c.cc
|
||||
unsigned-integer-overflow:rocks*/b/src/util/crc32c.h
|
||||
unsigned-integer-overflow:rocks*/b/src/include/rocksdb/utilities/options_type.h
|
||||
unsigned-integer-overflow:rocks*/b/src/table/format.h
|
||||
unsigned-integer-overflow:rocks*/b/src/table/format.cc
|
||||
unsigned-integer-overflow:rocks*/b/src/table/block_based/block_based_table_builder.cc
|
||||
unsigned-integer-overflow:rocks*/b/src/table/block_based/reader_common.cc
|
||||
unsigned-integer-overflow:rocks*/b/src/db/version_set.cc
|
||||
|
||||
# RocksDB misaligned loads (intentional for performance on ARM64)
|
||||
alignment:rocks*/b/src/util/crc32c_arm64.cc
|
||||
|
||||
# nudb intentional overflows in hash functions
|
||||
unsigned-integer-overflow:nudb/detail/xxhash.hpp
|
||||
alignment:nudb/detail/xxhash.hpp
|
||||
|
||||
# Snappy compression library intentional overflows
|
||||
unsigned-integer-overflow:snapp*/b/src/snappy.cc
|
||||
|
||||
# Abseil intentional overflows
|
||||
unsigned-integer-overflow:abse*/b/src/absl/strings/numbers.cc
|
||||
unsigned-integer-overflow:abse*/b/src/absl/strings/internal/cord_rep_flat.h
|
||||
|
||||
# Standard library intentional overflows in chrono duration arithmetic
|
||||
unsigned-integer-overflow:__chrono/duration.h
|
||||
|
||||
# Suppress undefined errors in RocksDB and nudb
|
||||
undefined:rocks.*/b/src/util/crc32c_arm64.cc
|
||||
undefined:rocks.*/b/src/util/xxhash.h
|
||||
undefined:nudb
|
||||
@@ -1,642 +0,0 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
// DO NOT REMOVE
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpld/app/misc/LendingHelpers.h>
|
||||
#include <xrpld/app/misc/LoadFeeTrack.h>
|
||||
#include <xrpld/app/tx/detail/Batch.h>
|
||||
#include <xrpld/app/tx/detail/LoanSet.h>
|
||||
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class LendingHelpers_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testComputeRaisedRate()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedRaisedRate;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedRaisedRate = Number{1}, // (1 + r)^0 = 1
|
||||
},
|
||||
{
|
||||
.name = "One payment remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 1,
|
||||
.expectedRaisedRate = Number{105, -2},
|
||||
}, // 1.05^1
|
||||
{
|
||||
.name = "Multiple payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 3,
|
||||
.expectedRaisedRate = Number{1157625, -6},
|
||||
}, // 1.05^3
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedRaisedRate = Number{1}, // (1 + 0)^5 = 1
|
||||
}};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeRaisedRate: " + tc.name);
|
||||
|
||||
auto const computedRaisedRate =
|
||||
computeRaisedRate(tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedRaisedRate == tc.expectedRaisedRate,
|
||||
"Raised rate mismatch: expected " +
|
||||
to_string(tc.expectedRaisedRate) + ", got " +
|
||||
to_string(computedRaisedRate));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testComputePaymentFactor()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPaymentFactor;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPaymentFactor = Number{25, -2},
|
||||
}, // 1/4 = 0.25
|
||||
{
|
||||
.name = "One payment remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 1,
|
||||
.expectedPaymentFactor = Number{105, -2},
|
||||
}, // 0.05/1 = 1.05
|
||||
{
|
||||
.name = "Multiple payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPaymentFactor = Number{367208564631245, -15},
|
||||
}, // from calc
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPaymentFactor = Number{0},
|
||||
} // edge case
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computePaymentFactor: " + tc.name);
|
||||
|
||||
auto const computedPaymentFactor =
|
||||
computePaymentFactor(tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPaymentFactor == tc.expectedPaymentFactor,
|
||||
"Payment factor mismatch: expected " +
|
||||
to_string(tc.expectedPaymentFactor) + ", got " +
|
||||
to_string(computedPaymentFactor));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanPeriodicPayment()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPeriodicPayment;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedPeriodicPayment = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPeriodicPayment = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPeriodicPayment = Number{250},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate =
|
||||
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPeriodicPayment =
|
||||
Number{3895690663961231, -13}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanPeriodicPayment: " + tc.name);
|
||||
|
||||
auto const computedPeriodicPayment = loanPeriodicPayment(
|
||||
tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPeriodicPayment == tc.expectedPeriodicPayment,
|
||||
"Periodic payment mismatch: expected " +
|
||||
to_string(tc.expectedPeriodicPayment) + ", got " +
|
||||
to_string(computedPeriodicPayment));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanPrincipalFromPeriodicPayment()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicPayment;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPrincipalOutstanding;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero periodic payment",
|
||||
.periodicPayment = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedPrincipalOutstanding = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicPayment = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPrincipalOutstanding = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicPayment = Number{250},
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPrincipalOutstanding = Number{1'000},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.periodicPayment = Number{3895690663961231, -13}, // from calc
|
||||
.periodicRate =
|
||||
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPrincipalOutstanding = Number{1'000},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanPrincipalFromPeriodicPayment: " + tc.name);
|
||||
|
||||
auto const computedPrincipalOutstanding =
|
||||
loanPrincipalFromPeriodicPayment(
|
||||
tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
|
||||
"Principal outstanding mismatch: expected " +
|
||||
to_string(tc.expectedPrincipalOutstanding) + ", got " +
|
||||
to_string(computedPrincipalOutstanding));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testComputeOverpaymentComponents()
|
||||
{
|
||||
testcase("computeOverpaymentComponents");
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const IOU = issuer["IOU"];
|
||||
int32_t const loanScale = 1;
|
||||
auto const overpayment = Number{1'000};
|
||||
auto const overpaymentInterestRate = TenthBips32{10'000}; // 10%
|
||||
auto const overpaymentFeeRate = TenthBips32{50'000}; // 50%
|
||||
auto const managementFeeRate = TenthBips16{10'000}; // 10%
|
||||
|
||||
auto const expectedOverpaymentFee = Number{500}; // 50% of 1,000
|
||||
auto const expectedOverpaymentInterestGross =
|
||||
Number{100}; // 10% of 1,000
|
||||
auto const expectedOverpaymentInterestNet =
|
||||
Number{90}; // 100 - 10% of 100
|
||||
auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
|
||||
auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
|
||||
|
||||
auto const components = detail::computeOverpaymentComponents(
|
||||
IOU,
|
||||
loanScale,
|
||||
overpayment,
|
||||
overpaymentInterestRate,
|
||||
overpaymentFeeRate,
|
||||
managementFeeRate);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.untrackedManagementFee == expectedOverpaymentFee);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.untrackedInterest == expectedOverpaymentInterestNet);
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta ==
|
||||
expectedOverpaymentManagementFee);
|
||||
BEAST_EXPECT(
|
||||
components.trackedPrincipalDelta == expectedPrincipalPortion);
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta +
|
||||
components.untrackedInterest ==
|
||||
expectedOverpaymentInterestGross);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta +
|
||||
components.untrackedInterest +
|
||||
components.trackedPrincipalDelta +
|
||||
components.untrackedManagementFee ==
|
||||
overpayment);
|
||||
}
|
||||
|
||||
void
|
||||
testComputeInterestAndFeeParts()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number interest;
|
||||
TenthBips16 managementFeeRate;
|
||||
Number expectedInterestPart;
|
||||
Number expectedFeePart;
|
||||
};
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const IOU = issuer["IOU"];
|
||||
std::int32_t const loanScale = 1;
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{.name = "Zero interest",
|
||||
.interest = Number{0},
|
||||
.managementFeeRate = TenthBips16{10'000},
|
||||
.expectedInterestPart = Number{0},
|
||||
.expectedFeePart = Number{0}},
|
||||
{.name = "Zero fee rate",
|
||||
.interest = Number{1'000},
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
.expectedInterestPart = Number{1'000},
|
||||
.expectedFeePart = Number{0}},
|
||||
{.name = "10% fee rate",
|
||||
.interest = Number{1'000},
|
||||
.managementFeeRate = TenthBips16{10'000},
|
||||
.expectedInterestPart = Number{900},
|
||||
.expectedFeePart = Number{100}},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeInterestAndFeeParts: " + tc.name);
|
||||
|
||||
auto const [computedInterestPart, computedFeePart] =
|
||||
computeInterestAndFeeParts(
|
||||
IOU, tc.interest, tc.managementFeeRate, loanScale);
|
||||
BEAST_EXPECTS(
|
||||
computedInterestPart == tc.expectedInterestPart,
|
||||
"Interest part mismatch: expected " +
|
||||
to_string(tc.expectedInterestPart) + ", got " +
|
||||
to_string(computedInterestPart));
|
||||
BEAST_EXPECTS(
|
||||
computedFeePart == tc.expectedFeePart,
|
||||
"Fee part mismatch: expected " + to_string(tc.expectedFeePart) +
|
||||
", got " + to_string(computedFeePart));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanLatePaymentInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
TenthBips32 lateInterestRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t nextPaymentDueDate;
|
||||
Number expectedLateInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "On-time payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 3'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Early payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 4'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "No principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "No late interest rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{0}, // 0%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Late payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{100'000}, // 100%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest =
|
||||
Number{3170979198376459, -17}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanLatePaymentInterest: " + tc.name);
|
||||
|
||||
auto const computedLateInterest = loanLatePaymentInterest(
|
||||
tc.principalOutstanding,
|
||||
tc.lateInterestRate,
|
||||
tc.parentCloseTime,
|
||||
tc.nextPaymentDueDate);
|
||||
BEAST_EXPECTS(
|
||||
computedLateInterest == tc.expectedLateInterest,
|
||||
"Late interest mismatch: expected " +
|
||||
to_string(tc.expectedLateInterest) + ", got " +
|
||||
to_string(computedLateInterest));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanAccruedInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
Number periodicRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t startDate;
|
||||
std::uint32_t prevPaymentDate;
|
||||
std::uint32_t paymentInterval;
|
||||
Number expectedAccruedInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Before start date",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{1'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 1'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{0},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payment interval",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 0,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 1'000,
|
||||
.prevPaymentDate = 2'000,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest =
|
||||
Number{1929012345679012, -17}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanAccruedInterest: " + tc.name);
|
||||
|
||||
auto const computedAccruedInterest = loanAccruedInterest(
|
||||
tc.principalOutstanding,
|
||||
tc.periodicRate,
|
||||
tc.parentCloseTime,
|
||||
tc.startDate,
|
||||
tc.prevPaymentDate,
|
||||
tc.paymentInterval);
|
||||
BEAST_EXPECTS(
|
||||
computedAccruedInterest == tc.expectedAccruedInterest,
|
||||
"Accrued interest mismatch: expected " +
|
||||
to_string(tc.expectedAccruedInterest) + ", got " +
|
||||
to_string(computedAccruedInterest));
|
||||
}
|
||||
}
|
||||
|
||||
// This test overlaps with testLoanAccruedInterest, the test cases only
|
||||
// exercise the computeFullPaymentInterest parts unique to it.
|
||||
void
|
||||
testComputeFullPaymentInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number rawPrincipalOutstanding;
|
||||
Number periodicRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t paymentInterval;
|
||||
std::uint32_t prevPaymentDate;
|
||||
std::uint32_t startDate;
|
||||
TenthBips32 closeInterestRate;
|
||||
Number expectedFullPaymentInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.rawPrincipalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{10'000},
|
||||
.expectedFullPaymentInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero close interest rate",
|
||||
.rawPrincipalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{0},
|
||||
.expectedFullPaymentInterest =
|
||||
Number{1929012345679012, -17}, // from calc
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.rawPrincipalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{10'000},
|
||||
.expectedFullPaymentInterest =
|
||||
Number{1000192901234568, -13}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeFullPaymentInterest: " + tc.name);
|
||||
|
||||
auto const computedFullPaymentInterest = computeFullPaymentInterest(
|
||||
tc.rawPrincipalOutstanding,
|
||||
tc.periodicRate,
|
||||
tc.parentCloseTime,
|
||||
tc.paymentInterval,
|
||||
tc.prevPaymentDate,
|
||||
tc.startDate,
|
||||
tc.closeInterestRate);
|
||||
BEAST_EXPECTS(
|
||||
computedFullPaymentInterest == tc.expectedFullPaymentInterest,
|
||||
"Full payment interest mismatch: expected " +
|
||||
to_string(tc.expectedFullPaymentInterest) + ", got " +
|
||||
to_string(computedFullPaymentInterest));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testComputeFullPaymentInterest();
|
||||
testLoanAccruedInterest();
|
||||
testLoanLatePaymentInterest();
|
||||
testLoanPeriodicPayment();
|
||||
testLoanPrincipalFromPeriodicPayment();
|
||||
testComputeRaisedRate();
|
||||
testComputePaymentFactor();
|
||||
testComputeOverpaymentComponents();
|
||||
testComputeInterestAndFeeParts();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LendingHelpers, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -141,7 +141,7 @@ protected:
|
||||
using namespace jtx;
|
||||
|
||||
auto const vaultSle = env.le(keylet::vault(vaultID));
|
||||
return getAssetsTotalScale(vaultSle);
|
||||
return getVaultScale(vaultSle);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -551,15 +551,12 @@ protected:
|
||||
broker.vaultScale(env),
|
||||
state.principalOutstanding.exponent())));
|
||||
BEAST_EXPECT(state.paymentInterval == 600);
|
||||
{
|
||||
NumberRoundModeGuard mg(Number::upward);
|
||||
BEAST_EXPECT(
|
||||
state.totalValue ==
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
state.periodicPayment * state.paymentRemaining,
|
||||
state.loanScale));
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
state.totalValue ==
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
state.periodicPayment * state.paymentRemaining,
|
||||
state.loanScale));
|
||||
BEAST_EXPECT(
|
||||
state.managementFeeOutstanding ==
|
||||
computeManagementFee(
|
||||
@@ -700,8 +697,7 @@ protected:
|
||||
interval,
|
||||
total,
|
||||
feeRate,
|
||||
asset(brokerParams.vaultDeposit).number().exponent(),
|
||||
env.journal);
|
||||
asset(brokerParams.vaultDeposit).number().exponent());
|
||||
log << "Loan properties:\n"
|
||||
<< "\tPrincipal: " << principal << std::endl
|
||||
<< "\tInterest rate: " << interest << std::endl
|
||||
@@ -1275,8 +1271,7 @@ protected:
|
||||
verifyLoanStatus,
|
||||
issuer,
|
||||
lender,
|
||||
borrower,
|
||||
PaymentParameters{.showStepBalances = true});
|
||||
borrower);
|
||||
}
|
||||
|
||||
/** Runs through the complete lifecycle of a loan
|
||||
@@ -1482,8 +1477,7 @@ protected:
|
||||
state.paymentInterval,
|
||||
state.paymentRemaining,
|
||||
broker.params.managementFeeRate,
|
||||
state.loanScale,
|
||||
env.journal);
|
||||
state.loanScale);
|
||||
|
||||
verifyLoanStatus(
|
||||
0,
|
||||
@@ -2454,18 +2448,13 @@ protected:
|
||||
// Make all the payments in one transaction
|
||||
// service fee is 2
|
||||
auto const startingPayments = state.paymentRemaining;
|
||||
STAmount const payoffAmount = [&]() {
|
||||
NumberRoundModeGuard mg(Number::upward);
|
||||
auto const rawPayoff = startingPayments *
|
||||
(state.periodicPayment + broker.asset(2).value());
|
||||
STAmount const payoffAmount{broker.asset, rawPayoff};
|
||||
BEAST_EXPECTS(
|
||||
payoffAmount ==
|
||||
broker.asset(Number(1024014840139457, -12)),
|
||||
to_string(payoffAmount));
|
||||
BEAST_EXPECT(payoffAmount > state.principalOutstanding);
|
||||
return payoffAmount;
|
||||
}();
|
||||
auto const rawPayoff = startingPayments *
|
||||
(state.periodicPayment + broker.asset(2).value());
|
||||
STAmount const payoffAmount{broker.asset, rawPayoff};
|
||||
BEAST_EXPECT(
|
||||
payoffAmount ==
|
||||
broker.asset(Number(1024014840139457, -12)));
|
||||
BEAST_EXPECT(payoffAmount > state.principalOutstanding);
|
||||
|
||||
singlePayment(
|
||||
loanKeylet,
|
||||
@@ -2738,12 +2727,8 @@ protected:
|
||||
broker.params.managementFeeRate);
|
||||
|
||||
BEAST_EXPECT(
|
||||
paymentComponents.trackedValueDelta ==
|
||||
roundedPeriodicPayment ||
|
||||
(paymentComponents.specialCase ==
|
||||
detail::PaymentSpecialCase::final &&
|
||||
paymentComponents.trackedValueDelta <
|
||||
roundedPeriodicPayment));
|
||||
paymentComponents.trackedValueDelta <=
|
||||
roundedPeriodicPayment);
|
||||
|
||||
ripple::LoanState const nextTrueState = computeRawLoanState(
|
||||
state.periodicPayment,
|
||||
@@ -4024,7 +4009,7 @@ protected:
|
||||
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
|
||||
// Fails in preclaim because principal requested can't be
|
||||
// represented as XRP
|
||||
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
|
||||
env(createJson, ter(tecPRECISION_LOSS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet));
|
||||
@@ -4036,7 +4021,7 @@ protected:
|
||||
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
|
||||
// Fails in doApply because the payment is too small to be
|
||||
// represented as XRP.
|
||||
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
|
||||
env(createJson, ter(tecPRECISION_LOSS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
@@ -5011,7 +4996,7 @@ protected:
|
||||
auto const keylet = keylet::loan(broker.brokerID, loanSequence);
|
||||
|
||||
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
|
||||
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
|
||||
env(createJson, ter(tecPRECISION_LOSS));
|
||||
env.close(startDate);
|
||||
|
||||
auto loanPayTx = env.json(
|
||||
@@ -6159,16 +6144,15 @@ protected:
|
||||
// Accrued + prepayment-penalty interest based on current periodic
|
||||
// schedule
|
||||
auto const fullPaymentInterest = computeFullPaymentInterest(
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
after.periodicPayment, periodicRate2, after.paymentRemaining),
|
||||
after.periodicPayment,
|
||||
periodicRate2,
|
||||
after.paymentRemaining,
|
||||
env.current()->parentCloseTime(),
|
||||
after.paymentInterval,
|
||||
after.previousPaymentDate,
|
||||
static_cast<std::uint32_t>(
|
||||
after.startDate.time_since_epoch().count()),
|
||||
closeInterestRate);
|
||||
|
||||
// Round to asset scale and split interest/fee parts
|
||||
auto const roundedInterest =
|
||||
roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
|
||||
@@ -6196,9 +6180,9 @@ protected:
|
||||
// window by clamping prevPaymentDate to 'now' for the full-pay path.
|
||||
auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
|
||||
auto const fullPaymentInterestClamped = computeFullPaymentInterest(
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
after.periodicPayment, periodicRate2, after.paymentRemaining),
|
||||
after.periodicPayment,
|
||||
periodicRate2,
|
||||
after.paymentRemaining,
|
||||
env.current()->parentCloseTime(),
|
||||
after.paymentInterval,
|
||||
prevClamped,
|
||||
@@ -7209,15 +7193,15 @@ class LoanArbitrary_test : public LoanBatch_test
|
||||
.vaultDeposit = 10000,
|
||||
.debtMax = 0,
|
||||
.coverRateMin = TenthBips32{0},
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
// .managementFeeRate = TenthBips16{5919},
|
||||
.coverRateLiquidation = TenthBips32{0}};
|
||||
LoanParameters const loanParams{
|
||||
.account = Account("lender"),
|
||||
.counter = Account("borrower"),
|
||||
.principalRequest = Number{200000, -6},
|
||||
.interest = TenthBips32{50000},
|
||||
.payTotal = 2,
|
||||
.payInterval = 200};
|
||||
.principalRequest = Number{10000, 0},
|
||||
// .interest = TenthBips32{0},
|
||||
// .payTotal = 5816,
|
||||
.payInterval = 150};
|
||||
|
||||
runLoan(AssetType::XRP, brokerParams, loanParams);
|
||||
}
|
||||
|
||||
@@ -644,7 +644,7 @@ MPTTester::operator[](std::string const& name) const
|
||||
}
|
||||
|
||||
PrettyAmount
|
||||
MPTTester::operator()(std::int64_t amount) const
|
||||
MPTTester::operator()(std::uint64_t amount) const
|
||||
{
|
||||
return MPT("", issuanceID())(amount);
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ public:
|
||||
operator[](std::string const& name) const;
|
||||
|
||||
PrettyAmount
|
||||
operator()(std::int64_t amount) const;
|
||||
operator()(std::uint64_t amount) const;
|
||||
|
||||
operator Asset() const;
|
||||
|
||||
|
||||
@@ -187,9 +187,9 @@ class Feature_test : public beast::unit_test::suite
|
||||
BEAST_EXPECTS(jrr[jss::status] == jss::success, "status");
|
||||
jrr.removeMember(jss::status);
|
||||
BEAST_EXPECT(jrr.size() == 1);
|
||||
BEAST_EXPECT(jrr.isMember(
|
||||
"740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D6"
|
||||
"28A06927F11"));
|
||||
BEAST_EXPECT(
|
||||
jrr.isMember("12523DF04B553A0B1AD74F42DDB741DE8DC06A03FC089A0EF197E"
|
||||
"2A87F1D8107"));
|
||||
auto feature = *(jrr.begin());
|
||||
|
||||
BEAST_EXPECTS(feature[jss::name] == "fixAMMOverflowOffer", "name");
|
||||
|
||||
@@ -179,12 +179,11 @@ adjustImpreciseNumber(
|
||||
}
|
||||
|
||||
inline int
|
||||
getAssetsTotalScale(SLE::const_ref vaultSle)
|
||||
getVaultScale(SLE::const_ref vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::minExponent - 1; // LCOV_EXCL_LINE
|
||||
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}
|
||||
.exponent();
|
||||
return vaultSle->at(sfAssetsTotal).exponent();
|
||||
}
|
||||
|
||||
TER
|
||||
@@ -203,6 +202,14 @@ computeRawLoanState(
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate);
|
||||
|
||||
LoanState
|
||||
computeRawLoanState(
|
||||
Number const& periodicPayment,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate);
|
||||
|
||||
// Constructs a valid LoanState object from arbitrary inputs
|
||||
LoanState
|
||||
constructLoanState(
|
||||
@@ -232,6 +239,17 @@ computeFullPaymentInterest(
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate);
|
||||
|
||||
Number
|
||||
computeFullPaymentInterest(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentRemaining,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate);
|
||||
|
||||
namespace detail {
|
||||
// These classes and functions should only be accessed by LendingHelper
|
||||
// functions and unit tests
|
||||
@@ -369,58 +387,6 @@ struct LoanStateDeltas
|
||||
nonNegative();
|
||||
};
|
||||
|
||||
Number
|
||||
computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
computePaymentFactor(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
std::pair<Number, Number>
|
||||
computeInterestAndFeeParts(
|
||||
Asset const& asset,
|
||||
Number const& interest,
|
||||
TenthBips16 managementFeeRate,
|
||||
std::int32_t loanScale);
|
||||
|
||||
Number
|
||||
loanPeriodicPayment(
|
||||
Number const& principalOutstanding,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
loanPrincipalFromPeriodicPayment(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
loanLatePaymentInterest(
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 lateInterestRate,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t nextPaymentDueDate);
|
||||
|
||||
Number
|
||||
loanAccruedInterest(
|
||||
Number const& principalOutstanding,
|
||||
Number const& periodicRate,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t startDate,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t paymentInterval);
|
||||
|
||||
ExtendedPaymentComponents
|
||||
computeOverpaymentComponents(
|
||||
Asset const& asset,
|
||||
int32_t const loanScale,
|
||||
Number const& overpayment,
|
||||
TenthBips32 const overpaymentInterestRate,
|
||||
TenthBips32 const overpaymentFeeRate,
|
||||
TenthBips16 const managementFeeRate);
|
||||
|
||||
PaymentComponents
|
||||
computePaymentComponents(
|
||||
Asset const& asset,
|
||||
@@ -452,8 +418,7 @@ computeLoanProperties(
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining,
|
||||
TenthBips32 managementFeeRate,
|
||||
std::int32_t minimumScale,
|
||||
beast::Journal j);
|
||||
std::int32_t minimumScale);
|
||||
|
||||
bool
|
||||
isRounded(Asset const& asset, Number const& value, std::int32_t scale);
|
||||
|
||||
@@ -100,9 +100,6 @@ computePaymentFactor(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (paymentsRemaining == 0)
|
||||
return numZero;
|
||||
|
||||
// For zero interest, payment factor is simply 1/paymentsRemaining
|
||||
if (periodicRate == beast::zero)
|
||||
return Number{1} / paymentsRemaining;
|
||||
@@ -135,6 +132,27 @@ loanPeriodicPayment(
|
||||
computePaymentFactor(periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Calculates the periodic payment amount from annualized interest rate.
|
||||
* Converts the annual rate to periodic rate before computing payment.
|
||||
*
|
||||
* Equation (7) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
*/
|
||||
Number
|
||||
loanPeriodicPayment(
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (principalOutstanding == 0 || paymentsRemaining == 0)
|
||||
return 0;
|
||||
|
||||
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
||||
|
||||
return loanPeriodicPayment(
|
||||
principalOutstanding, periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Reverse-calculates principal from periodic payment amount.
|
||||
* Used to determine theoretical principal at any point in the schedule.
|
||||
*
|
||||
@@ -146,9 +164,6 @@ loanPrincipalFromPeriodicPayment(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (paymentsRemaining == 0)
|
||||
return numZero;
|
||||
|
||||
if (periodicRate == 0)
|
||||
return periodicPayment * paymentsRemaining;
|
||||
|
||||
@@ -156,6 +171,21 @@ loanPrincipalFromPeriodicPayment(
|
||||
computePaymentFactor(periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Splits gross interest into net interest (to vault) and management fee (to
|
||||
* broker). Returns pair of (net interest, management fee).
|
||||
*
|
||||
* Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
*/
|
||||
std::pair<Number, Number>
|
||||
computeInterestAndFeeParts(
|
||||
Number const& interest,
|
||||
TenthBips16 managementFeeRate)
|
||||
{
|
||||
auto const fee = tenthBipsOfValue(interest, managementFeeRate);
|
||||
|
||||
return std::make_pair(interest - fee, fee);
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes the interest and management fee parts from interest amount.
|
||||
*
|
||||
@@ -186,12 +216,6 @@ loanLatePaymentInterest(
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t nextPaymentDueDate)
|
||||
{
|
||||
if (principalOutstanding == beast::zero)
|
||||
return numZero;
|
||||
|
||||
if (lateInterestRate == TenthBips32{0})
|
||||
return numZero;
|
||||
|
||||
auto const now = parentCloseTime.time_since_epoch().count();
|
||||
|
||||
// If the payment is not late by any amount of time, then there's no late
|
||||
@@ -224,9 +248,6 @@ loanAccruedInterest(
|
||||
if (periodicRate == beast::zero)
|
||||
return numZero;
|
||||
|
||||
if (paymentInterval == 0)
|
||||
return numZero;
|
||||
|
||||
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
|
||||
auto const now = parentCloseTime.time_since_epoch().count();
|
||||
|
||||
@@ -430,8 +451,7 @@ tryOverpayment(
|
||||
paymentInterval,
|
||||
paymentRemaining,
|
||||
managementFeeRate,
|
||||
loanScale,
|
||||
j);
|
||||
loanScale);
|
||||
|
||||
JLOG(j.debug()) << "new periodic payment: "
|
||||
<< newLoanProperties.periodicPayment
|
||||
@@ -527,28 +547,21 @@ tryOverpayment(
|
||||
|
||||
auto const deltas = rounded - newRounded;
|
||||
|
||||
// The change in loan management fee is equal to the change between the old
|
||||
// and the new outstanding management fees
|
||||
XRPL_ASSERT_PARTS(
|
||||
deltas.managementFee ==
|
||||
rounded.managementFeeDue - managementFeeOutstanding,
|
||||
"ripple::detail::tryOverpayment",
|
||||
"no fee change");
|
||||
|
||||
auto const hypotheticalValueOutstanding =
|
||||
rounded.valueOutstanding - deltas.principal;
|
||||
|
||||
// Calculate how the loan's value changed due to the overpayment.
|
||||
// This should be negative (value decreased) or zero. A principal
|
||||
// overpayment should never increase the loan's value.
|
||||
auto const valueChange = newRounded.valueOutstanding -
|
||||
hypotheticalValueOutstanding - deltas.managementFee;
|
||||
auto const valueChange =
|
||||
newRounded.valueOutstanding - hypotheticalValueOutstanding;
|
||||
if (valueChange > 0)
|
||||
{
|
||||
JLOG(j.warn()) << "Principal overpayment would increase the value of "
|
||||
"the loan. Ignore the overpayment";
|
||||
return Unexpected(tesSUCCESS);
|
||||
}
|
||||
|
||||
return LoanPaymentParts{
|
||||
// Principal paid is the reduction in principal outstanding
|
||||
.principalPaid = deltas.principal,
|
||||
@@ -663,6 +676,12 @@ doOverpayment(
|
||||
"ripple::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedManagementFeeDelta ==
|
||||
managementFeeOutstandingProxy - managementFeeOutstanding,
|
||||
"ripple::detail::doOverpayment",
|
||||
"no fee change");
|
||||
|
||||
// I'm not 100% sure the following asserts are correct. If in doubt, and
|
||||
// everything else works, remove any that cause trouble.
|
||||
|
||||
@@ -693,6 +712,13 @@ doOverpayment(
|
||||
"ripple::detail::doOverpayment",
|
||||
"principal payment matches");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.feePaid ==
|
||||
overpaymentComponents.untrackedManagementFee +
|
||||
overpaymentComponents.trackedManagementFeeDelta,
|
||||
"ripple::detail::doOverpayment",
|
||||
"fee payment matches");
|
||||
|
||||
// All validations passed, so update the proxy objects (which will
|
||||
// modify the actual Loan ledger object)
|
||||
totalValueOutstandingProxy = totalValueOutstanding;
|
||||
@@ -1075,23 +1101,6 @@ computePaymentComponents(
|
||||
"ripple::detail::computePaymentComponents",
|
||||
"excess non-negative");
|
||||
};
|
||||
auto giveTo =
|
||||
[](Number& component, Number& shortage, Number const& maximum) {
|
||||
if (shortage > beast::zero)
|
||||
{
|
||||
// Put as much of the shortage as we can into the provided part
|
||||
// and the total
|
||||
auto part = std::min(maximum - component, shortage);
|
||||
component += part;
|
||||
shortage -= part;
|
||||
}
|
||||
// If the shortage goes negative, we put too much, which should be
|
||||
// impossible
|
||||
XRPL_ASSERT_PARTS(
|
||||
shortage >= beast::zero,
|
||||
"ripple::detail::computePaymentComponents",
|
||||
"excess non-negative");
|
||||
};
|
||||
// Helper to reduce deltas when they collectively exceed a limit.
|
||||
// Order matters: we prefer to reduce interest first (most flexible),
|
||||
// then management fee, then principal (least flexible).
|
||||
@@ -1101,19 +1110,6 @@ computePaymentComponents(
|
||||
takeFrom(deltas.managementFee, excess);
|
||||
takeFrom(deltas.principal, excess);
|
||||
};
|
||||
// Helper to increase deltas when they collectively do not reach an
|
||||
// expected value.
|
||||
// Order matters: we prefer to increase interest first (most flexible),
|
||||
// then management fee, then principal (least flexible).
|
||||
auto addressShortage = [&giveTo](
|
||||
LoanDeltas& deltas,
|
||||
Number& shortage,
|
||||
LoanState const& current) {
|
||||
giveTo(deltas.interestDueDelta, shortage, current.interestDue);
|
||||
giveTo(
|
||||
deltas.managementFeeDueDelta, shortage, current.managementFeeDue);
|
||||
giveTo(deltas.principalDelta, shortage, current.principalOutstanding);
|
||||
};
|
||||
|
||||
// Check if deltas exceed the total outstanding value. This should never
|
||||
// happen due to earlier caps, but handle it defensively.
|
||||
@@ -1145,19 +1141,12 @@ computePaymentComponents(
|
||||
addressExcess(deltas, excess);
|
||||
shortage = -excess;
|
||||
}
|
||||
else if (shortage > beast::zero && totalOverpayment < beast::zero)
|
||||
{
|
||||
// If there's a shortage, and there's room in the loan itself, we can
|
||||
// top up the parts to make the payment correct.
|
||||
shortage = std::min(-totalOverpayment, shortage);
|
||||
addressShortage(deltas, shortage, currentLedgerState);
|
||||
}
|
||||
|
||||
// At this point, shortage >= 0 means we're paying less than the full
|
||||
// periodic payment (due to rounding or component caps).
|
||||
// shortage < 0 means mean we're trying to pay more than allowed (bug).
|
||||
// shortage < 0 would mean we're trying to pay more than allowed (bug).
|
||||
XRPL_ASSERT_PARTS(
|
||||
shortage == beast::zero,
|
||||
shortage >= beast::zero,
|
||||
"ripple::detail::computePaymentComponents",
|
||||
"no shortage or excess");
|
||||
|
||||
@@ -1242,12 +1231,17 @@ computeOverpaymentComponents(
|
||||
// This interest doesn't follow the normal amortization schedule - it's
|
||||
// a one-time charge for paying early.
|
||||
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
auto const [rawOverpaymentInterest, _] = [&]() {
|
||||
Number const interest =
|
||||
tenthBipsOfValue(overpayment, overpaymentInterestRate);
|
||||
return detail::computeInterestAndFeeParts(interest, managementFeeRate);
|
||||
}();
|
||||
|
||||
// Round the penalty interest components to the loan scale
|
||||
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
|
||||
[&]() {
|
||||
auto const interest = roundToAsset(
|
||||
asset,
|
||||
tenthBipsOfValue(overpayment, overpaymentInterestRate),
|
||||
loanScale);
|
||||
Number const interest =
|
||||
roundToAsset(asset, rawOverpaymentInterest, loanScale);
|
||||
return detail::computeInterestAndFeeParts(
|
||||
asset, interest, managementFeeRate, loanScale);
|
||||
}();
|
||||
@@ -1441,6 +1435,31 @@ computeFullPaymentInterest(
|
||||
return accruedInterest + prepaymentPenalty;
|
||||
}
|
||||
|
||||
Number
|
||||
computeFullPaymentInterest(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentRemaining,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate)
|
||||
{
|
||||
Number const rawPrincipalOutstanding =
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
periodicPayment, periodicRate, paymentRemaining);
|
||||
|
||||
return computeFullPaymentInterest(
|
||||
rawPrincipalOutstanding,
|
||||
periodicRate,
|
||||
parentCloseTime,
|
||||
paymentInterval,
|
||||
prevPaymentDate,
|
||||
startDate,
|
||||
closeInterestRate);
|
||||
}
|
||||
|
||||
/* Calculates the theoretical loan state at maximum precision for a given point
|
||||
* in the amortization schedule.
|
||||
*
|
||||
@@ -1502,6 +1521,21 @@ computeRawLoanState(
|
||||
.managementFeeDue = rawManagementFeeOutstanding};
|
||||
};
|
||||
|
||||
LoanState
|
||||
computeRawLoanState(
|
||||
Number const& periodicPayment,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate)
|
||||
{
|
||||
return computeRawLoanState(
|
||||
periodicPayment,
|
||||
loanPeriodicRate(interestRate, paymentInterval),
|
||||
paymentRemaining,
|
||||
managementFeeRate);
|
||||
}
|
||||
|
||||
/* Constructs a LoanState from rounded Loan ledger object values.
|
||||
*
|
||||
* This function creates a LoanState structure from the three tracked values
|
||||
@@ -1577,8 +1611,7 @@ computeLoanProperties(
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining,
|
||||
TenthBips32 managementFeeRate,
|
||||
std::int32_t minimumScale,
|
||||
beast::Journal j)
|
||||
std::int32_t minimumScale)
|
||||
{
|
||||
auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
||||
XRPL_ASSERT(
|
||||
@@ -1589,22 +1622,13 @@ computeLoanProperties(
|
||||
principalOutstanding, periodicRate, paymentsRemaining);
|
||||
|
||||
auto const [totalValueOutstanding, loanScale] = [&]() {
|
||||
// only round up if there should be interest
|
||||
NumberRoundModeGuard mg(
|
||||
periodicRate == 0 ? Number::to_nearest : Number::upward);
|
||||
NumberRoundModeGuard mg(Number::to_nearest);
|
||||
// Use STAmount's internal rounding instead of roundToAsset, because
|
||||
// we're going to use this result to determine the scale for all the
|
||||
// other rounding.
|
||||
|
||||
// Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
STAmount amount{asset, periodicPayment * paymentsRemaining};
|
||||
JLOG(j.debug()) << "computeLoanProperties:" << " Principal requested: "
|
||||
<< principalOutstanding
|
||||
<< ". Periodic payment: " << periodicPayment
|
||||
<< ". Payments remaining: " << paymentsRemaining
|
||||
<< ". Raw total value: "
|
||||
<< periodicPayment * paymentsRemaining
|
||||
<< ". Candidate total value: " << amount << std::endl;
|
||||
|
||||
// Base the loan scale on the total value, since that's going to be
|
||||
// the biggest number involved (barring unusual parameters for late,
|
||||
@@ -1619,10 +1643,7 @@ computeLoanProperties(
|
||||
|
||||
// We may need to truncate the total value because of the minimum
|
||||
// scale
|
||||
amount = roundToAsset(asset, amount, loanScale);
|
||||
|
||||
JLOG(j.debug()) << "computeLoanProperties: Loan scale:" << loanScale
|
||||
<< ". Actual total value: " << amount << std::endl;
|
||||
amount = roundToAsset(asset, amount, loanScale, Number::to_nearest);
|
||||
|
||||
return std::make_pair(amount, loanScale);
|
||||
}();
|
||||
|
||||
@@ -56,7 +56,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
if (!vault)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const asset = vault->at(sfAsset);
|
||||
auto const scale = getAssetsTotalScale(vault);
|
||||
auto const scale = getVaultScale(vault);
|
||||
|
||||
auto const rounded =
|
||||
roundToAsset(asset, debtTotal, scale, Number::towards_zero);
|
||||
@@ -67,7 +67,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is "
|
||||
<< debtTotal << ", which rounds to " << rounded;
|
||||
return tecHAS_OBLIGATIONS;
|
||||
// LCOV_EXCL_STOP
|
||||
// LCOV_EXCL_START
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ LoanDelete::doApply()
|
||||
roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
debtTotalProxy,
|
||||
getAssetsTotalScale(vaultSle),
|
||||
getVaultScale(vaultSle),
|
||||
Number::towards_zero) == beast::zero,
|
||||
"ripple::LoanDelete::doApply",
|
||||
"last loan, remaining debt rounds to zero");
|
||||
|
||||
@@ -106,7 +106,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
if (loanBrokerSle->at(sfOwner) != account)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "LoanBroker for Loan does not belong to the account. LoanManage "
|
||||
<< "LoanBroker for Loan does not belong to the account. LoanModify "
|
||||
"can only be submitted by the Loan Broker.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
@@ -178,7 +178,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:
|
||||
@@ -242,11 +242,7 @@ LoanManage::defaultLoan(
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
adjustImpreciseNumber(
|
||||
vaultLossUnrealizedProxy,
|
||||
-totalDefaultAmount,
|
||||
vaultAsset,
|
||||
vaultScale);
|
||||
vaultLossUnrealizedProxy -= totalDefaultAmount;
|
||||
}
|
||||
view.update(vaultSle);
|
||||
}
|
||||
@@ -254,9 +250,11 @@ LoanManage::defaultLoan(
|
||||
// Update the LoanBroker object:
|
||||
|
||||
{
|
||||
auto const asset = *vaultSle->at(sfAsset);
|
||||
|
||||
// Decrease the Debt of the LoanBroker:
|
||||
adjustImpreciseNumber(
|
||||
brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale);
|
||||
brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale);
|
||||
// Decrease the First-Loss Capital Cover Available:
|
||||
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
|
||||
if (coverAvailableProxy < defaultCovered)
|
||||
@@ -299,20 +297,13 @@ LoanManage::impairLoan(
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
Number const lossUnrealized = owedToVault(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);
|
||||
vaultLossUnrealizedProxy += lossUnrealized;
|
||||
if (vaultLossUnrealizedProxy >
|
||||
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
{
|
||||
@@ -343,14 +334,8 @@ LoanManage::unimpairLoan(
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
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);
|
||||
Number const lossReversed = owedToVault(loanSle);
|
||||
@@ -362,10 +347,7 @@ LoanManage::unimpairLoan(
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// Reverse the "paper loss"
|
||||
adjustImpreciseNumber(
|
||||
vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
|
||||
vaultLossUnrealizedProxy -= lossReversed;
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the Loan object
|
||||
@@ -421,14 +403,12 @@ LoanManage::doApply()
|
||||
}
|
||||
else if (tx.isFlag(tfLoanImpair))
|
||||
{
|
||||
if (auto const ter =
|
||||
impairLoan(view, loanSle, vaultSle, vaultAsset, j_))
|
||||
if (auto const ter = impairLoan(view, loanSle, vaultSle, j_))
|
||||
return ter;
|
||||
}
|
||||
else if (tx.isFlag(tfLoanUnimpair))
|
||||
{
|
||||
if (auto const ter =
|
||||
unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_))
|
||||
if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_))
|
||||
return ter;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ public:
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
/** Helper function that might be needed by other transactors
|
||||
@@ -54,7 +53,6 @@ public:
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
TER
|
||||
|
||||
@@ -305,7 +305,7 @@ LoanPay::doApply()
|
||||
// change will be discarded.
|
||||
if (loanSle->isFlag(lsfLoanImpaired))
|
||||
{
|
||||
LoanManage::unimpairLoan(view, loanSle, vaultSle, asset, j_);
|
||||
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
|
||||
}
|
||||
|
||||
LoanPaymentType const paymentType = [&tx]() {
|
||||
@@ -379,7 +379,7 @@ LoanPay::doApply()
|
||||
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the payment by rounding some of the values to that scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const vaultScale = assetsTotalProxy.value().exponent();
|
||||
|
||||
auto const totalPaidToVaultRaw =
|
||||
paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
|
||||
@@ -383,7 +383,7 @@ LoanSet::doApply()
|
||||
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
if (vaultAvailableProxy < principalRequested)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
@@ -404,8 +404,7 @@ LoanSet::doApply()
|
||||
paymentInterval,
|
||||
paymentTotal,
|
||||
TenthBips16{brokerSle->at(sfManagementFeeRate)},
|
||||
vaultScale,
|
||||
j_);
|
||||
vaultScale);
|
||||
|
||||
// Check that relevant values won't lose precision. This is mostly only
|
||||
// relevant for IOU assets.
|
||||
@@ -441,10 +440,7 @@ LoanSet::doApply()
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn())
|
||||
<< "Computed loan properties are invalid. Does not compute."
|
||||
<< " Management fee: " << properties.managementFeeOwedToBroker
|
||||
<< ". Total Value: " << properties.totalValueOutstanding
|
||||
<< ". PeriodicPayment: " << properties.periodicPayment;
|
||||
<< "Computed loan properties are invalid. Does not compute.";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user