diff --git a/.config/cspell.config.yaml b/.config/cspell.config.yaml index 02b5c9a033..36e46bf064 100644 --- a/.config/cspell.config.yaml +++ b/.config/cspell.config.yaml @@ -28,6 +28,7 @@ ignoreRegExpList: - /[\['"`]-[DWw][a-zA-Z0-9_-]+['"`\]]/g # compile flags suggestWords: - xprl->xrpl + - xprld->xrpld - unsynched->unsynced - synched->synced - synch->sync @@ -61,6 +62,7 @@ words: - compr - conanfile - conanrun + - confs - connectability - coro - coros @@ -90,6 +92,7 @@ words: - finalizers - firewalled - fmtdur + - fsanitize - funclets - gcov - gcovr @@ -126,6 +129,7 @@ words: - lseq - lsmf - ltype + - mcmodel - MEMORYSTATUSEX - Merkle - Metafuncton @@ -235,6 +239,8 @@ words: - txn - txns - txs + - UBSAN + - ubsan - umant - unacquired - unambiguity @@ -271,6 +277,7 @@ words: - xbridge - xchain - ximinez + - EXPECT_STREQ - XMACRO - xrpkuwait - xrpl diff --git a/.github/actions/build-deps/action.yml b/.github/actions/build-deps/action.yml index d1fb980dac..9d52be1998 100644 --- a/.github/actions/build-deps/action.yml +++ b/.github/actions/build-deps/action.yml @@ -18,6 +18,10 @@ inputs: description: "The logging verbosity." required: false default: "verbose" + sanitizers: + description: "The sanitizers to enable." + required: false + default: "" runs: using: composite @@ -29,9 +33,11 @@ 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.' conan install \ + --profile ci \ --build="${BUILD_OPTION}" \ --options:host='&:tests=True' \ --options:host='&:xrpld=True' \ diff --git a/.github/actions/setup-conan/action.yml b/.github/actions/setup-conan/action.yml index 1184cfd3d9..dedf53f109 100644 --- a/.github/actions/setup-conan/action.yml +++ b/.github/actions/setup-conan/action.yml @@ -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 diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index c9c65fb0dd..8d17e1167f 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -104,6 +104,7 @@ test.overlay > xrpl.basics test.overlay > xrpld.app test.overlay > xrpld.overlay test.overlay > xrpld.peerfinder +test.overlay > xrpl.nodestore test.overlay > xrpl.protocol test.overlay > xrpl.shamap test.peerfinder > test.beast diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index c3d2da1f9f..0e44b1be54 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -20,8 +20,8 @@ class Config: Generate a strategy matrix for GitHub Actions CI. On each PR commit we will build a selection of Debian, RHEL, Ubuntu, MacOS, and -Windows configurations, while upon merge into the develop, release, or master -branches, we will build all configurations, and test most of them. +Windows configurations, while upon merge into the develop or release branches, +we will build all configurations, and test most of them. We will further set additional CMake arguments as follows: - All builds will have the `tests`, `werr`, and `xrpld` options. @@ -229,7 +229,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 "-Dcoverage=ON" in cmake_args: @@ -240,17 +240,53 @@ 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. + # GCC-Asan rippled-embedded tests are failing because of https://github.com/google/sanitizers/issues/856 + if ( + os["distro_version"] == "bookworm" + and f"{os['compiler_name']}-{os['compiler_version']}" == "clang-20" + ): + # Add ASAN + UBSAN configuration. + configurations.append( + { + "config_name": config_name + "-asan-ubsan", + "cmake_args": cmake_args, + "cmake_target": cmake_target, + "build_only": build_only, + "build_type": build_type, + "os": os, + "architecture": architecture, + "sanitizers": "address,undefinedbehavior", + } + ) + # TSAN is deactivated due to seg faults with latest compilers. + activate_tsan = False + if activate_tsan: + configurations.append( + { + "config_name": config_name + "-tsan-ubsan", + "cmake_args": cmake_args, + "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, + "sanitizers": "", + } + ) return configurations diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 669754554c..e64a05f925 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -15,196 +15,196 @@ "distro_version": "bookworm", "compiler_name": "gcc", "compiler_version": "12", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", "compiler_version": "13", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", "compiler_version": "15", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", "compiler_version": "16", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", "compiler_version": "17", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", "compiler_version": "18", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", "compiler_version": "19", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", "compiler_version": "20", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "trixie", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "trixie", "compiler_name": "gcc", "compiler_version": "15", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "trixie", "compiler_name": "clang", "compiler_version": "20", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "debian", "distro_version": "trixie", "compiler_name": "clang", "compiler_version": "21", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "8", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "8", "compiler_name": "clang", "compiler_version": "any", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "9", "compiler_name": "gcc", "compiler_version": "12", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "9", "compiler_name": "gcc", "compiler_version": "13", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "9", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "9", "compiler_name": "clang", "compiler_version": "any", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "10", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "rhel", "distro_version": "10", "compiler_name": "clang", "compiler_version": "any", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "ubuntu", "distro_version": "jammy", "compiler_name": "gcc", "compiler_version": "12", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "gcc", "compiler_version": "13", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "gcc", "compiler_version": "14", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", "compiler_version": "16", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", "compiler_version": "17", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", "compiler_version": "18", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", "compiler_version": "19", - "image_sha": "cc09fd3" + "image_sha": "ab4d1f0" } ], "build_type": ["Debug", "Release"], diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 3aa48ac070..dad211f94f 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -125,7 +125,7 @@ jobs: needs: - should-run - build-test - if: ${{ needs.should-run.outputs.go == 'true' && (startsWith(github.base_ref, 'release') || github.base_ref == 'master') }} + if: ${{ needs.should-run.outputs.go == 'true' && startsWith(github.ref, 'refs/heads/release') }} uses: ./.github/workflows/reusable-notify-clio.yml secrets: clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }} diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 2c63c2baa5..ef7bf41fa2 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -1,9 +1,8 @@ -# This workflow runs all workflows to build the dependencies required for the -# project on various Linux flavors, as well as on MacOS and Windows, on a -# scheduled basis, on merge into the 'develop', 'release', or 'master' branches, -# or manually. The missing commits check is only run when the code is merged -# into the 'develop' or 'release' branches, and the documentation is built when -# the code is merged into the 'develop' branch. +# This workflow runs all workflows to build and test the code on various Linux +# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into +# the 'develop' or 'release*' branches, or when requested manually. Upon +# successful completion, it also uploads the built libxrpl package to the Conan +# remote. name: Trigger on: @@ -11,7 +10,6 @@ on: branches: - "develop" - "release*" - - "master" paths: # These paths are unique to `on-trigger.yml`. - ".github/workflows/on-trigger.yml" @@ -70,10 +68,10 @@ jobs: with: # Enable ccache only for events targeting the XRPLF repository, since # other accounts will not have access to our remote cache storage. - # However, we do not enable ccache for events targeting the master or a - # release branch, to protect against the rare case that the output - # produced by ccache is not identical to a regular compilation. - ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !(github.base_ref == 'master' || startsWith(github.base_ref, 'release')) }} + # However, we do not enable ccache for events targeting a release branch, + # to protect against the rare case that the output produced by ccache is + # not identical to a regular compilation. + ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }} os: ${{ matrix.os }} strategy_matrix: ${{ github.event_name == 'schedule' && 'all' || 'minimal' }} secrets: diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 41e82fb6bb..6b8fd9955e 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -3,13 +3,15 @@ name: Run pre-commit hooks on: pull_request: push: - branches: [develop, release, master] + branches: + - "develop" + - "release*" workflow_dispatch: jobs: # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: - uses: XRPLF/actions/.github/workflows/pre-commit.yml@5ca417783f0312ab26d6f48b85c78edf1de99bbd + uses: XRPLF/actions/.github/workflows/pre-commit.yml@282890f46d6921249d5659dd38babcb0bd8aef48 with: runs_on: ubuntu-latest - container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-a8c7be1" }' + container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-ab4d1f0" }' diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index ae91a8bf20..b8c82aa94d 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -51,6 +51,12 @@ on: type: number default: 2 + sanitizers: + description: "The sanitizers to enable." + required: false + type: string + default: "" + secrets: CODECOV_TOKEN: description: "The Codecov token to use for uploading coverage reports." @@ -91,6 +97,7 @@ jobs: # Determine if coverage and voidstar should be enabled. COVERAGE_ENABLED: ${{ contains(inputs.cmake_args, '-Dcoverage=ON') }} VOIDSTAR_ENABLED: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }} + SANITIZERS_ENABLED: ${{ inputs.sanitizers != '' }} steps: - name: Cleanup workspace (macOS and Windows) if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }} @@ -100,7 +107,7 @@ jobs: uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@121d1de2775d486d46140b9a91b32d5002c08153 + uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 with: enable_ccache: ${{ inputs.ccache_enabled }} @@ -128,11 +135,13 @@ 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: ${{ env.BUILD_DIR }} env: BUILD_TYPE: ${{ inputs.build_type }} + SANITIZERS: ${{ inputs.sanitizers }} CMAKE_ARGS: ${{ inputs.cmake_args }} run: | cmake \ @@ -174,7 +183,7 @@ jobs: if-no-files-found: error - name: Check linking (Linux) - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && env.SANITIZERS_ENABLED == 'false' }} working-directory: ${{ env.BUILD_DIR }} run: | ldd ./xrpld @@ -191,6 +200,14 @@ jobs: run: | ./xrpld --version | grep libvoidstar + - name: Set sanitizer options + if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }} + run: | + echo "ASAN_OPTIONS=print_stacktrace=1:detect_container_overflow=0:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp" >> ${GITHUB_ENV} + echo "TSAN_OPTIONS=second_deadlock_stack=1:halt_on_error=0:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >> ${GITHUB_ENV} + echo "UBSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >> ${GITHUB_ENV} + echo "LSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >> ${GITHUB_ENV} + - name: Run the separate tests if: ${{ !inputs.build_only }} working-directory: ${{ env.BUILD_DIR }} diff --git a/.github/workflows/reusable-build-test.yml b/.github/workflows/reusable-build-test.yml index 2b4d38da61..0086cbbfb5 100644 --- a/.github/workflows/reusable-build-test.yml +++ b/.github/workflows/reusable-build-test.yml @@ -57,5 +57,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 }} diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 55a9ab8864..29ae95fce5 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@65da1c59e81965eeb257caa3587b9d45066fb925 + uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 with: enable_ccache: false diff --git a/BUILD.md b/BUILD.md index 85b3e3ea74..b239b10be6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,5 +1,5 @@ -| :warning: **WARNING** :warning: -|---| +| :warning: **WARNING** :warning: | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md). | > These instructions also assume a basic familiarity with Conan and CMake. @@ -148,7 +148,8 @@ function extract_version { } # Define which recipes to export. -recipes=(ed25519 grpc secp256k1 snappy soci) +recipes=('ed25519' 'grpc' 'openssl' 'secp256k1' 'snappy' 'soci') +folders=('all' 'all' '3.x.x' 'all' 'all' 'all') # Selectively check out the recipes from our CCI fork. cd external @@ -157,20 +158,24 @@ 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 +for ((index = 1; index <= ${#recipes[@]}; index++)); do + recipe=${recipes[index]} + folder=${folders[index]} + echo "Checking out recipe '${recipe}' from folder '${folder}'..." + git sparse-checkout add recipes/${recipe}/${folder} done git fetch origin master git checkout master cd ../.. # Export the recipes into the local cache. -for recipe in ${recipes[@]}; do +for ((index = 1; index <= ${#recipes[@]}; index++)); do + recipe=${recipes[index]} + folder=${folders[index]} version=$(extract_version ${recipe}) - echo "Exporting ${recipe}/${version}..." + echo "Exporting '${recipe}/${version}' from '${recipe}/${folder}'..." conan export --version $(extract_version ${recipe}) \ - external/conan-center-index/recipes/${recipe}/all + external/conan-center-index/recipes/${recipe}/${folder} done ``` @@ -518,18 +523,32 @@ stored inside the build directory, as either of: - file named `coverage.`_extension_, with a suitable extension for the report format, or - directory named `coverage`, with the `index.html` and other files inside, for the `html-details` or `html-nested` report formats. +## Sanitizers + +To build dependencies and xrpld with sanitizer instrumentation, set the +`SANITIZERS` environment variable (only once before running conan and cmake) and use the `sanitizers` profile in conan: + +```bash +export SANITIZERS=address,undefinedbehavior + +conan install .. --output-folder . --profile:all sanitizers --build missing --settings build_type=Debug + +cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug -Dxrpld=ON -Dtests=ON .. +``` + +See [Sanitizers docs](./docs/build/sanitizers.md) for more details. + ## Options -| Option | Default Value | Description | -| ---------- | ------------- | ------------------------------------------------------------------ | -| `assert` | OFF | Enable assertions. | -| `coverage` | OFF | Prepare the coverage report. | -| `san` | N/A | Enable a sanitizer with Clang. Choices are `thread` and `address`. | -| `tests` | OFF | Build tests. | -| `unity` | OFF | Configure a unity build. | -| `xrpld` | OFF | Build the xrpld application, and not just the libxrpl library. | -| `werr` | OFF | Treat compilation warnings as errors | -| `wextra` | OFF | Enable additional compilation warnings | +| Option | Default Value | Description | +| ---------- | ------------- | -------------------------------------------------------------- | +| `assert` | OFF | Enable assertions. | +| `coverage` | OFF | Prepare the coverage report. | +| `tests` | OFF | Build tests. | +| `unity` | OFF | Configure a unity build. | +| `xrpld` | OFF | Build the xrpld application, and not just the libxrpl library. | +| `werr` | OFF | Treat compilation warnings as errors | +| `wextra` | OFF | Enable additional compilation warnings | [Unity builds][5] may be faster for the first build (at the cost of much more memory) since they concatenate sources into fewer diff --git a/CMakeLists.txt b/CMakeLists.txt index 91bb12b0bc..bd08c8e291 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,14 +16,16 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") +include(CompilationEnv) + +if(is_gcc) # GCC-specific fixes add_compile_options(-Wno-unknown-pragmas -Wno-subobject-linkage) # -Wno-subobject-linkage can be removed when we upgrade GCC version to at least 13.3 -elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") +elseif(is_clang) # Clang-specific fixes add_compile_options(-Wno-unknown-warning-option) # Ignore unknown warning options -elseif(MSVC) +elseif(is_msvc) # MSVC-specific fixes add_compile_options(/wd4068) # Ignore unknown pragmas endif() @@ -77,6 +79,7 @@ if (packages_only) return () endif () include(XrplCompiler) +include(XrplSanitizers) include(XrplInterface) option(only_docs "Include only the docs target?" FALSE) @@ -88,34 +91,18 @@ endif() ### include(deps/Boost) -find_package(OpenSSL 1.1.1 REQUIRED) -set_target_properties(OpenSSL::SSL PROPERTIES - INTERFACE_COMPILE_DEFINITIONS OPENSSL_NO_SSL2 -) add_subdirectory(external/antithesis-sdk) -find_package(gRPC REQUIRED) -find_package(lz4 REQUIRED) -# Target names with :: are not allowed in a generator expression. -# We need to pull the include directories and imported location properties -# from separate targets. -find_package(LibArchive REQUIRED) -find_package(SOCI REQUIRED) -find_package(SQLite3 REQUIRED) - -option(rocksdb "Enable RocksDB" ON) -if(rocksdb) - find_package(RocksDB REQUIRED) - set_target_properties(RocksDB::rocksdb PROPERTIES - INTERFACE_COMPILE_DEFINITIONS XRPL_ROCKSDB_AVAILABLE=1 - ) - target_link_libraries(xrpl_libs INTERFACE RocksDB::rocksdb) -endif() - find_package(date REQUIRED) find_package(ed25519 REQUIRED) +find_package(gRPC REQUIRED) +find_package(LibArchive REQUIRED) +find_package(lz4 REQUIRED) find_package(nudb REQUIRED) +find_package(OpenSSL REQUIRED) find_package(secp256k1 REQUIRED) +find_package(SOCI REQUIRED) +find_package(SQLite3 REQUIRED) find_package(wasmi REQUIRED) find_package(xxHash REQUIRED) @@ -129,6 +116,15 @@ target_link_libraries(xrpl_libs INTERFACE SQLite::SQLite3 ) +option(rocksdb "Enable RocksDB" ON) +if(rocksdb) + find_package(RocksDB REQUIRED) + set_target_properties(RocksDB::rocksdb PROPERTIES + INTERFACE_COMPILE_DEFINITIONS XRPL_ROCKSDB_AVAILABLE=1 + ) + target_link_libraries(xrpl_libs INTERFACE RocksDB::rocksdb) +endif() + # Work around changes to Conan recipe for now. if(TARGET nudb::core) set(nudb nudb::core) diff --git a/clang_format.sh b/clang_format.sh deleted file mode 100755 index 2d834b515d..0000000000 --- a/clang_format.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# cspell: ignore clangf -modified=$1 -dir=`pwd` -clangf=clang-format-10 -clangf=clang-format -if [ "$1" = "--all" ] -then - modified=`git status|egrep "modified|new file"|egrep "(cpp|h)$" | sed -E -e 's/modified://' -e 's/new file://' -e 's/^[[:space:]]+//'` -fi -for i in $modified -do - basedir=$(dirname "$i") - file=$(basename "$i") - echo "$basedir $file" - cd $basedir - $clangf -style=file -i "$file" - cd $dir -done diff --git a/cmake/CompilationEnv.cmake b/cmake/CompilationEnv.cmake new file mode 100644 index 0000000000..345e4cdd62 --- /dev/null +++ b/cmake/CompilationEnv.cmake @@ -0,0 +1,54 @@ + # Shared detection of compiler, operating system, and architecture. + # + # This module centralizes environment detection so that other + # CMake modules can use the same variables instead of repeating + # checks on CMAKE_* and built-in platform variables. + +# Only run once per configure step. +include_guard(GLOBAL) + +# -------------------------------------------------------------------- +# Compiler detection (C++) +# -------------------------------------------------------------------- +set(is_clang FALSE) +set(is_gcc FALSE) +set(is_msvc FALSE) + +if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") # Clang or AppleClang + set(is_clang TRUE) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(is_gcc TRUE) +elseif(MSVC) + set(is_msvc TRUE) +else() + message(FATAL_ERROR "Unsupported C++ compiler: ${CMAKE_CXX_COMPILER_ID}") +endif() + + +# -------------------------------------------------------------------- +# Operating system detection +# -------------------------------------------------------------------- +set(is_linux FALSE) +set(is_windows FALSE) +set(is_macos FALSE) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(is_linux TRUE) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(is_windows TRUE) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(is_macos TRUE) +endif() + +# -------------------------------------------------------------------- +# Architecture +# -------------------------------------------------------------------- +set(is_amd64 FALSE) +set(is_arm64 FALSE) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + set(is_amd64 TRUE) +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(is_arm64 TRUE) +else() + message(FATAL_ERROR "Unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}") +endif() diff --git a/cmake/XrplCompiler.cmake b/cmake/XrplCompiler.cmake index 0777bf948c..a7c5ff0423 100644 --- a/cmake/XrplCompiler.cmake +++ b/cmake/XrplCompiler.cmake @@ -2,16 +2,23 @@ setup project-wide compiler settings #]===================================================================] +include(CompilationEnv) + #[=========================================================[ TODO some/most of these common settings belong in a toolchain file, especially the ABI-impacting ones #]=========================================================] add_library (common INTERFACE) add_library (Xrpl::common ALIAS common) +include(XrplSanitizers) # add a single global dependency on this interface lib link_libraries (Xrpl::common) +# Respect CMAKE_POSITION_INDEPENDENT_CODE setting (may be set by Conan toolchain) +if(NOT DEFINED CMAKE_POSITION_INDEPENDENT_CODE) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() set_target_properties (common - PROPERTIES INTERFACE_POSITION_INDEPENDENT_CODE ON) + PROPERTIES INTERFACE_POSITION_INDEPENDENT_CODE ${CMAKE_POSITION_INDEPENDENT_CODE}) set(CMAKE_CXX_EXTENSIONS OFF) target_compile_definitions (common INTERFACE @@ -116,8 +123,8 @@ else () # link to static libc/c++ iff: # * static option set and # * NOT APPLE (AppleClang does not support static libc/c++) and - # * NOT san (sanitizers typically don't work with static libc/c++) - $<$,$>,$>>: + # * NOT SANITIZERS (sanitizers typically don't work with static libc/c++) + $<$,$>,$>>: -static-libstdc++ -static-libgcc >) diff --git a/cmake/XrplInterface.cmake b/cmake/XrplInterface.cmake index 9847f39fba..6e0203c099 100644 --- a/cmake/XrplInterface.cmake +++ b/cmake/XrplInterface.cmake @@ -2,6 +2,8 @@ xrpld compile options/settings via an interface library #]===================================================================] +include(CompilationEnv) + add_library (opts INTERFACE) add_library (Xrpl::opts ALIAS opts) target_compile_definitions (opts @@ -42,22 +44,6 @@ if(jemalloc) target_link_libraries(opts INTERFACE jemalloc::jemalloc) endif () -if (san) - target_compile_options (opts - INTERFACE - # sanitizers recommend minimum of -O1 for reasonable performance - $<$:-O1> - ${SAN_FLAG} - -fno-omit-frame-pointer) - target_compile_definitions (opts - INTERFACE - $<$:SANITIZER=ASAN> - $<$:SANITIZER=TSAN> - $<$:SANITIZER=MSAN> - $<$:SANITIZER=UBSAN>) - target_link_libraries (opts INTERFACE ${SAN_FLAG} ${SAN_LIB}) -endif () - #[===================================================================[ xrpld transitive library deps via an interface library #]===================================================================] diff --git a/cmake/XrplSanitizers.cmake b/cmake/XrplSanitizers.cmake new file mode 100644 index 0000000000..050a5ef6f0 --- /dev/null +++ b/cmake/XrplSanitizers.cmake @@ -0,0 +1,198 @@ +#[===================================================================[ + Configure sanitizers based on environment variables. + + This module reads the following environment variables: + - SANITIZERS: The sanitizers to enable. Possible values: + - "address" + - "address,undefinedbehavior" + - "thread" + - "thread,undefinedbehavior" + - "undefinedbehavior" + + The compiler type and platform are detected in CompilationEnv.cmake. + The sanitizer compile options are applied to the 'common' interface library + which is linked to all targets in the project. + + Internal flag variables set by this module: + + - SANITIZER_TYPES: List of sanitizer types to enable (e.g., "address", + "thread", "undefined"). And two more flags for undefined behavior sanitizer (e.g., "float-divide-by-zero", "unsigned-integer-overflow"). + This list is joined with commas and passed to -fsanitize=. + + - SANITIZERS_COMPILE_FLAGS: Compiler flags for sanitizer instrumentation. + Includes: + * -fno-omit-frame-pointer: Preserves frame pointers for stack traces + * -O1: Minimum optimization for reasonable performance + * -fsanitize=: Enables sanitizer instrumentation + * -fsanitize-ignorelist=: (Clang only) Compile-time ignorelist + * -mcmodel=large/medium: (GCC only) Code model for large binaries + * -Wno-stringop-overflow: (GCC only) Suppresses false positive warnings + * -Wno-tsan: (For GCC TSAN combination only) Suppresses atomic_thread_fence warnings + + - SANITIZERS_LINK_FLAGS: Linker flags for sanitizer runtime libraries. + Includes: + * -fsanitize=: Links sanitizer runtime libraries + * -mcmodel=large/medium: (GCC only) Matches compile-time code model + + - SANITIZERS_RELOCATION_FLAGS: (GCC only) Code model flags for linking. + Used to handle large instrumented binaries on x86_64: + * -mcmodel=large: For AddressSanitizer (prevents relocation errors) + * -mcmodel=medium: For ThreadSanitizer (large model is incompatible) +#]===================================================================] + +include(CompilationEnv) + +# Read environment variable +set(SANITIZERS $ENV{SANITIZERS}) + +# Set SANITIZERS_ENABLED flag for use in other modules +if(SANITIZERS MATCHES "address|thread|undefinedbehavior") + set(SANITIZERS_ENABLED TRUE) +else() + set(SANITIZERS_ENABLED FALSE) + return() +endif() + +# Sanitizers are not supported on Windows/MSVC +if(is_msvc) + message(FATAL_ERROR "Sanitizers are not supported on Windows/MSVC. " + "Please unset the SANITIZERS environment variable.") +endif() + +message(STATUS "Configuring sanitizers: ${SANITIZERS}") + +# Parse SANITIZERS value to determine which sanitizers to enable +set(enable_asan FALSE) +set(enable_tsan FALSE) +set(enable_ubsan FALSE) + +# Normalize SANITIZERS into a list +set(san_list "${SANITIZERS}") +string(REPLACE "," ";" san_list "${san_list}") +separate_arguments(san_list) + +foreach(san IN LISTS san_list) + if(san STREQUAL "address") + set(enable_asan TRUE) + elseif(san STREQUAL "thread") + set(enable_tsan TRUE) + elseif(san STREQUAL "undefinedbehavior") + set(enable_ubsan TRUE) + else() + message(FATAL_ERROR "Unsupported sanitizer type: ${san}" + "Supported: address, thread, undefinedbehavior and their combinations.") + endif() +endforeach() + +# Validate sanitizer compatibility +if(enable_asan AND enable_tsan) + message(FATAL_ERROR "AddressSanitizer and ThreadSanitizer are incompatible and cannot be enabled simultaneously. " + "Use 'address' or 'thread', optionally with 'undefinedbehavior'.") +endif() + +# Frame pointer is required for meaningful stack traces. Sanitizers recommend minimum of -O1 for reasonable performance +set(SANITIZERS_COMPILE_FLAGS "-fno-omit-frame-pointer" "-O1") + +# Build the sanitizer flags list +set(SANITIZER_TYPES) + +if(enable_asan) + list(APPEND SANITIZER_TYPES "address") +elseif(enable_tsan) + list(APPEND SANITIZER_TYPES "thread") +endif() + +if(enable_ubsan) + # UB sanitizer flags + list(APPEND SANITIZER_TYPES "undefined" "float-divide-by-zero") + if(is_clang) + # Clang supports additional UB checks. More info here https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html + list(APPEND SANITIZER_TYPES "unsigned-integer-overflow") + endif() +endif() + +# Configure code model for GCC on amd64 +# Use large code model for ASAN to avoid relocation errors +# Use medium code model for TSAN (large is not compatible with TSAN) +set(SANITIZERS_RELOCATION_FLAGS) + +# Compiler-specific configuration +if(is_gcc) + # Disable mold, gold and lld linkers for GCC with sanitizers + # Use default linker (bfd/ld) which is more lenient with mixed code models + # This is needed since the size of instrumented binary exceeds the limits set by mold, lld and gold linkers + set(use_mold OFF CACHE BOOL "Use mold linker" FORCE) + set(use_gold OFF CACHE BOOL "Use gold linker" FORCE) + set(use_lld OFF CACHE BOOL "Use lld linker" FORCE) + message(STATUS " Disabled mold, gold, and lld linkers for GCC with sanitizers") + + # Suppress false positive warnings in GCC with stringop-overflow + list(APPEND SANITIZERS_COMPILE_FLAGS "-Wno-stringop-overflow") + + if(is_amd64 AND enable_asan) + message(STATUS " Using large code model (-mcmodel=large)") + list(APPEND SANITIZERS_COMPILE_FLAGS "-mcmodel=large") + list(APPEND SANITIZERS_RELOCATION_FLAGS "-mcmodel=large") + elseif(enable_tsan) + # GCC doesn't support atomic_thread_fence with tsan. Suppress warnings. + list(APPEND SANITIZERS_COMPILE_FLAGS "-Wno-tsan") + message(STATUS " Using medium code model (-mcmodel=medium)") + list(APPEND SANITIZERS_COMPILE_FLAGS "-mcmodel=medium") + list(APPEND SANITIZERS_RELOCATION_FLAGS "-mcmodel=medium") + endif() + + # Join sanitizer flags with commas for -fsanitize option + list(JOIN SANITIZER_TYPES "," SANITIZER_TYPES_STR) + + # Add sanitizer to compile and link flags + list(APPEND SANITIZERS_COMPILE_FLAGS "-fsanitize=${SANITIZER_TYPES_STR}") + set(SANITIZERS_LINK_FLAGS "${SANITIZERS_RELOCATION_FLAGS}" "-fsanitize=${SANITIZER_TYPES_STR}") + +elseif(is_clang) + # Add ignorelist for Clang (GCC doesn't support this) + # Use CMAKE_SOURCE_DIR to get the path to the ignorelist + set(IGNORELIST_PATH "${CMAKE_SOURCE_DIR}/sanitizers/suppressions/sanitizer-ignorelist.txt") + if(NOT EXISTS "${IGNORELIST_PATH}") + message(FATAL_ERROR "Sanitizer ignorelist not found: ${IGNORELIST_PATH}") + endif() + + list(APPEND SANITIZERS_COMPILE_FLAGS "-fsanitize-ignorelist=${IGNORELIST_PATH}") + message(STATUS " Using sanitizer ignorelist: ${IGNORELIST_PATH}") + + # Join sanitizer flags with commas for -fsanitize option + list(JOIN SANITIZER_TYPES "," SANITIZER_TYPES_STR) + + # Add sanitizer to compile and link flags + list(APPEND SANITIZERS_COMPILE_FLAGS "-fsanitize=${SANITIZER_TYPES_STR}") + set(SANITIZERS_LINK_FLAGS "-fsanitize=${SANITIZER_TYPES_STR}") +endif() + +message(STATUS " Compile flags: ${SANITIZERS_COMPILE_FLAGS}") +message(STATUS " Link flags: ${SANITIZERS_LINK_FLAGS}") + +# Apply the sanitizer flags to the 'common' interface library +# This is the same library used by XrplCompiler.cmake +target_compile_options(common INTERFACE + $<$:${SANITIZERS_COMPILE_FLAGS}> + $<$:${SANITIZERS_COMPILE_FLAGS}> +) + +# Apply linker flags +target_link_options(common INTERFACE ${SANITIZERS_LINK_FLAGS}) + +# Define SANITIZERS macro for BuildInfo.cpp +set(sanitizers_list) +if(enable_asan) + list(APPEND sanitizers_list "ASAN") +endif() +if(enable_tsan) + list(APPEND sanitizers_list "TSAN") +endif() +if(enable_ubsan) + list(APPEND sanitizers_list "UBSAN") +endif() + +if(sanitizers_list) + list(JOIN sanitizers_list "." sanitizers_str) + target_compile_definitions(common INTERFACE SANITIZERS=${sanitizers_str}) +endif() diff --git a/cmake/XrplSanity.cmake b/cmake/XrplSanity.cmake index db983da73d..7464ca396c 100644 --- a/cmake/XrplSanity.cmake +++ b/cmake/XrplSanity.cmake @@ -2,6 +2,8 @@ sanity checks #]===================================================================] +include(CompilationEnv) + get_property(is_multiconfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) set (CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) @@ -16,14 +18,12 @@ if (NOT is_multiconfig) endif () endif () -if ("${CMAKE_CXX_COMPILER_ID}" MATCHES ".*Clang") # both Clang and AppleClang - set (is_clang TRUE) +if (is_clang) # both Clang and AppleClang if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0) message (FATAL_ERROR "This project requires clang 16 or later") endif () -elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - set (is_gcc TRUE) +elseif (is_gcc) if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 12.0) message (FATAL_ERROR "This project requires GCC 12 or later") endif () @@ -40,11 +40,6 @@ if (MSVC AND CMAKE_GENERATOR_PLATFORM STREQUAL "Win32") message (FATAL_ERROR "Visual Studio 32-bit build is not supported.") endif () -if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) - message (FATAL_ERROR "Xrpld requires a 64 bit target architecture.\n" - "The most likely cause of this warning is trying to build xrpld with a 32-bit OS.") -endif () - if (APPLE AND NOT HOMEBREW) find_program (HOMEBREW brew) endif () diff --git a/cmake/XrplSettings.cmake b/cmake/XrplSettings.cmake index c3f013c575..647e95837d 100644 --- a/cmake/XrplSettings.cmake +++ b/cmake/XrplSettings.cmake @@ -2,11 +2,7 @@ declare options and variables #]===================================================================] -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - set (is_linux TRUE) -else() - set(is_linux FALSE) -endif() +include(CompilationEnv) if("$ENV{CI}" STREQUAL "true" OR "$ENV{CONTINUOUS_INTEGRATION}" STREQUAL "true") set(is_ci TRUE) @@ -62,7 +58,7 @@ else() set(wextra OFF CACHE BOOL "gcc/clang only" FORCE) endif() -if(is_linux) +if(is_linux AND NOT SANITIZER) option(BUILD_SHARED_LIBS "build shared xrpl libraries" OFF) option(static "link protobuf, openssl, libc++, and boost statically" ON) option(perf "Enables flags that assist with perf recording" OFF) @@ -107,33 +103,6 @@ option(local_protobuf option(local_grpc "Force a local build of gRPC instead of looking for an installed version." OFF) -# this one is a string and therefore can't be an option -set(san "" CACHE STRING "On gcc & clang, add sanitizer instrumentation") -set_property(CACHE san PROPERTY STRINGS ";undefined;memory;address;thread") -if(san) - string(TOLOWER ${san} san) - set(SAN_FLAG "-fsanitize=${san}") - set(SAN_LIB "") - if(is_gcc) - if(san STREQUAL "address") - set(SAN_LIB "asan") - elseif(san STREQUAL "thread") - set(SAN_LIB "tsan") - elseif(san STREQUAL "memory") - set(SAN_LIB "msan") - elseif(san STREQUAL "undefined") - set(SAN_LIB "ubsan") - endif() - endif() - set(_saved_CRL ${CMAKE_REQUIRED_LIBRARIES}) - set(CMAKE_REQUIRED_LIBRARIES "${SAN_FLAG};${SAN_LIB}") - check_cxx_compiler_flag(${SAN_FLAG} COMPILER_SUPPORTS_SAN) - set(CMAKE_REQUIRED_LIBRARIES ${_saved_CRL}) - if(NOT COMPILER_SUPPORTS_SAN) - message(FATAL_ERROR "${san} sanitizer does not seem to be supported by your compiler") - endif() -endif() - # the remaining options are obscure and rarely used option(beast_no_unit_test_inline "Prevents unit test definitions from being inserted into global table" diff --git a/cmake/deps/Boost.cmake b/cmake/deps/Boost.cmake index 475c1033b2..b73698efd8 100644 --- a/cmake/deps/Boost.cmake +++ b/cmake/deps/Boost.cmake @@ -1,4 +1,7 @@ -find_package(Boost 1.82 REQUIRED +include(CompilationEnv) +include(XrplSanitizers) + +find_package(Boost REQUIRED COMPONENTS chrono container @@ -32,7 +35,7 @@ target_link_libraries(xrpl_boost if(Boost_COMPILER) target_link_libraries(xrpl_boost INTERFACE Boost::disable_autolinking) endif() -if(san AND is_clang) +if(SANITIZERS_ENABLED AND is_clang) # TODO: gcc does not support -fsanitize-blacklist...can we do something else # for gcc ? if(NOT Boost_INCLUDE_DIRS AND TARGET Boost::headers) diff --git a/conan.lock b/conan.lock index 3215456b63..99ca08526b 100644 --- a/conan.lock +++ b/conan.lock @@ -1,45 +1,45 @@ { "version": "0.5", "requires": [ - "zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497", - "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1756234289.683", - "wasmi/0.42.1#2a96357d4e6bf40dfe201106d849c24f%1764802092.014", - "sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1756234266.869", - "soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1756234262.318", - "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246", - "secp256k1/0.7.0#9c4ab67bdc3860c16ea5b36aed8f74ea%1765202256.763", - "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1762797952.535", - "re2/20230301#ca3b241baec15bd31ea9187150e0b333%1764175362.029", - "protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1764863245.83", - "openssl/3.5.4#a1d5835cc6ed5c5b8f3cd5b9b5d24205%1760106486.594", - "nudb/2.0.9#fb8dfd1a5557f5e0528114c2da17721e%1763150366.909", - "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1756234228.999", - "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1756223727.64", - "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1756230911.03", - "libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1764175360.142", + "zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1765850150.075", + "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987", + "wasmi/1.0.6#407c9db14601a8af1c7dd3b388f3e4cd%1768164779.349", + "sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1765850149.926", + "soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1765850149.46", + "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878", + "secp256k1/0.7.0#9c4ab67bdc3860c16ea5b36aed8f74ea%1765850147.928", + "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86", + "re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103", + "protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038", + "openssl/3.5.4#1b986e61b38fdfda3b40bebc1b234393%1768312656.257", + "nudb/2.0.9#fb8dfd1a5557f5e0528114c2da17721e%1765850143.957", + "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", + "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492", + "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", + "libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1765850144.736", "jemalloc/5.3.0#e951da9cf599e956cebc117880d2d9f8%1729241615.244", - "grpc/1.72.0#f244a57bff01e708c55a1100b12e1589%1763158050.628", - "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1764270189.893", - "doctest/2.4.12#eb9fb352fb2fdfc8abb17ec270945165%1762797941.757", - "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1763584497.32", - "c-ares/1.34.5#5581c2b62a608b40bb85d965ab3ec7c8%1764175359.429", - "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1764175359.429", - "boost/1.88.0#8852c0b72ce8271fb8ff7c53456d4983%1756223752.326", - "abseil/20250127.0#9e8e8cfc89a1324139fc0ee3bd4d8c8c%1753819045.301" + "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152", + "grpc/1.72.0#f244a57bff01e708c55a1100b12e1589%1765850193.734", + "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772", + "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772", + "c-ares/1.34.5#5581c2b62a608b40bb85d965ab3ec7c8%1765850144.336", + "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837", + "boost/1.88.0#8852c0b72ce8271fb8ff7c53456d4983%1765850172.862", + "abseil/20250127.0#99262a368bd01c0ccca8790dfced9719%1766517936.993" ], "build_requires": [ - "zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497", - "strawberryperl/5.32.1.1#707032463aa0620fa17ec0d887f5fe41%1756234281.733", - "protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1764863245.83", - "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1756234232.901", + "zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1765850150.075", + "strawberryperl/5.32.1.1#707032463aa0620fa17ec0d887f5fe41%1765850165.196", + "protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038", + "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707", "msys2/cci.latest#1996656c3c98e5765b25b60ff5cf77b4%1764840888.758", "m4/1.4.19#70dc8bbb33e981d119d2acc0175cf381%1763158052.846", - "cmake/4.2.0#ae0a44f44a1ef9ab68fd4b3e9a1f8671%1764175359.44", - "cmake/3.31.10#313d16a1aa16bbdb2ca0792467214b76%1764175359.429", - "b2/5.3.3#107c15377719889654eb9a162a673975%1756234226.28", + "cmake/4.2.0#ae0a44f44a1ef9ab68fd4b3e9a1f8671%1765850153.937", + "cmake/3.31.10#313d16a1aa16bbdb2ca0792467214b76%1765850153.479", + "b2/5.3.3#107c15377719889654eb9a162a673975%1765850144.355", "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56", "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86", - "abseil/20250127.0#9e8e8cfc89a1324139fc0ee3bd4d8c8c%1753819045.301" + "abseil/20250127.0#99262a368bd01c0ccca8790dfced9719%1766517936.993" ], "python_requires": [], "overrides": { diff --git a/conan/profiles/ci b/conan/profiles/ci new file mode 100644 index 0000000000..c4c0898ad5 --- /dev/null +++ b/conan/profiles/ci @@ -0,0 +1 @@ + include(sanitizers) diff --git a/conan/profiles/sanitizers b/conan/profiles/sanitizers new file mode 100644 index 0000000000..d7a622359a --- /dev/null +++ b/conan/profiles/sanitizers @@ -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", "-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.append("-Wno-tsan") %} + {% endif %} + + {% if "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}} {{" ".join(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", "-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}} {{" ".join(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"] diff --git a/conanfile.py b/conanfile.py index 797e5beee5..37c26ef5c0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,12 +35,12 @@ class Xrpl(ConanFile): "openssl/3.5.4", "secp256k1/0.7.0", "soci/4.0.3", - "wasmi/0.42.1", + "wasmi/1.0.6", "zlib/1.3.1", ] test_requires = [ - "doctest/2.4.12", + "gtest/1.17.0", ] tool_requires = [ @@ -88,7 +88,13 @@ class Xrpl(ConanFile): "libarchive/*:with_xattr": False, "libarchive/*:with_zlib": False, "lz4/*:shared": False, + "openssl/*:no_dtls": True, + "openssl/*:no_ssl": True, + "openssl/*:no_ssl3": True, + "openssl/*:no_tls1": True, + "openssl/*:no_tls1_1": True, "openssl/*:shared": False, + "openssl/*:tls_security_level": 2, "protobuf/*:shared": False, "protobuf/*:with_zlib": True, "rocksdb/*:enable_sse": False, diff --git a/docs/build/sanitizers.md b/docs/build/sanitizers.md new file mode 100644 index 0000000000..3f9809ae98 --- /dev/null +++ b/docs/build/sanitizers.md @@ -0,0 +1,207 @@ +# Sanitizer Configuration for Rippled + +This document explains how to properly configure and run sanitizers (AddressSanitizer, undefinedbehaviorSanitizer, ThreadSanitizer) with the xrpld 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) + - [Summary](#summary) + - [Build steps:](#build-steps) + - [Install dependencies](#install-dependencies) + - [Call CMake](#call-cmake) + - [Build](#build) + - [Running Tests with Sanitizers](#running-tests-with-sanitizers) + - [AddressSanitizer (ASAN)](#addresssanitizer-asan) + - [ThreadSanitizer (TSan)](#threadsanitizer-tsan) + - [LeakSanitizer (LSan)](#leaksanitizer-lsan) + - [UndefinedBehaviorSanitizer (UBSan)](#undefinedbehaviorsanitizer-ubsan) + - [Suppression Files](#suppression-files) + - [`asan.supp`](#asansupp) + - [`lsan.supp`](#lsansupp) + - [`ubsan.supp`](#ubsansupp) + - [`tsan.supp`](#tsansupp) + - [`sanitizer-ignorelist.txt`](#sanitizer-ignorelisttxt) + - [Troubleshooting](#troubleshooting) + - ["ASAN is ignoring requested \_\_asan_handle_no_return" warnings](#asan-is-ignoring-requested-__asan_handle_no_return-warnings) + - [Sanitizer Mismatch Errors](#sanitizer-mismatch-errors) + - [References](#references) + +## Building with Sanitizers + +### Summary + +Follow the same instructions as mentioned in [BUILD.md](../../BUILD.md) but with the following changes: + +1. Make sure you have a clean build directory. +2. Set the `SANITIZERS` environment variable before calling conan install and cmake. Only set it once. Make sure both conan and cmake read the same values. + Example: `export SANITIZERS=address,undefinedbehavior` +3. Optionally use `--profile:all sanitizers` with Conan to build dependencies with sanitizer instrumentation. [!NOTE]Building with sanitizer-instrumented dependencies is slower but produces fewer false positives. +4. Set `ASAN_OPTIONS`, `LSAN_OPTIONS`, `UBSAN_OPTIONS` and `TSAN_OPTIONS` environment variables to configure sanitizer behavior when running executables. [More details below](#running-tests-with-sanitizers). + +--- + +### Build steps: + +```bash +cd /path/to/rippled +rm -rf .build +mkdir .build +cd .build +``` + +#### Install dependencies + +The `SANITIZERS` environment variable is used by both Conan and CMake. + +```bash +export SANITIZERS=address,undefinedbehavior +# Standard build (without instrumenting dependencies) +conan install .. --output-folder . --build missing --settings build_type=Debug + +# Or with sanitizer-instrumented dependencies (takes longer but fewer false positives) +conan install .. --output-folder . --profile:all sanitizers --build missing --settings build_type=Debug +``` + +[!CAUTION] +Do not mix Address and Thread sanitizers - they are incompatible. + +Since you already set the `SANITIZERS` environment variable when running Conan, same values will be read for the next part. + +#### Call CMake + +```bash +cmake .. -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ + -DCMAKE_BUILD_TYPE=Debug \ + -Dtests=ON -Dxrpld=ON +``` + +#### Build + +```bash +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="print_stacktrace=1:detect_container_overflow=0:suppressions=path/to/asan.supp:halt_on_error=0:log_path=asan.log" +export LSAN_OPTIONS="suppressions=path/to/lsan.supp:halt_on_error=0:log_path=lsan.log" + +# Run tests +./xrpld --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. +- Building dependencies with ASAN instrumentation reduces false positives. But we don't want to instrument dependencies like Boost with ASAN because it is slow (to compile as well as run tests) and not necessary. +- See: https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow +- More such flags are detailed [here](https://github.com/google/sanitizers/wiki/AddressSanitizerFlags) + +### ThreadSanitizer (TSan) + +```bash +export TSAN_OPTIONS="suppressions=path/to/tsan.supp halt_on_error=0 log_path=tsan.log" + +# Run tests +./xrpld --unittest --unittest-jobs=5 +``` + +More details [here](https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual). + +### LeakSanitizer (LSan) + +LSan is automatically enabled with ASAN. To disable it: + +```bash +export ASAN_OPTIONS="detect_leaks=0" +``` + +More details [here](https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer). + +### UndefinedBehaviorSanitizer (UBSan) + +```bash +export UBSAN_OPTIONS="suppressions=path/to/ubsan.supp:print_stacktrace=1:halt_on_error=0:log_path=ubsan.log" + +# Run tests +./xrpld --unittest --unittest-jobs=5 +``` + +More details [here](https://clang.llvm.org/docs/undefinedbehaviorSanitizer.html). + +## Suppression Files + +[!NOTE] Attached files contain more details. + +### [`asan.supp`](../../sanitizers/suppressions/asan.supp) + +- **Purpose**: Suppress AddressSanitizer (ASAN) errors only +- **Format**: `interceptor_name:` 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`](../../sanitizers/suppressions/lsan.supp) + +- **Purpose**: Suppress LeakSanitizer (LSan) errors only +- **Format**: `leak:` where pattern matches function/file names +- **More info**: [LeakSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer) + +### [`ubsan.supp`](../../sanitizers/suppressions/ubsan.supp) + +- **Purpose**: Suppress undefinedbehaviorSanitizer errors +- **Format**: `:` (e.g., `unsigned-integer-overflow:protobuf`) +- **Covers**: Intentional overflows in sanitizers/suppressions libraries (protobuf, gRPC, stdlib) +- More info [UBSan suppressions](https://clang.llvm.org/docs/SanitizerSpecialCaseList.html). + +### [`tsan.supp`](../../sanitizers/suppressions/tsan.supp) + +- **Purpose**: Suppress ThreadSanitizer data race warnings +- **Format**: `race:` where pattern matches function/file names +- **More info**: [ThreadSanitizer suppressions](https://github.com/google/sanitizers/wiki/ThreadSanitizerSuppressions) + +### [`sanitizer-ignorelist.txt`](../../sanitizers/suppressions/sanitizer-ignorelist.txt) + +- **Purpose**: Compile-time ignorelist for all sanitizers +- **Usage**: Passed via `-fsanitize-ignorelist=absolute/path/to/sanitizer-ignorelist.txt` +- **Format**: `:` (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) +- [UndefinedBehavior Sanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html) +- [ThreadSanitizer](https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index fe0db38f16..d673bbe160 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -1,8 +1,11 @@ #ifndef XRPL_BASICS_NUMBER_H_INCLUDED #define XRPL_BASICS_NUMBER_H_INCLUDED +#include + #include #include +#include #include #include @@ -13,42 +16,252 @@ class Number; std::string to_string(Number const& amount); +template +constexpr std::optional +logTen(T value) +{ + int log = 0; + while (value >= 10 && value % 10 == 0) + { + value /= 10; + ++log; + } + if (value == 1) + return log; + return std::nullopt; +} + template constexpr bool isPowerOfTen(T value) { - while (value >= 10 && value % 10 == 0) - value /= 10; - return value == 1; + return logTen(value).has_value(); } +/** MantissaRange defines a range for the mantissa of a normalized Number. + * + * The mantissa is in the range [min, max], where + * * min is a power of 10, and + * * max = min * 10 - 1. + * + * The mantissa_scale enum indicates whether the range is "small" or "large". + * This intentionally restricts the number of MantissaRanges that can be + * instantiated to two: one for each scale. + * + * The "small" scale is based on the behavior of STAmount for IOUs. It has a min + * value of 10^15, and a max value of 10^16-1. This was sufficient for + * uses before Lending Protocol was implemented, mostly related to AMM. + * + * However, it does not have sufficient precision to represent the full integer + * range of int64_t values (-2^63 to 2^63-1), which are needed for XRP and MPT + * values. The implementation of SingleAssetVault, and LendingProtocol need to + * represent those integer values accurately and precisely, both for the + * STNumber field type, and for internal calculations. That necessitated the + * "large" scale. + * + * The "large" scale is intended to represent all values that can be represented + * by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max + * value of 10^19-1. + * + * Note that if the mentioned amendments are eventually retired, this class + * should be left in place, but the "small" scale option should be removed. This + * will allow for future expansion beyond 64-bits if it is ever needed. + */ +struct MantissaRange +{ + using rep = std::uint64_t; + enum mantissa_scale { small, large }; + + explicit constexpr MantissaRange(mantissa_scale scale_) + : min(getMin(scale_)) + , max(min * 10 - 1) + , log(logTen(min).value_or(-1)) + , scale(scale_) + { + } + + rep min; + rep max; + int log; + mantissa_scale scale; + +private: + static constexpr rep + getMin(mantissa_scale scale_) + { + switch (scale_) + { + case small: + return 1'000'000'000'000'000ULL; + case large: + return 1'000'000'000'000'000'000ULL; + default: + // Since this can never be called outside a non-constexpr + // context, this throw assures that the build fails if an + // invalid scale is used. + throw std::runtime_error("Unknown mantissa scale"); + } + } +}; + +// Like std::integral, but only 64-bit integral types. +template +concept Integral64 = + std::is_same_v || std::is_same_v; + +/** Number is a floating point type that can represent a wide range of values. + * + * It can represent all values that can be represented by an STAmount - + * regardless of asset type - XRPAmount, MPTAmount, and IOUAmount, with at least + * as much precision as those types require. + * + * ---- Internal Representation ---- + * + * Internally, Number is represented with three values: + * 1. a bool sign flag, + * 2. a std::uint64_t mantissa, + * 3. an int exponent. + * + * The internal mantissa is an unsigned integer in the range defined by the + * current MantissaRange. The exponent is an integer in the range + * [minExponent, maxExponent]. + * + * See the description of MantissaRange for more details on the ranges. + * + * A non-zero mantissa is (almost) always normalized, meaning it and the + * exponent are grown or shrunk until the mantissa is in the range + * [MantissaRange.min, MantissaRange.max]. + * + * Note: + * 1. Normalization can be disabled by using the "unchecked" ctor tag. This + * should only be used at specific conversion points, some constexpr + * values, and in unit tests. + * 2. The max of the "large" range, 10^19-1, is the largest 10^X-1 value that + * fits in an unsigned 64-bit number. (10^19-1 < 2^64-1 and + * 10^20-1 > 2^64-1). This avoids under- and overflows. + * + * ---- External Interface ---- + * + * The external interface of Number consists of a std::int64_t mantissa, which + * is restricted to 63-bits, and an int exponent, which must be in the range + * [minExponent, maxExponent]. The range of the mantissa depends on which + * MantissaRange is currently active. For the "short" range, the mantissa will + * be between 10^15 and 10^16-1. For the "large" range, the mantissa will be + * between -(2^63-1) and 2^63-1. As noted above, the "large" range is needed to + * represent the full range of valid XRP and MPT integer values accurately. + * + * Note: + * 1. 2^63-1 is between 10^18 and 10^19-1, which are the limits of the "large" + * mantissa range. + * 2. The functions mantissa() and exponent() return the external view of the + * Number value, specifically using a signed 63-bit mantissa. This may + * require altering the internal representation to fit into that range + * before the value is returned. The interface guarantees consistency of + * the two values. + * 3. Number cannot represent -2^63 (std::numeric_limits::min()) + * as an exact integer, but it doesn't need to, because all asset values + * on-ledger are non-negative. This is due to implementation details of + * several operations which use unsigned arithmetic internally. This is + * sufficient to represent all valid XRP values (where the absolute value + * can not exceed INITIAL_XRP: 10^17), and MPT values (where the absolute + * value can not exceed maxMPTokenAmount: 2^63-1). + * + * ---- Mantissa Range Switching ---- + * + * The mantissa range may be changed at runtime via setMantissaScale(). The + * default mantissa range is "large". The range is updated whenever transaction + * processing begins, based on whether SingleAssetVault or LendingProtocol are + * enabled. If either is enabled, the mantissa range is set to "large". If not, + * it is set to "small", preserving backward compatibility and correct + * "amendment-gating". + * + * It is extremely unlikely that any more calls to setMantissaScale() will be + * needed outside of unit tests. + * + * ---- Usage With Different Ranges ---- + * + * Outside of unit tests, and existing checks, code that uses Number should not + * know or care which mantissa range is active. + * + * The results of computations using Numbers with a small mantissa may differ + * from computations using Numbers with a large mantissa, specifically as it + * effects the results after rounding. That is why the large mantissa range is + * amendment gated in transaction processing. + * + * It is extremely unlikely that any more calls to getMantissaScale() will be + * needed outside of unit tests. + * + * Code that uses Number should not assume or check anything about the + * mantissa() or exponent() except that they fit into the "large" range + * specified in the "External Interface" section. + * + * ----- Unit Tests ----- + * + * Within unit tests, it may be useful to explicitly switch between the two + * ranges, or to check which range is active when checking the results of + * computations. If the test is doing the math directly, the + * set/getMantissaScale() functions may be most appropriate. However, if the + * test has anything to do with transaction processing, it should enable or + * disable the amendments that control the mantissa range choice + * (SingleAssetVault and LendingProtocol), and/or check if either of those + * amendments are enabled to determine which result to expect. + * + */ class Number { using rep = std::int64_t; - rep mantissa_{0}; + using internalrep = MantissaRange::rep; + + bool negative_{false}; + internalrep mantissa_{0}; int exponent_{std::numeric_limits::lowest()}; public: - // The range for the mantissa when normalized - constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; - static_assert(isPowerOfTen(minMantissa)); - constexpr static std::int64_t maxMantissa = minMantissa * 10 - 1; - static_assert(maxMantissa == 9'999'999'999'999'999LL); - // The range for the exponent when normalized constexpr static int minExponent = -32768; constexpr static int maxExponent = 32768; + constexpr static internalrep maxRep = std::numeric_limits::max(); + static_assert(maxRep == 9'223'372'036'854'775'807); + static_assert(-maxRep == std::numeric_limits::min() + 1); + + // May need to make unchecked private struct unchecked { explicit unchecked() = default; }; + // Like unchecked, normalized is used with the ctors that take an + // internalrep mantissa. Unlike unchecked, those ctors will normalize the + // value. + // Only unit tests are expected to use this class + struct normalized + { + explicit normalized() = default; + }; + explicit constexpr Number() = default; Number(rep mantissa); explicit Number(rep mantissa, int exponent); - explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; + explicit constexpr Number( + bool negative, + internalrep mantissa, + int exponent, + unchecked) noexcept; + // Assume unsigned values are... unsigned. i.e. positive + explicit constexpr Number( + internalrep mantissa, + int exponent, + unchecked) noexcept; + // Only unit tests are expected to use this ctor + explicit Number( + bool negative, + internalrep mantissa, + int exponent, + normalized); + // Assume unsigned values are... unsigned. i.e. positive + explicit Number(internalrep mantissa, int exponent, normalized); constexpr rep mantissa() const noexcept; @@ -78,11 +291,11 @@ public: Number& operator/=(Number const& x); - static constexpr Number + static Number min() noexcept; - static constexpr Number + static Number max() noexcept; - static constexpr Number + static Number lowest() noexcept; /** Conversions to Number are implicit and conversions away from Number @@ -96,7 +309,8 @@ public: friend constexpr bool operator==(Number const& x, Number const& y) noexcept { - return x.mantissa_ == y.mantissa_ && x.exponent_ == y.exponent_; + return x.negative_ == y.negative_ && x.mantissa_ == y.mantissa_ && + x.exponent_ == y.exponent_; } friend constexpr bool @@ -110,8 +324,8 @@ public: { // If the two amounts have different signs (zero is treated as positive) // then the comparison is true iff the left is negative. - bool const lneg = x.mantissa_ < 0; - bool const rneg = y.mantissa_ < 0; + bool const lneg = x.negative_; + bool const rneg = y.negative_; if (lneg != rneg) return lneg; @@ -139,7 +353,7 @@ public: constexpr int signum() const noexcept { - return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0); + return negative_ ? -1 : (mantissa_ ? 1 : 0); } Number @@ -169,6 +383,15 @@ public: return os << to_string(x); } + friend std::string + to_string(Number const& amount); + + friend Number + root(Number f, unsigned d); + + friend Number + root2(Number f); + // Thread local rounding control. Default is to_nearest enum rounding_mode { to_nearest, towards_zero, downward, upward }; static rounding_mode @@ -177,44 +400,206 @@ public: static rounding_mode setround(rounding_mode mode); + /** Returns which mantissa scale is currently in use for normalization. + * + * If you think you need to call this outside of unit tests, no you don't. + */ + static MantissaRange::mantissa_scale + getMantissaScale(); + /** Changes which mantissa scale is used for normalization. + * + * If you think you need to call this outside of unit tests, no you don't. + */ + static void + setMantissaScale(MantissaRange::mantissa_scale scale); + + inline static internalrep + minMantissa() + { + return range_.get().min; + } + + inline static internalrep + maxMantissa() + { + return range_.get().max; + } + + inline static int + mantissaLog() + { + return range_.get().log; + } + + /// oneSmall is needed because the ranges are private + constexpr static Number + oneSmall(); + /// oneLarge is needed because the ranges are private + constexpr static Number + oneLarge(); + + // And one is needed because it needs to choose between oneSmall and + // oneLarge based on the current range + static Number + one(); + + template + [[nodiscard]] + std::pair + normalizeToRange(T minMantissa, T maxMantissa) const; + private: static thread_local rounding_mode mode_; + // The available ranges for mantissa + + constexpr static MantissaRange smallRange{MantissaRange::small}; + static_assert(isPowerOfTen(smallRange.min)); + static_assert(smallRange.min == 1'000'000'000'000'000LL); + static_assert(smallRange.max == 9'999'999'999'999'999LL); + static_assert(smallRange.log == 15); + static_assert(smallRange.min < maxRep); + static_assert(smallRange.max < maxRep); + constexpr static MantissaRange largeRange{MantissaRange::large}; + static_assert(isPowerOfTen(largeRange.min)); + static_assert(largeRange.min == 1'000'000'000'000'000'000ULL); + static_assert(largeRange.max == internalrep(9'999'999'999'999'999'999ULL)); + static_assert(largeRange.log == 18); + static_assert(largeRange.min < maxRep); + static_assert(largeRange.max > maxRep); + + // The range for the mantissa when normalized. + // Use reference_wrapper to avoid making copies, and prevent accidentally + // changing the values inside the range. + static thread_local std::reference_wrapper range_; void normalize(); - constexpr bool + + /** Normalize Number components to an arbitrary range. + * + * min/maxMantissa are parameters because this function is used by both + * normalize(), which reads from range_, and by normalizeToRange, + * which is public and can accept an arbitrary range from the caller. + */ + template + static void + normalize( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa); + + template + friend void + doNormalize( + bool& negative, + T& mantissa_, + int& exponent_, + MantissaRange::rep const& minMantissa, + MantissaRange::rep const& maxMantissa); + + bool isnormal() const noexcept; + // Copy the number, but modify the exponent by "exponentDelta". Because the + // mantissa doesn't change, the result will be "mostly" normalized, but the + // exponent could go out of range, so it will be checked. + Number + shiftExponent(int exponentDelta) const; + + // Safely convert rep (int64) mantissa to internalrep (uint64). If the rep + // is negative, returns the positive value. This takes a little extra work + // because converting std::numeric_limits::min() flirts with + // UB, and can vary across compilers. + static internalrep + externalToInternal(rep mantissa); + class Guard; }; +inline constexpr Number::Number( + bool negative, + internalrep mantissa, + int exponent, + unchecked) noexcept + : negative_(negative), mantissa_{mantissa}, exponent_{exponent} +{ +} + +inline constexpr Number::Number( + internalrep mantissa, + int exponent, + unchecked) noexcept + : Number(false, mantissa, exponent, unchecked{}) +{ +} + constexpr static Number numZero{}; -inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept - : mantissa_{mantissa}, exponent_{exponent} +inline Number::Number( + bool negative, + internalrep mantissa, + int exponent, + normalized) + : Number(negative, mantissa, exponent, unchecked{}) +{ + normalize(); +} + +inline Number::Number(internalrep mantissa, int exponent, normalized) + : Number(false, mantissa, exponent, normalized{}) { } inline Number::Number(rep mantissa, int exponent) - : mantissa_{mantissa}, exponent_{exponent} + : Number(mantissa < 0, externalToInternal(mantissa), exponent, normalized{}) { - normalize(); } inline Number::Number(rep mantissa) : Number{mantissa, 0} { } +/** Returns the mantissa of the external view of the Number. + * + * Please see the "---- External Interface ----" section of the class + * documentation for an explanation of why the internal value may be modified. + */ inline constexpr Number::rep Number::mantissa() const noexcept { - return mantissa_; + auto m = mantissa_; + if (m > maxRep) + { + XRPL_ASSERT_PARTS( + !isnormal() || (m % 10 == 0 && m / 10 <= maxRep), + "xrpl::Number::mantissa", + "large normalized mantissa has no remainder"); + m /= 10; + } + auto const sign = negative_ ? -1 : 1; + return sign * static_cast(m); } +/** Returns the exponent of the external view of the Number. + * + * Please see the "---- External Interface ----" section of the class + * documentation for an explanation of why the internal value may be modified. + */ inline constexpr int Number::exponent() const noexcept { - return exponent_; + auto e = exponent_; + if (mantissa_ > maxRep) + { + XRPL_ASSERT_PARTS( + !isnormal() || (mantissa_ % 10 == 0 && mantissa_ / 10 <= maxRep), + "xrpl::Number::exponent", + "large normalized mantissa has no remainder"); + ++e; + } + return e; } inline constexpr Number @@ -226,15 +611,17 @@ Number::operator+() const noexcept inline constexpr Number Number::operator-() const noexcept { + if (mantissa_ == 0) + return Number{}; auto x = *this; - x.mantissa_ = -x.mantissa_; + x.negative_ = !x.negative_; return x; } inline Number& Number::operator++() { - *this += Number{1000000000000000, -15, unchecked{}}; + *this += one(); return *this; } @@ -249,7 +636,7 @@ Number::operator++(int) inline Number& Number::operator--() { - *this -= Number{1000000000000000, -15, unchecked{}}; + *this -= one(); return *this; } @@ -299,30 +686,54 @@ operator/(Number const& x, Number const& y) return z; } -inline constexpr Number +inline Number Number::min() noexcept { - return Number{minMantissa, minExponent, unchecked{}}; + return Number{false, range_.get().min, minExponent, unchecked{}}; } -inline constexpr Number +inline Number Number::max() noexcept { - return Number{maxMantissa, maxExponent, unchecked{}}; + return Number{ + false, std::min(range_.get().max, maxRep), maxExponent, unchecked{}}; } -inline constexpr Number +inline Number Number::lowest() noexcept { - return -Number{maxMantissa, maxExponent, unchecked{}}; + return Number{ + true, std::min(range_.get().max, maxRep), maxExponent, unchecked{}}; } -inline constexpr bool +inline bool Number::isnormal() const noexcept { - auto const abs_m = mantissa_ < 0 ? -mantissa_ : mantissa_; - return minMantissa <= abs_m && abs_m <= maxMantissa && - minExponent <= exponent_ && exponent_ <= maxExponent; + MantissaRange const& range = range_; + auto const abs_m = mantissa_; + return *this == Number{} || + (range.min <= abs_m && abs_m <= range.max && + (abs_m <= maxRep || abs_m % 10 == 0) && minExponent <= exponent_ && + exponent_ <= maxExponent); +} + +template +std::pair +Number::normalizeToRange(T minMantissa, T maxMantissa) const +{ + bool negative = negative_; + internalrep mantissa = mantissa_; + int exponent = exponent_; + + if constexpr (std::is_unsigned_v) + XRPL_ASSERT_PARTS( + !negative, + "xrpl::Number::normalizeToRange", + "Number is non-negative for unsigned range."); + Number::normalize(negative, mantissa, exponent, minMantissa, maxMantissa); + + auto const sign = negative ? -1 : 1; + return std::make_pair(static_cast(sign * mantissa), exponent); } inline constexpr Number @@ -368,6 +779,20 @@ squelch(Number const& x, Number const& limit) noexcept return x; } +inline std::string +to_string(MantissaRange::mantissa_scale const& scale) +{ + switch (scale) + { + case MantissaRange::small: + return "small"; + case MantissaRange::large: + return "large"; + default: + throw std::runtime_error("Bad scale"); + } +} + class saveNumberRoundMode { Number::rounding_mode mode_; @@ -406,6 +831,34 @@ public: operator=(NumberRoundModeGuard const&) = delete; }; +/** Sets the new scale and restores the old scale when it leaves scope. + * + * If you think you need to use this class outside of unit tests, no you don't. + * + */ +class NumberMantissaScaleGuard +{ + MantissaRange::mantissa_scale const saved_; + +public: + explicit NumberMantissaScaleGuard( + MantissaRange::mantissa_scale scale) noexcept + : saved_{Number::getMantissaScale()} + { + Number::setMantissaScale(scale); + } + + ~NumberMantissaScaleGuard() + { + Number::setMantissaScale(saved_); + } + + NumberMantissaScaleGuard(NumberMantissaScaleGuard const&) = delete; + + NumberMantissaScaleGuard& + operator=(NumberMantissaScaleGuard const&) = delete; +}; + } // namespace xrpl #endif // XRPL_BASICS_NUMBER_H_INCLUDED diff --git a/src/xrpld/app/paths/Credit.h b/include/xrpl/ledger/Credit.h similarity index 93% rename from src/xrpld/app/paths/Credit.h rename to include/xrpl/ledger/Credit.h index 5bdcd70e74..09b65b3dde 100644 --- a/src/xrpld/app/paths/Credit.h +++ b/include/xrpl/ledger/Credit.h @@ -1,5 +1,5 @@ -#ifndef XRPL_APP_PATHS_CREDIT_H_INCLUDED -#define XRPL_APP_PATHS_CREDIT_H_INCLUDED +#ifndef XRPL_LEDGER_CREDIT_H_INCLUDED +#define XRPL_LEDGER_CREDIT_H_INCLUDED #include #include diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 767622596b..707a08b890 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -61,6 +61,9 @@ enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN }; /** Controls the treatment of unauthorized MPT balances */ enum AuthHandling { ahIGNORE_AUTH, ahZERO_IF_UNAUTHORIZED }; +/** Controls whether to include the account's full spendable balance */ +enum SpendableHandling { shSIMPLE_BALANCE, shFULL_BALANCE }; + [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); @@ -305,86 +308,57 @@ isLPTokenFrozen( Issue const& asset, Issue const& asset2); -// Returns the amount an account can spend without going into debt. +// Returns the amount an account can spend. // -// <-- saAmount: amount of currency held by account. May be negative. -[[nodiscard]] STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer, - FreezeHandling zeroIfFrozen, - beast::Journal j); - -[[nodiscard]] STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - Issue const& issue, - FreezeHandling zeroIfFrozen, - beast::Journal j); - -[[nodiscard]] STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j); - -[[nodiscard]] STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - Asset const& asset, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j); - -// Returns the amount an account can spend total. +// If shSIMPLE_BALANCE is specified, this is the amount the account can spend +// without going into debt. // -// These functions use accountHolds, but unlike accountHolds: -// * The account can go into debt. -// * If the account is the asset issuer the only limit is defined by the asset / +// If shFULL_BALANCE is specified, this is the amount the account can spend +// total. Specifically: +// * The account can go into debt if using a trust line, and the other side has +// a non-zero limit. +// * If the account is the asset issuer the limit is defined by the asset / // issuance. // // <-- saAmount: amount of currency held by account. May be negative. [[nodiscard]] STAmount -accountSpendable( +accountHolds( ReadView const& view, AccountID const& account, Currency const& currency, AccountID const& issuer, FreezeHandling zeroIfFrozen, - beast::Journal j); + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE); [[nodiscard]] STAmount -accountSpendable( +accountHolds( ReadView const& view, AccountID const& account, Issue const& issue, FreezeHandling zeroIfFrozen, - beast::Journal j); + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE); [[nodiscard]] STAmount -accountSpendable( +accountHolds( ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, FreezeHandling zeroIfFrozen, AuthHandling zeroIfUnauthorized, - beast::Journal j); + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE); [[nodiscard]] STAmount -accountSpendable( +accountHolds( ReadView const& view, AccountID const& account, Asset const& asset, FreezeHandling zeroIfFrozen, AuthHandling zeroIfUnauthorized, - beast::Journal j); + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE); // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in @@ -655,7 +629,7 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField); -// Returns true iff sleAcct is a pseudo-account or specific +// Returns true if and only if sleAcct is a pseudo-account or specific // pseudo-accounts in pseudoFieldFilter. // // Returns false if sleAcct is @@ -710,13 +684,16 @@ checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if * the sender has it. + * - Checks that the receiver will not exceed the limit (IOU trustline limit + * or MPT MaximumAmount). */ [[nodiscard]] TER canWithdraw( - AccountID const& from, ReadView const& view, + AccountID const& from, AccountID const& to, SLE::const_ref toSle, + STAmount const& amount, bool hasDestinationTag); /** Checks that can withdraw funds from an object to itself or a destination. @@ -730,12 +707,15 @@ canWithdraw( * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if * the sender has it. + * - Checks that the receiver will not exceed the limit (IOU trustline limit + * or MPT MaximumAmount). */ [[nodiscard]] TER canWithdraw( - AccountID const& from, ReadView const& view, + AccountID const& from, AccountID const& to, + STAmount const& amount, bool hasDestinationTag); /** Checks that can withdraw funds from an object to itself or a destination. @@ -749,6 +729,8 @@ canWithdraw( * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if * the sender has it. + * - Checks that the receiver will not exceed the limit (IOU trustline limit + * or MPT MaximumAmount). */ [[nodiscard]] TER canWithdraw(ReadView const& view, STTx const& tx); diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 195e373fa0..2cdccecabb 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -121,7 +121,7 @@ toAmount( { if (isXRP(issue)) return STAmount(issue, static_cast(n)); - return STAmount(issue, n.mantissa(), n.exponent()); + return STAmount(issue, n); } else { diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index 154b870f7c..405de18e29 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -26,8 +26,10 @@ class IOUAmount : private boost::totally_ordered, private boost::additive { private: - std::int64_t mantissa_; - int exponent_; + using mantissa_type = std::int64_t; + using exponent_type = int; + mantissa_type mantissa_; + exponent_type exponent_; /** Adjusts the mantissa and exponent to the proper range. @@ -38,18 +40,14 @@ private: void normalize(); -public: - /* The range for the mantissa when normalized */ - static std::int64_t constexpr minMantissa = 1000000000000000ull; - static std::int64_t constexpr maxMantissa = 9999999999999999ull; - /* The range for the exponent when normalized */ - static int constexpr minExponent = -96; - static int constexpr maxExponent = 80; + static IOUAmount + fromNumber(Number const& number); +public: IOUAmount() = default; explicit IOUAmount(Number const& other); IOUAmount(beast::Zero); - IOUAmount(std::int64_t mantissa, int exponent); + IOUAmount(mantissa_type mantissa, exponent_type exponent); IOUAmount& operator=(beast::Zero); @@ -78,10 +76,10 @@ public: int signum() const noexcept; - int + exponent_type exponent() const noexcept; - std::int64_t + mantissa_type mantissa() const noexcept; static IOUAmount @@ -99,7 +97,7 @@ inline IOUAmount::IOUAmount(beast::Zero) *this = beast::zero; } -inline IOUAmount::IOUAmount(std::int64_t mantissa, int exponent) +inline IOUAmount::IOUAmount(mantissa_type mantissa, exponent_type exponent) : mantissa_(mantissa), exponent_(exponent) { normalize(); @@ -156,13 +154,13 @@ IOUAmount::signum() const noexcept return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0); } -inline int +inline IOUAmount::exponent_type IOUAmount::exponent() const noexcept { return exponent_; } -inline std::int64_t +inline IOUAmount::mantissa_type IOUAmount::mantissa() const noexcept { return mantissa_; diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index 519d7a96f3..a76b7a8316 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -37,6 +37,9 @@ public: bool native() const; + bool + integral() const; + friend constexpr std::weak_ordering operator<=>(Issue const& lhs, Issue const& rhs); }; diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index b89b59ee0d..ca81548a29 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -46,6 +46,12 @@ public: { return false; } + + bool + integral() const + { + return true; + } }; constexpr bool diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 18bf56c3db..722bd6ae41 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -233,6 +233,7 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024; /** The maximum amount of MPTokenIssuance */ std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull; +static_assert(Number::maxRep >= maxMPTokenAmount); /** The maximum length of Data payload */ std::size_t constexpr maxDataPayloadLength = 256; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index b1d353196d..7f404b4d5f 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -135,7 +135,10 @@ public: sMD_Always = 0x10, // value when node containing it is affected at all sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT - // _only_, then it is a pseudo-account + // _only_, then it is a pseudo-account + sMD_NeedsAsset = 0x80, // This field needs to be associated with an + // asset before it is serialized as a ledger + // object. Intended for STNumber. sMD_Default = sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create }; diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 79cbf51436..4d86aed2ec 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -138,7 +138,7 @@ public: template STAmount(A const& asset, Number const& number) - : STAmount(asset, number.mantissa(), number.exponent()) + : STAmount(fromNumber(asset, number)) { } @@ -282,6 +282,10 @@ public: mpt() const; private: + template + static STAmount + fromNumber(A const& asset, Number const& number); + static std::unique_ptr construct(SerialIter&, SField const& name); @@ -345,10 +349,19 @@ STAmount::STAmount( , mIsNegative(negative) { // mValue is uint64, but needs to fit in the range of int64 - XRPL_ASSERT( - mValue <= std::numeric_limits::max(), - "xrpl::STAmount::STAmount(SField, A, std::uint64_t, int, bool) : " - "maximum mantissa input"); + if (Number::getMantissaScale() == MantissaRange::small) + { + XRPL_ASSERT( + mValue <= std::numeric_limits::max(), + "xrpl::STAmount::STAmount(SField, A, std::uint64_t, int, bool) : " + "maximum mantissa input"); + } + else + { + if (integral() && mValue > std::numeric_limits::max()) + throw std::overflow_error( + "STAmount mantissa is too large " + std::to_string(mantissa)); + } canonicalize(); } @@ -542,14 +555,23 @@ STAmount::operator=(XRPAmount const& amount) return *this; } -inline STAmount& -STAmount::operator=(Number const& number) +template +inline STAmount +STAmount::fromNumber(A const& a, Number const& number) { - mIsNegative = number.mantissa() < 0; - mValue = mIsNegative ? -number.mantissa() : number.mantissa(); - mOffset = number.exponent(); - canonicalize(); - return *this; + bool const negative = number.mantissa() < 0; + Number const working{negative ? -number : number}; + Asset asset{a}; + if (asset.integral()) + { + std::uint64_t const intValue = static_cast(working); + return STAmount{asset, intValue, 0, negative}; + } + + auto const [mantissa, exponent] = + working.normalizeToRange(cMinValue, cMaxValue); + + return STAmount{asset, mantissa, exponent, negative}; } inline void @@ -699,17 +721,32 @@ getRate(STAmount const& offerOut, STAmount const& offerIn); * @param rounding Optional Number rounding mode * */ -STAmount +[[nodiscard]] STAmount roundToScale( STAmount const& value, std::int32_t scale, Number::rounding_mode rounding = Number::getround()); +/** Round an arbitrary precision Number IN PLACE to the precision of a given + * Asset. + * + * This is used to ensure that calculations do not collect dust for IOUs, or + * fractional amounts for the integral types XRP and MPT. + * + * @param asset The relevant asset + * @param value The lvalue to be rounded + */ +template +void +roundToAsset(A const& asset, Number& value) +{ + value = STAmount{asset, value}; +} + /** Round an arbitrary precision Number to the precision of a given Asset. * - * This is used to ensure that calculations do not collect dust beyond the - * precision of the reference value for IOUs, or fractional amounts for the - * integral types XRP and MPT. + * This is used to ensure that calculations do not collect dust beyond specified + * scale for IOUs, or fractional amounts for the integral types XRP and MPT. * * @param asset The relevant asset * @param value The value to be rounded @@ -718,7 +755,7 @@ roundToScale( * @param rounding Optional Number rounding mode */ template -Number +[[nodiscard]] Number roundToAsset( A const& asset, Number const& value, diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index dfdb16af93..39b0c3b042 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -19,8 +20,19 @@ namespace xrpl { * it can represent a value of any token type (XRP, IOU, or MPT) * without paying the storage cost of duplicating asset information * that may be deduced from the context. + * + * STNumber derives from STTakesAsset, so that it can be associated with the + * related Asset during transaction processing. Which asset is relevant depends + * on the object and transaction. As of this writing, only Vault, LoanBroker, + * and Loan objects use STNumber fields. All of those fields represent amounts + * of the Vault's Asset, so they should be associated with the Vault's Asset. + * + * e.g. + * associateAsset(*loanSle, asset); + * associateAsset(*brokerSle, asset); + * associateAsset(*vaultSle, asset); */ -class STNumber : public STBase, public CountedObject +class STNumber : public STTakesAsset, public CountedObject { private: Number value_; @@ -56,6 +68,9 @@ public: bool isDefault() const override; + void + associateAsset(Asset const& a) override; + operator Number() const { return value_; diff --git a/include/xrpl/protocol/STTakesAsset.h b/include/xrpl/protocol/STTakesAsset.h new file mode 100644 index 0000000000..767223b97d --- /dev/null +++ b/include/xrpl/protocol/STTakesAsset.h @@ -0,0 +1,63 @@ +#ifndef XRPL_PROTOCOL_STTAKESASSET_H_INCLUDED +#define XRPL_PROTOCOL_STTAKESASSET_H_INCLUDED + +#include +#include + +namespace xrpl { + +/** Intermediate class for any STBase-derived class to store an Asset. + * + * In the class definition, this class should be specified as a base class + * _instead_ of STBase. + * + * Specifically, the Asset is only stored and used at runtime. It should not be + * serialized to the ledger. + * + * The derived class decides what to do with the Asset, and when. It will not + * necessarily be set at any given time. As of this writing, only STNumber uses + * it to round the stored Number to the Asset's precision both when associated, + * and when serializing the Number. + */ +class STTakesAsset : public STBase +{ +protected: + std::optional asset_; + +public: + using STBase::STBase; + using STBase::operator=; + + virtual void + associateAsset(Asset const& a); +}; + +inline void +STTakesAsset::associateAsset(Asset const& a) +{ + asset_.emplace(a); +} + +class STLedgerEntry; + +/** Associate an Asset with all sMD_NeedsAsset fields in a ledger entry. + * + * This function iterates over all fields in the given ledger entry. For each + * field that is set and has the SField::sMD_NeedsAsset metadata flag, it calls + * `associateAsset` on that field with the given Asset. Such field must be + * derived from STTakesAsset - if it is not, the conversion will throw. + * + * Typically, associateAsset should be called near the end of doApply() of any + * Transactor classes on the SLEs of any new or modified ledger entries + * containing STNumber fields, after doing all of the modifications t the SLEs. + * + * @param sle The ledger entry whose fields will be updated. + * @param asset The Asset to associate with the relevant fields. + * + */ +void +associateAsset(STLedgerEntry& sle, Asset const& asset); + +} // namespace xrpl + +#endif diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index c0732bc9fe..c2f66e9ea1 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -23,6 +23,8 @@ systemName() /** Number of drops in the genesis account. */ constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP}; +static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000); +static_assert(Number::maxRep >= INITIAL_XRP.drops()); /** Returns true if the amount does not exceed the initial XRP in existence. */ inline bool diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index f2af59a903..0604e2df35 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -18,7 +18,7 @@ XRPL_FEATURE(SmartEscrow, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo) +XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (IncludeKeyletFields, Supported::yes, VoteBehavior::DefaultNo) @@ -32,7 +32,7 @@ XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) +XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (FrozenLPTokenTransfer, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 9cf9a55566..d5b082e539 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -548,7 +548,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfStartDate, soeREQUIRED}, {sfPaymentInterval, soeREQUIRED}, {sfGracePeriod, soeDEFAULT}, - {sfPreviousPaymentDate, soeDEFAULT}, + {sfPreviousPaymentDueDate, soeDEFAULT}, {sfNextPaymentDueDate, soeDEFAULT}, // The loan object tracks these values: // diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 4b329c9bbb..e6374037b7 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -102,7 +102,7 @@ TYPED_SFIELD(sfMutableFlags, UINT32, 53) TYPED_SFIELD(sfStartDate, UINT32, 54) TYPED_SFIELD(sfPaymentInterval, UINT32, 55) TYPED_SFIELD(sfGracePeriod, UINT32, 56) -TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 57) +TYPED_SFIELD(sfPreviousPaymentDueDate, UINT32, 57) TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 58) TYPED_SFIELD(sfPaymentRemaining, UINT32, 59) TYPED_SFIELD(sfPaymentTotal, UINT32, 60) @@ -212,22 +212,22 @@ TYPED_SFIELD(sfLoanID, UINT256, 38) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) -TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2) -TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) -TYPED_SFIELD(sfAssetsTotal, NUMBER, 4) -TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) -TYPED_SFIELD(sfDebtTotal, NUMBER, 6) -TYPED_SFIELD(sfDebtMaximum, NUMBER, 7) -TYPED_SFIELD(sfCoverAvailable, NUMBER, 8) +TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfAssetsTotal, NUMBER, 4, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfLossUnrealized, NUMBER, 5, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfDebtTotal, NUMBER, 6, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfDebtMaximum, NUMBER, 7, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfCoverAvailable, NUMBER, 8, SField::sMD_NeedsAsset | SField::sMD_Default) TYPED_SFIELD(sfLoanOriginationFee, NUMBER, 9) TYPED_SFIELD(sfLoanServiceFee, NUMBER, 10) TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11) TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12) -TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) +TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13, SField::sMD_NeedsAsset | SField::sMD_Default) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) -TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) +TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::sMD_NeedsAsset | SField::sMD_Default) TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) -TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17) +TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::sMD_NeedsAsset | SField::sMD_Default) // 32-bit signed (common) TYPED_SFIELD(sfLoanScale, INT32, 1) diff --git a/sanitizers/suppressions/asan.supp b/sanitizers/suppressions/asan.supp new file mode 100644 index 0000000000..7ba98766bd --- /dev/null +++ b/sanitizers/suppressions/asan.supp @@ -0,0 +1,29 @@ +# The idea is to empty this file gradually by fixing the underlying issues and removing suppressions. +# +# ASAN_OPTIONS="print_stacktrace=1: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 + +# 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 diff --git a/sanitizers/suppressions/lsan.supp b/sanitizers/suppressions/lsan.supp new file mode 100644 index 0000000000..a81d7d89fa --- /dev/null +++ b/sanitizers/suppressions/lsan.supp @@ -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 diff --git a/sanitizers/suppressions/sanitizer-ignorelist.txt b/sanitizers/suppressions/sanitizer-ignorelist.txt new file mode 100644 index 0000000000..5dbead48a2 --- /dev/null +++ b/sanitizers/suppressions/sanitizer-ignorelist.txt @@ -0,0 +1,29 @@ +# We were seeing some false positives and some repeated errors(since these are library files) in following files. +# Clang will skip instrumenting the files added here. +# We should fix the underlying issues(if any) and remove these entries. + +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_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_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 +src:src/test/app/Invariants_test.cpp diff --git a/sanitizers/suppressions/tsan.supp b/sanitizers/suppressions/tsan.supp new file mode 100644 index 0000000000..74f3371e68 --- /dev/null +++ b/sanitizers/suppressions/tsan.supp @@ -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 diff --git a/sanitizers/suppressions/ubsan.supp b/sanitizers/suppressions/ubsan.supp new file mode 100644 index 0000000000..1504ef685f --- /dev/null +++ b/sanitizers/suppressions/ubsan.supp @@ -0,0 +1,237 @@ +# 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*/*/util/xxhash.h +unsigned-integer-overflow:rocks*/*/util/xxph3.h +unsigned-integer-overflow:rocks*/*/util/hash.cc +unsigned-integer-overflow:rocks*/*/util/crc32c.cc +unsigned-integer-overflow:rocks*/*/util/crc32c.h +unsigned-integer-overflow:rocks*/*/include/rocksdb/utilities/options_type.h +unsigned-integer-overflow:rocks*/*/table/format.h +unsigned-integer-overflow:rocks*/*/table/format.cc +unsigned-integer-overflow:rocks*/*/table/block_based/block_based_table_builder.cc +unsigned-integer-overflow:rocks*/*/table/block_based/reader_common.cc +unsigned-integer-overflow:rocks*/*/db/version_set.cc + +# RocksDB misaligned loads (intentional for performance on ARM64) +alignment:rocks*/*/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:snappy.cc + +# Abseil intentional overflows +unsigned-integer-overflow:absl/strings/numbers.cc +unsigned-integer-overflow:absl/strings/internal/cord_rep_flat.h +unsigned-integer-overflow:absl/base/internal/low_level_alloc.cc +unsigned-integer-overflow:absl/hash/internal/hash.h +unsigned-integer-overflow:absl/container/internal/raw_hash_set.h + +# Standard library intentional overflows in chrono duration arithmetic +unsigned-integer-overflow:__chrono/duration.h + +# Suppress undefined errors in RocksDB and nudb +undefined:rocks.*/*/util/crc32c_arm64.cc +undefined:rocks.*/*/util/xxhash.h +undefined:nudb diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 18fa8f9492..2fcd88f3b0 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -1,4 +1,6 @@ #include +// Keep Number.h first to ensure it can build without hidden dependencies +#include #include #include @@ -13,16 +15,20 @@ #include #ifdef _MSC_VER -#pragma message("Using boost::multiprecision::uint128_t") +#pragma message("Using boost::multiprecision::uint128_t and int128_t") #include using uint128_t = boost::multiprecision::uint128_t; +using int128_t = boost::multiprecision::int128_t; #else // !defined(_MSC_VER) using uint128_t = __uint128_t; +using int128_t = __int128_t; #endif // !defined(_MSC_VER) namespace xrpl { thread_local Number::rounding_mode Number::mode_ = Number::to_nearest; +thread_local std::reference_wrapper Number::range_ = + largeRange; Number::rounding_mode Number::getround() @@ -36,12 +42,30 @@ Number::setround(rounding_mode mode) return std::exchange(mode_, mode); } +MantissaRange::mantissa_scale +Number::getMantissaScale() +{ + return range_.get().scale; +} + +void +Number::setMantissaScale(MantissaRange::mantissa_scale scale) +{ + if (scale != MantissaRange::small && scale != MantissaRange::large) + LogicError("Unknown mantissa scale"); + range_ = scale == MantissaRange::small ? smallRange : largeRange; +} + // Guard // The Guard class is used to temporarily add extra digits of // precision to an operation. This enables the final result // to be correctly rounded to the internal precision of Number. +template +concept UnsignedMantissa = + std::is_unsigned_v || std::is_same_v; + class Number::Guard { std::uint64_t digits_; // 16 decimal guard digits @@ -62,8 +86,9 @@ public: is_negative() const noexcept; // add a digit + template void - push(unsigned d) noexcept; + push(T d) noexcept; // recover a digit unsigned @@ -76,16 +101,40 @@ public: round() noexcept; // Modify the result to the correctly rounded value + template void - doRoundUp(rep& mantissa, int& exponent, std::string location); + doRoundUp( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa, + std::string location); + + // Modify the result to the correctly rounded value + template + void + doRoundDown( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa); // Modify the result to the correctly rounded value void - doRoundDown(rep& mantissa, int& exponent); + doRound(rep& drops, std::string location); - // Modify the result to the correctly rounded value +private: void - doRound(rep& drops); + doPush(unsigned d) noexcept; + + template + void + bringIntoRange( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa); }; inline void @@ -107,13 +156,20 @@ Number::Guard::is_negative() const noexcept } inline void -Number::Guard::push(unsigned d) noexcept +Number::Guard::doPush(unsigned d) noexcept { - xbit_ = xbit_ || (digits_ & 0x0000'0000'0000'000F) != 0; + xbit_ = xbit_ || ((digits_ & 0x0000'0000'0000'000F) != 0); digits_ >>= 4; digits_ |= (d & 0x0000'0000'0000'000FULL) << 60; } +template +inline void +Number::Guard::push(T d) noexcept +{ + doPush(static_cast(d)); +} + inline unsigned Number::Guard::pop() noexcept { @@ -163,30 +219,65 @@ Number::Guard::round() noexcept return 0; } +template void -Number::Guard::doRoundUp(rep& mantissa, int& exponent, std::string location) +Number::Guard::bringIntoRange( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa) +{ + // Bring mantissa back into the minMantissa / maxMantissa range AFTER + // rounding + if (mantissa < minMantissa) + { + mantissa *= 10; + --exponent; + } + if (exponent < minExponent) + { + constexpr Number zero = Number{}; + + negative = zero.negative_; + mantissa = zero.mantissa_; + exponent = zero.exponent_; + } +} + +template +void +Number::Guard::doRoundUp( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa, + std::string location) { auto r = round(); if (r == 1 || (r == 0 && (mantissa & 1) == 1)) { ++mantissa; - if (mantissa > maxMantissa) + // Ensure mantissa after incrementing fits within both the + // min/maxMantissa range and is a valid "rep". + if (mantissa > maxMantissa || mantissa > maxRep) { mantissa /= 10; ++exponent; } } - if (exponent < minExponent) - { - mantissa = 0; - exponent = Number{}.exponent_; - } + bringIntoRange(negative, mantissa, exponent, minMantissa); if (exponent > maxExponent) throw std::overflow_error(location); } +template void -Number::Guard::doRoundDown(rep& mantissa, int& exponent) +Number::Guard::doRoundDown( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa) { auto r = round(); if (r == 1 || (r == 0 && (mantissa & 1) == 1)) @@ -198,20 +289,27 @@ Number::Guard::doRoundDown(rep& mantissa, int& exponent) --exponent; } } - if (exponent < minExponent) - { - mantissa = 0; - exponent = Number{}.exponent_; - } + bringIntoRange(negative, mantissa, exponent, minMantissa); } // Modify the result to the correctly rounded value void -Number::Guard::doRound(rep& drops) +Number::Guard::doRound(rep& drops, std::string location) { auto r = round(); if (r == 1 || (r == 0 && (drops & 1) == 1)) { + if (drops >= maxRep) + { + static_assert(sizeof(internalrep) == sizeof(rep)); + // This should be impossible, because it's impossible to represent + // "maxRep + 0.6" in Number, regardless of the scale. There aren't + // enough digits available. You'd either get a mantissa of "maxRep" + // or "(maxRep + 1) / 10", neither of which will round up when + // converting to rep, though the latter might overflow _before_ + // rounding. + throw std::overflow_error(location); // LCOV_EXCL_LINE + } ++drops; } if (is_negative()) @@ -220,20 +318,88 @@ Number::Guard::doRound(rep& drops) // Number -constexpr Number one{1000000000000000, -15, Number::unchecked{}}; - -void -Number::normalize() +// Safely convert rep (int64) mantissa to internalrep (uint64). If the rep is +// negative, returns the positive value. This takes a little extra work because +// converting std::numeric_limits::min() flirts with UB, and can +// vary across compilers. +Number::internalrep +Number::externalToInternal(rep mantissa) { + // If the mantissa is already positive, just return it + if (mantissa >= 0) + return mantissa; + // If the mantissa is negative, but fits within the positive range of rep, + // return it negated + if (mantissa >= -std::numeric_limits::max()) + return -mantissa; + + // If the mantissa doesn't fit within the positive range, convert to + // int128_t, negate that, and cast it back down to the internalrep + // In practice, this is only going to cover the case of + // std::numeric_limits::min(). + int128_t temp = mantissa; + return static_cast(-temp); +} + +constexpr Number +Number::oneSmall() +{ + return Number{ + false, + Number::smallRange.min, + -Number::smallRange.log, + Number::unchecked{}}; +}; + +constexpr Number oneSml = Number::oneSmall(); + +constexpr Number +Number::oneLarge() +{ + return Number{ + false, + Number::largeRange.min, + -Number::largeRange.log, + Number::unchecked{}}; +}; + +constexpr Number oneLrg = Number::oneLarge(); + +Number +Number::one() +{ + if (&range_.get() == &smallRange) + return oneSml; + XRPL_ASSERT(&range_.get() == &largeRange, "Number::one() : valid range_"); + return oneLrg; +} + +// Use the member names in this static function for now so the diff is cleaner +// TODO: Rename the function parameters to get rid of the "_" suffix +template +void +doNormalize( + bool& negative, + T& mantissa_, + int& exponent_, + MantissaRange::rep const& minMantissa, + MantissaRange::rep const& maxMantissa) +{ + auto constexpr minExponent = Number::minExponent; + auto constexpr maxExponent = Number::maxExponent; + auto constexpr maxRep = Number::maxRep; + + using Guard = Number::Guard; + + constexpr Number zero = Number{}; if (mantissa_ == 0) { - *this = Number{}; + mantissa_ = zero.mantissa_; + exponent_ = zero.exponent_; + negative = zero.negative_; return; } - bool const negative = (mantissa_ < 0); - auto m = static_cast>(mantissa_); - if (negative) - m = -m; + auto m = mantissa_; while ((m < minMantissa) && (exponent_ > minExponent)) { m *= 10; @@ -250,57 +416,161 @@ Number::normalize() m /= 10; ++exponent_; } - mantissa_ = m; - if ((exponent_ < minExponent) || (mantissa_ < minMantissa)) + if ((exponent_ < minExponent) || (m < minMantissa)) { - *this = Number{}; + mantissa_ = zero.mantissa_; + exponent_ = zero.exponent_; + negative = zero.negative_; return; } - g.doRoundUp(mantissa_, exponent_, "Number::normalize 2"); + // When using the largeRange, "m" needs fit within an int64, even if + // the final mantissa_ is going to end up larger to fit within the + // MantissaRange. Cut it down here so that the rounding will be done while + // it's smaller. + // + // Example: 9,900,000,000,000,123,456 > 9,223,372,036,854,775,807, + // so "m" will be modified to 990,000,000,000,012,345. Then that value + // will be rounded to 990,000,000,000,012,345 or + // 990,000,000,000,012,346, depending on the rounding mode. Finally, + // mantissa_ will be "m*10" so it fits within the range, and end up as + // 9,900,000,000,000,123,450 or 9,900,000,000,000,123,460. + // mantissa() will return mantissa_ / 10, and exponent() will return + // exponent_ + 1. + if (m > maxRep) + { + if (exponent_ >= maxExponent) + throw std::overflow_error("Number::normalize 1.5"); + g.push(m % 10); + m /= 10; + ++exponent_; + } + // Before modification, m should be within the min/max range. After + // modification, it must be less than maxRep. In other words, the original + // value should have been no more than maxRep * 10. + // (maxRep * 10 > maxMantissa) + XRPL_ASSERT_PARTS( + m <= maxRep, + "xrpl::doNormalize", + "intermediate mantissa fits in int64"); + mantissa_ = m; - if (negative) - mantissa_ = -mantissa_; + g.doRoundUp( + negative, + mantissa_, + exponent_, + minMantissa, + maxMantissa, + "Number::normalize 2"); + XRPL_ASSERT_PARTS( + mantissa_ >= minMantissa && mantissa_ <= maxMantissa, + "xrpl::doNormalize", + "final mantissa fits in range"); +} + +template <> +void +Number::normalize( + bool& negative, + uint128_t& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa) +{ + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); +} + +template <> +void +Number::normalize( + bool& negative, + unsigned long long& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa) +{ + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); +} + +template <> +void +Number::normalize( + bool& negative, + unsigned long& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa) +{ + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); +} + +void +Number::normalize() +{ + auto const& range = range_.get(); + normalize(negative_, mantissa_, exponent_, range.min, range.max); +} + +// Copy the number, but set a new exponent. Because the mantissa doesn't change, +// the result will be "mostly" normalized, but the exponent could go out of +// range. +Number +Number::shiftExponent(int exponentDelta) const +{ + XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::shiftExponent", "normalized"); + auto const newExponent = exponent_ + exponentDelta; + if (newExponent >= maxExponent) + throw std::overflow_error("Number::shiftExponent"); + if (newExponent < minExponent) + { + return Number{}; + } + Number const result{negative_, mantissa_, newExponent, unchecked{}}; + XRPL_ASSERT_PARTS( + result.isnormal(), + "xrpl::Number::shiftExponent", + "result is normalized"); + return result; } Number& Number::operator+=(Number const& y) { - if (y == Number{}) + constexpr Number zero = Number{}; + if (y == zero) return *this; - if (*this == Number{}) + if (*this == zero) { *this = y; return *this; } if (*this == -y) { - *this = Number{}; + *this = zero; return *this; } + XRPL_ASSERT( isnormal() && y.isnormal(), "xrpl::Number::operator+=(Number) : is normal"); - auto xm = mantissa(); - auto xe = exponent(); - int xn = 1; - if (xm < 0) - { - xm = -xm; - xn = -1; - } - auto ym = y.mantissa(); - auto ye = y.exponent(); - int yn = 1; - if (ym < 0) - { - ym = -ym; - yn = -1; - } + // *n = negative + // *s = sign + // *m = mantissa + // *e = exponent + + // Need to use uint128_t, because large mantissas can overflow when added + // together. + bool xn = negative_; + uint128_t xm = mantissa_; + auto xe = exponent_; + + bool yn = y.negative_; + uint128_t ym = y.mantissa_; + auto ye = y.exponent_; Guard g; if (xe < ye) { - if (xn == -1) + if (xn) g.set_negative(); do { @@ -311,7 +581,7 @@ Number::operator+=(Number const& y) } else if (xe > ye) { - if (yn == -1) + if (yn) g.set_negative(); do { @@ -320,16 +590,22 @@ Number::operator+=(Number const& y) ++ye; } while (xe > ye); } + + auto const& range = range_.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + if (xn == yn) { xm += ym; - if (xm > maxMantissa) + if (xm > maxMantissa || xm > maxRep) { g.push(xm % 10); xm /= 10; ++xe; } - g.doRoundUp(xm, xe, "Number::addition overflow"); + g.doRoundUp( + xn, xm, xe, minMantissa, maxMantissa, "Number::addition overflow"); } else { @@ -343,16 +619,19 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - while (xm < minMantissa) + while (xm < minMantissa && xm * 10 <= maxRep) { xm *= 10; xm -= g.pop(); --xe; } - g.doRoundDown(xm, xe); + g.doRoundDown(xn, xm, xe, minMantissa); } - mantissa_ = xm * xn; + + negative_ = xn; + mantissa_ = static_cast(xm); exponent_ = xe; + normalize(); return *this; } @@ -387,39 +666,42 @@ divu10(uint128_t& u) Number& Number::operator*=(Number const& y) { - if (*this == Number{}) + constexpr Number zero = Number{}; + if (*this == zero) return *this; - if (y == Number{}) + if (y == zero) { *this = y; return *this; } - XRPL_ASSERT( - isnormal() && y.isnormal(), - "xrpl::Number::operator*=(Number) : is normal"); - auto xm = mantissa(); - auto xe = exponent(); - int xn = 1; - if (xm < 0) - { - xm = -xm; - xn = -1; - } - auto ym = y.mantissa(); - auto ye = y.exponent(); - int yn = 1; - if (ym < 0) - { - ym = -ym; - yn = -1; - } + // *n = negative + // *s = sign + // *m = mantissa + // *e = exponent + + bool xn = negative_; + int xs = xn ? -1 : 1; + internalrep xm = mantissa_; + auto xe = exponent_; + + bool yn = y.negative_; + int ys = yn ? -1 : 1; + internalrep ym = y.mantissa_; + auto ye = y.exponent_; + auto zm = uint128_t(xm) * uint128_t(ym); auto ze = xe + ye; - auto zn = xn * yn; + auto zs = xs * ys; + bool zn = (zs == -1); Guard g; - if (zn == -1) + if (zn) g.set_negative(); - while (zm > maxMantissa) + + auto const& range = range_.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + + while (zm > maxMantissa || zm > maxRep) { // The following is optimization for: // g.push(static_cast(zm % 10)); @@ -427,61 +709,129 @@ Number::operator*=(Number const& y) g.push(divu10(zm)); ++ze; } - xm = static_cast(zm); + xm = static_cast(zm); xe = ze; g.doRoundUp( + zn, xm, xe, + minMantissa, + maxMantissa, "Number::multiplication overflow : exponent is " + std::to_string(xe)); - mantissa_ = xm * zn; + negative_ = zn; + mantissa_ = xm; exponent_ = xe; - XRPL_ASSERT( - isnormal() || *this == Number{}, - "xrpl::Number::operator*=(Number) : result is normal"); + + normalize(); return *this; } Number& Number::operator/=(Number const& y) { - if (y == Number{}) + constexpr Number zero = Number{}; + if (y == zero) throw std::overflow_error("Number: divide by 0"); - if (*this == Number{}) + if (*this == zero) return *this; - int np = 1; - auto nm = mantissa(); - auto ne = exponent(); - if (nm < 0) + // n* = numerator + // d* = denominator + // *p = negative (positive?) + // *s = sign + // *m = mantissa + // *e = exponent + + bool np = negative_; + int ns = (np ? -1 : 1); + auto nm = mantissa_; + auto ne = exponent_; + + bool dp = y.negative_; + int ds = (dp ? -1 : 1); + auto dm = y.mantissa_; + auto de = y.exponent_; + + auto const& range = range_.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + + // Shift by 10^17 gives greatest precision while not overflowing + // uint128_t or the cast back to int64_t + // TODO: Can/should this be made bigger for largeRange? + // log(2^128,10) ~ 38.5 + // largeRange.log = 18, fits in 10^19 + // f can be up to 10^(38-19) = 10^19 safely + static_assert(smallRange.log == 15); + static_assert(largeRange.log == 18); + bool small = Number::getMantissaScale() == MantissaRange::small; + uint128_t const f = + small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL; + XRPL_ASSERT_PARTS( + f >= minMantissa * 10, "Number::operator/=", "factor expected size"); + + // unsigned denominator + auto const dmu = static_cast(dm); + // correctionFactor can be anything between 10 and f, depending on how much + // extra precision we want to only use for rounding with the + // largeRange. Three digits seems like plenty, and is more than + // the smallRange uses. + uint128_t const correctionFactor = 1'000; + + auto const numerator = uint128_t(nm) * f; + + auto zm = numerator / dmu; + auto ze = ne - de - (small ? 17 : 19); + bool zn = (ns * ds) < 0; + if (!small) { - nm = -nm; - np = -1; + // Virtually multiply numerator by correctionFactor. Since that would + // overflow in the existing uint128_t, we'll do that part separately. + // The math for this would work for small mantissas, but we need to + // preserve existing behavior. + // + // Consider: + // ((numerator * correctionFactor) / dmu) / correctionFactor + // = ((numerator / dmu) * correctionFactor) / correctionFactor) + // + // But that assumes infinite precision. With integer math, this is + // equivalent to + // + // = ((numerator / dmu * correctionFactor) + // + ((numerator % dmu) * correctionFactor) / dmu) / correctionFactor + // + // We have already set `mantissa_ = numerator / dmu`. Now we + // compute `remainder = numerator % dmu`, and if it is + // nonzero, we do the rest of the arithmetic. If it's zero, we can skip + // it. + auto const remainder = (numerator % dmu); + if (remainder != 0) + { + zm *= correctionFactor; + auto const correction = remainder * correctionFactor / dmu; + zm += correction; + // divide by 1000 by moving the exponent, so we don't lose the + // integer value we just computed + ze -= 3; + } } - int dp = 1; - auto dm = y.mantissa(); - auto de = y.exponent(); - if (dm < 0) - { - dm = -dm; - dp = -1; - } - // Shift by 10^17 gives greatest precision while not overflowing uint128_t - // or the cast back to int64_t - uint128_t const f = 100'000'000'000'000'000; - mantissa_ = static_cast(uint128_t(nm) * f / uint128_t(dm)); - exponent_ = ne - de - 17; - mantissa_ *= np * dp; - normalize(); + normalize(zn, zm, ze, minMantissa, maxMantissa); + negative_ = zn; + mantissa_ = static_cast(zm); + exponent_ = ze; + XRPL_ASSERT_PARTS( + isnormal(), "xrpl::Number::operator/=", "result is normalized"); + return *this; } Number::operator rep() const { - rep drops = mantissa_; - int offset = exponent_; + rep drops = mantissa(); + int offset = exponent(); Guard g; if (drops != 0) { - if (drops < 0) + if (negative_) { g.set_negative(); drops = -drops; @@ -493,11 +843,11 @@ Number::operator rep() const } for (; offset > 0; --offset) { - if (drops > std::numeric_limits::max() / 10) + if (drops > maxRep / 10) throw std::overflow_error("Number::operator rep() overflow"); drops *= 10; } - g.doRound(drops); + g.doRound(drops, "Number::operator rep() rounding overflow"); } return drops; } @@ -524,34 +874,37 @@ std::string to_string(Number const& amount) { // keep full internal accuracy, but make more human friendly if possible - if (amount == Number{}) + constexpr Number zero = Number{}; + if (amount == zero) return "0"; - auto const exponent = amount.exponent(); - auto mantissa = amount.mantissa(); + auto exponent = amount.exponent_; + auto mantissa = amount.mantissa_; + bool const negative = amount.negative_; // Use scientific notation for exponents that are too small or too large - if (((exponent != 0) && ((exponent < -25) || (exponent > -5)))) + auto const rangeLog = Number::mantissaLog(); + if (((exponent != 0) && + ((exponent < -(rangeLog + 10)) || (exponent > -(rangeLog - 10))))) { - std::string ret = std::to_string(mantissa); + while (mantissa != 0 && mantissa % 10 == 0 && + exponent < Number::maxExponent) + { + mantissa /= 10; + ++exponent; + } + std::string ret = negative ? "-" : ""; + ret.append(std::to_string(mantissa)); ret.append(1, 'e'); ret.append(std::to_string(exponent)); return ret; } - bool negative = false; - - if (mantissa < 0) - { - mantissa = -mantissa; - negative = true; - } - XRPL_ASSERT( exponent + 43 > 0, "xrpl::to_string(Number) : minimum exponent"); - ptrdiff_t const pad_prefix = 27; - ptrdiff_t const pad_suffix = 23; + ptrdiff_t const pad_prefix = rangeLog + 12; + ptrdiff_t const pad_suffix = rangeLog + 8; std::string const raw_value(std::to_string(mantissa)); std::string val; @@ -561,7 +914,7 @@ to_string(Number const& amount) val.append(raw_value); val.append(pad_suffix, '0'); - ptrdiff_t const offset(exponent + 43); + ptrdiff_t const offset(exponent + pad_prefix + rangeLog + 1); auto pre_from(val.begin()); auto const pre_to(val.begin() + offset); @@ -621,7 +974,7 @@ Number power(Number const& f, unsigned n) { if (n == 0) - return one; + return Number::one(); if (n == 1) return f; auto r = power(f, n / 2); @@ -685,6 +1038,9 @@ lg(Number const& x) Number root(Number f, unsigned d) { + constexpr Number zero = Number{}; + auto const one = Number::one(); + if (f == one || d == 1) return f; if (d == 0) @@ -692,16 +1048,16 @@ root(Number f, unsigned d) if (f == -one) return one; if (abs(f) < one) - return Number{}; + return zero; throw std::overflow_error("Number::root infinity"); } - if (f < Number{} && d % 2 == 0) + if (f < zero && d % 2 == 0) throw std::overflow_error("Number::root nan"); - if (f == Number{}) + if (f == zero) return f; // Scale f into the range (0, 1) such that f's exponent is a multiple of d - auto e = f.exponent() + 16; + auto e = f.exponent_ + Number::mantissaLog() + 1; auto const di = static_cast(d); auto ex = [e = e, di = di]() // Euclidean remainder of e/d { @@ -712,9 +1068,12 @@ root(Number f, unsigned d) return di - k2; }(); e += ex; - f = Number{f.mantissa(), f.exponent() - e}; // f /= 10^e; + f = f.shiftExponent(-e); // f /= 10^e; + + XRPL_ASSERT_PARTS( + f.isnormal(), "xrpl::root(Number, unsigned)", "f is normalized"); bool neg = false; - if (f < Number{}) + if (f < zero) { neg = true; f = -f; @@ -744,24 +1103,33 @@ root(Number f, unsigned d) } while (r != rm1 && r != rm2); // return r * 10^(e/d) to reverse scaling - return Number{r.mantissa(), r.exponent() + e / di}; + auto const result = r.shiftExponent(e / di); + XRPL_ASSERT_PARTS( + result.isnormal(), + "xrpl::root(Number, unsigned)", + "result is normalized"); + return result; } Number root2(Number f) { + constexpr Number zero = Number{}; + auto const one = Number::one(); + if (f == one) return f; - if (f < Number{}) + if (f < zero) throw std::overflow_error("Number::root nan"); - if (f == Number{}) + if (f == zero) return f; // Scale f into the range (0, 1) such that f's exponent is a multiple of d - auto e = f.exponent() + 16; + auto e = f.exponent_ + Number::mantissaLog() + 1; if (e % 2 != 0) ++e; - f = Number{f.mantissa(), f.exponent() - e}; // f /= 10^e; + f = f.shiftExponent(-e); // f /= 10^e; + XRPL_ASSERT_PARTS(f.isnormal(), "xrpl::root2(Number)", "f is normalized"); // Quadratic least squares curve fit of f^(1/d) in the range [0, 1] auto const D = 105; @@ -782,7 +1150,11 @@ root2(Number f) } while (r != rm1 && r != rm2); // return r * 10^(e/2) to reverse scaling - return Number{r.mantissa(), r.exponent() + e / 2}; + auto const result = r.shiftExponent(e / 2); + XRPL_ASSERT_PARTS( + result.isnormal(), "xrpl::root2(Number)", "result is normalized"); + + return result; } // Returns f^(n/d) @@ -790,6 +1162,9 @@ root2(Number f) Number power(Number const& f, unsigned n, unsigned d) { + constexpr Number zero = Number{}; + auto const one = Number::one(); + if (f == one) return f; auto g = std::gcd(n, d); @@ -800,7 +1175,7 @@ power(Number const& f, unsigned n, unsigned d) if (f == -one) return one; if (abs(f) < one) - return Number{}; + return zero; // abs(f) > one throw std::overflow_error("Number::power infinity"); } @@ -808,7 +1183,7 @@ power(Number const& f, unsigned n, unsigned d) return one; n /= g; d /= g; - if ((n % 2) == 1 && (d % 2) == 0 && f < Number{}) + if ((n % 2) == 1 && (d % 2) == 0 && f < zero) throw std::overflow_error("Number::power nan"); return root(power(f, n), d); } diff --git a/src/xrpld/app/paths/Credit.cpp b/src/libxrpl/ledger/Credit.cpp similarity index 100% rename from src/xrpld/app/paths/Credit.cpp rename to src/libxrpl/ledger/Credit.cpp diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 329d3cfcae..16ec23ffab 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -450,9 +451,8 @@ getTrustLineBalance( amount.clear(Issue{currency, issuer}); } - JLOG(j.trace()) << "getTrustLineBalance:" - << " account=" << to_string(account) - << " amount=" << amount.getFullText(); + JLOG(j.trace()) << "getTrustLineBalance:" << " account=" + << to_string(account) << " amount=" << amount.getFullText(); return view.balanceHook(account, issuer, amount); } @@ -464,7 +464,8 @@ accountHolds( Currency const& currency, AccountID const& issuer, FreezeHandling zeroIfFrozen, - beast::Journal j) + beast::Journal j, + SpendableHandling includeFullBalance) { STAmount amount; if (isXRP(currency)) @@ -472,11 +473,19 @@ accountHolds( return {xrpLiquid(view, account, 0, j)}; } + bool const returnSpendable = (includeFullBalance == shFULL_BALANCE); + if (returnSpendable && account == issuer) + // If the account is the issuer, then their limit is effectively + // infinite + return STAmount{ + Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset}; + // IOU: Return balance on trust line modulo freeze SLE::const_pointer const sle = getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); - return getTrustLineBalance(view, sle, account, currency, issuer, false, j); + return getTrustLineBalance( + view, sle, account, currency, issuer, returnSpendable, j); } STAmount @@ -485,10 +494,17 @@ accountHolds( AccountID const& account, Issue const& issue, FreezeHandling zeroIfFrozen, - beast::Journal j) + beast::Journal j, + SpendableHandling includeFullBalance) { return accountHolds( - view, account, issue.currency, issue.account, zeroIfFrozen, j); + view, + account, + issue.currency, + issue.account, + zeroIfFrozen, + j, + includeFullBalance); } STAmount @@ -498,8 +514,28 @@ accountHolds( MPTIssue const& mptIssue, FreezeHandling zeroIfFrozen, AuthHandling zeroIfUnauthorized, - beast::Journal j) + beast::Journal j, + SpendableHandling includeFullBalance) { + bool const returnSpendable = (includeFullBalance == shFULL_BALANCE); + + if (returnSpendable && account == mptIssue.getIssuer()) + { + // if the account is the issuer, and the issuance exists, their limit is + // the issuance limit minus the outstanding value + auto const issuance = + view.read(keylet::mptIssuance(mptIssue.getMptID())); + + if (!issuance) + { + return STAmount{mptIssue}; + } + return STAmount{ + mptIssue, + issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - + issuance->at(sfOutstandingAmount)}; + } + STAmount amount; auto const sleMpt = @@ -547,108 +583,27 @@ accountHolds( Asset const& asset, FreezeHandling zeroIfFrozen, AuthHandling zeroIfUnauthorized, - beast::Journal j) + beast::Journal j, + SpendableHandling includeFullBalance) { return std::visit( - [&](auto const& value) { - if constexpr (std::is_same_v< - std::remove_cvref_t, - Issue>) + [&](TIss const& value) { + if constexpr (std::is_same_v) { - return accountHolds(view, account, value, zeroIfFrozen, j); + return accountHolds( + view, account, value, zeroIfFrozen, j, includeFullBalance); } - return accountHolds( - view, account, value, zeroIfFrozen, zeroIfUnauthorized, j); - }, - asset.value()); -} - -STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer, - FreezeHandling zeroIfFrozen, - beast::Journal j) -{ - if (isXRP(currency)) - return accountHolds(view, account, currency, issuer, zeroIfFrozen, j); - - if (account == issuer) - // If the account is the issuer, then their limit is effectively - // infinite - return STAmount{ - Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset}; - - // IOU: Return balance on trust line modulo freeze - SLE::const_pointer const sle = - getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); - - return getTrustLineBalance(view, sle, account, currency, issuer, true, j); -} - -STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Issue const& issue, - FreezeHandling zeroIfFrozen, - beast::Journal j) -{ - return accountSpendable( - view, account, issue.currency, issue.account, zeroIfFrozen, j); -} - -STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j) -{ - if (account == mptIssue.getIssuer()) - { - // if the account is the issuer, and the issuance exists, their limit is - // the issuance limit minus the outstanding value - auto const issuance = - view.read(keylet::mptIssuance(mptIssue.getMptID())); - - if (!issuance) - { - return STAmount{mptIssue}; - } - return STAmount{ - mptIssue, - issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - - issuance->at(sfOutstandingAmount)}; - } - - return accountHolds( - view, account, mptIssue, zeroIfFrozen, zeroIfUnauthorized, j); -} - -[[nodiscard]] STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Asset const& asset, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j) -{ - return std::visit( - [&](auto const& value) { - if constexpr (std::is_same_v< - std::remove_cvref_t, - Issue>) + else if constexpr (std::is_same_v) { - return accountSpendable(view, account, value, zeroIfFrozen, j); + return accountHolds( + view, + account, + value, + zeroIfFrozen, + zeroIfUnauthorized, + j, + includeFullBalance); } - return accountSpendable( - view, account, value, zeroIfFrozen, zeroIfUnauthorized, j); }, asset.value()); } @@ -744,8 +699,7 @@ xrpLiquid( STAmount const amount = (balance < reserve) ? STAmount{0} : balance - reserve; - JLOG(j.trace()) << "accountHolds:" - << " account=" << to_string(id) + JLOG(j.trace()) << "accountHolds:" << " account=" << to_string(id) << " amount=" << amount.getFullText() << " fullBalance=" << fullBalance.getFullText() << " balance=" << balance.getFullText() @@ -1151,7 +1105,7 @@ adjustOwnerCount( std::function describeOwnerDir(AccountID const& account) { - return [&account](std::shared_ptr const& sle) { + return [account](std::shared_ptr const& sle) { (*sle)[sfOwner] = account; }; } @@ -1205,8 +1159,7 @@ getPseudoAccountFields() // LCOV_EXCL_START LogicError( "xrpl::getPseudoAccountFields : unable to find account root " - "ledger " - "format"); + "ledger format"); // LCOV_EXCL_STOP } auto const& soTemplate = ar->getSOTemplate(); @@ -1342,12 +1295,58 @@ checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag) return tesSUCCESS; } +/* + * Checks if a withdrawal amount into the destination account exceeds + * any applicable receiving limit. + * Called by VaultWithdraw and LoanBrokerCoverWithdraw. + * + * IOU : Performs the trustline check against the destination account's + * credit limit to ensure the account's trust maximum is not exceeded. + * + * MPT: The limit check is effectively skipped (returns true). This is + * because MPT MaximumAmount relates to token supply, and withdrawal does not + * involve minting new tokens that could exceed the global cap. + * On withdrawal, tokens are simply transferred from the vault's pseudo-account + * to the destination account. Since no new MPT tokens are minted during this + * transfer, the withdrawal cannot violate the MPT MaximumAmount/supply cap + * even if `from` is the issuer. + */ +static TER +withdrawToDestExceedsLimit( + ReadView const& view, + AccountID const& from, + AccountID const& to, + STAmount const& amount) +{ + auto const& issuer = amount.getIssuer(); + if (from == to || to == issuer || isXRP(issuer)) + return tesSUCCESS; + + return std::visit( + [&](TIss const& issue) -> TER { + if constexpr (std::is_same_v) + { + auto const& currency = issue.currency; + auto const owed = creditBalance(view, to, issuer, currency); + if (owed <= beast::zero) + { + auto const limit = creditLimit(view, to, issuer, currency); + if (-owed >= limit || amount > (limit + owed)) + return tecNO_LINE; + } + } + return tesSUCCESS; + }, + amount.asset().value()); +} + [[nodiscard]] TER canWithdraw( - AccountID const& from, ReadView const& view, + AccountID const& from, AccountID const& to, SLE::const_ref toSle, + STAmount const& amount, bool hasDestinationTag) { if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag)) @@ -1362,19 +1361,20 @@ canWithdraw( return tecNO_PERMISSION; } - return tesSUCCESS; + return withdrawToDestExceedsLimit(view, from, to, amount); } [[nodiscard]] TER canWithdraw( - AccountID const& from, ReadView const& view, + AccountID const& from, AccountID const& to, + STAmount const& amount, bool hasDestinationTag) { auto const toSle = view.read(keylet::account(to)); - return canWithdraw(from, view, to, toSle, hasDestinationTag); + return canWithdraw(view, from, to, toSle, amount, hasDestinationTag); } [[nodiscard]] TER @@ -1383,7 +1383,8 @@ canWithdraw(ReadView const& view, STTx const& tx) auto const from = tx[sfAccount]; auto const to = tx[~sfDestination].value_or(from); - return canWithdraw(from, view, to, tx.isFieldPresent(sfDestinationTag)); + return canWithdraw( + view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag)); } TER diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index dc56987f3a..b5e2354165 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include #include #include @@ -20,7 +22,7 @@ namespace BuildInfo { char const* const versionString = "3.2.0-b0" // clang-format on -#if defined(DEBUG) || defined(SANITIZER) +#if defined(DEBUG) || defined(SANITIZERS) "+" #ifdef GIT_COMMIT_HASH GIT_COMMIT_HASH @@ -28,13 +30,13 @@ char const* const versionString = "3.2.0-b0" #endif #ifdef DEBUG "DEBUG" -#ifdef SANITIZER +#ifdef SANITIZERS "." #endif #endif -#ifdef SANITIZER - BOOST_PP_STRINGIZE(SANITIZER) // cspell: disable-line +#ifdef SANITIZERS + BOOST_PP_STRINGIZE(SANITIZERS) // cspell: disable-line #endif #endif diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp index b33eaa67b7..297c2bac12 100644 --- a/src/libxrpl/protocol/IOUAmount.cpp +++ b/src/libxrpl/protocol/IOUAmount.cpp @@ -1,8 +1,11 @@ +#include +// Do not remove. Forces IOUAmount.h to stay first, to verify it can compile +// without any hidden dependencies #include #include #include #include -#include +#include #include @@ -39,6 +42,26 @@ setSTNumberSwitchover(bool v) *getStaticSTNumberSwitchover() = v; } +/* The range for the mantissa when normalized */ +// log(2^63,10) ~ 18.96 +// +static std::int64_t constexpr minMantissa = STAmount::cMinValue; +static std::int64_t constexpr maxMantissa = STAmount::cMaxValue; +/* The range for the exponent when normalized */ +static int constexpr minExponent = STAmount::cMinOffset; +static int constexpr maxExponent = STAmount::cMaxOffset; + +IOUAmount +IOUAmount::fromNumber(Number const& number) +{ + // Need to create a default IOUAmount and assign directly so it doesn't try + // to normalize, which calls fromNumber + IOUAmount result{}; + std::tie(result.mantissa_, result.exponent_) = + number.normalizeToRange(minMantissa, maxMantissa); + return result; +} + IOUAmount IOUAmount::minPositiveAmount() { @@ -57,8 +80,7 @@ IOUAmount::normalize() if (getSTNumberSwitchover()) { Number const v{mantissa_, exponent_}; - mantissa_ = v.mantissa(); - exponent_ = v.exponent(); + *this = fromNumber(v); if (exponent_ > maxExponent) Throw("value overflow"); if (exponent_ < minExponent) @@ -99,8 +121,7 @@ IOUAmount::normalize() mantissa_ = -mantissa_; } -IOUAmount::IOUAmount(Number const& other) - : mantissa_(other.mantissa()), exponent_(other.exponent()) +IOUAmount::IOUAmount(Number const& other) : IOUAmount(fromNumber(other)) { if (exponent_ > maxExponent) Throw("value overflow"); @@ -286,8 +307,7 @@ mulRatio( { if (!result) { - return IOUAmount( - -IOUAmount::minMantissa, IOUAmount::minExponent); + return IOUAmount(-minMantissa, minExponent); } // This subtraction cannot underflow because `result` is not zero return IOUAmount(result.mantissa() - 1, result.exponent()); diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index b858a31e3e..ca5bf35e8b 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -49,6 +49,12 @@ Issue::native() const return *this == xrpIssue(); } +bool +Issue::integral() const +{ + return native(); +} + bool isConsistent(Issue const& ac) { diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index b1f2c2d631..3710322699 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -1,10 +1,13 @@ +#include +// Do not remove. Forces Rules.h to stay first, to verify it can compile +// without any hidden dependencies #include +#include #include #include #include #include #include -#include #include #include @@ -33,6 +36,15 @@ getCurrentTransactionRules() void setCurrentTransactionRules(std::optional r) { + // Make global changes associated with the rules before the value is moved. + // Push the appropriate setting, instead of having the class pull every time + // the value is needed. That could get expensive fast. + bool enableLargeNumbers = !r || + (r->enabled(featureSingleAssetVault) || + r->enabled(featureLendingProtocol)); + Number::setMantissaScale( + enableLargeNumbers ? MantissaRange::large : MantissaRange::small); + *getCurrentTransactionRulesRef() = std::move(r); } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index ebccfb3e64..ec60971e63 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -310,7 +312,8 @@ STAmount& STAmount::operator=(IOUAmount const& iou) { XRPL_ASSERT( - native() == false, "xrpl::STAmount::operator=(IOUAmount) : is not XRP"); + integral() == false, + "xrpl::STAmount::operator=(IOUAmount) : is not integral"); mOffset = iou.exponent(); mIsNegative = iou < beast::zero; if (mIsNegative) @@ -320,6 +323,26 @@ STAmount::operator=(IOUAmount const& iou) return *this; } +STAmount& +STAmount::operator=(Number const& number) +{ + if (!getCurrentTransactionRules() || + isFeatureEnabled(featureSingleAssetVault) || + isFeatureEnabled(featureLendingProtocol)) + { + *this = fromNumber(mAsset, number); + } + else + { + auto const originalMantissa = number.mantissa(); + mIsNegative = originalMantissa < 0; + mValue = mIsNegative ? -originalMantissa : originalMantissa; + mOffset = number.exponent(); + } + canonicalize(); + return *this; +} + //------------------------------------------------------------------------------ // // Operators @@ -849,11 +872,11 @@ STAmount::canonicalize() if (getSTNumberSwitchover()) { - Number num( - mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); + Number num(mIsNegative, mValue, mOffset, Number::unchecked{}); auto set = [&](auto const& val) { - mIsNegative = val.value() < 0; - mValue = mIsNegative ? -val.value() : val.value(); + auto const value = val.value(); + mIsNegative = value < 0; + mValue = mIsNegative ? -value : value; }; if (native()) set(XRPAmount{num}); @@ -1323,7 +1346,7 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) if (getSTNumberSwitchover()) { auto const r = Number{v1} * Number{v2}; - return STAmount{asset, r.mantissa(), r.exponent()}; + return STAmount{asset, r}; } std::uint64_t value1 = v1.mantissa(); @@ -1471,6 +1494,10 @@ roundToScale( if (value.integral()) return value; + // Nothing to do for zero. + if (value == beast::zero) + return value; + // If the value's exponent is greater than or equal to the scale, then // rounding will do nothing, and might even lose precision, so just return // the value. diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index f85bb48e0a..2f2dae7493 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -1,9 +1,13 @@ +#include +// Do not remove. Keep STNumber.h first #include #include #include +#include #include +#include #include -#include +#include #include #include @@ -17,11 +21,11 @@ namespace xrpl { STNumber::STNumber(SField const& field, Number const& value) - : STBase(field), value_(value) + : STTakesAsset(field), value_(value) { } -STNumber::STNumber(SerialIter& sit, SField const& field) : STBase(field) +STNumber::STNumber(SerialIter& sit, SField const& field) : STTakesAsset(field) { // We must call these methods in separate statements // to guarantee their order of execution. @@ -42,6 +46,19 @@ STNumber::getText() const return to_string(value_); } +void +STNumber::associateAsset(Asset const& a) +{ + STTakesAsset::associateAsset(a); + + XRPL_ASSERT_PARTS( + getFName().shouldMeta(SField::sMD_NeedsAsset), + "STNumber::associateAsset", + "field needs asset"); + + roundToAsset(a, value_); +} + void STNumber::add(Serializer& s) const { @@ -49,8 +66,49 @@ STNumber::add(Serializer& s) const XRPL_ASSERT( getFName().fieldType == getSType(), "xrpl::STNumber::add : field type match"); - s.add64(value_.mantissa()); - s.add32(value_.exponent()); + + auto value = value_; + auto const mantissa = value.mantissa(); + auto const exponent = value.exponent(); + + SField const& field = getFName(); + if (field.shouldMeta(SField::sMD_NeedsAsset)) + { + // asset is defined in the STTakesAsset base class + if (asset_) + { + // The number should be rounded to the asset's precision, but round + // it here if it has an asset assigned. + roundToAsset(*asset_, value); + XRPL_ASSERT_PARTS( + value_ == value, + "xrpl::STNumber::add", + "value is already rounded"); + } + else + { +#if !NDEBUG + // There are circumstances where an already-rounded Number is + // serialized without being touched by a transactor, and thus + // without an asset. We can't know if it's rounded, because it could + // represent _anything_, particularly when serializing user-provided + // Json. Regardless, the only time we should be serializing an + // STNumber is when the scale is large. + XRPL_ASSERT_PARTS( + Number::getMantissaScale() == MantissaRange::large, + "xrpl::STNumber::add", + "STNumber only used with large mantissa scale"); +#endif + } + } + + XRPL_ASSERT_PARTS( + mantissa <= std::numeric_limits::max() && + mantissa >= std::numeric_limits::min(), + "xrpl::STNumber::add", + "mantissa in valid range"); + s.add64(mantissa); + s.add32(exponent); } Number const& @@ -179,20 +237,30 @@ numberFromJson(SField const& field, Json::Value const& value) else if (value.isString()) { parts = partsFromString(value.asString()); - // Only strings can represent out-of-range values. - if (parts.mantissa > std::numeric_limits::max()) - Throw("too high"); + + XRPL_ASSERT_PARTS( + !getCurrentTransactionRules(), + "xrpld::numberFromJson", + "Not in a Transactor context"); + + // Number mantissas are much bigger than the allowable parsed values, so + // it can't be out of range. + static_assert( + std::numeric_limits::max() >= + std::numeric_limits::max()); } else { Throw("not a number"); } - std::int64_t mantissa = parts.mantissa; - if (parts.negative) - mantissa = -mantissa; - - return STNumber{field, Number{mantissa, parts.exponent}}; + return STNumber{ + field, + Number{ + parts.negative, + parts.mantissa, + parts.exponent, + Number::normalized{}}}; } } // namespace xrpl diff --git a/src/libxrpl/protocol/STTakesAsset.cpp b/src/libxrpl/protocol/STTakesAsset.cpp new file mode 100644 index 0000000000..d43e7b04a1 --- /dev/null +++ b/src/libxrpl/protocol/STTakesAsset.cpp @@ -0,0 +1,29 @@ +#include +// Do not remove. Force STTakesAsset.h first +#include + +namespace xrpl { + +void +associateAsset(SLE& sle, Asset const& asset) +{ + // Iterating by offset is the only way to get non-const references + for (int i = 0; i < sle.getCount(); ++i) + { + STBase& entry = sle.getIndex(i); + SField const& field = entry.getFName(); + if (field.shouldMeta(SField::sMD_NeedsAsset)) + { + auto const type = entry.getSType(); + // If the field is not set or present, skip it. + if (type == STI_NOTPRESENT) + continue; + // If the type doesn't downcast, then the flag shouldn't be on the + // SField + auto& ta = entry.downcast(); + ta.associateAsset(asset); + } + } +} + +} // namespace xrpl diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 93fda8fe34..52f05f9ed5 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2425,7 +2425,10 @@ class AMMClawback_test : public beast::unit_test::suite void run() override { - FeatureBitset const all = jtx::testable_amendments(); + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + FeatureBitset const all = jtx::testable_amendments() - + featureSingleAssetVault - featureLendingProtocol; testInvalidRequest(); testFeatureDisabled(all - featureAMMClawback); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 317f6cb63d..d1816df51b 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -26,6 +26,9 @@ namespace test { */ struct AMMExtended_test : public jtx::AMMTest { + // Use small Number mantissas for the life of this test. + NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::small}; + private: void testRmFundedOffer(FeatureBitset features) @@ -42,6 +45,7 @@ private: // funded and not used for the payment. using namespace jtx; + Env env{*this, features}; fund( @@ -1418,7 +1422,12 @@ private: testOffers() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; + testRmFundedOffer(all); testRmFundedOffer(all - fixAMMv1_1 - fixAMMv1_3); testEnforceNoRipple(all); @@ -3746,7 +3755,11 @@ private: testFlow() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; testFalseDry(all); testBookStep(all); @@ -3760,7 +3773,11 @@ private: testCrossingLimits() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; testStepLimit(all); testStepLimit(all - fixAMMv1_1 - fixAMMv1_3); } @@ -3769,7 +3786,11 @@ private: testDeliverMin() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; test_convert_all_of_an_asset(all); test_convert_all_of_an_asset(all - fixAMMv1_1 - fixAMMv1_3); } @@ -3777,7 +3798,12 @@ private: void testDepositAuth() { - testPayment(jtx::testable_amendments()); + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; + testPayment(all); testPayIOU(); } @@ -3785,7 +3811,11 @@ private: testFreeze() { using namespace test::jtx; - auto const sa = testable_amendments(); + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const sa{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; testRippleState(sa); testGlobalFreeze(sa); testOffersWhenFrozen(sa); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 905262a80b..0040058b53 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -30,7 +30,19 @@ namespace test { */ struct AMM_test : public jtx::AMMTest { + // Use small Number mantissas for the life of this test. + NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::small}; + private: + static FeatureBitset + testable_amendments() + { + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + return jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol; + } + void testInstanceCreate() { @@ -38,6 +50,7 @@ private: using namespace jtx; +#if NUMBERTODO // XRP to IOU, with featureSingleAssetVault testAMM( [&](AMM& ammAlice, Env&) { @@ -48,6 +61,7 @@ private: 0, {}, {testable_amendments() | featureSingleAssetVault}); +#endif // XRP to IOU, without featureSingleAssetVault testAMM( @@ -1365,8 +1379,8 @@ private: { testcase("Deposit"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; // Equal deposit: 1000000 tokens, 10% of the current pool testAMM([&](AMM& ammAlice, Env& env) { @@ -1384,15 +1398,14 @@ private: // equal asset deposit: unit test to exercise the rounding-down of // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations // The LPTokens need to have 16 significant digits and a fractional part - for (Number const deltaLPTokens : + for (Number const& deltaLPTokens : {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}}) { testAMM([&](AMM& ammAlice, Env& env) { // initial LPToken balance IOUAmount const initLPToken = ammAlice.getLPTokensBalance(); - IOUAmount const newLPTokens{ - deltaLPTokens.mantissa(), deltaLPTokens.exponent()}; + IOUAmount const newLPTokens{deltaLPTokens}; // carol performs a two-asset deposit ammAlice.deposit( @@ -1417,11 +1430,9 @@ private: Number const deltaXRP = fr * 1e10; Number const deltaUSD = fr * 1e4; - STAmount const depositUSD = - STAmount{USD, deltaUSD.mantissa(), deltaUSD.exponent()}; + STAmount const depositUSD = STAmount{USD, deltaUSD}; - STAmount const depositXRP = - STAmount{XRP, deltaXRP.mantissa(), deltaXRP.exponent()}; + STAmount const depositXRP = STAmount{XRP, deltaXRP}; // initial LPTokens (1e7) + newLPTokens BEAST_EXPECT(ammAlice.expectBalances( @@ -1487,7 +1498,7 @@ private: }); // Single deposit: 100000 tokens worth of XRP - testAMM([&](AMM& ammAlice, Env&) { + testAMM([&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 100'000, XRP(205)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'201), USD(10'000), IOUAmount{10'100'000, 0})); @@ -1668,8 +1679,8 @@ private: { testcase("Invalid Withdraw"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; testAMM( [&](AMM& ammAlice, Env& env) { @@ -2248,8 +2259,8 @@ private: { testcase("Withdraw"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current // pool @@ -2669,8 +2680,8 @@ private: testFeeVote() { testcase("Fee Vote"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; // One vote sets fee to 1%. testAMM([&](AMM& ammAlice, Env& env) { @@ -3014,6 +3025,10 @@ private: using namespace jtx; using namespace std::chrono; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + features = features - featureSingleAssetVault - featureLendingProtocol; + // Auction slot initially is owned by AMM creator, who pays 0 price. // Bid 110 tokens. Pay bidMin. @@ -3758,6 +3773,11 @@ private: testcase("Basic Payment"); using namespace jtx; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + features = features - featureSingleAssetVault - featureLendingProtocol - + featureLendingProtocol; + // Payment 100USD for 100XRP. // Force one path with tfNoRippleDirect. testAMM( @@ -4836,12 +4856,12 @@ private: testAmendment() { testcase("Amendment"); - using namespace jtx; FeatureBitset const all{testable_amendments()}; FeatureBitset const noAMM{all - featureAMM}; FeatureBitset const noNumber{all - fixUniversalNumber}; FeatureBitset const noAMMAndNumber{ all - featureAMM - fixUniversalNumber}; + using namespace jtx; for (auto const& feature : {noAMM, noNumber, noAMMAndNumber}) { @@ -6476,6 +6496,8 @@ private: Env env(*this, features, std::make_unique(&logs)); auto rules = env.current()->rules(); CurrentTransactionRulesGuard rg(rules); + NumberMantissaScaleGuard sg(MantissaRange::small); + for (auto const& t : tests) { auto getPool = [&](std::string const& v, bool isXRP) { @@ -7025,7 +7047,7 @@ private: {{xrpPool, iouPool}}, 889, std::nullopt, - {jtx::testable_amendments() | fixAMMv1_1}); + {testable_amendments() | fixAMMv1_1}); } void @@ -7566,6 +7588,7 @@ private: { auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); + NumberMantissaScaleGuard sg(MantissaRange::small); NumberRoundModeGuard g( env.enabled(fixAMMv1_3) ? Number::upward : Number::getround()); auto const res = root2(amount * amount2); @@ -7880,7 +7903,7 @@ private: void run() override { - FeatureBitset const all{jtx::testable_amendments()}; + FeatureBitset const all{testable_amendments()}; testInvalidInstance(); testInstanceCreate(); testInvalidDeposit(all); diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 68bf7e833b..67b0933ae2 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -427,6 +427,7 @@ class Batch_test : public beast::unit_test::suite auto const batchFee = batch::calcBatchFee(env, 0, 2); auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); tx1[jss::Fee] = "1.5"; + env.set_parse_failure_expected(true); try { env(batch::outer(alice, seq, batchFee, tfAllOrNothing), @@ -438,6 +439,7 @@ class Batch_test : public beast::unit_test::suite { BEAST_EXPECT(true); } + env.set_parse_failure_expected(false); } // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence diff --git a/src/test/app/EscrowSmart_test.cpp b/src/test/app/EscrowSmart_test.cpp index 1ff82dff40..ec173b2cbd 100644 --- a/src/test/app/EscrowSmart_test.cpp +++ b/src/test/app/EscrowSmart_test.cpp @@ -390,7 +390,7 @@ struct EscrowSmart_test : public beast::unit_test::suite // Tests whether the ledger index is >= 5 // getLedgerSqn() >= 5} auto const& wasmHex = ledgerSqnWasmHex; - std::uint32_t const allowance = 65; + std::uint32_t const allowance = 178; auto escrowCreate = escrow::create(alice, carol, XRP(1000)); auto [createFee, finishFee] = [&]() { Env env(*this, features); @@ -683,7 +683,7 @@ struct EscrowSmart_test : public beast::unit_test::suite { env.require(balance(alice, XRP(4000) - txnFees)); - auto const allowance = 1014; + auto const allowance = 1420; XRPAmount const finishFee = env.current()->fees().base + (allowance * env.current()->fees().gasPrice) / MICRO_DROPS_PER_DROP + @@ -727,7 +727,7 @@ struct EscrowSmart_test : public beast::unit_test::suite // Tests whether the ledger index is >= 5 // getLedgerSqn() >= 5} auto const& wasmHex = ledgerSqnWasmHex; - uint64_t const allowance = 65; + uint64_t const allowance = 178; auto escrowCreate = escrow::create(alice, carol, XRP(1000)); auto createFee = [&]() { Env env(*this, features); @@ -873,7 +873,7 @@ struct EscrowSmart_test : public beast::unit_test::suite auto const txMeta = env.meta(); if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed))) BEAST_EXPECTS( - txMeta->getFieldU32(sfGasUsed) == 38'054, + txMeta->getFieldU32(sfGasUsed) == 62'715, std::to_string(txMeta->getFieldU32(sfGasUsed))); if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) BEAST_EXPECT(txMeta->getFieldI32(sfWasmReturnCode) == 1); @@ -958,7 +958,7 @@ struct EscrowSmart_test : public beast::unit_test::suite env.close(); env.close(); - auto const allowance = 138'485; + auto const allowance = 182'903; auto const finishFee = env.current()->fees().base + (allowance * env.current()->fees().gasPrice) / MICRO_DROPS_PER_DROP + diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 955ca8f449..589a8b474e 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -559,12 +559,15 @@ struct EscrowToken_test : public beast::unit_test::suite env(pay(gw, bob, USD(1))); env.close(); + bool const largeMantissa = features[featureSingleAssetVault] || + features[featureLendingProtocol]; + // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, USD(1)), escrow::condition(escrow::cb1), escrow::finish_time(env.now() + 1s), fee(baseFee * 150), - ter(tecPRECISION_LOSS)); + ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS)); env.close(); } } @@ -2076,12 +2079,15 @@ struct EscrowToken_test : public beast::unit_test::suite env(pay(gw, bob, USD(1))); env.close(); + bool const largeMantissa = features[featureSingleAssetVault] || + features[featureLendingProtocol]; + // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, USD(1)), escrow::condition(escrow::cb1), escrow::finish_time(env.now() + 1s), fee(baseFee * 150), - ter(tecPRECISION_LOSS)); + ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS)); env.close(); auto const seq1 = env.seq(alice); @@ -3924,9 +3930,13 @@ public: { using namespace test::jtx; FeatureBitset const all{testable_amendments()}; - testIOUWithFeats(all); - testMPTWithFeats(all); - testMPTWithFeats(all - fixTokenEscrowV1); + for (FeatureBitset const& feats : + {all - featureSingleAssetVault - featureLendingProtocol, all}) + { + testIOUWithFeats(feats); + testMPTWithFeats(feats); + testMPTWithFeats(feats - fixTokenEscrowV1); + } } }; diff --git a/src/test/app/HostFuncImpl_test.cpp b/src/test/app/HostFuncImpl_test.cpp index 06d69b3b12..3b13312bb6 100644 --- a/src/test/app/HostFuncImpl_test.cpp +++ b/src/test/app/HostFuncImpl_test.cpp @@ -29,6 +29,12 @@ toBytes(std::uint32_t value) return Bytes{b, e}; } +static Bytes +toBytes(uint256 const& value) +{ + return Bytes{value.begin(), value.end()}; +} + static Bytes toBytes(Asset const& asset) { @@ -62,19 +68,23 @@ static ApplyContext createApplyContext( test::jtx::Env& env, OpenView& ov, + beast::Journal j, STTx const& tx = STTx(ttESCROW_FINISH, [](STObject&) {})) { ApplyContext ac{ - env.app(), - ov, - tx, - tesSUCCESS, - env.current()->fees().base, - tapNONE, - env.journal}; + env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, tapNONE, j}; return ac; } +static ApplyContext +createApplyContext( + test::jtx::Env& env, + OpenView& ov, + STTx const& tx = STTx(ttESCROW_FINISH, [](STObject&) {})) +{ + return createApplyContext(env, ov, env.journal, tx); +} + struct HostFuncImpl_test : public beast::unit_test::suite { void @@ -299,6 +309,11 @@ struct HostFuncImpl_test : public beast::unit_test::suite testcase("getTxField"); using namespace test::jtx; + std::string const credIdHex = + "0011223344556677889900112233445566778899001122334455667788990011"; + uint256 credId; + BEAST_EXPECT(credId.parseHex(credIdHex)); + Env env{*this}; OpenView ov{*env.current()}; STTx const stx = STTx(ttESCROW_FINISH, [&](auto& obj) { @@ -306,6 +321,9 @@ struct HostFuncImpl_test : public beast::unit_test::suite obj.setAccountID(sfOwner, env.master.id()); obj.setFieldU32(sfOfferSequence, env.seq(env.master)); obj.setFieldArray(sfMemos, STArray{}); + STVector256 credIds; + credIds.push_back(credId); + obj.setFieldV256(sfCredentialIDs, credIds); }); ApplyContext ac = createApplyContext(env, ov, stx); auto const dummyEscrow = @@ -336,6 +354,12 @@ struct HostFuncImpl_test : public beast::unit_test::suite BEAST_EXPECT( memos.error() == HostFunctionError::NOT_LEAF_FIELD); + auto const credentialIds = hfs.getTxField(sfCredentialIDs); + if (BEAST_EXPECT(!credentialIds.has_value())) + BEAST_EXPECTS( + credentialIds.error() == HostFunctionError::NOT_LEAF_FIELD, + std::to_string(HfErrorToInt(credentialIds.error()))); + auto const nonField = hfs.getTxField(sfInvalid); if (BEAST_EXPECT(!nonField.has_value())) BEAST_EXPECT( @@ -446,6 +470,15 @@ struct HostFuncImpl_test : public beast::unit_test::suite BEAST_EXPECT(*amountField == toBytes(XRP(100))); } + // Should return the PreviousTxnID field from the escrow ledger object + auto const previousTxnId = + hfs.getCurrentLedgerObjField(sfPreviousTxnID); + if (BEAST_EXPECT(previousTxnId.has_value())) + { + BEAST_EXPECT( + *previousTxnId == toBytes(env.tx()->getTransactionID())); + } + // Should return nullopt for a field not present auto const notPresent = hfs.getCurrentLedgerObjField(sfOwner); BEAST_EXPECT( @@ -537,6 +570,11 @@ struct HostFuncImpl_test : public beast::unit_test::suite Env env{*this}; OpenView ov{*env.current()}; + std::string const credIdHex = + "0011223344556677889900112233445566778899001122334455667788990011"; + uint256 credId; + BEAST_EXPECT(credId.parseHex(credIdHex)); + // Create a transaction with a nested array field STTx const stx = STTx(ttESCROW_FINISH, [&](auto& obj) { obj.setAccountID(sfAccount, env.master.id()); @@ -545,6 +583,9 @@ struct HostFuncImpl_test : public beast::unit_test::suite memoObj.setFieldVL(sfMemoData, Slice("hello", 5)); memos.push_back(memoObj); obj.setFieldArray(sfMemos, memos); + STVector256 credIds; + credIds.push_back(credId); + obj.setFieldV256(sfCredentialIDs, credIds); }); ApplyContext ac = createApplyContext(env, ov, stx); @@ -574,6 +615,24 @@ struct HostFuncImpl_test : public beast::unit_test::suite } } + { + // Locator for sfCredentialIDs[0] + std::vector locatorVec = {sfCredentialIDs.fieldCode, 0}; + Slice locator( + reinterpret_cast(locatorVec.data()), + locatorVec.size() * sizeof(int32_t)); + + auto const result = hfs.getTxNestedField(locator); + if (BEAST_EXPECTS( + result.has_value(), + std::to_string(static_cast(result.error())))) + { + std::string credIdResult( + result.value().begin(), result.value().end()); + BEAST_EXPECT(strHex(credIdResult) == credIdHex); + } + } + { // can use the nested locator for base fields too std::vector locatorVec = {sfAccount.fieldCode}; @@ -590,6 +649,24 @@ struct HostFuncImpl_test : public beast::unit_test::suite } } + { + // unaligned locator + std::vector locatorVec(sizeof(int32_t) + 1); + memcpy( + locatorVec.data() + 1, &sfAccount.fieldCode, sizeof(int32_t)); + Slice locator( + reinterpret_cast(locatorVec.data() + 1), + sizeof(int32_t)); + + auto const account = hfs.getTxNestedField(locator); + if (BEAST_EXPECTS( + account.has_value(), + std::to_string(static_cast(account.error())))) + { + BEAST_EXPECT(std::ranges::equal(*account, env.master.id())); + } + } + auto expectError = [&](std::vector const& locatorVec, HostFunctionError expectedError) { Slice locator( @@ -615,6 +692,11 @@ struct HostFuncImpl_test : public beast::unit_test::suite sfMemoData.fieldCode}, HostFunctionError::INDEX_OUT_OF_BOUNDS); + // Locator for non-existent index + expectError( + {sfCredentialIDs.fieldCode, 1}, // index 1 does not exist + HostFunctionError::INDEX_OUT_OF_BOUNDS); + // Locator for non-existent nested field expectError( {sfMemos.fieldCode, @@ -639,6 +721,10 @@ struct HostFuncImpl_test : public beast::unit_test::suite // Locator for STArray expectError({sfMemos.fieldCode}, HostFunctionError::NOT_LEAF_FIELD); + // Locator for STVector256 + expectError( + {sfCredentialIDs.fieldCode}, HostFunctionError::NOT_LEAF_FIELD); + // Locator for nesting into non-array/object field expectError( {sfAccount.fieldCode, // sfAccount is not an array or object @@ -934,6 +1020,11 @@ struct HostFuncImpl_test : public beast::unit_test::suite testcase("getTxArrayLen"); using namespace test::jtx; + std::string const credIdHex = + "0011223344556677889900112233445566778899001122334455667788990011"; + uint256 credId; + BEAST_EXPECT(credId.parseHex(credIdHex)); + Env env{*this}; OpenView ov{*env.current()}; @@ -952,6 +1043,9 @@ struct HostFuncImpl_test : public beast::unit_test::suite memos.push_back(memoObj); } obj.setFieldArray(sfMemos, memos); + STVector256 credIds; + credIds.push_back(credId); + obj.setFieldV256(sfCredentialIDs, credIds); }); ApplyContext ac = createApplyContext(env, ov, stx); @@ -974,6 +1068,11 @@ struct HostFuncImpl_test : public beast::unit_test::suite if (BEAST_EXPECT(!missingArray.has_value())) BEAST_EXPECT( missingArray.error() == HostFunctionError::FIELD_NOT_FOUND); + + // Should return 1 for sfCredentialIDs + auto const credIdsLen = hfs.getTxArrayLen(sfCredentialIDs); + if (BEAST_EXPECT(credIdsLen.has_value())) + BEAST_EXPECT(credIdsLen.value() == 1); } void @@ -1913,24 +2012,63 @@ struct HostFuncImpl_test : public beast::unit_test::suite testcase("trace"); using namespace test::jtx; - Env env{*this}; - OpenView ov{*env.current()}; - ApplyContext ac = createApplyContext(env, ov); + { + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kTrace}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = - keylet::escrow(env.master, env.seq(env.master)); - WasmHostFunctionsImpl hfs(ac, dummyEscrow); + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); - std::string msg = "test trace"; - std::string data = "abc"; - auto const slice = Slice(data.data(), data.size()); - auto const result = hfs.trace(msg, slice, false); - BEAST_EXPECT(result.has_value()); - BEAST_EXPECT(result.value() == msg.size() + data.size()); + std::string msg = "test trace"; + std::string data = "abc"; + auto const slice = Slice(data.data(), data.size()); + auto const result = hfs.trace(msg, slice, false); + if (BEAST_EXPECT(result.has_value())) + { + BEAST_EXPECT(result.value() == msg.size() + data.size()); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.find(msg) != std::string::npos); + } - auto const resultHex = hfs.trace(msg, slice, true); - BEAST_EXPECT(resultHex.has_value()); - BEAST_EXPECT(resultHex.value() == msg.size() + data.size() * 2); + auto const resultHex = hfs.trace(msg, slice, true); + if (BEAST_EXPECT(resultHex.has_value())) + { + BEAST_EXPECT(resultHex.has_value()); + BEAST_EXPECT(resultHex.value() == msg.size() + data.size() * 2); + auto const messages = sink.messages().str(); + std::string hex; + hex.reserve(data.size() * 2); + boost::algorithm::hex( + data.begin(), data.end(), std::back_inserter(hex)); + BEAST_EXPECT(messages.find(msg) != std::string::npos); + BEAST_EXPECT(messages.find(hex) != std::string::npos); + } + } + + { + // logs disabled (trace < error) + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kError}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + + std::string msg = "test trace"; + std::string data = "abc"; + auto const slice = Slice(data.data(), data.size()); + auto const result = hfs.trace(msg, slice, false); + BEAST_EXPECT(result && *result == msg.size() + data.size()); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.empty()); + } } void @@ -1939,19 +2077,49 @@ struct HostFuncImpl_test : public beast::unit_test::suite testcase("traceNum"); using namespace test::jtx; - Env env{*this}; - OpenView ov{*env.current()}; - ApplyContext ac = createApplyContext(env, ov); + { + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kTrace}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = - keylet::escrow(env.master, env.seq(env.master)); - WasmHostFunctionsImpl hfs(ac, dummyEscrow); + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); - std::string msg = "trace number"; - int64_t num = 123456789; - auto const result = hfs.traceNum(msg, num); - BEAST_EXPECT(result.has_value()); - BEAST_EXPECT(result.value() == msg.size() + sizeof(num)); + std::string msg = "trace number"; + int64_t num = 123456789; + auto const result = hfs.traceNum(msg, num); + if (BEAST_EXPECT(result.has_value())) + { + BEAST_EXPECT(result.value() == msg.size() + sizeof(num)); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.find(msg) != std::string::npos); + BEAST_EXPECT( + messages.find(std::to_string(num)) != std::string::npos); + } + } + + { + // logs disabled + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kError}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + + std::string msg = "trace number"; + int64_t num = 123456789; + auto const result = hfs.traceNum(msg, num); + BEAST_EXPECT(result && *result == msg.size() + sizeof(int64_t)); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.empty()); + } } void @@ -1960,22 +2128,47 @@ struct HostFuncImpl_test : public beast::unit_test::suite testcase("traceAccount"); using namespace test::jtx; - Env env{*this}; - OpenView ov{*env.current()}; - ApplyContext ac = createApplyContext(env, ov); - - auto const dummyEscrow = - keylet::escrow(env.master, env.seq(env.master)); - WasmHostFunctionsImpl hfs(ac, dummyEscrow); - - std::string msg = "trace account"; - // Valid account { + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kTrace}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + + std::string msg = "trace account"; auto const result = hfs.traceAccount(msg, env.master.id()); if (BEAST_EXPECT(result.has_value())) + { BEAST_EXPECT( - result.value() == - msg.size() + toBase58(env.master.id()).size()); + result.value() == msg.size() + env.master.id().size()); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.find(msg) != std::string::npos); + BEAST_EXPECT( + messages.find(env.master.human()) != std::string::npos); + } + } + + { + // logs disabled + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kError}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + std::string msg = "trace account"; + auto const result = hfs.traceAccount(msg, env.master.id()); + BEAST_EXPECT( + result && *result == msg.size() + env.master.id().size()); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.empty()); } } @@ -1985,46 +2178,72 @@ struct HostFuncImpl_test : public beast::unit_test::suite testcase("traceAmount"); using namespace test::jtx; - Env env{*this}; - OpenView ov{*env.current()}; - ApplyContext ac = createApplyContext(env, ov); - - auto const dummyEscrow = - keylet::escrow(env.master, env.seq(env.master)); - WasmHostFunctionsImpl hfs(ac, dummyEscrow); - - std::string msg = "trace amount"; - STAmount amount = XRP(12345); { + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kTrace}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + + std::string msg = "trace amount"; + STAmount amount = XRP(12345); + { + auto const result = hfs.traceAmount(msg, amount); + if (BEAST_EXPECT(result.has_value())) + { + BEAST_EXPECT(*result == msg.size()); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.find(msg) != std::string::npos); + BEAST_EXPECT( + messages.find(amount.getFullText()) != + std::string::npos); + } + } + + // IOU amount + Account const alice("alice"); + env.fund(XRP(1000), alice); + env.close(); + STAmount iouAmount = env.master["USD"](100); + { + auto const result = hfs.traceAmount(msg, iouAmount); + if (BEAST_EXPECT(result.has_value())) + BEAST_EXPECT(*result == msg.size()); + } + + // MPT amount + { + auto const mptId = makeMptID(42, env.master.id()); + Asset mptAsset = Asset(mptId); + STAmount mptAmount(mptAsset, 123456); + auto const result = hfs.traceAmount(msg, mptAmount); + if (BEAST_EXPECT(result.has_value())) + BEAST_EXPECT(*result == msg.size()); + } + } + + { + // logs disabled + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kError}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + + std::string msg = "trace amount"; + STAmount amount = XRP(12345); auto const result = hfs.traceAmount(msg, amount); - if (BEAST_EXPECT(result.has_value())) - BEAST_EXPECT( - result.value() == msg.size() + amount.getFullText().size()); - } - - // IOU amount - Account const alice("alice"); - env.fund(XRP(1000), alice); - env.close(); - STAmount iouAmount = env.master["USD"](100); - { - auto const result = hfs.traceAmount(msg, iouAmount); - if (BEAST_EXPECT(result.has_value())) - BEAST_EXPECT( - result.value() == - msg.size() + iouAmount.getFullText().size()); - } - - // MPT amount - { - auto const mptId = makeMptID(42, env.master.id()); - Asset mptAsset = Asset(mptId); - STAmount mptAmount(mptAsset, 123456); - auto const result = hfs.traceAmount(msg, mptAmount); - if (BEAST_EXPECT(result.has_value())) - BEAST_EXPECT( - result.value() == - msg.size() + mptAmount.getFullText().size()); + BEAST_EXPECT(result && *result == msg.size()); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.empty()); } } @@ -2035,7 +2254,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite Bytes const floatIntMin = {0x99, 0x20, 0xc4, 0x9b, 0xa5, 0xe3, 0x53, 0xf8}; // -2^63 Bytes const floatIntZero = {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // 0 Bytes const floatIntMax = {0xd9, 0x20, 0xc4, 0x9b, 0xa5, 0xe3, 0x53, 0xf8}; // 2^63-1 - Bytes const floatUIntMax = {0xd9, 0x46, 0x8d, 0xb8, 0xba, 0xc7, 0x10, 0xcb}; // 2^64 + Bytes const floatUIntMax = {0xd9, 0x46, 0x8d, 0xb8, 0xba, 0xc7, 0x10, 0xcb}; // 2^64-1 Bytes const floatMaxExp = {0xEC, 0x43, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00}; // 1e(80+15) Bytes const floatPreMaxExp = {0xEC, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00}; // 1e(79+15) Bytes const floatMinusMaxExp = {0xAC, 0x43, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00}; // -1e(80+15) @@ -2054,40 +2273,63 @@ struct HostFuncImpl_test : public beast::unit_test::suite // clang-format on void - testFloatTrace() + testTraceFloat() { - testcase("FloatTrace"); + testcase("traceFloat"); using namespace test::jtx; - Env env{*this}; - OpenView ov{*env.current()}; - ApplyContext ac = createApplyContext(env, ov); - - auto const dummyEscrow = - keylet::escrow(env.master, env.seq(env.master)); - WasmHostFunctionsImpl hfs(ac, dummyEscrow); - - std::string msg = "trace float"; - { - auto const result = hfs.traceFloat(msg, makeSlice(invalid)); - BEAST_EXPECT( - result && - *result == - msg.size() + 14 /* error msg size*/ + invalid.size() * 2); + Env env{*this}; + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + + std::string msg = "trace float"; + + { + auto const result = hfs.traceFloat(msg, makeSlice(invalid)); + BEAST_EXPECT( + result && + *result == msg.size() + makeSlice(invalid).size()); + } + + { + auto const result = hfs.traceFloat(msg, makeSlice(floatMaxExp)); + BEAST_EXPECT( + result && + *result == msg.size() + makeSlice(floatMaxExp).size()); + } } { - auto const result = hfs.traceFloat(msg, makeSlice(floatMaxExp)); + // logs disabled + Env env(*this); + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kError}; + beast::Journal jlog{sink}; + ApplyContext ac = createApplyContext(env, ov, jlog); + + auto const dummyEscrow = + keylet::escrow(env.master, env.seq(env.master)); + WasmHostFunctionsImpl hfs(ac, dummyEscrow); + + std::string msg = "trace float"; + + auto const result = hfs.traceFloat(msg, makeSlice(invalid)); BEAST_EXPECT( - result && *result == msg.size() + 19 /* string represenation*/); + result && *result == msg.size() + makeSlice(invalid).size()); + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.empty()); } } void testFloatFromInt() { - testcase("FloatFromInt"); + testcase("floatFromInt"); using namespace test::jtx; Env env{*this}; @@ -2134,7 +2376,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite void testFloatFromUint() { - testcase("FloatFromUint"); + testcase("floatFromUint"); using namespace test::jtx; Env env{*this}; @@ -2175,7 +2417,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite void testFloatSet() { - testcase("FloatSet"); + testcase("floatSet"); using namespace test::jtx; Env env{*this}; @@ -2201,7 +2443,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite { auto const result = - hfs.floatSet(1, Number::maxExponent + normalExp + 1, 0); + hfs.floatSet(1, wasm_float::maxExponent + normalExp + 1, 0); BEAST_EXPECT(!result) && BEAST_EXPECT( result.error() == @@ -2210,7 +2452,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite { auto const result = - hfs.floatSet(1, IOUAmount::maxExponent + normalExp + 1, 0); + hfs.floatSet(1, wasm_float::maxExponent + normalExp + 1, 0); BEAST_EXPECT(!result) && BEAST_EXPECT( result.error() == @@ -2219,37 +2461,37 @@ struct HostFuncImpl_test : public beast::unit_test::suite { auto const result = - hfs.floatSet(1, IOUAmount::minExponent + normalExp - 1, 0); + hfs.floatSet(1, wasm_float::minExponent + normalExp - 1, 0); BEAST_EXPECT(result) && BEAST_EXPECT(*result == floatIntZero); } { auto const result = - hfs.floatSet(1, IOUAmount::maxExponent + normalExp, 0); + hfs.floatSet(1, wasm_float::maxExponent + normalExp, 0); BEAST_EXPECT(result) && BEAST_EXPECT(*result == floatMaxExp); } { auto const result = - hfs.floatSet(-1, IOUAmount::maxExponent + normalExp, 0); + hfs.floatSet(-1, wasm_float::maxExponent + normalExp, 0); BEAST_EXPECT(result) && BEAST_EXPECT(*result == floatMinusMaxExp); } { auto const result = - hfs.floatSet(1, IOUAmount::maxExponent + normalExp - 1, 0); + hfs.floatSet(1, wasm_float::maxExponent + normalExp - 1, 0); BEAST_EXPECT(result) && BEAST_EXPECT(*result == floatPreMaxExp); } { auto const result = - hfs.floatSet(IOUAmount::maxMantissa, IOUAmount::maxExponent, 0); + hfs.floatSet(STAmount::cMaxValue, wasm_float::maxExponent, 0); BEAST_EXPECT(result) && BEAST_EXPECT(*result == floatMaxIOU); } { auto const result = - hfs.floatSet(1, IOUAmount::minExponent + normalExp, 0); + hfs.floatSet(1, wasm_float::minExponent + normalExp, 0); BEAST_EXPECT(result) && BEAST_EXPECT(*result == floatMinExp); } @@ -2262,7 +2504,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite void testFloatCompare() { - testcase("FloatCompare"); + testcase("floatCompare"); using namespace test::jtx; Env env{*this}; @@ -2549,7 +2791,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite { auto const y = hfs.floatSet( - IOUAmount::maxMantissa, -normalExp - 1, 0); // 0.9999999... + STAmount::cMaxValue, -normalExp - 1, 0); // 0.9999999... if (BEAST_EXPECT(y)) { auto const result = @@ -2907,7 +3149,7 @@ struct HostFuncImpl_test : public beast::unit_test::suite void testFloats() { - testFloatTrace(); + testTraceFloat(); testFloatFromInt(); testFloatFromUint(); testFloatSet(); diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/LendingHelpers_test.cpp new file mode 100644 index 0000000000..55fffad6b0 --- /dev/null +++ b/src/test/app/LendingHelpers_test.cpp @@ -0,0 +1,1352 @@ +#include +// DO NOT REMOVE +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace xrpl { +namespace test { + +class LendingHelpers_test : public beast::unit_test::suite +{ + void + testComputeRaisedRate() + { + using namespace jtx; + using namespace xrpl::detail; + struct TestCase + { + std::string name; + Number periodicRate; + std::uint32_t paymentsRemaining; + Number expectedRaisedRate; + }; + + auto const testCases = std::vector{ + { + .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 xrpl::detail; + struct TestCase + { + std::string name; + Number periodicRate; + std::uint32_t paymentsRemaining; + Number expectedPaymentFactor; + }; + + auto const testCases = std::vector{ + { + .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{3672085646312450436, -19}, + }, // 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 xrpl::detail; + + struct TestCase + { + std::string name; + Number principalOutstanding; + Number periodicRate; + std::uint32_t paymentsRemaining; + Number expectedPeriodicPayment; + }; + + auto const testCases = std::vector{ + { + .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{389569066396123265, -15}, // 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 xrpl::detail; + + struct TestCase + { + std::string name; + Number periodicPayment; + Number periodicRate; + std::uint32_t paymentsRemaining; + Number expectedPrincipalOutstanding; + }; + + auto const testCases = std::vector{ + { + .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{389569066396123265, -15}, // 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 xrpl::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.trackedInterestPart() == 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 xrpl::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{ + {.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 xrpl::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{ + { + .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{317097919837645865, -19}, // 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 xrpl::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{ + { + .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{1929012345679012346, -20}, // 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 xrpl::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{ + { + .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{1929012345679012346, -20}, // 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{1000192901234567901, -16}, // 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)); + } + } + + void + testTryOverpaymentNoInterestNoFee() + { + // This test ensures that overpayment with no interest works correctly. + testcase("tryOverpayment - No Interest No Fee"); + + using namespace jtx; + using namespace xrpl::detail; + + Env env{*this}; + Account const issuer{"issuer"}; + PrettyAsset const asset = issuer["USD"]; + std::int32_t const loanScale = -5; + TenthBips16 const managementFeeRate{0}; // 0% + TenthBips32 const loanInterestRate{0}; // 0% + Number const loanPrincipal{1'000}; + std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; + std::uint32_t const paymentsRemaining = 10; + auto const periodicRate = + loanPeriodicRate(loanInterestRate, paymentInterval); + Number const overpaymentAmount{50}; + + ExtendedPaymentComponents const overpaymentComponents = + computeOverpaymentComponents( + asset, + loanScale, + overpaymentAmount, + TenthBips32(0), + TenthBips32(0), + managementFeeRate); + + auto const loanProperites = computeLoanProperties( + asset, + loanPrincipal, + loanInterestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate, + loanScale); + + Number const periodicPayment = loanProperites.periodicPayment; + + auto const ret = tryOverpayment( + asset, + loanScale, + overpaymentComponents, + loanProperites.loanState, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate, + env.journal); + + BEAST_EXPECT(ret); + + auto const& [actualPaymentParts, newLoanProperties] = *ret; + auto const& newState = newLoanProperties.loanState; + + // =========== VALIDATE PAYMENT PARTS =========== + BEAST_EXPECTS( + actualPaymentParts.valueChange == 0, + " valueChange mismatch: expected 0, got " + + to_string(actualPaymentParts.valueChange)); + + BEAST_EXPECTS( + actualPaymentParts.feePaid == 0, + " feePaid mismatch: expected 0, got " + + to_string(actualPaymentParts.feePaid)); + + BEAST_EXPECTS( + actualPaymentParts.interestPaid == 0, + " interestPaid mismatch: expected 0, got " + + to_string(actualPaymentParts.interestPaid)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == overpaymentAmount, + " principalPaid mismatch: expected " + + to_string(overpaymentAmount) + ", got " + + to_string(actualPaymentParts.principalPaid)); + + // =========== VALIDATE STATE CHANGES =========== + BEAST_EXPECTS( + loanProperites.loanState.interestDue - newState.interestDue == 0, + " interest change mismatch: expected 0, got " + + to_string( + loanProperites.loanState.interestDue - + newState.interestDue)); + + BEAST_EXPECTS( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue == + 0, + " management fee change mismatch: expected 0, got " + + to_string( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding, + " principalPaid mismatch: expected " + + to_string( + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding) + + ", got " + to_string(actualPaymentParts.principalPaid)); + } + + void + testTryOverpaymentNoInterestOverpaymentFee() + { + testcase("tryOverpayment - No Interest With Overpayment Fee"); + + using namespace jtx; + using namespace xrpl::detail; + + Env env{*this}; + Account const issuer{"issuer"}; + PrettyAsset const asset = issuer["USD"]; + std::int32_t const loanScale = -5; + TenthBips16 const managementFeeRate{0}; // 0% + TenthBips32 const loanInterestRate{0}; // 0% + Number const loanPrincipal{1'000}; + std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; + std::uint32_t const paymentsRemaining = 10; + auto const periodicRate = + loanPeriodicRate(loanInterestRate, paymentInterval); + + ExtendedPaymentComponents const overpaymentComponents = + computeOverpaymentComponents( + asset, + loanScale, + Number{50, 0}, + TenthBips32(0), + TenthBips32(10'000), // 10% overpayment fee + managementFeeRate); + + auto const loanProperites = computeLoanProperties( + asset, + loanPrincipal, + loanInterestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate, + loanScale); + + Number const periodicPayment = loanProperites.periodicPayment; + + auto const ret = tryOverpayment( + asset, + loanScale, + overpaymentComponents, + loanProperites.loanState, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate, + env.journal); + + BEAST_EXPECT(ret); + + auto const& [actualPaymentParts, newLoanProperties] = *ret; + auto const& newState = newLoanProperties.loanState; + + // =========== VALIDATE PAYMENT PARTS =========== + BEAST_EXPECTS( + actualPaymentParts.valueChange == 0, + " valueChange mismatch: expected 0, got " + + to_string(actualPaymentParts.valueChange)); + + BEAST_EXPECTS( + actualPaymentParts.feePaid == 5, + " feePaid mismatch: expected 5, got " + + to_string(actualPaymentParts.feePaid)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == 45, + " principalPaid mismatch: expected 45, got `" + + to_string(actualPaymentParts.principalPaid)); + + BEAST_EXPECTS( + actualPaymentParts.interestPaid == 0, + " interestPaid mismatch: expected 0, got " + + to_string(actualPaymentParts.interestPaid)); + + // =========== VALIDATE STATE CHANGES =========== + // With no Loan interest, interest outstanding should not change + BEAST_EXPECTS( + loanProperites.loanState.interestDue - newState.interestDue == 0, + " interest change mismatch: expected 0, got " + + to_string( + loanProperites.loanState.interestDue - + newState.interestDue)); + + // With no Loan management fee, management fee due should not change + BEAST_EXPECTS( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue == + 0, + " management fee change mismatch: expected 0, got " + + to_string( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding, + " principalPaid mismatch: expected " + + to_string( + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding) + + ", got " + to_string(actualPaymentParts.principalPaid)); + } + + void + testTryOverpaymentLoanInterestNoOverpaymentFees() + { + testcase("tryOverpayment - Loan Interest, No Overpayment Fees"); + + using namespace jtx; + using namespace xrpl::detail; + + Env env{*this}; + Account const issuer{"issuer"}; + PrettyAsset const asset = issuer["USD"]; + std::int32_t const loanScale = -5; + TenthBips16 const managementFeeRate{0}; // 0% + TenthBips32 const loanInterestRate{10'000}; // 10% + Number const loanPrincipal{1'000}; + std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; + std::uint32_t const paymentsRemaining = 10; + auto const periodicRate = + loanPeriodicRate(loanInterestRate, paymentInterval); + + ExtendedPaymentComponents const overpaymentComponents = + computeOverpaymentComponents( + asset, + loanScale, + Number{50, 0}, + TenthBips32(0), // no overpayment interest + TenthBips32(0), // 0% overpayment fee + managementFeeRate); + + auto const loanProperites = computeLoanProperties( + asset, + loanPrincipal, + loanInterestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate, + loanScale); + + Number const periodicPayment = loanProperites.periodicPayment; + + auto const ret = tryOverpayment( + asset, + loanScale, + overpaymentComponents, + loanProperites.loanState, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate, + env.journal); + + BEAST_EXPECT(ret); + + auto const& [actualPaymentParts, newLoanProperties] = *ret; + auto const& newState = newLoanProperties.loanState; + + // =========== VALIDATE PAYMENT PARTS =========== + // with no overpayment interest portion, value change should equal + // interest decrease + BEAST_EXPECTS( + (actualPaymentParts.valueChange == Number{-228802, -5}), + " valueChange mismatch: expected " + + to_string(Number{-228802, -5}) + ", got " + + to_string(actualPaymentParts.valueChange)); + + // with no fee portion, fee paid should be zero + BEAST_EXPECTS( + actualPaymentParts.feePaid == 0, + " feePaid mismatch: expected 0, got " + + to_string(actualPaymentParts.feePaid)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == 50, + " principalPaid mismatch: expected 50, got `" + + to_string(actualPaymentParts.principalPaid)); + + // with no interest portion, interest paid should be zero + BEAST_EXPECTS( + actualPaymentParts.interestPaid == 0, + " interestPaid mismatch: expected 0, got " + + to_string(actualPaymentParts.interestPaid)); + + // =========== VALIDATE STATE CHANGES =========== + BEAST_EXPECTS( + actualPaymentParts.principalPaid == + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding, + " principalPaid mismatch: expected " + + to_string( + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding) + + ", got " + to_string(actualPaymentParts.principalPaid)); + + BEAST_EXPECTS( + actualPaymentParts.valueChange == + newState.interestDue - loanProperites.loanState.interestDue, + " valueChange mismatch: expected " + + to_string( + newState.interestDue - + loanProperites.loanState.interestDue) + + ", got " + to_string(actualPaymentParts.valueChange)); + + // With no Loan management fee, management fee due should not change + BEAST_EXPECTS( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue == + 0, + " management fee change mismatch: expected 0, got " + + to_string( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue)); + } + + void + testTryOverpaymentLoanInterestOverpaymentInterest() + { + testcase( + "tryOverpayment - Loan Interest, Overpayment Interest, No Fee"); + + using namespace jtx; + using namespace xrpl::detail; + + Env env{*this}; + Account const issuer{"issuer"}; + PrettyAsset const asset = issuer["USD"]; + std::int32_t const loanScale = -5; + TenthBips16 const managementFeeRate{0}; // 0% + TenthBips32 const loanInterestRate{10'000}; // 10% + Number const loanPrincipal{1'000}; + std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; + std::uint32_t const paymentsRemaining = 10; + auto const periodicRate = + loanPeriodicRate(loanInterestRate, paymentInterval); + + ExtendedPaymentComponents const overpaymentComponents = + computeOverpaymentComponents( + asset, + loanScale, + Number{50, 0}, + TenthBips32(10'000), // 10% overpayment interest + TenthBips32(0), // 0% overpayment fee + managementFeeRate); + + auto const loanProperites = computeLoanProperties( + asset, + loanPrincipal, + loanInterestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate, + loanScale); + + Number const periodicPayment = loanProperites.periodicPayment; + + auto const ret = tryOverpayment( + asset, + loanScale, + overpaymentComponents, + loanProperites.loanState, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate, + env.journal); + + BEAST_EXPECT(ret); + + auto const& [actualPaymentParts, newLoanProperties] = *ret; + auto const& newState = newLoanProperties.loanState; + + // =========== VALIDATE PAYMENT PARTS =========== + // with overpayment interest portion, interest paid should be 5 + BEAST_EXPECTS( + actualPaymentParts.interestPaid == 5, + " interestPaid mismatch: expected 5, got " + + to_string(actualPaymentParts.interestPaid)); + + // With overpayment interest portion, value change should equal the + // interest decrease plus overpayment interest portion + BEAST_EXPECTS( + (actualPaymentParts.valueChange == + Number{-205922, -5} + actualPaymentParts.interestPaid), + " valueChange mismatch: expected " + + to_string( + actualPaymentParts.valueChange - + actualPaymentParts.interestPaid) + + ", got " + to_string(actualPaymentParts.valueChange)); + + // with no fee portion, fee paid should be zero + BEAST_EXPECTS( + actualPaymentParts.feePaid == 0, + " feePaid mismatch: expected 0, got " + + to_string(actualPaymentParts.feePaid)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == 45, + " principalPaid mismatch: expected 45, got `" + + to_string(actualPaymentParts.principalPaid)); + + // =========== VALIDATE STATE CHANGES =========== + BEAST_EXPECTS( + actualPaymentParts.principalPaid == + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding, + " principalPaid mismatch: expected " + + to_string( + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding) + + ", got " + to_string(actualPaymentParts.principalPaid)); + + // The change in interest is equal to the value change sans the + // overpayment interest + BEAST_EXPECTS( + actualPaymentParts.valueChange - actualPaymentParts.interestPaid == + newState.interestDue - loanProperites.loanState.interestDue, + " valueChange mismatch: expected " + + to_string( + newState.interestDue - + loanProperites.loanState.interestDue + + actualPaymentParts.interestPaid) + + ", got " + to_string(actualPaymentParts.valueChange)); + + // With no Loan management fee, management fee due should not change + BEAST_EXPECTS( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue == + 0, + " management fee change mismatch: expected 0, got " + + to_string( + loanProperites.loanState.managementFeeDue - + newState.managementFeeDue)); + } + + void + testTryOverpaymentLoanInterestFeeOverpaymentInterestNoFee() + { + testcase( + "tryOverpayment - Loan Interest and Fee, Overpayment Interest, No " + "Fee"); + + using namespace jtx; + using namespace xrpl::detail; + + Env env{*this}; + Account const issuer{"issuer"}; + PrettyAsset const asset = issuer["USD"]; + std::int32_t const loanScale = -5; + TenthBips16 const managementFeeRate{10'000}; // 10% + TenthBips32 const loanInterestRate{10'000}; // 10% + Number const loanPrincipal{1'000}; + std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; + std::uint32_t const paymentsRemaining = 10; + auto const periodicRate = + loanPeriodicRate(loanInterestRate, paymentInterval); + + ExtendedPaymentComponents const overpaymentComponents = + computeOverpaymentComponents( + asset, + loanScale, + Number{50, 0}, + TenthBips32(10'000), // 10% overpayment interest + TenthBips32(0), // 0% overpayment fee + managementFeeRate); + + auto const loanProperites = computeLoanProperties( + asset, + loanPrincipal, + loanInterestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate, + loanScale); + + Number const periodicPayment = loanProperites.periodicPayment; + + auto const ret = tryOverpayment( + asset, + loanScale, + overpaymentComponents, + loanProperites.loanState, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate, + env.journal); + + BEAST_EXPECT(ret); + + auto const& [actualPaymentParts, newLoanProperties] = *ret; + auto const& newState = newLoanProperties.loanState; + + // =========== VALIDATE PAYMENT PARTS =========== + + // Since there is loan management fee, the fee is charged against + // overpayment interest portion first, so interest paid remains 4.5 + BEAST_EXPECTS( + (actualPaymentParts.interestPaid == Number{45, -1}), + " interestPaid mismatch: expected 4.5, got " + + to_string(actualPaymentParts.interestPaid)); + + // With overpayment interest portion, value change should equal the + // interest decrease plus overpayment interest portion + BEAST_EXPECTS( + (actualPaymentParts.valueChange == + Number{-18533, -4} + actualPaymentParts.interestPaid), + " valueChange mismatch: expected " + + to_string( + Number{-18533, -4} + actualPaymentParts.interestPaid) + + ", got " + to_string(actualPaymentParts.valueChange)); + + // While there is no overpayment fee, fee paid should equal the + // management fee charged against the overpayment interest portion + BEAST_EXPECTS( + (actualPaymentParts.feePaid == Number{5, -1}), + " feePaid mismatch: expected 0.5, got " + + to_string(actualPaymentParts.feePaid)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == 45, + " principalPaid mismatch: expected 45, got `" + + to_string(actualPaymentParts.principalPaid)); + + // =========== VALIDATE STATE CHANGES =========== + BEAST_EXPECTS( + actualPaymentParts.principalPaid == + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding, + " principalPaid mismatch: expected " + + to_string( + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding) + + ", got " + to_string(actualPaymentParts.principalPaid)); + + // Note that the management fee value change is not captured, as this + // value is not needed to correctly update the Vault state. + BEAST_EXPECTS( + (newState.managementFeeDue - + loanProperites.loanState.managementFeeDue == + Number{-20592, -5}), + " management fee change mismatch: expected " + + to_string(Number{-20592, -5}) + ", got " + + to_string( + newState.managementFeeDue - + loanProperites.loanState.managementFeeDue)); + + BEAST_EXPECTS( + actualPaymentParts.valueChange - actualPaymentParts.interestPaid == + newState.interestDue - loanProperites.loanState.interestDue, + " valueChange mismatch: expected " + + to_string( + newState.interestDue - + loanProperites.loanState.interestDue) + + ", got " + + to_string( + actualPaymentParts.valueChange - + actualPaymentParts.interestPaid)); + } + + void + testTryOverpaymentLoanInterestFeeOverpaymentInterestFee() + { + testcase( + "tryOverpayment - Loan Interest, Fee, Overpayment Interest, Fee"); + + using namespace jtx; + using namespace xrpl::detail; + + Env env{*this}; + Account const issuer{"issuer"}; + PrettyAsset const asset = issuer["USD"]; + std::int32_t const loanScale = -5; + TenthBips16 const managementFeeRate{10'000}; // 10% + TenthBips32 const loanInterestRate{10'000}; // 10% + Number const loanPrincipal{1'000}; + std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; + std::uint32_t const paymentsRemaining = 10; + auto const periodicRate = + loanPeriodicRate(loanInterestRate, paymentInterval); + + ExtendedPaymentComponents const overpaymentComponents = + computeOverpaymentComponents( + asset, + loanScale, + Number{50, 0}, + TenthBips32(10'000), // 10% overpayment interest + TenthBips32(10'000), // 10% overpayment fee + managementFeeRate); + + auto const loanProperites = computeLoanProperties( + asset, + loanPrincipal, + loanInterestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate, + loanScale); + + Number const periodicPayment = loanProperites.periodicPayment; + + auto const ret = tryOverpayment( + asset, + loanScale, + overpaymentComponents, + loanProperites.loanState, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate, + env.journal); + + BEAST_EXPECT(ret); + + auto const& [actualPaymentParts, newLoanProperties] = *ret; + auto const& newState = newLoanProperties.loanState; + + // =========== VALIDATE PAYMENT PARTS =========== + + // Since there is loan management fee, the fee is charged against + // overpayment interest portion first, so interest paid remains 4.5 + BEAST_EXPECTS( + (actualPaymentParts.interestPaid == Number{45, -1}), + " interestPaid mismatch: expected 4.5, got " + + to_string(actualPaymentParts.interestPaid)); + + // With overpayment interest portion, value change should equal the + // interest decrease plus overpayment interest portion + BEAST_EXPECTS( + (actualPaymentParts.valueChange == + Number{-164737, -5} + actualPaymentParts.interestPaid), + " valueChange mismatch: expected " + + to_string( + Number{-164737, -5} + actualPaymentParts.interestPaid) + + ", got " + to_string(actualPaymentParts.valueChange)); + + // While there is no overpayment fee, fee paid should equal the + // management fee charged against the overpayment interest portion + BEAST_EXPECTS( + (actualPaymentParts.feePaid == Number{55, -1}), + " feePaid mismatch: expected 5.5, got " + + to_string(actualPaymentParts.feePaid)); + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == 40, + " principalPaid mismatch: expected 40, got `" + + to_string(actualPaymentParts.principalPaid)); + + // =========== VALIDATE STATE CHANGES =========== + + BEAST_EXPECTS( + actualPaymentParts.principalPaid == + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding, + " principalPaid mismatch: expected " + + to_string( + loanProperites.loanState.principalOutstanding - + newState.principalOutstanding) + + ", got " + to_string(actualPaymentParts.principalPaid)); + + // Note that the management fee value change is not captured, as this + // value is not needed to correctly update the Vault state. + BEAST_EXPECTS( + (newState.managementFeeDue - + loanProperites.loanState.managementFeeDue == + Number{-18304, -5}), + " management fee change mismatch: expected " + + to_string(Number{-18304, -5}) + ", got " + + to_string( + newState.managementFeeDue - + loanProperites.loanState.managementFeeDue)); + + BEAST_EXPECTS( + actualPaymentParts.valueChange - actualPaymentParts.interestPaid == + newState.interestDue - loanProperites.loanState.interestDue, + " valueChange mismatch: expected " + + to_string( + newState.interestDue - + loanProperites.loanState.interestDue) + + ", got " + + to_string( + actualPaymentParts.valueChange - + actualPaymentParts.interestPaid)); + } + +public: + void + run() override + { + testTryOverpaymentNoInterestNoFee(); + testTryOverpaymentNoInterestOverpaymentFee(); + testTryOverpaymentLoanInterestNoOverpaymentFees(); + testTryOverpaymentLoanInterestOverpaymentInterest(); + testTryOverpaymentLoanInterestFeeOverpaymentInterestNoFee(); + testTryOverpaymentLoanInterestFeeOverpaymentInterestFee(); + + testComputeFullPaymentInterest(); + testLoanAccruedInterest(); + testLoanLatePaymentInterest(); + testLoanPeriodicPayment(); + testLoanPrincipalFromPeriodicPayment(); + testComputeRaisedRate(); + testComputePaymentFactor(); + testComputeOverpaymentComponents(); + testComputeInterestAndFeeParts(); + } +}; + +BEAST_DEFINE_TESTSUITE(LendingHelpers, app, xrpl); + +} // namespace test +} // namespace xrpl diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 5915ebae91..769ed40321 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -752,30 +752,36 @@ class LoanBroker_test : public beast::unit_test::suite // LoanBrokerID env(set(alice, vault.vaultID), loanBrokerID(nextKeylet.key), - ter(tecNO_ENTRY)); + ter(tecNO_ENTRY), + THISLINE); // VaultID env(set(alice, nextKeylet.key), loanBrokerID(broker->key()), - ter(tecNO_PERMISSION)); + ter(tecNO_ENTRY), + THISLINE); // Owner env(set(evan, vault.vaultID), loanBrokerID(broker->key()), - ter(tecNO_PERMISSION)); + ter(tecNO_PERMISSION), + THISLINE); // ManagementFeeRate env(set(alice, vault.vaultID), loanBrokerID(broker->key()), managementFeeRate(maxManagementFeeRate), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // CoverRateMinimum env(set(alice, vault.vaultID), loanBrokerID(broker->key()), coverRateMinimum(maxManagementFeeRate), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // CoverRateLiquidation env(set(alice, vault.vaultID), loanBrokerID(broker->key()), coverRateLiquidation(maxManagementFeeRate), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // fields that can be changed testData = "Test Data 1234"; @@ -783,23 +789,43 @@ class LoanBroker_test : public beast::unit_test::suite env(set(alice, vault.vaultID), loanBrokerID(broker->key()), data(std::string(maxDataPayloadLength + 1, 'W')), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // Bad debt maximum env(set(alice, vault.vaultID), loanBrokerID(broker->key()), debtMaximum(Number(-175, -1)), - ter(temINVALID)); + ter(temINVALID), + THISLINE); + Number debtMax{175, -1}; + if (vault.asset.integral()) + { + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + data(testData), + debtMaximum(debtMax), + ter(tecPRECISION_LOSS), + THISLINE); + roundToAsset(vault.asset, debtMax); + } // Data & Debt maximum env(set(alice, vault.vaultID), loanBrokerID(broker->key()), data(testData), - debtMaximum(Number(175, -1))); + debtMaximum(debtMax), + THISLINE); }, [&](SLE::const_ref broker) { // Check the updated fields BEAST_EXPECT(checkVL(broker->at(sfData), testData)); - BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(175, -1)); + Number const expected = + STAmount{vault.asset, Number(175, -1)}; + auto const actual = broker->at(sfDebtMaximum); + BEAST_EXPECTS( + actual == expected, + "Expected: " + to_string(expected) + + ", Actual: " + to_string(actual)); }); lifecycle( @@ -1024,6 +1050,12 @@ class LoanBroker_test : public beast::unit_test::suite destination(dest), ter(tecFROZEN), THISLINE); + + // preclaim: tecPSEUDO_ACCOUNT + env(coverWithdraw(alice, brokerKeylet.key, asset(10)), + destination(vaultInfo.pseudoAccount), + ter(tecPSEUDO_ACCOUNT), + THISLINE); } if (brokerTest == CoverClawback) @@ -1436,10 +1468,506 @@ class LoanBroker_test : public beast::unit_test::suite }); } + void + testLoanBrokerSetDebtMaximum() + { + testcase("testLoanBrokerSetDebtMaximum"); + using namespace jtx; + using namespace loanBroker; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Env env(*this); + Vault vault{env}; + + env.fund(XRP(100'000), issuer, alice); + env.close(); + + PrettyAsset const asset = [&]() { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + env.close(); + PrettyAsset const mptAsset = mptt["MPT"]; + mptt.authorize({.account = alice}); + env.close(); + return mptAsset; + }(); + + env(pay(issuer, alice, asset(100'000)), THISLINE); + env.close(); + + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(tx, THISLINE); + env.close(); + auto const le = env.le(vaultKeylet); + VaultInfo vaultInfo = [&]() { + if (BEAST_EXPECT(le)) + return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)}; + return VaultInfo{asset, {}, {}}; + }(); + if (vaultInfo.vaultID == uint256{}) + return; + + env(vault.deposit( + {.depositor = alice, + .id = vaultKeylet.key, + .amount = asset(50)}), + THISLINE); + env.close(); + + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + env(set(alice, vaultInfo.vaultID), THISLINE); + env.close(); + + Account const borrower{"borrower"}; + env.fund(XRP(1'000), borrower); + env(loan::set(borrower, brokerKeylet.key, asset(50).value()), + sig(sfCounterpartySignature, alice), + fee(env.current()->fees().base * 2), + THISLINE); + auto const broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + + BEAST_EXPECT(broker->at(sfDebtTotal) == 50); + auto debtTotal = broker->at(sfDebtTotal); + + auto tx2 = set(alice, vaultInfo.vaultID); + tx2[sfLoanBrokerID] = to_string(brokerKeylet.key); + tx2[sfDebtMaximum] = debtTotal - 1; + env(tx2, ter(tecLIMIT_EXCEEDED), THISLINE); + + tx2[sfDebtMaximum] = debtTotal + 1; + env(tx2, ter(tesSUCCESS), THISLINE); + + tx2[sfDebtMaximum] = 0; + env(tx2, ter(tesSUCCESS), THISLINE); + + tx2[sfDebtMaximum] = Json::Value::maxInt; + env(tx2, ter(tesSUCCESS), THISLINE); + + { + auto const dm = power(2, 64) - 1; + BEAST_EXPECT(dm > maxMPTokenAmount); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(temINVALID), THISLINE); + } + + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm > maxMPTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(temINVALID), THISLINE); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(tesSUCCESS), THISLINE); + } + + { + auto const dm = 2 * (power(2, 62) - 1) + 1; + BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(tesSUCCESS), THISLINE); + } + + tx2[sfDebtMaximum] = Number{9223372036854775807, 0}; + env(tx2, ter(tesSUCCESS), THISLINE); + } + + void + testRIPD4323() + { + testcase << "RIPD-4323"; + using namespace jtx; + Account const issuer("issuer"); + Account const holder("holder"); + Account const& broker = issuer; + + auto test = [&](auto&& getToken) { + Env env(*this); + + env.fund(XRP(1'000), issuer, holder); + env.close(); + + auto const [token, deposit, err] = getToken(env); + + Vault vault(env); + auto const [tx, keylet] = + vault.create({.owner = broker, .asset = token.asset()}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = broker, .id = keylet.key, .amount = deposit}), + ter(err)); + env.close(); + + auto const brokerKeylet = + keylet::loanbroker(broker, env.seq(broker)); + + env(loanBroker::set(broker, keylet.key)); + env.close(); + + env(loanBroker::coverDeposit(broker, brokerKeylet.key, deposit), + ter(err)); + env.close(); + }; + + test([&](Env&) { + // issuer can issue any amount + auto const token = issuer["IOU"]; + return std::make_tuple(token, token(1'000), tesSUCCESS); + }); + std::vector, // max amount + std::uint64_t, // deposit amount + TER>> // expected error + mptTests = { + // issuer can issue up to 2'000 tokens + {2'000, 4'000, 1'000, tesSUCCESS}, + // issuer can issue 500 tokens (250 VaultDeposit + + // 250 LoanBrokerCoverDeposit) + {2'000, 2'500, 250, tesSUCCESS}, + // issuer can issue 500 tokens (250 VaultDeposit + + // 250 LoanBrokerCoverDeposit). MaximumAmount is default. + {maxMPTokenAmount - 500, std::nullopt, 250, tesSUCCESS}, + // issuer can issue 500, and fails on depositing 1'000 + {2'000, 2'500, 1'000, tecINSUFFICIENT_FUNDS}, + // issuer has already issued MaximumAmount + {2'000, 2'000, 1'000, tecINSUFFICIENT_FUNDS}, + // issuer has already issued MaximumAmount. MaximumAmount is + // default. + {maxMPTokenAmount, std::nullopt, 250, tecINSUFFICIENT_FUNDS}, + }; + for (auto const& [pay, max, deposit, err] : mptTests) + { + test([&](Env& env) -> std::tuple { + MPT const token = MPTTester( + {.env = env, + .issuer = issuer, + .holders = {holder}, + .pay = pay, + .flags = MPTDEXFlags, + .maxAmt = max}); + return std::make_tuple(token, token(deposit), err); + }); + } + } + + void + testAMB06_VaultFreezeCheckMissing() + { + testcase << "RIPD-4466 - LoanBrokerSet disallows frozen vaults"; + using namespace jtx; + Env env(*this); + + Account const issuer{"issuer"}, lender{"lender"}, borrower{"borrower"}; + env.fund(XRP(20'000), issuer, lender, borrower); + auto const IOU = issuer["IOU"]; + + Vault vault{env}; + auto [tx, vaultKeylet] = + vault.create({.owner = lender, .asset = IOU.asset()}); + env(tx); + env.close(); + + // Get vault pseudo-account and FREEZE it + auto const vaultSle = env.le(vaultKeylet); + auto const vaultPseudo = vaultSle->at(sfAccount); + auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo); + env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze)); + + env(loanBroker::set(lender, vaultKeylet.key), ter(tecFROZEN)); + } + + void + testRIPD4274IOU() + { + using namespace jtx; + Account issuer("broker"); + Account broker("issuer"); + Account dest("destination"); + auto const token = issuer["IOU"]; + + enum TrustState { + RequireAuth, + ZeroLimit, + ReachedLimit, + NearLimit, + NoTrustLine, + }; + + auto test = [&](TrustState trustState) { + Env env(*this); + + testcase << "RIPD-4274 IOU with state: " + << static_cast(trustState); + + auto setTrustLine = [&](Account const& acct, TrustState state) { + switch (state) + { + case RequireAuth: + env(trust(issuer, token(0), acct, tfSetfAuth)); + break; + case ZeroLimit: { + auto jv = trust(acct, token(0)); + // set QualityIn so that the trustline is not + // auto-deleted + jv[sfQualityIn] = 10'000'000; + env(jv); + } + break; + case ReachedLimit: { + env(trust(acct, token(1'000))); + env(pay(issuer, acct, token(1'000))); + env.close(); + } + break; + case NearLimit: { + env(trust(acct, token(1'000))); + env(pay(issuer, acct, token(950))); + env.close(); + } + break; + case NoTrustLine: + // don't create a trustline + break; + default: + BEAST_EXPECT(false); + } + env.close(); + }; + + env.fund(XRP(1'000), issuer, broker, dest); + env.close(); + + if (trustState == RequireAuth) + { + env(fset(issuer, asfRequireAuth)); + env.close(); + + setTrustLine(broker, RequireAuth); + } + + setTrustLine(dest, trustState); + + env(trust(broker, token(2'000), 0)); + env(pay(issuer, broker, token(2'000))); + env.close(); + + Vault vault(env); + auto const [tx, keylet] = + vault.create({.owner = broker, .asset = token.asset()}); + env(tx); + env.close(); + + // Test Vault withdraw + env(vault.deposit( + {.depositor = broker, + .id = keylet.key, + .amount = token(1'000)})); + env.close(); + + env(vault.withdraw( + {.depositor = broker, + .id = keylet.key, + .amount = token(1'000)}), + loanBroker::destination(dest), + ter(std::ignore)); + BEAST_EXPECT(env.ter() == tecNO_LINE); + env.close(); + + env(vault.withdraw( + {.depositor = broker, + .id = keylet.key, + .amount = token(1'000)})); + + // Test LoanBroker withdraw + auto const brokerKeylet = + keylet::loanbroker(broker, env.seq(broker)); + + env(loanBroker::set(broker, keylet.key)); + env.close(); + + env(loanBroker::coverDeposit( + broker, brokerKeylet.key, token(1'000))); + env.close(); + + env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loanBroker::destination(dest), + ter(std::ignore)); + BEAST_EXPECT(env.ter() == tecNO_LINE); + env.close(); + + // Clearing RequireAuth shouldn't change the result + if (trustState == RequireAuth) + { + env(fclear(issuer, asfRequireAuth)); + env.close(); + + env(loanBroker::coverWithdraw( + broker, brokerKeylet.key, token(100)), + loanBroker::destination(dest), + ter(std::ignore)); + BEAST_EXPECT(env.ter() == tecNO_LINE); + env.close(); + } + }; + + test(RequireAuth); + test(ZeroLimit); + test(ReachedLimit); + test(NearLimit); + test(NoTrustLine); + } + + void + testRIPD4274MPT() + { + using namespace jtx; + Account issuer("broker"); + Account broker("issuer"); + Account dest("destination"); + + enum MPTState { + RequireAuth, + ReachedMAX, + NoMPT, + }; + + auto test = [&](MPTState MPTState) { + Env env(*this); + + testcase << "RIPD-4274 MPT with state: " + << static_cast(MPTState); + + env.fund(XRP(1'000), issuer, broker, dest); + env.close(); + + auto const maybeToken = [&]() -> std::optional { + switch (MPTState) + { + case RequireAuth: { + auto tester = MPTTester( + {.env = env, + .issuer = issuer, + .holders = {broker, dest}, + .pay = 2'000, + .flags = MPTDEXFlags | tfMPTRequireAuth, + .authHolder = true, + .maxAmt = 5'000}); + // unauthorize dest + tester.authorize( + {.account = issuer, + .holder = dest, + .flags = tfMPTUnauthorize}); + return tester; + } + case ReachedMAX: { + auto tester = MPTTester( + {.env = env, + .issuer = issuer, + .holders = {broker, dest}, + .pay = 2'000, + .flags = MPTDEXFlags, + .maxAmt = 4'000}); + BEAST_EXPECT( + env.balance(issuer, tester) == tester(-4'000)); + return tester; + } + case NoMPT: { + return MPTTester( + {.env = env, + .issuer = issuer, + .holders = {broker}, + .pay = 2'000, + .flags = MPTDEXFlags, + .maxAmt = 4'000}); + } + default: + return std::nullopt; + } + }(); + if (!BEAST_EXPECT(maybeToken)) + return; + + auto const& token = *maybeToken; + + Vault vault(env); + auto const [tx, keylet] = + vault.create({.owner = broker, .asset = token.asset()}); + env(tx); + env.close(); + + // Test Vault withdraw + env(vault.deposit( + {.depositor = broker, + .id = keylet.key, + .amount = token(1'000)})); + env.close(); + + env(vault.withdraw( + {.depositor = broker, + .id = keylet.key, + .amount = token(1'000)}), + loanBroker::destination(dest), + ter(std::ignore)); + + // Shouldn't fail if at MaximumAmount since no new tokens are issued + TER const err = + MPTState == ReachedMAX ? TER(tesSUCCESS) : tecNO_AUTH; + BEAST_EXPECT(env.ter() == err); + env.close(); + + if (err != tesSUCCESS) + { + env(vault.withdraw( + {.depositor = broker, + .id = keylet.key, + .amount = token(1'000)})); + } + + // Test LoanBroker withdraw + auto const brokerKeylet = + keylet::loanbroker(broker, env.seq(broker)); + + env(loanBroker::set(broker, keylet.key)); + env.close(); + + env(loanBroker::coverDeposit( + broker, brokerKeylet.key, token(1'000))); + env.close(); + + env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loanBroker::destination(dest), + ter(std::ignore)); + BEAST_EXPECT(env.ter() == err); + env.close(); + }; + + test(RequireAuth); + test(ReachedMAX); + test(NoMPT); + } + + void + testRIPD4274() + { + testRIPD4274IOU(); + testRIPD4274MPT(); + } + public: void run() override { + testLoanBrokerSetDebtMaximum(); testLoanBrokerCoverDepositNullVault(); testDisabled(); @@ -1451,6 +1979,11 @@ public: testInvalidLoanBrokerSet(); testRequireAuth(); + testRIPD4323(); + testAMB06_VaultFreezeCheckMissing(); + + testRIPD4274(); + // TODO: Write clawback failure tests with an issuer / MPT that doesn't // have the right flags set. } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 7c2e83aa19..e9780211de 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -11,6 +11,8 @@ #include #include +#include + namespace xrpl { namespace test { @@ -141,7 +143,7 @@ protected: using namespace jtx; auto const vaultSle = env.le(keylet::vault(vaultID)); - return getVaultScale(vaultSle); + return getAssetsTotalScale(vaultSle); } }; @@ -350,8 +352,14 @@ protected: env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount), borrowerScale); - env.test.BEAST_EXPECT( - roundToScale(difference, loanScale) >= beast::zero); + env.test.expect( + roundToScale(difference, loanScale) >= beast::zero, + "Balance before: " + to_string(balanceBefore.value()) + + ", expected change: " + to_string(balanceChangeAmount) + + ", difference (balance after - expected): " + + to_string(difference), + __FILE__, + __LINE__); } } @@ -372,7 +380,7 @@ protected: if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) { env.test.BEAST_EXPECT( - loan->at(sfPreviousPaymentDate) == previousPaymentDate); + loan->at(sfPreviousPaymentDueDate) == previousPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); env.test.BEAST_EXPECT( @@ -507,7 +515,7 @@ protected: if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) { return LoanState{ - .previousPaymentDate = loan->at(sfPreviousPaymentDate), + .previousPaymentDate = loan->at(sfPreviousPaymentDueDate), .startDate = tp{d{loan->at(sfStartDate)}}, .nextPaymentDate = loan->at(sfNextPaymentDueDate), .paymentRemaining = loan->at(sfPaymentRemaining), @@ -551,12 +559,15 @@ protected: broker.vaultScale(env), state.principalOutstanding.exponent()))); BEAST_EXPECT(state.paymentInterval == 600); - BEAST_EXPECT( - state.totalValue == - roundToAsset( - broker.asset, - state.periodicPayment * state.paymentRemaining, - state.loanScale)); + { + NumberRoundModeGuard mg(Number::upward); + BEAST_EXPECT( + state.totalValue == + roundToAsset( + broker.asset, + state.periodicPayment * state.paymentRemaining, + state.loanScale)); + } BEAST_EXPECT( state.managementFeeOutstanding == computeManagementFee( @@ -589,7 +600,7 @@ protected: auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + state.totalValue - state.managementFeeOutstanding; - if (unrealizedLoss > assetsUnavailable) + if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable)) { return false; } @@ -705,8 +716,9 @@ protected: << "\tManagement Fee Rate: " << feeRate << std::endl << "\tTotal Payments: " << total << std::endl << "\tPeriodic Payment: " << props.periodicPayment << std::endl - << "\tTotal Value: " << props.totalValueOutstanding << std::endl - << "\tManagement Fee: " << props.managementFeeOwedToBroker + << "\tTotal Value: " << props.loanState.valueOutstanding + << std::endl + << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl << "\tLoan Scale: " << props.loanScale << std::endl << "\tFirst payment principal: " << props.firstPaymentPrincipal @@ -856,9 +868,6 @@ protected: using namespace std::chrono_literals; using d = NetClock::duration; - // Account const evan{"evan"}; - // Account const alice{"alice"}; - bool const showStepBalances = paymentParams.showStepBalances; auto const currencyLabel = getCurrencyLabel(broker.asset); @@ -911,7 +920,7 @@ protected: state.principalOutstanding, state.managementFeeOutstanding); { - auto const raw = computeRawLoanState( + auto const raw = computeTheoreticalLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -964,7 +973,7 @@ protected: Number totalFeesPaid = 0; std::size_t totalPaymentsMade = 0; - xrpl::LoanState currentTrueState = computeRawLoanState( + xrpl::LoanState currentTrueState = computeTheoreticalLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -1019,7 +1028,7 @@ protected: paymentComponents.trackedInterestPart() + paymentComponents.trackedManagementFeeDelta); - xrpl::LoanState const nextTrueState = computeRawLoanState( + xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( state.periodicPayment, periodicRate, state.paymentRemaining - 1, @@ -1271,7 +1280,8 @@ protected: verifyLoanStatus, issuer, lender, - borrower); + borrower, + PaymentParameters{.showStepBalances = true}); } /** Runs through the complete lifecycle of a loan @@ -1452,7 +1462,7 @@ protected: BEAST_EXPECT( loan->at(sfPaymentInterval) == *loanParams.payInterval); BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd); - BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0); + BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0); BEAST_EXPECT( loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval); @@ -1484,9 +1494,9 @@ protected: startDate + *loanParams.payInterval, *loanParams.payTotal, state.loanScale, - loanProperties.totalValueOutstanding, + loanProperties.loanState.valueOutstanding, principalRequestAmount, - loanProperties.managementFeeOwedToBroker, + loanProperties.loanState.managementFeeDue, loanProperties.periodicPayment, loanFlags | 0); @@ -1541,9 +1551,9 @@ protected: nextDueDate, *loanParams.payTotal, loanProperties.loanScale, - loanProperties.totalValueOutstanding, + loanProperties.loanState.valueOutstanding, principalRequestAmount, - loanProperties.managementFeeOwedToBroker, + loanProperties.loanState.managementFeeDue, loanProperties.periodicPayment, loanFlags | 0); @@ -2392,7 +2402,7 @@ protected: interval * Number(12, -2) / secondsInYear; BEAST_EXPECT( periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); + Number(2283105022831050228ULL, -24, Number::normalized{})); STAmount const principalOutstanding{ broker.asset, state.principalOutstanding}; STAmount const accruedInterest{ @@ -2445,16 +2455,38 @@ protected: getCurrentState(env, broker, loanKeylet, verifyLoanStatus); env.close(); + BEAST_EXPECT( + STAmount(broker.asset, state.periodicPayment) == + broker.asset(Number(8333457002039338267, -17))); + // Make all the payments in one transaction // service fee is 2 auto const startingPayments = state.paymentRemaining; - 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); + STAmount const payoffAmount = [&]() { + NumberRoundModeGuard mg(Number::upward); + auto const rawPayoff = startingPayments * + (state.periodicPayment + broker.asset(2).value()); + STAmount payoffAmount{broker.asset, rawPayoff}; + BEAST_EXPECTS( + payoffAmount == + broker.asset(Number(1024014840244721, -12)), + to_string(payoffAmount)); + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + + payoffAmount = roundToScale(payoffAmount, state.loanScale); + + return payoffAmount; + }(); + + auto const totalPayoffValue = state.totalValue + + startingPayments * broker.asset(2).value(); + STAmount const totalPayoffAmount{ + broker.asset, totalPayoffValue}; + + BEAST_EXPECTS( + totalPayoffAmount == payoffAmount, + "Payoff amount: " + to_string(payoffAmount) + + ". Total Value: " + to_string(totalPayoffAmount)); singlePayment( loanKeylet, @@ -2624,7 +2656,7 @@ protected: interval * Number(12, -2) / secondsInYear; BEAST_EXPECT( periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); + Number(2283105022831050228, -24, Number::normalized{})); STAmount const roundedPeriodicPayment{ broker.asset, roundPeriodicPayment( @@ -2642,7 +2674,7 @@ protected: roundedPeriodicPayment == roundToScale( broker.asset( - Number(8333457001162141, -14), Number::upward), + Number(8333457002039338267, -17), Number::upward), state.loanScale, Number::upward)); // 83334570.01162141 @@ -2657,12 +2689,12 @@ protected: totalDue == roundToScale( broker.asset( - Number(8533457001162141, -14), Number::upward), + Number(8533457002039338267, -17), Number::upward), state.loanScale, Number::upward)); { - auto const raw = computeRawLoanState( + auto const raw = computeTheoreticalLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -2693,7 +2725,7 @@ protected: transactionAmount == roundToScale( broker.asset( - Number(9533457001162141, -14), Number::upward), + Number(9533457002039400, -14), Number::upward), state.loanScale, Number::upward)); @@ -2705,7 +2737,7 @@ protected: Number totalInterestPaid = 0; std::size_t totalPaymentsMade = 0; - xrpl::LoanState currentTrueState = computeRawLoanState( + xrpl::LoanState currentTrueState = computeTheoreticalLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -2726,15 +2758,22 @@ protected: state.paymentRemaining, broker.params.managementFeeRate); - BEAST_EXPECT( - paymentComponents.trackedValueDelta <= - roundedPeriodicPayment); + BEAST_EXPECTS( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || + paymentComponents.trackedValueDelta <= + roundedPeriodicPayment, + "Delta: " + + to_string(paymentComponents.trackedValueDelta) + + ", periodic payment: " + + to_string(roundedPeriodicPayment)); - xrpl::LoanState const nextTrueState = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); + xrpl::LoanState const nextTrueState = + computeTheoreticalLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; @@ -2782,8 +2821,10 @@ protected: paymentComponents.trackedInterestPart() + paymentComponents.trackedManagementFeeDelta); BEAST_EXPECT( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || paymentComponents.trackedValueDelta <= - roundedPeriodicPayment); + roundedPeriodicPayment); BEAST_EXPECT( state.paymentRemaining < 12 || @@ -2794,7 +2835,7 @@ protected: Number::upward) == roundToScale( broker.asset( - Number(8333228695260180, -14), + Number(8333228691531218890, -17), Number::upward), state.loanScale, Number::upward)); @@ -3453,11 +3494,12 @@ protected: ter{tecNO_AUTH}); env.close(); - // Can create loan without origination fee + // Cannot create loan, even without an origination fee env(set(borrower, broker.brokerID, principalRequest), counterparty(lender), sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); + fee(env.current()->fees().base * 5), + ter{tecNO_AUTH}); env.close(); // No MPToken for lender - no authorization and no payment @@ -3578,6 +3620,52 @@ protected: fee(env.current()->fees().base * 5)); }, CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + Vault vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit; + env(tx); + env.close(); + + testcase("Vault at maximum value"); + env(set(issuer, broker.brokerID, principalRequest), + counterparty(lender), + interestRate(TenthBips32(10'000)), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter(tecLIMIT_EXCEEDED), + THISLINE); + }, + nullptr); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + Vault vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = + BrokerParameters::defaults().vaultDeposit + + broker.asset(1).number(); + env(tx); + env.close(); + + testcase("Vault maximum value exceeded"); + env(set(issuer, broker.brokerID, principalRequest), + counterparty(lender), + interestRate(TenthBips32(100'000)), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + paymentTotal(2), + paymentInterval(3600 * 24), + ter(tecLIMIT_EXCEEDED), + THISLINE); + }, + nullptr); } void @@ -3632,7 +3720,7 @@ protected: env(pay(issuer, borrower, mptAsset(10'000))); env.close(); - std::array const assets{xrpAsset, mptAsset, iouAsset}; + std::array const assets{iouAsset, xrpAsset, mptAsset}; // Create vaults and loan brokers std::vector brokers; @@ -3813,7 +3901,7 @@ protected: BEAST_EXPECT(loan[sfPaymentInterval] == 60); BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000"); BEAST_EXPECT(loan[sfPaymentRemaining] == 1); - BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate)); + BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate)); BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); BEAST_EXPECT(!loan.isMember(sfLoanScale)); @@ -3994,7 +4082,6 @@ protected: createJson["CloseInterestRate"] = 55374; createJson["ClosePaymentFee"] = "3825205248"; - createJson["GracePeriod"] = 0; createJson["LatePaymentFee"] = "237"; createJson["LoanOriginationFee"] = "0"; createJson["OverpaymentFee"] = 35167; @@ -4009,7 +4096,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)); + env(createJson, ter(tecPRECISION_LOSS), THISLINE); env.close(); BEAST_EXPECT(!env.le(keylet)); @@ -4021,7 +4108,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)); + env(createJson, ter(tecPRECISION_LOSS), THISLINE); env.close(); } @@ -4455,15 +4542,6 @@ protected: }; } - void - testBasicMath() - { - // Test the functions defined in LendingHelpers.h - testcase("Basic Math"); - - pass(); - } - void testIssuerLoan() { @@ -4679,7 +4757,30 @@ protected: jtx::fee const& loanSetFee, Number const& debtMaximumRequest) { // first temBAD_SIGNER: TODO + // invalid grace period + { + // zero grace period + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + gracePeriod(0), + loanSetFee, + ter(temINVALID)); + // grace period less than default minimum + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + gracePeriod(LoanSet::defaultGracePeriod - 1), + loanSetFee, + ter(temINVALID)); + + // grace period greater than payment interval + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + paymentInterval(120), + gracePeriod(121), + loanSetFee, + ter(temINVALID)); + } // empty/zero broker ID { auto jv = set(borrower, uint256{}, debtMaximumRequest); @@ -4971,7 +5072,6 @@ protected: auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; auto createJson = env.json( set(borrower, broker.brokerID, principalRequest), @@ -4980,7 +5080,6 @@ protected: createJson["CloseInterestRate"] = 47299; createJson["ClosePaymentFee"] = "3985819770"; - createJson["GracePeriod"] = 0; createJson["InterestRate"] = 92; createJson["LatePaymentFee"] = "3866894865"; createJson["LoanOriginationFee"] = "0"; @@ -4996,13 +5095,13 @@ protected: auto const keylet = keylet::loan(broker.brokerID, loanSequence); createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tecPRECISION_LOSS)); - env.close(startDate); + env(createJson, THISLINE); + env.close(); auto loanPayTx = env.json( pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); loanPayTx["Amount"]["value"] = "0.000281284125490196"; - env(loanPayTx, ter(tecNO_ENTRY)); + env(loanPayTx, ter(tecINSUFFICIENT_PAYMENT), THISLINE); env.close(); } @@ -5133,7 +5232,6 @@ protected: json(sfCounterpartySignature, Json::objectValue)); createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 0; createJson["InterestRate"] = 24346; createJson["LateInterestRate"] = 65535; createJson["LatePaymentFee"] = "0"; @@ -5253,7 +5351,6 @@ protected: json(sfCounterpartySignature, Json::objectValue)); createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 0; createJson["InterestRate"] = 12833; createJson["LateInterestRate"] = 77048; createJson["LatePaymentFee"] = "0"; @@ -5347,7 +5444,7 @@ protected: set(borrower, broker.brokerID, Number{55524'81, -2}), fee(loanSetFee), closePaymentFee(0), - gracePeriod(0), + gracePeriod(LoanSet::defaultGracePeriod), interestRate(TenthBips32(12833)), lateInterestRate(TenthBips32(77048)), latePaymentFee(0), @@ -5573,21 +5670,26 @@ protected: BEAST_EXPECT(beforeState.periodicPayment > 0); // pay all but the last payment - Number const payment = beforeState.periodicPayment * (total - 1); - XRPAmount const payFee{ - baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)}; - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, payment}), - fee(payFee)); - env(loanPayTx, ter(tesSUCCESS)); - env.close(); + { + NumberRoundModeGuard mg{Number::upward}; + Number const payment = + beforeState.periodicPayment * (total - 1); + XRPAmount const payFee{ + baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)}; + STAmount const paymentAmount = roundToScale( + STAmount{broker.asset, payment}, beforeState.loanScale); + auto loanPayTx = env.json( + pay(borrower, keylet.key, paymentAmount), fee(payFee)); + env(loanPayTx, ter(tesSUCCESS)); + env.close(); + } // The loan is on the last payment auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.paymentRemaining == 1); BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace); BEAST_EXPECT( afterState.previousPaymentDate == maxTime - grace - interval); - BEAST_EXPECT(afterState.paymentRemaining == 1); } } @@ -5851,7 +5953,7 @@ protected: auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval); - auto const rawLoanState = computeRawLoanState( + auto const rawLoanState = computeTheoreticalLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -6029,7 +6131,7 @@ protected: { // --- PoC Summary ---------------------------------------------------- // Scenario: Borrower makes one periodic payment early (before next due) - // so doPayment sets sfPreviousPaymentDate to the (future) + // so doPayment sets sfPreviousPaymentDueDate to the (future) // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one // interval. Borrower then immediately performs a full-payment // (tfLoanFullPayment). Why it matters: Full-payment interest accrual @@ -6144,15 +6246,16 @@ protected: // Accrued + prepayment-penalty interest based on current periodic // schedule auto const fullPaymentInterest = computeFullPaymentInterest( - after.periodicPayment, + detail::loanPrincipalFromPeriodicPayment( + after.periodicPayment, periodicRate2, after.paymentRemaining), periodicRate2, - after.paymentRemaining, env.current()->parentCloseTime(), after.paymentInterval, after.previousPaymentDate, static_cast( 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); @@ -6180,9 +6283,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( - after.periodicPayment, + detail::loanPrincipalFromPeriodicPayment( + after.periodicPayment, periodicRate2, after.paymentRemaining), periodicRate2, - after.paymentRemaining, env.current()->parentCloseTime(), after.paymentInterval, prevClamped, @@ -6436,8 +6539,7 @@ protected: .lateFee = Number{200, -6}, .interest = TenthBips32{50'000}, .payTotal = 10, - .payInterval = 150, - .gracePd = 0}; + .payInterval = 150}; auto const assetType = AssetType::XRP; @@ -6458,9 +6560,6 @@ protected: auto state = getCurrentState(env, broker, loanKeylet); if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) { - // log << "loan after create: " << to_string(loan->getJson()) - // << std::endl; - env.close(tp{d{ loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); } @@ -6475,16 +6574,10 @@ protected: { auto const submitParam = to_string(jv); - // log << "about to submit: " << submitParam << std::endl; auto const jr = env.rpc("submit", borrower.name(), submitParam); - // log << jr << std::endl; BEAST_EXPECT(jr.isMember(jss::result)); auto const jResult = jr[jss::result]; - // BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); - // BEAST_EXPECT( - // jResult[jss::error_exception] == - // "fails local checks: Transaction has bad signature."); } env.close(); @@ -6520,8 +6613,7 @@ protected: .counter = borrower, .principalRequest = Number{100'000, -4}, .interest = TenthBips32{100'000}, - .payTotal = 10, - .gracePd = 0}; + .payTotal = 10}; auto const assetType = AssetType::MPT; @@ -7007,11 +7099,8 @@ protected: env.close(); PaymentParameters paymentParams{ - //.overpaymentFactor = Number{15, -1}, - //.overpaymentExtra = Number{1, -6}, - //.flags = tfLoanOverpayment, - .showStepBalances = true, - //.validateBalances = false, + .showStepBalances = false, + .validateBalances = true, }; makeLoanPayments( @@ -7026,6 +7115,532 @@ protected: paymentParams); } + void + testOverpaymentManagementFee() + { + testcase("testOverpaymentManagementFee"); + + using namespace jtx; + using namespace loan; + + Env env(*this, all); + + Account const lender{"lender"}, borrower{"borrower"}; + + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1000}; + + auto const result = createVaultAndBroker( + env, + asset, + lender, + { + .vaultDeposit = asset(100'000).value(), + .managementFeeRate = TenthBips16(10'000), + }); + + auto const loanSetFee = fee(env.current()->fees().base * 2); + + auto const loanKeylet = keylet::loan( + result.brokerKeylet().key, + (env.le(result.brokerKeylet()))->at(sfLoanSequence)); + env(loan::set( + borrower, + result.brokerKeylet().key, + asset(10'000).value(), + tfLoanOverpayment), + sig(sfCounterpartySignature, lender), + loan::paymentInterval(86400 * 30), + loan::paymentTotal(3), + loan::overpaymentInterestRate( + TenthBips32(percentageToTenthBips(20))), + loanSetFee); + + // From calculator + auto const expectedOverpaymentManagementFee = Number{33333, 0}; + auto const loanBrokerBalanceBefore = env.balance(lender); + + auto const loanPayFee = fee(env.current()->fees().base * 2); + env(pay(borrower, + loanKeylet.key, + asset(5'000).value(), + tfLoanOverpayment), + loanPayFee); + env.close(); + + BEAST_EXPECTS( + env.balance(lender) - loanBrokerBalanceBefore == + expectedOverpaymentManagementFee, + "overpayment management fee missmatch; expected:" + + to_string(expectedOverpaymentManagementFee) + " got: " + + to_string(env.balance(lender) - loanBrokerBalanceBefore)); + } + + void + testLoanPayBrokerOwnerMissingTrustline() + { + testcase << "LoanPay Broker Owner Missing Trustline (PoC)"; + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + auto const IOU = issuer["IOU"]; + Env env(*this, all); + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + // Set up trustlines and fund accounts + env(trust(broker, IOU(20'000'000))); + env(trust(borrower, IOU(20'000'000))); + env(pay(issuer, broker, IOU(10'000'000))); + env(pay(issuer, borrower, IOU(1'000))); + env.close(); + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, IOU, broker); + // Create a loan first (this creates debt) + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + env(set(borrower, brokerInfo.brokerID, 10'000), + sig(sfCounterpartySignature, broker), + loanServiceFee(IOU(100).value()), + paymentInterval(100), + fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = IOU(50'000).value(); + env(loanBroker::coverDeposit( + broker, brokerInfo.brokerID, STAmount{IOU, additionalCover})); + env.close(); + // Verify broker owner has a trustline + auto const brokerTrustline = keylet::line(broker, IOU); + BEAST_EXPECT(env.le(brokerTrustline) != nullptr); + // Broker owner deletes their trustline + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, IOU); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Remove the trustline by setting limit to 0 + env(trust(broker, IOU(0))); + env.close(); + // Verify trustline is deleted + BEAST_EXPECT(env.le(brokerTrustline) == nullptr); + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_LINE. + env(pay(borrower, keylet.key, IOU(10'100)), + fee(XRP(100)), + ter(tesSUCCESS)); + env.close(); + // Verify trustline is still deleted + BEAST_EXPECT(env.le(brokerTrustline) == nullptr); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = + env.le(keylet::loanbroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, IOU); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS( + balance == IOU(51'100), to_string(Json::Value(balance))); + } + } + + void + testLoanPayBrokerOwnerUnauthorizedMPT() + { + testcase << "LoanPay Broker Owner MPT unauthorized"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env(*this, all); + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + + PrettyAsset const MPT{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + + env.close(); + + // Fund accounts + env(pay(issuer, broker, MPT(10'000'000))); + env(pay(issuer, borrower, MPT(1'000))); + env.close(); + + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, MPT, broker); + // Create a loan first (this creates debt) + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + env(set(borrower, brokerInfo.brokerID, 10'000), + sig(sfCounterpartySignature, broker), + loanServiceFee(MPT(100).value()), + paymentInterval(100), + fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = MPT(50'000).value(); + env(loanBroker::coverDeposit( + broker, brokerInfo.brokerID, STAmount{MPT, additionalCover})); + env.close(); + // Verify broker owner is authorized + auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); + BEAST_EXPECT(env.le(brokerMpt) != nullptr); + // Broker owner unauthorizes. + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, MPT); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Then, unauthorize the MPT. + mptt.authorize({.account = broker, .flags = tfMPTUnauthorize}); + env.close(); + // Verify the MPT is unauthorized. + BEAST_EXPECT(env.le(brokerMpt) == nullptr); + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_AUTH. + auto const borrowerBalance = env.balance(borrower, MPT); + env(pay(borrower, keylet.key, MPT(10'100)), + fee(XRP(100)), + ter(tesSUCCESS)); + env.close(); + // Verify the MPT is still unauthorized. + BEAST_EXPECT(env.le(brokerMpt) == nullptr); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = + env.le(keylet::loanbroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, MPT); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS( + balance == MPT(51'100), to_string(Json::Value(balance))); + } + } + + void + testLoanPayBrokerOwnerNoPermissionedDomainMPT() + { + testcase + << "LoanPay Broker Owner without permissioned domain of the MPT"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env(*this, all); + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + auto credType = "credential1"; + + pdomain::Credentials const credentials1{{issuer, credType}}; + env(pdomain::setTx(issuer, credentials1)); + env.close(); + + auto domainID = pdomain::getNewDomain(env.meta()); + + env(credentials::create(broker, issuer, credType)); + env(credentials::accept(broker, issuer, credType)); + env.close(); + + env(credentials::create(borrower, issuer, credType)); + env(credentials::accept(borrower, issuer, credType)); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create({ + .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | + tfMPTCanLock, + .domainID = domainID, + }); + + PrettyAsset const MPT{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + + env.close(); + + // Fund accounts + env(pay(issuer, broker, MPT(10'000'000))); + env(pay(issuer, borrower, MPT(1'000))); + env.close(); + + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, MPT, broker); + // Create a loan first (this creates debt) + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + env(set(borrower, brokerInfo.brokerID, 10'000), + sig(sfCounterpartySignature, broker), + loanServiceFee(MPT(100).value()), + paymentInterval(100), + fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = MPT(50'000).value(); + env(loanBroker::coverDeposit( + broker, brokerInfo.brokerID, STAmount{MPT, additionalCover})); + env.close(); + // Verify broker owner is authorized + auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); + BEAST_EXPECT(env.le(brokerMpt) != nullptr); + // Remove the credentials for the Broker owner. + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, MPT); + env(pay(broker, issuer, brokerBalance)); + env.close(); + + env(credentials::deleteCred(broker, broker, issuer, credType)); + env.close(); + + // Make sure the broker is not authorized to hold the MPT after we + // deleted the credentials + env(pay(issuer, broker, MPT(1'000)), ter(tecNO_AUTH)); + + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_AUTH. + auto const borrowerBalance = env.balance(borrower, MPT); + env(pay(borrower, keylet.key, MPT(10'100)), + fee(XRP(100)), + ter(tesSUCCESS)); + env.close(); + // Verify broker is still not authorized + env(pay(issuer, broker, MPT(1'000)), ter(tecNO_AUTH)); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = + env.le(keylet::loanbroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, MPT); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS( + balance == MPT(51'100), to_string(Json::Value(balance))); + } + } + + void + testLoanSetBrokerOwnerNoPermissionedDomainMPT() + { + testcase + << "LoanSet Broker Owner without permissioned domain of the MPT"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env(*this, all); + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + auto credType = "credential1"; + + pdomain::Credentials const credentials1{{issuer, credType}}; + env(pdomain::setTx(issuer, credentials1)); + env.close(); + + auto domainID = pdomain::getNewDomain(env.meta()); + + // Add credentials for the broker and borrower + env(credentials::create(broker, issuer, credType)); + env(credentials::accept(broker, issuer, credType)); + env.close(); + + env(credentials::create(borrower, issuer, credType)); + env(credentials::accept(borrower, issuer, credType)); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create({ + .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | + tfMPTCanLock, + .domainID = domainID, + }); + + PrettyAsset const MPT{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + env.close(); + + // Fund accounts + env(pay(issuer, broker, MPT(10'000'000))); + env(pay(issuer, borrower, MPT(1'000))); + env.close(); + + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, MPT, broker); + + // Remove the credentials for the Broker owner. + // Clear the balance first. + auto const brokerBalance = env.balance(broker, MPT); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Delete the credentials + env(credentials::deleteCred(broker, broker, issuer, credType)); + env.close(); + + // Create a loan, this should fail for tecNO_AUTH + env(set(borrower, brokerInfo.brokerID, 10'000), + sig(sfCounterpartySignature, broker), + loanServiceFee(MPT(100).value()), + paymentInterval(100), + fee(XRP(100)), + ter(tecNO_AUTH)); + env.close(); + } + + void + testSequentialFLCDepletion() + { + testcase << "First-Loss Capital Depletion on Sequential Defaults"; + + using namespace jtx; + using namespace loan; + using namespace loanBroker; + + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrowerA{"borrowerA"}; + Account const borrowerB{"borrowerB"}; + + env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB); + env.close(); + + PrettyAsset const asset = xrpIssue(); + auto const vaultDepositAmount = + asset(200'000); // Enough for 2 x 50k loans plus interest/fees + + auto const brokerInfo = createVaultAndBroker( + env, + asset, + lender, + { + .vaultDeposit = vaultDepositAmount.value(), + .debtMax = 0, + .coverRateMin = TenthBips32(20000), // 20% + .coverDeposit = 21'000, + .managementFeeRate = TenthBips16(100), // 0.1% + .coverRateLiquidation = TenthBips32(100000), + }); + auto const brokerKeylet = brokerInfo.brokerKeylet(); + + // Create two identical loans: each 50,000 XRP principal (scaled down to + // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal + // + interest) Formula will calculate cover as: 100% × (20% × 100,000) = + // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first + // default + auto const principalAmount = Number(50'000); + auto const loanPaymentInterval = 2592000; // 30 days + auto const loanGracePeriod = 604800; // 7 days + + // Create Loan A + auto loanATx = env.jt( + set(borrowerA, brokerKeylet.key, principalAmount), + sig(sfCounterpartySignature, lender), + interestRate(TenthBips32(500)), // 5% + paymentTotal(12), + loan::paymentInterval(loanPaymentInterval), + loan::gracePeriod(loanGracePeriod), + fee(XRP(10))); // Sufficient fee for multi-sig transaction + env(loanATx); + env.close(); + + auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1); + + // Create Loan B + auto loanBTx = env.jt( + set(borrowerB, brokerKeylet.key, principalAmount), + sig(sfCounterpartySignature, lender), + interestRate(TenthBips32(500)), // 5% + paymentTotal(12), + loan::paymentInterval(loanPaymentInterval), + loan::gracePeriod(loanGracePeriod), + fee(XRP(10))); // Sufficient fee for multi-sig transaction + env(loanBTx); + env.close(); + + auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2); + + auto loanASle = env.le(loanAKeylet); + if (!BEAST_EXPECT(loanASle)) + return; + + // Advance time past grace period for both loans to be defaultable + auto const loanANextDue = loanASle->at(sfNextPaymentDueDate); + auto const loanAGrace = loanASle->at(sfGracePeriod); + env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60}); + + env(manage(lender, loanAKeylet.key, tfLoanDefault), ter(tesSUCCESS)); + env.close(); + + // Verify Loan A is defaulted + loanASle = env.le(loanAKeylet); + if (!BEAST_EXPECT(loanASle)) + return; + BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault)); + BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0); + + // Check broker state after first default (from committed ledger) + auto brokerSle = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal); + auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable); + + // DebtTotal should have decreased by Loan A's debt + BEAST_EXPECT(afterFirstDebtTotal == 50'134); + + // CoverAvailable should have decreased significantly + BEAST_EXPECT(afterFirstCoverAvailable == 946); + + env(manage(lender, loanBKeylet.key, tfLoanDefault), ter(tesSUCCESS)); + + brokerSle = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal); + auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable); + + BEAST_EXPECT(afterSecondDebtTotal == 0); + + BEAST_EXPECT(afterSecondCoverAvailable == 0); + } + public: void run() override @@ -7034,6 +7649,8 @@ public: testLoanPayLateFullPaymentBypassesPenalties(); testLoanCoverMinimumRoundingExploit(); #endif + testInvalidLoanSet(); + testCoverDepositWithdrawNonTransferableMPT(); testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); @@ -7045,12 +7662,9 @@ public: testServiceFeeOnBrokerDeepFreeze(); testRPC(); - testBasicMath(); - testInvalidLoanDelete(); testInvalidLoanManage(); testInvalidLoanPay(); - testInvalidLoanSet(); testBatchBypassCounterparty(); testLoanPayComputePeriodicPaymentValidRateInvariant(); @@ -7074,6 +7688,12 @@ public: testBorrowerIsBroker(); testIssuerIsBorrower(); testLimitExceeded(); + testOverpaymentManagementFee(); + testLoanPayBrokerOwnerMissingTrustline(); + testLoanPayBrokerOwnerUnauthorizedMPT(); + testLoanPayBrokerOwnerNoPermissionedDomainMPT(); + testLoanSetBrokerOwnerNoPermissionedDomainMPT(); + testSequentialFLCDepletion(); } }; @@ -7193,15 +7813,15 @@ class LoanArbitrary_test : public LoanBatch_test .vaultDeposit = 10000, .debtMax = 0, .coverRateMin = TenthBips32{0}, - // .managementFeeRate = TenthBips16{5919}, + .managementFeeRate = TenthBips16{0}, .coverRateLiquidation = TenthBips32{0}}; LoanParameters const loanParams{ .account = Account("lender"), .counter = Account("borrower"), - .principalRequest = Number{10000, 0}, - // .interest = TenthBips32{0}, - // .payTotal = 5816, - .payInterval = 150}; + .principalRequest = Number{200000, -6}, + .interest = TenthBips32{50000}, + .payTotal = 2, + .payInterval = 200}; runLoan(AssetType::XRP, brokerParams, loanParams); } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 747f78ef6b..65cb753755 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1594,7 +1594,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::secret] = alice.name(); jv[jss::tx_json] = pay(alice, bob, mpt); jv[jss::tx_json][jss::Amount][jss::value] = - to_string(maxMPTokenAmount + 1); + std::to_string(maxMPTokenAmount + 1); auto const jrr = env.rpc("json", "submit", to_string(jv)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); } @@ -2474,7 +2474,7 @@ class MPToken_test : public beast::unit_test::suite alice.name(), makeMptID(env.seq(alice), alice)); Json::Value jv = claw(alice, mpt(1), bob); - jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); + jv[jss::Amount][jss::value] = std::to_string(maxMPTokenAmount + 1); Json::Value jv1; jv1[jss::secret] = alice.name(); jv1[jss::tx_json] = jv; diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h index 0cb010b0e9..17a210441d 100644 --- a/src/test/app/TestHostFunctions.h +++ b/src/test/app/TestHostFunctions.h @@ -380,163 +380,146 @@ public: return 4; } - Expected - trace(std::string_view const& msg, Slice const& data, bool asHex) override + template + void + log(std::string_view const& msg, F&& dataFn) { #ifdef DEBUG_OUTPUT auto& j = std::cerr; #else + if (!getJournal().active(beast::severities::kTrace)) + return; auto j = getJournal().trace(); #endif - if (!asHex) - { - j << "WASM TRACE: " << msg << " " - << std::string_view( - reinterpret_cast(data.data()), data.size()); - } - else - { - std::string hex; - hex.reserve(data.size() * 2); - boost::algorithm::hex( - data.begin(), data.end(), std::back_inserter(hex)); - j << "WASM DEV TRACE: " << msg << " " << hex; - } + j << "WasmTrace: " << msg << " " << dataFn(); #ifdef DEBUG_OUTPUT j << std::endl; #endif + } - return msg.size() + data.size() * (asHex ? 2 : 1); + Expected + trace(std::string_view const& msg, Slice const& data, bool asHex) override + { + auto const ret = msg.size() + data.size() * (asHex ? 2 : 1); + + if (!asHex) + { + log(msg, [&data] { + return std::string_view( + reinterpret_cast(data.data()), data.size()); + }); + } + else + { + log(msg, [&data] { + std::string hex; + hex.reserve(data.size() * 2); + boost::algorithm::hex( + data.begin(), data.end(), std::back_inserter(hex)); + return hex; + }); + } + + return ret; } Expected traceNum(std::string_view const& msg, int64_t data) override { -#ifdef DEBUG_OUTPUT - auto& j = std::cerr; -#else - auto j = getJournal().trace(); -#endif - j << "WASM TRACE NUM: " << msg << " " << data; - -#ifdef DEBUG_OUTPUT - j << std::endl; -#endif - return msg.size() + sizeof(data); + auto const ret = msg.size() + sizeof(data); + log(msg, [data] { return data; }); + return ret; } Expected traceAccount(std::string_view const& msg, AccountID const& account) override { -#ifdef DEBUG_OUTPUT - auto& j = std::cerr; -#else - auto j = getJournal().trace(); -#endif - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - - auto const accountStr = toBase58(account); - - j << "WASM TRACE ACCOUNT: " << msg << " " << accountStr; - return msg.size() + accountStr.size(); + auto const ret = msg.size() + account.size(); + log(msg, [&account] { return toBase58(account); }); + return ret; } Expected traceFloat(std::string_view const& msg, Slice const& data) override { -#ifdef DEBUG_OUTPUT - auto& j = std::cerr; -#else - auto j = getJournal().trace(); -#endif - auto const s = floatToString(data); - j << "WASM TRACE FLOAT: " << msg << " " << s; - -#ifdef DEBUG_OUTPUT - j << std::endl; -#endif - return msg.size() + s.size(); + auto const ret = msg.size() + data.size(); + log(msg, [&data] { return wasm_float::floatToString(data); }); + return ret; } Expected traceAmount(std::string_view const& msg, STAmount const& amount) override { -#ifdef DEBUG_OUTPUT - auto& j = std::cerr; -#else - auto j = getJournal().trace(); -#endif - auto const amountStr = amount.getFullText(); - j << "WASM TRACE AMOUNT: " << msg << " " << amountStr; - return msg.size() + amountStr.size(); + auto const ret = msg.size(); + log(msg, [&amount] { return amount.getFullText(); }); + return ret; } Expected floatFromInt(int64_t x, int32_t mode) override { - return floatFromIntImpl(x, mode); + return wasm_float::floatFromIntImpl(x, mode); } Expected floatFromUint(uint64_t x, int32_t mode) override { - return floatFromUintImpl(x, mode); + return wasm_float::floatFromUintImpl(x, mode); } Expected floatSet(int64_t mantissa, int32_t exponent, int32_t mode) override { - return floatSetImpl(mantissa, exponent, mode); + return wasm_float::floatSetImpl(mantissa, exponent, mode); } Expected floatCompare(Slice const& x, Slice const& y) override { - return floatCompareImpl(x, y); + return wasm_float::floatCompareImpl(x, y); } Expected floatAdd(Slice const& x, Slice const& y, int32_t mode) override { - return floatAddImpl(x, y, mode); + return wasm_float::floatAddImpl(x, y, mode); } Expected floatSubtract(Slice const& x, Slice const& y, int32_t mode) override { - return floatSubtractImpl(x, y, mode); + return wasm_float::floatSubtractImpl(x, y, mode); } Expected floatMultiply(Slice const& x, Slice const& y, int32_t mode) override { - return floatMultiplyImpl(x, y, mode); + return wasm_float::floatMultiplyImpl(x, y, mode); } Expected floatDivide(Slice const& x, Slice const& y, int32_t mode) override { - return floatDivideImpl(x, y, mode); + return wasm_float::floatDivideImpl(x, y, mode); } Expected floatRoot(Slice const& x, int32_t n, int32_t mode) override { - return floatRootImpl(x, n, mode); + return wasm_float::floatRootImpl(x, n, mode); } Expected floatPower(Slice const& x, int32_t n, int32_t mode) override { - return floatPowerImpl(x, n, mode); + return wasm_float::floatPowerImpl(x, n, mode); } Expected floatLog(Slice const& x, int32_t mode) override { - return floatLogImpl(x, mode); + return wasm_float::floatLogImpl(x, mode); } }; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index d0a1450d6c..41a4fc2b3b 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2076,7 +2076,7 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { - testcase("MPT failed reserve to re-create MPToken"); + testcase("MPT fail reserve to re-create MPToken"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -5324,7 +5324,7 @@ class Vault_test : public beast::unit_test::suite // Create a simple Loan for the full amount of Vault assets env(set(depositor, brokerKeylet.key, asset(100).value()), loan::interestRate(TenthBips32(0)), - gracePeriod(10), + gracePeriod(60), paymentInterval(120), paymentTotal(10), sig(sfCounterpartySignature, owner), @@ -5344,7 +5344,7 @@ class Vault_test : public beast::unit_test::suite THISLINE); env.close(); - env.close(std::chrono::seconds{120 + 10}); + env.close(std::chrono::seconds{120 + 60}); env(manage(owner, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS), diff --git a/src/test/app/Wasm_test.cpp b/src/test/app/Wasm_test.cpp index 81e3245803..66a15d0a36 100644 --- a/src/test/app/Wasm_test.cpp +++ b/src/test/app/Wasm_test.cpp @@ -84,9 +84,9 @@ struct Wasm_test : public beast::unit_test::suite // clang-format on auto& vm = WasmEngine::instance(); - ImportVec imports; + std::shared_ptr imports(std::make_shared()); WasmImpFunc( - imports, "func-add", reinterpret_cast(&Add)); + *imports, "func-add", reinterpret_cast(&Add)); auto re = vm.run(wasm, "addTwo", wasmParams(1234, 5678), imports); @@ -95,7 +95,7 @@ struct Wasm_test : public beast::unit_test::suite if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 6'912, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 3, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 59, std::to_string(re->cost)); } } @@ -107,7 +107,7 @@ struct Wasm_test : public beast::unit_test::suite using namespace test::jtx; Env env{*this}; - HostFunctions hfs(env.journal); + std::shared_ptr hfs(new HostFunctions(env.journal)); { auto wasmHex = "00000000"; @@ -165,10 +165,10 @@ struct Wasm_test : public beast::unit_test::suite using namespace test::jtx; Env env{*this}; - TestLedgerDataProvider hf(env); - - ImportVec imports; - WASM_IMPORT_FUNC2(imports, getLedgerSqn, "get_ledger_sqn", &hf, 33); + std::shared_ptr hfs(new TestLedgerDataProvider(env)); + auto imports = std::make_shared(); + WASM_IMPORT_FUNC2( + *imports, getLedgerSqn, "get_ledger_sqn", hfs.get(), 33); auto& engine = WasmEngine::instance(); auto re = engine.run( @@ -176,14 +176,14 @@ struct Wasm_test : public beast::unit_test::suite ESCROW_FUNCTION_NAME, {}, imports, - &hf, + hfs, 1'000'000, env.journal); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 0, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 38, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 151, std::to_string(re->cost)); } env.close(); @@ -191,12 +191,12 @@ struct Wasm_test : public beast::unit_test::suite // empty module - run the same instance re = engine.run( - {}, ESCROW_FUNCTION_NAME, {}, imports, &hf, 1'000'000, env.journal); + {}, ESCROW_FUNCTION_NAME, {}, imports, hfs, 1'000'000, env.journal); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 5, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 76, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 190, std::to_string(re->cost)); } } @@ -214,7 +214,7 @@ struct Wasm_test : public beast::unit_test::suite if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 55, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 696, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 1'137, std::to_string(re->cost)); } } @@ -233,7 +233,7 @@ struct Wasm_test : public beast::unit_test::suite if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 34'432, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 145'573, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 151'155, std::to_string(re->cost)); } } @@ -258,7 +258,7 @@ struct Wasm_test : public beast::unit_test::suite if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 700, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 2'701'528, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 2'886'069, std::to_string(re->cost)); } } @@ -277,9 +277,9 @@ struct Wasm_test : public beast::unit_test::suite auto& engine = WasmEngine::instance(); - TestHostFunctions hfs(env, 0); - ImportVec imp = createWasmImport(hfs); - for (auto& i : imp) + std::shared_ptr hfs(new TestHostFunctions(env, 0)); + auto imp = createWasmImport(*hfs); + for (auto& i : *imp) i.second.gas = 0; auto re = engine.run( @@ -287,14 +287,14 @@ struct Wasm_test : public beast::unit_test::suite ESCROW_FUNCTION_NAME, {}, imp, - &hfs, + hfs, 1'000'000, env.journal); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 1, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 842, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 25'503, std::to_string(re->cost)); } env.close(); @@ -313,22 +313,22 @@ struct Wasm_test : public beast::unit_test::suite auto& engine = WasmEngine::instance(); - TestHostFunctions hfs(env, 0); - ImportVec const imp = createWasmImport(hfs); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); + auto const imp = createWasmImport(*hfs); auto re = engine.run( wasm, ESCROW_FUNCTION_NAME, {}, imp, - &hfs, + hfs, 1'000'000, env.journal); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 1, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 39'602, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 64'263, std::to_string(re->cost)); } env.close(); @@ -342,11 +342,11 @@ struct Wasm_test : public beast::unit_test::suite auto& engine = WasmEngine::instance(); - TestHostFunctions hfs(env, 0); - ImportVec const imp = createWasmImport(hfs); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); + auto const imp = createWasmImport(*hfs); auto re = engine.run( - wasm, ESCROW_FUNCTION_NAME, {}, imp, &hfs, 200, env.journal); + wasm, ESCROW_FUNCTION_NAME, {}, imp, hfs, 200, env.journal); if (BEAST_EXPECT(!re)) { @@ -371,24 +371,24 @@ struct Wasm_test : public beast::unit_test::suite using namespace test::jtx; Env env{*this}; { - TestHostFunctions nfs(env, 0); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); auto re = - runEscrowWasm(wasm, nfs, ESCROW_FUNCTION_NAME, {}, 100'000); + runEscrowWasm(wasm, hfs, ESCROW_FUNCTION_NAME, {}, 100'000); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 1, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 39'602, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 64'263, std::to_string(re->cost)); } } { // max() gas - TestHostFunctions nfs(env, 0); - auto re = runEscrowWasm(wasm, nfs, ESCROW_FUNCTION_NAME, {}, -1); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); + auto re = runEscrowWasm(wasm, hfs, ESCROW_FUNCTION_NAME, {}, -1); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 1, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 39'602, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 64'263, std::to_string(re->cost)); } } @@ -404,13 +404,14 @@ struct Wasm_test : public beast::unit_test::suite return Unexpected(HostFunctionError::FIELD_NOT_FOUND); } }; - BadTestHostFunctions nfs(env); + + std::shared_ptr hfs(new BadTestHostFunctions(env)); auto re = - runEscrowWasm(wasm, nfs, ESCROW_FUNCTION_NAME, {}, 100'000); + runEscrowWasm(wasm, hfs, ESCROW_FUNCTION_NAME, {}, 100'000); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == -201, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 5'012, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 28'148, std::to_string(re->cost)); } } @@ -426,13 +427,14 @@ struct Wasm_test : public beast::unit_test::suite return Bytes((128 + 1) * 64 * 1024, 1); } }; - BadTestHostFunctions nfs(env); + + std::shared_ptr hfs(new BadTestHostFunctions(env)); auto re = - runEscrowWasm(wasm, nfs, ESCROW_FUNCTION_NAME, {}, 100'000); + runEscrowWasm(wasm, hfs, ESCROW_FUNCTION_NAME, {}, 100'000); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == -201, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 5'012, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 28'148, std::to_string(re->cost)); } } @@ -441,14 +443,15 @@ struct Wasm_test : public beast::unit_test::suite auto const wasmStr = boost::algorithm::unhex(deepRecursionHex); std::vector wasm(wasmStr.begin(), wasmStr.end()); - TestHostFunctionsSink nfs(env); + std::shared_ptr hfs( + new TestHostFunctionsSink(env)); std::string funcName("finish"); - auto re = runEscrowWasm(wasm, nfs, funcName, {}, 1'000'000'000); + auto re = runEscrowWasm(wasm, hfs, funcName, {}, 1'000'000'000); BEAST_EXPECT(!re && re.error()); // std::cout << "bad case (deep recursion) result " << re.error() // << std::endl; - auto const& sink = nfs.getSink(); + auto const& sink = hfs->getSink(); auto countSubstr = [](std::string const& str, std::string const& substr) { std::size_t pos = 0; @@ -471,7 +474,7 @@ struct Wasm_test : public beast::unit_test::suite auto const wasmStr = boost::algorithm::unhex(infiniteLoopWasmHex); Bytes wasm(wasmStr.begin(), wasmStr.end()); std::string const funcName("loop"); - TestHostFunctions hfs(env, 0); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); // infinite loop should be caught and fail auto const re = runEscrowWasm(wasm, hfs, funcName, {}, 1'000'000); @@ -485,11 +488,10 @@ struct Wasm_test : public beast::unit_test::suite // expected import not provided auto wasmStr = boost::algorithm::unhex(ledgerSqnWasmHex); Bytes wasm(wasmStr.begin(), wasmStr.end()); - TestLedgerDataProvider ledgerDataProvider(env); - - ImportVec imports; + std::shared_ptr hfs(new TestLedgerDataProvider(env)); + std::shared_ptr imports(std::make_shared()); WASM_IMPORT_FUNC2( - imports, getLedgerSqn, "get_ledger_sqn2", &ledgerDataProvider); + *imports, getLedgerSqn, "get_ledger_sqn2", hfs.get()); auto& engine = WasmEngine::instance(); @@ -498,7 +500,7 @@ struct Wasm_test : public beast::unit_test::suite ESCROW_FUNCTION_NAME, {}, imports, - &ledgerDataProvider, + hfs, 1'000'000, env.journal); @@ -509,12 +511,11 @@ struct Wasm_test : public beast::unit_test::suite // bad import format auto wasmStr = boost::algorithm::unhex(ledgerSqnWasmHex); Bytes wasm(wasmStr.begin(), wasmStr.end()); - TestLedgerDataProvider ledgerDataProvider(env); - - ImportVec imports; + std::shared_ptr hfs(new TestLedgerDataProvider(env)); + std::shared_ptr imports(std::make_shared()); WASM_IMPORT_FUNC2( - imports, getLedgerSqn, "get_ledger_sqn", &ledgerDataProvider); - imports[0].first = nullptr; + *imports, getLedgerSqn, "get_ledger_sqn", hfs.get()); + (*imports)[0].first = nullptr; auto& engine = WasmEngine::instance(); @@ -523,7 +524,7 @@ struct Wasm_test : public beast::unit_test::suite ESCROW_FUNCTION_NAME, {}, imports, - &ledgerDataProvider, + hfs, 1'000'000, env.journal); @@ -534,21 +535,14 @@ struct Wasm_test : public beast::unit_test::suite // bad function name auto wasmStr = boost::algorithm::unhex(ledgerSqnWasmHex); Bytes wasm(wasmStr.begin(), wasmStr.end()); - TestLedgerDataProvider ledgerDataProvider(env); - - ImportVec imports; + std::shared_ptr hfs(new TestLedgerDataProvider(env)); + std::shared_ptr imports(std::make_shared()); WASM_IMPORT_FUNC2( - imports, getLedgerSqn, "get_ledger_sqn", &ledgerDataProvider); + *imports, getLedgerSqn, "get_ledger_sqn", hfs.get()); auto& engine = WasmEngine::instance(); auto re = engine.run( - wasm, - "func1", - {}, - imports, - &ledgerDataProvider, - 1'000'000, - env.journal); + wasm, "func1", {}, imports, hfs, 1'000'000, env.journal); BEAST_EXPECT(!re); } @@ -569,12 +563,12 @@ struct Wasm_test : public beast::unit_test::suite std::string const wasmStr = boost::algorithm::unhex(wasmHex); std::vector const wasm(wasmStr.begin(), wasmStr.end()); - TestHostFunctions hf(env, 0); - auto re = runEscrowWasm(wasm, hf, funcName, {}, 100'000); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); + auto re = runEscrowWasm(wasm, hfs, funcName, {}, 200'000); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 1, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 97'356, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 110'699, std::to_string(re->cost)); } env.close(); } @@ -584,12 +578,12 @@ struct Wasm_test : public beast::unit_test::suite std::string const wasmStr = boost::algorithm::unhex(wasmHex); std::vector const wasm(wasmStr.begin(), wasmStr.end()); - TestHostFunctions hf(env, 0); - auto re = runEscrowWasm(wasm, hf, funcName, {}, 100'000); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); + auto re = runEscrowWasm(wasm, hfs, funcName, {}, 100'000); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECTS(re->result == 1, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 2'054, std::to_string(re->cost)); + BEAST_EXPECTS(re->cost == 4'259, std::to_string(re->cost)); } env.close(); } @@ -671,9 +665,10 @@ struct Wasm_test : public beast::unit_test::suite env(token::mint(alan, 0u)); env.close(); - PerfHostFunctions nfs(env, k, env.tx()); + std::shared_ptr hfs( + new PerfHostFunctions(env, k, env.tx())); - auto re = runEscrowWasm(wasm, nfs, ESCROW_FUNCTION_NAME); + auto re = runEscrowWasm(wasm, hfs, ESCROW_FUNCTION_NAME); if (BEAST_EXPECT(re.has_value())) { BEAST_EXPECT(re->result); @@ -701,9 +696,9 @@ struct Wasm_test : public beast::unit_test::suite auto const wasmStr = boost::algorithm::unhex(codecovTestsWasmHex); Bytes const wasm(wasmStr.begin(), wasmStr.end()); - TestHostFunctions hfs(env, 0); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); - auto const allowance = 292'345; + auto const allowance = 323'774; auto re = runEscrowWasm(wasm, hfs, ESCROW_FUNCTION_NAME, {}, allowance); if (BEAST_EXPECT(re.has_value())) @@ -724,7 +719,7 @@ struct Wasm_test : public beast::unit_test::suite auto const wasmStr = boost::algorithm::unhex(disabledFloatHex); Bytes wasm(wasmStr.begin(), wasmStr.end()); std::string const funcName("finish"); - TestHostFunctions hfs(env, 0); + std::shared_ptr hfs(new TestHostFunctions(env, 0)); { // f32 set constant, opcode disabled exception @@ -804,6 +799,14 @@ struct Wasm_test : public beast::unit_test::suite runFinishFunction(proposalTailCallHex).has_value() == false); BEAST_EXPECT( runFinishFunction(proposalExtendedConstHex).has_value() == false); + BEAST_EXPECT( + runFinishFunction(proposalMultiMemoryHex).has_value() == false); + BEAST_EXPECT( + runFinishFunction(proposalCustomPageSizesHex).has_value() == false); + BEAST_EXPECT( + runFinishFunction(proposalMemory64Hex).has_value() == false); + BEAST_EXPECT( + runFinishFunction(proposalWideArithmeticHex).has_value() == false); } void @@ -860,12 +863,12 @@ struct Wasm_test : public beast::unit_test::suite auto wasmStr = boost::algorithm::unhex(startLoopHex); Bytes wasm(wasmStr.begin(), wasmStr.end()); - TestLedgerDataProvider ledgerDataProvider(env); - ImportVec imports; + std::shared_ptr hfs(new TestLedgerDataProvider(env)); + std::shared_ptr imports(std::make_shared()); auto& engine = WasmEngine::instance(); - auto checkRes = engine.check( - wasm, "finish", {}, imports, &ledgerDataProvider, env.journal); + auto checkRes = + engine.check(wasm, "finish", {}, imports, hfs, env.journal); BEAST_EXPECTS( checkRes == tesSUCCESS, std::to_string(TERtoInt(checkRes))); @@ -874,7 +877,7 @@ struct Wasm_test : public beast::unit_test::suite ESCROW_FUNCTION_NAME, {}, imports, - &ledgerDataProvider, + hfs, 1'000'000, env.journal); BEAST_EXPECTS( @@ -894,9 +897,9 @@ struct Wasm_test : public beast::unit_test::suite using namespace test::jtx; Env env{*this}; - TestLedgerDataProvider hf(env); + std::shared_ptr hfs(new TestLedgerDataProvider(env)); - // ImportVec imports; + // std::shared_ptr imports(std::make_shared()); uint8_t buf1[8] = {7, 8, 9, 10, 11, 12, 13, 14}; { // forged "allocate" return valid address std::vector params = { @@ -905,11 +908,11 @@ struct Wasm_test : public beast::unit_test::suite auto& engine = WasmEngine::instance(); auto re = engine.run( - wasm, "test", params, {}, &hf, 1'000'000, env.journal); + wasm, "test", params, {}, hfs, 1'000'000, env.journal); if (BEAST_EXPECT(re)) { BEAST_EXPECTS(re->result == 7, std::to_string(re->result)); - BEAST_EXPECTS(re->cost == 10, std::to_string(re->result)); + BEAST_EXPECTS(re->cost == 430, std::to_string(re->cost)); } } @@ -918,7 +921,7 @@ struct Wasm_test : public beast::unit_test::suite {.type = WT_U8V, .of = {.u8v = {.d = buf1, .sz = 0}}}}; auto& engine = WasmEngine::instance(); auto re = engine.run( - wasm, "test", params, {}, &hf, 1'000'000, env.journal); + wasm, "test", params, {}, hfs, 1'000'000, env.journal); BEAST_EXPECT(!re) && BEAST_EXPECT(re.error() == tecFAILED_PROCESSING); } @@ -928,7 +931,7 @@ struct Wasm_test : public beast::unit_test::suite {.type = WT_U8V, .of = {.u8v = {.d = buf1, .sz = 1}}}}; auto& engine = WasmEngine::instance(); auto re = engine.run( - wasm, "test", params, {}, &hf, 1'000'000, env.journal); + wasm, "test", params, {}, hfs, 1'000'000, env.journal); BEAST_EXPECT(!re) && BEAST_EXPECT(re.error() == tecFAILED_PROCESSING); } @@ -938,7 +941,7 @@ struct Wasm_test : public beast::unit_test::suite {.type = WT_U8V, .of = {.u8v = {.d = buf1, .sz = 2}}}}; auto& engine = WasmEngine::instance(); auto re = engine.run( - wasm, "test", params, {}, &hf, 1'000'000, env.journal); + wasm, "test", params, {}, hfs, 1'000'000, env.journal); BEAST_EXPECT(!re) && BEAST_EXPECT(re.error() == tecFAILED_PROCESSING); } @@ -948,7 +951,7 @@ struct Wasm_test : public beast::unit_test::suite {.type = WT_U8V, .of = {.u8v = {.d = buf1, .sz = 3}}}}; auto& engine = WasmEngine::instance(); auto re = engine.run( - wasm, "test", params, {}, &hf, 1'000'000, env.journal); + wasm, "test", params, {}, hfs, 1'000'000, env.journal); BEAST_EXPECT(!re) && BEAST_EXPECT(re.error() == tecFAILED_PROCESSING); @@ -957,6 +960,33 @@ struct Wasm_test : public beast::unit_test::suite env.close(); } + void + testBadAlign() + { + testcase("Wasm Bad Align"); + + // bad_align.c + auto wasmStr = boost::algorithm::unhex(badAlignHex); + Bytes wasm(wasmStr.begin(), wasmStr.end()); + + using namespace test::jtx; + + Env env{*this}; + std::shared_ptr hfs(new TestHostFunctions(env, 0)); + auto imports = createWasmImport(*hfs); + + { // Calls float_from_uint with bad aligment. + // Can be checked through codecov + auto& engine = WasmEngine::instance(); + + auto re = engine.run( + wasm, "test", {}, imports, hfs, 1'000'000, env.journal); + BEAST_EXPECT(re && re->result == 0xbab88d46); + } + + env.close(); + } + void run() override { @@ -987,6 +1017,7 @@ struct Wasm_test : public beast::unit_test::suite testStartFunctionLoop(); testBadAlloc(); + testBadAlign(); // perfTest(); } diff --git a/src/test/app/wasm_fixtures/bad_align.c b/src/test/app/wasm_fixtures/bad_align.c new file mode 100644 index 0000000000..ff29456c2a --- /dev/null +++ b/src/test/app/wasm_fixtures/bad_align.c @@ -0,0 +1,23 @@ +#include + +int32_t +float_from_uint(uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); +int32_t +get_tx_nested_field(uint8_t const*, int32_t, uint8_t*, int32_t); + +uint8_t e_data[32 * 1024]; + +int32_t +test() +{ + e_data[1] = 0xFF; + e_data[2] = 0xFF; + e_data[3] = 0xFF; + e_data[4] = 0xFF; + e_data[5] = 0xFF; + e_data[6] = 0xFF; + e_data[7] = 0xFF; + e_data[8] = 0xFF; + float_from_uint(&e_data[1], 8, &e_data[35], 12, 0); + return *((int32_t*)(&e_data[36])); +} diff --git a/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs b/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs index 4cbc7e1707..a5aa2eb6e5 100644 --- a/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs +++ b/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs @@ -248,7 +248,7 @@ pub extern "C" fn finish() -> i32 { account.0.len(), ) }, - 47, + (message.len() + 20) as i32, "trace_account", ); let amount = &[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F]; // 95 drops of XRP @@ -261,7 +261,7 @@ pub extern "C" fn finish() -> i32 { amount.len(), ) }, - 19, + message.len() as i32, "trace_amount", ); let amount = &[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // 0 drops of XRP @@ -274,7 +274,7 @@ pub extern "C" fn finish() -> i32 { amount.len(), ) }, - 18, + message.len() as i32, "trace_amount_zero", ); @@ -307,32 +307,6 @@ pub extern "C" fn finish() -> i32 { "get_parent_ledger_hash_len_too_long", ) }); - let message = "testing trace"; - check_result( - unsafe { - host::trace_account( - message.as_ptr(), - message.len(), - account.0.as_ptr(), - account.0.len(), - ) - }, - 47, - "trace_account", - ); - let amount = &[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F]; // 95 drops of XRP - check_result( - unsafe { - host::trace_amount( - message.as_ptr(), - message.len(), - amount.as_ptr(), - amount.len(), - ) - }, - 19, - "trace_amount", - ); // ######################################## // Step #3: Test getData[Type] edge cases diff --git a/src/test/app/wasm_fixtures/fixtures.cpp b/src/test/app/wasm_fixtures/fixtures.cpp index 4e67b074a7..43f0bb5db4 100644 --- a/src/test/app/wasm_fixtures/fixtures.cpp +++ b/src/test/app/wasm_fixtures/fixtures.cpp @@ -661,251 +661,251 @@ extern std::string const codecovTestsWasmHex = "61696e5f6b65796c6574000108686f73745f6c69620e7369676e6572735f6b65796c657400" "0008686f73745f6c69620d7469636b65745f6b65796c6574000108686f73745f6c69620c76" "61756c745f6b65796c657400010303020a0505030100110619037f01418080c0000b7f0041" - "a99ec0000b7f0041b09ec0000b072e04066d656d6f727902000666696e697368003d0a5f5f" - "646174615f656e6403010b5f5f686561705f6261736503020a9e2702460002402000200147" - "04402002200341004100410010001a20004100480d01418b80c000410b2000ad1001000b20" - "0220032000ac10011a0f0b418b80c000410b2000ac1001000bd426020a7f017e230041f001" - "6b22002400419680c000412341004100410010001a100241b9e00041b980c000410e103c10" - "0341b2920441c780c0004116103c200041f0006a22044200370300200041e8006a22064200" - "370300200041e0006a2203420037030020004200370358200041d8006a2201412010044120" - "41d88cc0004116103c1005410a41dd80c000410c103c200041186a2207428182848890a0c0" - "8001370300200041106a2208428182848890a0c08001370300200041086a22094281828488" - "90a0c080013703002000428182848890a0c0800137030041e980c000410e1006410141f780" - "c0004111103c200041201006410141f780c0004111103c4181802020014114100722024114" - "4604400240200041266a200041da006a2d00003a00002000200029005f3703c80120002000" - "41e4006a2900003700cd01200041306a20002900cd01370000200020002f00583b01242000" - "200028005b360027200020002903c80137002b200442003703002006420037030020034200" - "37030020004200370358200041246a2204411420014120100822024120470d002000413a6a" - "20002d005a3a0000200041d0016a2202200041e7006a290000220a370300200041c7006a20" - "0a370000200041cf006a200041ef006a290000370000200041d7006a200041f7006a2d0000" - "3a0000200020002f01583b01382000200028005b36003b2000200029005f37003f20004138" - "6a4120410010094101418881c0004110103c20064100360200200342003703002000420037" - "03584181802020014114100a411441ee8cc000411c103c2006410036020020034200370300" - "2000420037035841014181802020014114100b4114418a8dc0004114103c20004104360298" - "01200041818020360258200041d8016a2203410036020020024200370300200042003703c8" - "0120014104200041c8016a22064114100c4114419e8dc0004113103c200341003602002002" - "4200370300200042003703c801200120002802980120064114100d411441b18dc000412310" - "3c2003410036020020024200370300200042003703c8014101200120002802980120064114" - "100e411441d48dc000411b103c4189803c100f4120419881c0004110103c4189803c101041" - "2041a881c0004120103c41014189803c1011412041c881c0004118103c2001200028029801" - "1012412041e081c0004117103c20012000280298011013412041f781c0004127103c410120" - "0120002802980110144120419e82c000411f103c200441141015411441bd82c000410b103c" - "200041e0016a220542003703002003420037030020024200370300200042003703c8012001" - "200028029801200641201016412041ef8dc0004113103c41c882c000410c41d482c000410b" - "41df82c000410e1017410141ed82c0004109103c200041b8016a2007290300370300200041" - "b0016a2008290300370300200041a8016a2009290300370300200020002903003703a00120" - "0341003b010020024200370300200042003703c80120044114200041a0016a220741202006" - "41121018411241828ec0004107103c2003410036020020024200370300200042003703c801" - "20074120200641141019411441898ec000410e103c200041003602c8012007412020064104" - "101a410441978ec000410d103c20074120101b410841f682c000410d103c20074120101c41" - "0a418383c0004114103c200041003602c8012007412020064104101d410441a48ec000410e" - "103c419783c000410d20044114101e412f41a483c000410d103c419783c000410d41b183c0" - "004108101f411341b983c000410c103c419783c000410d41c583c0004108101f411241cd83" - "c0004111103c417f41041004417141de83c000411e103c200041003602c8012006417f1004" - "417141b28ec000411e103c200041ca016a41003a0000200041003b01c80120064103100441" - "7d41d08ec0004124103c200041003602c8012006418094ebdc031004417341f48ec0004123" - "103c419783c000410d20044114101e412f41a483c000410d103c419783c000410d41b183c0" - "004108101f411341b983c000410c103c200542003703002003420037030020024200370300" - "200042003703c801200041d894ebdc036a220741082006412041001020417341978fc00041" - "17103c200542003703002003420037030020024200370300200042003703c8012001200028" - "0298012006412041001020417141ae8fc0004119103c4102100f416f41fc83c000411f103c" - "417f20002802980110124171419b84c000411f103c2001417f1012417141ba84c000411f10" - "3c20014181201012417441d984c0004120103c20072000280298011012417341f984c00041" - "1f103c2007200028029801410110094173419885c0004118103c2001200028029801410110" - "09417141b085c000411a103c20054200370300200342003703002002420037030020004200" - "3703c8012007200028029801200641201008417341c78fc0004116103c2005420037030020" - "03420037030020024200370300200042003703c80120012000280298012006412010084171" - "41dd8fc0004118103c200542003703002003420037030020024200370300200042003703c8" - "0120044114200441142007200028029801200641201021417341f58fc000411c103c200542" - "003703002003420037030020024200370300200042003703c8012004411420044114200120" - "00280298012006412010214171419190c000411e103c200542003703002003420037030020" - "024200370300200042003703c80141959ec000411420072000280298012006412010224173" - "41af90c0004119103c200542003703002003420037030020024200370300200042003703c8" - "0141959ec00041142001200028029801200641201022417141c890c000411f103c20054200" - "3703002003420037030020024200370300200042003703c80141959ec000411441ca85c000" - "4114200641201022417141e790c0004129103c200542003703002003420037030020024200" - "370300200042003703c80141de85c000412841959ec00041142006412010224171419091c0" - "004125103c200041dc016a200041346a280100360200200041d4016a2000412c6a29010037" - "0200200020002901243702cc01200041808080083602c801200041003b01c0012006411841" - "959ec0004114200041c0016a220341021022417141b591c000410e103c2007200028029801" - "422a10014173418686c0004111103c200041003b01c0014102200341021007416f41c391c0" - "00411b103c200041003b01c001410220034102100a416f41de91c000412b103c200041003b" - "01c0014101410220034102100b416f418992c0004123103c4102100f416f41fc83c000411f" - "103c41021010416f419786c000412f103c410141021011416f41c686c0004127103c41e980" - "c0004181201006417441ed86c000411f103c41e980c00041c10010064174418c87c000411a" - "103c200041003b01c001200141812020034102100c417441ac92c0004121103c200041003b" - "01c001200141812020034102100d417441cd92c0004131103c200041003b01c00141012001" - "41812020034102100e417441fe92c0004129103c20014181201012417441a687c000412510" - "3c20014181201013417441cb87c0004135103c4101200141812010144174418088c000412d" - "103c20014181201015417441ad88c0004119103c419783c00041812041d482c000410b41df" - "82c000410e1017417441ed82c0004109103c419783c000410d41d482c00041812041df82c0" - "00410e1017417441ed82c0004109103c419783c000410d41d482c000410b41df82c0004181" - "201017417441ed82c0004109103c200041003b01c0012001418120200341021016417441a7" - "93c0004121103c200041003b01c00141959ec00041812041959ec000411420034102102241" - "7441c893c0004118103c200041003b01c00120044114200441142001418120200341021023" - "417441e093c000411f103c200041003b01c001200641812020044114200341021024417441" - "ff93c0004122103c419783c000410d200720002802980141001000417341c688c000410f10" - "3c200042d487b6f4c7d4b1c0003700c001419783c000410d200041c095ebdc036a22054108" - "1025417341d588c000411c103c419783c000410d2007200028029801101f417341f188c000" - "4116103c200541082003410810264173418789c0004118103c200341082005410810264173" - "419f89c0004118103c200041003b01ec012005410820034108200041ec016a220241024100" - "1027417341a194c0004114103c200041003b01ec0120034108200541082002410241001027" - "417341b594c0004114103c200041003b01ec01200541082003410820024102410010284173" - "41c994c0004119103c200041003b01ec0120034108200541082002410241001028417341e2" - "94c0004119103c200041003b01ec0120054108200341082002410241001029417341fb94c0" - "004119103c200041003b01ec01200341082005410820024102410010294173419495c00041" - "19103c200041003b01ec012005410820034108200241024100102a417341ad95c000411710" - "3c200041003b01ec012003410820054108200241024100102a417341c495c0004117103c20" - "0041003b01ec01200541084103200241024100102b417341db95c0004114103c200041003b" - "01ec01200541084103200241024100102c417341ef95c0004113103c200041003b01ec0120" - "054108200241024100102d4173418296c0004113103c200120002802980141001009417141" - "b789c0004123103c200041003b01ec01200441142001200028029801200241021018417141" - "9596c000411a103c200041003b01ec012001200028029801200241021019417141af96c000" - "4121103c200041003b01ec01200120002802980120024102101a417141d096c0004120103c" - "2001200028029801101b417141da89c0004120103c2001200028029801101c417141fa89c0" - "004127103c200041003602ec01200120002802980120024104101d417141f096c000412110" - "3c200041003b01ec0120012000280298012002410210084171419197c0004123103c200041" - "003b01ec012001200028029801410120024102102e417141b497c0004121103c200041003b" - "01ec01200120002802980122052004411420012005200241021023417141d597c000412710" - "3c200041003b01ec01200441142001200028029801220520012005200241021023417141fc" - "97c0004127103c200041003b01ec0120012000280298012004411420024102102f417141a3" - "98c0004125103c200041003b01ec0120044114200120002802980120024102102f417141c8" - "98c0004125103c200041003b01ec01200120002802980120044114200241021030417141ed" - "98c000412c103c200041003b01ec0120044114200120002802980120024102103041714199" - "99c000412c103c200041003b01ec012001200028029801200241021031417141c599c00041" - "1f103c200041003b01ec0120012000280298014101200241021032417141e499c000412210" - "3c200041003b01ec0120012000280298012004411441ca85c0004114200241021021417141" - "869ac0004121103c200041003b01ec0120044114200120002802980141ca85c00041142002" - "41021021417141a79ac0004121103c200041003b01ec012001200028029801410120024102" - "1033417141c89ac0004128103c200041003b01ec0120064118200120002802980120024102" - "1024417141f09ac0004123103c200041003b01ec0120012000280298014101200241021034" - "417141939bc0004125103c200041003b01ec01200120002802980141012002410210354171" - "41b89bc0004121103c200041003b01ec0120012000280298014101200241021036417141d9" - "9bc0004122103c200041003b01ec0120012000280298012004411441012002410210374171" - "41fb9bc0004124103c200041003b01ec012004411420012000280298014101200241021037" - "4171419f9cc0004124103c200041003b01ec01200120002802980141012002410210384171" - "41c39cc000412f103c200041003b01ec012001200028029801200241021039417141f29cc0" - "004123103c200041003b01ec012001200028029801410120024102103a417141959dc00041" - "22103c200041003b01ec012001200028029801410120024102103b417141b79dc000412110" - "3c200041003b01ec01200120002802980141a18ac0004120200241021018417141d89dc000" - "411c103c419783c000410d2001200028029801101e417141c18ac0004122103c419797abdd" - "03410d41a18ac000412041001000417341e38ac0004110103c419797abdd03410d20034108" - "1025417341f38ac000411d103c419797abdd03410d20044114101e417341908bc000411810" - "3c419797abdd03410d41b183c0004108101f417341a88bc0004117103c2001200028029801" - "200141812041001000417441bf8bc000410e103c200141812042011001417441cd8bc00041" - "12103c419783c000418120200341081025417441df8bc000411b103c419783c00041812020" - "044114101e417441fa8bc0004116103c419783c00041812041b183c0004108101f41744190" - "8cc0004115103c419783c000410d2001200028029801101f417141a58cc0004119103c2000" - "41003b01ec01200120002802980120044114200241021024417141f49dc0004121103c4101" - "410020044114101e412241be8cc000411a103c200041f0016a240041010f0b0b418080c000" - "410b417f20022002417f4e1bac1001000b0b801e0200418080c0000bde056572726f725f63" - "6f64653d54455354204641494c45442424242424205354415254494e47205741534d204558" - "45435554494f4e2024242424246765745f6c65646765725f73716e6765745f706172656e74" - "5f6c65646765725f74696d656765745f626173655f666565746573745f616d656e646d656e" - "74616d656e646d656e745f656e61626c656463616368655f6c65646765725f6f626a676574" - "5f74785f61727261795f6c656e6765745f63757272656e745f6c65646765725f6f626a5f61" - "727261795f6c656e6765745f6c65646765725f6f626a5f61727261795f6c656e6765745f74" - "785f6e65737465645f61727261795f6c656e6765745f63757272656e745f6c65646765725f" - "6f626a5f6e65737465645f61727261795f6c656e6765745f6c65646765725f6f626a5f6e65" - "737465645f61727261795f6c656e7570646174655f6461746174657374206d657373616765" - "74657374207075626b657974657374207369676e6174757265636865636b5f736967676574" - "5f6e66745f666c6167736765745f6e66745f7472616e736665725f66656574657374696e67" - "20747261636574726163655f6163636f756e74400000000000005f74726163655f616d6f75" - "6e74400000000000000074726163655f616d6f756e745f7a65726f6765745f706172656e74" - "5f6c65646765725f686173685f6e65675f7074726765745f74785f61727261795f6c656e5f" - "696e76616c69645f736669656c646765745f74785f6e65737465645f61727261795f6c656e" - "5f6e65675f7074726765745f74785f6e65737465645f61727261795f6c656e5f6e65675f6c" - "656e6765745f74785f6e65737465645f61727261795f6c656e5f746f6f5f6c6f6e67676574" - "5f74785f6e65737465645f61727261795f6c656e5f7074725f6f6f6263616368655f6c6564" - "6765725f6f626a5f7074725f6f6f6263616368655f6c65646765725f6f626a5f77726f6e67" - "5f6c656e555344303030303030303030303030303030303000418686c0000b8f1874726163" - "655f6e756d5f6f6f625f7374726765745f63757272656e745f6c65646765725f6f626a5f61" - "727261795f6c656e5f696e76616c69645f736669656c646765745f6c65646765725f6f626a" - "5f61727261795f6c656e5f696e76616c69645f736669656c64616d656e646d656e745f656e" - "61626c65645f746f6f5f6269675f736c696365616d656e646d656e745f656e61626c65645f" - "746f6f5f6c6f6e676765745f74785f6e65737465645f61727261795f6c656e5f746f6f5f62" - "69675f736c6963656765745f63757272656e745f6c65646765725f6f626a5f6e6573746564" - "5f61727261795f6c656e5f746f6f5f6269675f736c6963656765745f6c65646765725f6f62" - "6a5f6e65737465645f61727261795f6c656e5f746f6f5f6269675f736c6963657570646174" - "655f646174615f746f6f5f6269675f736c69636574726163655f6f6f625f736c6963657472" - "6163655f6f70617175655f666c6f61745f6f6f625f736c69636574726163655f616d6f756e" - "745f6f6f625f736c696365666c6f61745f636f6d706172655f6f6f625f736c69636531666c" - "6f61745f636f6d706172655f6f6f625f736c6963653263616368655f6c65646765725f6f62" - "6a5f77726f6e675f73697a655f75696e743235366765745f6e66745f666c6167735f77726f" - "6e675f73697a655f75696e743235366765745f6e66745f7472616e736665725f6665655f77" - "726f6e675f73697a655f75696e743235363030303030303030303030303030303030303030" - "30303030303030303030303174726163655f6163636f756e745f77726f6e675f73697a655f" - "6163636f756e74696474726163655f6f6f625f737472696e6774726163655f6f7061717565" - "5f666c6f61745f6f6f625f737472696e6774726163655f6163636f756e745f6f6f625f7374" - "72696e6774726163655f616d6f756e745f6f6f625f737472696e6774726163655f746f6f5f" - "6c6f6e6774726163655f6e756d5f746f6f5f6c6f6e6774726163655f6f70617175655f666c" - "6f61745f746f6f5f6c6f6e6774726163655f6163636f756e745f746f6f5f6c6f6e67747261" - "63655f616d6f756e745f746f6f5f6c6f6e6774726163655f616d6f756e745f77726f6e675f" - "6c656e67746874726163655f6163636f756e745f636865636b5f646573796e636765745f70" - "6172656e745f6c65646765725f686173686765745f63757272656e745f6c65646765725f6f" - "626a5f6669656c646765745f6c65646765725f6f626a5f6669656c646765745f74785f6e65" - "737465645f6669656c646765745f63757272656e745f6c65646765725f6f626a5f6e657374" - "65645f6669656c646765745f6c65646765725f6f626a5f6e65737465645f6669656c64636f" - "6d707574655f7368613531325f68616c666765745f6e66746765745f6e66745f6973737565" - "726765745f6e66745f7461786f6e6765745f6e66745f73657269616c6765745f706172656e" - "745f6c65646765725f686173685f6e65675f6c656e6765745f706172656e745f6c65646765" - "725f686173685f6275665f746f6f5f736d616c6c6765745f706172656e745f6c6564676572" - "5f686173685f6c656e5f746f6f5f6c6f6e67666c6f61745f66726f6d5f75696e745f6c656e" - "5f6f6f62666c6f61745f66726f6d5f75696e745f77726f6e675f6c656e6163636f756e745f" - "6b65796c65745f6c656e5f6f6f626163636f756e745f6b65796c65745f77726f6e675f6c65" - "6e6c696e655f6b65796c65745f6c656e5f6f6f625f63757272656e63796c696e655f6b6579" - "6c65745f77726f6e675f6c656e5f63757272656e6379616d6d5f6b65796c65745f6c656e5f" - "6f6f625f617373657432616d6d5f6b65796c65745f6c656e5f77726f6e675f6c656e5f6173" - "73657432616d6d5f6b65796c65745f6c656e5f77726f6e675f6e6f6e5f7872705f63757272" - "656e63795f6c656e616d6d5f6b65796c65745f6c656e5f77726f6e675f7872705f63757272" - "656e63795f6c656e616d6d5f6b65796c65745f6d70746765745f74785f6669656c645f696e" - "76616c69645f736669656c646765745f63757272656e745f6c65646765725f6f626a5f6669" - "656c645f696e76616c69645f736669656c646765745f6c65646765725f6f626a5f6669656c" - "645f696e76616c69645f736669656c646765745f74785f6e65737465645f6669656c645f74" - "6f6f5f6269675f736c6963656765745f63757272656e745f6c65646765725f6f626a5f6e65" - "737465645f6669656c645f746f6f5f6269675f736c6963656765745f6c65646765725f6f62" - "6a5f6e65737465645f6669656c645f746f6f5f6269675f736c696365636f6d707574655f73" - "68613531325f68616c665f746f6f5f6269675f736c696365616d6d5f6b65796c65745f746f" - "6f5f6269675f736c69636563726564656e7469616c5f6b65796c65745f746f6f5f6269675f" - "736c6963656d70746f6b656e5f6b65796c65745f746f6f5f6269675f736c6963655f6d7074" - "6964666c6f61745f6164645f6f6f625f736c69636531666c6f61745f6164645f6f6f625f73" - "6c69636532666c6f61745f73756274726163745f6f6f625f736c69636531666c6f61745f73" - "756274726163745f6f6f625f736c69636532666c6f61745f6d756c7469706c795f6f6f625f" - "736c69636531666c6f61745f6d756c7469706c795f6f6f625f736c69636532666c6f61745f" - "6469766964655f6f6f625f736c69636531666c6f61745f6469766964655f6f6f625f736c69" - "636532666c6f61745f726f6f745f6f6f625f736c696365666c6f61745f706f775f6f6f625f" - "736c696365666c6f61745f6c6f675f6f6f625f736c6963656765745f6e66745f77726f6e67" - "5f73697a655f75696e743235366765745f6e66745f6973737565725f77726f6e675f73697a" - "655f75696e743235366765745f6e66745f7461786f6e5f77726f6e675f73697a655f75696e" - "743235366765745f6e66745f73657269616c5f77726f6e675f73697a655f75696e74323536" - "6163636f756e745f6b65796c65745f77726f6e675f73697a655f6163636f756e7469646368" - "65636b5f6b65796c65745f77726f6e675f73697a655f6163636f756e74696463726564656e" - "7469616c5f6b65796c65745f77726f6e675f73697a655f6163636f756e7469643163726564" - "656e7469616c5f6b65796c65745f77726f6e675f73697a655f6163636f756e746964326465" - "6c65676174655f6b65796c65745f77726f6e675f73697a655f6163636f756e746964316465" - "6c65676174655f6b65796c65745f77726f6e675f73697a655f6163636f756e746964326465" - "706f7369745f707265617574685f6b65796c65745f77726f6e675f73697a655f6163636f75" - "6e746964316465706f7369745f707265617574685f6b65796c65745f77726f6e675f73697a" - "655f6163636f756e746964326469645f6b65796c65745f77726f6e675f73697a655f616363" - "6f756e746964657363726f775f6b65796c65745f77726f6e675f73697a655f6163636f756e" - "7469646c696e655f6b65796c65745f77726f6e675f73697a655f6163636f756e746964316c" - "696e655f6b65796c65745f77726f6e675f73697a655f6163636f756e746964326d70745f69" - "737375616e63655f6b65796c65745f77726f6e675f73697a655f6163636f756e7469646d70" - "746f6b656e5f6b65796c65745f77726f6e675f73697a655f6163636f756e7469646e66745f" - "6f666665725f6b65796c65745f77726f6e675f73697a655f6163636f756e7469646f666665" - "725f6b65796c65745f77726f6e675f73697a655f6163636f756e7469646f7261636c655f6b" - "65796c65745f77726f6e675f73697a655f6163636f756e7469647061796368616e5f6b6579" - "6c65745f77726f6e675f73697a655f6163636f756e746964317061796368616e5f6b65796c" - "65745f77726f6e675f73697a655f6163636f756e746964327065726d697373696f6e65645f" - "646f6d61696e5f6b65796c65745f77726f6e675f73697a655f6163636f756e746964736967" - "6e6572735f6b65796c65745f77726f6e675f73697a655f6163636f756e7469647469636b65" - "745f6b65796c65745f77726f6e675f73697a655f6163636f756e7469647661756c745f6b65" - "796c65745f77726f6e675f73697a655f6163636f756e7469646765745f6e66745f77726f6e" - "675f73697a655f6163636f756e7469646d70746f6b656e5f6b65796c65745f6d707469645f" - "77726f6e675f6c656e677468004d0970726f64756365727302086c616e6775616765010452" - "757374000c70726f6365737365642d6279010572757374631d312e38352e31202834656231" - "363132353020323032352d30332d313529002c0f7461726765745f6665617475726573022b" - "0f6d757461626c652d676c6f62616c732b087369676e2d657874"; + "8f9ec0000b7f0041909ec0000b072e04066d656d6f727902000666696e697368003d0a5f5f" + "646174615f656e6403010b5f5f686561705f6261736503020c01020ad62602460002402000" + "20014704402002200341004100410010001a20004100480d01418b80c000410b2000ad1001" + "000b200220032000ac10011a0f0b418b80c000410b2000ac1001000b8c26020a7f017e2300" + "41f0016b22002400419680c000412341004100410010001a100241b9e00041b980c000410e" + "103c100341b2920441c780c0004116103c200041f0006a22064200370300200041e8006a22" + "054200370300200041e0006a2203420037030020004200370358200041d8006a2201412010" + "04412041be8cc0004116103c1005410a41dd80c000410c103c200041186a22074281828488" + "90a0c08001370300200041106a2208428182848890a0c08001370300200041086a22094281" + "82848890a0c080013703002000428182848890a0c0800137030041e980c000410e10064101" + "41f780c0004111103c200041201006410141f780c0004111103c4181802020014114100722" + "0241144604400240200041266a200041da006a2d00003a00002000200029005f3703c80120" + "00200041e4006a2900003700cd01200041306a20002900cd01370000200020002f00583b01" + "242000200028005b360027200020002903c80137002b200642003703002005420037030020" + "03420037030020004200370358200041246a2206411420014120100822024120470d002000" + "413a6a20002d005a3a0000200041d0016a2202200041e7006a290000220a370300200041c7" + "006a200a370000200041cf006a200041ef006a290000370000200041d7006a200041f7006a" + "2d00003a0000200020002f01583b01382000200028005b36003b2000200029005f37003f20" + "0041386a4120410010094101418881c0004110103c20054100360200200342003703002000" + "42003703584181802020014114100a411441d48cc000411c103c2005410036020020034200" + "3703002000420037035841014181802020014114100b411441f08cc0004114103c20004104" + "36029801200041818020360258200041d8016a220341003602002002420037030020004200" + "3703c80120014104200041c8016a22054114100c411441848dc0004113103c200341003602" + "0020024200370300200042003703c801200120002802980120054114100d411441978dc000" + "4123103c2003410036020020024200370300200042003703c8014101200120002802980120" + "054114100e411441ba8dc000411b103c4189803c100f4120419881c0004110103c4189803c" + "1010412041a881c0004120103c41014189803c1011412041c881c0004118103c2001200028" + "0298011012412041e081c0004117103c20012000280298011013412041f781c0004127103c" + "4101200120002802980110144120419e82c000411f103c200641141015411441bd82c00041" + "0b103c200041e0016a220442003703002003420037030020024200370300200042003703c8" + "012001200028029801200541201016412041d58dc0004113103c41c882c000410c41d482c0" + "00410b41df82c000410e1017410141ed82c0004109103c200041b8016a2007290300370300" + "200041b0016a2008290300370300200041a8016a2009290300370300200020002903003703" + "a001200341003b010020024200370300200042003703c80120064114200041a0016a220741" + "20200541121018411241e88dc0004107103c20034100360200200242003703002000420037" + "03c80120074120200541141019411441ef8dc000410e103c200041003602c8012007412020" + "054104101a410441fd8dc000410d103c20074120101b410841f682c000410d103c20074120" + "101c410a418383c0004114103c200041003602c8012007412020054104101d4104418a8ec0" + "00410e103c419783c000410d20064114101e412141a483c000410d103c419783c000410d41" + "b183c0004108101f410d41b983c000410c103c419783c000410d41c583c0004108101f410d" + "41cd83c0004111103c417f41041004417141de83c000411e103c200041003602c801200541" + "7f1004417141988ec000411e103c200041ca016a41003a0000200041003b01c80120054103" + "1004417d41b68ec0004124103c200041003602c8012005418094ebdc031004417341da8ec0" + "004123103c200442003703002003420037030020024200370300200042003703c801200041" + "d894ebdc036a220741082005412041001020417341fd8ec0004117103c2004420037030020" + "03420037030020024200370300200042003703c80120012000280298012005412041001020" + "417141948fc0004119103c4102100f416f41fc83c000411f103c417f200028029801101241" + "71419b84c000411f103c2001417f1012417141ba84c000411f103c20014181201012417441" + "d984c0004120103c20072000280298011012417341f984c000411f103c2007200028029801" + "410110094173419885c0004118103c200120002802980141011009417141b085c000411a10" + "3c200442003703002003420037030020024200370300200042003703c80120072000280298" + "01200541201008417341ad8fc0004116103c20044200370300200342003703002002420037" + "0300200042003703c8012001200028029801200541201008417141c38fc0004118103c2004" + "42003703002003420037030020024200370300200042003703c80120064114200641142007" + "200028029801200541201021417341db8fc000411c103c2004420037030020034200370300" + "20024200370300200042003703c80120064114200641142001200028029801200541201021" + "417141f78fc000411e103c2004420037030020034200370300200242003703002000420037" + "03c80141fb9dc000411420072000280298012005412010224173419590c0004119103c2004" + "42003703002003420037030020024200370300200042003703c80141fb9dc0004114200120" + "0028029801200541201022417141ae90c000411f103c200442003703002003420037030020" + "024200370300200042003703c80141fb9dc000411441ca85c0004114200541201022417141" + "cd90c0004129103c200442003703002003420037030020024200370300200042003703c801" + "41de85c000412841fb9dc0004114200541201022417141f690c0004125103c200041dc016a" + "200041346a280100360200200041d4016a2000412c6a290100370200200020002901243702" + "cc01200041808080083602c801200041003b01c0012005411841fb9dc0004114200041c001" + "6a2203410210224171419b91c000410e103c2007200028029801422a10014173418686c000" + "4111103c200041003b01c0014102200341021007416f41a991c000411b103c200041003b01" + "c001410220034102100a416f41c491c000412b103c200041003b01c0014101410220034102" + "100b416f41ef91c0004123103c4102100f416f41fc83c000411f103c41021010416f419786" + "c000412f103c410141021011416f41c686c0004127103c41e980c0004181201006417441ed" + "86c000411f103c41e980c00041c10010064174418c87c000411a103c200041003b01c00120" + "0141812020034102100c4174419292c0004121103c200041003b01c0012001418120200341" + "02100d417441b392c0004131103c200041003b01c0014101200141812020034102100e4174" + "41e492c0004129103c20014181201012417441a687c0004125103c20014181201013417441" + "cb87c0004135103c4101200141812010144174418088c000412d103c200141812010154174" + "41ad88c0004119103c419783c00041812041d482c000410b41df82c000410e1017417441ed" + "82c0004109103c419783c000410d41d482c00041812041df82c000410e1017417441ed82c0" + "004109103c419783c000410d41d482c000410b41df82c0004181201017417441ed82c00041" + "09103c200041003b01c00120014181202003410210164174418d93c0004121103c20004100" + "3b01c00141fb9dc00041812041fb9dc0004114200341021022417441ae93c0004118103c20" + "0041003b01c00120064114200641142001418120200341021023417441c693c000411f103c" + "200041003b01c001200541812020064114200341021024417441e593c0004122103c419783" + "c000410d200720002802980141001000417341c688c000410f103c200042d487b6f4c7d4b1" + "c0003700c001419783c000410d200041c095ebdc036a220441081025417341d588c000411c" + "103c419783c000410d2007200028029801101f417341f188c0004116103c20044108200341" + "0810264173418789c0004118103c200341082004410810264173419f89c0004118103c2000" + "41003b01ec012004410820034108200041ec016a22024102410010274173418794c0004114" + "103c200041003b01ec01200341082004410820024102410010274173419b94c0004114103c" + "200041003b01ec0120044108200341082002410241001028417341af94c0004119103c2000" + "41003b01ec0120034108200441082002410241001028417341c894c0004119103c20004100" + "3b01ec0120044108200341082002410241001029417341e194c0004119103c200041003b01" + "ec0120034108200441082002410241001029417341fa94c0004119103c200041003b01ec01" + "2004410820034108200241024100102a4173419395c0004117103c200041003b01ec012003" + "410820044108200241024100102a417341aa95c0004117103c200041003b01ec0120044108" + "4103200241024100102b417341c195c0004114103c200041003b01ec012004410841032002" + "41024100102c417341d595c0004113103c200041003b01ec0120044108200241024100102d" + "417341e895c0004113103c200120002802980141001009417141b789c0004123103c200041" + "003b01ec01200641142001200028029801200241021018417141fb95c000411a103c200041" + "003b01ec0120012000280298012002410210194171419596c0004121103c200041003b01ec" + "01200120002802980120024102101a417141b696c0004120103c2001200028029801101b41" + "7141da89c0004120103c2001200028029801101c417141fa89c0004127103c200041003602" + "ec01200120002802980120024104101d417141d696c0004121103c200041003b01ec012001" + "200028029801200241021008417141f796c0004123103c200041003b01ec01200120002802" + "9801410120024102102e4171419a97c0004121103c200041003b01ec012001200028029801" + "22042006411420012004200241021023417141bb97c0004127103c200041003b01ec012006" + "41142001200028029801220420012004200241021023417141e297c0004127103c20004100" + "3b01ec0120012000280298012006411420024102102f4171418998c0004125103c20004100" + "3b01ec0120064114200120002802980120024102102f417141ae98c0004125103c20004100" + "3b01ec01200120002802980120064114200241021030417141d398c000412c103c20004100" + "3b01ec01200641142001200028029801200241021030417141ff98c000412c103c20004100" + "3b01ec012001200028029801200241021031417141ab99c000411f103c200041003b01ec01" + "20012000280298014101200241021032417141ca99c0004122103c200041003b01ec012001" + "2000280298012006411441ca85c0004114200241021021417141ec99c0004121103c200041" + "003b01ec0120064114200120002802980141ca85c00041142002410210214171418d9ac000" + "4121103c200041003b01ec0120012000280298014101200241021033417141ae9ac0004128" + "103c200041003b01ec01200541182001200028029801200241021024417141d69ac0004123" + "103c200041003b01ec0120012000280298014101200241021034417141f99ac0004125103c" + "200041003b01ec01200120002802980141012002410210354171419e9bc0004121103c2000" + "41003b01ec0120012000280298014101200241021036417141bf9bc0004122103c20004100" + "3b01ec012001200028029801200641144101200241021037417141e19bc0004124103c2000" + "41003b01ec012006411420012000280298014101200241021037417141859cc0004124103c" + "200041003b01ec0120012000280298014101200241021038417141a99cc000412f103c2000" + "41003b01ec012001200028029801200241021039417141d89cc0004123103c200041003b01" + "ec012001200028029801410120024102103a417141fb9cc0004122103c200041003b01ec01" + "2001200028029801410120024102103b4171419d9dc0004121103c200041003b01ec012001" + "20002802980141a18ac0004120200241021018417141be9dc000411c103c419783c000410d" + "2001200028029801101e417141c18ac0004122103c419797abdd03410d41a18ac000412041" + "001000417341e38ac0004110103c419797abdd03410d200341081025417341f38ac000411d" + "103c419797abdd03410d20064114101e417341908bc0004118103c419797abdd03410d41c5" + "83c0004108101f417341a88bc0004117103c20012000280298012001418120410010004174" + "41bf8bc000410e103c200141812042011001417441cd8bc0004112103c419783c000418120" + "200341081025417441df8bc000411b103c419783c00041812020064114101e417441fa8bc0" + "004116103c419783c00041812041c583c0004108101f417441908cc0004115103c419783c0" + "00410d2001200028029801101f417141a58cc0004119103c200041003b01ec012001200028" + "02980120064114200241021024417141da9dc0004121103c200041f0016a240041010f0b0b" + "418080c000410b417f20022002417f4e1bac1001000b0be61d0200418080c0000bde056572" + "726f725f636f64653d54455354204641494c45442424242424205354415254494e47205741" + "534d20455845435554494f4e2024242424246765745f6c65646765725f73716e6765745f70" + "6172656e745f6c65646765725f74696d656765745f626173655f666565746573745f616d65" + "6e646d656e74616d656e646d656e745f656e61626c656463616368655f6c65646765725f6f" + "626a6765745f74785f61727261795f6c656e6765745f63757272656e745f6c65646765725f" + "6f626a5f61727261795f6c656e6765745f6c65646765725f6f626a5f61727261795f6c656e" + "6765745f74785f6e65737465645f61727261795f6c656e6765745f63757272656e745f6c65" + "646765725f6f626a5f6e65737465645f61727261795f6c656e6765745f6c65646765725f6f" + "626a5f6e65737465645f61727261795f6c656e7570646174655f6461746174657374206d65" + "737361676574657374207075626b657974657374207369676e6174757265636865636b5f73" + "69676765745f6e66745f666c6167736765745f6e66745f7472616e736665725f6665657465" + "7374696e6720747261636574726163655f6163636f756e74400000000000005f7472616365" + "5f616d6f756e74400000000000000074726163655f616d6f756e745f7a65726f6765745f70" + "6172656e745f6c65646765725f686173685f6e65675f7074726765745f74785f6172726179" + "5f6c656e5f696e76616c69645f736669656c646765745f74785f6e65737465645f61727261" + "795f6c656e5f6e65675f7074726765745f74785f6e65737465645f61727261795f6c656e5f" + "6e65675f6c656e6765745f74785f6e65737465645f61727261795f6c656e5f746f6f5f6c6f" + "6e676765745f74785f6e65737465645f61727261795f6c656e5f7074725f6f6f6263616368" + "655f6c65646765725f6f626a5f7074725f6f6f6263616368655f6c65646765725f6f626a5f" + "77726f6e675f6c656e555344303030303030303030303030303030303000418686c0000bf5" + "1774726163655f6e756d5f6f6f625f7374726765745f63757272656e745f6c65646765725f" + "6f626a5f61727261795f6c656e5f696e76616c69645f736669656c646765745f6c65646765" + "725f6f626a5f61727261795f6c656e5f696e76616c69645f736669656c64616d656e646d65" + "6e745f656e61626c65645f746f6f5f6269675f736c696365616d656e646d656e745f656e61" + "626c65645f746f6f5f6c6f6e676765745f74785f6e65737465645f61727261795f6c656e5f" + "746f6f5f6269675f736c6963656765745f63757272656e745f6c65646765725f6f626a5f6e" + "65737465645f61727261795f6c656e5f746f6f5f6269675f736c6963656765745f6c656467" + "65725f6f626a5f6e65737465645f61727261795f6c656e5f746f6f5f6269675f736c696365" + "7570646174655f646174615f746f6f5f6269675f736c69636574726163655f6f6f625f736c" + "69636574726163655f6f70617175655f666c6f61745f6f6f625f736c69636574726163655f" + "616d6f756e745f6f6f625f736c696365666c6f61745f636f6d706172655f6f6f625f736c69" + "636531666c6f61745f636f6d706172655f6f6f625f736c6963653263616368655f6c656467" + "65725f6f626a5f77726f6e675f73697a655f75696e743235366765745f6e66745f666c6167" + "735f77726f6e675f73697a655f75696e743235366765745f6e66745f7472616e736665725f" + "6665655f77726f6e675f73697a655f75696e74323536303030303030303030303030303030" + "303030303030303030303030303030303174726163655f6163636f756e745f77726f6e675f" + "73697a655f6163636f756e74696474726163655f6f6f625f737472696e6774726163655f6f" + "70617175655f666c6f61745f6f6f625f737472696e6774726163655f6163636f756e745f6f" + "6f625f737472696e6774726163655f616d6f756e745f6f6f625f737472696e677472616365" + "5f746f6f5f6c6f6e6774726163655f6e756d5f746f6f5f6c6f6e6774726163655f6f706171" + "75655f666c6f61745f746f6f5f6c6f6e6774726163655f6163636f756e745f746f6f5f6c6f" + "6e6774726163655f616d6f756e745f746f6f5f6c6f6e6774726163655f616d6f756e745f77" + "726f6e675f6c656e6774686765745f706172656e745f6c65646765725f686173686765745f" + "63757272656e745f6c65646765725f6f626a5f6669656c646765745f6c65646765725f6f62" + "6a5f6669656c646765745f74785f6e65737465645f6669656c646765745f63757272656e74" + "5f6c65646765725f6f626a5f6e65737465645f6669656c646765745f6c65646765725f6f62" + "6a5f6e65737465645f6669656c64636f6d707574655f7368613531325f68616c666765745f" + "6e66746765745f6e66745f6973737565726765745f6e66745f7461786f6e6765745f6e6674" + "5f73657269616c6765745f706172656e745f6c65646765725f686173685f6e65675f6c656e" + "6765745f706172656e745f6c65646765725f686173685f6275665f746f6f5f736d616c6c67" + "65745f706172656e745f6c65646765725f686173685f6c656e5f746f6f5f6c6f6e67666c6f" + "61745f66726f6d5f75696e745f6c656e5f6f6f62666c6f61745f66726f6d5f75696e745f77" + "726f6e675f6c656e6163636f756e745f6b65796c65745f6c656e5f6f6f626163636f756e74" + "5f6b65796c65745f77726f6e675f6c656e6c696e655f6b65796c65745f6c656e5f6f6f625f" + "63757272656e63796c696e655f6b65796c65745f77726f6e675f6c656e5f63757272656e63" + "79616d6d5f6b65796c65745f6c656e5f6f6f625f617373657432616d6d5f6b65796c65745f" + "6c656e5f77726f6e675f6c656e5f617373657432616d6d5f6b65796c65745f6c656e5f7772" + "6f6e675f6e6f6e5f7872705f63757272656e63795f6c656e616d6d5f6b65796c65745f6c65" + "6e5f77726f6e675f7872705f63757272656e63795f6c656e616d6d5f6b65796c65745f6d70" + "746765745f74785f6669656c645f696e76616c69645f736669656c646765745f6375727265" + "6e745f6c65646765725f6f626a5f6669656c645f696e76616c69645f736669656c64676574" + "5f6c65646765725f6f626a5f6669656c645f696e76616c69645f736669656c646765745f74" + "785f6e65737465645f6669656c645f746f6f5f6269675f736c6963656765745f6375727265" + "6e745f6c65646765725f6f626a5f6e65737465645f6669656c645f746f6f5f6269675f736c" + "6963656765745f6c65646765725f6f626a5f6e65737465645f6669656c645f746f6f5f6269" + "675f736c696365636f6d707574655f7368613531325f68616c665f746f6f5f6269675f736c" + "696365616d6d5f6b65796c65745f746f6f5f6269675f736c69636563726564656e7469616c" + "5f6b65796c65745f746f6f5f6269675f736c6963656d70746f6b656e5f6b65796c65745f74" + "6f6f5f6269675f736c6963655f6d70746964666c6f61745f6164645f6f6f625f736c696365" + "31666c6f61745f6164645f6f6f625f736c69636532666c6f61745f73756274726163745f6f" + "6f625f736c69636531666c6f61745f73756274726163745f6f6f625f736c69636532666c6f" + "61745f6d756c7469706c795f6f6f625f736c69636531666c6f61745f6d756c7469706c795f" + "6f6f625f736c69636532666c6f61745f6469766964655f6f6f625f736c69636531666c6f61" + "745f6469766964655f6f6f625f736c69636532666c6f61745f726f6f745f6f6f625f736c69" + "6365666c6f61745f706f775f6f6f625f736c696365666c6f61745f6c6f675f6f6f625f736c" + "6963656765745f6e66745f77726f6e675f73697a655f75696e743235366765745f6e66745f" + "6973737565725f77726f6e675f73697a655f75696e743235366765745f6e66745f7461786f" + "6e5f77726f6e675f73697a655f75696e743235366765745f6e66745f73657269616c5f7772" + "6f6e675f73697a655f75696e743235366163636f756e745f6b65796c65745f77726f6e675f" + "73697a655f6163636f756e746964636865636b5f6b65796c65745f77726f6e675f73697a65" + "5f6163636f756e74696463726564656e7469616c5f6b65796c65745f77726f6e675f73697a" + "655f6163636f756e7469643163726564656e7469616c5f6b65796c65745f77726f6e675f73" + "697a655f6163636f756e7469643264656c65676174655f6b65796c65745f77726f6e675f73" + "697a655f6163636f756e7469643164656c65676174655f6b65796c65745f77726f6e675f73" + "697a655f6163636f756e746964326465706f7369745f707265617574685f6b65796c65745f" + "77726f6e675f73697a655f6163636f756e746964316465706f7369745f707265617574685f" + "6b65796c65745f77726f6e675f73697a655f6163636f756e746964326469645f6b65796c65" + "745f77726f6e675f73697a655f6163636f756e746964657363726f775f6b65796c65745f77" + "726f6e675f73697a655f6163636f756e7469646c696e655f6b65796c65745f77726f6e675f" + "73697a655f6163636f756e746964316c696e655f6b65796c65745f77726f6e675f73697a65" + "5f6163636f756e746964326d70745f69737375616e63655f6b65796c65745f77726f6e675f" + "73697a655f6163636f756e7469646d70746f6b656e5f6b65796c65745f77726f6e675f7369" + "7a655f6163636f756e7469646e66745f6f666665725f6b65796c65745f77726f6e675f7369" + "7a655f6163636f756e7469646f666665725f6b65796c65745f77726f6e675f73697a655f61" + "63636f756e7469646f7261636c655f6b65796c65745f77726f6e675f73697a655f6163636f" + "756e7469647061796368616e5f6b65796c65745f77726f6e675f73697a655f6163636f756e" + "746964317061796368616e5f6b65796c65745f77726f6e675f73697a655f6163636f756e74" + "6964327065726d697373696f6e65645f646f6d61696e5f6b65796c65745f77726f6e675f73" + "697a655f6163636f756e7469647369676e6572735f6b65796c65745f77726f6e675f73697a" + "655f6163636f756e7469647469636b65745f6b65796c65745f77726f6e675f73697a655f61" + "63636f756e7469647661756c745f6b65796c65745f77726f6e675f73697a655f6163636f75" + "6e7469646765745f6e66745f77726f6e675f73697a655f6163636f756e7469646d70746f6b" + "656e5f6b65796c65745f6d707469645f77726f6e675f6c656e677468004d0970726f647563" + "65727302086c616e6775616765010452757374000c70726f6365737365642d627901057275" + "7374631d312e38392e30202832393438333838336520323032352d30382d3034290094010f" + "7461726765745f6665617475726573082b0f6d757461626c652d676c6f62616c732b136e6f" + "6e7472617070696e672d6670746f696e742b0b62756c6b2d6d656d6f72792b087369676e2d" + "6578742b0f7265666572656e63652d74797065732b0a6d756c746976616c75652b0f62756c" + "6b2d6d656d6f72792d6f70742b1663616c6c2d696e6469726563742d6f7665726c6f6e67"; extern std::string const floatTestsWasmHex = "0061736d0100000001430860077f7f7f7f7f7f7f017f60057f7f7f7f7f017f60047f7f7f7f" @@ -1176,6 +1176,22 @@ extern std::string const proposalExtendedConstHex = "0061736d010000000105016000017f030201000609017f00410a41206a0b070a010666696e" "69736800000a090107002300412a460b"; +extern std::string const proposalMultiMemoryHex = + "0061736d010000000105016000017f0302010005050200000001070a010666696e69736800" + "000a060104003f010b"; + +extern std::string const proposalCustomPageSizesHex = + "0061736d010000000105016000017f0302010005040108010a070a010666696e6973680000" + "0a0601040041010b0010046e616d65010901000666696e697368"; + +extern std::string const proposalMemory64Hex = + "0061736d010000000105016000017f030201000503010401070a010666696e69736800000a" + "10010e004200412a3a00003f004201510b0010046e616d65010901000666696e697368"; + +extern std::string const proposalWideArithmeticHex = + "0061736d010000000105016000017f03020100070a010666696e69736800000a0e010c0042" + "014202fc161a1a41010b0010046e616d65010901000666696e697368"; + extern std::string const trapDivideBy0Hex = "0061736d010000000105016000017f03020100070a010666696e69736800000a0c010a0041" "2a41006d1a41010b"; @@ -1362,6 +1378,23 @@ extern std::string const badAllocHex = "616c732b087369676e2d6578742b0f7265666572656e63652d74797065732b0a6d756c7469" "76616c7565"; +extern std::string const badAlignHex = + "0061736d0100000001110360057f7f7f7f7f017f6000006000017f02170103656e760f666c" + "6f61745f66726f6d5f75696e7400000303020102050301000206400a7f004180080b7f0041" + "80080b7f00418088020b7f00418088020b7f00418088060b7f004180080b7f00418088060b" + "7f00418080080b7f0041000b7f0041010b07b1010d066d656d6f72790200115f5f7761736d" + "5f63616c6c5f63746f727300010474657374000206655f6461746103000c5f5f64736f5f68" + "616e646c6503010a5f5f646174615f656e6403020b5f5f737461636b5f6c6f7703030c5f5f" + "737461636b5f6869676803040d5f5f676c6f62616c5f6261736503050b5f5f686561705f62" + "61736503060a5f5f686561705f656e6403070d5f5f6d656d6f72795f6261736503080c5f5f" + "7461626c655f6261736503090a240202000b1f00418108427f370000418108410841a30841" + "0c410010001a41a4082802000b007f0970726f647563657273010c70726f6365737365642d" + "62790105636c616e675f31392e312e352d776173692d73646b202868747470733a2f2f6769" + "746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a6563742061623462356132646235" + "3832393538616631656533303861373930636664623432626432343732302900490f746172" + "6765745f6665617475726573042b0f6d757461626c652d676c6f62616c732b087369676e2d" + "6578742b0f7265666572656e63652d74797065732b0a6d756c746976616c7565"; + extern std::string const updateDataWasmHex = "0061736d01000000010e0360027f7f017f6000006000017f02130103656e760b7570646174" "655f64617461000003030201020503010002063f0a7f01419088040b7f004180080b7f0041" diff --git a/src/test/app/wasm_fixtures/fixtures.h b/src/test/app/wasm_fixtures/fixtures.h index db276614ec..91eadba0d3 100644 --- a/src/test/app/wasm_fixtures/fixtures.h +++ b/src/test/app/wasm_fixtures/fixtures.h @@ -55,6 +55,10 @@ extern std::string const proposalBulkMemoryHex; extern std::string const proposalRefTypesHex; extern std::string const proposalTailCallHex; extern std::string const proposalExtendedConstHex; +extern std::string const proposalMultiMemoryHex; +extern std::string const proposalCustomPageSizesHex; +extern std::string const proposalMemory64Hex; +extern std::string const proposalWideArithmeticHex; extern std::string const trapDivideBy0Hex; extern std::string const trapIntOverflowHex; @@ -80,5 +84,6 @@ extern std::string const infiniteLoopWasmHex; extern std::string const startLoopHex; extern std::string const badAllocHex; +extern std::string const badAlignHex; extern std::string const updateDataWasmHex; diff --git a/src/test/app/wasm_fixtures/wat/custom_page_sizes.wat b/src/test/app/wasm_fixtures/wat/custom_page_sizes.wat new file mode 100644 index 0000000000..a9d404fadc --- /dev/null +++ b/src/test/app/wasm_fixtures/wat/custom_page_sizes.wat @@ -0,0 +1,13 @@ +(module + ;; Define a memory with 1 initial page. + ;; CRITICAL: We explicitly set the page size to 1 kilobyte. + ;; Standard Wasm implies (pagesize 65536). + (memory 1 (pagesize 1024)) + + (func $finish (result i32) + ;; If this module instantiates, the runtime accepted the custom page size. + i32.const 1 + ) + + (export "finish" (func $finish)) +) diff --git a/src/test/app/wasm_fixtures/wat/memory64.wat b/src/test/app/wasm_fixtures/wat/memory64.wat new file mode 100644 index 0000000000..02c8f4f590 --- /dev/null +++ b/src/test/app/wasm_fixtures/wat/memory64.wat @@ -0,0 +1,21 @@ +(module + ;; Define a 64-bit memory (index type i64) + ;; Start with 1 page. + (memory i64 1) + + (func $finish (result i32) + ;; 1. Perform a store using a 64-bit address. + ;; Even if the value is small (0), the type MUST be i64. + i64.const 0 ;; Address (64-bit) + i32.const 42 ;; Value (32-bit) + i32.store8 ;; Opcode doesn't change, but validation rules do. + + ;; 2. check memory size + ;; memory.size now returns an i64. + memory.size + i64.const 1 + i64.eq ;; Returns i32 (1 if true) + ) + + (export "finish" (func $finish)) +) diff --git a/src/test/app/wasm_fixtures/wat/multi_memory.wat b/src/test/app/wasm_fixtures/wat/multi_memory.wat new file mode 100644 index 0000000000..c2f36e253f --- /dev/null +++ b/src/test/app/wasm_fixtures/wat/multi_memory.wat @@ -0,0 +1,16 @@ +(module + ;; Memory 0: Index 0 (Empty) + (memory 0) + + ;; Memory 1: Index 1 (Size 1 page) + ;; If multi-memory is disabled, this line causes a validation error (max 1 memory). + (memory 1) + + (func $finish (result i32) + ;; Query size of Memory Index 1. + ;; Should return 1 (success). + memory.size 1 + ) + + (export "finish" (func $finish)) +) diff --git a/src/test/app/wasm_fixtures/wat/wide_arithmetic.wat b/src/test/app/wasm_fixtures/wat/wide_arithmetic.wat new file mode 100644 index 0000000000..cd796162ae --- /dev/null +++ b/src/test/app/wasm_fixtures/wat/wide_arithmetic.wat @@ -0,0 +1,22 @@ +(module + (func $finish (result i32) + ;; 1. Push operands + i64.const 1 + i64.const 2 + + ;; 2. Execute Wide Multiplication + ;; If the feature is DISABLED, the parser/validator will trap here + ;; with "unknown instruction" or "invalid opcode". + ;; Input: [i64, i64] -> Output: [i64, i64] + i64.mul_wide_u + + ;; 3. Clean up the stack (drop the two i64 results) + drop + drop + + ;; 4. Return 1 to signal that validation passed + i32.const 1 + ) + + (export "finish" (func $finish)) +) diff --git a/src/test/basics/IOUAmount_test.cpp b/src/test/basics/IOUAmount_test.cpp index 305f2c83a1..d299f439d4 100644 --- a/src/test/basics/IOUAmount_test.cpp +++ b/src/test/basics/IOUAmount_test.cpp @@ -141,15 +141,28 @@ public: { testcase("IOU strings"); - BEAST_EXPECT(to_string(IOUAmount(-2, 0)) == "-2"); - BEAST_EXPECT(to_string(IOUAmount(0, 0)) == "0"); - BEAST_EXPECT(to_string(IOUAmount(2, 0)) == "2"); - BEAST_EXPECT(to_string(IOUAmount(25, -3)) == "0.025"); - BEAST_EXPECT(to_string(IOUAmount(-25, -3)) == "-0.025"); - BEAST_EXPECT(to_string(IOUAmount(25, 1)) == "250"); - BEAST_EXPECT(to_string(IOUAmount(-25, 1)) == "-250"); - BEAST_EXPECT(to_string(IOUAmount(2, 20)) == "2000000000000000e5"); - BEAST_EXPECT(to_string(IOUAmount(-2, -20)) == "-2000000000000000e-35"); + auto test = [this](IOUAmount const& n, std::string const& expected) { + auto const result = to_string(n); + std::stringstream ss; + ss << "to_string(" << result << "). Expected: " << expected; + BEAST_EXPECTS(result == expected, ss.str()); + }; + + for (auto const mantissaSize : + {MantissaRange::small, MantissaRange::large}) + { + NumberMantissaScaleGuard mg(mantissaSize); + + test(IOUAmount(-2, 0), "-2"); + test(IOUAmount(0, 0), "0"); + test(IOUAmount(2, 0), "2"); + test(IOUAmount(25, -3), "0.025"); + test(IOUAmount(-25, -3), "-0.025"); + test(IOUAmount(25, 1), "250"); + test(IOUAmount(-25, 1), "-250"); + test(IOUAmount(2, 20), "2e20"); + test(IOUAmount(-2, -20), "-2e-20"); + } } void diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index b7c5ee45b7..1fa5ae6e8f 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -14,46 +15,84 @@ public: void testZero() { - testcase("zero"); + testcase << "zero " << to_string(Number::getMantissaScale()); - Number const z{0, 0}; + for (Number const& z : {Number{0, 0}, Number{0}}) + { + BEAST_EXPECT(z.mantissa() == 0); + BEAST_EXPECT(z.exponent() == Number{}.exponent()); - BEAST_EXPECT(z.mantissa() == 0); - BEAST_EXPECT(z.exponent() == Number{}.exponent()); - - BEAST_EXPECT((z + z) == z); - BEAST_EXPECT((z - z) == z); - BEAST_EXPECT(z == -z); + BEAST_EXPECT((z + z) == z); + BEAST_EXPECT((z - z) == z); + BEAST_EXPECT(z == -z); + } } void test_limits() { - testcase("test_limits"); + auto const scale = Number::getMantissaScale(); + testcase << "test_limits " << to_string(scale); bool caught = false; + auto const minMantissa = Number::minMantissa(); try { - Number x{10'000'000'000'000'000, 32768}; + Number x = + Number{false, minMantissa * 10, 32768, Number::normalized{}}; } catch (std::overflow_error const&) { caught = true; } BEAST_EXPECT(caught); - Number x{10'000'000'000'000'000, 32767}; - BEAST_EXPECT((x == Number{1'000'000'000'000'000, 32768})); - Number z{1'000'000'000'000'000, -32769}; - BEAST_EXPECT(z == Number{}); - Number y{1'000'000'000'000'001'500, 32000}; - BEAST_EXPECT((y == Number{1'000'000'000'000'002, 32003})); - Number m{std::numeric_limits::min()}; - BEAST_EXPECT((m == Number{-9'223'372'036'854'776, 3})); - Number M{std::numeric_limits::max()}; - BEAST_EXPECT((M == Number{9'223'372'036'854'776, 3})); + + auto test = [this](auto const& x, auto const& y, int line) { + auto const result = x == y; + std::stringstream ss; + ss << x << " == " << y << " -> " << (result ? "true" : "false"); + expect(result, ss.str(), __FILE__, line); + }; + + test( + Number{false, minMantissa * 10, 32767, Number::normalized{}}, + Number{false, minMantissa, 32768, Number::normalized{}}, + __LINE__); + test( + Number{false, minMantissa, -32769, Number::normalized{}}, + Number{}, + __LINE__); + test( + Number{false, minMantissa, 32000, Number::normalized{}} * 1'000 + + Number{false, 1'500, 32000, Number::normalized{}}, + Number{false, minMantissa + 2, 32003, Number::normalized{}}, + __LINE__); + // 9,223,372,036,854,775,808 + + test( + Number{std::numeric_limits::min()}, + scale == MantissaRange::small + ? Number{-9'223'372'036'854'776, 3} + : Number{true, 9'223'372'036'854'775'808ULL, 0, Number::normalized{}}, + __LINE__); + test( + Number{std::numeric_limits::min() + 1}, + scale == MantissaRange::small ? Number{-9'223'372'036'854'776, 3} + : Number{-9'223'372'036'854'775'807}, + __LINE__); + test( + Number{std::numeric_limits::max()}, + Number{ + scale == MantissaRange::small + ? 9'223'372'036'854'776 + : std::numeric_limits::max(), + 18 - Number::mantissaLog()}, + __LINE__); caught = false; try { - Number q{99'999'999'999'999'999, 32767}; + [[maybe_unused]] + Number q = + Number{false, minMantissa, 32767, Number::normalized{}} * 100; } catch (std::overflow_error const&) { @@ -65,76 +104,307 @@ public: void test_add() { - testcase("test_add"); + auto const scale = Number::getMantissaScale(); + testcase << "test_add " << to_string(scale); + using Case = std::tuple; - Case c[]{ - {Number{1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'066, -15}}, - {Number{-1'000'000'000'000'000, -15}, - Number{-6'555'555'555'555'555, -29}, - Number{-1'000'000'000'000'066, -15}}, - {Number{-1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{-9'999'999'999'999'344, -16}}, - {Number{-6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'344, -16}}, - {Number{}, Number{5}, Number{5}}, - {Number{5'555'555'555'555'555, -32768}, - Number{-5'555'555'555'555'554, -32768}, - Number{0}}, - {Number{-9'999'999'999'999'999, -31}, - Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'990, -16}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x + y == z); - bool caught = false; - try + auto const cSmall = std::to_array( + {{Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'066, -15}}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'066, -15}}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{-9'999'999'999'999'344, -16}}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'344, -16}}, + {Number{}, Number{5}, Number{5}}, + {Number{5}, Number{}, Number{5}}, + {Number{5'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'554, -32768}, + Number{0}}, + {Number{-9'999'999'999'999'999, -31}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'990, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + { + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'065'556, -18}}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'065'556, -18}}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{ + true, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{ + false, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{}, Number{5}, Number{5}}, + {Number{5}, Number{}, Number{5}}, + {Number{5'555'555'555'555'555'000, -32768}, + Number{-5'555'555'555'555'554'000, -32768}, + Number{0}}, + {Number{-9'999'999'999'999'999, -31}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'990, -16}}, + // Items from cSmall expanded for the larger mantissa + {Number{1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -35}, + Number{1'000'000'000'000'000'066, -18}}, + {Number{-1'000'000'000'000'000'000, -18}, + Number{-6'555'555'555'555'555'555, -35}, + Number{-1'000'000'000'000'000'066, -18}}, + {Number{-1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -35}, + Number{ + true, + 9'999'999'999'999'999'344ULL, + -19, + Number::normalized{}}}, + {Number{-6'555'555'555'555'555'555, -35}, + Number{1'000'000'000'000'000'000, -18}, + Number{ + false, + 9'999'999'999'999'999'344ULL, + -19, + Number::normalized{}}}, + {Number{}, Number{5}, Number{5}}, + {Number{5'555'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'555'554, -32768}, + Number{0}}, + {Number{ + true, + 9'999'999'999'999'999'999ULL, + -37, + Number::normalized{}}, + Number{1'000'000'000'000'000'000, -18}, + Number{ + false, + 9'999'999'999'999'999'990ULL, + -19, + Number::normalized{}}}, + {Number{Number::maxRep}, + Number{6, -1}, + Number{Number::maxRep / 10, 1}}, + {Number{Number::maxRep - 1}, + Number{1, 0}, + Number{Number::maxRep}}, + // Test extremes + { + // Each Number operand rounds up, so the actual mantissa is + // minMantissa + Number{ + false, + 9'999'999'999'999'999'999ULL, + 0, + Number::normalized{}}, + Number{ + false, + 9'999'999'999'999'999'999ULL, + 0, + Number::normalized{}}, + Number{2, 19}, + }, + { + // Does not round. Mantissas are going to be > maxRep, so if + // added together as uint64_t's, the result will overflow. + // With addition using uint128_t, there's no problem. After + // normalizing, the resulting mantissa ends up less than + // maxRep. + Number{ + false, + 9'999'999'999'999'999'990ULL, + 0, + Number::normalized{}}, + Number{ + false, + 9'999'999'999'999'999'990ULL, + 0, + Number::normalized{}}, + Number{ + false, + 1'999'999'999'999'999'998ULL, + 1, + Number::normalized{}}, + }, + }); + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x + y; + std::stringstream ss; + ss << x << " + " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); { - Number{9'999'999'999'999'999, 32768} + - Number{5'000'000'000'000'000, 32767}; + bool caught = false; + try + { + Number{ + false, Number::maxMantissa(), 32768, Number::normalized{}} + + Number{ + false, + Number::minMantissa(), + 32767, + Number::normalized{}} * + 5; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); } void test_sub() { - testcase("test_sub"); + auto const scale = Number::getMantissaScale(); + testcase << "test_sub " << to_string(scale); + using Case = std::tuple; - Case c[]{ - {Number{1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{9'999'999'999'999'344, -16}}, - {Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'000, -15}, - Number{-9'999'999'999'999'344, -16}}, - {Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'000, -15}, - Number{0}}, - {Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'001, -15}, - Number{-1'000'000'000'000'000, -30}}, - {Number{1'000'000'000'000'001, -15}, - Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'000, -30}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x - y == z); + auto const cSmall = std::to_array( + {{Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{9'999'999'999'999'344, -16}}, + {Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{-9'999'999'999'999'344, -16}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'001, -15}, + Number{-1'000'000'000'000'000, -30}}, + {Number{1'000'000'000'000'001, -15}, + Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -30}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + { + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{ + false, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{ + true, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'001, -15}, + Number{-1'000'000'000'000'000, -30}}, + {Number{1'000'000'000'000'001, -15}, + Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -30}}, + // Items from cSmall expanded for the larger mantissa + {Number{1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -32}, + Number{ + false, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{6'555'555'555'555'555'555, -32}, + Number{1'000'000'000'000'000'000, -18}, + Number{ + true, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'000, -18}, + Number{0}}, + {Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'001, -18}, + Number{-1'000'000'000'000'000'000, -36}}, + {Number{1'000'000'000'000'000'001, -18}, + Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'000, -36}}, + {Number{Number::maxRep}, + Number{6, -1}, + Number{Number::maxRep - 1}}, + {Number{false, Number::maxRep + 1, 0, Number::normalized{}}, + Number{1, 0}, + Number{Number::maxRep / 10 + 1, 1}}, + {Number{false, Number::maxRep + 1, 0, Number::normalized{}}, + Number{3, 0}, + Number{Number::maxRep}}, + {power(2, 63), Number{3, 0}, Number{Number::maxRep}}, + }); + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x - y; + std::stringstream ss; + ss << x << " - " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); } void test_mul() { - testcase("test_mul"); + auto const scale = Number::getMantissaScale(); + testcase << "test_mul " << to_string(scale); + using Case = std::tuple; + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x * y; + std::stringstream ss; + ss << x << " * " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + auto tests = [&](auto const& cSmall, auto const& cLarge) { + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); + }; + auto const maxMantissa = Number::maxMantissa(); + saveNumberRoundMode save{Number::setround(Number::to_nearest)}; { - Case c[]{ + auto const cSmall = std::to_array({ {Number{7}, Number{8}, Number{56}}, {Number{1414213562373095, -15}, Number{1414213562373095, -15}, @@ -150,166 +420,520 @@ public: Number{1000000000000000, -14}}, {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, - Number{0}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x * y == z); + Number{0}}, + // Maximum mantissa range + {Number{9'999'999'999'999'999, 0}, + Number{9'999'999'999'999'999, 0}, + Number{9'999'999'999'999'998, 16}}, + }); + auto const cLarge = std::to_array({ + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{ + false, + 9'999'999'999'999'999'579ULL, + -18, + Number::normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2000000000000000001, -18}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999998, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds up to 1e19 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1, 38}}, + // Maximum int64 range + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'85, 19}}, + }); + tests(cSmall, cLarge); } Number::setround(Number::towards_zero); + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " towards_zero"; { - Case c[]{ - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{9999999999999999, -15}}, - {Number{1000000000000000, -32768}, - Number{1000000000000000, -32768}, - Number{0}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x * y == z); + auto const cSmall = std::to_array( + {{Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{9999999999999999, -15}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{ + false, + 9999999999999999579ULL, + -18, + Number::normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2, 0}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999997, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds down to maxMantissa/10e1 + // 99'999'999'999'999'999'800'000'000'000'000'000'100 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{ + false, + maxMantissa / 10 - 1, + 20, + Number::normalized{}}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'84, 19}}, + }); + tests(cSmall, cLarge); } Number::setround(Number::downward); + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " downward"; { - Case c[]{ - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-2000000000000000, -15}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{9999999999999999, -15}}, - {Number{1000000000000000, -32768}, - Number{1000000000000000, -32768}, - Number{0}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x * y == z); + auto const cSmall = std::to_array( + {{Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{9999999999999999, -15}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{ + false, + 9'999'999'999'999'999'579ULL, + -18, + Number::normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2, 0}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999998, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds down to maxMantissa/10e1 + // 99'999'999'999'999'999'800'000'000'000'000'000'100 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{ + false, + maxMantissa / 10 - 1, + 20, + Number::normalized{}}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'84, 19}}, + }); + tests(cSmall, cLarge); } Number::setround(Number::upward); + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " upward"; { - Case c[]{ - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{2000000000000000, -15}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{2000000000000000, -15}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{1000000000000000, -14}}, - {Number{1000000000000000, -32768}, - Number{1000000000000000, -32768}, - Number{0}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x * y == z); + auto const cSmall = std::to_array( + {{Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{1000000000000000, -14}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{999999999999999958, -17}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2000000000000000001, -18}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999997, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{2, 0}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{1000000000000000001, -17}}, + // Maximum mantissa range - rounds up to minMantissa*10 + // 1e19*1e19=1e38 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1, 38}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'85, 19}}, + }); + tests(cSmall, cLarge); } - bool caught = false; - try + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " overflow"; { - Number{9'999'999'999'999'999, 32768} * - Number{5'000'000'000'000'000, 32767}; + bool caught = false; + try + { + Number{false, maxMantissa, 32768, Number::normalized{}} * + Number{ + false, + Number::minMantissa() * 5, + 32767, + Number::normalized{}}; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); } void test_div() { - testcase("test_div"); + auto const scale = Number::getMantissaScale(); + testcase << "test_div " << to_string(scale); + using Case = std::tuple; + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x / y; + std::stringstream ss; + ss << x << " / " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + auto const maxMantissa = Number::maxMantissa(); + auto tests = [&](auto const& cSmall, auto const& cLarge) { + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); + }; saveNumberRoundMode save{Number::setround(Number::to_nearest)}; { - Case c[]{ - {Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, - Number{1414213562373095, -10}, - Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x / y == z); + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'667, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'667, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " towards_zero"; Number::setround(Number::towards_zero); { - Case c[]{ - {Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, - Number{1414213562373095, -10}, - Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x / y == z); + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'666, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'666, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " downward"; Number::setround(Number::downward); { - Case c[]{ - {Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, - Number{1414213562373095, -10}, - Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x / y == z); + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'666, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'667, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " upward"; Number::setround(Number::upward); { - Case c[]{ - {Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, - Number{1414213562373095, -10}, - Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x / y == z); + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'429, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'667, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'666, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'572, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " overflow"; bool caught = false; try { @@ -325,20 +949,59 @@ public: void test_root() { - testcase("test_root"); + auto const scale = Number::getMantissaScale(); + testcase << "test_root " << to_string(scale); + using Case = std::tuple; - Case c[]{ - {Number{2}, 2, Number{1414213562373095, -15}}, - {Number{2'000'000}, 2, Number{1414213562373095, -12}}, - {Number{2, -30}, 2, Number{1414213562373095, -30}}, - {Number{-27}, 3, Number{-3}}, - {Number{1}, 5, Number{1}}, - {Number{-1}, 0, Number{1}}, - {Number{5, -1}, 0, Number{0}}, - {Number{0}, 5, Number{0}}, - {Number{5625, -4}, 2, Number{75, -2}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT((root(x, y) == z)); + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = root(x, y); + std::stringstream ss; + ss << "root(" << x << ", " << y << ") = " << result + << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + /* + auto tests = [&](auto const& cSmall, auto const& cLarge) { + test(cSmall); + if (scale != MantissaRange::small) + test(cLarge); + }; + */ + + auto const cSmall = std::to_array( + {{Number{2}, 2, Number{1414213562373095049, -18}}, + {Number{2'000'000}, 2, Number{1414213562373095049, -15}}, + {Number{2, -30}, 2, Number{1414213562373095049, -33}}, + {Number{-27}, 3, Number{-3}}, + {Number{1}, 5, Number{1}}, + {Number{-1}, 0, Number{1}}, + {Number{5, -1}, 0, Number{0}}, + {Number{0}, 5, Number{0}}, + {Number{5625, -4}, 2, Number{75, -2}}}); + auto const cLarge = std::to_array({ + {Number{false, Number::maxMantissa() - 9, -1, Number::normalized{}}, + 2, + Number{false, 999'999'999'999'999'999, -9, Number::normalized{}}}, + {Number{false, Number::maxMantissa() - 9, 0, Number::normalized{}}, + 2, + Number{ + false, 3'162'277'660'168'379'330, -9, Number::normalized{}}}, + {Number{Number::maxRep}, + 2, + Number{false, 3'037'000'499'976049692, -9, Number::normalized{}}}, + {Number{Number::maxRep}, + 4, + Number{false, 55'108'98747006743627, -14, Number::normalized{}}}, + }); + test(cSmall); + if (Number::getMantissaScale() != MantissaRange::small) + { + NumberRoundModeGuard mg(Number::towards_zero); + test(cLarge); + } bool caught = false; try { @@ -361,10 +1024,52 @@ public: BEAST_EXPECT(caught); } + void + test_root2() + { + auto const scale = Number::getMantissaScale(); + testcase << "test_root2 " << to_string(scale); + + auto test = [this](auto const& c) { + for (auto const& x : c) + { + auto const expected = root(x, 2); + auto const result = root2(x); + std::stringstream ss; + ss << "root2(" << x << ") = " << result + << ". Expected: " << expected; + BEAST_EXPECTS(result == expected, ss.str()); + } + }; + + auto const cSmall = std::to_array({ + Number{2}, + Number{2'000'000}, + Number{2, -30}, + Number{27}, + Number{1}, + Number{5, -1}, + Number{0}, + Number{5625, -4}, + Number{Number::maxRep}, + }); + test(cSmall); + bool caught = false; + try + { + (void)root2(Number{-2}); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + void test_power1() { - testcase("test_power1"); + testcase << "test_power1 " << to_string(Number::getMantissaScale()); using Case = std::tuple; Case c[]{ {Number{64}, 0, Number{1}}, @@ -372,7 +1077,13 @@ public: {Number{64}, 2, Number{4096}}, {Number{-64}, 2, Number{4096}}, {Number{64}, 3, Number{262144}}, - {Number{-64}, 3, Number{-262144}}}; + {Number{-64}, 3, Number{-262144}}, + {Number{64}, + 11, + Number{false, 7378697629483820646ULL, 1, Number::normalized{}}}, + {Number{-64}, + 11, + Number{true, 7378697629483820646ULL, 1, Number::normalized{}}}}; for (auto const& [x, y, z] : c) BEAST_EXPECT((power(x, y) == z)); } @@ -380,7 +1091,7 @@ public: void test_power2() { - testcase("test_power2"); + testcase << "test_power2 " << to_string(Number::getMantissaScale()); using Case = std::tuple; Case c[]{ {Number{1}, 3, 7, Number{1}}, @@ -426,7 +1137,7 @@ public: void testConversions() { - testcase("testConversions"); + testcase << "testConversions " << to_string(Number::getMantissaScale()); IOUAmount x{5, 6}; Number y = x; @@ -452,7 +1163,7 @@ public: void test_to_integer() { - testcase("test_to_integer"); + testcase << "test_to_integer " << to_string(Number::getMantissaScale()); using Case = std::tuple; saveNumberRoundMode save{Number::setround(Number::to_nearest)}; { @@ -620,7 +1331,7 @@ public: void test_squelch() { - testcase("test_squelch"); + testcase << "test_squelch " << to_string(Number::getMantissaScale()); Number limit{1, -6}; BEAST_EXPECT((squelch(Number{2, -6}, limit) == Number{2, -6})); BEAST_EXPECT((squelch(Number{1, -6}, limit) == Number{1, -6})); @@ -633,22 +1344,129 @@ public: void testToString() { - testcase("testToString"); - BEAST_EXPECT(to_string(Number(-2, 0)) == "-2"); - BEAST_EXPECT(to_string(Number(0, 0)) == "0"); - BEAST_EXPECT(to_string(Number(2, 0)) == "2"); - BEAST_EXPECT(to_string(Number(25, -3)) == "0.025"); - BEAST_EXPECT(to_string(Number(-25, -3)) == "-0.025"); - BEAST_EXPECT(to_string(Number(25, 1)) == "250"); - BEAST_EXPECT(to_string(Number(-25, 1)) == "-250"); - BEAST_EXPECT(to_string(Number(2, 20)) == "2000000000000000e5"); - BEAST_EXPECT(to_string(Number(-2, -20)) == "-2000000000000000e-35"); + auto const scale = Number::getMantissaScale(); + testcase << "testToString " << to_string(scale); + + auto test = [this](Number const& n, std::string const& expected) { + auto const result = to_string(n); + std::stringstream ss; + ss << "to_string(" << result << "). Expected: " << expected; + BEAST_EXPECTS(result == expected, ss.str()); + }; + + test(Number(-2, 0), "-2"); + test(Number(0, 0), "0"); + test(Number(2, 0), "2"); + test(Number(25, -3), "0.025"); + test(Number(-25, -3), "-0.025"); + test(Number(25, 1), "250"); + test(Number(-25, 1), "-250"); + test(Number(2, 20), "2e20"); + test(Number(-2, -20), "-2e-20"); + // Test the edges + // ((exponent < -(25)) || (exponent > -(5))))) + // or ((exponent < -(28)) || (exponent > -(8))))) + test(Number(2, -10), "0.0000000002"); + test(Number(2, -11), "2e-11"); + + test(Number(-2, 10), "-20000000000"); + test(Number(-2, 11), "-2e11"); + + switch (scale) + { + case MantissaRange::small: + + test(Number::min(), "1e-32753"); + test(Number::max(), "9999999999999999e32768"); + test(Number::lowest(), "-9999999999999999e32768"); + { + NumberRoundModeGuard mg(Number::towards_zero); + + auto const maxMantissa = Number::maxMantissa(); + BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999); + test( + Number{ + false, + maxMantissa * 1000 + 999, + -3, + Number::normalized()}, + "9999999999999999"); + test( + Number{ + true, + maxMantissa * 1000 + 999, + -3, + Number::normalized()}, + "-9999999999999999"); + + test( + Number{std::numeric_limits::max(), -3}, + "9223372036854775"); + test( + -(Number{std::numeric_limits::max(), -3}), + "-9223372036854775"); + + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775e3"); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775e3"); + } + break; + case MantissaRange::large: + // Test the edges + // ((exponent < -(28)) || (exponent > -(8))))) + test(Number::min(), "1e-32750"); + test(Number::max(), "9223372036854775807e32768"); + test(Number::lowest(), "-9223372036854775807e32768"); + { + NumberRoundModeGuard mg(Number::towards_zero); + + auto const maxMantissa = Number::maxMantissa(); + BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL); + test( + Number{false, maxMantissa, 0, Number::normalized{}}, + "9999999999999999990"); + test( + Number{true, maxMantissa, 0, Number::normalized{}}, + "-9999999999999999990"); + + test( + Number{std::numeric_limits::max(), 0}, + "9223372036854775807"); + test( + -(Number{std::numeric_limits::max(), 0}), + "-9223372036854775807"); + + // Because the absolute value of min is larger than max, it + // will be scaled down to fit under max. Since we're + // rounding towards zero, the 8 at the end is dropped. + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775800"); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775800"); + } + + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775810"); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775810"); + break; + default: + BEAST_EXPECT(false); + } } void test_relationals() { - testcase("test_relationals"); + testcase << "test_relationals " + << to_string(Number::getMantissaScale()); BEAST_EXPECT(!(Number{100} < Number{10})); BEAST_EXPECT(Number{100} > Number{10}); BEAST_EXPECT(Number{100} >= Number{10}); @@ -658,7 +1476,7 @@ public: void test_stream() { - testcase("test_stream"); + testcase << "test_stream " << to_string(Number::getMantissaScale()); Number x{100}; std::ostringstream os; os << x; @@ -668,7 +1486,7 @@ public: void test_inc_dec() { - testcase("test_inc_dec"); + testcase << "test_inc_dec " << to_string(Number::getMantissaScale()); Number x{100}; Number y = +x; BEAST_EXPECT(x == y); @@ -685,19 +1503,19 @@ public: Issue const issue; Number const n{7'518'783'80596, -5}; saveNumberRoundMode const save{Number::setround(Number::to_nearest)}; - auto res2 = STAmount{issue, n.mantissa(), n.exponent()}; + auto res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518784}); Number::setround(Number::towards_zero); - res2 = STAmount{issue, n.mantissa(), n.exponent()}; + res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518783}); Number::setround(Number::downward); - res2 = STAmount{issue, n.mantissa(), n.exponent()}; + res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518783}); Number::setround(Number::upward); - res2 = STAmount{issue, n.mantissa(), n.exponent()}; + res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518784}); } @@ -834,28 +1652,94 @@ public: } } + void + testInt64() + { + auto const scale = Number::getMantissaScale(); + testcase << "std::int64_t " << to_string(scale); + + // Control case + BEAST_EXPECT(Number::maxMantissa() > 10); + Number ten{10}; + BEAST_EXPECT(ten.exponent() <= 0); + + if (scale == MantissaRange::small) + { + BEAST_EXPECT( + std::numeric_limits::max() > INITIAL_XRP.drops()); + BEAST_EXPECT(Number::maxMantissa() < INITIAL_XRP.drops()); + Number const initalXrp{INITIAL_XRP}; + BEAST_EXPECT(initalXrp.exponent() > 0); + + Number const maxInt64{Number::maxRep}; + BEAST_EXPECT(maxInt64.exponent() > 0); + // 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits + BEAST_EXPECT( + (power(maxInt64, 2) == Number{85'070'591'730'234'62, 22})); + + Number const max = + Number{false, Number::maxMantissa(), 0, Number::normalized{}}; + BEAST_EXPECT(max.exponent() <= 0); + // 99'999'999'999'999'980'000'000'000'000'001 - 32 digits + BEAST_EXPECT((power(max, 2) == Number{99'999'999'999'999'98, 16})); + } + else + { + BEAST_EXPECT( + std::numeric_limits::max() > INITIAL_XRP.drops()); + BEAST_EXPECT(Number::maxMantissa() > INITIAL_XRP.drops()); + Number const initalXrp{INITIAL_XRP}; + BEAST_EXPECT(initalXrp.exponent() <= 0); + + Number const maxInt64{Number::maxRep}; + BEAST_EXPECT(maxInt64.exponent() <= 0); + // 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits + BEAST_EXPECT( + (power(maxInt64, 2) == Number{85'070'591'730'234'615'85, 19})); + + NumberRoundModeGuard mg(Number::towards_zero); + + auto const maxMantissa = Number::maxMantissa(); + Number const max = + Number{false, maxMantissa, 0, Number::normalized{}}; + BEAST_EXPECT(max.mantissa() == maxMantissa / 10); + BEAST_EXPECT(max.exponent() == 1); + // 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38 + // digits + BEAST_EXPECT(( + power(max, 2) == + Number{false, maxMantissa / 10 - 1, 20, Number::normalized{}})); + } + } + void run() override { - testZero(); - test_limits(); - test_add(); - test_sub(); - test_mul(); - test_div(); - test_root(); - test_power1(); - test_power2(); - testConversions(); - test_to_integer(); - test_squelch(); - testToString(); - test_relationals(); - test_stream(); - test_inc_dec(); - test_toSTAmount(); - test_truncate(); - testRounding(); + for (auto const scale : {MantissaRange::small, MantissaRange::large}) + { + NumberMantissaScaleGuard sg(scale); + testZero(); + test_limits(); + testToString(); + test_add(); + test_sub(); + test_mul(); + test_div(); + test_root(); + test_root2(); + test_power1(); + test_power2(); + testConversions(); + test_to_integer(); + test_squelch(); + test_relationals(); + test_stream(); + test_inc_dec(); + test_toSTAmount(); + test_truncate(); + testRounding(); + testInt64(); + } } }; diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index 83366d61e2..208e3c4e5f 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -21,7 +21,12 @@ struct TestAMMArg std::optional> pool = std::nullopt; std::uint16_t tfee = 0; std::optional ter = std::nullopt; - std::vector features = {testable_amendments()}; + std::vector features = { + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; + bool noLog = false; }; @@ -66,6 +71,15 @@ protected: public: AMMTestBase(); + static FeatureBitset + testable_amendments() + { + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + return jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol; + } + protected: /** testAMM() funds 30,000XRP and 30,000IOU * for each non-XRP asset to Alice and Carol diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index a310fc5b44..147307f7b7 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -261,6 +261,12 @@ struct XRP_t return xrpIssue(); } + bool + integral() const + { + return true; + } + /** Returns an amount of XRP as PrettyAmount, which is trivially convertible to STAmount @@ -400,6 +406,11 @@ public: { return issue(); } + bool + integral() const + { + return issue().integral(); + } /** Implicit conversion to Issue or Asset. @@ -490,6 +501,11 @@ public: { return mptIssue(); } + bool + integral() const + { + return true; + } /** Implicit conversion to MPTIssue or asset. diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index de7ce5504b..139b9113b9 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -105,9 +105,14 @@ AMMTestBase::testAMM( for (auto const& features : arg.features) { + // Use small Number mantissas for the life of this test. + NumberMantissaScaleGuard const sg{xrpl::MantissaRange::small}; + + // For now, just disable SAV entirely, which locks in the small Number + // mantissas Env env{ *this, - features, + features - featureSingleAssetVault - featureLendingProtocol, arg.noLog ? std::make_unique(&logs) : nullptr}; auto const [asset1, asset2] = diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index adffa8548a..ceb60eb319 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -644,7 +644,7 @@ MPTTester::operator[](std::string const& name) const } PrettyAmount -MPTTester::operator()(std::uint64_t amount) const +MPTTester::operator()(std::int64_t amount) const { return MPT("", issuanceID())(amount); } diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 2f6bbb9ea8..3eea362b58 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -272,7 +272,7 @@ public: operator[](std::string const& name) const; PrettyAmount - operator()(std::uint64_t amount) const; + operator()(std::int64_t amount) const; operator Asset() const; diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp new file mode 100644 index 0000000000..71a485416d --- /dev/null +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -0,0 +1,211 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl { +namespace test { + +using namespace jtx; + +/** + * Test for TMGetObjectByHash reply size limiting. + * + * This verifies the fix that limits TMGetObjectByHash replies to + * Tuning::hardMaxReplyNodes to prevent excessive memory usage and + * potential DoS attacks from peers requesting large numbers of objects. + */ +class TMGetObjectByHash_test : public beast::unit_test::suite +{ + using middle_type = boost::beast::tcp_stream; + using stream_type = boost::beast::ssl_stream; + using socket_type = boost::asio::ip::tcp::socket; + using shared_context = std::shared_ptr; + /** + * Test peer that captures sent messages for verification. + */ + class PeerTest : public PeerImp + { + public: + PeerTest( + Application& app, + std::shared_ptr const& slot, + http_request_type&& request, + PublicKey const& publicKey, + ProtocolVersion protocol, + Resource::Consumer consumer, + std::unique_ptr&& stream_ptr, + OverlayImpl& overlay) + : PeerImp( + app, + id_++, + slot, + std::move(request), + publicKey, + protocol, + consumer, + std::move(stream_ptr), + overlay) + { + } + + ~PeerTest() = default; + + void + run() override + { + } + + void + send(std::shared_ptr const& m) override + { + lastSentMessage_ = m; + } + + std::shared_ptr + getLastSentMessage() const + { + return lastSentMessage_; + } + + static void + resetId() + { + id_ = 0; + } + + private: + inline static Peer::id_t id_ = 0; + std::shared_ptr lastSentMessage_; + }; + + shared_context context_{make_SSLContext("")}; + ProtocolVersion protocolVersion_{1, 7}; + + std::shared_ptr + createPeer(jtx::Env& env) + { + auto& overlay = dynamic_cast(env.app().overlay()); + boost::beast::http::request request; + auto stream_ptr = std::make_unique( + socket_type(env.app().getIOContext()), *context_); + + beast::IP::Endpoint local( + boost::asio::ip::make_address("172.1.1.1"), 51235); + beast::IP::Endpoint remote( + boost::asio::ip::make_address("172.1.1.2"), 51235); + + PublicKey key(std::get<0>(randomKeyPair(KeyType::ed25519))); + auto consumer = overlay.resourceManager().newInboundEndpoint(remote); + auto [slot, _] = overlay.peerFinder().new_inbound_slot(local, remote); + + auto peer = std::make_shared( + env.app(), + slot, + std::move(request), + key, + protocolVersion_, + consumer, + std::move(stream_ptr), + overlay); + + overlay.add_active(peer); + return peer; + } + + std::shared_ptr + createRequest(size_t const numObjects, Env& env) + { + // Store objects in the NodeStore that will be found during the query + auto& nodeStore = env.app().getNodeStore(); + + // Create and store objects + std::vector hashes; + hashes.reserve(numObjects); + for (int i = 0; i < numObjects; ++i) + { + uint256 hash(xrpl::sha512Half(i)); + hashes.push_back(hash); + + Blob data(100, static_cast(i % 256)); + nodeStore.store( + hotLEDGER, + std::move(data), + hash, + nodeStore.earliestLedgerSeq()); + } + + // Create a request with more objects than hardMaxReplyNodes + auto request = std::make_shared(); + request->set_type(protocol::TMGetObjectByHash_ObjectType_otLEDGER); + request->set_query(true); + + for (int i = 0; i < numObjects; ++i) + { + auto object = request->add_objects(); + object->set_hash(hashes[i].data(), hashes[i].size()); + object->set_ledgerseq(i); + } + return request; + } + + /** + * Test that reply is limited to hardMaxReplyNodes when more objects + * are requested than the limit allows. + */ + void + testReplyLimit(size_t const numObjects, int const expectedReplySize) + { + testcase("Reply Limit"); + + Env env(*this); + PeerTest::resetId(); + + auto peer = createPeer(env); + + auto request = createRequest(numObjects, env); + // Call the onMessage handler + peer->onMessage(request); + + // Verify that a reply was sent + auto sentMessage = peer->getLastSentMessage(); + BEAST_EXPECT(sentMessage != nullptr); + + // Parse the reply message + auto const& buffer = + sentMessage->getBuffer(compression::Compressed::Off); + + BEAST_EXPECT(buffer.size() > 6); + // Skip the message header (6 bytes: 4 for size, 2 for type) + protocol::TMGetObjectByHash reply; + BEAST_EXPECT( + reply.ParseFromArray(buffer.data() + 6, buffer.size() - 6) == true); + + // Verify the reply is limited to expectedReplySize + BEAST_EXPECT(reply.objects_size() == expectedReplySize); + } + + void + run() override + { + int const limit = static_cast(Tuning::hardMaxReplyNodes); + testReplyLimit(limit + 1, limit); + testReplyLimit(limit, limit); + testReplyLimit(limit - 1, limit - 1); + } +}; + +BEAST_DEFINE_TESTSUITE(TMGetObjectByHash, overlay, xrpl); + +} // namespace test +} // namespace xrpl diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index 1275c756cf..4e7a8388ee 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -29,10 +29,8 @@ struct STNumber_test : public beast::unit_test::suite } void - run() override + doRun() { - static_assert(!std::is_convertible_v); - { STNumber const stnum{sfNumber}; BEAST_EXPECT(stnum.getSType() == STI_NUMBER); @@ -127,6 +125,41 @@ struct STNumber_test : public beast::unit_test::suite BEAST_EXPECT( numberFromJson(sfNumber, "-0.000e6") == STNumber(sfNumber, 0)); + { + NumberRoundModeGuard mg(Number::towards_zero); + // maxint64 9,223,372,036,854,775,807 + auto const maxInt = + std::to_string(std::numeric_limits::max()); + // minint64 -9,223,372,036,854,775,808 + auto const minInt = + std::to_string(std::numeric_limits::min()); + if (Number::getMantissaScale() == MantissaRange::small) + { + BEAST_EXPECT( + numberFromJson(sfNumber, maxInt) == + STNumber(sfNumber, Number{9'223'372'036'854'775, 3})); + BEAST_EXPECT( + numberFromJson(sfNumber, minInt) == + STNumber(sfNumber, Number{-9'223'372'036'854'775, 3})); + } + else + { + BEAST_EXPECT( + numberFromJson(sfNumber, maxInt) == + STNumber( + sfNumber, Number{9'223'372'036'854'775'807, 0})); + BEAST_EXPECT( + numberFromJson(sfNumber, minInt) == + STNumber( + sfNumber, + Number{ + true, + 9'223'372'036'854'775'808ULL, + 0, + Number::normalized{}})); + } + } + constexpr auto imin = std::numeric_limits::min(); BEAST_EXPECT( numberFromJson(sfNumber, imin) == @@ -279,15 +312,21 @@ struct STNumber_test : public beast::unit_test::suite } } } + + void + run() override + { + static_assert(!std::is_convertible_v); + + for (auto const scale : {MantissaRange::small, MantissaRange::large}) + { + NumberMantissaScaleGuard sg(scale); + testcase << to_string(Number::getMantissaScale()); + doRun(); + } + } }; BEAST_DEFINE_TESTSUITE(STNumber, protocol, xrpl); -void -testCompile(std::ostream& out) -{ - STNumber number{sfNumber, 42}; - out << number; -} - } // namespace xrpl diff --git a/src/test/rpc/GetAggregatePrice_test.cpp b/src/test/rpc/GetAggregatePrice_test.cpp index 52f82ffc6c..0ffefc6cb6 100644 --- a/src/test/rpc/GetAggregatePrice_test.cpp +++ b/src/test/rpc/GetAggregatePrice_test.cpp @@ -191,18 +191,38 @@ public: // Aggregate data set includes all price oracle instances, no trimming // or time threshold { - Env env(*this); - OraclesData oracles; - prep(env, oracles); - // entire and trimmed stats - auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles); - BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45"); - BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10); - BEAST_EXPECT( - ret[jss::entire_set][jss::standard_deviation] == - "0.3027650354097492"); - BEAST_EXPECT(ret[jss::median] == "74.45"); - BEAST_EXPECT(ret[jss::time] == 946694900); + auto const all = testable_amendments(); + for (auto const& feats : + {all - featureSingleAssetVault - featureLendingProtocol, all}) + { + for (auto const mantissaSize : + {MantissaRange::small, MantissaRange::large}) + { + // Regardless of the features enabled, RPC is controlled by + // the global mantissa size. And since it's a thread-local, + // overriding it locally won't make a difference either. + // This will mean all RPC will use the default of "large". + NumberMantissaScaleGuard mg(mantissaSize); + + Env env(*this, feats); + OraclesData oracles; + prep(env, oracles); + // entire and trimmed stats + auto ret = + Oracle::aggregatePrice(env, "XRP", "USD", oracles); + BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45"); + BEAST_EXPECT( + ret[jss::entire_set][jss::size].asUInt() == 10); + // Short: 0.3027650354097492 + BEAST_EXPECTS( + ret[jss::entire_set][jss::standard_deviation] == + "0.3027650354097491666", + ret[jss::entire_set][jss::standard_deviation] + .asString()); + BEAST_EXPECT(ret[jss::median] == "74.45"); + BEAST_EXPECT(ret[jss::time] == 946694900); + } + } } // Aggregate data set includes all price oracle instances @@ -215,15 +235,19 @@ public: Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, 100); BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45"); BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10); - BEAST_EXPECT( + // Short: "0.3027650354097492", + BEAST_EXPECTS( ret[jss::entire_set][jss::standard_deviation] == - "0.3027650354097492"); + "0.3027650354097491666", + ret[jss::entire_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::median] == "74.45"); BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.45"); BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 6); - BEAST_EXPECT( + // Short: "0.187082869338697", + BEAST_EXPECTS( ret[jss::trimmed_set][jss::standard_deviation] == - "0.187082869338697"); + "0.1870828693386970693", + ret[jss::trimmed_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::time] == 946694900); } @@ -274,15 +298,19 @@ public: Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, "200"); BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.6"); BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 7); - BEAST_EXPECT( + // Short: 0.2160246899469287 + BEAST_EXPECTS( ret[jss::entire_set][jss::standard_deviation] == - "0.2160246899469287"); + "0.2160246899469286744", + ret[jss::entire_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::median] == "74.6"); BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.6"); BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 5); - BEAST_EXPECT( + // Short: 0.158113883008419 + BEAST_EXPECTS( ret[jss::trimmed_set][jss::standard_deviation] == - "0.158113883008419"); + "0.1581138830084189666", + ret[jss::trimmed_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::time] == 946694900); } diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 551e67dc5e..1b7079341c 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include #include @@ -30,6 +32,7 @@ enum class FieldType { CurrencyField, HashField, HashOrObjectField, + FixedHashField, IssueField, ObjectField, StringField, @@ -86,6 +89,7 @@ getTypeName(FieldType typeID) case FieldType::CurrencyField: return "Currency"; case FieldType::HashField: + case FieldType::FixedHashField: return "hex string"; case FieldType::HashOrObjectField: return "hex string or object"; @@ -202,6 +206,7 @@ class LedgerEntry_test : public beast::unit_test::suite static auto const& badBlobValues = remove({3, 7, 8, 16}); static auto const& badCurrencyValues = remove({14}); static auto const& badHashValues = remove({2, 3, 7, 8, 16}); + static auto const& badFixedHashValues = remove({1, 2, 3, 4, 7, 8, 16}); static auto const& badIndexValues = remove({12, 16, 18, 19}); static auto const& badUInt32Values = remove({2, 3}); static auto const& badUInt64Values = remove({2, 3}); @@ -222,6 +227,8 @@ class LedgerEntry_test : public beast::unit_test::suite return badHashValues; case FieldType::HashOrObjectField: return badIndexValues; + case FieldType::FixedHashField: + return badFixedHashValues; case FieldType::IssueField: return badIssueValues; case FieldType::UInt32Field: @@ -717,7 +724,12 @@ class LedgerEntry_test : public beast::unit_test::suite } // negative tests - runLedgerEntryTest(env, jss::amendments); + testMalformedField( + env, + Json::Value{}, + jss::amendments, + FieldType::FixedHashField, + "malformedRequest"); } void @@ -1538,7 +1550,12 @@ class LedgerEntry_test : public beast::unit_test::suite } // negative tests - runLedgerEntryTest(env, jss::fee); + testMalformedField( + env, + Json::Value{}, + jss::fee, + FieldType::FixedHashField, + "malformedRequest"); } void @@ -1561,7 +1578,12 @@ class LedgerEntry_test : public beast::unit_test::suite } // negative tests - runLedgerEntryTest(env, jss::hashes); + testMalformedField( + env, + Json::Value{}, + jss::hashes, + FieldType::FixedHashField, + "malformedRequest"); } void @@ -1686,7 +1708,12 @@ class LedgerEntry_test : public beast::unit_test::suite } // negative tests - runLedgerEntryTest(env, jss::nunl); + testMalformedField( + env, + Json::Value{}, + jss::nunl, + FieldType::FixedHashField, + "malformedRequest"); } void @@ -2343,6 +2370,438 @@ class LedgerEntry_test : public beast::unit_test::suite } } + /// Test the ledger entry types that don't take parameters + void + testFixed() + { + using namespace test::jtx; + + Account const alice{"alice"}; + Account const bob{"bob"}; + + Env env{*this, envconfig([](auto cfg) { + cfg->START_UP = Config::FRESH; + return cfg; + })}; + + env.close(); + + /** Verifies that the RPC result has the expected data + * + * @param good: Indicates that the request should have succeeded + * and returned a ledger object of `expectedType` type. + * @param jv: The RPC result Json value + * @param expectedType: The type that the ledger object should + * have if "good". + * @param expectedError: Optional. The expected error if not + * good. Defaults to "entryNotFound". + */ + auto checkResult = + [&](bool good, + Json::Value const& jv, + Json::StaticString const& expectedType, + std::optional const& expectedError = {}) { + if (good) + { + BEAST_EXPECTS( + jv.isObject() && jv.isMember(jss::result) && + !jv[jss::result].isMember(jss::error) && + jv[jss::result].isMember(jss::node) && + jv[jss::result][jss::node].isMember( + sfLedgerEntryType.jsonName) && + jv[jss::result][jss::node] + [sfLedgerEntryType.jsonName] == expectedType, + to_string(jv)); + } + else + { + BEAST_EXPECTS( + jv.isObject() && jv.isMember(jss::result) && + jv[jss::result].isMember(jss::error) && + !jv[jss::result].isMember(jss::node) && + jv[jss::result][jss::error] == + expectedError.value_or("entryNotFound"), + to_string(jv)); + } + }; + + /** Runs a series of tests for a given fixed-position ledger + * entry. + * + * @param field: The Json request field to use. + * @param expectedType: The type that the ledger object should + * have if "good". + * @param expectedKey: The keylet of the fixed object. + * @param good: Indicates whether the object is expected to + * exist. + */ + auto test = [&](Json::StaticString const& field, + Json::StaticString const& expectedType, + Keylet const& expectedKey, + bool good) { + testcase << expectedType.c_str() << (good ? "" : " not") + << " found"; + + auto const hexKey = strHex(expectedKey.key); + + { + // Test bad values + // "field":null + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[field] = Json::nullValue; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, expectedType, "malformedRequest"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + // "field":"string" + params[jss::ledger_index] = jss::validated; + params[field] = "arbitrary string"; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, expectedType, "malformedRequest"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + // "field":false + params[jss::ledger_index] = jss::validated; + params[field] = false; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, expectedType, "invalidParams"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + + // "field":[incorrect index hash] + auto const badKey = strHex(expectedKey.key + uint256{1}); + params[jss::ledger_index] = jss::validated; + params[field] = badKey; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, expectedType, "entryNotFound"); + BEAST_EXPECTS( + jv[jss::result][jss::index] == badKey, to_string(jv)); + } + + { + Json::Value params; + // "index":"field" using API 2 + params[jss::ledger_index] = jss::validated; + params[jss::index] = field; + params[jss::api_version] = 2; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, expectedType, "malformedRequest"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + std::string const pdIdx = [&]() { + { + Json::Value params; + // Test good values + // Use the "field":true notation + params[jss::ledger_index] = jss::validated; + params[field] = true; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + // Index will always be returned for valid parameters. + std::string const pdIdx = + jv[jss::result][jss::index].asString(); + BEAST_EXPECTS(hexKey == pdIdx, to_string(jv)); + checkResult(good, jv, expectedType); + + return pdIdx; + } + }(); + + { + Json::Value params; + // "field":"[index hash]" + params[jss::ledger_index] = jss::validated; + params[field] = hexKey; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(good, jv, expectedType); + BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey); + } + + { + // Bad value + // Use the "index":"field" notation with API 2 + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::index] = field; + params[jss::api_version] = 2; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, expectedType, "malformedRequest"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + // Use the "index":"field" notation with API 3 + params[jss::ledger_index] = jss::validated; + params[jss::index] = field; + params[jss::api_version] = 3; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + // Index is correct either way + BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey); + checkResult(good, jv, expectedType); + } + + { + Json::Value params; + // Use the "index":"[index hash]" notation + params[jss::ledger_index] = jss::validated; + params[jss::index] = pdIdx; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + // Index is correct either way + BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey); + checkResult(good, jv, expectedType); + } + }; + + test(jss::amendments, jss::Amendments, keylet::amendments(), true); + test(jss::fee, jss::FeeSettings, keylet::fees(), true); + // There won't be an nunl + test(jss::nunl, jss::NegativeUNL, keylet::negativeUNL(), false); + // Can only get the short skip list this way + test(jss::hashes, jss::LedgerHashes, keylet::skip(), true); + } + + void + testHashes() + { + using namespace test::jtx; + + Account const alice{"alice"}; + Account const bob{"bob"}; + + Env env{*this, envconfig([](auto cfg) { + cfg->START_UP = Config::FRESH; + return cfg; + })}; + + env.close(); + + /** Verifies that the RPC result has the expected data + * + * @param good: Indicates that the request should have succeeded + * and returned a ledger object of `expectedType` type. + * @param jv: The RPC result Json value + * @param expectedCount: The number of Hashes expected in the + * object if "good". + * @param expectedError: Optional. The expected error if not + * good. Defaults to "entryNotFound". + */ + auto checkResult = + [&](bool good, + Json::Value const& jv, + int expectedCount, + std::optional const& expectedError = {}) { + if (good) + { + BEAST_EXPECTS( + jv.isObject() && jv.isMember(jss::result) && + !jv[jss::result].isMember(jss::error) && + jv[jss::result].isMember(jss::node) && + jv[jss::result][jss::node].isMember( + sfLedgerEntryType.jsonName) && + jv[jss::result][jss::node] + [sfLedgerEntryType.jsonName] == jss::LedgerHashes, + to_string(jv)); + BEAST_EXPECTS( + jv[jss::result].isMember(jss::node) && + jv[jss::result][jss::node].isMember("Hashes") && + jv[jss::result][jss::node]["Hashes"].size() == + expectedCount, + to_string(jv[jss::result][jss::node]["Hashes"].size())); + } + else + { + BEAST_EXPECTS( + jv.isObject() && jv.isMember(jss::result) && + jv[jss::result].isMember(jss::error) && + !jv[jss::result].isMember(jss::node) && + jv[jss::result][jss::error] == + expectedError.value_or("entryNotFound"), + to_string(jv)); + } + }; + + /** Runs a series of tests for a given ledger index. + * + * @param ledger: The ledger index value of the "hashes" request + * parameter. May not necessarily be a number. + * @param expectedKey: The expected keylet of the object. + * @param good: Indicates whether the object is expected to + * exist. + * @param expectedCount: The number of Hashes expected in the + * object if "good". + */ + auto test = [&](Json::Value ledger, + Keylet const& expectedKey, + bool good, + int expectedCount = 0) { + testcase << "LedgerHashes: seq: " << env.current()->header().seq + << " \"hashes\":" << to_string(ledger) + << (good ? "" : " not") << " found"; + + auto const hexKey = strHex(expectedKey.key); + + { + // Test bad values + // "hashes":null + Json::Value params; + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = Json::nullValue; + auto jv = env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, 0, "malformedRequest"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + // "hashes":"non-uint string" + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = "arbitrary string"; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, 0, "malformedRequest"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + // "hashes":"uint string" is invalid, too + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = "10"; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, 0, "malformedRequest"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + // "hashes":false + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = false; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, 0, "invalidParams"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + { + Json::Value params; + // "hashes":-1 + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = -1; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, 0, "internal"); + BEAST_EXPECT(!jv[jss::result].isMember(jss::index)); + } + + // "hashes":[incorrect index hash] + { + Json::Value params; + auto const badKey = strHex(expectedKey.key + uint256{1}); + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = badKey; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(false, jv, 0, "entryNotFound"); + BEAST_EXPECT(jv[jss::result][jss::index] == badKey); + } + + { + Json::Value params; + // Test good values + // Use the "hashes":ledger notation + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = ledger; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(good, jv, expectedCount); + // Index will always be returned for valid parameters. + std::string const pdIdx = + jv[jss::result][jss::index].asString(); + BEAST_EXPECTS(hexKey == pdIdx, strHex(pdIdx)); + } + + { + Json::Value params; + // "hashes":"[index hash]" + params[jss::ledger_index] = jss::validated; + params[jss::hashes] = hexKey; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(good, jv, expectedCount); + // Index is correct either way + BEAST_EXPECTS( + hexKey == jv[jss::result][jss::index].asString(), + strHex(jv[jss::result][jss::index].asString())); + } + + { + Json::Value params; + // Use the "index":"[index hash]" notation + params[jss::ledger_index] = jss::validated; + params[jss::index] = hexKey; + auto const jv = + env.rpc("json", "ledger_entry", to_string(params)); + checkResult(good, jv, expectedCount); + // Index is correct either way + BEAST_EXPECTS( + hexKey == jv[jss::result][jss::index].asString(), + strHex(jv[jss::result][jss::index].asString())); + } + }; + + // short skip list + test(true, keylet::skip(), true, 2); + // long skip list at index 0 + test(1, keylet::skip(1), false); + // long skip list at index 1 + test(1 << 17, keylet::skip(1 << 17), false); + + // Close more ledgers, but stop short of the flag ledger + for (auto i = env.current()->seq(); i <= 250; ++i) + env.close(); + + // short skip list + test(true, keylet::skip(), true, 249); + // long skip list at index 0 + test(1, keylet::skip(1), false); + // long skip list at index 1 + test(1 << 17, keylet::skip(1 << 17), false); + + // Close a flag ledger so the first "long" skip list is created + for (auto i = env.current()->seq(); i <= 260; ++i) + env.close(); + + // short skip list + test(true, keylet::skip(), true, 256); + // long skip list at index 0 + test(1, keylet::skip(1), true, 1); + // long skip list at index 1 + test(1 << 17, keylet::skip(1 << 17), false); + } + void testCLI() { @@ -2400,6 +2859,8 @@ public: testOracleLedgerEntry(); testMPT(); testPermissionedDomain(); + testFixed(); + testHashes(); testCLI(); } }; diff --git a/src/tests/README.md b/src/tests/README.md index 7c4cc5edf8..2a642a7633 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -1,5 +1,5 @@ # Unit tests This directory contains unit tests for the project. The difference from existing `src/test` folder -is that we switch to 3rd party testing framework (doctest). We intend to gradually move existing tests -from our own framework to doctest and such tests will be moved to this new folder. +is that we switch to 3rd party testing framework (`gtest`). We intend to gradually move existing tests +from our own framework to `gtest` and such tests will be moved to this new folder. diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index a2374698d9..74dc184700 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -1,14 +1,14 @@ include(XrplAddTest) # Test requirements. -find_package(doctest REQUIRED) +find_package(GTest REQUIRED) # Custom target for all tests defined in this file add_custom_target(xrpl.tests) # Common library dependencies for the rest of the tests. add_library(xrpl.imports.test INTERFACE) -target_link_libraries(xrpl.imports.test INTERFACE doctest::doctest xrpl.libxrpl) +target_link_libraries(xrpl.imports.test INTERFACE gtest::gtest xrpl.libxrpl) # One test for each module. xrpl_add_test(basics) diff --git a/src/tests/libxrpl/basics/RangeSet.cpp b/src/tests/libxrpl/basics/RangeSet.cpp index 8c43b26758..d0fc656368 100644 --- a/src/tests/libxrpl/basics/RangeSet.cpp +++ b/src/tests/libxrpl/basics/RangeSet.cpp @@ -1,15 +1,13 @@ #include -#include +#include #include #include using namespace xrpl; -TEST_SUITE_BEGIN("RangeSet"); - -TEST_CASE("prevMissing") +TEST(RangeSet, prevMissing) { // Set will include: // [ 0, 5] @@ -31,80 +29,78 @@ TEST_CASE("prevMissing") expected = ((i % 10) > 6) ? (i - 1) : oneBelowRange; } - CHECK(prevMissing(set, i) == expected); + EXPECT_EQ(prevMissing(set, i), expected); } } -TEST_CASE("toString") +TEST(RangeSet, toString) { RangeSet set; - CHECK(to_string(set) == "empty"); + EXPECT_EQ(to_string(set), "empty"); set.insert(1); - CHECK(to_string(set) == "1"); + EXPECT_EQ(to_string(set), "1"); set.insert(range(4u, 6u)); - CHECK(to_string(set) == "1,4-6"); + EXPECT_EQ(to_string(set), "1,4-6"); set.insert(2); - CHECK(to_string(set) == "1-2,4-6"); + EXPECT_EQ(to_string(set), "1-2,4-6"); set.erase(range(4u, 5u)); - CHECK(to_string(set) == "1-2,6"); + EXPECT_EQ(to_string(set), "1-2,6"); } -TEST_CASE("fromString") +TEST(RangeSet, fromString) { RangeSet set; - CHECK(!from_string(set, "")); - CHECK(boost::icl::length(set) == 0); + EXPECT_FALSE(from_string(set, "")); + EXPECT_EQ(boost::icl::length(set), 0); - CHECK(!from_string(set, "#")); - CHECK(boost::icl::length(set) == 0); + EXPECT_FALSE(from_string(set, "#")); + EXPECT_EQ(boost::icl::length(set), 0); - CHECK(!from_string(set, ",")); - CHECK(boost::icl::length(set) == 0); + EXPECT_FALSE(from_string(set, ",")); + EXPECT_EQ(boost::icl::length(set), 0); - CHECK(!from_string(set, ",-")); - CHECK(boost::icl::length(set) == 0); + EXPECT_FALSE(from_string(set, ",-")); + EXPECT_EQ(boost::icl::length(set), 0); - CHECK(!from_string(set, "1,,2")); - CHECK(boost::icl::length(set) == 0); + EXPECT_FALSE(from_string(set, "1,,2")); + EXPECT_EQ(boost::icl::length(set), 0); - CHECK(from_string(set, "1")); - CHECK(boost::icl::length(set) == 1); - CHECK(boost::icl::first(set) == 1); + EXPECT_TRUE(from_string(set, "1")); + EXPECT_EQ(boost::icl::length(set), 1); + EXPECT_EQ(boost::icl::first(set), 1); - CHECK(from_string(set, "1,1")); - CHECK(boost::icl::length(set) == 1); - CHECK(boost::icl::first(set) == 1); + EXPECT_TRUE(from_string(set, "1,1")); + EXPECT_EQ(boost::icl::length(set), 1); + EXPECT_EQ(boost::icl::first(set), 1); - CHECK(from_string(set, "1-1")); - CHECK(boost::icl::length(set) == 1); - CHECK(boost::icl::first(set) == 1); + EXPECT_TRUE(from_string(set, "1-1")); + EXPECT_EQ(boost::icl::length(set), 1); + EXPECT_EQ(boost::icl::first(set), 1); - CHECK(from_string(set, "1,4-6")); - CHECK(boost::icl::length(set) == 4); - CHECK(boost::icl::first(set) == 1); - CHECK(!boost::icl::contains(set, 2)); - CHECK(!boost::icl::contains(set, 3)); - CHECK(boost::icl::contains(set, 4)); - CHECK(boost::icl::contains(set, 5)); - CHECK(boost::icl::last(set) == 6); + EXPECT_TRUE(from_string(set, "1,4-6")); + EXPECT_EQ(boost::icl::length(set), 4); + EXPECT_EQ(boost::icl::first(set), 1); + EXPECT_FALSE(boost::icl::contains(set, 2)); + EXPECT_FALSE(boost::icl::contains(set, 3)); + EXPECT_TRUE(boost::icl::contains(set, 4)); + EXPECT_TRUE(boost::icl::contains(set, 5)); + EXPECT_EQ(boost::icl::last(set), 6); - CHECK(from_string(set, "1-2,4-6")); - CHECK(boost::icl::length(set) == 5); - CHECK(boost::icl::first(set) == 1); - CHECK(boost::icl::contains(set, 2)); - CHECK(boost::icl::contains(set, 4)); - CHECK(boost::icl::last(set) == 6); + EXPECT_TRUE(from_string(set, "1-2,4-6")); + EXPECT_EQ(boost::icl::length(set), 5); + EXPECT_EQ(boost::icl::first(set), 1); + EXPECT_TRUE(boost::icl::contains(set, 2)); + EXPECT_TRUE(boost::icl::contains(set, 4)); + EXPECT_EQ(boost::icl::last(set), 6); - CHECK(from_string(set, "1-2,6")); - CHECK(boost::icl::length(set) == 3); - CHECK(boost::icl::first(set) == 1); - CHECK(boost::icl::contains(set, 2)); - CHECK(boost::icl::last(set) == 6); + EXPECT_TRUE(from_string(set, "1-2,6")); + EXPECT_EQ(boost::icl::length(set), 3); + EXPECT_EQ(boost::icl::first(set), 1); + EXPECT_TRUE(boost::icl::contains(set, 2)); + EXPECT_EQ(boost::icl::last(set), 6); } - -TEST_SUITE_END(); diff --git a/src/tests/libxrpl/basics/Slice.cpp b/src/tests/libxrpl/basics/Slice.cpp index 03d89ff174..b36abe596d 100644 --- a/src/tests/libxrpl/basics/Slice.cpp +++ b/src/tests/libxrpl/basics/Slice.cpp @@ -1,6 +1,6 @@ #include -#include +#include #include #include @@ -12,37 +12,35 @@ static std::uint8_t const data[] = { 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; -TEST_SUITE_BEGIN("Slice"); - -TEST_CASE("equality & inequality") +TEST(Slice, equality_and_inequality) { Slice const s0{}; - CHECK(s0.size() == 0); - CHECK(s0.data() == nullptr); - CHECK(s0 == s0); + EXPECT_EQ(s0.size(), 0); + EXPECT_EQ(s0.data(), nullptr); + EXPECT_EQ(s0, s0); // Test slices of equal and unequal size pointing to same data: for (std::size_t i = 0; i != sizeof(data); ++i) { Slice const s1{data, i}; - CHECK(s1.size() == i); - CHECK(s1.data() != nullptr); + EXPECT_EQ(s1.size(), i); + EXPECT_NE(s1.data(), nullptr); if (i == 0) - CHECK(s1 == s0); + EXPECT_EQ(s1, s0); else - CHECK(s1 != s0); + EXPECT_NE(s1, s0); for (std::size_t j = 0; j != sizeof(data); ++j) { Slice const s2{data, j}; if (i == j) - CHECK(s1 == s2); + EXPECT_EQ(s1, s2); else - CHECK(s1 != s2); + EXPECT_NE(s1, s2); } } @@ -53,22 +51,22 @@ TEST_CASE("equality & inequality") for (std::size_t i = 0; i != sizeof(data); ++i) a[i] = b[i] = data[i]; - CHECK(makeSlice(a) == makeSlice(b)); + EXPECT_EQ(makeSlice(a), makeSlice(b)); b[7]++; - CHECK(makeSlice(a) != makeSlice(b)); + EXPECT_NE(makeSlice(a), makeSlice(b)); a[7]++; - CHECK(makeSlice(a) == makeSlice(b)); + EXPECT_EQ(makeSlice(a), makeSlice(b)); } -TEST_CASE("indexing") +TEST(Slice, indexing) { Slice const s{data, sizeof(data)}; for (std::size_t i = 0; i != sizeof(data); ++i) - CHECK(s[i] == data[i]); + EXPECT_EQ(s[i], data[i]); } -TEST_CASE("advancing") +TEST(Slice, advancing) { for (std::size_t i = 0; i < sizeof(data); ++i) { @@ -77,10 +75,8 @@ TEST_CASE("advancing") Slice s(data + i, sizeof(data) - i); s += j; - CHECK(s.data() == data + i + j); - CHECK(s.size() == sizeof(data) - i - j); + EXPECT_EQ(s.data(), data + i + j); + EXPECT_EQ(s.size(), sizeof(data) - i - j); } } } - -TEST_SUITE_END(); diff --git a/src/tests/libxrpl/basics/base64.cpp b/src/tests/libxrpl/basics/base64.cpp index f6544105d8..cfffadf660 100644 --- a/src/tests/libxrpl/basics/base64.cpp +++ b/src/tests/libxrpl/basics/base64.cpp @@ -1,6 +1,6 @@ #include -#include +#include #include @@ -10,11 +10,11 @@ static void check(std::string const& in, std::string const& out) { auto const encoded = base64_encode(in); - CHECK(encoded == out); - CHECK(base64_decode(encoded) == in); + EXPECT_EQ(encoded, out); + EXPECT_EQ(base64_decode(encoded), in); } -TEST_CASE("base64") +TEST(base64, base64) { // cspell: disable check("", ""); @@ -46,5 +46,5 @@ TEST_CASE("base64") std::string const notBase64 = "not_base64!!"; std::string const truncated = "not"; - CHECK(base64_decode(notBase64) == base64_decode(truncated)); + EXPECT_EQ(base64_decode(notBase64), base64_decode(truncated)); } diff --git a/src/tests/libxrpl/basics/contract.cpp b/src/tests/libxrpl/basics/contract.cpp index a1f6f0b777..d9b729e85d 100644 --- a/src/tests/libxrpl/basics/contract.cpp +++ b/src/tests/libxrpl/basics/contract.cpp @@ -1,13 +1,13 @@ #include -#include +#include #include #include using namespace xrpl; -TEST_CASE("contract") +TEST(contract, contract) { try { @@ -15,7 +15,7 @@ TEST_CASE("contract") } catch (std::runtime_error const& e1) { - CHECK(std::string(e1.what()) == "Throw test"); + EXPECT_STREQ(e1.what(), "Throw test"); try { @@ -23,15 +23,15 @@ TEST_CASE("contract") } catch (std::runtime_error const& e2) { - CHECK(std::string(e2.what()) == "Throw test"); + EXPECT_STREQ(e2.what(), "Throw test"); } catch (...) { - CHECK(false); + FAIL() << "std::runtime_error should have been re-caught"; } } catch (...) { - CHECK(false); + FAIL() << "std::runtime_error should have been caught the first time"; } } diff --git a/src/tests/libxrpl/basics/main.cpp b/src/tests/libxrpl/basics/main.cpp index 0a3f254ea8..5142bbe08a 100644 --- a/src/tests/libxrpl/basics/main.cpp +++ b/src/tests/libxrpl/basics/main.cpp @@ -1,2 +1,8 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include +#include + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/tests/libxrpl/basics/mulDiv.cpp b/src/tests/libxrpl/basics/mulDiv.cpp index d3c58ea2f4..c98c3fd61a 100644 --- a/src/tests/libxrpl/basics/mulDiv.cpp +++ b/src/tests/libxrpl/basics/mulDiv.cpp @@ -1,45 +1,45 @@ #include -#include +#include #include #include using namespace xrpl; -TEST_CASE("mulDiv") +TEST(mulDiv, mulDiv) { auto const max = std::numeric_limits::max(); std::uint64_t const max32 = std::numeric_limits::max(); auto result = mulDiv(85, 20, 5); - REQUIRE(result); - CHECK(*result == 340); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 340); result = mulDiv(20, 85, 5); - REQUIRE(result); - CHECK(*result == 340); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 340); result = mulDiv(0, max - 1, max - 3); - REQUIRE(result); - CHECK(*result == 0); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0); result = mulDiv(max - 1, 0, max - 3); - REQUIRE(result); - CHECK(*result == 0); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0); result = mulDiv(max, 2, max / 2); - REQUIRE(result); - CHECK(*result == 4); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 4); result = mulDiv(max, 1000, max / 1000); - REQUIRE(result); - CHECK(*result == 1000000); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1000000); result = mulDiv(max, 1000, max / 1001); - REQUIRE(result); - CHECK(*result == 1001000); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1001000); result = mulDiv(max32 + 1, max32 + 1, 5); - REQUIRE(result); - CHECK(*result == 3689348814741910323); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 3689348814741910323); // Overflow result = mulDiv(max - 1, max - 2, 5); - CHECK(!result); + EXPECT_FALSE(result.has_value()); } diff --git a/src/tests/libxrpl/basics/scope.cpp b/src/tests/libxrpl/basics/scope.cpp index b3774d54bd..309a41ec04 100644 --- a/src/tests/libxrpl/basics/scope.cpp +++ b/src/tests/libxrpl/basics/scope.cpp @@ -1,10 +1,10 @@ #include -#include +#include using namespace xrpl; -TEST_CASE("scope_exit") +TEST(scope, scope_exit) { // scope_exit always executes the functor on destruction, // unless release() is called @@ -12,23 +12,23 @@ TEST_CASE("scope_exit") { scope_exit x{[&i]() { i = 1; }}; } - CHECK(i == 1); + EXPECT_EQ(i, 1); { scope_exit x{[&i]() { i = 2; }}; x.release(); } - CHECK(i == 1); + EXPECT_EQ(i, 1); { scope_exit x{[&i]() { i += 2; }}; auto x2 = std::move(x); } - CHECK(i == 3); + EXPECT_EQ(i, 3); { scope_exit x{[&i]() { i = 4; }}; x.release(); auto x2 = std::move(x); } - CHECK(i == 3); + EXPECT_EQ(i, 3); { try { @@ -39,7 +39,7 @@ TEST_CASE("scope_exit") { } } - CHECK(i == 5); + EXPECT_EQ(i, 5); { try { @@ -51,10 +51,10 @@ TEST_CASE("scope_exit") { } } - CHECK(i == 5); + EXPECT_EQ(i, 5); } -TEST_CASE("scope_fail") +TEST(scope, scope_fail) { // scope_fail executes the functor on destruction only // if an exception is unwinding, unless release() is called @@ -62,23 +62,23 @@ TEST_CASE("scope_fail") { scope_fail x{[&i]() { i = 1; }}; } - CHECK(i == 0); + EXPECT_EQ(i, 0); { scope_fail x{[&i]() { i = 2; }}; x.release(); } - CHECK(i == 0); + EXPECT_EQ(i, 0); { scope_fail x{[&i]() { i = 3; }}; auto x2 = std::move(x); } - CHECK(i == 0); + EXPECT_EQ(i, 0); { scope_fail x{[&i]() { i = 4; }}; x.release(); auto x2 = std::move(x); } - CHECK(i == 0); + EXPECT_EQ(i, 0); { try { @@ -89,7 +89,7 @@ TEST_CASE("scope_fail") { } } - CHECK(i == 5); + EXPECT_EQ(i, 5); { try { @@ -101,10 +101,10 @@ TEST_CASE("scope_fail") { } } - CHECK(i == 5); + EXPECT_EQ(i, 5); } -TEST_CASE("scope_success") +TEST(scope, scope_success) { // scope_success executes the functor on destruction only // if an exception is not unwinding, unless release() is called @@ -112,23 +112,23 @@ TEST_CASE("scope_success") { scope_success x{[&i]() { i = 1; }}; } - CHECK(i == 1); + EXPECT_EQ(i, 1); { scope_success x{[&i]() { i = 2; }}; x.release(); } - CHECK(i == 1); + EXPECT_EQ(i, 1); { scope_success x{[&i]() { i += 2; }}; auto x2 = std::move(x); } - CHECK(i == 3); + EXPECT_EQ(i, 3); { scope_success x{[&i]() { i = 4; }}; x.release(); auto x2 = std::move(x); } - CHECK(i == 3); + EXPECT_EQ(i, 3); { try { @@ -139,7 +139,7 @@ TEST_CASE("scope_success") { } } - CHECK(i == 3); + EXPECT_EQ(i, 3); { try { @@ -151,5 +151,5 @@ TEST_CASE("scope_success") { } } - CHECK(i == 3); + EXPECT_EQ(i, 3); } diff --git a/src/tests/libxrpl/basics/tagged_integer.cpp b/src/tests/libxrpl/basics/tagged_integer.cpp index 45efc579ab..09f8b6787b 100644 --- a/src/tests/libxrpl/basics/tagged_integer.cpp +++ b/src/tests/libxrpl/basics/tagged_integer.cpp @@ -1,6 +1,6 @@ #include -#include +#include #include @@ -102,127 +102,123 @@ static_assert( !std::is_convertible::value, "TagUInt2 should not be convertible to a TagUInt3"); -TEST_SUITE_BEGIN("tagged_integer"); - using TagInt = tagged_integer; -TEST_CASE("comparison operators") +TEST(tagged_integer, comparison_operators) { TagInt const zero(0); TagInt const one(1); - CHECK(one == one); - CHECK(!(one == zero)); + EXPECT_TRUE(one == one); + EXPECT_FALSE(one == zero); - CHECK(one != zero); - CHECK(!(one != one)); + EXPECT_TRUE(one != zero); + EXPECT_FALSE(one != one); - CHECK(zero < one); - CHECK(!(one < zero)); + EXPECT_TRUE(zero < one); + EXPECT_FALSE(one < zero); - CHECK(one > zero); - CHECK(!(zero > one)); + EXPECT_TRUE(one > zero); + EXPECT_FALSE(zero > one); - CHECK(one >= one); - CHECK(one >= zero); - CHECK(!(zero >= one)); + EXPECT_TRUE(one >= one); + EXPECT_TRUE(one >= zero); + EXPECT_FALSE(zero >= one); - CHECK(zero <= one); - CHECK(zero <= zero); - CHECK(!(one <= zero)); + EXPECT_TRUE(zero <= one); + EXPECT_TRUE(zero <= zero); + EXPECT_FALSE(one <= zero); } -TEST_CASE("increment / decrement operators") +TEST(tagged_integer, increment_decrement_operators) { TagInt const zero(0); TagInt const one(1); TagInt a{0}; ++a; - CHECK(a == one); + EXPECT_EQ(a, one); --a; - CHECK(a == zero); + EXPECT_EQ(a, zero); a++; - CHECK(a == one); + EXPECT_EQ(a, one); a--; - CHECK(a == zero); + EXPECT_EQ(a, zero); } -TEST_CASE("arithmetic operators") +TEST(tagged_integer, arithmetic_operators) { TagInt a{-2}; - CHECK(+a == TagInt{-2}); - CHECK(-a == TagInt{2}); - CHECK(TagInt{-3} + TagInt{4} == TagInt{1}); - CHECK(TagInt{-3} - TagInt{4} == TagInt{-7}); - CHECK(TagInt{-3} * TagInt{4} == TagInt{-12}); - CHECK(TagInt{8} / TagInt{4} == TagInt{2}); - CHECK(TagInt{7} % TagInt{4} == TagInt{3}); + EXPECT_EQ(+a, TagInt{-2}); + EXPECT_EQ(-a, TagInt{2}); + EXPECT_EQ(TagInt{-3} + TagInt{4}, TagInt{1}); + EXPECT_EQ(TagInt{-3} - TagInt{4}, TagInt{-7}); + EXPECT_EQ(TagInt{-3} * TagInt{4}, TagInt{-12}); + EXPECT_EQ(TagInt{8} / TagInt{4}, TagInt{2}); + EXPECT_EQ(TagInt{7} % TagInt{4}, TagInt{3}); - CHECK(~TagInt{8} == TagInt{~TagInt::value_type{8}}); - CHECK((TagInt{6} & TagInt{3}) == TagInt{2}); - CHECK((TagInt{6} | TagInt{3}) == TagInt{7}); - CHECK((TagInt{6} ^ TagInt{3}) == TagInt{5}); + EXPECT_EQ(~TagInt{8}, TagInt{~TagInt::value_type{8}}); + EXPECT_EQ((TagInt{6} & TagInt{3}), TagInt{2}); + EXPECT_EQ((TagInt{6} | TagInt{3}), TagInt{7}); + EXPECT_EQ((TagInt{6} ^ TagInt{3}), TagInt{5}); - CHECK((TagInt{4} << TagInt{2}) == TagInt{16}); - CHECK((TagInt{16} >> TagInt{2}) == TagInt{4}); + EXPECT_EQ((TagInt{4} << TagInt{2}), TagInt{16}); + EXPECT_EQ((TagInt{16} >> TagInt{2}), TagInt{4}); } -TEST_CASE("assignment operators") +TEST(tagged_integer, assignment_operators) { TagInt a{-2}; TagInt b{0}; b = a; - CHECK(b == TagInt{-2}); + EXPECT_EQ(b, TagInt{-2}); // -3 + 4 == 1 a = TagInt{-3}; a += TagInt{4}; - CHECK(a == TagInt{1}); + EXPECT_EQ(a, TagInt{1}); // -3 - 4 == -7 a = TagInt{-3}; a -= TagInt{4}; - CHECK(a == TagInt{-7}); + EXPECT_EQ(a, TagInt{-7}); // -3 * 4 == -12 a = TagInt{-3}; a *= TagInt{4}; - CHECK(a == TagInt{-12}); + EXPECT_EQ(a, TagInt{-12}); // 8/4 == 2 a = TagInt{8}; a /= TagInt{4}; - CHECK(a == TagInt{2}); + EXPECT_EQ(a, TagInt{2}); // 7 % 4 == 3 a = TagInt{7}; a %= TagInt{4}; - CHECK(a == TagInt{3}); + EXPECT_EQ(a, TagInt{3}); // 6 & 3 == 2 a = TagInt{6}; a /= TagInt{3}; - CHECK(a == TagInt{2}); + EXPECT_EQ(a, TagInt{2}); // 6 | 3 == 7 a = TagInt{6}; a |= TagInt{3}; - CHECK(a == TagInt{7}); + EXPECT_EQ(a, TagInt{7}); // 6 ^ 3 == 5 a = TagInt{6}; a ^= TagInt{3}; - CHECK(a == TagInt{5}); + EXPECT_EQ(a, TagInt{5}); // 4 << 2 == 16 a = TagInt{4}; a <<= TagInt{2}; - CHECK(a == TagInt{16}); + EXPECT_EQ(a, TagInt{16}); // 16 >> 2 == 4 a = TagInt{16}; a >>= TagInt{2}; - CHECK(a == TagInt{4}); + EXPECT_EQ(a, TagInt{4}); } - -TEST_SUITE_END(); diff --git a/src/tests/libxrpl/crypto/csprng.cpp b/src/tests/libxrpl/crypto/csprng.cpp index e59c8a555a..41dcfd57a9 100644 --- a/src/tests/libxrpl/crypto/csprng.cpp +++ b/src/tests/libxrpl/crypto/csprng.cpp @@ -1,15 +1,15 @@ #include -#include +#include using namespace xrpl; -TEST_CASE("get values") +TEST(csprng, get_values) { auto& engine = crypto_prng(); auto rand_val = engine(); - CHECK(rand_val >= engine.min()); - CHECK(rand_val <= engine.max()); + EXPECT_GE(rand_val, engine.min()); + EXPECT_LE(rand_val, engine.max()); uint16_t twoByte{0}; engine(&twoByte, sizeof(uint16_t)); } diff --git a/src/tests/libxrpl/crypto/main.cpp b/src/tests/libxrpl/crypto/main.cpp index 0a3f254ea8..5142bbe08a 100644 --- a/src/tests/libxrpl/crypto/main.cpp +++ b/src/tests/libxrpl/crypto/main.cpp @@ -1,2 +1,8 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include +#include + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/tests/libxrpl/json/Output.cpp b/src/tests/libxrpl/json/Output.cpp index 6e6c20a0e5..96d7369d51 100644 --- a/src/tests/libxrpl/json/Output.cpp +++ b/src/tests/libxrpl/json/Output.cpp @@ -2,31 +2,29 @@ #include #include -#include +#include #include using namespace xrpl; using namespace Json; -TEST_SUITE_BEGIN("JsonOutput"); - static void checkOutput(std::string const& valueDesc) { std::string output; Json::Value value; - REQUIRE(Json::Reader().parse(valueDesc, value)); + ASSERT_TRUE(Json::Reader().parse(valueDesc, value)); auto out = stringOutput(output); outputJson(value, out); auto expected = Json::FastWriter().write(value); - CHECK(output == expected); - CHECK(output == valueDesc); - CHECK(output == jsonAsString(value)); + EXPECT_EQ(output, expected); + EXPECT_EQ(output, valueDesc); + EXPECT_EQ(output, jsonAsString(value)); } -TEST_CASE("output cases") +TEST(JsonOutput, output_cases) { checkOutput("{}"); checkOutput("[]"); @@ -36,5 +34,3 @@ TEST_CASE("output cases") checkOutput("[[]]"); checkOutput(R"({"array":[{"12":23},{},null,false,0.5]})"); } - -TEST_SUITE_END(); diff --git a/src/tests/libxrpl/json/Value.cpp b/src/tests/libxrpl/json/Value.cpp index 25bd2f548d..4db1274a4d 100644 --- a/src/tests/libxrpl/json/Value.cpp +++ b/src/tests/libxrpl/json/Value.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include @@ -14,9 +14,7 @@ namespace xrpl { -TEST_SUITE_BEGIN("json_value"); - -TEST_CASE("limits") +TEST(json_value, limits) { using namespace Json; static_assert(Value::minInt == Int(~(UInt(-1) / 2))); @@ -24,31 +22,31 @@ TEST_CASE("limits") static_assert(Value::maxUInt == UInt(-1)); } -TEST_CASE("construct and compare Json::StaticString") +TEST(json_value, construct_and_compare_Json_StaticString) { static constexpr char sample[]{"Contents of a Json::StaticString"}; static constexpr Json::StaticString test1(sample); char const* addrTest1{test1}; - CHECK(addrTest1 == &sample[0]); - CHECK(test1.c_str() == &sample[0]); + EXPECT_EQ(addrTest1, &sample[0]); + EXPECT_EQ(test1.c_str(), &sample[0]); static constexpr Json::StaticString test2{ "Contents of a Json::StaticString"}; static constexpr Json::StaticString test3{"Another StaticString"}; - CHECK(test1 == test2); - CHECK(test1 != test3); + EXPECT_EQ(test1, test2); + EXPECT_NE(test1, test3); std::string str{sample}; - CHECK(str == test2); - CHECK(str != test3); - CHECK(test2 == str); - CHECK(test3 != str); + EXPECT_EQ(str, test2); + EXPECT_NE(str, test3); + EXPECT_EQ(test2, str); + EXPECT_NE(test3, str); } -TEST_CASE("different types") +TEST(json_value, different_types) { // Exercise ValueType constructor static constexpr Json::StaticString staticStr{"staticStr"}; @@ -56,166 +54,166 @@ TEST_CASE("different types") auto testCopy = [](Json::ValueType typ) { Json::Value val{typ}; Json::Value cpy{val}; - CHECK(val.type() == typ); - CHECK(cpy.type() == typ); + EXPECT_EQ(val.type(), typ); + EXPECT_EQ(cpy.type(), typ); return val; }; { Json::Value const nullV{testCopy(Json::nullValue)}; - CHECK(nullV.isNull()); - CHECK(!nullV.isBool()); - CHECK(!nullV.isInt()); - CHECK(!nullV.isUInt()); - CHECK(!nullV.isIntegral()); - CHECK(!nullV.isDouble()); - CHECK(!nullV.isNumeric()); - CHECK(!nullV.isString()); - CHECK(!nullV.isArray()); - CHECK(nullV.isArrayOrNull()); - CHECK(!nullV.isObject()); - CHECK(nullV.isObjectOrNull()); + EXPECT_TRUE(nullV.isNull()); + EXPECT_FALSE(nullV.isBool()); + EXPECT_FALSE(nullV.isInt()); + EXPECT_FALSE(nullV.isUInt()); + EXPECT_FALSE(nullV.isIntegral()); + EXPECT_FALSE(nullV.isDouble()); + EXPECT_FALSE(nullV.isNumeric()); + EXPECT_FALSE(nullV.isString()); + EXPECT_FALSE(nullV.isArray()); + EXPECT_TRUE(nullV.isArrayOrNull()); + EXPECT_FALSE(nullV.isObject()); + EXPECT_TRUE(nullV.isObjectOrNull()); } { Json::Value const intV{testCopy(Json::intValue)}; - CHECK(!intV.isNull()); - CHECK(!intV.isBool()); - CHECK(intV.isInt()); - CHECK(!intV.isUInt()); - CHECK(intV.isIntegral()); - CHECK(!intV.isDouble()); - CHECK(intV.isNumeric()); - CHECK(!intV.isString()); - CHECK(!intV.isArray()); - CHECK(!intV.isArrayOrNull()); - CHECK(!intV.isObject()); - CHECK(!intV.isObjectOrNull()); + EXPECT_FALSE(intV.isNull()); + EXPECT_FALSE(intV.isBool()); + EXPECT_TRUE(intV.isInt()); + EXPECT_FALSE(intV.isUInt()); + EXPECT_TRUE(intV.isIntegral()); + EXPECT_FALSE(intV.isDouble()); + EXPECT_TRUE(intV.isNumeric()); + EXPECT_FALSE(intV.isString()); + EXPECT_FALSE(intV.isArray()); + EXPECT_FALSE(intV.isArrayOrNull()); + EXPECT_FALSE(intV.isObject()); + EXPECT_FALSE(intV.isObjectOrNull()); } { Json::Value const uintV{testCopy(Json::uintValue)}; - CHECK(!uintV.isNull()); - CHECK(!uintV.isBool()); - CHECK(!uintV.isInt()); - CHECK(uintV.isUInt()); - CHECK(uintV.isIntegral()); - CHECK(!uintV.isDouble()); - CHECK(uintV.isNumeric()); - CHECK(!uintV.isString()); - CHECK(!uintV.isArray()); - CHECK(!uintV.isArrayOrNull()); - CHECK(!uintV.isObject()); - CHECK(!uintV.isObjectOrNull()); + EXPECT_FALSE(uintV.isNull()); + EXPECT_FALSE(uintV.isBool()); + EXPECT_FALSE(uintV.isInt()); + EXPECT_TRUE(uintV.isUInt()); + EXPECT_TRUE(uintV.isIntegral()); + EXPECT_FALSE(uintV.isDouble()); + EXPECT_TRUE(uintV.isNumeric()); + EXPECT_FALSE(uintV.isString()); + EXPECT_FALSE(uintV.isArray()); + EXPECT_FALSE(uintV.isArrayOrNull()); + EXPECT_FALSE(uintV.isObject()); + EXPECT_FALSE(uintV.isObjectOrNull()); } { Json::Value const realV{testCopy(Json::realValue)}; - CHECK(!realV.isNull()); - CHECK(!realV.isBool()); - CHECK(!realV.isInt()); - CHECK(!realV.isUInt()); - CHECK(!realV.isIntegral()); - CHECK(realV.isDouble()); - CHECK(realV.isNumeric()); - CHECK(!realV.isString()); - CHECK(!realV.isArray()); - CHECK(!realV.isArrayOrNull()); - CHECK(!realV.isObject()); - CHECK(!realV.isObjectOrNull()); + EXPECT_FALSE(realV.isNull()); + EXPECT_FALSE(realV.isBool()); + EXPECT_FALSE(realV.isInt()); + EXPECT_FALSE(realV.isUInt()); + EXPECT_FALSE(realV.isIntegral()); + EXPECT_TRUE(realV.isDouble()); + EXPECT_TRUE(realV.isNumeric()); + EXPECT_FALSE(realV.isString()); + EXPECT_FALSE(realV.isArray()); + EXPECT_FALSE(realV.isArrayOrNull()); + EXPECT_FALSE(realV.isObject()); + EXPECT_FALSE(realV.isObjectOrNull()); } { Json::Value const stringV{testCopy(Json::stringValue)}; - CHECK(!stringV.isNull()); - CHECK(!stringV.isBool()); - CHECK(!stringV.isInt()); - CHECK(!stringV.isUInt()); - CHECK(!stringV.isIntegral()); - CHECK(!stringV.isDouble()); - CHECK(!stringV.isNumeric()); - CHECK(stringV.isString()); - CHECK(!stringV.isArray()); - CHECK(!stringV.isArrayOrNull()); - CHECK(!stringV.isObject()); - CHECK(!stringV.isObjectOrNull()); + EXPECT_FALSE(stringV.isNull()); + EXPECT_FALSE(stringV.isBool()); + EXPECT_FALSE(stringV.isInt()); + EXPECT_FALSE(stringV.isUInt()); + EXPECT_FALSE(stringV.isIntegral()); + EXPECT_FALSE(stringV.isDouble()); + EXPECT_FALSE(stringV.isNumeric()); + EXPECT_TRUE(stringV.isString()); + EXPECT_FALSE(stringV.isArray()); + EXPECT_FALSE(stringV.isArrayOrNull()); + EXPECT_FALSE(stringV.isObject()); + EXPECT_FALSE(stringV.isObjectOrNull()); } { Json::Value const staticStrV{staticStr}; { Json::Value cpy{staticStrV}; - CHECK(staticStrV.type() == Json::stringValue); - CHECK(cpy.type() == Json::stringValue); + EXPECT_EQ(staticStrV.type(), Json::stringValue); + EXPECT_EQ(cpy.type(), Json::stringValue); } - CHECK(!staticStrV.isNull()); - CHECK(!staticStrV.isBool()); - CHECK(!staticStrV.isInt()); - CHECK(!staticStrV.isUInt()); - CHECK(!staticStrV.isIntegral()); - CHECK(!staticStrV.isDouble()); - CHECK(!staticStrV.isNumeric()); - CHECK(staticStrV.isString()); - CHECK(!staticStrV.isArray()); - CHECK(!staticStrV.isArrayOrNull()); - CHECK(!staticStrV.isObject()); - CHECK(!staticStrV.isObjectOrNull()); + EXPECT_FALSE(staticStrV.isNull()); + EXPECT_FALSE(staticStrV.isBool()); + EXPECT_FALSE(staticStrV.isInt()); + EXPECT_FALSE(staticStrV.isUInt()); + EXPECT_FALSE(staticStrV.isIntegral()); + EXPECT_FALSE(staticStrV.isDouble()); + EXPECT_FALSE(staticStrV.isNumeric()); + EXPECT_TRUE(staticStrV.isString()); + EXPECT_FALSE(staticStrV.isArray()); + EXPECT_FALSE(staticStrV.isArrayOrNull()); + EXPECT_FALSE(staticStrV.isObject()); + EXPECT_FALSE(staticStrV.isObjectOrNull()); } { Json::Value const boolV{testCopy(Json::booleanValue)}; - CHECK(!boolV.isNull()); - CHECK(boolV.isBool()); - CHECK(!boolV.isInt()); - CHECK(!boolV.isUInt()); - CHECK(boolV.isIntegral()); - CHECK(!boolV.isDouble()); - CHECK(boolV.isNumeric()); - CHECK(!boolV.isString()); - CHECK(!boolV.isArray()); - CHECK(!boolV.isArrayOrNull()); - CHECK(!boolV.isObject()); - CHECK(!boolV.isObjectOrNull()); + EXPECT_FALSE(boolV.isNull()); + EXPECT_TRUE(boolV.isBool()); + EXPECT_FALSE(boolV.isInt()); + EXPECT_FALSE(boolV.isUInt()); + EXPECT_TRUE(boolV.isIntegral()); + EXPECT_FALSE(boolV.isDouble()); + EXPECT_TRUE(boolV.isNumeric()); + EXPECT_FALSE(boolV.isString()); + EXPECT_FALSE(boolV.isArray()); + EXPECT_FALSE(boolV.isArrayOrNull()); + EXPECT_FALSE(boolV.isObject()); + EXPECT_FALSE(boolV.isObjectOrNull()); } { Json::Value const arrayV{testCopy(Json::arrayValue)}; - CHECK(!arrayV.isNull()); - CHECK(!arrayV.isBool()); - CHECK(!arrayV.isInt()); - CHECK(!arrayV.isUInt()); - CHECK(!arrayV.isIntegral()); - CHECK(!arrayV.isDouble()); - CHECK(!arrayV.isNumeric()); - CHECK(!arrayV.isString()); - CHECK(arrayV.isArray()); - CHECK(arrayV.isArrayOrNull()); - CHECK(!arrayV.isObject()); - CHECK(!arrayV.isObjectOrNull()); + EXPECT_FALSE(arrayV.isNull()); + EXPECT_FALSE(arrayV.isBool()); + EXPECT_FALSE(arrayV.isInt()); + EXPECT_FALSE(arrayV.isUInt()); + EXPECT_FALSE(arrayV.isIntegral()); + EXPECT_FALSE(arrayV.isDouble()); + EXPECT_FALSE(arrayV.isNumeric()); + EXPECT_FALSE(arrayV.isString()); + EXPECT_TRUE(arrayV.isArray()); + EXPECT_TRUE(arrayV.isArrayOrNull()); + EXPECT_FALSE(arrayV.isObject()); + EXPECT_FALSE(arrayV.isObjectOrNull()); } { Json::Value const objectV{testCopy(Json::objectValue)}; - CHECK(!objectV.isNull()); - CHECK(!objectV.isBool()); - CHECK(!objectV.isInt()); - CHECK(!objectV.isUInt()); - CHECK(!objectV.isIntegral()); - CHECK(!objectV.isDouble()); - CHECK(!objectV.isNumeric()); - CHECK(!objectV.isString()); - CHECK(!objectV.isArray()); - CHECK(!objectV.isArrayOrNull()); - CHECK(objectV.isObject()); - CHECK(objectV.isObjectOrNull()); + EXPECT_FALSE(objectV.isNull()); + EXPECT_FALSE(objectV.isBool()); + EXPECT_FALSE(objectV.isInt()); + EXPECT_FALSE(objectV.isUInt()); + EXPECT_FALSE(objectV.isIntegral()); + EXPECT_FALSE(objectV.isDouble()); + EXPECT_FALSE(objectV.isNumeric()); + EXPECT_FALSE(objectV.isString()); + EXPECT_FALSE(objectV.isArray()); + EXPECT_FALSE(objectV.isArrayOrNull()); + EXPECT_TRUE(objectV.isObject()); + EXPECT_TRUE(objectV.isObjectOrNull()); } } -TEST_CASE("compare strings") +TEST(json_value, compare_strings) { auto doCompare = [&](Json::Value const& lhs, Json::Value const& rhs, bool lhsEqRhs, bool lhsLtRhs, int line) { - CAPTURE(line); - CHECK((lhs == rhs) == lhsEqRhs); - CHECK((lhs != rhs) != lhsEqRhs); - CHECK((lhs < rhs) == (!(lhsEqRhs || !lhsLtRhs))); - CHECK((lhs <= rhs) == (lhsEqRhs || lhsLtRhs)); - CHECK((lhs >= rhs) == (lhsEqRhs || !lhsLtRhs)); - CHECK((lhs > rhs) == (!(lhsEqRhs || lhsLtRhs))); + SCOPED_TRACE(line); + EXPECT_EQ((lhs == rhs), lhsEqRhs); + EXPECT_NE((lhs != rhs), lhsEqRhs); + EXPECT_EQ((lhs < rhs), (!(lhsEqRhs || !lhsLtRhs))); + EXPECT_EQ((lhs <= rhs), (lhsEqRhs || lhsLtRhs)); + EXPECT_EQ((lhs >= rhs), (lhsEqRhs || !lhsLtRhs)); + EXPECT_EQ((lhs > rhs), (!(lhsEqRhs || lhsLtRhs))); }; Json::Value const null0; @@ -556,40 +554,40 @@ TEST_CASE("compare strings") #pragma pop_macro("DO_COMPARE") } -TEST_CASE("bool") +TEST(json_value, bool) { - CHECK(!Json::Value()); + EXPECT_FALSE(Json::Value()); - CHECK(!Json::Value("")); + EXPECT_FALSE(Json::Value("")); - CHECK(bool(Json::Value("empty"))); - CHECK(bool(Json::Value(false))); - CHECK(bool(Json::Value(true))); - CHECK(bool(Json::Value(0))); - CHECK(bool(Json::Value(1))); + EXPECT_TRUE(bool(Json::Value("empty"))); + EXPECT_TRUE(bool(Json::Value(false))); + EXPECT_TRUE(bool(Json::Value(true))); + EXPECT_TRUE(bool(Json::Value(0))); + EXPECT_TRUE(bool(Json::Value(1))); Json::Value array(Json::arrayValue); - CHECK(!array); + EXPECT_FALSE(array); array.append(0); - CHECK(bool(array)); + EXPECT_TRUE(bool(array)); Json::Value object(Json::objectValue); - CHECK(!object); + EXPECT_FALSE(object); object[""] = false; - CHECK(bool(object)); + EXPECT_TRUE(bool(object)); } -TEST_CASE("bad json") +TEST(json_value, bad_json) { char const* s(R"({"method":"ledger","params":[{"ledger_index":1e300}]})"); Json::Value j; Json::Reader r; - CHECK(r.parse(s, j)); + EXPECT_TRUE(r.parse(s, j)); } -TEST_CASE("edge cases") +TEST(json_value, edge_cases) { std::uint32_t max_uint = std::numeric_limits::max(); std::int32_t max_int = std::numeric_limits::max(); @@ -611,28 +609,27 @@ TEST_CASE("edge cases") Json::Value j1; Json::Reader r1; - CHECK(r1.parse(json, j1)); - CHECK(j1["max_uint"].asUInt() == max_uint); - CHECK(j1["max_uint"].asAbsUInt() == max_uint); - CHECK(j1["max_int"].asInt() == max_int); - CHECK(j1["max_int"].asAbsUInt() == max_int); - CHECK(j1["min_int"].asInt() == min_int); - CHECK( - j1["min_int"].asAbsUInt() == - static_cast(min_int) * -1); - CHECK(j1["a_uint"].asUInt() == a_uint); - CHECK(j1["a_uint"].asAbsUInt() == a_uint); - CHECK(j1["a_uint"] > a_large_int); - CHECK(j1["a_uint"] > a_small_int); - CHECK(j1["a_large_int"].asInt() == a_large_int); - CHECK(j1["a_large_int"].asAbsUInt() == a_large_int); - CHECK(j1["a_large_int"].asUInt() == a_large_int); - CHECK(j1["a_large_int"] < a_uint); - CHECK(j1["a_small_int"].asInt() == a_small_int); - CHECK( - j1["a_small_int"].asAbsUInt() == + EXPECT_TRUE(r1.parse(json, j1)); + EXPECT_EQ(j1["max_uint"].asUInt(), max_uint); + EXPECT_EQ(j1["max_uint"].asAbsUInt(), max_uint); + EXPECT_EQ(j1["max_int"].asInt(), max_int); + EXPECT_EQ(j1["max_int"].asAbsUInt(), max_int); + EXPECT_EQ(j1["min_int"].asInt(), min_int); + EXPECT_EQ( + j1["min_int"].asAbsUInt(), static_cast(min_int) * -1); + EXPECT_EQ(j1["a_uint"].asUInt(), a_uint); + EXPECT_EQ(j1["a_uint"].asAbsUInt(), a_uint); + EXPECT_GT(j1["a_uint"], a_large_int); + EXPECT_GT(j1["a_uint"], a_small_int); + EXPECT_EQ(j1["a_large_int"].asInt(), a_large_int); + EXPECT_EQ(j1["a_large_int"].asAbsUInt(), a_large_int); + EXPECT_EQ(j1["a_large_int"].asUInt(), a_large_int); + EXPECT_LT(j1["a_large_int"], a_uint); + EXPECT_EQ(j1["a_small_int"].asInt(), a_small_int); + EXPECT_EQ( + j1["a_small_int"].asAbsUInt(), static_cast(a_small_int) * -1); - CHECK(j1["a_small_int"] < a_uint); + EXPECT_LT(j1["a_small_int"], a_uint); } std::uint64_t overflow = std::uint64_t(max_uint) + 1; @@ -644,7 +641,7 @@ TEST_CASE("edge cases") Json::Value j2; Json::Reader r2; - CHECK(!r2.parse(json, j2)); + EXPECT_FALSE(r2.parse(json, j2)); } std::int64_t underflow = std::int64_t(min_int) - 1; @@ -656,167 +653,167 @@ TEST_CASE("edge cases") Json::Value j3; Json::Reader r3; - CHECK(!r3.parse(json, j3)); + EXPECT_FALSE(r3.parse(json, j3)); } { Json::Value intString{std::to_string(overflow)}; - CHECK_THROWS_AS(intString.asUInt(), beast::BadLexicalCast); - CHECK_THROWS_AS(intString.asAbsUInt(), Json::error); + EXPECT_THROW(intString.asUInt(), beast::BadLexicalCast); + EXPECT_THROW(intString.asAbsUInt(), Json::error); intString = "4294967295"; - CHECK(intString.asUInt() == 4294967295u); - CHECK(intString.asAbsUInt() == 4294967295u); + EXPECT_EQ(intString.asUInt(), 4294967295u); + EXPECT_EQ(intString.asAbsUInt(), 4294967295u); intString = "0"; - CHECK(intString.asUInt() == 0); - CHECK(intString.asAbsUInt() == 0); + EXPECT_EQ(intString.asUInt(), 0); + EXPECT_EQ(intString.asAbsUInt(), 0); intString = "-1"; - CHECK_THROWS_AS(intString.asUInt(), beast::BadLexicalCast); - CHECK(intString.asAbsUInt() == 1); + EXPECT_THROW(intString.asUInt(), beast::BadLexicalCast); + EXPECT_EQ(intString.asAbsUInt(), 1); intString = "-4294967295"; - CHECK(intString.asAbsUInt() == 4294967295); + EXPECT_EQ(intString.asAbsUInt(), 4294967295); intString = "-4294967296"; - CHECK_THROWS_AS(intString.asAbsUInt(), Json::error); + EXPECT_THROW(intString.asAbsUInt(), Json::error); intString = "2147483648"; - CHECK_THROWS_AS(intString.asInt(), beast::BadLexicalCast); - CHECK(intString.asAbsUInt() == 2147483648); + EXPECT_THROW(intString.asInt(), beast::BadLexicalCast); + EXPECT_EQ(intString.asAbsUInt(), 2147483648); intString = "2147483647"; - CHECK(intString.asInt() == 2147483647); - CHECK(intString.asAbsUInt() == 2147483647); + EXPECT_EQ(intString.asInt(), 2147483647); + EXPECT_EQ(intString.asAbsUInt(), 2147483647); intString = "-2147483648"; - CHECK(intString.asInt() == -2147483648LL); // MSVC wants the LL - CHECK(intString.asAbsUInt() == 2147483648LL); + EXPECT_EQ(intString.asInt(), -2147483648LL); // MSVC wants the LL + EXPECT_EQ(intString.asAbsUInt(), 2147483648LL); intString = "-2147483649"; - CHECK_THROWS_AS(intString.asInt(), beast::BadLexicalCast); - CHECK(intString.asAbsUInt() == 2147483649); + EXPECT_THROW(intString.asInt(), beast::BadLexicalCast); + EXPECT_EQ(intString.asAbsUInt(), 2147483649); } { Json::Value intReal{4294967297.0}; - CHECK_THROWS_AS(intReal.asUInt(), Json::error); - CHECK_THROWS_AS(intReal.asAbsUInt(), Json::error); + EXPECT_THROW(intReal.asUInt(), Json::error); + EXPECT_THROW(intReal.asAbsUInt(), Json::error); intReal = 4294967295.0; - CHECK(intReal.asUInt() == 4294967295u); - CHECK(intReal.asAbsUInt() == 4294967295u); + EXPECT_EQ(intReal.asUInt(), 4294967295u); + EXPECT_EQ(intReal.asAbsUInt(), 4294967295u); intReal = 0.0; - CHECK(intReal.asUInt() == 0); - CHECK(intReal.asAbsUInt() == 0); + EXPECT_EQ(intReal.asUInt(), 0); + EXPECT_EQ(intReal.asAbsUInt(), 0); intReal = -1.0; - CHECK_THROWS_AS(intReal.asUInt(), Json::error); - CHECK(intReal.asAbsUInt() == 1); + EXPECT_THROW(intReal.asUInt(), Json::error); + EXPECT_EQ(intReal.asAbsUInt(), 1); intReal = -4294967295.0; - CHECK(intReal.asAbsUInt() == 4294967295); + EXPECT_EQ(intReal.asAbsUInt(), 4294967295); intReal = -4294967296.0; - CHECK_THROWS_AS(intReal.asAbsUInt(), Json::error); + EXPECT_THROW(intReal.asAbsUInt(), Json::error); intReal = 2147483648.0; - CHECK_THROWS_AS(intReal.asInt(), Json::error); - CHECK(intReal.asAbsUInt() == 2147483648); + EXPECT_THROW(intReal.asInt(), Json::error); + EXPECT_EQ(intReal.asAbsUInt(), 2147483648); intReal = 2147483647.0; - CHECK(intReal.asInt() == 2147483647); - CHECK(intReal.asAbsUInt() == 2147483647); + EXPECT_EQ(intReal.asInt(), 2147483647); + EXPECT_EQ(intReal.asAbsUInt(), 2147483647); intReal = -2147483648.0; - CHECK(intReal.asInt() == -2147483648LL); // MSVC wants the LL - CHECK(intReal.asAbsUInt() == 2147483648LL); + EXPECT_EQ(intReal.asInt(), -2147483648LL); // MSVC wants the LL + EXPECT_EQ(intReal.asAbsUInt(), 2147483648LL); intReal = -2147483649.0; - CHECK_THROWS_AS(intReal.asInt(), Json::error); - CHECK(intReal.asAbsUInt() == 2147483649); + EXPECT_THROW(intReal.asInt(), Json::error); + EXPECT_EQ(intReal.asAbsUInt(), 2147483649); } } -TEST_CASE("copy") +TEST(json_value, copy) { Json::Value v1{2.5}; - CHECK(v1.isDouble()); - CHECK(v1.asDouble() == 2.5); + EXPECT_TRUE(v1.isDouble()); + EXPECT_EQ(v1.asDouble(), 2.5); Json::Value v2 = v1; - CHECK(v1.isDouble()); - CHECK(v1.asDouble() == 2.5); - CHECK(v2.isDouble()); - CHECK(v2.asDouble() == 2.5); - CHECK(v1 == v2); + EXPECT_TRUE(v1.isDouble()); + EXPECT_EQ(v1.asDouble(), 2.5); + EXPECT_TRUE(v2.isDouble()); + EXPECT_EQ(v2.asDouble(), 2.5); + EXPECT_EQ(v1, v2); v1 = v2; - CHECK(v1.isDouble()); - CHECK(v1.asDouble() == 2.5); - CHECK(v2.isDouble()); - CHECK(v2.asDouble() == 2.5); - CHECK(v1 == v2); + EXPECT_TRUE(v1.isDouble()); + EXPECT_EQ(v1.asDouble(), 2.5); + EXPECT_TRUE(v2.isDouble()); + EXPECT_EQ(v2.asDouble(), 2.5); + EXPECT_EQ(v1, v2); } -TEST_CASE("move") +TEST(json_value, move) { Json::Value v1{2.5}; - CHECK(v1.isDouble()); - CHECK(v1.asDouble() == 2.5); + EXPECT_TRUE(v1.isDouble()); + EXPECT_EQ(v1.asDouble(), 2.5); Json::Value v2 = std::move(v1); - CHECK(!v1); - CHECK(v2.isDouble()); - CHECK(v2.asDouble() == 2.5); - CHECK(v1 != v2); + EXPECT_FALSE(v1); + EXPECT_TRUE(v2.isDouble()); + EXPECT_EQ(v2.asDouble(), 2.5); + EXPECT_NE(v1, v2); v1 = std::move(v2); - CHECK(v1.isDouble()); - CHECK(v1.asDouble() == 2.5); - CHECK(!v2); - CHECK(v1 != v2); + EXPECT_TRUE(v1.isDouble()); + EXPECT_EQ(v1.asDouble(), 2.5); + EXPECT_FALSE(v2); + EXPECT_NE(v1, v2); } -TEST_CASE("comparisons") +TEST(json_value, comparisons) { Json::Value a, b; auto testEquals = [&](std::string const& name) { - CHECK(a == b); - CHECK(a <= b); - CHECK(a >= b); + EXPECT_TRUE(a == b); + EXPECT_TRUE(a <= b); + EXPECT_TRUE(a >= b); - CHECK(!(a != b)); - CHECK(!(a < b)); - CHECK(!(a > b)); + EXPECT_FALSE(a != b); + EXPECT_FALSE(a < b); + EXPECT_FALSE(a > b); - CHECK(b == a); - CHECK(b <= a); - CHECK(b >= a); + EXPECT_TRUE(b == a); + EXPECT_TRUE(b <= a); + EXPECT_TRUE(b >= a); - CHECK(!(b != a)); - CHECK(!(b < a)); - CHECK(!(b > a)); + EXPECT_FALSE(b != a); + EXPECT_FALSE(b < a); + EXPECT_FALSE(b > a); }; auto testGreaterThan = [&](std::string const& name) { - CHECK(!(a == b)); - CHECK(!(a <= b)); - CHECK(a >= b); + EXPECT_FALSE(a == b); + EXPECT_FALSE(a <= b); + EXPECT_TRUE(a >= b); - CHECK(a != b); - CHECK(!(a < b)); - CHECK(a > b); + EXPECT_TRUE(a != b); + EXPECT_FALSE(a < b); + EXPECT_TRUE(a > b); - CHECK(!(b == a)); - CHECK(b <= a); - CHECK(!(b >= a)); + EXPECT_FALSE(b == a); + EXPECT_TRUE(b <= a); + EXPECT_FALSE(b >= a); - CHECK(b != a); - CHECK(b < a); - CHECK(!(b > a)); + EXPECT_TRUE(b != a); + EXPECT_TRUE(b < a); + EXPECT_FALSE(b > a); }; a["a"] = Json::UInt(0); @@ -835,7 +832,7 @@ TEST_CASE("comparisons") testGreaterThan("big"); } -TEST_CASE("compact") +TEST(json_value, compact) { Json::Value j; Json::Reader r; @@ -847,20 +844,20 @@ TEST_CASE("compact") }); }; - CHECK(r.parse(s, j)); + EXPECT_TRUE(r.parse(s, j)); { std::stringstream ss; ss << j; - CHECK(countLines(ss.str()) > 1); + EXPECT_GT(countLines(ss.str()), 1); } { std::stringstream ss; ss << Json::Compact(std::move(j)); - CHECK(countLines(ss.str()) == 1); + EXPECT_EQ(countLines(ss.str()), 1); } } -TEST_CASE("conversions") +TEST(json_value, conversions) { // We have Json::Int, but not Json::Double or Json::Real. // We have Json::Int, Json::Value::Int, and Json::ValueType::intValue. @@ -869,336 +866,336 @@ TEST_CASE("conversions") { // null Json::Value val; - CHECK(val.isNull()); + EXPECT_TRUE(val.isNull()); // val.asCString() should trigger an assertion failure - CHECK(val.asString() == ""); - CHECK(val.asInt() == 0); - CHECK(val.asUInt() == 0); - CHECK(val.asAbsUInt() == 0); - CHECK(val.asDouble() == 0.0); - CHECK(val.asBool() == false); + EXPECT_EQ(val.asString(), ""); + EXPECT_EQ(val.asInt(), 0); + EXPECT_EQ(val.asUInt(), 0); + EXPECT_EQ(val.asAbsUInt(), 0); + EXPECT_EQ(val.asDouble(), 0.0); + EXPECT_FALSE(val.asBool()); - CHECK(val.isConvertibleTo(Json::nullValue)); - CHECK(val.isConvertibleTo(Json::intValue)); - CHECK(val.isConvertibleTo(Json::uintValue)); - CHECK(val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(val.isConvertibleTo(Json::booleanValue)); - CHECK(val.isConvertibleTo(Json::arrayValue)); - CHECK(val.isConvertibleTo(Json::objectValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::nullValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::intValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::uintValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::objectValue)); } { // int Json::Value val = -1234; - CHECK(val.isInt()); + EXPECT_TRUE(val.isInt()); // val.asCString() should trigger an assertion failure - CHECK(val.asString() == "-1234"); - CHECK(val.asInt() == -1234); - CHECK_THROWS_AS(val.asUInt(), Json::error); - CHECK(val.asAbsUInt() == 1234u); - CHECK(val.asDouble() == -1234.0); - CHECK(val.asBool() == true); + EXPECT_EQ(val.asString(), "-1234"); + EXPECT_EQ(val.asInt(), -1234); + EXPECT_THROW(val.asUInt(), Json::error); + EXPECT_EQ(val.asAbsUInt(), 1234u); + EXPECT_EQ(val.asDouble(), -1234.0); + EXPECT_TRUE(val.asBool()); - CHECK(!val.isConvertibleTo(Json::nullValue)); - CHECK(val.isConvertibleTo(Json::intValue)); - CHECK(!val.isConvertibleTo(Json::uintValue)); - CHECK(val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::nullValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::intValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::uintValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // uint Json::Value val = 1234U; - CHECK(val.isUInt()); + EXPECT_TRUE(val.isUInt()); // val.asCString() should trigger an assertion failure - CHECK(val.asString() == "1234"); - CHECK(val.asInt() == 1234); - CHECK(val.asUInt() == 1234u); - CHECK(val.asAbsUInt() == 1234u); - CHECK(val.asDouble() == 1234.0); - CHECK(val.asBool() == true); + EXPECT_EQ(val.asString(), "1234"); + EXPECT_EQ(val.asInt(), 1234); + EXPECT_EQ(val.asUInt(), 1234u); + EXPECT_EQ(val.asAbsUInt(), 1234u); + EXPECT_EQ(val.asDouble(), 1234.0); + EXPECT_TRUE(val.asBool()); - CHECK(!val.isConvertibleTo(Json::nullValue)); - CHECK(val.isConvertibleTo(Json::intValue)); - CHECK(val.isConvertibleTo(Json::uintValue)); - CHECK(val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::nullValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::intValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::uintValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // real Json::Value val = 2.0; - CHECK(val.isDouble()); + EXPECT_TRUE(val.isDouble()); // val.asCString() should trigger an assertion failure - CHECK(std::regex_match(val.asString(), std::regex("^2\\.0*$"))); - CHECK(val.asInt() == 2); - CHECK(val.asUInt() == 2u); - CHECK(val.asAbsUInt() == 2u); - CHECK(val.asDouble() == 2.0); - CHECK(val.asBool() == true); + EXPECT_TRUE(std::regex_match(val.asString(), std::regex("^2\\.0*$"))); + EXPECT_EQ(val.asInt(), 2); + EXPECT_EQ(val.asUInt(), 2u); + EXPECT_EQ(val.asAbsUInt(), 2u); + EXPECT_EQ(val.asDouble(), 2.0); + EXPECT_TRUE(val.asBool()); - CHECK(!val.isConvertibleTo(Json::nullValue)); - CHECK(val.isConvertibleTo(Json::intValue)); - CHECK(val.isConvertibleTo(Json::uintValue)); - CHECK(val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::nullValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::intValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::uintValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // numeric string Json::Value val = "54321"; - CHECK(val.isString()); - CHECK(strcmp(val.asCString(), "54321") == 0); - CHECK(val.asString() == "54321"); - CHECK(val.asInt() == 54321); - CHECK(val.asUInt() == 54321u); - CHECK(val.asAbsUInt() == 54321); - CHECK_THROWS_AS(val.asDouble(), Json::error); - CHECK(val.asBool() == true); + EXPECT_TRUE(val.isString()); + EXPECT_EQ(strcmp(val.asCString(), "54321"), 0); + EXPECT_EQ(val.asString(), "54321"); + EXPECT_EQ(val.asInt(), 54321); + EXPECT_EQ(val.asUInt(), 54321u); + EXPECT_EQ(val.asAbsUInt(), 54321); + EXPECT_THROW(val.asDouble(), Json::error); + EXPECT_TRUE(val.asBool()); - CHECK(!val.isConvertibleTo(Json::nullValue)); - CHECK(!val.isConvertibleTo(Json::intValue)); - CHECK(!val.isConvertibleTo(Json::uintValue)); - CHECK(!val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(!val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::nullValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::intValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::uintValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // non-numeric string Json::Value val(Json::stringValue); - CHECK(val.isString()); - CHECK(val.asCString() == nullptr); - CHECK(val.asString() == ""); - CHECK_THROWS_AS(val.asInt(), std::exception); - CHECK_THROWS_AS(val.asUInt(), std::exception); - CHECK_THROWS_AS(val.asAbsUInt(), std::exception); - CHECK_THROWS_AS(val.asDouble(), std::exception); - CHECK(val.asBool() == false); + EXPECT_TRUE(val.isString()); + EXPECT_EQ(val.asCString(), nullptr); + EXPECT_EQ(val.asString(), ""); + EXPECT_THROW(val.asInt(), std::exception); + EXPECT_THROW(val.asUInt(), std::exception); + EXPECT_THROW(val.asAbsUInt(), std::exception); + EXPECT_THROW(val.asDouble(), std::exception); + EXPECT_TRUE(val.asBool() == false); - CHECK(val.isConvertibleTo(Json::nullValue)); - CHECK(!val.isConvertibleTo(Json::intValue)); - CHECK(!val.isConvertibleTo(Json::uintValue)); - CHECK(!val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(!val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::nullValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::intValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::uintValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // bool false Json::Value val = false; - CHECK(val.isBool()); + EXPECT_TRUE(val.isBool()); // val.asCString() should trigger an assertion failure - CHECK(val.asString() == "false"); - CHECK(val.asInt() == 0); - CHECK(val.asUInt() == 0); - CHECK(val.asAbsUInt() == 0); - CHECK(val.asDouble() == 0.0); - CHECK(val.asBool() == false); + EXPECT_EQ(val.asString(), "false"); + EXPECT_EQ(val.asInt(), 0); + EXPECT_EQ(val.asUInt(), 0); + EXPECT_EQ(val.asAbsUInt(), 0); + EXPECT_EQ(val.asDouble(), 0.0); + EXPECT_FALSE(val.asBool()); - CHECK(val.isConvertibleTo(Json::nullValue)); - CHECK(val.isConvertibleTo(Json::intValue)); - CHECK(val.isConvertibleTo(Json::uintValue)); - CHECK(val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::nullValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::intValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::uintValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // bool true Json::Value val = true; - CHECK(val.isBool()); + EXPECT_TRUE(val.isBool()); // val.asCString() should trigger an assertion failure - CHECK(val.asString() == "true"); - CHECK(val.asInt() == 1); - CHECK(val.asUInt() == 1); - CHECK(val.asAbsUInt() == 1); - CHECK(val.asDouble() == 1.0); - CHECK(val.asBool() == true); + EXPECT_EQ(val.asString(), "true"); + EXPECT_EQ(val.asInt(), 1); + EXPECT_EQ(val.asUInt(), 1); + EXPECT_EQ(val.asAbsUInt(), 1); + EXPECT_EQ(val.asDouble(), 1.0); + EXPECT_TRUE(val.asBool()); - CHECK(!val.isConvertibleTo(Json::nullValue)); - CHECK(val.isConvertibleTo(Json::intValue)); - CHECK(val.isConvertibleTo(Json::uintValue)); - CHECK(val.isConvertibleTo(Json::realValue)); - CHECK(val.isConvertibleTo(Json::stringValue)); - CHECK(val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::nullValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::intValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::uintValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::realValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::stringValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // array type Json::Value val(Json::arrayValue); - CHECK(val.isArray()); + EXPECT_TRUE(val.isArray()); // val.asCString should trigger an assertion failure - CHECK_THROWS_AS(val.asString(), Json::error); - CHECK_THROWS_AS(val.asInt(), Json::error); - CHECK_THROWS_AS(val.asUInt(), Json::error); - CHECK_THROWS_AS(val.asAbsUInt(), Json::error); - CHECK_THROWS_AS(val.asDouble(), Json::error); - CHECK(val.asBool() == false); // empty or not + EXPECT_THROW(val.asString(), Json::error); + EXPECT_THROW(val.asInt(), Json::error); + EXPECT_THROW(val.asUInt(), Json::error); + EXPECT_THROW(val.asAbsUInt(), Json::error); + EXPECT_THROW(val.asDouble(), Json::error); + EXPECT_FALSE(val.asBool()); // empty or not - CHECK(val.isConvertibleTo(Json::nullValue)); - CHECK(!val.isConvertibleTo(Json::intValue)); - CHECK(!val.isConvertibleTo(Json::uintValue)); - CHECK(!val.isConvertibleTo(Json::realValue)); - CHECK(!val.isConvertibleTo(Json::stringValue)); - CHECK(!val.isConvertibleTo(Json::booleanValue)); - CHECK(val.isConvertibleTo(Json::arrayValue)); - CHECK(!val.isConvertibleTo(Json::objectValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::nullValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::intValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::uintValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::realValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::stringValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::objectValue)); } { // object type Json::Value val(Json::objectValue); - CHECK(val.isObject()); + EXPECT_TRUE(val.isObject()); // val.asCString should trigger an assertion failure - CHECK_THROWS_AS(val.asString(), Json::error); - CHECK_THROWS_AS(val.asInt(), Json::error); - CHECK_THROWS_AS(val.asUInt(), Json::error); - CHECK_THROWS_AS(val.asAbsUInt(), Json::error); - CHECK_THROWS_AS(val.asDouble(), Json::error); - CHECK(val.asBool() == false); // empty or not + EXPECT_THROW(val.asString(), Json::error); + EXPECT_THROW(val.asInt(), Json::error); + EXPECT_THROW(val.asUInt(), Json::error); + EXPECT_THROW(val.asAbsUInt(), Json::error); + EXPECT_THROW(val.asDouble(), Json::error); + EXPECT_FALSE(val.asBool()); // empty or not - CHECK(val.isConvertibleTo(Json::nullValue)); - CHECK(!val.isConvertibleTo(Json::intValue)); - CHECK(!val.isConvertibleTo(Json::uintValue)); - CHECK(!val.isConvertibleTo(Json::realValue)); - CHECK(!val.isConvertibleTo(Json::stringValue)); - CHECK(!val.isConvertibleTo(Json::booleanValue)); - CHECK(!val.isConvertibleTo(Json::arrayValue)); - CHECK(val.isConvertibleTo(Json::objectValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::nullValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::intValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::uintValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::realValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::stringValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::booleanValue)); + EXPECT_FALSE(val.isConvertibleTo(Json::arrayValue)); + EXPECT_TRUE(val.isConvertibleTo(Json::objectValue)); } } -TEST_CASE("access members") +TEST(json_value, access_members) { Json::Value val; - CHECK(val.type() == Json::nullValue); - CHECK(val.size() == 0); - CHECK(!val.isValidIndex(0)); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.type(), Json::nullValue); + EXPECT_EQ(val.size(), 0); + EXPECT_FALSE(val.isValidIndex(0)); + EXPECT_FALSE(val.isMember("key")); { Json::Value const constVal = val; - CHECK(constVal[7u].type() == Json::nullValue); - CHECK(!constVal.isMember("key")); - CHECK(constVal["key"].type() == Json::nullValue); - CHECK(constVal.getMemberNames().empty()); - CHECK(constVal.get(1u, "default0") == "default0"); - CHECK(constVal.get(std::string("not"), "oh") == "oh"); - CHECK(constVal.get("missing", "default2") == "default2"); + EXPECT_EQ(constVal[7u].type(), Json::nullValue); + EXPECT_FALSE(constVal.isMember("key")); + EXPECT_EQ(constVal["key"].type(), Json::nullValue); + EXPECT_TRUE(constVal.getMemberNames().empty()); + EXPECT_EQ(constVal.get(1u, "default0"), "default0"); + EXPECT_EQ(constVal.get(std::string("not"), "oh"), "oh"); + EXPECT_EQ(constVal.get("missing", "default2"), "default2"); } val = -7; - CHECK(val.type() == Json::intValue); - CHECK(val.size() == 0); - CHECK(!val.isValidIndex(0)); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.type(), Json::intValue); + EXPECT_EQ(val.size(), 0); + EXPECT_FALSE(val.isValidIndex(0)); + EXPECT_FALSE(val.isMember("key")); val = 42u; - CHECK(val.type() == Json::uintValue); - CHECK(val.size() == 0); - CHECK(!val.isValidIndex(0)); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.type(), Json::uintValue); + EXPECT_EQ(val.size(), 0); + EXPECT_FALSE(val.isValidIndex(0)); + EXPECT_FALSE(val.isMember("key")); val = 3.14159; - CHECK(val.type() == Json::realValue); - CHECK(val.size() == 0); - CHECK(!val.isValidIndex(0)); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.type(), Json::realValue); + EXPECT_EQ(val.size(), 0); + EXPECT_FALSE(val.isValidIndex(0)); + EXPECT_FALSE(val.isMember("key")); val = true; - CHECK(val.type() == Json::booleanValue); - CHECK(val.size() == 0); - CHECK(!val.isValidIndex(0)); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.type(), Json::booleanValue); + EXPECT_EQ(val.size(), 0); + EXPECT_FALSE(val.isValidIndex(0)); + EXPECT_FALSE(val.isMember("key")); val = "string"; - CHECK(val.type() == Json::stringValue); - CHECK(val.size() == 0); - CHECK(!val.isValidIndex(0)); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.type(), Json::stringValue); + EXPECT_EQ(val.size(), 0); + EXPECT_FALSE(val.isValidIndex(0)); + EXPECT_FALSE(val.isMember("key")); val = Json::Value(Json::objectValue); - CHECK(val.type() == Json::objectValue); - CHECK(val.size() == 0); + EXPECT_EQ(val.type(), Json::objectValue); + EXPECT_EQ(val.size(), 0); static Json::StaticString const staticThree("three"); val[staticThree] = 3; val["two"] = 2; - CHECK(val.size() == 2); - CHECK(val.isValidIndex(1)); - CHECK(!val.isValidIndex(2)); - CHECK(val[staticThree] == 3); - CHECK(val.isMember("two")); - CHECK(val.isMember(staticThree)); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.size(), 2); + EXPECT_TRUE(val.isValidIndex(1)); + EXPECT_FALSE(val.isValidIndex(2)); + EXPECT_EQ(val[staticThree], 3); + EXPECT_TRUE(val.isMember("two")); + EXPECT_TRUE(val.isMember(staticThree)); + EXPECT_FALSE(val.isMember("key")); { Json::Value const constVal = val; - CHECK(constVal["two"] == 2); - CHECK(constVal["four"].type() == Json::nullValue); - CHECK(constVal[staticThree] == 3); - CHECK(constVal.isMember("two")); - CHECK(constVal.isMember(staticThree)); - CHECK(!constVal.isMember("key")); - CHECK(val.get(std::string("two"), "backup") == 2); - CHECK(val.get("missing", "default2") == "default2"); + EXPECT_EQ(constVal["two"], 2); + EXPECT_EQ(constVal["four"].type(), Json::nullValue); + EXPECT_EQ(constVal[staticThree], 3); + EXPECT_TRUE(constVal.isMember("two")); + EXPECT_TRUE(constVal.isMember(staticThree)); + EXPECT_FALSE(constVal.isMember("key")); + EXPECT_EQ(val.get(std::string("two"), "backup"), 2); + EXPECT_EQ(val.get("missing", "default2"), "default2"); } val = Json::Value(Json::arrayValue); - CHECK(val.type() == Json::arrayValue); - CHECK(val.size() == 0); + EXPECT_EQ(val.type(), Json::arrayValue); + EXPECT_EQ(val.size(), 0); val[0u] = "zero"; val[1u] = "one"; - CHECK(val.size() == 2); - CHECK(val.isValidIndex(1)); - CHECK(!val.isValidIndex(2)); - CHECK(val[20u].type() == Json::nullValue); - CHECK(!val.isMember("key")); + EXPECT_EQ(val.size(), 2); + EXPECT_TRUE(val.isValidIndex(1)); + EXPECT_FALSE(val.isValidIndex(2)); + EXPECT_EQ(val[20u].type(), Json::nullValue); + EXPECT_FALSE(val.isMember("key")); { Json::Value const constVal = val; - CHECK(constVal[0u] == "zero"); - CHECK(constVal[2u].type() == Json::nullValue); - CHECK(!constVal.isMember("key")); - CHECK(val.get(1u, "default0") == "one"); - CHECK(val.get(3u, "default1") == "default1"); + EXPECT_EQ(constVal[0u], "zero"); + EXPECT_EQ(constVal[2u].type(), Json::nullValue); + EXPECT_FALSE(constVal.isMember("key")); + EXPECT_EQ(val.get(1u, "default0"), "one"); + EXPECT_EQ(val.get(3u, "default1"), "default1"); } } -TEST_CASE("remove members") +TEST(json_value, remove_members) { Json::Value val; - CHECK(val.removeMember(std::string("member")).type() == Json::nullValue); + EXPECT_EQ(val.removeMember(std::string("member")).type(), Json::nullValue); val = Json::Value(Json::objectValue); static Json::StaticString const staticThree("three"); val[staticThree] = 3; val["two"] = 2; - CHECK(val.size() == 2); + EXPECT_EQ(val.size(), 2); - CHECK(val.removeMember(std::string("six")).type() == Json::nullValue); - CHECK(val.size() == 2); + EXPECT_EQ(val.removeMember(std::string("six")).type(), Json::nullValue); + EXPECT_EQ(val.size(), 2); - CHECK(val.removeMember(staticThree) == 3); - CHECK(val.size() == 1); + EXPECT_EQ(val.removeMember(staticThree), 3); + EXPECT_EQ(val.size(), 1); - CHECK(val.removeMember(staticThree).type() == Json::nullValue); - CHECK(val.size() == 1); + EXPECT_EQ(val.removeMember(staticThree).type(), Json::nullValue); + EXPECT_EQ(val.size(), 1); - CHECK(val.removeMember(std::string("two")) == 2); - CHECK(val.size() == 0); + EXPECT_EQ(val.removeMember(std::string("two")), 2); + EXPECT_EQ(val.size(), 0); - CHECK(val.removeMember(std::string("two")).type() == Json::nullValue); - CHECK(val.size() == 0); + EXPECT_EQ(val.removeMember(std::string("two")).type(), Json::nullValue); + EXPECT_EQ(val.size(), 0); } -TEST_CASE("iterator") +TEST(json_value, iterator) { { // Iterating an array. @@ -1215,27 +1212,27 @@ TEST_CASE("iterator") Json::ValueIterator i2 = e; --i2; - // key(), index(), and memberName() on an object iterator. - CHECK(b != e); - CHECK(!(b == e)); - CHECK(i1.key() == 0); - CHECK(i2.key() == 3); - CHECK(i1.index() == 0); - CHECK(i2.index() == 3); - CHECK(std::strcmp(i1.memberName(), "") == 0); - CHECK(std::strcmp(i2.memberName(), "") == 0); + // key(), index(), and memberName() on an array iterator. + EXPECT_TRUE(b != e); + EXPECT_FALSE(b == e); + EXPECT_EQ(i1.key(), 0); + EXPECT_EQ(i2.key(), 3); + EXPECT_EQ(i1.index(), 0); + EXPECT_EQ(i2.index(), 3); + EXPECT_STREQ(i1.memberName(), ""); + EXPECT_STREQ(i2.memberName(), ""); // Pre and post increment and decrement. *i1++ = "0"; - CHECK(*i1 == "one"); + EXPECT_EQ(*i1, "one"); *i1 = "1"; ++i1; *i2-- = "3"; - CHECK(*i2 == "two"); - CHECK(i1 == i2); + EXPECT_EQ(*i2, "two"); + EXPECT_EQ(i1, i2); *i2 = "2"; - CHECK(*i1 == "2"); + EXPECT_EQ(*i1, "2"); } { // Iterating a const object. @@ -1253,38 +1250,38 @@ TEST_CASE("iterator") --i2; // key(), index(), and memberName() on an object iterator. - CHECK(i1 != i2); - CHECK(!(i1 == i2)); - CHECK(i1.key() == "0"); - CHECK(i2.key() == "3"); - CHECK(i1.index() == -1); - CHECK(i2.index() == -1); - CHECK(std::strcmp(i1.memberName(), "0") == 0); - CHECK(std::strcmp(i2.memberName(), "3") == 0); + EXPECT_TRUE(i1 != i2); + EXPECT_FALSE(i1 == i2); + EXPECT_EQ(i1.key(), "0"); + EXPECT_EQ(i2.key(), "3"); + EXPECT_EQ(i1.index(), -1); + EXPECT_EQ(i2.index(), -1); + EXPECT_STREQ(i1.memberName(), "0"); + EXPECT_STREQ(i2.memberName(), "3"); // Pre and post increment and decrement. - CHECK(*i1++ == 0); - CHECK(*i1 == 1); + EXPECT_EQ(*i1++, 0); + EXPECT_EQ(*i1, 1); ++i1; - CHECK(*i2-- == 3); - CHECK(*i2 == 2); - CHECK(i1 == i2); - CHECK(*i1 == 2); + EXPECT_EQ(*i2--, 3); + EXPECT_EQ(*i2, 2); + EXPECT_EQ(i1, i2); + EXPECT_EQ(*i1, 2); } { // Iterating a non-const null object. Json::Value nul{}; - CHECK(nul.begin() == nul.end()); + EXPECT_EQ(nul.begin(), nul.end()); } { // Iterating a const Int. Json::Value const i{-3}; - CHECK(i.begin() == i.end()); + EXPECT_EQ(i.begin(), i.end()); } } -TEST_CASE("nest limits") +TEST(json_value, nest_limits) { Json::Reader r; { @@ -1302,14 +1299,14 @@ TEST_CASE("nest limits") // Within object nest limit auto json{nest(std::min(10u, Json::Reader::nest_limit))}; Json::Value j; - CHECK(r.parse(json, j)); + EXPECT_TRUE(r.parse(json, j)); } { // Exceed object nest limit auto json{nest(Json::Reader::nest_limit + 1)}; Json::Value j; - CHECK(!r.parse(json, j)); + EXPECT_FALSE(r.parse(json, j)); } } @@ -1326,41 +1323,39 @@ TEST_CASE("nest limits") // Exceed array nest limit auto json{nest(Json::Reader::nest_limit + 1)}; Json::Value j; - CHECK(!r.parse(json, j)); + EXPECT_FALSE(r.parse(json, j)); } } -TEST_CASE("memory leak") +TEST(json_value, memory_leak) { // When run with the address sanitizer, this test confirms there is no // memory leak with the scenarios below. { Json::Value a; a[0u] = 1; - CHECK(a.type() == Json::arrayValue); - CHECK(a[0u].type() == Json::intValue); + EXPECT_EQ(a.type(), Json::arrayValue); + EXPECT_EQ(a[0u].type(), Json::intValue); a = std::move(a[0u]); - CHECK(a.type() == Json::intValue); + EXPECT_EQ(a.type(), Json::intValue); } { Json::Value b; Json::Value temp; temp["a"] = "Probably avoids the small string optimization"; temp["b"] = "Also probably avoids the small string optimization"; - CHECK(temp.type() == Json::objectValue); + EXPECT_EQ(temp.type(), Json::objectValue); b.append(temp); - CHECK(temp.type() == Json::objectValue); - CHECK(b.size() == 1); + EXPECT_EQ(temp.type(), Json::objectValue); + EXPECT_EQ(b.size(), 1); b.append(std::move(temp)); - CHECK(b.size() == 2); + EXPECT_EQ(b.size(), 2); // Note that the type() == nullValue check is implementation // specific and not guaranteed to be valid in the future. - CHECK(temp.type() == Json::nullValue); + EXPECT_EQ(temp.type(), Json::nullValue); } } -TEST_SUITE_END(); - } // namespace xrpl diff --git a/src/tests/libxrpl/json/Writer.cpp b/src/tests/libxrpl/json/Writer.cpp index 9637184c95..7016b4322d 100644 --- a/src/tests/libxrpl/json/Writer.cpp +++ b/src/tests/libxrpl/json/Writer.cpp @@ -1,7 +1,7 @@ #include -#include #include +#include #include #include @@ -9,14 +9,14 @@ using namespace xrpl; using namespace Json; -TEST_SUITE_BEGIN("JsonWriter"); - -struct WriterFixture +class WriterFixture : public ::testing::Test { +protected: std::string output; std::unique_ptr writer; - WriterFixture() + void + SetUp() override { writer = std::make_unique(stringOutput(output)); } @@ -31,7 +31,7 @@ struct WriterFixture void expectOutput(std::string const& expected) const { - CHECK(output == expected); + EXPECT_EQ(output, expected); } void @@ -42,20 +42,20 @@ struct WriterFixture } }; -TEST_CASE_FIXTURE(WriterFixture, "trivial") +TEST_F(WriterFixture, trivial) { - CHECK(output.empty()); + EXPECT_TRUE(output.empty()); checkOutputAndReset(""); } -TEST_CASE_FIXTURE(WriterFixture, "near trivial") +TEST_F(WriterFixture, near_trivial) { - CHECK(output.empty()); + EXPECT_TRUE(output.empty()); writer->output(0); checkOutputAndReset("0"); } -TEST_CASE_FIXTURE(WriterFixture, "primitives") +TEST_F(WriterFixture, primitives) { writer->output(true); checkOutputAndReset("true"); @@ -79,7 +79,7 @@ TEST_CASE_FIXTURE(WriterFixture, "primitives") checkOutputAndReset("null"); } -TEST_CASE_FIXTURE(WriterFixture, "empty") +TEST_F(WriterFixture, empty) { writer->startRoot(Writer::array); writer->finish(); @@ -90,7 +90,7 @@ TEST_CASE_FIXTURE(WriterFixture, "empty") checkOutputAndReset("{}"); } -TEST_CASE_FIXTURE(WriterFixture, "escaping") +TEST_F(WriterFixture, escaping) { writer->output("\\"); checkOutputAndReset(R"("\\")"); @@ -108,7 +108,7 @@ TEST_CASE_FIXTURE(WriterFixture, "escaping") checkOutputAndReset(R"("\b\f\n\r\t")"); } -TEST_CASE_FIXTURE(WriterFixture, "array") +TEST_F(WriterFixture, array) { writer->startRoot(Writer::array); writer->append(12); @@ -116,7 +116,7 @@ TEST_CASE_FIXTURE(WriterFixture, "array") checkOutputAndReset("[12]"); } -TEST_CASE_FIXTURE(WriterFixture, "long array") +TEST_F(WriterFixture, long_array) { writer->startRoot(Writer::array); writer->append(12); @@ -126,7 +126,7 @@ TEST_CASE_FIXTURE(WriterFixture, "long array") checkOutputAndReset(R"([12,true,"hello"])"); } -TEST_CASE_FIXTURE(WriterFixture, "embedded array simple") +TEST_F(WriterFixture, embedded_array_simple) { writer->startRoot(Writer::array); writer->startAppend(Writer::array); @@ -135,7 +135,7 @@ TEST_CASE_FIXTURE(WriterFixture, "embedded array simple") checkOutputAndReset("[[]]"); } -TEST_CASE_FIXTURE(WriterFixture, "object") +TEST_F(WriterFixture, object) { writer->startRoot(Writer::object); writer->set("hello", "world"); @@ -143,7 +143,7 @@ TEST_CASE_FIXTURE(WriterFixture, "object") checkOutputAndReset(R"({"hello":"world"})"); } -TEST_CASE_FIXTURE(WriterFixture, "complex object") +TEST_F(WriterFixture, complex_object) { writer->startRoot(Writer::object); writer->set("hello", "world"); @@ -160,7 +160,7 @@ TEST_CASE_FIXTURE(WriterFixture, "complex object") R"({"hello":"world","array":[true,12,[{"goodbye":"cruel world.","subarray":[23.5]}]]})"); } -TEST_CASE_FIXTURE(WriterFixture, "json value") +TEST_F(WriterFixture, json_value) { Json::Value value(Json::objectValue); value["foo"] = 23; @@ -169,5 +169,3 @@ TEST_CASE_FIXTURE(WriterFixture, "json value") writer->finish(); checkOutputAndReset(R"({"hello":{"foo":23}})"); } - -TEST_SUITE_END(); diff --git a/src/tests/libxrpl/json/main.cpp b/src/tests/libxrpl/json/main.cpp index 0a3f254ea8..5142bbe08a 100644 --- a/src/tests/libxrpl/json/main.cpp +++ b/src/tests/libxrpl/json/main.cpp @@ -1,2 +1,8 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include +#include + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp index 5a484c1f56..cfd206edde 100644 --- a/src/tests/libxrpl/net/HTTPClient.cpp +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include @@ -217,7 +217,7 @@ runHTTPTest( } // anonymous namespace -TEST_CASE("HTTPClient case insensitive Content-Length") +TEST(HTTPClient, case_insensitive_content_length) { // Test different cases of Content-Length header std::vector header_cases = { @@ -249,14 +249,14 @@ TEST_CASE("HTTPClient case insensitive Content-Length") result_error); // Verify results - CHECK(test_completed); - CHECK(!result_error); - CHECK(result_status == 200); - CHECK(result_data == test_body); + EXPECT_TRUE(test_completed); + EXPECT_FALSE(result_error); + EXPECT_EQ(result_status, 200); + EXPECT_EQ(result_data, test_body); } } -TEST_CASE("HTTPClient basic HTTP request") +TEST(HTTPClient, basic_http_request) { TestHTTPServer server; std::string test_body = "Test response body"; @@ -271,13 +271,13 @@ TEST_CASE("HTTPClient basic HTTP request") bool test_completed = runHTTPTest( server, "/basic", completed, result_status, result_data, result_error); - CHECK(test_completed); - CHECK(!result_error); - CHECK(result_status == 200); - CHECK(result_data == test_body); + EXPECT_TRUE(test_completed); + EXPECT_FALSE(result_error); + EXPECT_EQ(result_status, 200); + EXPECT_EQ(result_data, test_body); } -TEST_CASE("HTTPClient empty response") +TEST(HTTPClient, empty_response) { TestHTTPServer server; server.setResponseBody(""); // Empty body @@ -291,13 +291,13 @@ TEST_CASE("HTTPClient empty response") bool test_completed = runHTTPTest( server, "/empty", completed, result_status, result_data, result_error); - CHECK(test_completed); - CHECK(!result_error); - CHECK(result_status == 200); - CHECK(result_data.empty()); + EXPECT_TRUE(test_completed); + EXPECT_FALSE(result_error); + EXPECT_EQ(result_status, 200); + EXPECT_TRUE(result_data.empty()); } -TEST_CASE("HTTPClient different status codes") +TEST(HTTPClient, different_status_codes) { std::vector status_codes = {200, 404, 500}; @@ -320,8 +320,8 @@ TEST_CASE("HTTPClient different status codes") result_data, result_error); - CHECK(test_completed); - CHECK(!result_error); - CHECK(result_status == static_cast(status)); + EXPECT_TRUE(test_completed); + EXPECT_FALSE(result_error); + EXPECT_EQ(result_status, static_cast(status)); } } diff --git a/src/tests/libxrpl/net/main.cpp b/src/tests/libxrpl/net/main.cpp index 0a3f254ea8..5142bbe08a 100644 --- a/src/tests/libxrpl/net/main.cpp +++ b/src/tests/libxrpl/net/main.cpp @@ -1,2 +1,8 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include +#include + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 071466f05c..79fc617569 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -84,50 +84,10 @@ struct LoanPaymentParts operator==(LoanPaymentParts const& other) const; }; -/* Describes the initial computed properties of a loan. - * - * This structure contains the fundamental calculated values that define a - * loan's payment structure and amortization schedule. These properties are - * computed: - * - At loan creation (LoanSet transaction) - * - When loan terms change (e.g., after an overpayment that reduces the loan - * balance) - */ -struct LoanProperties -{ - // The unrounded amount to be paid at each regular payment period. - // Calculated using the standard amortization formula based on principal, - // interest rate, and number of payments. - // The actual amount paid in the LoanPay transaction must be rounded up to - // the precision of the asset and loan. - Number periodicPayment; - - // The total amount the borrower will pay over the life of the loan. - // Equal to periodicPayment * paymentsRemaining. - // This includes principal, interest, and management fees. - Number totalValueOutstanding; - - // The total management fee that will be paid to the broker over the - // loan's lifetime. This is a percentage of the total interest (gross) - // as specified by the broker's management fee rate. - Number managementFeeOwedToBroker; - - // The scale (decimal places) used for rounding all loan amounts. - // This is the maximum of: - // - The asset's native scale - // - A minimum scale required to represent the periodic payment accurately - // All loan state values (principal, interest, fees) are rounded to this - // scale. - std::int32_t loanScale; - - // The principal portion of the first payment. - Number firstPaymentPrincipal; -}; - /** This structure captures the parts of a loan state. * - * Whether the values are raw (unrounded) or rounded will depend on how it was - * computed. + * Whether the values are theoretical (unrounded) or rounded will depend on how + * it was computed. * * Many of the fields can be derived from each other, but they're all provided * here to reduce code duplication and possible mistakes. @@ -161,6 +121,39 @@ struct LoanState } }; +/* Describes the initial computed properties of a loan. + * + * This structure contains the fundamental calculated values that define a + * loan's payment structure and amortization schedule. These properties are + * computed: + * - At loan creation (LoanSet transaction) + * - When loan terms change (e.g., after an overpayment that reduces the loan + * balance) + */ +struct LoanProperties +{ + // The unrounded amount to be paid at each regular payment period. + // Calculated using the standard amortization formula based on principal, + // interest rate, and number of payments. + // The actual amount paid in the LoanPay transaction must be rounded up to + // the precision of the asset and loan. + Number periodicPayment; + + // The loan's current state, with all values rounded to the loan's scale. + LoanState loanState; + + // The scale (decimal places) used for rounding all loan amounts. + // This is the maximum of: + // - The asset's native scale + // - A minimum scale required to represent the periodic payment accurately + // All loan state values (principal, interest, fees) are rounded to this + // scale. + std::int32_t loanScale; + + // The principal portion of the first payment. + Number firstPaymentPrincipal; +}; + // Some values get re-rounded to the vault scale any time they are adjusted. In // addition, they are prevented from ever going below zero. This helps avoid // accumulated rounding errors and leftover dust amounts. @@ -179,11 +172,12 @@ adjustImpreciseNumber( } inline int -getVaultScale(SLE::const_ref vaultSle) +getAssetsTotalScale(SLE::const_ref vaultSle) { if (!vaultSle) return Number::minExponent - 1; // LCOV_EXCL_LINE - return vaultSle->at(sfAssetsTotal).exponent(); + return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)} + .exponent(); } TER @@ -196,20 +190,12 @@ checkLoanGuards( beast::Journal j); LoanState -computeRawLoanState( +computeTheoreticalLoanState( Number const& periodicPayment, Number const& periodicRate, 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( @@ -231,7 +217,7 @@ computeManagementFee( Number computeFullPaymentInterest( - Number const& rawPrincipalOutstanding, + Number const& theoreticalPrincipalOutstanding, Number const& periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, @@ -239,17 +225,6 @@ 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 @@ -387,6 +362,70 @@ struct LoanStateDeltas nonNegative(); }; +Expected, TER> +tryOverpayment( + Asset const& asset, + std::int32_t loanScale, + ExtendedPaymentComponents const& overpaymentComponents, + LoanState const& roundedLoanState, + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining, + TenthBips16 const managementFeeRate, + beast::Journal j); + +Number +computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining); + +Number +computePaymentFactor( + Number const& periodicRate, + std::uint32_t paymentsRemaining); + +std::pair +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, @@ -413,13 +452,22 @@ operator+(LoanState const& lhs, detail::LoanStateDeltas const& rhs); LoanProperties computeLoanProperties( Asset const& asset, - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale); +LoanProperties +computeLoanProperties( + Asset const& asset, + Number const& principalOutstanding, + Number const& periodicRate, + std::uint32_t paymentsRemaining, + TenthBips32 managementFeeRate, + std::int32_t minimumScale); + bool isRounded(Asset const& asset, Number const& value, std::int32_t scale); diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 37385583e7..a8354ff049 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -100,6 +100,9 @@ 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; @@ -132,27 +135,6 @@ 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. * @@ -164,6 +146,9 @@ loanPrincipalFromPeriodicPayment( Number const& periodicRate, std::uint32_t paymentsRemaining) { + if (paymentsRemaining == 0) + return numZero; + if (periodicRate == 0) return periodicPayment * paymentsRemaining; @@ -171,21 +156,6 @@ 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 -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. * @@ -216,6 +186,12 @@ 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 @@ -248,6 +224,9 @@ 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(); @@ -401,42 +380,33 @@ doPayment( * The function preserves accumulated rounding errors across the re-amortization * to ensure the loan state remains consistent with its payment history. */ -Expected +Expected, TER> tryOverpayment( Asset const& asset, std::int32_t loanScale, ExtendedPaymentComponents const& overpaymentComponents, - Number& totalValueOutstanding, - Number& principalOutstanding, - Number& managementFeeOutstanding, - Number& periodicPayment, - TenthBips32 interestRate, - std::uint32_t paymentInterval, + LoanState const& roundedOldState, + Number const& periodicPayment, Number const& periodicRate, std::uint32_t paymentRemaining, - std::uint32_t prevPaymentDate, - std::optional nextDueDate, TenthBips16 const managementFeeRate, beast::Journal j) { // Calculate what the loan state SHOULD be theoretically (at full precision) - auto const raw = computeRawLoanState( + auto const theoreticalState = computeTheoreticalLoanState( periodicPayment, periodicRate, paymentRemaining, managementFeeRate); - // Get the actual loan state (with accumulated rounding from past payments) - auto const rounded = constructLoanState( - totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - // Calculate the accumulated rounding errors. These need to be preserved // across the re-amortization to maintain consistency with the loan's // payment history. Without preserving these errors, the loan could end // up with a different total value than what the borrower has actually paid. - auto const errors = rounded - raw; + auto const errors = roundedOldState - theoreticalState; - // Compute the new principal by applying the overpayment to the raw - // (theoretical) principal. Use max with 0 to ensure we never go negative. - auto const newRawPrincipal = std::max( - raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta, + // Compute the new principal by applying the overpayment to the theoretical + // principal. Use max with 0 to ensure we never go negative. + auto const newTheoreticalPrincipal = std::max( + theoreticalState.principalOutstanding - + overpaymentComponents.trackedPrincipalDelta, Number{0}); // Compute new loan properties based on the reduced principal. This @@ -444,9 +414,8 @@ tryOverpayment( // for the remaining payment schedule. auto newLoanProperties = computeLoanProperties( asset, - newRawPrincipal, - interestRate, - paymentInterval, + newTheoreticalPrincipal, + periodicRate, paymentRemaining, managementFeeRate, loanScale); @@ -454,56 +423,60 @@ tryOverpayment( JLOG(j.debug()) << "new periodic payment: " << newLoanProperties.periodicPayment << ", new total value: " - << newLoanProperties.totalValueOutstanding + << newLoanProperties.loanState.valueOutstanding << ", first payment principal: " << newLoanProperties.firstPaymentPrincipal; // Calculate what the new loan state should be with the new periodic payment - auto const newRaw = computeRawLoanState( - newLoanProperties.periodicPayment, - periodicRate, - paymentRemaining, - managementFeeRate) + + // including rounding errors + auto const newTheoreticalState = computeTheoreticalLoanState( + newLoanProperties.periodicPayment, + periodicRate, + paymentRemaining, + managementFeeRate) + errors; - JLOG(j.debug()) << "new raw value: " << newRaw.valueOutstanding - << ", principal: " << newRaw.principalOutstanding - << ", interest gross: " << newRaw.interestOutstanding(); - // Update the loan state variables with the new values PLUS the preserved - // rounding errors. This ensures the loan's tracked state remains - // consistent with its payment history. + JLOG(j.debug()) << "new theoretical value: " + << newTheoreticalState.valueOutstanding << ", principal: " + << newTheoreticalState.principalOutstanding + << ", interest gross: " + << newTheoreticalState.interestOutstanding(); - principalOutstanding = std::clamp( - roundToAsset( - asset, newRaw.principalOutstanding, loanScale, Number::upward), - numZero, - rounded.principalOutstanding); - totalValueOutstanding = std::clamp( + // Update the loan state variables with the new values that include the + // preserved rounding errors. This ensures the loan's tracked state remains + // consistent with its payment history. + auto const principalOutstanding = std::clamp( roundToAsset( asset, - principalOutstanding + newRaw.interestOutstanding(), + newTheoreticalState.principalOutstanding, loanScale, Number::upward), numZero, - rounded.valueOutstanding); - managementFeeOutstanding = std::clamp( - roundToAsset(asset, newRaw.managementFeeDue, loanScale), + roundedOldState.principalOutstanding); + auto const totalValueOutstanding = std::clamp( + roundToAsset( + asset, + principalOutstanding + newTheoreticalState.interestOutstanding(), + loanScale, + Number::upward), numZero, - rounded.managementFeeDue); + roundedOldState.valueOutstanding); + auto const managementFeeOutstanding = std::clamp( + roundToAsset(asset, newTheoreticalState.managementFeeDue, loanScale), + numZero, + roundedOldState.managementFeeDue); - auto const newRounded = constructLoanState( + auto const roundedNewState = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); // Update newLoanProperties so that checkLoanGuards can make an accurate // evaluation. - newLoanProperties.totalValueOutstanding = newRounded.valueOutstanding; + newLoanProperties.loanState = roundedNewState; - JLOG(j.debug()) << "new rounded value: " << newRounded.valueOutstanding - << ", principal: " << newRounded.principalOutstanding - << ", interest gross: " << newRounded.interestOutstanding(); - - // Update the periodic payment to reflect the re-amortized schedule - periodicPayment = newLoanProperties.periodicPayment; + JLOG(j.debug()) << "new rounded value: " << roundedNewState.valueOutstanding + << ", principal: " << roundedNewState.principalOutstanding + << ", interest gross: " + << roundedNewState.interestOutstanding(); // check that the loan is still valid if (auto const ter = checkLoanGuards( @@ -513,7 +486,7 @@ tryOverpayment( // small interest amounts, that may have already been paid // off. Check what's still outstanding. This should // guarantee that the interest checks pass. - newRounded.interestOutstanding() != beast::zero, + roundedNewState.interestOutstanding() != beast::zero, paymentRemaining, newLoanProperties, j)) @@ -527,32 +500,40 @@ tryOverpayment( // Validate that all computed properties are reasonable. These checks should // never fail under normal circumstances, but we validate defensively. if (newLoanProperties.periodicPayment <= 0 || - newLoanProperties.totalValueOutstanding <= 0 || - newLoanProperties.managementFeeOwedToBroker < 0) + newLoanProperties.loanState.valueOutstanding <= 0 || + newLoanProperties.loanState.managementFeeDue < 0) { // LCOV_EXCL_START JLOG(j.warn()) << "Overpayment not allowed: Computed loan " "properties are invalid. Does " "not compute. TotalValueOutstanding: " - << newLoanProperties.totalValueOutstanding + << newLoanProperties.loanState.valueOutstanding << ", PeriodicPayment : " << newLoanProperties.periodicPayment << ", ManagementFeeOwedToBroker: " - << newLoanProperties.managementFeeOwedToBroker; + << newLoanProperties.loanState.managementFeeDue; return Unexpected(tesSUCCESS); // LCOV_EXCL_STOP } - auto const deltas = rounded - newRounded; + auto const deltas = roundedOldState - roundedNewState; - auto const hypotheticalValueOutstanding = - rounded.valueOutstanding - deltas.principal; + // 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 == + roundedOldState.managementFeeDue - managementFeeOutstanding, + "xrpl::detail::tryOverpayment", + "no fee change"); // 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; + // The value change is derived from the reduction in interest due to + // the lower principal. + // We do not consider the change in management fee here, since + // management fees are excluded from the valueOutstanding. + auto const valueChange = -deltas.interest; if (valueChange > 0) { JLOG(j.warn()) << "Principal overpayment would increase the value of " @@ -560,21 +541,23 @@ tryOverpayment( return Unexpected(tesSUCCESS); } - return LoanPaymentParts{ - // Principal paid is the reduction in principal outstanding - .principalPaid = deltas.principal, - // Interest paid is the reduction in interest due - .interestPaid = - deltas.interest + overpaymentComponents.untrackedInterest, - // Value change includes both the reduction from paying down principal - // (negative) and any untracked interest penalties (positive, e.g., if - // the overpayment itself incurs a fee) - .valueChange = - valueChange + overpaymentComponents.trackedInterestPart(), - // Fee paid includes both the reduction in tracked management fees and - // any untracked fees on the overpayment itself - .feePaid = deltas.managementFee + - overpaymentComponents.untrackedManagementFee}; + return std::make_pair( + LoanPaymentParts{ + // Principal paid is the reduction in principal outstanding + .principalPaid = deltas.principal, + // Interest paid is the reduction in interest due + .interestPaid = overpaymentComponents.untrackedInterest, + // Value change includes both the reduction from paying down + // principal (negative) and any untracked interest penalties + // (positive, e.g., if the overpayment itself incurs a fee) + .valueChange = + valueChange + overpaymentComponents.untrackedInterest, + // Fee paid includes both the reduction in tracked management fees + // and any untracked fees on the overpayment itself + .feePaid = overpaymentComponents.untrackedManagementFee + + overpaymentComponents.trackedManagementFeeDelta, + }, + newLoanProperties); } /* Validates and applies an overpayment to the loan state. @@ -598,23 +581,16 @@ doOverpayment( NumberProxy& principalOutstandingProxy, NumberProxy& managementFeeOutstandingProxy, NumberProxy& periodicPaymentProxy, - TenthBips32 const interestRate, - std::uint32_t const paymentInterval, Number const& periodicRate, std::uint32_t const paymentRemaining, - std::uint32_t const prevPaymentDate, - std::optional const nextDueDate, TenthBips16 const managementFeeRate, beast::Journal j) { - // Create temporary copies of the loan state that can be safely modified - // and discarded if the overpayment doesn't work out. This prevents - // corrupting the actual ledger data if validation fails. - Number totalValueOutstanding = totalValueOutstandingProxy; - Number principalOutstanding = principalOutstandingProxy; - Number managementFeeOutstanding = managementFeeOutstandingProxy; - Number periodicPayment = periodicPaymentProxy; - + auto const loanState = constructLoanState( + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy); + auto const periodicPayment = periodicPaymentProxy; JLOG(j.debug()) << "overpayment components:" << ", totalValue before: " << *totalValueOutstandingProxy @@ -633,33 +609,28 @@ doOverpayment( asset, loanScale, overpaymentComponents, - totalValueOutstanding, - principalOutstanding, - managementFeeOutstanding, + loanState, periodicPayment, - interestRate, - paymentInterval, periodicRate, paymentRemaining, - prevPaymentDate, - nextDueDate, managementFeeRate, j); if (!ret) return Unexpected(ret.error()); - auto const& loanPaymentParts = *ret; + auto const& [loanPaymentParts, newLoanProperties] = *ret; + auto const newRoundedLoanState = newLoanProperties.loanState; // Safety check: the principal must have decreased. If it didn't (or // increased!), something went wrong in the calculation and we should // reject the overpayment. - if (principalOutstandingProxy <= principalOutstanding) + if (principalOutstandingProxy <= newRoundedLoanState.principalOutstanding) { // LCOV_EXCL_START JLOG(j.warn()) << "Overpayment not allowed: principal " << "outstanding did not decrease. Before: " - << *principalOutstandingProxy - << ". After: " << principalOutstanding; + << *principalOutstandingProxy << ". After: " + << newRoundedLoanState.principalOutstanding; return Unexpected(tesSUCCESS); // LCOV_EXCL_STOP } @@ -670,34 +641,29 @@ doOverpayment( XRPL_ASSERT_PARTS( overpaymentComponents.trackedPrincipalDelta == - principalOutstandingProxy - principalOutstanding, + principalOutstandingProxy - + newRoundedLoanState.principalOutstanding, "xrpl::detail::doOverpayment", "principal change agrees"); - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedManagementFeeDelta == - managementFeeOutstandingProxy - managementFeeOutstanding, - "xrpl::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. - JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange - << ", totalValue before: " << *totalValueOutstandingProxy - << ", totalValue after: " << totalValueOutstanding - << ", totalValue delta: " - << (totalValueOutstandingProxy - totalValueOutstanding) - << ", principalDelta: " - << overpaymentComponents.trackedPrincipalDelta - << ", principalPaid: " << loanPaymentParts.principalPaid - << ", Computed difference: " - << overpaymentComponents.trackedPrincipalDelta - - (totalValueOutstandingProxy - totalValueOutstanding); + JLOG(j.debug()) + << "valueChange: " << loanPaymentParts.valueChange + << ", totalValue before: " << *totalValueOutstandingProxy + << ", totalValue after: " << newRoundedLoanState.valueOutstanding + << ", totalValue delta: " + << (totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding) + << ", principalDelta: " << overpaymentComponents.trackedPrincipalDelta + << ", principalPaid: " << loanPaymentParts.principalPaid + << ", Computed difference: " + << overpaymentComponents.trackedPrincipalDelta - + (totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding); XRPL_ASSERT_PARTS( loanPaymentParts.valueChange == - totalValueOutstanding - + newRoundedLoanState.valueOutstanding - (totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta) + overpaymentComponents.trackedInterestPart(), @@ -710,19 +676,12 @@ doOverpayment( "xrpl::detail::doOverpayment", "principal payment matches"); - XRPL_ASSERT_PARTS( - loanPaymentParts.feePaid == - overpaymentComponents.untrackedManagementFee + - overpaymentComponents.trackedManagementFeeDelta, - "xrpl::detail::doOverpayment", - "fee payment matches"); - // All validations passed, so update the proxy objects (which will // modify the actual Loan ledger object) - totalValueOutstandingProxy = totalValueOutstanding; - principalOutstandingProxy = principalOutstanding; - managementFeeOutstandingProxy = managementFeeOutstanding; - periodicPaymentProxy = periodicPayment; + totalValueOutstandingProxy = newRoundedLoanState.valueOutstanding; + principalOutstandingProxy = newRoundedLoanState.principalOutstanding; + managementFeeOutstandingProxy = newRoundedLoanState.managementFeeDue; + periodicPaymentProxy = newLoanProperties.periodicPayment; return loanPaymentParts; } @@ -789,25 +748,21 @@ computeLatePayment( // this to keep the logic clear. This preserves all the other fields without // having to enumerate them. - ExtendedPaymentComponents const late = [&]() { - auto inner = periodic; + ExtendedPaymentComponents const late{ + periodic, + // Untracked management fee includes: + // 1. Regular service fee (from periodic.untrackedManagementFee) + // 2. Late payment fee (fixed penalty) + // 3. Management fee portion of late interest + periodic.untrackedManagementFee + latePaymentFee + + roundedLateManagementFee, - return ExtendedPaymentComponents{ - inner, - // Untracked management fee includes: - // 1. Regular service fee (from periodic.untrackedManagementFee) - // 2. Late payment fee (fixed penalty) - // 3. Management fee portion of late interest - periodic.untrackedManagementFee + latePaymentFee + - roundedLateManagementFee, - - // Untracked interest includes: - // 1. Any untracked interest from the regular payment (usually 0) - // 2. Late penalty interest (increases loan value) - // This positive value indicates the loan's value increased due - // to the late payment. - periodic.untrackedInterest + roundedLateInterest}; - }(); + // Untracked interest includes: + // 1. Any untracked interest from the regular payment (usually 0) + // 2. Late penalty interest (increases loan value) + // This positive value indicates the loan's value increased due + // to the late payment. + periodic.untrackedInterest + roundedLateInterest}; XRPL_ASSERT_PARTS( isRounded(asset, late.totalDue, loanScale), @@ -875,15 +830,16 @@ computeFullPayment( } // Calculate the theoretical principal based on the payment schedule. - // This raw (unrounded) value is used to compute interest and penalties - // accurately. - Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); + // This theoretical (unrounded) value is used to compute interest and + // penalties accurately. + Number const theoreticalPrincipalOutstanding = + loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); // Full payment interest includes both accrued interest (time since last // payment) and prepayment penalty (for closing early). auto const fullPaymentInterest = computeFullPaymentInterest( - rawPrincipalOutstanding, + theoreticalPrincipalOutstanding, periodicRate, view.parentCloseTime(), paymentInterval, @@ -896,9 +852,8 @@ computeFullPayment( auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { auto const interest = roundToAsset( asset, fullPaymentInterest, loanScale, Number::downward); - auto const parts = computeInterestAndFeeParts( + return computeInterestAndFeeParts( asset, interest, managementFeeRate, loanScale); - return std::make_tuple(parts.first, parts.second); }(); ExtendedPaymentComponents const full{ @@ -943,7 +898,8 @@ computeFullPayment( JLOG(j.trace()) << "computeFullPayment result: periodicPayment: " << periodicPayment << ", periodicRate: " << periodicRate << ", paymentRemaining: " << paymentRemaining - << ", rawPrincipalOutstanding: " << rawPrincipalOutstanding + << ", theoreticalPrincipalOutstanding: " + << theoreticalPrincipalOutstanding << ", fullPaymentInterest: " << fullPaymentInterest << ", roundedFullInterest: " << roundedFullInterest << ", roundedFullManagementFee: " @@ -980,6 +936,8 @@ PaymentComponents::trackedInterestPart() const * * Special handling for the final payment: all remaining balances are paid off * regardless of the periodic payment amount. + * + * Implements the pseudo-code function `compute_payment_due()`. */ PaymentComponents computePaymentComponents( @@ -1023,7 +981,7 @@ computePaymentComponents( // Calculate what the loan state SHOULD be after this payment (the target). // This is computed at full precision using the theoretical amortization. - LoanState const trueTarget = computeRawLoanState( + LoanState const trueTarget = computeTheoreticalLoanState( periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate); // Round the target to the loan's scale to match how actual loan values @@ -1229,17 +1187,12 @@ 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] = [&]() { - Number const interest = - roundToAsset(asset, rawOverpaymentInterest, loanScale); + auto const interest = roundToAsset( + asset, + tenthBipsOfValue(overpayment, overpaymentInterestRate), + loanScale); return detail::computeInterestAndFeeParts( asset, interest, managementFeeRate, loanScale); }(); @@ -1256,12 +1209,11 @@ computeOverpaymentComponents( .specialCase = detail::PaymentSpecialCase::extra}, // Untracked management fee is the fixed overpayment fee overpaymentFee, - // Untracked interest is the penalty interest charged for - // overpaying. - // This is positive, representing a one-time cost, but it's - // typically - // much smaller than the interest savings from reducing - // principal. + // Untracked interest is the penalty interest charged for overpaying. + // This is positive, representing a one-time cost, but it's typically + // much smaller than the interest savings from reducing principal. + // It is equal to the paymentComponents.trackedInterestPart() + // but is kept separate for clarity. roundedOverpaymentInterest}; XRPL_ASSERT_PARTS( result.trackedInterestPart() == roundedOverpaymentInterest, @@ -1320,7 +1272,7 @@ checkLoanGuards( beast::Journal j) { auto const totalInterestOutstanding = - properties.totalValueOutstanding - principalRequested; + properties.loanState.valueOutstanding - principalRequested; // Guard 1: if there is no computed total interest over the life of the // loan for a non-zero interest rate, we cannot properly amortize the // loan @@ -1375,13 +1327,13 @@ checkLoanGuards( NumberRoundModeGuard mg(Number::upward); if (std::int64_t const computedPayments{ - properties.totalValueOutstanding / roundedPayment}; + properties.loanState.valueOutstanding / roundedPayment}; computedPayments != paymentTotal) { JLOG(j.warn()) << "Loan Periodic payment (" << properties.periodicPayment << ") rounding (" << roundedPayment << ") on a total value of " - << properties.totalValueOutstanding + << properties.loanState.valueOutstanding << " can not complete the loan in the specified " "number of payments (" << computedPayments << " != " << paymentTotal << ")"; @@ -1399,7 +1351,7 @@ checkLoanGuards( */ Number computeFullPaymentInterest( - Number const& rawPrincipalOutstanding, + Number const& theoreticalPrincipalOutstanding, Number const& periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, @@ -1408,7 +1360,7 @@ computeFullPaymentInterest( TenthBips32 closeInterestRate) { auto const accruedInterest = detail::loanAccruedInterest( - rawPrincipalOutstanding, + theoreticalPrincipalOutstanding, periodicRate, parentCloseTime, startDate, @@ -1422,7 +1374,7 @@ computeFullPaymentInterest( // Equation (28) from XLS-66 spec, Section A-2 Equation Glossary auto const prepaymentPenalty = closeInterestRate == beast::zero ? Number{} - : tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); + : tenthBipsOfValue(theoreticalPrincipalOutstanding, closeInterestRate); XRPL_ASSERT( prepaymentPenalty >= 0, @@ -1433,42 +1385,17 @@ 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. * * This function computes what the loan's outstanding balances should be based * on the periodic payment amount and number of payments remaining, * without considering any rounding that may have been applied to the actual - * Loan object's state. This "raw" (unrounded) state is used as a target for - * computing payment components and validating that the loan's tracked state + * Loan object's state. This "theoretical" (unrounded) state is used as a target + * for computing payment components and validating that the loan's tracked state * hasn't drifted too far from the theoretical values. * - * The raw state serves several purposes: + * The theoretical state serves several purposes: * 1. Computing the expected payment breakdown (principal, interest, fees) * 2. Detecting and correcting rounding errors that accumulate over time * 3. Validating that overpayments are calculated correctly @@ -1476,9 +1403,12 @@ computeFullPaymentInterest( * * If paymentRemaining is 0, returns a fully zeroed-out LoanState, * representing a completely paid-off loan. + * + * Implements the `calculate_true_loan_state` function from the XLS-66 spec + * section 3.2.4.4 Transaction Pseudo-code */ LoanState -computeRawLoanState( +computeTheoreticalLoanState( Number const& periodicPayment, Number const& periodicRate, std::uint32_t const paymentRemaining, @@ -1494,55 +1424,42 @@ computeRawLoanState( } // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawTotalValueOutstanding = periodicPayment * paymentRemaining; + Number const totalValueOutstanding = periodicPayment * paymentRemaining; - Number const rawPrincipalOutstanding = + Number const principalOutstanding = detail::loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); // Equation (31) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawInterestOutstandingGross = - rawTotalValueOutstanding - rawPrincipalOutstanding; + Number const interestOutstandingGross = + totalValueOutstanding - principalOutstanding; // Equation (32) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawManagementFeeOutstanding = - tenthBipsOfValue(rawInterestOutstandingGross, managementFeeRate); + Number const managementFeeOutstanding = + tenthBipsOfValue(interestOutstandingGross, managementFeeRate); // Equation (33) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawInterestOutstandingNet = - rawInterestOutstandingGross - rawManagementFeeOutstanding; + Number const interestOutstandingNet = + interestOutstandingGross - managementFeeOutstanding; return LoanState{ - .valueOutstanding = rawTotalValueOutstanding, - .principalOutstanding = rawPrincipalOutstanding, - .interestDue = rawInterestOutstandingNet, - .managementFeeDue = rawManagementFeeOutstanding}; + .valueOutstanding = totalValueOutstanding, + .principalOutstanding = principalOutstanding, + .interestDue = interestOutstandingNet, + .managementFeeDue = managementFeeOutstanding, + }; }; -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 - * stored in a Loan ledger object. Unlike calculateRawLoanState(), which + * stored in a Loan ledger object. Unlike calculateTheoreticalLoanState(), which * computes theoretical unrounded values, this function works with values * that have already been rounded to the loan's scale. * - * The key difference from calculateRawLoanState(): - * - calculateRawLoanState: Computes theoretical values at full precision + * The key difference from calculateTheoreticalLoanState(): + * - calculateTheoreticalLoanState: Computes theoretical values at full + * precision * - constructRoundedLoanState: Builds state from actual rounded ledger values * * The interestDue field is derived from the other three values rather than @@ -1600,11 +1517,16 @@ computeManagementFee( /* * Given the loan parameters, compute the derived properties of the loan. + * + * Pulls together several formulas from the XLS-66 spec, which are noted at each + * step, plus the concepts from 3.2.4.3 Conceptual Loan Value. They are used for + * to check some of the conditions in 3.2.1.5 Failure Conditions for the LoanSet + * transaction. */ LoanProperties computeLoanProperties( Asset const& asset, - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, @@ -1615,12 +1537,39 @@ computeLoanProperties( XRPL_ASSERT( interestRate == 0 || periodicRate > 0, "xrpl::computeLoanProperties : valid rate"); + return computeLoanProperties( + asset, + principalOutstanding, + periodicRate, + paymentsRemaining, + managementFeeRate, + minimumScale); +} +/* + * Given the loan parameters, compute the derived properties of the loan. + * + * Pulls together several formulas from the XLS-66 spec, which are noted at each + * step, plus the concepts from 3.2.4.3 Conceptual Loan Value. They are used for + * to check some of the conditions in 3.2.1.5 Failure Conditions for the LoanSet + * transaction. + */ +LoanProperties +computeLoanProperties( + Asset const& asset, + Number const& principalOutstanding, + Number const& periodicRate, + std::uint32_t paymentsRemaining, + TenthBips32 managementFeeRate, + std::int32_t minimumScale) +{ auto const periodicPayment = detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); auto const [totalValueOutstanding, loanScale] = [&]() { - NumberRoundModeGuard mg(Number::to_nearest); + // only round up if there should be interest + NumberRoundModeGuard mg( + periodicRate == 0 ? Number::to_nearest : Number::upward); // 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. @@ -1641,7 +1590,7 @@ computeLoanProperties( // We may need to truncate the total value because of the minimum // scale - amount = roundToAsset(asset, amount, loanScale, Number::to_nearest); + amount = roundToAsset(asset, amount, loanScale); return std::make_pair(amount, loanScale); }(); @@ -1649,12 +1598,12 @@ computeLoanProperties( // Since we just figured out the loan scale, we haven't been able to // validate that the principal fits in it, so to allow this function to // succeed, round it here, and let the caller do the validation. - principalOutstanding = roundToAsset( + auto const roundedPrincipalOutstanding = roundToAsset( asset, principalOutstanding, loanScale, Number::to_nearest); // Equation (31) from XLS-66 spec, Section A-2 Equation Glossary auto const totalInterestOutstanding = - totalValueOutstanding - principalOutstanding; + totalValueOutstanding - roundedPrincipalOutstanding; auto const feeOwedToBroker = computeManagementFee( asset, totalInterestOutstanding, managementFeeRate, loanScale); @@ -1664,13 +1613,13 @@ computeLoanProperties( auto const firstPaymentPrincipal = [&]() { // Compute the parts for the first payment. Ensure that the // principal payment will actually change the principal. - auto const startingState = computeRawLoanState( + auto const startingState = computeTheoreticalLoanState( periodicPayment, periodicRate, paymentsRemaining, managementFeeRate); - auto const firstPaymentState = computeRawLoanState( + auto const firstPaymentState = computeTheoreticalLoanState( periodicPayment, periodicRate, paymentsRemaining - 1, @@ -1684,10 +1633,13 @@ computeLoanProperties( return LoanProperties{ .periodicPayment = periodicPayment, - .totalValueOutstanding = totalValueOutstanding, - .managementFeeOwedToBroker = feeOwedToBroker, + .loanState = constructLoanState( + totalValueOutstanding, + roundedPrincipalOutstanding, + feeOwedToBroker), .loanScale = loanScale, - .firstPaymentPrincipal = firstPaymentPrincipal}; + .firstPaymentPrincipal = firstPaymentPrincipal, + }; } /* @@ -1740,7 +1692,7 @@ loanMakePayment( Number const periodicPayment = loan->at(sfPeriodicPayment); - auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); + auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate); std::uint32_t const startDate = loan->at(sfStartDate); std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); @@ -2016,12 +1968,8 @@ loanMakePayment( principalOutstandingProxy, managementFeeOutstandingProxy, periodicPaymentProxy, - interestRate, - paymentInterval, periodicRate, paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, managementFeeRate, j)) totalParts += *overResult; diff --git a/src/xrpld/app/paths/Flow.cpp b/src/xrpld/app/paths/Flow.cpp index a102e44854..b5088d15b3 100644 --- a/src/xrpld/app/paths/Flow.cpp +++ b/src/xrpld/app/paths/Flow.cpp @@ -1,11 +1,11 @@ #include -#include #include #include #include #include #include +#include #include #include diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index 4e701d348f..3d3a76f42d 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -1,8 +1,8 @@ -#include #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/paths/detail/StrandFlow.h b/src/xrpld/app/paths/detail/StrandFlow.h index fab92dca35..ca4b18f0a3 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -11,6 +10,7 @@ #include #include +#include #include #include #include diff --git a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp index 83271321be..ed1866bf24 100644 --- a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp +++ b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp @@ -1,9 +1,9 @@ -#include #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 595e573041..b19cbacbe6 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -217,7 +217,7 @@ EscrowCreate::preflight(PreflightContext const& ctx) return temMALFORMED; } - HostFunctions mock(ctx.j); + auto mock(std::make_shared(ctx.j)); auto const re = preflightEscrowWasm(code, mock, ESCROW_FUNCTION_NAME); if (!isTesSuccess(re)) { @@ -1206,7 +1206,8 @@ EscrowFinish::doApply() auto const wasmStr = slep->getFieldVL(sfFinishFunction); std::vector wasm(wasmStr.begin(), wasmStr.end()); - WasmHostFunctionsImpl ledgerDataProvider(ctx_, k); + auto ledgerDataProvider( + std::make_shared(ctx_, k)); if (!ctx_.tx.isFieldPresent(sfComputationAllowance)) { @@ -1218,7 +1219,7 @@ EscrowFinish::doApply() wasm, ledgerDataProvider, ESCROW_FUNCTION_NAME, {}, allowance); JLOG(j_.trace()) << "Escrow WASM ran"; - if (auto const& data = ledgerDataProvider.getData(); data.has_value()) + if (auto const& data = ledgerDataProvider->getData(); data.has_value()) { slep->setFieldVL(sfData, makeSlice(*data)); ctx_.view().update(slep); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 32c8fecf20..c61145dda1 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -270,7 +272,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum."; return findClawAmount.error(); } - STAmount const clawAmount = *findClawAmount; + STAmount const& clawAmount = *findClawAmount; // Explicitly check the balance of the trust line / MPT to make sure the // balance is actually there. It should always match `sfCoverAvailable`, so @@ -287,6 +289,14 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) // Check if the vault asset issuer has the correct flags auto const sleIssuer = ctx.view.read(keylet::account(vaultAsset.getIssuer())); + if (!sleIssuer) + { + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Issuer account does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + return std::visit( [&](T const&) { return preclaimHelper(ctx, *sleIssuer, clawAmount); @@ -321,7 +331,7 @@ LoanBrokerCoverClawback::doApply() determineClawAmount(*sleBroker, vaultAsset, amount); if (!findClawAmount) return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const clawAmount = *findClawAmount; + STAmount const& clawAmount = *findClawAmount; // Just for paranoia's sake if (clawAmount.native()) return tecINTERNAL; // LCOV_EXCL_LINE @@ -330,6 +340,8 @@ LoanBrokerCoverClawback::doApply() sleBroker->at(sfCoverAvailable) -= clawAmount; view().update(sleBroker); + associateAsset(*sleBroker, vaultAsset); + // Transfer assets from pseudo-account to depositor. return accountSend( view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index c894df2c2b..ed47d40631 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -81,7 +83,8 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) vaultAsset, FreezeHandling::fhZERO_IF_FROZEN, AuthHandling::ahZERO_IF_UNAUTHORIZED, - ctx.j) < amount) + ctx.j, + SpendableHandling::shFULL_BALANCE) < amount) return tecINSUFFICIENT_FUNDS; return tesSUCCESS; @@ -99,6 +102,12 @@ LoanBrokerCoverDeposit::doApply() if (!broker) return tecINTERNAL; // LCOV_EXCL_LINE + auto const vault = view().read(keylet::vault(broker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const vaultAsset = vault->at(sfAsset); + auto const brokerPseudoID = broker->at(sfAccount); // Transfer assets from depositor to pseudo-account. @@ -115,6 +124,8 @@ LoanBrokerCoverDeposit::doApply() broker->at(sfCoverAvailable) += amount; view().update(broker); + associateAsset(*broker, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 4c0b3e9af5..5d4d2053ed 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace xrpl { @@ -48,6 +49,11 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) auto const dstAcct = tx[~sfDestination].value_or(account); + if (isPseudoAccount(ctx.view, dstAcct)) + { + JLOG(ctx.j.warn()) << "Trying to withdraw into a pseudo-account."; + return tecPSEUDO_ACCOUNT; + } auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); if (!sleBroker) { @@ -151,12 +157,20 @@ LoanBrokerCoverWithdraw::doApply() if (!broker) return tecINTERNAL; // LCOV_EXCL_LINE + auto const vault = view().read(keylet::vault(broker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const vaultAsset = vault->at(sfAsset); + auto const brokerPseudoID = *broker->at(sfAccount); // Decrease the LoanBroker's CoverAvailable by Amount broker->at(sfCoverAvailable) -= amount; view().update(broker); + associateAsset(*broker, vaultAsset); + return doWithdraw( view(), tx, diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 76773037fa..f3f57f8659 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -46,30 +48,6 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "LoanBrokerDelete: Owner count is " << ownerCount; return tecHAS_OBLIGATIONS; } - if (auto const debtTotal = sleBroker->at(sfDebtTotal); - debtTotal != beast::zero) - { - // Any remaining debt should have been wiped out by the last Loan - // Delete. This check is purely defensive. - auto const vault = - ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); - if (!vault) - return tefINTERNAL; // LCOV_EXCL_LINE - auto const asset = vault->at(sfAsset); - auto const scale = getVaultScale(vault); - - auto const rounded = - roundToAsset(asset, debtTotal, scale, Number::towards_zero); - - if (rounded != beast::zero) - { - // LCOV_EXCL_START - JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is " - << debtTotal << ", which rounds to " << rounded; - return tecHAS_OBLIGATIONS; - // LCOV_EXCL_START - } - } auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); if (!vault) @@ -82,6 +60,26 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx) Asset const asset = vault->at(sfAsset); + if (auto const debtTotal = sleBroker->at(sfDebtTotal); + debtTotal != beast::zero) + { + // Any remaining debt should have been wiped out by the last Loan + // Delete. This check is purely defensive. + auto const scale = getAssetsTotalScale(vault); + + auto const rounded = + roundToAsset(asset, debtTotal, scale, Number::towards_zero); + + if (rounded != beast::zero) + { + // LCOV_EXCL_START + JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is " + << debtTotal << ", which rounds to " << rounded; + return tecHAS_OBLIGATIONS; + // LCOV_EXCL_STOP + } + } + auto const coverAvailable = STAmount{asset, sleBroker->at(sfCoverAvailable)}; // If there are assets in the cover, broker will receive them on deletion. @@ -189,6 +187,8 @@ LoanBrokerDelete::doApply() adjustOwnerCount(view(), owner, -2, j_); } + associateAsset(*broker, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 7b12a6cf39..b7e9e4c79f 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -62,6 +64,15 @@ LoanBrokerSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } +std::vector> const& +LoanBrokerSet::getValueFields() +{ + static std::vector> const valueFields{ + ~sfDebtMaximum}; + + return valueFields; +} + TER LoanBrokerSet::preclaim(PreclaimContext const& ctx) { @@ -70,8 +81,24 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) auto const account = tx[sfAccount]; auto const vaultID = tx[sfVaultID]; + auto const sleVault = ctx.view.read(keylet::vault(vaultID)); + if (!sleVault) + { + JLOG(ctx.j.warn()) << "Vault does not exist."; + return tecNO_ENTRY; + } + Asset const asset = sleVault->at(sfAsset); + + if (account != sleVault->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the Vault."; + return tecNO_PERMISSION; + } + if (auto const brokerID = tx[~sfLoanBrokerID]) { + // Updating an existing Broker + auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID)); if (!sleBroker) { @@ -89,23 +116,46 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; return tecNO_PERMISSION; } + + if (auto const debtMax = tx[~sfDebtMaximum]) + { + // Can't reduce the debt maximum below the current total debt + auto const currentDebtTotal = sleBroker->at(sfDebtTotal); + if (*debtMax != 0 && *debtMax < currentDebtTotal) + { + JLOG(ctx.j.warn()) + << "Cannot reduce DebtMaximum below current DebtTotal."; + return tecLIMIT_EXCEEDED; + } + } } else { - auto const sleVault = ctx.view.read(keylet::vault(vaultID)); - if (!sleVault) - { - JLOG(ctx.j.warn()) << "Vault does not exist."; - return tecNO_ENTRY; - } - if (account != sleVault->at(sfOwner)) - { - JLOG(ctx.j.warn()) << "Account is not the owner of the Vault."; - return tecNO_PERMISSION; - } - if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset))) + if (auto const ter = canAddHolding(ctx.view, asset)) return ter; + + if (auto const ter = checkFrozen( + ctx.view, sleVault->at(sfAccount), sleVault->at(sfAsset))) + { + JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; + return ter; + } } + + // Check that relevant values can be represented as the vault asset + // type. This is mostly only relevant for integral (non-IOU) types + for (auto const& field : getValueFields()) + { + if (auto const value = tx[field]; + value && STAmount{asset, *value} != *value) + { + JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value + << ") can not be represented as a(n) " + << to_string(asset) << "."; + return tecPRECISION_LOSS; + } + } + return tesSUCCESS; } @@ -128,12 +178,20 @@ LoanBrokerSet::doApply() // LCOV_EXCL_STOP } + auto const vault = view.read(keylet::vault(broker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const vaultAsset = vault->at(sfAsset); + if (auto const data = tx[~sfData]) broker->at(sfData) = *data; if (auto const debtMax = tx[~sfDebtMaximum]) broker->at(sfDebtMaximum) = *debtMax; view.update(broker); + + associateAsset(*broker, vaultAsset); } else { @@ -149,6 +207,7 @@ LoanBrokerSet::doApply() // LCOV_EXCL_STOP } auto const vaultPseudoID = sleVault->at(sfAccount); + auto const vaultAsset = sleVault->at(sfAsset); auto const sequence = tx.getSeqValue(); auto owner = view.peek(keylet::account(account_)); @@ -205,6 +264,8 @@ LoanBrokerSet::doApply() broker->at(sfCoverRateLiquidation) = *coverLiq; view.insert(broker); + + associateAsset(*broker, vaultAsset); } return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.h b/src/xrpld/app/tx/detail/LoanBrokerSet.h index 625c0adeb2..57170b9cb9 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.h +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.h @@ -20,6 +20,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static std::vector> const& + getValueFields(); + static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 659f0bba8b..ddb286db12 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -78,9 +80,10 @@ LoanDelete::doApply() return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const brokerPseudoAccount = brokerSle->at(sfAccount); - auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); + auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID))); if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultAsset = vaultSle->at(sfAsset); // Remove LoanID from Directory of the LoanBroker pseudo-account. if (!view.dirRemove( @@ -115,7 +118,7 @@ LoanDelete::doApply() roundToAsset( vaultSle->at(sfAsset), debtTotalProxy, - getVaultScale(vaultSle), + getAssetsTotalScale(vaultSle), Number::towards_zero) == beast::zero, "xrpl::LoanDelete::doApply", "last loan, remaining debt rounds to zero"); @@ -125,6 +128,11 @@ LoanDelete::doApply() // Decrement the borrower's owner count adjustOwnerCount(view, borrowerSle, -1, j_); + // These associations shouldn't do anything, but do them just to be safe + associateAsset(*loanSle, vaultAsset); + associateAsset(*brokerSle, vaultAsset); + associateAsset(*vaultSle, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 2d405f204b..17dee8ffe8 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -2,6 +2,7 @@ // #include +#include #include namespace xrpl { @@ -106,7 +107,7 @@ LoanManage::preclaim(PreclaimContext const& ctx) if (loanBrokerSle->at(sfOwner) != account) { JLOG(ctx.j.warn()) - << "LoanBroker for Loan does not belong to the account. LoanModify " + << "LoanBroker for Loan does not belong to the account. LoanManage " "can only be submitted by the Loan Broker."; return tecNO_PERMISSION; } @@ -158,7 +159,7 @@ LoanManage::defaultLoan( auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum); // Round the liquidation amount up, too - return roundToAsset( + auto const covered = roundToAsset( vaultAsset, /* * This formula is from the XLS-66 spec, section 3.2.3.2 (State @@ -169,6 +170,9 @@ LoanManage::defaultLoan( tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount), loanScale); + auto const coverAvailable = *brokerSle->at(sfCoverAvailable); + + return std::min(covered, coverAvailable); }(); auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered; @@ -178,7 +182,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 = getVaultScale(vaultSle); + auto const vaultScale = getAssetsTotalScale(vaultSle); { // Decrease the Total Value of the Vault: @@ -223,11 +227,13 @@ LoanManage::defaultLoan( } if (*vaultAvailableProxy > *vaultTotalProxy) { - JLOG(j.warn()) << "Vault assets available must not be greater " - "than assets outstanding. Available: " - << *vaultAvailableProxy - << ", Total: " << *vaultTotalProxy; - return tecLIMIT_EXCEEDED; + // LCOV_EXCL_START + JLOG(j.fatal()) + << "Vault assets available must not be greater " + "than assets outstanding. Available: " + << *vaultAvailableProxy << ", Total: " << *vaultTotalProxy; + return tecINTERNAL; + // LCOV_EXCL_STOP } // The loss has been realized @@ -242,7 +248,11 @@ LoanManage::defaultLoan( return tefBAD_LEDGER; // LCOV_EXCL_STOP } - vaultLossUnrealizedProxy -= totalDefaultAmount; + adjustImpreciseNumber( + vaultLossUnrealizedProxy, + -totalDefaultAmount, + vaultAsset, + vaultScale); } view.update(vaultSle); } @@ -250,11 +260,9 @@ LoanManage::defaultLoan( // Update the LoanBroker object: { - auto const asset = *vaultSle->at(sfAsset); - // Decrease the Debt of the LoanBroker: adjustImpreciseNumber( - brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale); + brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale); // Decrease the First-Loss Capital Cover Available: auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); if (coverAvailableProxy < defaultCovered) @@ -297,13 +305,20 @@ 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); - vaultLossUnrealizedProxy += lossUnrealized; + adjustImpreciseNumber( + vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale); if (vaultLossUnrealizedProxy > vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable)) { @@ -329,13 +344,19 @@ LoanManage::impairLoan( return tesSUCCESS; } -TER +[[nodiscard]] TER 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); @@ -347,14 +368,18 @@ LoanManage::unimpairLoan( return tefBAD_LEDGER; // LCOV_EXCL_STOP } - vaultLossUnrealizedProxy -= lossReversed; + // Reverse the "paper loss" + adjustImpreciseNumber( + vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale); + view.update(vaultSle); // Update the Loan object loanSle->clearFlag(lsfLoanImpaired); auto const paymentInterval = loanSle->at(sfPaymentInterval); auto const normalPaymentDueDate = - std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) + + std::max( + loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) + paymentInterval; if (!hasExpired(view, normalPaymentDueDate)) { @@ -388,7 +413,7 @@ LoanManage::doApply() if (!brokerSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); + auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID))); if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const vaultAsset = vaultSle->at(sfAsset); @@ -396,21 +421,16 @@ LoanManage::doApply() // Valid flag combinations are checked in preflight. No flags is valid - // just a noop. if (tx.isFlag(tfLoanDefault)) - { - if (auto const ter = - defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_)) - return ter; - } - else if (tx.isFlag(tfLoanImpair)) - { - if (auto const ter = impairLoan(view, loanSle, vaultSle, j_)) - return ter; - } - else if (tx.isFlag(tfLoanUnimpair)) - { - if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_)) - return ter; - } + return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_); + if (tx.isFlag(tfLoanImpair)) + return impairLoan(view, loanSle, vaultSle, vaultAsset, j_); + if (tx.isFlag(tfLoanUnimpair)) + return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_); + // Noop, as described above. + + associateAsset(*loanSle, vaultAsset); + associateAsset(*brokerSle, vaultAsset); + associateAsset(*vaultSle, vaultAsset); return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h index 7a02c7a16f..155611580f 100644 --- a/src/xrpld/app/tx/detail/LoanManage.h +++ b/src/xrpld/app/tx/detail/LoanManage.h @@ -44,15 +44,17 @@ public: ApplyView& view, SLE::ref loanSle, SLE::ref vaultSle, + Asset const& vaultAsset, beast::Journal j); /** Helper function that might be needed by other transactors */ - static TER + [[nodiscard]] static TER unimpairLoan( ApplyView& view, SLE::ref loanSle, SLE::ref vaultSle, + Asset const& vaultAsset, beast::Journal j); TER diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index d34a766d70..13f62d4c0d 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -152,9 +153,7 @@ LoanPay::preclaim(PreclaimContext const& ctx) } auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); - TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; auto const paymentRemaining = loanSle->at(sfPaymentRemaining); - TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; if (paymentRemaining == 0 || principalOutstanding == 0) { @@ -211,13 +210,14 @@ LoanPay::preclaim(PreclaimContext const& ctx) // Do not support "partial payments" - if the transaction says to pay X, // then the account must have X available, even if the loan payment takes // less. - if (auto const balance = accountSpendable( + if (auto const balance = accountHolds( ctx.view, account, asset, fhZERO_IF_FROZEN, ahZERO_IF_UNAUTHORIZED, - ctx.j); + ctx.j, + SpendableHandling::shFULL_BALANCE); balance < amount) { JLOG(ctx.j.warn()) << "Payment amount too large. Amount: " @@ -262,11 +262,12 @@ LoanPay::doApply() auto debtTotalProxy = brokerSle->at(sfDebtTotal); // Send the broker fee to the owner if they have sufficient cover available, - // _and_ if the owner can receive funds. If not, so as not to block the - // payment, add it to the cover balance (send it to the broker pseudo - // account). + // _and_ if the owner can receive funds + // _and_ if the broker is authorized to hold funds. If not, so as not to + // block the payment, add it to the cover balance (send it to the broker + // pseudo account). // - // Normally freeze status is checked in preflight, but we do it here to + // Normally freeze status is checked in preclaim, but we do it here to // avoid duplicating the check. It'll claim a fee either way. bool const sendBrokerFeeToOwner = [&]() { // Round the minimum required cover up to be conservative. This ensures @@ -278,7 +279,8 @@ LoanPay::doApply() asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale) && - !isDeepFrozen(view, brokerOwner, asset); + !isDeepFrozen(view, brokerOwner, asset) && + !requireAuth(view, asset, brokerOwner, AuthType::StrongAuth); }(); auto const brokerPayee = @@ -305,7 +307,12 @@ LoanPay::doApply() // change will be discarded. if (loanSle->isFlag(lsfLoanImpaired)) { - LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); + if (auto const ret = + LoanManage::unimpairLoan(view, loanSle, vaultSle, asset, j_)) + { + JLOG(j_.fatal()) << "Failed to unimpair loan before payment."; + return ret; // LCOV_EXCL_LINE + } } LoanPaymentType const paymentType = [&tx]() { @@ -377,7 +384,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 = assetsTotalProxy.value().exponent(); + auto const vaultScale = getAssetsTotalScale(vaultSle); auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid; @@ -421,35 +428,41 @@ LoanPay::doApply() // Vault object state changes view.update(vaultSle); - Number const assetsAvailableBefore = *assetsAvailableProxy; - Number const pseudoAccountBalanceBefore = accountHolds( - view, - vaultPseudoAccount, - asset, - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j_); - +#if !NDEBUG { + Number const assetsAvailableBefore = *assetsAvailableProxy; + Number const pseudoAccountBalanceBefore = accountHolds( + view, + vaultPseudoAccount, + asset, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_); + XRPL_ASSERT_PARTS( assetsAvailableBefore == pseudoAccountBalanceBefore, "xrpl::LoanPay::doApply", "vault pseudo balance agrees before"); + } +#endif - assetsAvailableProxy += totalPaidToVaultRounded; - assetsTotalProxy += paymentParts->valueChange; + assetsAvailableProxy += totalPaidToVaultRounded; + assetsTotalProxy += paymentParts->valueChange; - XRPL_ASSERT_PARTS( - *assetsAvailableProxy <= *assetsTotalProxy, - "xrpl::LoanPay::doApply", - "assets available must not be greater than assets outstanding"); + XRPL_ASSERT_PARTS( + *assetsAvailableProxy <= *assetsTotalProxy, + "xrpl::LoanPay::doApply", + "assets available must not be greater than assets outstanding"); - if (*assetsAvailableProxy > *assetsTotalProxy) - { - // LCOV_EXCL_START - return tecINTERNAL; - // LCOV_EXCL_STOP - } + if (*assetsAvailableProxy > *assetsTotalProxy) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Vault assets available must not be greater " + "than assets outstanding. Available: " + << *assetsAvailableProxy + << ", Total: " << *assetsTotalProxy; + return tecINTERNAL; + // LCOV_EXCL_STOP } JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw @@ -473,22 +486,45 @@ LoanPay::doApply() coverAvailableProxy += totalPaidToBroker; } + associateAsset(*loanSle, asset); + associateAsset(*brokerSle, asset); + associateAsset(*vaultSle, asset); + + // Duplicate some checks after rounding + XRPL_ASSERT_PARTS( + *assetsAvailableProxy <= *assetsTotalProxy, + "xrpl::LoanPay::doApply", + "assets available must not be greater than assets outstanding"); + #if !NDEBUG - auto const accountBalanceBefore = accountSpendable( - view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const accountBalanceBefore = accountHolds( + view, + account_, + asset, + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j_, + SpendableHandling::shFULL_BALANCE); auto const vaultBalanceBefore = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountSpendable( + : accountHolds( view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, - j_); + j_, + SpendableHandling::shFULL_BALANCE); auto const brokerBalanceBefore = account_ == brokerPayee ? STAmount{asset, 0} - : accountSpendable( - view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + : accountHolds( + view, + brokerPayee, + asset, + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j_, + SpendableHandling::shFULL_BALANCE); #endif if (totalPaidToVaultRounded != beast::zero) @@ -529,6 +565,7 @@ LoanPay::doApply() WaiveTransferFee::Yes)) return ter; +#if !NDEBUG Number const assetsAvailableAfter = *assetsAvailableProxy; Number const pseudoAccountBalanceAfter = accountHolds( view, @@ -542,22 +579,34 @@ LoanPay::doApply() "xrpl::LoanPay::doApply", "vault pseudo balance agrees after"); -#if !NDEBUG - auto const accountBalanceAfter = accountSpendable( - view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const accountBalanceAfter = accountHolds( + view, + account_, + asset, + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j_, + SpendableHandling::shFULL_BALANCE); auto const vaultBalanceAfter = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountSpendable( + : accountHolds( view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, - j_); + j_, + SpendableHandling::shFULL_BALANCE); auto const brokerBalanceAfter = account_ == brokerPayee ? STAmount{asset, 0} - : accountSpendable( - view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + : accountHolds( + view, + brokerPayee, + asset, + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j_, + SpendableHandling::shFULL_BALANCE); XRPL_ASSERT_PARTS( accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore == diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index e8fe76a48f..0b83d3009f 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -2,6 +2,7 @@ // #include +#include #include namespace xrpl { @@ -88,10 +89,12 @@ LoanSet::preflight(PreflightContext const& ctx) if (auto const paymentInterval = tx[~sfPaymentInterval]; !validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval)) return temINVALID; - - else if (!validNumericRange( - tx[~sfGracePeriod], - paymentInterval.value_or(LoanSet::defaultPaymentInterval))) + // Grace period is between min default value and payment interval + else if (auto const gracePeriod = tx[~sfGracePeriod]; // + !validNumericRange( + gracePeriod, + paymentInterval.value_or(LoanSet::defaultPaymentInterval), + defaultGracePeriod)) return temINVALID; // Copied from preflight2 @@ -282,6 +285,15 @@ LoanSet::preclaim(PreclaimContext const& ctx) if (!vault) // Should be impossible return tefBAD_LEDGER; // LCOV_EXCL_LINE + + if (vault->at(sfAssetsMaximum) != 0 && + vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum)) + { + JLOG(ctx.j.warn()) + << "Vault at maximum assets limit. Can't add another loan."; + return tecLIMIT_EXCEEDED; + } + Asset const asset = vault->at(sfAsset); auto const vaultPseudo = vault->at(sfAccount); @@ -290,17 +302,15 @@ LoanSet::preclaim(PreclaimContext const& ctx) // This check is almost duplicated in doApply, but that check is done after // the overall loan scale is known. This is mostly only relevant for // integral (non-IOU) types + for (auto const& field : getValueFields()) { - for (auto const& field : getValueFields()) + if (auto const value = tx[field]; + value && STAmount{asset, *value} != *value) { - if (auto const value = tx[field]; - value && STAmount{asset, *value} != *value) - { - JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value - << ") can not be represented as a(n) " - << to_string(asset) << "."; - return tecPRECISION_LOSS; - } + JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value + << ") can not be represented as a(n) " + << to_string(asset) << "."; + return tecPRECISION_LOSS; } } @@ -383,7 +393,7 @@ LoanSet::doApply() auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); - auto const vaultScale = getVaultScale(vaultSle); + auto const vaultScale = getAssetsTotalScale(vaultSle); if (vaultAvailableProxy < principalRequested) { JLOG(j_.warn()) @@ -406,21 +416,33 @@ LoanSet::doApply() TenthBips16{brokerSle->at(sfManagementFeeRate)}, vaultScale); + LoanState const state = constructLoanState( + properties.loanState.valueOutstanding, + principalRequested, + properties.loanState.managementFeeDue); + + auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum); + XRPL_ASSERT_PARTS( + vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy, + "xrpl::LoanSet::doApply", + "Vault is below maximum limit"); + if (vaultMaximum != 0 && state.interestDue > vaultMaximum - vaultTotalProxy) + { + JLOG(j_.warn()) << "Loan would exceed the maximum assets of the vault"; + return tecLIMIT_EXCEEDED; + } // Check that relevant values won't lose precision. This is mostly only // relevant for IOU assets. + for (auto const& field : getValueFields()) { - for (auto const& field : getValueFields()) + if (auto const value = tx[field]; + value && !isRounded(vaultAsset, *value, properties.loanScale)) { - if (auto const value = tx[field]; - value && !isRounded(vaultAsset, *value, properties.loanScale)) - { - JLOG(j_.warn()) - << field.f->getName() << " (" << *value - << ") has too much precision. Total loan value is " - << properties.totalValueOutstanding << " with a scale of " - << properties.loanScale; - return tecPRECISION_LOSS; - } + JLOG(j_.warn()) << field.f->getName() << " (" << *value + << ") has too much precision. Total loan value is " + << properties.loanState.valueOutstanding + << " with a scale of " << properties.loanScale; + return tecPRECISION_LOSS; } } @@ -434,22 +456,20 @@ LoanSet::doApply() return ret; // Check that the other computed values are valid - if (properties.managementFeeOwedToBroker < 0 || - properties.totalValueOutstanding <= 0 || + if (properties.loanState.managementFeeDue < 0 || + properties.loanState.valueOutstanding <= 0 || properties.periodicPayment <= 0) { // LCOV_EXCL_START JLOG(j_.warn()) - << "Computed loan properties are invalid. Does not compute."; + << "Computed loan properties are invalid. Does not compute." + << " Management fee: " << properties.loanState.managementFeeDue + << ". Total Value: " << properties.loanState.valueOutstanding + << ". PeriodicPayment: " << properties.periodicPayment; return tecINTERNAL; // LCOV_EXCL_STOP } - LoanState const state = constructLoanState( - properties.totalValueOutstanding, - principalRequested, - properties.managementFeeOwedToBroker); - auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{}); auto const loanAssetsToBorrower = principalRequested - originationFee; @@ -534,12 +554,12 @@ LoanSet::doApply() // ignore tecDUPLICATE. That means the holding already exists, // and is fine here return ter; - - if (auto const ter = requireAuth( - view, vaultAsset, brokerOwner, AuthType::StrongAuth)) - return ter; } + if (auto const ter = + requireAuth(view, vaultAsset, brokerOwner, AuthType::StrongAuth)) + return ter; + if (auto const ter = accountSendMulti( view, vaultPseudo, @@ -588,9 +608,10 @@ LoanSet::doApply() // Set dynamic / computed fields to their initial values loan->at(sfPrincipalOutstanding) = principalRequested; loan->at(sfPeriodicPayment) = properties.periodicPayment; - loan->at(sfTotalValueOutstanding) = properties.totalValueOutstanding; - loan->at(sfManagementFeeOutstanding) = properties.managementFeeOwedToBroker; - loan->at(sfPreviousPaymentDate) = 0; + loan->at(sfTotalValueOutstanding) = properties.loanState.valueOutstanding; + loan->at(sfManagementFeeOutstanding) = + properties.loanState.managementFeeDue; + loan->at(sfPreviousPaymentDueDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; view.insert(loan); @@ -624,6 +645,10 @@ LoanSet::doApply() if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode)) return ter; + associateAsset(*vaultSle, vaultAsset); + associateAsset(*brokerSle, vaultAsset); + associateAsset(*loan, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index a3dcc5730c..ef52108210 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1152,6 +1152,10 @@ Transactor::operator()() { JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID(); + // These global updates really should have been for every Transaction + // step: preflight, preclaim, and doApply. And even calculateBaseFee. See + // with_txn_type(). + // // raii classes for the current ledger rules. // fixUniversalNumber predate the rulesGuard and should be replaced. NumberSO stNumberSO{view().rules().enabled(fixUniversalNumber)}; @@ -1168,7 +1172,7 @@ Transactor::operator()() { // LCOV_EXCL_START JLOG(j_.fatal()) << "Transaction serdes mismatch"; - JLOG(j_.info()) << to_string(ctx_.tx.getJson(JsonOptions::none)); + JLOG(j_.fatal()) << ctx_.tx.getJson(JsonOptions::none); JLOG(j_.fatal()) << s2.getJson(JsonOptions::none); UNREACHABLE( "xrpl::Transactor::operator() : transaction serdes mismatch"); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 2552e8c1ff..dbdf4440ec 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -457,6 +458,8 @@ VaultClawback::doApply() } } + associateAsset(*vault, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 893a1108fa..402d877a00 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -230,6 +231,8 @@ VaultCreate::doApply() return err; } + associateAsset(*vault, asset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp index 756e7b94e6..9b63c7766b 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.cpp +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -85,6 +86,7 @@ VaultDelete::doApply() // Destroy the asset holding. auto asset = vault->at(sfAsset); + if (auto ter = removeEmptyHolding(view(), vault->at(sfAccount), asset, j_); !isTesSuccess(ter)) return ter; @@ -205,6 +207,8 @@ VaultDelete::doApply() // Destroy the vault. view().erase(vault); + associateAsset(*vault, asset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index d9471a38f5..02ef4afad1 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -115,16 +116,14 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) !isTesSuccess(ter)) return ter; - // Asset issuer does not have any balance, they can just create funds by - // depositing in the vault. - if ((vaultAsset.native() || vaultAsset.getIssuer() != account) && - accountHolds( + if (accountHolds( ctx.view, account, vaultAsset, FreezeHandling::fhZERO_IF_FROZEN, AuthHandling::ahZERO_IF_UNAUTHORIZED, - ctx.j) < assets) + ctx.j, + SpendableHandling::shFULL_BALANCE) < assets) return tecINSUFFICIENT_FUNDS; return tesSUCCESS; @@ -136,6 +135,7 @@ VaultDeposit::doApply() auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE + auto const vaultAsset = vault->at(sfAsset); auto const amount = ctx_.tx[sfAmount]; // Make sure the depositor can hold shares. @@ -284,6 +284,8 @@ VaultDeposit::doApply() !isTesSuccess(ter)) return ter; + associateAsset(*vault, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 648ac12c3d..13c8ad5db8 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -128,6 +129,8 @@ VaultSet::doApply() if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE + auto const vaultAsset = vault->at(sfAsset); + auto const mptIssuanceID = (*vault)[sfShareMPTID]; auto const sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID)); if (!sleIssuance) @@ -172,6 +175,8 @@ VaultSet::doApply() // to verify the operation. view().update(vault); + associateAsset(*vault, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index f8b7a1a739..9a4334e435 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -115,6 +116,7 @@ VaultWithdraw::doApply() auto const amount = ctx_.tx[sfAmount]; Asset const vaultAsset = vault->at(sfAsset); + MPTIssue const share{mptIssuanceID}; STAmount sharesRedeemed = {share}; STAmount assetsWithdrawn; @@ -239,6 +241,8 @@ VaultWithdraw::doApply() auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); + associateAsset(*vault, vaultAsset); + return doWithdraw( view(), ctx_.tx, diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index e0bd9d0d2d..0fae1a15e0 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -34,8 +34,38 @@ struct UnknownTxnType : std::exception // throw an "UnknownTxnType" exception on error template auto -with_txn_type(TxType txnType, F&& f) +with_txn_type(Rules const& rules, TxType txnType, F&& f) { + // These global updates really should have been for every Transaction + // step: preflight, preclaim, calculateBaseFee, and doApply. Unfortunately, + // they were only included in doApply (via Transactor::operator()). That may + // have been sufficient when the changes were only related to operations + // that mutated data, but some features will now change how they read data, + // so these need to be more global. + // + // To prevent unintentional side effects on existing checks, they will be + // set for every operation only once SingleAssetVault (or later + // LendingProtocol) are enabled. + // + // See also Transactor::operator(). + // + std::optional stNumberSO; + std::optional rulesGuard; + std::optional mantissaScaleGuard; + if (rules.enabled(featureSingleAssetVault) || + rules.enabled(featureLendingProtocol)) + { + // raii classes for the current ledger rules. + // fixUniversalNumber predates the rulesGuard and should be replaced. + stNumberSO.emplace(rules.enabled(fixUniversalNumber)); + rulesGuard.emplace(rules); + } + else + { + // Without those features enabled, always use the old number rules. + mantissaScaleGuard.emplace(MantissaRange::small); + } + switch (txnType) { #pragma push_macro("TRANSACTION") @@ -99,7 +129,7 @@ invoke_preflight(PreflightContext const& ctx) { try { - return with_txn_type(ctx.tx.getTxnType(), [&]() { + return with_txn_type(ctx.rules, ctx.tx.getTxnType(), [&]() { auto const tec = Transactor::invokePreflight(ctx); return std::make_pair( tec, @@ -126,50 +156,51 @@ invoke_preclaim(PreclaimContext const& ctx) { // use name hiding to accomplish compile-time polymorphism of static // class functions for Transactor and derived classes. - return with_txn_type(ctx.tx.getTxnType(), [&]() -> TER { - // preclaim functionality is divided into two sections: - // 1. Up to and including the signature check: returns NotTEC. - // All transaction checks before and including checkSign - // MUST return NotTEC, or something more restrictive. - // Allowing tec results in these steps risks theft or - // destruction of funds, as a fee will be charged before the - // signature is checked. - // 2. After the signature check: returns TER. + return with_txn_type( + ctx.view.rules(), ctx.tx.getTxnType(), [&]() -> TER { + // preclaim functionality is divided into two sections: + // 1. Up to and including the signature check: returns NotTEC. + // All transaction checks before and including checkSign + // MUST return NotTEC, or something more restrictive. + // Allowing tec results in these steps risks theft or + // destruction of funds, as a fee will be charged before the + // signature is checked. + // 2. After the signature check: returns TER. - // If the transactor requires a valid account and the - // transaction doesn't list one, preflight will have already - // a flagged a failure. - auto const id = ctx.tx.getAccountID(sfAccount); + // If the transactor requires a valid account and the + // transaction doesn't list one, preflight will have already + // a flagged a failure. + auto const id = ctx.tx.getAccountID(sfAccount); - if (id != beast::zero) - { - if (NotTEC const preSigResult = [&]() -> NotTEC { - if (NotTEC const result = - T::checkSeqProxy(ctx.view, ctx.tx, ctx.j)) - return result; + if (id != beast::zero) + { + if (NotTEC const preSigResult = [&]() -> NotTEC { + if (NotTEC const result = + T::checkSeqProxy(ctx.view, ctx.tx, ctx.j)) + return result; - if (NotTEC const result = - T::checkPriorTxAndLastLedger(ctx)) - return result; + if (NotTEC const result = + T::checkPriorTxAndLastLedger(ctx)) + return result; - if (NotTEC const result = - T::checkPermission(ctx.view, ctx.tx)) - return result; + if (NotTEC const result = + T::checkPermission(ctx.view, ctx.tx)) + return result; - if (NotTEC const result = T::checkSign(ctx)) - return result; + if (NotTEC const result = T::checkSign(ctx)) + return result; - return tesSUCCESS; - }()) - return preSigResult; + return tesSUCCESS; + }()) + return preSigResult; - if (TER const result = - T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx))) - return result; - } + if (TER const result = T::checkFee( + ctx, calculateBaseFee(ctx.view, ctx.tx))) + return result; + } - return T::preclaim(ctx); - }); + return T::preclaim(ctx); + }); } catch (UnknownTxnType const& e) { @@ -204,7 +235,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) { try { - return with_txn_type(tx.getTxnType(), [&]() { + return with_txn_type(view.rules(), tx.getTxnType(), [&]() { return T::calculateBaseFee(view, tx); }); } @@ -263,10 +294,11 @@ invoke_apply(ApplyContext& ctx) { try { - return with_txn_type(ctx.tx.getTxnType(), [&]() { - T p(ctx); - return p(); - }); + return with_txn_type( + ctx.view().rules(), ctx.tx.getTxnType(), [&]() { + T p(ctx); + return p(); + }); } catch (UnknownTxnType const& e) { diff --git a/src/xrpld/app/wasm/HostFunc.h b/src/xrpld/app/wasm/HostFunc.h index e59507757c..0ee4a615ce 100644 --- a/src/xrpld/app/wasm/HostFunc.h +++ b/src/xrpld/app/wasm/HostFunc.h @@ -44,6 +44,8 @@ HfErrorToInt(HostFunctionError e) return static_cast(e); } +namespace wasm_float { + std::string floatToString(Slice const& data); @@ -80,6 +82,8 @@ floatPowerImpl(Slice const& x, int32_t n, int32_t mode); Expected floatLogImpl(Slice const& x, int32_t mode); +} // namespace wasm_float + struct HostFunctions { beast::Journal j_; diff --git a/src/xrpld/app/wasm/HostFuncImpl.h b/src/xrpld/app/wasm/HostFuncImpl.h index fccea11252..26c55328db 100644 --- a/src/xrpld/app/wasm/HostFuncImpl.h +++ b/src/xrpld/app/wasm/HostFuncImpl.h @@ -41,6 +41,25 @@ class WasmHostFunctionsImpl : public HostFunctions return cacheIdx; } + template + void + log(std::string_view const& msg, F&& dataFn) + { +#ifdef DEBUG_OUTPUT + auto& j = std::cerr; +#else + if (!getJournal().active(beast::severities::kTrace)) + return; + auto j = getJournal().trace(); +#endif + j << "WasmTrace[" << to_short_string(leKey.key) << "]: " << msg << " " + << dataFn(); + +#ifdef DEBUG_OUTPUT + j << std::endl; +#endif + } + public: WasmHostFunctionsImpl(ApplyContext& ct, Keylet const& leKey) : HostFunctions(ct.journal), ctx(ct), leKey(leKey) @@ -271,4 +290,14 @@ public: floatLog(Slice const& x, int32_t mode) override; }; +namespace wasm_float { + +// The range for the mantissa and exponent when normalized +static std::int64_t constexpr minMantissa = 1'000'000'000'000'000ll; +static std::int64_t constexpr maxMantissa = (1ull << 54) - 1; +static int constexpr minExponent = -96; +static int constexpr maxExponent = 80; + +} // namespace wasm_float + } // namespace xrpl diff --git a/src/xrpld/app/wasm/WasmVM.h b/src/xrpld/app/wasm/WasmVM.h index c9c8d9c8e0..f14c475dc5 100644 --- a/src/xrpld/app/wasm/WasmVM.h +++ b/src/xrpld/app/wasm/WasmVM.h @@ -45,8 +45,8 @@ public: run(Bytes const& wasmCode, std::string_view funcName = {}, std::vector const& params = {}, - ImportVec const& imports = {}, - HostFunctions* hfs = nullptr, + std::shared_ptr const& imports = {}, + std::shared_ptr const& hfs = {}, int64_t gasLimit = -1, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); @@ -55,8 +55,8 @@ public: Bytes const& wasmCode, std::string_view funcName, std::vector const& params = {}, - ImportVec const& imports = {}, - HostFunctions* hfs = nullptr, + std::shared_ptr const& imports = {}, + std::shared_ptr const& hfs = {}, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); // Host functions helper functionality @@ -69,13 +69,13 @@ public: //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -ImportVec +std::shared_ptr createWasmImport(HostFunctions& hfs); Expected runEscrowWasm( Bytes const& wasmCode, - HostFunctions& hfs, + std::shared_ptr const& hfs, std::string_view funcName = ESCROW_FUNCTION_NAME, std::vector const& params = {}, int64_t gasLimit = -1); @@ -83,7 +83,7 @@ runEscrowWasm( NotTEC preflightEscrowWasm( Bytes const& wasmCode, - HostFunctions& hfs, + std::shared_ptr const& hfs, std::string_view funcName = ESCROW_FUNCTION_NAME, std::vector const& params = {}); diff --git a/src/xrpld/app/wasm/WasmiVM.h b/src/xrpld/app/wasm/WasmiVM.h index c39b577c46..d7e52d6a0e 100644 --- a/src/xrpld/app/wasm/WasmiVM.h +++ b/src/xrpld/app/wasm/WasmiVM.h @@ -108,6 +108,7 @@ struct InstanceWrapper { wasm_store_t* store_ = nullptr; WasmExternVec exports_; + mutable int memIdx_ = -1; InstancePtr instance_; beast::Journal j_ = beast::Journal(beast::Journal::getNullSink()); @@ -171,7 +172,7 @@ public: StorePtr& s, Bytes const& wasmBin, bool instantiate, - ImportVec const& imports, + std::shared_ptr const& imports, beast::Journal j); ~ModuleWrapper() = default; @@ -197,7 +198,7 @@ public: private: WasmExternVec - buildImports(StorePtr& s, ImportVec const& imports); + buildImports(StorePtr& s, std::shared_ptr const& imports); }; class WasmiEngine @@ -209,6 +210,10 @@ class WasmiEngine std::mutex m_; // 1 instance mutex + // to ensure lifetime during next executions with the same module + std::shared_ptr imports_; + std::shared_ptr hfs_; + public: WasmiEngine(); ~WasmiEngine() = default; @@ -220,8 +225,8 @@ public: run(Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, + std::shared_ptr const& imports, + std::shared_ptr const& hfs, int64_t gas, beast::Journal j); @@ -230,8 +235,8 @@ public: Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, + std::shared_ptr const& imports, + std::shared_ptr const& hfs, beast::Journal j); std::int64_t @@ -259,23 +264,16 @@ private: Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, int64_t gas); NotTEC checkHlp( Bytes const& wasmCode, std::string_view funcName, - std::vector const& params, - ImportVec const& imports); + std::vector const& params); int - addModule( - Bytes const& wasmCode, - bool instantiate, - int64_t gas, - ImportVec const& imports); + addModule(Bytes const& wasmCode, bool instantiate, int64_t gas); void clearModules(); diff --git a/src/xrpld/app/wasm/detail/HostFuncImpl.cpp b/src/xrpld/app/wasm/detail/HostFuncImpl.cpp index 7d2136f4f7..c0c514129f 100644 --- a/src/xrpld/app/wasm/detail/HostFuncImpl.cpp +++ b/src/xrpld/app/wasm/detail/HostFuncImpl.cpp @@ -1,407 +1,13 @@ -#include -#include #include #include #include -#ifdef _DEBUG -// #define DEBUG_OUTPUT 1 -#endif - namespace xrpl { -Expected -WasmHostFunctionsImpl::getLedgerSqn() -{ - auto seq = ctx.view().seq(); - if (seq > std::numeric_limits::max()) - return Unexpected(HostFunctionError::INTERNAL); // LCOV_EXCL_LINE - return static_cast(seq); -} - -Expected -WasmHostFunctionsImpl::getParentLedgerTime() -{ - auto time = ctx.view().parentCloseTime().time_since_epoch().count(); - if (time > std::numeric_limits::max()) - return Unexpected(HostFunctionError::INTERNAL); - return static_cast(time); -} - -Expected -WasmHostFunctionsImpl::getParentLedgerHash() -{ - return ctx.view().header().parentHash; -} - -Expected -WasmHostFunctionsImpl::getBaseFee() -{ - auto fee = ctx.view().fees().base.drops(); - if (fee > std::numeric_limits::max()) - return Unexpected(HostFunctionError::INTERNAL); - return static_cast(fee); -} - -Expected -WasmHostFunctionsImpl::isAmendmentEnabled(uint256 const& amendmentId) -{ - return ctx.view().rules().enabled(amendmentId); -} - -Expected -WasmHostFunctionsImpl::isAmendmentEnabled(std::string_view const& amendmentName) -{ - auto const& table = ctx.app.getAmendmentTable(); - auto const amendment = table.find(std::string(amendmentName)); - return ctx.view().rules().enabled(amendment); -} - -Expected -WasmHostFunctionsImpl::cacheLedgerObj(uint256 const& objId, int32_t cacheIdx) -{ - auto const& keylet = keylet::unchecked(objId); - if (cacheIdx < 0 || cacheIdx > MAX_CACHE) - return Unexpected(HostFunctionError::SLOT_OUT_RANGE); - - if (cacheIdx == 0) - { - for (cacheIdx = 0; cacheIdx < MAX_CACHE; ++cacheIdx) - if (!cache[cacheIdx]) - break; - } - else - { - cacheIdx--; // convert to 0-based index - } - - if (cacheIdx >= MAX_CACHE) - return Unexpected(HostFunctionError::SLOTS_FULL); - - cache[cacheIdx] = ctx.view().read(keylet); - if (!cache[cacheIdx]) - return Unexpected(HostFunctionError::LEDGER_OBJ_NOT_FOUND); - return cacheIdx + 1; // return 1-based index -} - -static Expected -getAnyFieldData(STBase const* obj) -{ - // auto const& fname = obj.getFName(); - if (!obj) - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - - auto const stype = obj->getSType(); - switch (stype) - { - // LCOV_EXCL_START - case STI_UNKNOWN: - case STI_NOTPRESENT: - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - break; - // LCOV_EXCL_STOP - case STI_OBJECT: - case STI_ARRAY: - return Unexpected(HostFunctionError::NOT_LEAF_FIELD); - break; - case STI_ACCOUNT: { - auto const* account(static_cast(obj)); - auto const& data = account->value(); - return Bytes{data.begin(), data.end()}; - } - break; - case STI_AMOUNT: - // will be processed by serializer - break; - case STI_ISSUE: { - auto const* issue(static_cast(obj)); - Asset const& asset(issue->value()); - // XRP and IOU will be processed by serializer - if (asset.holds()) - { - // MPT - auto const& mptIssue = asset.get(); - auto const& mptID = mptIssue.getMptID(); - return Bytes{mptID.cbegin(), mptID.cend()}; - } - } - break; - case STI_VL: { - auto const* vl(static_cast(obj)); - auto const& data = vl->value(); - return Bytes{data.begin(), data.end()}; - } - break; - case STI_UINT16: { - auto const& num(static_cast const*>(obj)); - std::uint16_t const data = num->value(); - auto const* b = reinterpret_cast(&data); - auto const* e = reinterpret_cast(&data + 1); - return Bytes{b, e}; - } - case STI_UINT32: { - auto const* num(static_cast const*>(obj)); - std::uint32_t const data = num->value(); - auto const* b = reinterpret_cast(&data); - auto const* e = reinterpret_cast(&data + 1); - return Bytes{b, e}; - } - break; - default: - break; // default to serializer - } - - Serializer msg; - obj->add(msg); - auto const data = msg.getData(); - - return data; -} - -Expected -WasmHostFunctionsImpl::getTxField(SField const& fname) -{ - return getAnyFieldData(ctx.tx.peekAtPField(fname)); -} - -Expected -WasmHostFunctionsImpl::getCurrentLedgerObjField(SField const& fname) -{ - auto const sle = getCurrentLedgerObj(); - if (!sle.has_value()) - return Unexpected(sle.error()); - return getAnyFieldData(sle.value()->peekAtPField(fname)); -} - -Expected -WasmHostFunctionsImpl::getLedgerObjField(int32_t cacheIdx, SField const& fname) -{ - auto const normalizedIdx = normalizeCacheIndex(cacheIdx); - if (!normalizedIdx.has_value()) - return Unexpected(normalizedIdx.error()); - return getAnyFieldData(cache[normalizedIdx.value()]->peekAtPField(fname)); -} - -static inline bool -noField(STBase const* field) -{ - return !field || (STI_NOTPRESENT == field->getSType()) || - (STI_UNKNOWN == field->getSType()); -} - -static Expected -locateField(STObject const& obj, Slice const& locator) -{ - if (locator.empty() || (locator.size() & 3)) // must be multiple of 4 - return Unexpected(HostFunctionError::LOCATOR_MALFORMED); - - int32_t const* locPtr = reinterpret_cast(locator.data()); - int32_t const locSize = locator.size() / 4; - STBase const* field = nullptr; - auto const& knownSFields = SField::getKnownCodeToField(); - - { - int32_t const sfieldCode = locPtr[0]; - auto const it = knownSFields.find(sfieldCode); - if (it == knownSFields.end()) - return Unexpected(HostFunctionError::INVALID_FIELD); - - auto const& fname(*it->second); - field = obj.peekAtPField(fname); - if (noField(field)) - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - } - - for (int i = 1; i < locSize; ++i) - { - int32_t const sfieldCode = locPtr[i]; - - if (STI_ARRAY == field->getSType()) - { - auto const* arr = static_cast(field); - if (sfieldCode >= arr->size()) - return Unexpected(HostFunctionError::INDEX_OUT_OF_BOUNDS); - field = &(arr->operator[](sfieldCode)); - } - else if (STI_OBJECT == field->getSType()) - { - auto const* o = static_cast(field); - - auto const it = knownSFields.find(sfieldCode); - if (it == knownSFields.end()) - return Unexpected(HostFunctionError::INVALID_FIELD); - - auto const& fname(*it->second); - field = o->peekAtPField(fname); - } - else // simple field must be the last one - { - return Unexpected(HostFunctionError::LOCATOR_MALFORMED); - } - - if (noField(field)) - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - } - - return field; -} - -Expected -WasmHostFunctionsImpl::getTxNestedField(Slice const& locator) -{ - auto const r = locateField(ctx.tx, locator); - if (!r) - return Unexpected(r.error()); - - return getAnyFieldData(r.value()); -} - -Expected -WasmHostFunctionsImpl::getCurrentLedgerObjNestedField(Slice const& locator) -{ - auto const sle = getCurrentLedgerObj(); - if (!sle.has_value()) - return Unexpected(sle.error()); - - auto const r = locateField(*sle.value(), locator); - if (!r) - return Unexpected(r.error()); - - return getAnyFieldData(r.value()); -} - -Expected -WasmHostFunctionsImpl::getLedgerObjNestedField( - int32_t cacheIdx, - Slice const& locator) -{ - auto const normalizedIdx = normalizeCacheIndex(cacheIdx); - if (!normalizedIdx.has_value()) - return Unexpected(normalizedIdx.error()); - - auto const r = locateField(*cache[normalizedIdx.value()], locator); - if (!r) - return Unexpected(r.error()); - - return getAnyFieldData(r.value()); -} - -Expected -WasmHostFunctionsImpl::getTxArrayLen(SField const& fname) -{ - if (fname.fieldType != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); - - auto const* field = ctx.tx.peekAtPField(fname); - if (noField(field)) - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - - if (field->getSType() != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); // LCOV_EXCL_LINE - int32_t const sz = static_cast(field)->size(); - - return sz; -} - -Expected -WasmHostFunctionsImpl::getCurrentLedgerObjArrayLen(SField const& fname) -{ - if (fname.fieldType != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); - - auto const sle = getCurrentLedgerObj(); - if (!sle.has_value()) - return Unexpected(sle.error()); - - auto const* field = sle.value()->peekAtPField(fname); - if (noField(field)) - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - - if (field->getSType() != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); // LCOV_EXCL_LINE - int32_t const sz = static_cast(field)->size(); - - return sz; -} - -Expected -WasmHostFunctionsImpl::getLedgerObjArrayLen( - int32_t cacheIdx, - SField const& fname) -{ - if (fname.fieldType != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); - - auto const normalizedIdx = normalizeCacheIndex(cacheIdx); - if (!normalizedIdx.has_value()) - return Unexpected(normalizedIdx.error()); - - auto const* field = cache[normalizedIdx.value()]->peekAtPField(fname); - if (noField(field)) - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - - if (field->getSType() != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); // LCOV_EXCL_LINE - - int32_t const sz = static_cast(field)->size(); - - return sz; -} - -Expected -WasmHostFunctionsImpl::getTxNestedArrayLen(Slice const& locator) -{ - auto const r = locateField(ctx.tx, locator); - if (!r) - return Unexpected(r.error()); - - auto const* field = r.value(); - if (field->getSType() != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); - int32_t const sz = static_cast(field)->size(); - - return sz; -} - -Expected -WasmHostFunctionsImpl::getCurrentLedgerObjNestedArrayLen(Slice const& locator) -{ - auto const sle = getCurrentLedgerObj(); - if (!sle.has_value()) - return Unexpected(sle.error()); - auto const r = locateField(*sle.value(), locator); - if (!r) - return Unexpected(r.error()); - - auto const* field = r.value(); - if (field->getSType() != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); - int32_t const sz = static_cast(field)->size(); - - return sz; -} - -Expected -WasmHostFunctionsImpl::getLedgerObjNestedArrayLen( - int32_t cacheIdx, - Slice const& locator) -{ - auto const normalizedIdx = normalizeCacheIndex(cacheIdx); - if (!normalizedIdx.has_value()) - return Unexpected(normalizedIdx.error()); - - auto const r = locateField(*cache[normalizedIdx.value()], locator); - if (!r) - return Unexpected(r.error()); - - auto const* field = r.value(); - if (field->getSType() != STI_ARRAY) - return Unexpected(HostFunctionError::NO_ARRAY); - int32_t const sz = static_cast(field)->size(); - - return sz; -} +// ========================================================= +// SECTION: WRITE FUNCTION +// ========================================================= Expected WasmHostFunctionsImpl::updateData(Slice const& data) @@ -414,6 +20,10 @@ WasmHostFunctionsImpl::updateData(Slice const& data) return data_->size(); } +// ========================================================= +// SECTION: UTILS +// ========================================================= + Expected WasmHostFunctionsImpl::checkSignature( Slice const& message, @@ -434,872 +44,4 @@ WasmHostFunctionsImpl::computeSha512HalfHash(Slice const& data) return hash; } -Expected -WasmHostFunctionsImpl::accountKeylet(AccountID const& account) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::account(account); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::ammKeylet(Asset const& issue1, Asset const& issue2) -{ - if (issue1 == issue2) - return Unexpected(HostFunctionError::INVALID_PARAMS); - - // note: this should be removed with the MPT DEX amendment - if (issue1.holds() || issue2.holds()) - return Unexpected(HostFunctionError::INVALID_PARAMS); - - auto const keylet = keylet::amm(issue1, issue2); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::checkKeylet(AccountID const& account, std::uint32_t seq) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::check(account, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::credentialKeylet( - AccountID const& subject, - AccountID const& issuer, - Slice const& credentialType) -{ - if (!subject || !issuer) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - - if (credentialType.empty() || - credentialType.size() > maxCredentialTypeLength) - return Unexpected(HostFunctionError::INVALID_PARAMS); - - auto const keylet = keylet::credential(subject, issuer, credentialType); - - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::didKeylet(AccountID const& account) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::did(account); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::delegateKeylet( - AccountID const& account, - AccountID const& authorize) -{ - if (!account || !authorize) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - if (account == authorize) - return Unexpected(HostFunctionError::INVALID_PARAMS); - auto const keylet = keylet::delegate(account, authorize); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::depositPreauthKeylet( - AccountID const& account, - AccountID const& authorize) -{ - if (!account || !authorize) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - if (account == authorize) - return Unexpected(HostFunctionError::INVALID_PARAMS); - auto const keylet = keylet::depositPreauth(account, authorize); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::escrowKeylet(AccountID const& account, std::uint32_t seq) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::escrow(account, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::lineKeylet( - AccountID const& account1, - AccountID const& account2, - Currency const& currency) -{ - if (!account1 || !account2) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - if (account1 == account2) - return Unexpected(HostFunctionError::INVALID_PARAMS); - if (currency.isZero()) - return Unexpected(HostFunctionError::INVALID_PARAMS); - - auto const keylet = keylet::line(account1, account2, currency); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::mptIssuanceKeylet( - AccountID const& issuer, - std::uint32_t seq) -{ - if (!issuer) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - - auto const keylet = keylet::mptIssuance(seq, issuer); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::mptokenKeylet( - MPTID const& mptid, - AccountID const& holder) -{ - if (!mptid) - return Unexpected(HostFunctionError::INVALID_PARAMS); - if (!holder) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - - auto const keylet = keylet::mptoken(mptid, holder); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::nftOfferKeylet( - AccountID const& account, - std::uint32_t seq) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::nftoffer(account, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::offerKeylet(AccountID const& account, std::uint32_t seq) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::offer(account, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::oracleKeylet( - AccountID const& account, - std::uint32_t documentId) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::oracle(account, documentId); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::paychanKeylet( - AccountID const& account, - AccountID const& destination, - std::uint32_t seq) -{ - if (!account || !destination) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - if (account == destination) - return Unexpected(HostFunctionError::INVALID_PARAMS); - auto const keylet = keylet::payChan(account, destination, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::permissionedDomainKeylet( - AccountID const& account, - std::uint32_t seq) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::permissionedDomain(account, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::signersKeylet(AccountID const& account) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::signers(account); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::ticketKeylet(AccountID const& account, std::uint32_t seq) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::ticket(account, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::vaultKeylet(AccountID const& account, std::uint32_t seq) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - auto const keylet = keylet::vault(account, seq); - return Bytes{keylet.key.begin(), keylet.key.end()}; -} - -Expected -WasmHostFunctionsImpl::getNFT(AccountID const& account, uint256 const& nftId) -{ - if (!account) - return Unexpected(HostFunctionError::INVALID_ACCOUNT); - - if (!nftId) - return Unexpected(HostFunctionError::INVALID_PARAMS); - - auto obj = nft::findToken(ctx.view(), account, nftId); - if (!obj) - return Unexpected(HostFunctionError::LEDGER_OBJ_NOT_FOUND); - - auto ouri = obj->at(~sfURI); - if (!ouri) - return Unexpected(HostFunctionError::FIELD_NOT_FOUND); - - Slice const s = ouri->value(); - return Bytes(s.begin(), s.end()); -} - -Expected -WasmHostFunctionsImpl::getNFTIssuer(uint256 const& nftId) -{ - auto const issuer = nft::getIssuer(nftId); - if (!issuer) - return Unexpected(HostFunctionError::INVALID_PARAMS); - - return Bytes{issuer.begin(), issuer.end()}; -} - -Expected -WasmHostFunctionsImpl::getNFTTaxon(uint256 const& nftId) -{ - return nft::toUInt32(nft::getTaxon(nftId)); -} - -Expected -WasmHostFunctionsImpl::getNFTFlags(uint256 const& nftId) -{ - return nft::getFlags(nftId); -} - -Expected -WasmHostFunctionsImpl::getNFTTransferFee(uint256 const& nftId) -{ - return nft::getTransferFee(nftId); -} - -Expected -WasmHostFunctionsImpl::getNFTSerial(uint256 const& nftId) -{ - return nft::getSerial(nftId); -} - -Expected -WasmHostFunctionsImpl::trace( - std::string_view const& msg, - Slice const& data, - bool asHex) -{ -#ifdef DEBUG_OUTPUT - auto j = getJournal().error(); -#else - auto j = getJournal().trace(); -#endif - if (!asHex) - { - j << "HF TRACE (" << leKey.key << "): " << msg << " " - << std::string_view( - reinterpret_cast(data.data()), data.size()); - } - else - { - std::string hex; - hex.reserve(data.size() * 2); - boost::algorithm::hex( - data.begin(), data.end(), std::back_inserter(hex)); - j << "HF DEV TRACE (" << leKey.key << "): " << msg << " " << hex; - } - - return msg.size() + data.size() * (asHex ? 2 : 1); -} - -Expected -WasmHostFunctionsImpl::traceNum(std::string_view const& msg, int64_t data) -{ -#ifdef DEBUG_OUTPUT - auto j = getJournal().error(); -#else - auto j = getJournal().trace(); -#endif - j << "HF TRACE NUM(" << leKey.key << "): " << msg << " " << data; - return msg.size() + sizeof(data); -} - -Expected -WasmHostFunctionsImpl::traceAccount( - std::string_view const& msg, - AccountID const& account) -{ -#ifdef DEBUG_OUTPUT - auto j = getJournal().error(); -#else - auto j = getJournal().trace(); -#endif - - auto const accountStr = toBase58(account); - - j << "HF TRACE ACCOUNT(" << leKey.key << "): " << msg << " " << accountStr; - return msg.size() + accountStr.size(); -} - -Expected -WasmHostFunctionsImpl::traceFloat( - std::string_view const& msg, - Slice const& data) -{ -#ifdef DEBUG_OUTPUT - auto j = getJournal().error(); -#else - auto j = getJournal().trace(); -#endif - auto const s = floatToString(data); - j << "HF TRACE FLOAT(" << leKey.key << "): " << msg << " " << s; - return msg.size() + s.size(); -} - -Expected -WasmHostFunctionsImpl::traceAmount( - std::string_view const& msg, - STAmount const& amount) -{ -#ifdef DEBUG_OUTPUT - auto j = getJournal().error(); -#else - auto j = getJournal().trace(); -#endif - auto const amountStr = amount.getFullText(); - j << "HF TRACE AMOUNT(" << leKey.key << "): " << msg << " " << amountStr; - return msg.size() + amountStr.size(); -} - -Expected -WasmHostFunctionsImpl::floatFromInt(int64_t x, int32_t mode) -{ - return floatFromIntImpl(x, mode); -} - -Expected -WasmHostFunctionsImpl::floatFromUint(uint64_t x, int32_t mode) -{ - return floatFromUintImpl(x, mode); -} - -Expected -WasmHostFunctionsImpl::floatSet( - int64_t mantissa, - int32_t exponent, - int32_t mode) -{ - return floatSetImpl(mantissa, exponent, mode); -} - -Expected -WasmHostFunctionsImpl::floatCompare(Slice const& x, Slice const& y) -{ - return floatCompareImpl(x, y); -} - -Expected -WasmHostFunctionsImpl::floatAdd(Slice const& x, Slice const& y, int32_t mode) -{ - return floatAddImpl(x, y, mode); -} - -Expected -WasmHostFunctionsImpl::floatSubtract( - Slice const& x, - Slice const& y, - int32_t mode) -{ - return floatSubtractImpl(x, y, mode); -} - -Expected -WasmHostFunctionsImpl::floatMultiply( - Slice const& x, - Slice const& y, - int32_t mode) -{ - return floatMultiplyImpl(x, y, mode); -} - -Expected -WasmHostFunctionsImpl::floatDivide(Slice const& x, Slice const& y, int32_t mode) -{ - return floatDivideImpl(x, y, mode); -} - -Expected -WasmHostFunctionsImpl::floatRoot(Slice const& x, int32_t n, int32_t mode) -{ - return floatRootImpl(x, n, mode); -} - -Expected -WasmHostFunctionsImpl::floatPower(Slice const& x, int32_t n, int32_t mode) -{ - return floatPowerImpl(x, n, mode); -} - -Expected -WasmHostFunctionsImpl::floatLog(Slice const& x, int32_t mode) -{ - return floatLogImpl(x, mode); -} - -class Number2 : public Number -{ -protected: - static Bytes const FLOAT_NULL; - - bool good_; - -public: - Number2(Slice const& data) : Number(), good_(false) - { - if (data.size() != 8) - return; - - if (std::ranges::equal(FLOAT_NULL, data)) - { - good_ = true; - return; - } - - uint64_t const v = SerialIter(data).get64(); - if (!(v & STAmount::cIssuedCurrency)) - return; - - int64_t const neg = (v & STAmount::cPositive) ? 1 : -1; - int32_t const e = static_cast((v >> (64 - 10)) & 0xFFull); - if (e < 1 || e > 177) - return; - - int64_t const m = neg * static_cast(v & ((1ull << 54) - 1)); - if (!m) - return; - - Number x(m, e + IOUAmount::minExponent - 1); - *static_cast(this) = x; - good_ = true; - } - - Number2() : Number(), good_(true) - { - } - - Number2(int64_t x) : Number(x), good_(true) - { - } - - Number2(uint64_t x) : Number(0), good_(false) - { - using mtype = std::invoke_result_t; - if (x <= std::numeric_limits::max()) - *this = Number(x); - else - *this = Number(x / 10, 1) + Number(x % 10); - - good_ = true; - } - - Number2(int64_t mantissa, int32_t exponent) - : Number(mantissa, exponent), good_(true) - { - } - - Number2(Number const& n) : Number(n), good_(true) - { - } - - operator bool() const - { - return good_; - } - - Expected - toBytes() const - { - uint64_t v = mantissa() >= 0 ? STAmount::cPositive : 0; - v |= STAmount::cIssuedCurrency; - - uint64_t const absM = mantissa() >= 0 ? mantissa() : -mantissa(); - if (!absM) - { - using etype = - std::invoke_result_t; - if (exponent() != std::numeric_limits::lowest()) - { - return Unexpected( - HostFunctionError:: - FLOAT_COMPUTATION_ERROR); // LCOV_EXCL_LINE - } - return FLOAT_NULL; - } - else if (absM > ((1ull << 54) - 1)) - { - return Unexpected( - HostFunctionError::FLOAT_COMPUTATION_ERROR); // LCOV_EXCL_LINE - } - else if (exponent() > IOUAmount::maxExponent) - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - else if (exponent() < IOUAmount::minExponent) - return FLOAT_NULL; - - int const e = exponent() - IOUAmount::minExponent + 1; //+97 - v |= absM; - v |= ((uint64_t)e) << 54; - - Serializer msg; - msg.add64(v); - auto const data = msg.getData(); - -#ifdef DEBUG_OUTPUT - std::cout << "m: " << std::setw(20) << mantissa() - << ", e: " << std::setw(12) << exponent() << ", hex: "; - std::cout << std::hex << std::uppercase << std::setfill('0'); - for (auto const& c : data) - std::cout << std::setw(2) << (unsigned)c << " "; - std::cout << std::dec << std::setfill(' ') << std::endl; -#endif - - return data; - } -}; - -Bytes const Number2::FLOAT_NULL = - {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -struct SetRound -{ - Number::rounding_mode oldMode_; - bool good_; - - SetRound(int32_t mode) : oldMode_(Number::getround()), good_(false) - { - if (mode < Number::rounding_mode::to_nearest || - mode > Number::rounding_mode::upward) - return; - - Number::setround(static_cast(mode)); - good_ = true; - } - - ~SetRound() - { - Number::setround(oldMode_); - } - - operator bool() const - { - return good_; - } -}; - -std::string -floatToString(Slice const& data) -{ - Number2 const num(data); - if (!num) - { - std::string hex; - hex.reserve(data.size() * 2); - boost::algorithm::hex( - data.begin(), data.end(), std::back_inserter(hex)); - return "Invalid data: " + hex; - } - - auto const s = to_string(num); - return s; -} - -Expected -floatFromIntImpl(int64_t x, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 num(x); - return num.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatFromUintImpl(uint64_t x, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 num(x); - return num.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatSetImpl(int64_t mantissa, int32_t exponent, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 num(mantissa, exponent); - return num.toBytes(); - } - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); -} - -Expected -floatCompareImpl(Slice const& x, Slice const& y) -{ - try - { - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 yy(y); - if (!yy) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - return xx < yy ? 2 : (xx == yy ? 0 : 1); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatAddImpl(Slice const& x, Slice const& y, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 yy(y); - if (!yy) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 res = xx + yy; - - return res.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatSubtractImpl(Slice const& x, Slice const& y, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 yy(y); - if (!yy) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 res = xx - yy; - - return res.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatMultiplyImpl(Slice const& x, Slice const& y, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 yy(y); - if (!yy) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 res = xx * yy; - - return res.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatDivideImpl(Slice const& x, Slice const& y, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 yy(y); - if (!yy) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - Number2 res = xx / yy; - - return res.toBytes(); - } - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); -} - -Expected -floatRootImpl(Slice const& x, int32_t n, int32_t mode) -{ - try - { - if (n < 1) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 res(root(xx, n)); - - return res.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatPowerImpl(Slice const& x, int32_t n, int32_t mode) -{ - try - { - if ((n < 0) || (n > IOUAmount::maxExponent)) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - if (xx == Number() && !n) - return Unexpected(HostFunctionError::INVALID_PARAMS); - - Number2 res(power(xx, n, 1)); - - return res.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - -Expected -floatLogImpl(Slice const& x, int32_t mode) -{ - try - { - SetRound rm(mode); - if (!rm) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 xx(x); - if (!xx) - return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); - - Number2 res(lg(xx)); - - return res.toBytes(); - } - // LCOV_EXCL_START - catch (...) - { - } - return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); - // LCOV_EXCL_STOP -} - } // namespace xrpl diff --git a/src/xrpld/app/wasm/detail/HostFuncImplFloat.cpp b/src/xrpld/app/wasm/detail/HostFuncImplFloat.cpp new file mode 100644 index 0000000000..e424e3cedf --- /dev/null +++ b/src/xrpld/app/wasm/detail/HostFuncImplFloat.cpp @@ -0,0 +1,550 @@ +#include + +#include +#include + +#ifdef _DEBUG +// #define DEBUG_OUTPUT 1 +#endif + +namespace xrpl { + +namespace wasm_float { + +namespace detail { + +class Number2 : public Number +{ +protected: + static Bytes const FLOAT_NULL; + + bool good_; + +public: + Number2(Slice const& data) : Number(), good_(false) + { + if (data.size() != 8) + return; + + if (std::ranges::equal(FLOAT_NULL, data)) + { + good_ = true; + return; + } + + uint64_t const v = SerialIter(data).get64(); + if (!(v & STAmount::cIssuedCurrency)) + return; + + int64_t const neg = (v & STAmount::cPositive) ? 1 : -1; + int32_t const e = static_cast((v >> (64 - 10)) & 0xFFull); + if (e < 1 || e > 177) + return; + + int64_t const m = neg * static_cast(v & ((1ull << 54) - 1)); + if (!m) + return; + + Number x(makeNumber(m, e + wasm_float::minExponent - 1)); + *static_cast(this) = x; + good_ = true; + } + + Number2() : Number(), good_(true) + { + } + + Number2(int64_t x) : Number(makeNumber(x, 0)), good_(true) + { + } + + Number2(uint64_t x) : Number(0), good_(false) + { + using mtype = std::invoke_result_t; + if (x <= std::numeric_limits::max()) + *this = makeNumber(x, 0); + else + *this = makeNumber(x / 10, 1); + good_ = true; + } + + Number2(int64_t mantissa, int32_t exponent) + : Number(makeNumber(mantissa, exponent)), good_(true) + { + } + + Number2(Number const& n) + : Number(makeNumber(n.mantissa(), n.exponent())), good_(true) + { + } + + static Number + makeNumber(int64_t mantissa, int32_t exponent) + { + if (mantissa < 0) + return Number(true, -mantissa, exponent, Number::normalized()); + return Number(false, mantissa, exponent, Number::normalized()); + } + + static Number + makeNumber(uint64_t mantissa, int32_t exponent) + { + return Number(false, mantissa, exponent, Number::normalized()); + } + + operator bool() const + { + return good_; + } + + Expected + toBytes() const + { + uint64_t v = mantissa() >= 0 ? STAmount::cPositive : 0; + v |= STAmount::cIssuedCurrency; + + uint64_t const absM = mantissa() >= 0 ? mantissa() : -mantissa(); + if (!absM) + { + using etype = + std::invoke_result_t; + if (exponent() != std::numeric_limits::lowest()) + { + return Unexpected( + HostFunctionError:: + FLOAT_COMPUTATION_ERROR); // LCOV_EXCL_LINE + } + return FLOAT_NULL; + } + else if (absM > wasm_float::maxMantissa) + { + return Unexpected( + HostFunctionError::FLOAT_COMPUTATION_ERROR); // LCOV_EXCL_LINE + } + else if (exponent() > wasm_float::maxExponent) + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + else if (exponent() < wasm_float::minExponent) + return FLOAT_NULL; + + int const e = exponent() - wasm_float::minExponent + 1; //+97 + v |= absM; + v |= ((uint64_t)e) << 54; + + Serializer msg; + msg.add64(v); + auto const data = msg.getData(); + +#ifdef DEBUG_OUTPUT + std::cout << "m: " << std::setw(20) << mantissa() + << ", e: " << std::setw(12) << exponent() << ", hex: "; + std::cout << std::hex << std::uppercase << std::setfill('0'); + for (auto const& c : data) + std::cout << std::setw(2) << (unsigned)c << " "; + std::cout << std::dec << std::setfill(' ') << std::endl; +#endif + + return data; + } +}; + +Bytes const Number2::FLOAT_NULL = + {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +struct FloatState +{ + Number::rounding_mode oldMode_; + MantissaRange::mantissa_scale oldScale_; + bool good_; + + FloatState(int32_t mode) + : oldMode_(Number::getround()) + , oldScale_(Number::getMantissaScale()) + , good_(false) + { + if (mode < Number::rounding_mode::to_nearest || + mode > Number::rounding_mode::upward) + return; + + Number::setround(static_cast(mode)); + Number::setMantissaScale(MantissaRange::mantissa_scale::small); + good_ = true; + } + + ~FloatState() + { + Number::setround(oldMode_); + Number::setMantissaScale(oldScale_); + } + + operator bool() const + { + return good_; + } +}; + +} // namespace detail + +std::string +floatToString(Slice const& data) +{ + detail::Number2 const num(data); + if (!num) + { + std::string hex; + hex.reserve(data.size() * 2); + boost::algorithm::hex( + data.begin(), data.end(), std::back_inserter(hex)); + return "Invalid data: " + hex; + } + + auto const s = to_string(num); + return s; +} + +Expected +floatFromIntImpl(int64_t x, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 num(x); + return num.toBytes(); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatFromUintImpl(uint64_t x, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 num(x); + auto r = num.toBytes(); + return r; + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatSetImpl(int64_t mantissa, int32_t exponent, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 num(mantissa, exponent); + return num.toBytes(); + } + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); +} + +Expected +floatCompareImpl(Slice const& x, Slice const& y) +{ + try + { + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 yy(y); + if (!yy) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + return xx < yy ? 2 : (xx == yy ? 0 : 1); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatAddImpl(Slice const& x, Slice const& y, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 yy(y); + if (!yy) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 res = xx + yy; + + return res.toBytes(); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatSubtractImpl(Slice const& x, Slice const& y, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 yy(y); + if (!yy) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 res = xx - yy; + + return res.toBytes(); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatMultiplyImpl(Slice const& x, Slice const& y, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 yy(y); + if (!yy) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 res = xx * yy; + + return res.toBytes(); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatDivideImpl(Slice const& x, Slice const& y, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 yy(y); + if (!yy) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + detail::Number2 res = xx / yy; + + return res.toBytes(); + } + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); +} + +Expected +floatRootImpl(Slice const& x, int32_t n, int32_t mode) +{ + try + { + if (n < 1) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 res(root(xx, n)); + + return res.toBytes(); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatPowerImpl(Slice const& x, int32_t n, int32_t mode) +{ + try + { + if ((n < 0) || (n > wasm_float::maxExponent)) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + if (xx == Number() && !n) + return Unexpected(HostFunctionError::INVALID_PARAMS); + + detail::Number2 res(power(xx, n, 1)); + + return res.toBytes(); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +Expected +floatLogImpl(Slice const& x, int32_t mode) +{ + try + { + detail::FloatState rm(mode); + if (!rm) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 xx(x); + if (!xx) + return Unexpected(HostFunctionError::FLOAT_INPUT_MALFORMED); + + detail::Number2 res(lg(xx)); + + return res.toBytes(); + } + // LCOV_EXCL_START + catch (...) + { + } + return Unexpected(HostFunctionError::FLOAT_COMPUTATION_ERROR); + // LCOV_EXCL_STOP +} + +} // namespace wasm_float + +// ========================================================= +// ACTUAL HOST FUNCTIONS +// ========================================================= + +Expected +WasmHostFunctionsImpl::floatFromInt(int64_t x, int32_t mode) +{ + return wasm_float::floatFromIntImpl(x, mode); +} + +Expected +WasmHostFunctionsImpl::floatFromUint(uint64_t x, int32_t mode) +{ + return wasm_float::floatFromUintImpl(x, mode); +} + +Expected +WasmHostFunctionsImpl::floatSet( + int64_t mantissa, + int32_t exponent, + int32_t mode) +{ + return wasm_float::floatSetImpl(mantissa, exponent, mode); +} + +Expected +WasmHostFunctionsImpl::floatCompare(Slice const& x, Slice const& y) +{ + return wasm_float::floatCompareImpl(x, y); +} + +Expected +WasmHostFunctionsImpl::floatAdd(Slice const& x, Slice const& y, int32_t mode) +{ + return wasm_float::floatAddImpl(x, y, mode); +} + +Expected +WasmHostFunctionsImpl::floatSubtract( + Slice const& x, + Slice const& y, + int32_t mode) +{ + return wasm_float::floatSubtractImpl(x, y, mode); +} + +Expected +WasmHostFunctionsImpl::floatMultiply( + Slice const& x, + Slice const& y, + int32_t mode) +{ + return wasm_float::floatMultiplyImpl(x, y, mode); +} + +Expected +WasmHostFunctionsImpl::floatDivide(Slice const& x, Slice const& y, int32_t mode) +{ + return wasm_float::floatDivideImpl(x, y, mode); +} + +Expected +WasmHostFunctionsImpl::floatRoot(Slice const& x, int32_t n, int32_t mode) +{ + return wasm_float::floatRootImpl(x, n, mode); +} + +Expected +WasmHostFunctionsImpl::floatPower(Slice const& x, int32_t n, int32_t mode) +{ + return wasm_float::floatPowerImpl(x, n, mode); +} + +Expected +WasmHostFunctionsImpl::floatLog(Slice const& x, int32_t mode) +{ + return wasm_float::floatLogImpl(x, mode); +} + +} // namespace xrpl diff --git a/src/xrpld/app/wasm/detail/HostFuncImplGetter.cpp b/src/xrpld/app/wasm/detail/HostFuncImplGetter.cpp new file mode 100644 index 0000000000..10f0d4a03e --- /dev/null +++ b/src/xrpld/app/wasm/detail/HostFuncImplGetter.cpp @@ -0,0 +1,395 @@ +#include + +#include +#include + +namespace xrpl { + +typedef std::variant FieldValue; + +namespace detail { + +static Expected +getAnyFieldData(STBase const* obj) +{ + if (!obj) + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + + auto const stype = obj->getSType(); + switch (stype) + { + // LCOV_EXCL_START + case STI_UNKNOWN: + case STI_NOTPRESENT: + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + break; + // LCOV_EXCL_STOP + case STI_OBJECT: + case STI_ARRAY: + case STI_VECTOR256: + return Unexpected(HostFunctionError::NOT_LEAF_FIELD); + break; + case STI_ACCOUNT: { + auto const* account(static_cast(obj)); + auto const& data = account->value(); + return Bytes{data.begin(), data.end()}; + } + break; + case STI_AMOUNT: + // will be processed by serializer + break; + case STI_ISSUE: { + auto const* issue(static_cast(obj)); + Asset const& asset(issue->value()); + // XRP and IOU will be processed by serializer + if (asset.holds()) + { + // MPT + auto const& mptIssue = asset.get(); + auto const& mptID = mptIssue.getMptID(); + return Bytes{mptID.cbegin(), mptID.cend()}; + } + } + break; + case STI_VL: { + auto const* vl(static_cast(obj)); + auto const& data = vl->value(); + return Bytes{data.begin(), data.end()}; + } + break; + case STI_UINT16: { + auto const& num(static_cast const*>(obj)); + std::uint16_t const data = num->value(); + auto const* b = reinterpret_cast(&data); + auto const* e = reinterpret_cast(&data + 1); + return Bytes{b, e}; + } + break; + case STI_UINT32: { + auto const* num(static_cast const*>(obj)); + std::uint32_t const data = num->value(); + auto const* b = reinterpret_cast(&data); + auto const* e = reinterpret_cast(&data + 1); + return Bytes{b, e}; + } + break; + case STI_UINT256: { + auto const* uint256Obj(static_cast(obj)); + auto const& data = uint256Obj->value(); + return Bytes{data.begin(), data.end()}; + } + break; + default: + break; // default to serializer + } + + Serializer msg; + obj->add(msg); + auto const data = msg.getData(); + + return data; +} + +static Expected +getAnyFieldData(FieldValue const& variantObj) +{ + if (STBase const* const* obj = std::get_if(&variantObj)) + { + return getAnyFieldData(*obj); + } + else if (uint256 const* const* u = std::get_if(&variantObj)) + { + return Bytes((*u)->begin(), (*u)->end()); + } + + return Unexpected(HostFunctionError::INTERNAL); // LCOV_EXCL_LINE +} + +static inline bool +noField(STBase const* field) +{ + return !field || (STI_NOTPRESENT == field->getSType()) || + (STI_UNKNOWN == field->getSType()); +} + +static Expected +locateField(STObject const& obj, Slice const& locator) +{ + if (locator.empty() || (locator.size() & 3)) // must be multiple of 4 + return Unexpected(HostFunctionError::LOCATOR_MALFORMED); + + int32_t locBuf[maxWasmParamLength / sizeof(int32_t)]; + int32_t const* locPtr = &locBuf[0]; + int32_t const locSize = locator.size() / sizeof(int32_t); + + uintptr_t p = reinterpret_cast(locator.data()); + if (p & (alignof(int32_t) - 1)) // unaligned + memcpy(&locBuf[0], locator.data(), locator.size()); + else + locPtr = reinterpret_cast(locator.data()); + + STBase const* field = nullptr; + auto const& knownSFields = SField::getKnownCodeToField(); + + { + int32_t const sfieldCode = locPtr[0]; + auto const it = knownSFields.find(sfieldCode); + if (it == knownSFields.end()) + return Unexpected(HostFunctionError::INVALID_FIELD); + + auto const& fname(*it->second); + field = obj.peekAtPField(fname); + if (noField(field)) + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + } + + for (int i = 1; i < locSize; ++i) + { + int32_t const sfieldCode = locPtr[i]; + + if (STI_ARRAY == field->getSType()) + { + auto const* arr = static_cast(field); + if (sfieldCode >= arr->size()) + return Unexpected(HostFunctionError::INDEX_OUT_OF_BOUNDS); + field = &(arr->operator[](sfieldCode)); + } + else if (STI_OBJECT == field->getSType()) + { + auto const* o = static_cast(field); + + auto const it = knownSFields.find(sfieldCode); + if (it == knownSFields.end()) + return Unexpected(HostFunctionError::INVALID_FIELD); + + auto const& fname(*it->second); + field = o->peekAtPField(fname); + } + else if (STI_VECTOR256 == field->getSType()) + { + auto const* v = static_cast(field); + if (sfieldCode >= v->size()) + return Unexpected(HostFunctionError::INDEX_OUT_OF_BOUNDS); + return FieldValue(&(v->operator[](sfieldCode))); + } + else // simple field must be the last one + { + return Unexpected(HostFunctionError::LOCATOR_MALFORMED); + } + + if (noField(field)) + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + } + + return FieldValue(field); +} + +static inline Expected +getArrayLen(FieldValue const& variantField) +{ + if (STBase const* const* field = std::get_if(&variantField)) + { + if ((*field)->getSType() == STI_VECTOR256) + return static_cast(*field)->size(); + if ((*field)->getSType() == STI_ARRAY) + return static_cast(*field)->size(); + } + // uint256 is not an array so that variant should still return NO_ARRAY + + return Unexpected(HostFunctionError::NO_ARRAY); // LCOV_EXCL_LINE +} + +} // namespace detail + +Expected +WasmHostFunctionsImpl::cacheLedgerObj(uint256 const& objId, int32_t cacheIdx) +{ + auto const& keylet = keylet::unchecked(objId); + if (cacheIdx < 0 || cacheIdx > MAX_CACHE) + return Unexpected(HostFunctionError::SLOT_OUT_RANGE); + + if (cacheIdx == 0) + { + for (cacheIdx = 0; cacheIdx < MAX_CACHE; ++cacheIdx) + if (!cache[cacheIdx]) + break; + } + else + { + cacheIdx--; // convert to 0-based index + } + + if (cacheIdx >= MAX_CACHE) + return Unexpected(HostFunctionError::SLOTS_FULL); + + cache[cacheIdx] = ctx.view().read(keylet); + if (!cache[cacheIdx]) + return Unexpected(HostFunctionError::LEDGER_OBJ_NOT_FOUND); + return cacheIdx + 1; // return 1-based index +} + +// Subsection: top level getters + +Expected +WasmHostFunctionsImpl::getTxField(SField const& fname) +{ + return detail::getAnyFieldData(ctx.tx.peekAtPField(fname)); +} + +Expected +WasmHostFunctionsImpl::getCurrentLedgerObjField(SField const& fname) +{ + auto const sle = getCurrentLedgerObj(); + if (!sle.has_value()) + return Unexpected(sle.error()); + return detail::getAnyFieldData(sle.value()->peekAtPField(fname)); +} + +Expected +WasmHostFunctionsImpl::getLedgerObjField(int32_t cacheIdx, SField const& fname) +{ + auto const normalizedIdx = normalizeCacheIndex(cacheIdx); + if (!normalizedIdx.has_value()) + return Unexpected(normalizedIdx.error()); + return detail::getAnyFieldData( + cache[normalizedIdx.value()]->peekAtPField(fname)); +} + +// Subsection: nested getters + +Expected +WasmHostFunctionsImpl::getTxNestedField(Slice const& locator) +{ + auto const r = detail::locateField(ctx.tx, locator); + if (!r) + return Unexpected(r.error()); + + return detail::getAnyFieldData(r.value()); +} + +Expected +WasmHostFunctionsImpl::getCurrentLedgerObjNestedField(Slice const& locator) +{ + auto const sle = getCurrentLedgerObj(); + if (!sle.has_value()) + return Unexpected(sle.error()); + + auto const r = detail::locateField(*sle.value(), locator); + if (!r) + return Unexpected(r.error()); + + return detail::getAnyFieldData(r.value()); +} + +Expected +WasmHostFunctionsImpl::getLedgerObjNestedField( + int32_t cacheIdx, + Slice const& locator) +{ + auto const normalizedIdx = normalizeCacheIndex(cacheIdx); + if (!normalizedIdx.has_value()) + return Unexpected(normalizedIdx.error()); + + auto const r = detail::locateField(*cache[normalizedIdx.value()], locator); + if (!r) + return Unexpected(r.error()); + + return detail::getAnyFieldData(r.value()); +} + +// Subsection: array length getters + +Expected +WasmHostFunctionsImpl::getTxArrayLen(SField const& fname) +{ + if (fname.fieldType != STI_ARRAY && fname.fieldType != STI_VECTOR256) + return Unexpected(HostFunctionError::NO_ARRAY); + + auto const* field = ctx.tx.peekAtPField(fname); + if (detail::noField(field)) + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + + return detail::getArrayLen(field); +} + +Expected +WasmHostFunctionsImpl::getCurrentLedgerObjArrayLen(SField const& fname) +{ + if (fname.fieldType != STI_ARRAY && fname.fieldType != STI_VECTOR256) + return Unexpected(HostFunctionError::NO_ARRAY); + + auto const sle = getCurrentLedgerObj(); + if (!sle.has_value()) + return Unexpected(sle.error()); + + auto const* field = sle.value()->peekAtPField(fname); + if (detail::noField(field)) + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + + return detail::getArrayLen(field); +} + +Expected +WasmHostFunctionsImpl::getLedgerObjArrayLen( + int32_t cacheIdx, + SField const& fname) +{ + if (fname.fieldType != STI_ARRAY && fname.fieldType != STI_VECTOR256) + return Unexpected(HostFunctionError::NO_ARRAY); + + auto const normalizedIdx = normalizeCacheIndex(cacheIdx); + if (!normalizedIdx.has_value()) + return Unexpected(normalizedIdx.error()); + + auto const* field = cache[normalizedIdx.value()]->peekAtPField(fname); + if (detail::noField(field)) + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + + return detail::getArrayLen(field); +} + +// Subsection: nested array length getters + +Expected +WasmHostFunctionsImpl::getTxNestedArrayLen(Slice const& locator) +{ + auto const r = detail::locateField(ctx.tx, locator); + if (!r) + return Unexpected(r.error()); + + auto const& field = r.value(); + return detail::getArrayLen(field); +} + +Expected +WasmHostFunctionsImpl::getCurrentLedgerObjNestedArrayLen(Slice const& locator) +{ + auto const sle = getCurrentLedgerObj(); + if (!sle.has_value()) + return Unexpected(sle.error()); + auto const r = detail::locateField(*sle.value(), locator); + if (!r) + return Unexpected(r.error()); + + auto const& field = r.value(); + return detail::getArrayLen(field); +} + +Expected +WasmHostFunctionsImpl::getLedgerObjNestedArrayLen( + int32_t cacheIdx, + Slice const& locator) +{ + auto const normalizedIdx = normalizeCacheIndex(cacheIdx); + if (!normalizedIdx.has_value()) + return Unexpected(normalizedIdx.error()); + + auto const r = detail::locateField(*cache[normalizedIdx.value()], locator); + if (!r) + return Unexpected(r.error()); + + auto const& field = r.value(); + return detail::getArrayLen(field); +} + +} // namespace xrpl diff --git a/src/xrpld/app/wasm/detail/HostFuncImplKeylet.cpp b/src/xrpld/app/wasm/detail/HostFuncImplKeylet.cpp new file mode 100644 index 0000000000..89ec35cd3b --- /dev/null +++ b/src/xrpld/app/wasm/detail/HostFuncImplKeylet.cpp @@ -0,0 +1,228 @@ +#include + +#include +#include + +namespace xrpl { + +Expected +WasmHostFunctionsImpl::accountKeylet(AccountID const& account) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::account(account); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::ammKeylet(Asset const& issue1, Asset const& issue2) +{ + if (issue1 == issue2) + return Unexpected(HostFunctionError::INVALID_PARAMS); + + // note: this should be removed with the MPT DEX amendment + if (issue1.holds() || issue2.holds()) + return Unexpected(HostFunctionError::INVALID_PARAMS); + + auto const keylet = keylet::amm(issue1, issue2); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::checkKeylet(AccountID const& account, std::uint32_t seq) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::check(account, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::credentialKeylet( + AccountID const& subject, + AccountID const& issuer, + Slice const& credentialType) +{ + if (!subject || !issuer) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + + if (credentialType.empty() || + credentialType.size() > maxCredentialTypeLength) + return Unexpected(HostFunctionError::INVALID_PARAMS); + + auto const keylet = keylet::credential(subject, issuer, credentialType); + + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::didKeylet(AccountID const& account) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::did(account); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::delegateKeylet( + AccountID const& account, + AccountID const& authorize) +{ + if (!account || !authorize) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + if (account == authorize) + return Unexpected(HostFunctionError::INVALID_PARAMS); + auto const keylet = keylet::delegate(account, authorize); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::depositPreauthKeylet( + AccountID const& account, + AccountID const& authorize) +{ + if (!account || !authorize) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + if (account == authorize) + return Unexpected(HostFunctionError::INVALID_PARAMS); + auto const keylet = keylet::depositPreauth(account, authorize); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::escrowKeylet(AccountID const& account, std::uint32_t seq) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::escrow(account, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::lineKeylet( + AccountID const& account1, + AccountID const& account2, + Currency const& currency) +{ + if (!account1 || !account2) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + if (account1 == account2) + return Unexpected(HostFunctionError::INVALID_PARAMS); + if (currency.isZero()) + return Unexpected(HostFunctionError::INVALID_PARAMS); + + auto const keylet = keylet::line(account1, account2, currency); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::mptIssuanceKeylet( + AccountID const& issuer, + std::uint32_t seq) +{ + if (!issuer) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + + auto const keylet = keylet::mptIssuance(seq, issuer); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::mptokenKeylet( + MPTID const& mptid, + AccountID const& holder) +{ + if (!mptid) + return Unexpected(HostFunctionError::INVALID_PARAMS); + if (!holder) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + + auto const keylet = keylet::mptoken(mptid, holder); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::nftOfferKeylet( + AccountID const& account, + std::uint32_t seq) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::nftoffer(account, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::offerKeylet(AccountID const& account, std::uint32_t seq) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::offer(account, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::oracleKeylet( + AccountID const& account, + std::uint32_t documentId) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::oracle(account, documentId); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::paychanKeylet( + AccountID const& account, + AccountID const& destination, + std::uint32_t seq) +{ + if (!account || !destination) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + if (account == destination) + return Unexpected(HostFunctionError::INVALID_PARAMS); + auto const keylet = keylet::payChan(account, destination, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::permissionedDomainKeylet( + AccountID const& account, + std::uint32_t seq) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::permissionedDomain(account, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::signersKeylet(AccountID const& account) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::signers(account); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::ticketKeylet(AccountID const& account, std::uint32_t seq) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::ticket(account, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +Expected +WasmHostFunctionsImpl::vaultKeylet(AccountID const& account, std::uint32_t seq) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + auto const keylet = keylet::vault(account, seq); + return Bytes{keylet.key.begin(), keylet.key.end()}; +} + +} // namespace xrpl diff --git a/src/xrpld/app/wasm/detail/HostFuncImplLedgerHeader.cpp b/src/xrpld/app/wasm/detail/HostFuncImplLedgerHeader.cpp new file mode 100644 index 0000000000..1889341ebe --- /dev/null +++ b/src/xrpld/app/wasm/detail/HostFuncImplLedgerHeader.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include + +namespace xrpl { + +// ========================================================= +// SECTION: LEDGER HEADER FUNCTIONS +// ========================================================= + +Expected +WasmHostFunctionsImpl::getLedgerSqn() +{ + auto seq = ctx.view().seq(); + if (seq > std::numeric_limits::max()) + return Unexpected(HostFunctionError::INTERNAL); // LCOV_EXCL_LINE + return static_cast(seq); +} + +Expected +WasmHostFunctionsImpl::getParentLedgerTime() +{ + auto time = ctx.view().parentCloseTime().time_since_epoch().count(); + if (time > std::numeric_limits::max()) + return Unexpected(HostFunctionError::INTERNAL); + return static_cast(time); +} + +Expected +WasmHostFunctionsImpl::getParentLedgerHash() +{ + return ctx.view().header().parentHash; +} + +Expected +WasmHostFunctionsImpl::getBaseFee() +{ + auto fee = ctx.view().fees().base.drops(); + if (fee > std::numeric_limits::max()) + return Unexpected(HostFunctionError::INTERNAL); + return static_cast(fee); +} + +Expected +WasmHostFunctionsImpl::isAmendmentEnabled(uint256 const& amendmentId) +{ + return ctx.view().rules().enabled(amendmentId); +} + +Expected +WasmHostFunctionsImpl::isAmendmentEnabled(std::string_view const& amendmentName) +{ + auto const& table = ctx.app.getAmendmentTable(); + auto const amendment = table.find(std::string(amendmentName)); + return ctx.view().rules().enabled(amendment); +} + +} // namespace xrpl diff --git a/src/xrpld/app/wasm/detail/HostFuncImplNFT.cpp b/src/xrpld/app/wasm/detail/HostFuncImplNFT.cpp new file mode 100644 index 0000000000..7c4ee00406 --- /dev/null +++ b/src/xrpld/app/wasm/detail/HostFuncImplNFT.cpp @@ -0,0 +1,68 @@ +#include +#include + +#include +#include + +namespace xrpl { + +// ========================================================= +// SECTION: NFT UTILS +// ========================================================= + +Expected +WasmHostFunctionsImpl::getNFT(AccountID const& account, uint256 const& nftId) +{ + if (!account) + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + + if (!nftId) + return Unexpected(HostFunctionError::INVALID_PARAMS); + + auto obj = nft::findToken(ctx.view(), account, nftId); + if (!obj) + return Unexpected(HostFunctionError::LEDGER_OBJ_NOT_FOUND); + + auto objUri = obj->at(~sfURI); + if (!objUri) + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + + Slice const s = objUri->value(); + return Bytes(s.begin(), s.end()); +} + +Expected +WasmHostFunctionsImpl::getNFTIssuer(uint256 const& nftId) +{ + auto const issuer = nft::getIssuer(nftId); + if (!issuer) + return Unexpected(HostFunctionError::INVALID_PARAMS); + + return Bytes{issuer.begin(), issuer.end()}; +} + +Expected +WasmHostFunctionsImpl::getNFTTaxon(uint256 const& nftId) +{ + return nft::toUInt32(nft::getTaxon(nftId)); +} + +Expected +WasmHostFunctionsImpl::getNFTFlags(uint256 const& nftId) +{ + return nft::getFlags(nftId); +} + +Expected +WasmHostFunctionsImpl::getNFTTransferFee(uint256 const& nftId) +{ + return nft::getTransferFee(nftId); +} + +Expected +WasmHostFunctionsImpl::getNFTSerial(uint256 const& nftId) +{ + return nft::getSerial(nftId); +} + +} // namespace xrpl diff --git a/src/xrpld/app/wasm/detail/HostFuncImplTrace.cpp b/src/xrpld/app/wasm/detail/HostFuncImplTrace.cpp new file mode 100644 index 0000000000..ddf4cde0e5 --- /dev/null +++ b/src/xrpld/app/wasm/detail/HostFuncImplTrace.cpp @@ -0,0 +1,79 @@ +#include + +#include +#include + +#ifdef _DEBUG +// #define DEBUG_OUTPUT 1 +#endif + +namespace xrpl { + +Expected +WasmHostFunctionsImpl::trace( + std::string_view const& msg, + Slice const& data, + bool asHex) +{ + auto const ret = msg.size() + data.size() * (asHex ? 2 : 1); + + if (!asHex) + { + log(msg, [&data] { + return std::string_view( + reinterpret_cast(data.data()), data.size()); + }); + } + else + { + log(msg, [&data] { + std::string hex; + hex.reserve(data.size() * 2); + boost::algorithm::hex( + data.begin(), data.end(), std::back_inserter(hex)); + return hex; + }); + } + + return ret; +} + +Expected +WasmHostFunctionsImpl::traceNum(std::string_view const& msg, int64_t data) +{ + auto const ret = msg.size() + sizeof(data); + log(msg, [data] { return data; }); + return ret; +} + +Expected +WasmHostFunctionsImpl::traceAccount( + std::string_view const& msg, + AccountID const& account) +{ + auto const ret = msg.size() + account.size(); + log(msg, [&account] { return toBase58(account); }); + return ret; +} + +Expected +WasmHostFunctionsImpl::traceFloat( + std::string_view const& msg, + Slice const& data) +{ + auto const ret = msg.size() + data.size(); + log(msg, [&data] { return wasm_float::floatToString(data); }); + return ret; +} + +Expected +WasmHostFunctionsImpl::traceAmount( + std::string_view const& msg, + STAmount const& amount) +{ + auto const ret = msg.size(); + log(msg, [&amount] { return amount.getFullText(); }); + return ret; +} + +} // namespace xrpl diff --git a/src/xrpld/app/wasm/detail/HostFuncWrapper.cpp b/src/xrpld/app/wasm/detail/HostFuncWrapper.cpp index b5da69f71d..8910f22aa1 100644 --- a/src/xrpld/app/wasm/detail/HostFuncWrapper.cpp +++ b/src/xrpld/app/wasm/detail/HostFuncWrapper.cpp @@ -71,7 +71,14 @@ getDataUInt64(IW const* runtime, wasm_val_vec_t const* params, int32_t& i) if (r->size() != sizeof(uint64_t)) return Unexpected(HostFunctionError::INVALID_PARAMS); - return *reinterpret_cast(r->data()); + uint64_t x; + uintptr_t p = reinterpret_cast(r->data()); + if (p & (alignof(uint64_t) - 1)) // unaligned + memcpy(&x, r->data(), sizeof(uint64_t)); + else + x = *reinterpret_cast(r->data()); + + return x; } template @@ -325,7 +332,14 @@ checkGas(void* env) int64_t const gas = runtime->getGas(); WasmImportFunc const& impFunc = udata->second; int64_t const x = gas >= impFunc.gas ? gas - impFunc.gas : 0; - runtime->setGas(x); + + if (runtime->setGas(x) < 0) + { + wasm_trap_t* trap = reinterpret_cast( + WasmEngine::instance().newTrap("can't set gas")); // LCOV_EXCL_LINE + return Unexpected(trap); // LCOV_EXCL_LINE + } + if (gas < impFunc.gas) { wasm_trap_t* trap = reinterpret_cast( diff --git a/src/xrpld/app/wasm/detail/WasmVM.cpp b/src/xrpld/app/wasm/detail/WasmVM.cpp index d2fac5f8d9..4171e78f21 100644 --- a/src/xrpld/app/wasm/detail/WasmVM.cpp +++ b/src/xrpld/app/wasm/detail/WasmVM.cpp @@ -89,13 +89,13 @@ setCommonHostFunctions(HostFunctions* hfs, ImportVec& i) // clang-format on } -ImportVec +std::shared_ptr createWasmImport(HostFunctions& hfs) { - ImportVec i; + std::shared_ptr i(std::make_shared()); - setCommonHostFunctions(&hfs, i); - WASM_IMPORT_FUNC2(i, updateData, "update_data", &hfs, 1000); + setCommonHostFunctions(&hfs, *i); + WASM_IMPORT_FUNC2(*i, updateData, "update_data", &hfs, 1000); return i; } @@ -103,7 +103,7 @@ createWasmImport(HostFunctions& hfs) Expected runEscrowWasm( Bytes const& wasmCode, - HostFunctions& hfs, + std::shared_ptr const& hfs, std::string_view funcName, std::vector const& params, int64_t gasLimit) @@ -116,10 +116,10 @@ runEscrowWasm( wasmCode, funcName, params, - createWasmImport(hfs), - &hfs, + createWasmImport(*hfs), + hfs, gasLimit, - hfs.getJournal()); + hfs->getJournal()); // std::cout << "runEscrowWasm, mod size: " << wasmCode.size() // << ", gasLimit: " << gasLimit << ", funcName: " << funcName; @@ -142,7 +142,7 @@ runEscrowWasm( NotTEC preflightEscrowWasm( Bytes const& wasmCode, - HostFunctions& hfs, + std::shared_ptr const& hfs, std::string_view funcName, std::vector const& params) { @@ -154,9 +154,9 @@ preflightEscrowWasm( wasmCode, funcName, params, - createWasmImport(hfs), - &hfs, - hfs.getJournal()); + createWasmImport(*hfs), + hfs, + hfs->getJournal()); return ret; } @@ -179,8 +179,8 @@ WasmEngine::run( Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, + std::shared_ptr const& imports, + std::shared_ptr const& hfs, int64_t gasLimit, beast::Journal j) { @@ -192,8 +192,8 @@ WasmEngine::check( Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, + std::shared_ptr const& imports, + std::shared_ptr const& hfs, beast::Journal j) { return impl->check(wasmCode, funcName, params, imports, hfs, j); diff --git a/src/xrpld/app/wasm/detail/WasmiVM.cpp b/src/xrpld/app/wasm/detail/WasmiVM.cpp index 9d3dded78b..037f787a26 100644 --- a/src/xrpld/app/wasm/detail/WasmiVM.cpp +++ b/src/xrpld/app/wasm/detail/WasmiVM.cpp @@ -20,23 +20,25 @@ print_wasm_error(std::string_view msg, wasm_trap_t* trap, beast::Journal jlog) auto& j = std::cerr; #else auto j = jlog.warn(); + if (jlog.active(beast::severities::kWarning)) #endif - - wasm_byte_vec_t error_message WASM_EMPTY_VEC; - - if (trap) - wasm_trap_message(trap, &error_message); - - if (error_message.size) { - j << "WASMI Error: " << msg << ", " - << std::string_view(error_message.data, error_message.size - 1); - } - else - j << "WASMI Error: " << msg; + wasm_byte_vec_t error_message WASM_EMPTY_VEC; - if (error_message.size) - wasm_byte_vec_delete(&error_message); + if (trap) + wasm_trap_message(trap, &error_message); + + if (error_message.size) + { + j << "WASMI Error: " << msg << ", " + << std::string_view(error_message.data, error_message.size - 1); + } + else + j << "WASMI Error: " << msg; + + if (error_message.size) + wasm_byte_vec_delete(&error_message); + } if (trap) wasm_trap_delete(trap); @@ -101,6 +103,8 @@ InstanceWrapper::operator=(InstanceWrapper&& o) store_ = o.store_; o.store_ = nullptr; exports_ = std::move(o.exports_); + memIdx_ = o.memIdx_; + o.memIdx_ = -1; instance_ = std::move(o.instance_); j_ = o.j_; @@ -160,22 +164,29 @@ InstanceWrapper::getFunc( wmem InstanceWrapper::getMem() const { - if (!instance_) - throw std::runtime_error("no instance"); // LCOV_EXCL_LINE + if (memIdx_ >= 0) + { + auto* e(exports_.vec_.data[memIdx_]); + wasm_memory_t* mem = wasm_extern_as_memory(e); + return { + reinterpret_cast(wasm_memory_data(mem)), + wasm_memory_data_size(mem)}; + } wasm_memory_t* mem = nullptr; - for (unsigned i = 0; i < exports_.vec_.size; ++i) + for (int i = 0; i < exports_.vec_.size; ++i) { auto* e(exports_.vec_.data[i]); if (wasm_extern_kind(e) == WASM_EXTERN_MEMORY) { + memIdx_ = i; mem = wasm_extern_as_memory(e); break; } } if (!mem) - throw std::runtime_error("no memory exported"); // LCOV_EXCL_LINE + return {}; // LCOV_EXCL_LINE return { reinterpret_cast(wasm_memory_data(mem)), @@ -207,7 +218,7 @@ InstanceWrapper::setGas(std::int64_t gas) const // LCOV_EXCL_START print_wasm_error("Can't set instance gas", nullptr, j_); wasmi_error_delete(err); - throw std::runtime_error("Can't set instance gas"); + return -1; // LCOV_EXCL_STOP } @@ -244,7 +255,7 @@ ModuleWrapper::ModuleWrapper( StorePtr& s, Bytes const& wasmBin, bool instantiate, - ImportVec const& imports, + std::shared_ptr const& imports, beast::Journal j) : module_(init(s, wasmBin, j)), j_(j) { @@ -331,13 +342,17 @@ makeImpReturn(WasmImportFunc const& imp) } WasmExternVec -ModuleWrapper::buildImports(StorePtr& s, ImportVec const& imports) +ModuleWrapper::buildImports( + StorePtr& s, + std::shared_ptr const& imports) { WasmImporttypeVec importTypes; wasm_module_imports(module_.get(), &importTypes.vec_); if (!importTypes.vec_.size) return {}; + if (!imports) + throw std::runtime_error("Missing imports"); WasmExternVec wimports(importTypes.vec_.size); @@ -363,7 +378,7 @@ ModuleWrapper::buildImports(StorePtr& s, ImportVec const& imports) // continue; bool impSet = false; - for (auto const& obj : imports) + for (auto const& obj : *imports) { auto const& imp = obj.second; if (imp.name != fieldName) @@ -510,6 +525,10 @@ WasmiEngine::init() wasmi_config_wasm_tail_call_set(config, false); wasmi_config_wasm_extended_const_set(config, false); wasmi_config_floats_set(config, false); + wasmi_config_wasm_multi_memory_set(config, false); + wasmi_config_wasm_custom_page_sizes_set(config, false); + wasmi_config_wasm_memory64_set(config, false); + wasmi_config_wasm_wide_arithmetic_set(config, false); return std::unique_ptr( wasm_engine_new_with_config(config), &wasm_engine_delete); @@ -521,11 +540,7 @@ WasmiEngine::WasmiEngine() } int -WasmiEngine::addModule( - Bytes const& wasmCode, - bool instantiate, - int64_t gas, - ImportVec const& imports) +WasmiEngine::addModule(Bytes const& wasmCode, bool instantiate, int64_t gas) { moduleWrap_.reset(); store_.reset(); // to free the memory before creating new store @@ -547,7 +562,7 @@ WasmiEngine::addModule( } moduleWrap_ = std::make_unique( - store_, wasmCode, instantiate, imports, j_); + store_, wasmCode, instantiate, imports_, j_); if (!moduleWrap_) throw std::runtime_error( @@ -586,11 +601,13 @@ WasmiEngine::convertParams(std::vector const& params) break; // LCOV_EXCL_STOP case WT_U8V: { + auto mem = getMem(); + if (!mem.s) + throw std::runtime_error( + "no memory exported"); // LCOV_EXCL_LINE auto const sz = p.of.u8v.sz; auto const ptr = allocate(sz); - auto mem = getMem(); memcpy(mem.p + ptr, p.of.u8v.d, sz); - v.push_back(WASM_I32_VAL(ptr)); v.push_back(WASM_I32_VAL(sz)); } @@ -745,8 +762,11 @@ WasmiEngine::call( std::size_t sz, Types&&... args) { - auto const ptr = allocate(sz); auto mem = getMem(); + if (!mem.s) + throw std::runtime_error("no memory exported"); // LCOV_EXCL_LINE + + auto const ptr = allocate(sz); memcpy(mem.p + ptr, d, sz); add_param(in, ptr); @@ -780,16 +800,24 @@ WasmiEngine::run( Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, + std::shared_ptr const& imports, + std::shared_ptr const& hfs, int64_t gas, beast::Journal j) { j_ = j; + + if (!wasmCode.empty()) + { // save values for reuse + imports_ = imports; + hfs_ = hfs; + } + try { - checkImports(imports, hfs); - return runHlp(wasmCode, funcName, params, imports, hfs, gas); + if (imports_) + checkImports(*imports_, hfs.get()); + return runHlp(wasmCode, funcName, params, gas); } catch (std::exception const& e) { @@ -809,8 +837,6 @@ WasmiEngine::runHlp( Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, int64_t gas) { // currently only 1 module support, possible parallel UT run @@ -819,14 +845,14 @@ WasmiEngine::runHlp( // Create and instantiate the module. if (!wasmCode.empty()) { - [[maybe_unused]] int const m = addModule(wasmCode, true, gas, imports); + [[maybe_unused]] int const m = addModule(wasmCode, true, gas); } if (!moduleWrap_ || !moduleWrap_->instanceWrap_) throw std::runtime_error("no instance"); // LCOV_EXCL_LINE - if (hfs) - hfs->setRT(&getRT()); + if (hfs_) + hfs_->setRT(&getRT()); // Call main auto const f = getFunc(!funcName.empty() ? funcName : "_start"); @@ -870,16 +896,23 @@ WasmiEngine::check( Bytes const& wasmCode, std::string_view funcName, std::vector const& params, - ImportVec const& imports, - HostFunctions* hfs, + std::shared_ptr const& imports, + std::shared_ptr const& hfs, beast::Journal j) { j_ = j; + if (!wasmCode.empty()) + { + imports_ = imports; + hfs_ = hfs; + } + try { - checkImports(imports, hfs); - return checkHlp(wasmCode, funcName, params, imports); + if (imports_) + checkImports(*imports_, hfs_.get()); + return checkHlp(wasmCode, funcName, params); } catch (std::exception const& e) { @@ -899,8 +932,7 @@ NotTEC WasmiEngine::checkHlp( Bytes const& wasmCode, std::string_view funcName, - std::vector const& params, - ImportVec const& imports) + std::vector const& params) { // currently only 1 module support, possible parallel UT run std::lock_guard lg(m_); @@ -909,7 +941,7 @@ WasmiEngine::checkHlp( if (wasmCode.empty()) throw std::runtime_error("empty nodule"); - int const m = addModule(wasmCode, false, -1, imports); + int const m = addModule(wasmCode, false, -1); if ((m < 0) || !moduleWrap_) throw std::runtime_error("no module"); // LCOV_EXCL_LINE diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 53237ed3ae..b64227288c 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1351,8 +1351,8 @@ PeerImp::handleTransaction( { // If we've never been in synch, there's nothing we can do // with a transaction - JLOG(p_journal_.debug()) << "Ignoring incoming transaction: " - << "Need network ledger"; + JLOG(p_journal_.debug()) + << "Ignoring incoming transaction: Need network ledger"; return; } @@ -2618,6 +2618,16 @@ PeerImp::onMessage(std::shared_ptr const& m) newObj.set_ledgerseq(obj.ledgerseq()); // VFALCO NOTE "seq" in the message is obsolete + + // Check if by adding this object, reply has reached its + // limit + if (reply.objects_size() >= Tuning::hardMaxReplyNodes) + { + fee_.update( + Resource::feeModerateBurdenPeer, + " Reply limit reached. Truncating reply."); + break; + } } } } diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 2e9d5b35bf..17d8e5e7bc 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -18,6 +18,32 @@ namespace xrpl { +using FunctionType = std::function( + Json::Value const&, + Json::StaticString const, + unsigned const apiVersion)>; + +static Expected +parseFixed( + Keylet const& keylet, + Json::Value const& params, + Json::StaticString const& fieldName, + unsigned const apiVersion); + +// Helper function to return FunctionType for objects that have a fixed +// location. That is, they don't take parameters to compute the index. +// e.g. amendments, fees, negative UNL, etc. +static FunctionType +fixed(Keylet const& keylet) +{ + return [keylet]( + Json::Value const& params, + Json::StaticString const fieldName, + unsigned const apiVersion) -> Expected { + return parseFixed(keylet, params, fieldName, apiVersion); + }; +} + static Expected parseObjectID( Json::Value const& params, @@ -33,13 +59,33 @@ parseObjectID( } static Expected -parseIndex(Json::Value const& params, Json::StaticString const fieldName) +parseIndex( + Json::Value const& params, + Json::StaticString const fieldName, + unsigned const apiVersion) { + if (apiVersion > 2u && params.isString()) + { + std::string const index = params.asString(); + if (index == jss::amendments.c_str()) + return keylet::amendments().key; + if (index == jss::fee.c_str()) + return keylet::fees().key; + if (index == jss::nunl) + return keylet::negativeUNL().key; + if (index == jss::hashes) + // Note this only finds the "short" skip list. Use "hashes":index to + // get the long list. + return keylet::skip().key; + } return parseObjectID(params, fieldName, "hex string"); } static Expected -parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName) +parseAccountRoot( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (auto const account = LedgerEntryHelpers::parse(params)) { @@ -50,14 +96,13 @@ parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName) "malformedAddress", fieldName, "AccountID"); } -static Expected -parseAmendments(Json::Value const& params, Json::StaticString const fieldName) -{ - return parseObjectID(params, fieldName, "hex string"); -} +auto const parseAmendments = fixed(keylet::amendments()); static Expected -parseAMM(Json::Value const& params, Json::StaticString const fieldName) +parseAMM( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -85,7 +130,10 @@ parseAMM(Json::Value const& params, Json::StaticString const fieldName) } static Expected -parseBridge(Json::Value const& params, Json::StaticString const fieldName) +parseBridge( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isMember(jss::bridge)) { @@ -116,13 +164,19 @@ parseBridge(Json::Value const& params, Json::StaticString const fieldName) } static Expected -parseCheck(Json::Value const& params, Json::StaticString const fieldName) +parseCheck( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { return parseObjectID(params, fieldName, "hex string"); } static Expected -parseCredential(Json::Value const& cred, Json::StaticString const fieldName) +parseCredential( + Json::Value const& cred, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!cred.isObject()) { @@ -153,7 +207,10 @@ parseCredential(Json::Value const& cred, Json::StaticString const fieldName) } static Expected -parseDelegate(Json::Value const& params, Json::StaticString const fieldName) +parseDelegate( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -244,7 +301,10 @@ parseAuthorizeCredentials(Json::Value const& jv) } static Expected -parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName) +parseDepositPreauth( + Json::Value const& dp, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!dp.isObject()) { @@ -297,7 +357,10 @@ parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName) } static Expected -parseDID(Json::Value const& params, Json::StaticString const fieldName) +parseDID( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { auto const account = LedgerEntryHelpers::parse(params); if (!account) @@ -312,7 +375,8 @@ parseDID(Json::Value const& params, Json::StaticString const fieldName) static Expected parseDirectoryNode( Json::Value const& params, - Json::StaticString const fieldName) + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -365,7 +429,10 @@ parseDirectoryNode( } static Expected -parseEscrow(Json::Value const& params, Json::StaticString const fieldName) +parseEscrow( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -384,20 +451,53 @@ parseEscrow(Json::Value const& params, Json::StaticString const fieldName) return keylet::escrow(*id, *seq).key; } +auto const parseFeeSettings = fixed(keylet::fees()); + static Expected -parseFeeSettings(Json::Value const& params, Json::StaticString const fieldName) +parseFixed( + Keylet const& keylet, + Json::Value const& params, + Json::StaticString const& fieldName, + [[maybe_unused]] unsigned const apiVersion) { - return parseObjectID(params, fieldName, "hex string"); + if (!params.isBool()) + { + return parseObjectID(params, fieldName, "hex string"); + } + if (!params.asBool()) + { + return LedgerEntryHelpers::invalidFieldError( + "invalidParams", fieldName, "true"); + } + + return keylet.key; } static Expected -parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName) +parseLedgerHashes( + Json::Value const& params, + Json::StaticString const fieldName, + unsigned const apiVersion) { - return parseObjectID(params, fieldName, "hex string"); + if (params.isUInt() || params.isInt()) + { + // If the index doesn't parse as a UInt, throw + auto const index = params.asUInt(); + + // Return the "long" skip list for the given ledger index. + auto const keylet = keylet::skip(index); + return keylet.key; + } + // Return the key in `params` or the "short" skip list, which contains + // hashes since the last flag ledger. + return parseFixed(keylet::skip(), params, fieldName, apiVersion); } static Expected -parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName) +parseLoanBroker( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -417,7 +517,10 @@ parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName) } static Expected -parseLoan(Json::Value const& params, Json::StaticString const fieldName) +parseLoan( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -425,7 +528,7 @@ parseLoan(Json::Value const& params, Json::StaticString const fieldName) } auto const id = LedgerEntryHelpers::requiredUInt256( - params, jss::loan_broker_id, "malformedLoanBrokerID"); + params, jss::loan_broker_id, "malformedBroker"); if (!id) return Unexpected(id.error()); auto const seq = LedgerEntryHelpers::requiredUInt32( @@ -437,7 +540,10 @@ parseLoan(Json::Value const& params, Json::StaticString const fieldName) } static Expected -parseMPToken(Json::Value const& params, Json::StaticString const fieldName) +parseMPToken( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -460,7 +566,8 @@ parseMPToken(Json::Value const& params, Json::StaticString const fieldName) static Expected parseMPTokenIssuance( Json::Value const& params, - Json::StaticString const fieldName) + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { auto const mptIssuanceID = LedgerEntryHelpers::parse(params); if (!mptIssuanceID) @@ -471,25 +578,30 @@ parseMPTokenIssuance( } static Expected -parseNFTokenOffer(Json::Value const& params, Json::StaticString const fieldName) +parseNFTokenOffer( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { return parseObjectID(params, fieldName, "hex string"); } static Expected -parseNFTokenPage(Json::Value const& params, Json::StaticString const fieldName) +parseNFTokenPage( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { return parseObjectID(params, fieldName, "hex string"); } -static Expected -parseNegativeUNL(Json::Value const& params, Json::StaticString const fieldName) -{ - return parseObjectID(params, fieldName, "hex string"); -} +auto const parseNegativeUNL = fixed(keylet::negativeUNL()); static Expected -parseOffer(Json::Value const& params, Json::StaticString const fieldName) +parseOffer( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -510,7 +622,10 @@ parseOffer(Json::Value const& params, Json::StaticString const fieldName) } static Expected -parseOracle(Json::Value const& params, Json::StaticString const fieldName) +parseOracle( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -531,7 +646,10 @@ parseOracle(Json::Value const& params, Json::StaticString const fieldName) } static Expected -parsePayChannel(Json::Value const& params, Json::StaticString const fieldName) +parsePayChannel( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { return parseObjectID(params, fieldName, "hex string"); } @@ -539,7 +657,8 @@ parsePayChannel(Json::Value const& params, Json::StaticString const fieldName) static Expected parsePermissionedDomain( Json::Value const& pd, - Json::StaticString const fieldName) + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (pd.isString()) { @@ -568,7 +687,8 @@ parsePermissionedDomain( static Expected parseRippleState( Json::Value const& jvRippleState, - Json::StaticString const fieldName) + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { Currency uCurrency; @@ -618,13 +738,19 @@ parseRippleState( } static Expected -parseSignerList(Json::Value const& params, Json::StaticString const fieldName) +parseSignerList( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { return parseObjectID(params, fieldName, "hex string"); } static Expected -parseTicket(Json::Value const& params, Json::StaticString const fieldName) +parseTicket( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -645,7 +771,10 @@ parseTicket(Json::Value const& params, Json::StaticString const fieldName) } static Expected -parseVault(Json::Value const& params, Json::StaticString const fieldName) +parseVault( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!params.isObject()) { @@ -668,7 +797,8 @@ parseVault(Json::Value const& params, Json::StaticString const fieldName) static Expected parseXChainOwnedClaimID( Json::Value const& claim_id, - Json::StaticString const fieldName) + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!claim_id.isObject()) { @@ -693,7 +823,8 @@ parseXChainOwnedClaimID( static Expected parseXChainOwnedCreateAccountClaimID( Json::Value const& claim_id, - Json::StaticString const fieldName) + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) { if (!claim_id.isObject()) { @@ -717,10 +848,6 @@ parseXChainOwnedCreateAccountClaimID( return keylet.key; } -using FunctionType = Expected (*)( - Json::Value const&, - Json::StaticString const); - struct LedgerEntry { Json::StaticString fieldName; @@ -753,7 +880,7 @@ doLedgerEntry(RPC::JsonContext& context) {jss::ripple_state, parseRippleState, ltRIPPLE_STATE}, }); - auto hasMoreThanOneMember = [&]() { + auto const hasMoreThanOneMember = [&]() { int count = 0; for (auto const& ledgerEntry : ledgerEntryParsers) @@ -797,8 +924,8 @@ doLedgerEntry(RPC::JsonContext& context) Json::Value const& params = ledgerEntry.fieldName == jss::bridge ? context.params : context.params[ledgerEntry.fieldName]; - auto const result = - ledgerEntry.parseFunction(params, ledgerEntry.fieldName); + auto const result = ledgerEntry.parseFunction( + params, ledgerEntry.fieldName, context.apiVersion); if (!result) return result.error(); @@ -829,9 +956,13 @@ doLedgerEntry(RPC::JsonContext& context) throw; } + // Return the computed index regardless of whether the node exists. + jvResult[jss::index] = to_string(uNodeIndex); + if (uNodeIndex.isZero()) { - return RPC::make_error(rpcENTRY_NOT_FOUND); + RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult); + return jvResult; } auto const sleNode = lpLedger->read(keylet::unchecked(uNodeIndex)); @@ -843,12 +974,14 @@ doLedgerEntry(RPC::JsonContext& context) if (!sleNode) { // Not found. - return RPC::make_error(rpcENTRY_NOT_FOUND); + RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult); + return jvResult; } if ((expectedType != ltANY) && (expectedType != sleNode->getType())) { - return RPC::make_error(rpcUNEXPECTED_LEDGER_TYPE); + RPC::inject_error(rpcUNEXPECTED_LEDGER_TYPE, jvResult); + return jvResult; } if (bNodeBinary) @@ -858,12 +991,10 @@ doLedgerEntry(RPC::JsonContext& context) sleNode->add(s); jvResult[jss::node_binary] = strHex(s.peekData()); - jvResult[jss::index] = to_string(uNodeIndex); } else { jvResult[jss::node] = sleNode->getJson(JsonOptions::none); - jvResult[jss::index] = to_string(uNodeIndex); } return jvResult;