diff --git a/.clang-tidy b/.clang-tidy index f7009c4666..5971b5dd14 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,105 +1,143 @@ --- Checks: "-*, - bugprone-argument-comment + bugprone-argument-comment, + bugprone-assert-side-effect, + bugprone-bad-signal-to-kill-thread, + bugprone-bool-pointer-implicit-conversion, + bugprone-casting-through-void, + bugprone-chained-comparison, + bugprone-compare-pointer-to-member-virtual-function, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-dynamic-static-initializers, + bugprone-empty-catch, + bugprone-fold-init-type, + bugprone-forward-declaration-namespace, + bugprone-inaccurate-erase, + bugprone-incorrect-enable-if, + bugprone-incorrect-roundings, + bugprone-infinite-loop, + bugprone-integer-division, + bugprone-lambda-function-name, + bugprone-macro-parentheses, + bugprone-macro-repeated-side-effects, + bugprone-misplaced-operator-in-strlen-in-alloc, + bugprone-misplaced-pointer-arithmetic-in-alloc, + bugprone-misplaced-widening-cast, + bugprone-multi-level-implicit-pointer-conversion, + bugprone-multiple-new-in-one-expression, + bugprone-multiple-statement-macro, + bugprone-no-escape, + bugprone-non-zero-enum-to-bool-conversion, + bugprone-parent-virtual-call, + bugprone-posix-return, + bugprone-redundant-branch-condition, + bugprone-shared-ptr-array-mismatch, + bugprone-signal-handler, + bugprone-signed-char-misuse, + bugprone-sizeof-container, + bugprone-spuriously-wake-up-functions, + bugprone-standalone-empty, + bugprone-string-constructor, + bugprone-string-integer-assignment, + bugprone-string-literal-with-embedded-nul, + bugprone-stringview-nullptr, + bugprone-suspicious-enum-usage, + bugprone-suspicious-include, + bugprone-suspicious-memory-comparison, + bugprone-suspicious-memset-usage, + bugprone-suspicious-realloc-usage, + bugprone-suspicious-semicolon, + bugprone-suspicious-string-compare, + bugprone-swapped-arguments, + bugprone-terminating-continue, + bugprone-throw-keyword-missing, + bugprone-undefined-memory-manipulation, + bugprone-undelegated-constructor, + bugprone-unhandled-exception-at-new, + bugprone-unique-ptr-array-mismatch, + bugprone-unsafe-functions, + bugprone-virtual-near-miss, + cppcoreguidelines-no-suspend-with-lock, + cppcoreguidelines-virtual-class-destructor, + hicpp-ignored-remove-result, + misc-definitions-in-headers, + misc-header-include-cycle, + misc-misplaced-const, + misc-static-assert, + misc-throw-by-value-catch-by-reference, + misc-unused-alias-decls, + misc-unused-using-decls, + readability-duplicate-include, + readability-enum-initial-value, + readability-misleading-indentation, + readability-non-const-parameter, + readability-redundant-declaration, + readability-reference-to-constructed-temporary, + modernize-deprecated-headers, + modernize-make-shared, + modernize-make-unique, + performance-implicit-conversion-in-loop, + performance-move-constructor-init, + performance-trivially-destructible " -# bugprone-assert-side-effect, -# bugprone-bad-signal-to-kill-thread, -# bugprone-bool-pointer-implicit-conversion, -# bugprone-casting-through-void, -# bugprone-chained-comparison, -# bugprone-compare-pointer-to-member-virtual-function, -# bugprone-copy-constructor-init, +# --- +# checks that have some issues that need to be resolved: +# # bugprone-crtp-constructor-accessibility, -# bugprone-dangling-handle, -# bugprone-dynamic-static-initializers, -# bugprone-empty-catch, -# bugprone-fold-init-type, -# bugprone-forward-declaration-namespace, -# bugprone-inaccurate-erase, # bugprone-inc-dec-in-conditions, -# bugprone-incorrect-enable-if, -# bugprone-incorrect-roundings, -# bugprone-infinite-loop, -# bugprone-integer-division, -# bugprone-lambda-function-name, -# bugprone-macro-parentheses, -# bugprone-macro-repeated-side-effects, -# bugprone-misplaced-operator-in-strlen-in-alloc, -# bugprone-misplaced-pointer-arithmetic-in-alloc, -# bugprone-misplaced-widening-cast, -# bugprone-move-forwarding-reference, -# bugprone-multi-level-implicit-pointer-conversion, -# bugprone-multiple-new-in-one-expression, -# bugprone-multiple-statement-macro, -# bugprone-no-escape, -# bugprone-non-zero-enum-to-bool-conversion, -# bugprone-optional-value-conversion, -# bugprone-parent-virtual-call, -# bugprone-pointer-arithmetic-on-polymorphic-object, -# bugprone-posix-return, -# bugprone-redundant-branch-condition, # bugprone-reserved-identifier, -# bugprone-return-const-ref-from-parameter, -# bugprone-shared-ptr-array-mismatch, -# bugprone-signal-handler, -# bugprone-signed-char-misuse, -# bugprone-sizeof-container, -# bugprone-sizeof-expression, -# bugprone-spuriously-wake-up-functions, -# bugprone-standalone-empty, -# bugprone-string-constructor, -# bugprone-string-integer-assignment, -# bugprone-string-literal-with-embedded-nul, -# bugprone-stringview-nullptr, -# bugprone-suspicious-enum-usage, -# bugprone-suspicious-include, -# bugprone-suspicious-memory-comparison, -# bugprone-suspicious-memset-usage, -# bugprone-suspicious-missing-comma, -# bugprone-suspicious-realloc-usage, -# bugprone-suspicious-semicolon, -# bugprone-suspicious-string-compare, -# bugprone-suspicious-stringview-data-usage, -# bugprone-swapped-arguments, -# bugprone-switch-missing-default-case, -# bugprone-terminating-continue, -# bugprone-throw-keyword-missing, -# bugprone-too-small-loop-variable, -# bugprone-undefined-memory-manipulation, -# bugprone-undelegated-constructor, -# bugprone-unhandled-exception-at-new, -# bugprone-unhandled-self-assignment, -# bugprone-unique-ptr-array-mismatch, -# bugprone-unsafe-functions, +# bugprone-move-forwarding-reference, # bugprone-unused-local-non-trivial-variable, -# bugprone-unused-raii, +# bugprone-return-const-ref-from-parameter, +# bugprone-switch-missing-default-case, +# bugprone-sizeof-expression, +# bugprone-suspicious-stringview-data-usage, +# bugprone-suspicious-missing-comma, +# bugprone-pointer-arithmetic-on-polymorphic-object, +# bugprone-optional-value-conversion, +# bugprone-too-small-loop-variable, # bugprone-unused-return-value, # bugprone-use-after-move, -# bugprone-virtual-near-miss, -# cppcoreguidelines-init-variables, +# bugprone-unhandled-self-assignment, +# bugprone-unused-raii, +# # cppcoreguidelines-misleading-capture-default-by-value, -# cppcoreguidelines-no-suspend-with-lock, +# cppcoreguidelines-init-variables, # cppcoreguidelines-pro-type-member-init, # cppcoreguidelines-pro-type-static-cast-downcast, -# cppcoreguidelines-rvalue-reference-param-not-moved, # cppcoreguidelines-use-default-member-init, -# cppcoreguidelines-virtual-class-destructor, -# hicpp-ignored-remove-result, +# cppcoreguidelines-rvalue-reference-param-not-moved, +# # llvm-namespace-comment, # misc-const-correctness, -# misc-definitions-in-headers, -# misc-header-include-cycle, # misc-include-cleaner, -# misc-misplaced-const, # misc-redundant-expression, -# misc-static-assert, -# misc-throw-by-value-catch-by-reference, -# misc-unused-alias-decls, -# misc-unused-using-decls, +# +# readability-avoid-nested-conditional-operator, +# readability-avoid-return-with-void-value, +# readability-braces-around-statements, +# readability-container-contains, +# readability-container-size-empty, +# readability-convert-member-functions-to-static, +# readability-const-return-type, +# readability-else-after-return, +# readability-implicit-bool-conversion, +# readability-inconsistent-declaration-parameter-name, +# readability-identifier-naming, +# readability-make-member-function-const, +# readability-math-missing-parentheses, +# readability-redundant-inline-specifier, +# readability-redundant-member-init, +# readability-redundant-casting, +# readability-redundant-string-init, +# readability-simplify-boolean-expr, +# readability-static-definition-in-anonymous-namespace, +# readability-suspicious-call-argument, +# readability-use-std-min-max, +# readability-static-accessed-through-instance, +# # modernize-concat-nested-namespaces, -# modernize-deprecated-headers, -# modernize-make-shared, -# modernize-make-unique, # modernize-pass-by-value, # modernize-type-traits, # modernize-use-designated-initializers, @@ -111,79 +149,50 @@ Checks: "-*, # modernize-use-starts-ends-with, # modernize-use-std-numbers, # modernize-use-using, +# # performance-faster-string-find, # performance-for-range-copy, -# performance-implicit-conversion-in-loop, # performance-inefficient-vector-operation, # performance-move-const-arg, -# performance-move-constructor-init, # performance-no-automatic-move, -# performance-trivially-destructible, -# readability-avoid-nested-conditional-operator, -# readability-avoid-return-with-void-value, -# readability-braces-around-statements, -# readability-const-return-type, -# readability-container-contains, -# readability-container-size-empty, -# readability-convert-member-functions-to-static, -# readability-duplicate-include, -# readability-else-after-return, -# readability-enum-initial-value, -# readability-implicit-bool-conversion, -# readability-inconsistent-declaration-parameter-name, -# readability-identifier-naming, -# readability-make-member-function-const, -# readability-math-missing-parentheses, -# readability-misleading-indentation, -# readability-non-const-parameter, -# readability-redundant-casting, -# readability-redundant-declaration, -# readability-redundant-inline-specifier, -# readability-redundant-member-init, -# readability-redundant-string-init, -# readability-reference-to-constructed-temporary, -# readability-simplify-boolean-expr, -# readability-static-accessed-through-instance, -# readability-static-definition-in-anonymous-namespace, -# readability-suspicious-call-argument, -# readability-use-std-min-max +# --- # -# CheckOptions: -# readability-braces-around-statements.ShortStatementLines: 2 -# readability-identifier-naming.MacroDefinitionCase: UPPER_CASE -# readability-identifier-naming.ClassCase: CamelCase -# readability-identifier-naming.StructCase: CamelCase -# readability-identifier-naming.UnionCase: CamelCase -# readability-identifier-naming.EnumCase: CamelCase -# readability-identifier-naming.EnumConstantCase: CamelCase -# readability-identifier-naming.ScopedEnumConstantCase: CamelCase -# readability-identifier-naming.GlobalConstantCase: UPPER_CASE -# readability-identifier-naming.GlobalConstantPrefix: "k" -# readability-identifier-naming.GlobalVariableCase: CamelCase -# readability-identifier-naming.GlobalVariablePrefix: "g" -# readability-identifier-naming.ConstexprFunctionCase: camelBack -# readability-identifier-naming.ConstexprMethodCase: camelBack -# readability-identifier-naming.ClassMethodCase: camelBack -# readability-identifier-naming.ClassMemberCase: camelBack -# readability-identifier-naming.ClassConstantCase: UPPER_CASE -# readability-identifier-naming.ClassConstantPrefix: "k" -# readability-identifier-naming.StaticConstantCase: UPPER_CASE -# readability-identifier-naming.StaticConstantPrefix: "k" -# readability-identifier-naming.StaticVariableCase: UPPER_CASE -# readability-identifier-naming.StaticVariablePrefix: "k" -# readability-identifier-naming.ConstexprVariableCase: UPPER_CASE -# readability-identifier-naming.ConstexprVariablePrefix: "k" -# readability-identifier-naming.LocalConstantCase: camelBack -# readability-identifier-naming.LocalVariableCase: camelBack -# readability-identifier-naming.TemplateParameterCase: CamelCase -# readability-identifier-naming.ParameterCase: camelBack -# readability-identifier-naming.FunctionCase: camelBack -# readability-identifier-naming.MemberCase: camelBack -# readability-identifier-naming.PrivateMemberSuffix: _ -# readability-identifier-naming.ProtectedMemberSuffix: _ -# readability-identifier-naming.PublicMemberSuffix: "" -# readability-identifier-naming.FunctionIgnoredRegexp: ".*tag_invoke.*" -# bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true +CheckOptions: + # readability-braces-around-statements.ShortStatementLines: 2 + # readability-identifier-naming.MacroDefinitionCase: UPPER_CASE + # readability-identifier-naming.ClassCase: CamelCase + # readability-identifier-naming.StructCase: CamelCase + # readability-identifier-naming.UnionCase: CamelCase + # readability-identifier-naming.EnumCase: CamelCase + # readability-identifier-naming.EnumConstantCase: CamelCase + # readability-identifier-naming.ScopedEnumConstantCase: CamelCase + # readability-identifier-naming.GlobalConstantCase: UPPER_CASE + # readability-identifier-naming.GlobalConstantPrefix: "k" + # readability-identifier-naming.GlobalVariableCase: CamelCase + # readability-identifier-naming.GlobalVariablePrefix: "g" + # readability-identifier-naming.ConstexprFunctionCase: camelBack + # readability-identifier-naming.ConstexprMethodCase: camelBack + # readability-identifier-naming.ClassMethodCase: camelBack + # readability-identifier-naming.ClassMemberCase: camelBack + # readability-identifier-naming.ClassConstantCase: UPPER_CASE + # readability-identifier-naming.ClassConstantPrefix: "k" + # readability-identifier-naming.StaticConstantCase: UPPER_CASE + # readability-identifier-naming.StaticConstantPrefix: "k" + # readability-identifier-naming.StaticVariableCase: UPPER_CASE + # readability-identifier-naming.StaticVariablePrefix: "k" + # readability-identifier-naming.ConstexprVariableCase: UPPER_CASE + # readability-identifier-naming.ConstexprVariablePrefix: "k" + # readability-identifier-naming.LocalConstantCase: camelBack + # readability-identifier-naming.LocalVariableCase: camelBack + # readability-identifier-naming.TemplateParameterCase: CamelCase + # readability-identifier-naming.ParameterCase: camelBack + # readability-identifier-naming.FunctionCase: camelBack + # readability-identifier-naming.MemberCase: camelBack + # readability-identifier-naming.PrivateMemberSuffix: _ + # readability-identifier-naming.ProtectedMemberSuffix: _ + # readability-identifier-naming.PublicMemberSuffix: "" + # readability-identifier-naming.FunctionIgnoredRegexp: ".*tag_invoke.*" + bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true # bugprone-unused-return-value.CheckedReturnTypes: ::std::error_code;::std::error_condition;::std::errc # misc-include-cleaner.IgnoreHeaders: '.*/(detail|impl)/.*;.*(expected|unexpected).*;.*ranges_lower_bound\.h;time.h;stdlib.h;__chrono/.*;fmt/chrono.h;boost/uuid/uuid_hash.hpp' # diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index f43275201c..54a84a426a 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -11,7 +11,7 @@ on: 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@320be44621ca2a080f05aeb15817c44b84518108 + uses: XRPLF/actions/.github/workflows/pre-commit.yml@56de1bdf19639e009639a50b8d17c28ca954f267 with: runs_on: ubuntu-latest - container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-ab4d1f0" }' + container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }' diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 4f52b68b84..75fe546b18 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -101,7 +101,7 @@ jobs: steps: - name: Cleanup workspace (macOS and Windows) if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }} - uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf + uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -177,7 +177,7 @@ jobs: - name: Upload the binary (Linux) if: ${{ github.repository_owner == 'XRPLF' && runner.os == 'Linux' }} - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: xrpld-${{ inputs.config_name }} path: ${{ env.BUILD_DIR }}/xrpld @@ -229,8 +229,21 @@ jobs: env: BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} run: | - ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" + set -o pipefail + ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log + - name: Show test failure summary + if: ${{ failure() && !inputs.build_only }} + working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }} + run: | + if [ ! -f unittest.log ]; then + echo "unittest.log not found; embedded tests may not have run." + exit 0 + fi + + if ! grep -E "failed" unittest.log; then + echo "Log present but no failure lines found in unittest.log." + fi - name: Debug failure (Linux) if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }} run: | diff --git a/.github/workflows/reusable-clang-tidy-files.yml b/.github/workflows/reusable-clang-tidy-files.yml index 432da1d15c..129726ec8f 100644 --- a/.github/workflows/reusable-clang-tidy-files.yml +++ b/.github/workflows/reusable-clang-tidy-files.yml @@ -78,13 +78,13 @@ jobs: id: run_clang_tidy continue-on-error: true env: - FILES: ${{ inputs.files }} + TARGETS: ${{ inputs.files != '' && inputs.files || 'src tests' }} run: | - run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "$BUILD_DIR" $FILES 2>&1 | tee clang-tidy-output.txt + run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" ${TARGETS} 2>&1 | tee clang-tidy-output.txt - name: Upload clang-tidy output if: steps.run_clang_tidy.outcome != 'success' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: clang-tidy-results path: clang-tidy-output.txt diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 7c300ee26e..7050d3509f 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -22,7 +22,8 @@ jobs: if: ${{ inputs.check_only_changed }} runs-on: ubuntu-latest outputs: - any_changed: ${{ steps.changed_files.outputs.any_changed }} + clang_tidy_config_changed: ${{ steps.changed_clang_tidy.outputs.any_changed }} + any_cpp_changed: ${{ steps.changed_files.outputs.any_changed }} all_changed_files: ${{ steps.changed_files.outputs.all_changed_files }} steps: - name: Checkout repository @@ -38,10 +39,17 @@ jobs: **/*.ipp separator: " " + - name: Get changed clang-tidy configuration + id: changed_clang_tidy + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + with: + files: | + .clang-tidy + run-clang-tidy: needs: [determine-files] - if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_changed == 'true') }} + if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_cpp_changed == 'true' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }} uses: ./.github/workflows/reusable-clang-tidy-files.yml with: - files: ${{ inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '' }} + files: ${{ (needs.determine-files.outputs.clang_tidy_config_changed == 'true' && '') || (inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '') }} create_issue_on_failure: ${{ inputs.create_issue_on_failure }} diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index b260c4c4f3..df8aa43a18 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -64,7 +64,7 @@ jobs: steps: - name: Cleanup workspace (macOS and Windows) if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }} - uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf + uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitignore b/.gitignore index a1c2f034d1..60e8fef56c 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,9 @@ gmon.out # Locally patched Conan recipes external/conan-center-index/ +# Local conan directory +.conan + # XCode IDE. *.pbxuser !default.pbxuser @@ -72,5 +75,8 @@ DerivedData /.claude /CLAUDE.md +# Direnv's directory +/.direnv + # clangd cache /.cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9117fe0d3e..2d0ff63b38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: [--assume-in-merge] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: 75ca4ad908dc4a99f57921f29b7e6c1521e10b26 # frozen: v21.1.8 + rev: cd481d7b0bfb5c7b3090c21846317f9a8262e891 # frozen: v22.1.0 hooks: - id: clang-format args: [--style=file] @@ -33,17 +33,17 @@ repos: additional_dependencies: [PyYAML] - repo: https://github.com/rbubley/mirrors-prettier - rev: 5ba47274f9b181bce26a5150a725577f3c336011 # frozen: v3.6.2 + rev: c2bc67fe8f8f549cc489e00ba8b45aa18ee713b1 # frozen: v3.8.1 hooks: - id: prettier - repo: https://github.com/psf/black-pre-commit-mirror - rev: 831207fd435b47aeffdf6af853097e64322b4d44 # frozen: v25.12.0 + rev: ea488cebbfd88a5f50b8bd95d5c829d0bb76feb8 # frozen: 26.1.0 hooks: - id: black - repo: https://github.com/streetsidesoftware/cspell-cli - rev: 1cfa010f078c354f3ffb8413616280cc28f5ba21 # frozen: v9.4.0 + rev: a42085ade523f591dca134379a595e7859986445 # frozen: v9.7.0 hooks: - id: cspell # Spell check changed files exclude: .config/cspell.config.yaml @@ -57,6 +57,24 @@ repos: - .git/COMMIT_EDITMSG stages: [commit-msg] + - repo: local + hooks: + - id: nix-fmt + name: Format Nix files + entry: | + bash -c ' + if command -v nix &> /dev/null || [ "$GITHUB_ACTIONS" = "true" ]; then + nix --extra-experimental-features "nix-command flakes" fmt "$@" + else + echo "Skipping nix-fmt: nix not installed and not in GitHub Actions" + exit 0 + fi + ' -- + language: system + types: + - nix + pass_filenames: true + exclude: | (?x)^( external/.*| diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a928065ef2..4bb1db8689 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -251,6 +251,29 @@ pip3 install pre-commit pre-commit install ``` +## Clang-tidy + +All code must pass `clang-tidy` checks according to the settings in [`.clang-tidy`](./.clang-tidy). + +There is a Continuous Integration job that runs clang-tidy on pull requests. The CI will check: + +- All changed C++ files (`.cpp`, `.h`, `.ipp`) when only code files are modified +- **All files in the repository** when the `.clang-tidy` configuration file is changed + +This ensures that configuration changes don't introduce new warnings across the codebase. + +### Running clang-tidy locally + +Before running clang-tidy, you must build the project to generate required files (particularly protobuf headers). Refer to [`BUILD.md`](./BUILD.md) for build instructions. + +Then run clang-tidy on your local changes: + +``` +run-clang-tidy -p build src tests +``` + +This will check all source files in the `src` and `tests` directories using the compile commands from your `build` directory. + ## Contracts and instrumentation We are using [Antithesis](https://antithesis.com/) for continuous fuzzing, diff --git a/cspell.config.yaml b/cspell.config.yaml index 87258758c4..98b6be81e7 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -173,6 +173,11 @@ words: - nftokens - nftpage - nikb + - nixfmt + - nixos + - nixpkgs + - NOLINT + - NOLINTNEXTLINE - nonxrp - noripple - nudb diff --git a/docs/build/environment.md b/docs/build/environment.md index c6b735ba48..c67877a082 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -3,6 +3,8 @@ environment complete with Git, Python, Conan, CMake, and a C++ compiler. This document exists to help readers set one up on any of the Big Three platforms: Linux, macOS, or Windows. +As an alternative to system packages, the Nix development shell can be used to provide a development environment. See [using nix development shell](./nix.md) for more details. + [BUILD.md]: ../../BUILD.md ## Linux diff --git a/docs/build/nix.md b/docs/build/nix.md new file mode 100644 index 0000000000..33bb3711d0 --- /dev/null +++ b/docs/build/nix.md @@ -0,0 +1,95 @@ +# Using Nix Development Shell for xrpld Development + +This guide explains how to use Nix to set up a reproducible development environment for xrpld. Using Nix eliminates the need to manually install utilities and ensures consistent tooling across different machines. + +## Benefits of Using Nix + +- **Reproducible environment**: Everyone gets the same versions of tools and compilers +- **No system pollution**: Dependencies are isolated and don't affect your system packages +- **Multiple compiler versions**: Easily switch between different GCC and Clang versions +- **Quick setup**: Get started with a single command +- **Works on Linux and macOS**: Consistent experience across platforms + +## Install Nix + +Please follow [the official installation instructions of nix package manager](https://nixos.org/download/) for your system. + +## Entering the Development Shell + +### Basic Usage + +From the root of the xrpld repository, enter the default development shell: + +```bash +nix --experimental-features 'nix-command flakes' develop +``` + +This will: + +- Download and set up all required development tools (CMake, Ninja, Conan, etc.) +- Configure the appropriate compiler for your platform: + - **macOS**: Apple Clang (default system compiler) + - **Linux**: GCC 15 + +The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster. + +> [!TIP] +> To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`: +> +> ```bash +> mkdir -p ~/.config/nix +> echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf +> ``` +> +> After this, you can simply use `nix develop` instead. + +> [!NOTE] +> The examples below assume you've enabled flakes in your config. If you haven't, add `--experimental-features 'nix-command flakes'` after each `nix` command. + +### Choosing a different compiler + +A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`. +Use `nix flake show` to see all the available development shells. + +Use `nix develop .#no_compiler` to use the compiler from your system. + +### Example Usage + +```bash +# Use GCC 14 +nix develop .#gcc14 + +# Use Clang 19 +nix develop .#clang19 + +# Use default for your platform +nix develop +``` + +### Using a different shell + +`nix develop` opens bash by default. If you want to use another shell this could be done by adding `-c` flag. For example: + +```bash +nix develop -c zsh +``` + +## Building xrpld with Nix + +Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). + +## Automatic Activation with direnv + +[direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory. + +## Conan and Prebuilt Packages + +Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source: + +```bash +conan install .. --output-folder . --build '*' --settings build_type=Release +``` + +## Updating `flake.lock` file + +To update `flake.lock` to the latest revision use `nix flake update` command. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..fd43f5b683 --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1769461804, + "narHash": "sha256-6h5sROT/3CTHvzPy9koKBmoCa2eJKh4fzQK8eYFEgl8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b579d443b37c9c5373044201ea77604e37e748c8", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..4c500f1933 --- /dev/null +++ b/flake.nix @@ -0,0 +1,16 @@ +{ + description = "Nix related things for xrpld"; + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + }; + + outputs = + { nixpkgs, ... }: + let + forEachSystem = (import ./nix/utils.nix { inherit nixpkgs; }).forEachSystem; + in + { + devShells = forEachSystem (import ./nix/devshell.nix); + formatter = forEachSystem ({ pkgs, ... }: pkgs.nixfmt); + }; +} diff --git a/include/xrpl/basics/MallocTrim.h b/include/xrpl/basics/MallocTrim.h new file mode 100644 index 0000000000..2d0cf989ba --- /dev/null +++ b/include/xrpl/basics/MallocTrim.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +#include +#include +#include + +namespace xrpl { + +// cSpell:ignore ptmalloc + +// ----------------------------------------------------------------------------- +// Allocator interaction note: +// - This facility invokes glibc's malloc_trim(0) on Linux/glibc to request that +// ptmalloc return free heap pages to the OS. +// - If an alternative allocator (e.g. jemalloc or tcmalloc) is linked or +// preloaded (LD_PRELOAD), calling glibc's malloc_trim typically has no effect +// on the *active* heap. The call is harmless but may not reclaim memory +// because those allocators manage their own arenas. +// - Only glibc sbrk/arena space is eligible for trimming; large mmap-backed +// allocations are usually returned to the OS on free regardless of trimming. +// - Call at known reclamation points (e.g., after cache sweeps / online delete) +// and consider rate limiting to avoid churn. +// ----------------------------------------------------------------------------- + +struct MallocTrimReport +{ + bool supported{false}; + int trimResult{-1}; + std::int64_t rssBeforeKB{-1}; + std::int64_t rssAfterKB{-1}; + std::chrono::microseconds durationUs{-1}; + std::int64_t minfltDelta{-1}; + std::int64_t majfltDelta{-1}; + + [[nodiscard]] std::int64_t + deltaKB() const noexcept + { + if (rssBeforeKB < 0 || rssAfterKB < 0) + return 0; + return rssAfterKB - rssBeforeKB; + } +}; + +/** + * @brief Attempt to return freed memory to the operating system. + * + * On Linux with glibc malloc, this issues ::malloc_trim(0), which may release + * free space from ptmalloc arenas back to the kernel. On other platforms, or if + * a different allocator is in use, this function is a no-op and the report will + * indicate that trimming is unsupported or had no effect. + * + * @param tag Identifier for logging/debugging purposes. + * @param journal Journal for diagnostic logging. + * @return Report containing before/after metrics and the trim result. + * + * @note If an alternative allocator (jemalloc/tcmalloc) is linked or preloaded, + * calling glibc's malloc_trim may have no effect on the active heap. The + * call is harmless but typically does not reclaim memory under those + * allocators. + * + * @note Only memory served from glibc's sbrk/arena heaps is eligible for trim. + * Large allocations satisfied via mmap are usually returned on free + * independently of trimming. + * + * @note Intended for use after operations that free significant memory (e.g., + * cache sweeps, ledger cleanup, online delete). Consider rate limiting. + */ +MallocTrimReport +mallocTrim(std::string_view tag, beast::Journal journal); + +} // namespace xrpl diff --git a/include/xrpl/ledger/detail/RawStateTable.h b/include/xrpl/ledger/detail/RawStateTable.h index 7a3e2077ff..499b9204c6 100644 --- a/include/xrpl/ledger/detail/RawStateTable.h +++ b/include/xrpl/ledger/detail/RawStateTable.h @@ -23,13 +23,13 @@ public: static constexpr size_t initialBufferSize = kilobytes(256); RawStateTable() - : monotonic_resource_{std::make_unique( - initialBufferSize)} + : monotonic_resource_{ + std::make_unique(initialBufferSize)} , items_{monotonic_resource_.get()} {}; RawStateTable(RawStateTable const& rhs) - : monotonic_resource_{std::make_unique( - initialBufferSize)} + : monotonic_resource_{ + std::make_unique(initialBufferSize)} , items_{rhs.items_, monotonic_resource_.get()} , dropsDestroyed_{rhs.dropsDestroyed_} {}; diff --git a/include/xrpl/nodestore/Backend.h b/include/xrpl/nodestore/Backend.h index 7c3ea57bb8..36fd36ec00 100644 --- a/include/xrpl/nodestore/Backend.h +++ b/include/xrpl/nodestore/Backend.h @@ -77,16 +77,16 @@ public: If the object is not found or an error is encountered, the result will indicate the condition. @note This will be called concurrently. - @param key A pointer to the key data. + @param hash The hash of the object. @param pObject [out] The created object if successful. @return The result of the operation. */ virtual Status - fetch(void const* key, std::shared_ptr* pObject) = 0; + fetch(uint256 const& hash, std::shared_ptr* pObject) = 0; /** Fetch a batch synchronously. */ virtual std::pair>, Status> - fetchBatch(std::vector const& hashes) = 0; + fetchBatch(std::vector const& hashes) = 0; /** Store a single object. Depending on the implementation this may happen immediately diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 27b01ad164..88a4642159 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -496,12 +496,14 @@ STAmount::zeroed() const return STAmount(mAsset); } -inline STAmount::operator bool() const noexcept +inline STAmount:: +operator bool() const noexcept { return *this != beast::zero; } -inline STAmount::operator Number() const +inline STAmount:: +operator Number() const { if (native()) return xrp(); @@ -737,6 +739,21 @@ canAdd(STAmount const& amt1, STAmount const& amt2); bool canSubtract(STAmount const& amt1, STAmount const& amt2); +/** Get the scale of a Number for a given asset. + * + * "scale" is similar to "exponent", but from the perspective of STAmount, which has different rules + * and mantissa ranges for determining the exponent than Number. + * + * @param number The Number to get the scale of. + * @param asset The asset to use for determining the scale. + * @return The scale of this Number for the given asset. + */ +inline int +scale(Number const& number, Asset const& asset) +{ + return STAmount{asset, number}.exponent(); +} + } // namespace xrpl //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index f0d286db99..5c7bd1bd4f 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -19,7 +19,7 @@ XRPL_FIX (LendingProtocolV1_1, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FIX (BatchInnerSigs, 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) @@ -33,7 +33,7 @@ XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo 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(Batch, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index b696a1d1c2..c0ac1ba526 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -868,6 +868,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, mustDeleteAcct | destroyMPTIssuance | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, + {sfMemoData, soeOPTIONAL}, })) /** This transaction trades assets for shares with a vault. */ diff --git a/include/xrpl/tx/InvariantCheck.h b/include/xrpl/tx/InvariantCheck.h deleted file mode 100644 index a0937ef9c2..0000000000 --- a/include/xrpl/tx/InvariantCheck.h +++ /dev/null @@ -1,743 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace xrpl { - -class ReadView; - -#if GENERATING_DOCS -/** - * @brief Prototype for invariant check implementations. - * - * __THIS CLASS DOES NOT EXIST__ - or rather it exists in documentation only to - * communicate the interface required of any invariant checker. Any invariant - * check implementation should implement the public methods documented here. - * - */ -class InvariantChecker_PROTOTYPE -{ -public: - explicit InvariantChecker_PROTOTYPE() = default; - - /** - * @brief called for each ledger entry in the current transaction. - * - * @param isDelete true if the SLE is being deleted - * @param before ledger entry before modification by the transaction - * @param after ledger entry after modification by the transaction - */ - void - visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after); - - /** - * @brief called after all ledger entries have been visited to determine - * the final status of the check - * - * @param tx the transaction being applied - * @param tec the current TER result of the transaction - * @param fee the fee actually charged for this transaction - * @param view a ReadView of the ledger being modified - * @param j journal for logging - * - * @return true if check passes, false if it fails - */ - bool - finalize( - STTx const& tx, - TER const tec, - XRPAmount const fee, - ReadView const& view, - beast::Journal const& j); -}; -#endif - -/** - * @brief Invariant: We should never charge a transaction a negative fee or a - * fee that is larger than what the transaction itself specifies. - * - * We can, in some circumstances, charge less. - */ -class TransactionFeeCheck -{ -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: A transaction must not create XRP and should only destroy - * the XRP fee. - * - * We iterate through all account roots, payment channels and escrow entries - * that were modified and calculate the net change in XRP caused by the - * transactions. - */ -class XRPNotCreated -{ - std::int64_t drops_ = 0; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: we cannot remove an account ledger entry - * - * We iterate all account roots that were modified, and ensure that any that - * were present before the transaction was applied continue to be present - * afterwards unless they were explicitly deleted by a successful - * AccountDelete transaction. - */ -class AccountRootsNotDeleted -{ - std::uint32_t accountsDeleted_ = 0; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: a deleted account must not have any objects left - * - * We iterate all deleted account roots, and ensure that there are no - * objects left that are directly accessible with that account's ID. - * - * There should only be one deleted account, but that's checked by - * AccountRootsNotDeleted. This invariant will handle multiple deleted account - * roots without a problem. - */ -class AccountRootsDeletedClean -{ - // Pair is . Before is used for most of the checks, so that - // if, for example, an object ID field is cleared, but the object is not - // deleted, it can still be found. After is used specifically for any checks - // that are expected as part of the deletion, such as zeroing out the - // balance. - std::vector, std::shared_ptr>> accountsDeleted_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: An account XRP balance must be in XRP and take a value - * between 0 and INITIAL_XRP drops, inclusive. - * - * We iterate all account roots modified by the transaction and ensure that - * their XRP balances are reasonable. - */ -class XRPBalanceChecks -{ - bool bad_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: corresponding modified ledger entries should match in type - * and added entries should be a valid type. - */ -class LedgerEntryTypesMatch -{ - bool typeMismatch_ = false; - bool invalidTypeAdded_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: Trust lines using XRP are not allowed. - * - * We iterate all the trust lines created by this transaction and ensure - * that they are against a valid issuer. - */ -class NoXRPTrustLines -{ - bool xrpTrustLine_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: Trust lines with deep freeze flag are not allowed if normal - * freeze flag is not set. - * - * We iterate all the trust lines created by this transaction and ensure - * that they don't have deep freeze flag set without normal freeze flag set. - */ -class NoDeepFreezeTrustLinesWithoutFreeze -{ - bool deepFreezeWithoutFreeze_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: frozen trust line balance change is not allowed. - * - * We iterate all affected trust lines and ensure that they don't have - * unexpected change of balance if they're frozen. - */ -class TransfersNotFrozen -{ - struct BalanceChange - { - std::shared_ptr const line; - int const balanceChangeSign; - }; - - struct IssuerChanges - { - std::vector senders; - std::vector receivers; - }; - - using ByIssuer = std::map; - ByIssuer balanceChanges_; - - std::map const> possibleIssuers_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); - -private: - bool - isValidEntry(std::shared_ptr const& before, std::shared_ptr const& after); - - STAmount - calculateBalanceChange( - std::shared_ptr const& before, - std::shared_ptr const& after, - bool isDelete); - - void - recordBalance(Issue const& issue, BalanceChange change); - - void - recordBalanceChanges(std::shared_ptr const& after, STAmount const& balanceChange); - - std::shared_ptr - findIssuer(AccountID const& issuerID, ReadView const& view); - - bool - validateIssuerChanges( - std::shared_ptr const& issuer, - IssuerChanges const& changes, - STTx const& tx, - beast::Journal const& j, - bool enforce); - - bool - validateFrozenState( - BalanceChange const& change, - bool high, - STTx const& tx, - beast::Journal const& j, - bool enforce, - bool globalFreeze); -}; - -/** - * @brief Invariant: offers should be for non-negative amounts and must not - * be XRP to XRP. - * - * Examine all offers modified by the transaction and ensure that there are - * no offers which contain negative amounts or which exchange XRP for XRP. - */ -class NoBadOffers -{ - bool bad_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: an escrow entry must take a value between 0 and - * INITIAL_XRP drops exclusive. - */ -class NoZeroEscrow -{ - bool bad_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: a new account root must be the consequence of a payment, - * must have the right starting sequence, and the payment - * may not create more than one new account root. - */ -class ValidNewAccountRoot -{ - std::uint32_t accountsCreated_ = 0; - std::uint32_t accountSeq_ = 0; - bool pseudoAccount_ = false; - std::uint32_t flags_ = 0; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: Validates several invariants for NFToken pages. - * - * The following checks are made: - * - The page is correctly associated with the owner. - * - The page is correctly ordered between the next and previous links. - * - The page contains at least one and no more than 32 NFTokens. - * - The NFTokens on this page do not belong on a lower or higher page. - * - The NFTokens are correctly sorted on the page. - * - Each URI, if present, is not empty. - */ -class ValidNFTokenPage -{ - bool badEntry_ = false; - bool badLink_ = false; - bool badSort_ = false; - bool badURI_ = false; - bool invalidSize_ = false; - bool deletedFinalPage_ = false; - bool deletedLink_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: Validates counts of NFTokens after all transaction types. - * - * The following checks are made: - * - The number of minted or burned NFTokens can only be changed by - * NFTokenMint or NFTokenBurn transactions. - * - A successful NFTokenMint must increase the number of NFTokens. - * - A failed NFTokenMint must not change the number of minted NFTokens. - * - An NFTokenMint transaction cannot change the number of burned NFTokens. - * - A successful NFTokenBurn must increase the number of burned NFTokens. - * - A failed NFTokenBurn must not change the number of burned NFTokens. - * - An NFTokenBurn transaction cannot change the number of minted NFTokens. - */ -class NFTokenCountTracking -{ - std::uint32_t beforeMintedTotal = 0; - std::uint32_t beforeBurnedTotal = 0; - std::uint32_t afterMintedTotal = 0; - std::uint32_t afterBurnedTotal = 0; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariant: Token holder's trustline balance cannot be negative after - * Clawback. - * - * We iterate all the trust lines affected by this transaction and ensure - * that no more than one trustline is modified, and also holder's balance is - * non-negative. - */ -class ValidClawback -{ - std::uint32_t trustlinesChanged = 0; - std::uint32_t mptokensChanged = 0; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -class ValidMPTIssuance -{ - std::uint32_t mptIssuancesCreated_ = 0; - std::uint32_t mptIssuancesDeleted_ = 0; - - std::uint32_t mptokensCreated_ = 0; - std::uint32_t mptokensDeleted_ = 0; - // non-MPT transactions may attempt to create - // MPToken by an issuer - bool mptCreatedByIssuer_ = false; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariants: Permissioned Domains must have some rules and - * AcceptedCredentials must have length between 1 and 10 inclusive. - * - * Since only permissions constitute rules, an empty credentials list - * means that there are no rules and the invariant is violated. - * - * Credentials must be sorted and no duplicates allowed - * - */ -class ValidPermissionedDomain -{ - struct SleStatus - { - std::size_t credentialsSize_{0}; - bool isSorted_ = false; - bool isUnique_ = false; - bool isDelete_ = false; - }; - std::vector sleStatus_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariants: Pseudo-accounts have valid and consistent properties - * - * Pseudo-accounts have certain properties, and some of those properties are - * unique to pseudo-accounts. Check that all pseudo-accounts are following the - * rules, and that only pseudo-accounts look like pseudo-accounts. - * - */ -class ValidPseudoAccounts -{ - std::vector errors_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -class ValidPermissionedDEX -{ - bool regularOffers_ = false; - bool badHybrids_ = false; - hash_set domains_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -class ValidAMM -{ - std::optional ammAccount_; - std::optional lptAMMBalanceAfter_; - std::optional lptAMMBalanceBefore_; - bool ammPoolChanged_; - -public: - enum class ZeroAllowed : bool { No = false, Yes = true }; - - ValidAMM() : ammPoolChanged_{false} - { - } - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); - -private: - bool - finalizeBid(bool enforce, beast::Journal const&) const; - bool - finalizeVote(bool enforce, beast::Journal const&) const; - bool - finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; - bool - finalizeDelete(bool enforce, TER res, beast::Journal const&) const; - bool - finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; - // Includes clawback - bool - finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; - bool - finalizeDEX(bool enforce, beast::Journal const&) const; - bool - generalInvariant(STTx const&, ReadView const&, ZeroAllowed zeroAllowed, beast::Journal const&) - const; -}; - -/** - * @brief Invariants: Some fields are unmodifiable - * - * Check that any fields specified as unmodifiable are not modified when the - * object is modified. Creation and deletion are ignored. - * - */ -class NoModifiedUnmodifiableFields -{ - // Pair is . - std::set> changedEntries_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariants: Loan brokers are internally consistent - * - * 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one - * node (the root), which will only hold entries for `RippleState` or - * `MPToken` objects. - * - */ -class ValidLoanBroker -{ - // Not all of these elements will necessarily be populated. Remaining items - // will be looked up as needed. - struct BrokerInfo - { - SLE::const_pointer brokerBefore = nullptr; - // After is used for most of the checks, except - // those that check changed values. - SLE::const_pointer brokerAfter = nullptr; - }; - // Collect all the LoanBrokers found directly or indirectly through - // pseudo-accounts. Key is the brokerID / index. It will be used to find the - // LoanBroker object if brokerBefore and brokerAfter are nullptr - std::map brokers_; - // Collect all the modified trust lines. Their high and low accounts will be - // loaded to look for LoanBroker pseudo-accounts. - std::vector lines_; - // Collect all the modified MPTokens. Their accounts will be loaded to look - // for LoanBroker pseudo-accounts. - std::vector mpts_; - - bool - goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/** - * @brief Invariants: Loans are internally consistent - * - * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` - * - */ -class ValidLoan -{ - // Pair is . After is used for most of the checks, except - // those that check changed values. - std::vector> loans_; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); -}; - -/* - * @brief Invariants: Vault object and MPTokenIssuance for vault shares - * - * - vault deleted and vault created is empty - * - vault created must be linked to pseudo-account for shares and assets - * - vault must have MPTokenIssuance for shares - * - vault without shares outstanding must have no shares - * - loss unrealized does not exceed the difference between assets total and - * assets available - * - assets available do not exceed assets total - * - vault deposit increases assets and share issuance, and adds to: - * total assets, assets available, shares outstanding - * - vault withdrawal and clawback reduce assets and share issuance, and - * subtracts from: total assets, assets available, shares outstanding - * - vault set must not alter the vault assets or shares balance - * - no vault transaction can change loss unrealized (it's updated by loan - * transactions) - * - */ -class ValidVault -{ - Number static constexpr zero{}; - - struct Vault final - { - uint256 key = beast::zero; - Asset asset = {}; - AccountID pseudoId = {}; - AccountID owner = {}; - uint192 shareMPTID = beast::zero; - Number assetsTotal = 0; - Number assetsAvailable = 0; - Number assetsMaximum = 0; - Number lossUnrealized = 0; - - Vault static make(SLE const&); - }; - - struct Shares final - { - MPTIssue share = {}; - std::uint64_t sharesTotal = 0; - std::uint64_t sharesMaximum = 0; - - Shares static make(SLE const&); - }; - -public: - struct DeltaInfo final - { - Number delta = numZero; - std::optional scale; - }; - -private: - std::vector afterVault_ = {}; - std::vector afterMPTs_ = {}; - std::vector beforeVault_ = {}; - std::vector beforeMPTs_ = {}; - std::unordered_map deltas_ = {}; - -public: - void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); - - bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); - - // Compute the coarsest scale required to represent all numbers - [[nodiscard]] static std::int32_t - computeMinScale(Asset const& asset, std::vector const& numbers); -}; - -// additional invariant checks can be declared above and then added to this -// tuple -using InvariantChecks = std::tuple< - TransactionFeeCheck, - AccountRootsNotDeleted, - AccountRootsDeletedClean, - LedgerEntryTypesMatch, - XRPBalanceChecks, - XRPNotCreated, - NoXRPTrustLines, - NoDeepFreezeTrustLinesWithoutFreeze, - TransfersNotFrozen, - NoBadOffers, - NoZeroEscrow, - ValidNewAccountRoot, - ValidNFTokenPage, - NFTokenCountTracking, - ValidClawback, - ValidMPTIssuance, - ValidPermissionedDomain, - ValidPermissionedDEX, - ValidAMM, - NoModifiedUnmodifiableFields, - ValidPseudoAccounts, - ValidLoanBroker, - ValidLoan, - ValidVault>; - -/** - * @brief get a tuple of all invariant checks - * - * @return std::tuple of instances that implement the required invariant check - * methods - * - * @see xrpl::InvariantChecker_PROTOTYPE - */ -inline InvariantChecks -getInvariantChecks() -{ - return InvariantChecks{}; -} - -} // namespace xrpl diff --git a/include/xrpl/tx/invariants/AMMInvariant.h b/include/xrpl/tx/invariants/AMMInvariant.h new file mode 100644 index 0000000000..63ebb804ae --- /dev/null +++ b/include/xrpl/tx/invariants/AMMInvariant.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class ValidAMM +{ + std::optional ammAccount_; + std::optional lptAMMBalanceAfter_; + std::optional lptAMMBalanceBefore_; + bool ammPoolChanged_; + +public: + enum class ZeroAllowed : bool { No = false, Yes = true }; + + ValidAMM() : ammPoolChanged_{false} + { + } + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); + +private: + bool + finalizeBid(bool enforce, beast::Journal const&) const; + bool + finalizeVote(bool enforce, beast::Journal const&) const; + bool + finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + bool + finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + bool + finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + // Includes clawback + bool + finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + bool + finalizeDEX(bool enforce, beast::Journal const&) const; + bool + generalInvariant(STTx const&, ReadView const&, ZeroAllowed zeroAllowed, beast::Journal const&) + const; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h b/include/xrpl/tx/invariants/FreezeInvariant.h new file mode 100644 index 0000000000..ac9d83166e --- /dev/null +++ b/include/xrpl/tx/invariants/FreezeInvariant.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Invariant: frozen trust line balance change is not allowed. + * + * We iterate all affected trust lines and ensure that they don't have + * unexpected change of balance if they're frozen. + */ +class TransfersNotFrozen +{ + struct BalanceChange + { + std::shared_ptr const line; + int const balanceChangeSign; + }; + + struct IssuerChanges + { + std::vector senders; + std::vector receivers; + }; + + using ByIssuer = std::map; + ByIssuer balanceChanges_; + + std::map const> possibleIssuers_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); + +private: + bool + isValidEntry(std::shared_ptr const& before, std::shared_ptr const& after); + + STAmount + calculateBalanceChange( + std::shared_ptr const& before, + std::shared_ptr const& after, + bool isDelete); + + void + recordBalance(Issue const& issue, BalanceChange change); + + void + recordBalanceChanges(std::shared_ptr const& after, STAmount const& balanceChange); + + std::shared_ptr + findIssuer(AccountID const& issuerID, ReadView const& view); + + bool + validateIssuerChanges( + std::shared_ptr const& issuer, + IssuerChanges const& changes, + STTx const& tx, + beast::Journal const& j, + bool enforce); + + bool + validateFrozenState( + BalanceChange const& change, + bool high, + STTx const& tx, + beast::Journal const& j, + bool enforce, + bool globalFreeze); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h new file mode 100644 index 0000000000..5ded5980da --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -0,0 +1,385 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +#if GENERATING_DOCS +/** + * @brief Prototype for invariant check implementations. + * + * __THIS CLASS DOES NOT EXIST__ - or rather it exists in documentation only to + * communicate the interface required of any invariant checker. Any invariant + * check implementation should implement the public methods documented here. + * + */ +class InvariantChecker_PROTOTYPE +{ +public: + explicit InvariantChecker_PROTOTYPE() = default; + + /** + * @brief called for each ledger entry in the current transaction. + * + * @param isDelete true if the SLE is being deleted + * @param before ledger entry before modification by the transaction + * @param after ledger entry after modification by the transaction + */ + void + visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after); + + /** + * @brief called after all ledger entries have been visited to determine + * the final status of the check + * + * @param tx the transaction being applied + * @param tec the current TER result of the transaction + * @param fee the fee actually charged for this transaction + * @param view a ReadView of the ledger being modified + * @param j journal for logging + * + * @return true if check passes, false if it fails + */ + bool + finalize( + STTx const& tx, + TER const tec, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j); +}; +#endif + +/** + * @brief Invariant: We should never charge a transaction a negative fee or a + * fee that is larger than what the transaction itself specifies. + * + * We can, in some circumstances, charge less. + */ +class TransactionFeeCheck +{ +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: A transaction must not create XRP and should only destroy + * the XRP fee. + * + * We iterate through all account roots, payment channels and escrow entries + * that were modified and calculate the net change in XRP caused by the + * transactions. + */ +class XRPNotCreated +{ + std::int64_t drops_ = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: we cannot remove an account ledger entry + * + * We iterate all account roots that were modified, and ensure that any that + * were present before the transaction was applied continue to be present + * afterwards unless they were explicitly deleted by a successful + * AccountDelete transaction. + */ +class AccountRootsNotDeleted +{ + std::uint32_t accountsDeleted_ = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: a deleted account must not have any objects left + * + * We iterate all deleted account roots, and ensure that there are no + * objects left that are directly accessible with that account's ID. + * + * There should only be one deleted account, but that's checked by + * AccountRootsNotDeleted. This invariant will handle multiple deleted account + * roots without a problem. + */ +class AccountRootsDeletedClean +{ + // Pair is . Before is used for most of the checks, so that + // if, for example, an object ID field is cleared, but the object is not + // deleted, it can still be found. After is used specifically for any checks + // that are expected as part of the deletion, such as zeroing out the + // balance. + std::vector, std::shared_ptr>> accountsDeleted_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: An account XRP balance must be in XRP and take a value + * between 0 and INITIAL_XRP drops, inclusive. + * + * We iterate all account roots modified by the transaction and ensure that + * their XRP balances are reasonable. + */ +class XRPBalanceChecks +{ + bool bad_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: corresponding modified ledger entries should match in type + * and added entries should be a valid type. + */ +class LedgerEntryTypesMatch +{ + bool typeMismatch_ = false; + bool invalidTypeAdded_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: Trust lines using XRP are not allowed. + * + * We iterate all the trust lines created by this transaction and ensure + * that they are against a valid issuer. + */ +class NoXRPTrustLines +{ + bool xrpTrustLine_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: Trust lines with deep freeze flag are not allowed if normal + * freeze flag is not set. + * + * We iterate all the trust lines created by this transaction and ensure + * that they don't have deep freeze flag set without normal freeze flag set. + */ +class NoDeepFreezeTrustLinesWithoutFreeze +{ + bool deepFreezeWithoutFreeze_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: offers should be for non-negative amounts and must not + * be XRP to XRP. + * + * Examine all offers modified by the transaction and ensure that there are + * no offers which contain negative amounts or which exchange XRP for XRP. + */ +class NoBadOffers +{ + bool bad_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: an escrow entry must take a value between 0 and + * INITIAL_XRP drops exclusive. + */ +class NoZeroEscrow +{ + bool bad_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: a new account root must be the consequence of a payment, + * must have the right starting sequence, and the payment + * may not create more than one new account root. + */ +class ValidNewAccountRoot +{ + std::uint32_t accountsCreated_ = 0; + std::uint32_t accountSeq_ = 0; + bool pseudoAccount_ = false; + std::uint32_t flags_ = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: Token holder's trustline balance cannot be negative after + * Clawback. + * + * We iterate all the trust lines affected by this transaction and ensure + * that no more than one trustline is modified, and also holder's balance is + * non-negative. + */ +class ValidClawback +{ + std::uint32_t trustlinesChanged = 0; + std::uint32_t mptokensChanged = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariants: Pseudo-accounts have valid and consistent properties + * + * Pseudo-accounts have certain properties, and some of those properties are + * unique to pseudo-accounts. Check that all pseudo-accounts are following the + * rules, and that only pseudo-accounts look like pseudo-accounts. + * + */ +class ValidPseudoAccounts +{ + std::vector errors_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariants: Some fields are unmodifiable + * + * Check that any fields specified as unmodifiable are not modified when the + * object is modified. Creation and deletion are ignored. + * + */ +class NoModifiedUnmodifiableFields +{ + // Pair is . + std::set> changedEntries_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +// additional invariant checks can be declared above and then added to this +// tuple +using InvariantChecks = std::tuple< + TransactionFeeCheck, + AccountRootsNotDeleted, + AccountRootsDeletedClean, + LedgerEntryTypesMatch, + XRPBalanceChecks, + XRPNotCreated, + NoXRPTrustLines, + NoDeepFreezeTrustLinesWithoutFreeze, + TransfersNotFrozen, + NoBadOffers, + NoZeroEscrow, + ValidNewAccountRoot, + ValidNFTokenPage, + NFTokenCountTracking, + ValidClawback, + ValidMPTIssuance, + ValidPermissionedDomain, + ValidPermissionedDEX, + ValidAMM, + NoModifiedUnmodifiableFields, + ValidPseudoAccounts, + ValidLoanBroker, + ValidLoan, + ValidVault>; + +/** + * @brief get a tuple of all invariant checks + * + * @return std::tuple of instances that implement the required invariant check + * methods + * + * @see xrpl::InvariantChecker_PROTOTYPE + */ +inline InvariantChecks +getInvariantChecks() +{ + return InvariantChecks{}; +} + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h new file mode 100644 index 0000000000..161b3572db --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include + +namespace xrpl { + +/* +assert(enforce) + +There are several asserts (or XRPL_ASSERTs) in invariant check files that check +a variable named `enforce` when an invariant fails. At first glance, those +asserts may look incorrect, but they are not. + +Those asserts take advantage of two facts: +1. `asserts` are not (normally) executed in release builds. +2. Invariants should *never* fail, except in tests that specifically modify + the open ledger to break them. + +This makes `assert(enforce)` sort of a second-layer of invariant enforcement +aimed at _developers_. It's designed to fire if a developer writes code that +violates an invariant, and runs it in unit tests or a develop build that _does +not have the relevant amendments enabled_. It's intentionally a pain in the neck +so that bad code gets caught and fixed as early as possible. +*/ + +enum Privilege { + noPriv = 0x0000, // The transaction can not do any of the enumerated operations + createAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object. + createPseudoAcct = 0x0002, // The transaction can create a pseudo account, + // which implies createAcct + mustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object + mayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT + // object, but does not have to + overrideFreeze = 0x0010, // The transaction can override some freeze rules + changeNFTCounts = 0x0020, // The transaction can mint or burn an NFT + createMPTIssuance = 0x0040, // The transaction can create a new MPT issuance + destroyMPTIssuance = 0x0080, // The transaction can destroy an MPT issuance + mustAuthorizeMPT = 0x0100, // The transaction MUST create or delete an MPT + // object (except by issuer) + mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT + // object (except by issuer) + mayDeleteMPT = 0x0400, // The transaction MAY delete an MPT object. May not create. + mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault + mayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault +}; + +constexpr Privilege +operator|(Privilege lhs, Privilege rhs) +{ + return safe_cast( + safe_cast>(lhs) | + safe_cast>(rhs)); +} + +bool +hasPrivilege(STTx const& tx, Privilege priv); + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/LoanInvariant.h b/include/xrpl/tx/invariants/LoanInvariant.h new file mode 100644 index 0000000000..be771cd582 --- /dev/null +++ b/include/xrpl/tx/invariants/LoanInvariant.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Invariants: Loan brokers are internally consistent + * + * 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one + * node (the root), which will only hold entries for `RippleState` or + * `MPToken` objects. + * + */ +class ValidLoanBroker +{ + // Not all of these elements will necessarily be populated. Remaining items + // will be looked up as needed. + struct BrokerInfo + { + SLE::const_pointer brokerBefore = nullptr; + // After is used for most of the checks, except + // those that check changed values. + SLE::const_pointer brokerAfter = nullptr; + }; + // Collect all the LoanBrokers found directly or indirectly through + // pseudo-accounts. Key is the brokerID / index. It will be used to find the + // LoanBroker object if brokerBefore and brokerAfter are nullptr + std::map brokers_; + // Collect all the modified trust lines. Their high and low accounts will be + // loaded to look for LoanBroker pseudo-accounts. + std::vector lines_; + // Collect all the modified MPTokens. Their accounts will be loaded to look + // for LoanBroker pseudo-accounts. + std::vector mpts_; + + bool + goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariants: Loans are internally consistent + * + * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` + * + */ +class ValidLoan +{ + // Pair is . After is used for most of the checks, except + // those that check changed values. + std::vector> loans_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h new file mode 100644 index 0000000000..b6533c263d --- /dev/null +++ b/include/xrpl/tx/invariants/MPTInvariant.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace xrpl { + +class ValidMPTIssuance +{ + std::uint32_t mptIssuancesCreated_ = 0; + std::uint32_t mptIssuancesDeleted_ = 0; + + std::uint32_t mptokensCreated_ = 0; + std::uint32_t mptokensDeleted_ = 0; + // non-MPT transactions may attempt to create + // MPToken by an issuer + bool mptCreatedByIssuer_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/NFTInvariant.h b/include/xrpl/tx/invariants/NFTInvariant.h new file mode 100644 index 0000000000..8a88ca1c63 --- /dev/null +++ b/include/xrpl/tx/invariants/NFTInvariant.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Invariant: Validates several invariants for NFToken pages. + * + * The following checks are made: + * - The page is correctly associated with the owner. + * - The page is correctly ordered between the next and previous links. + * - The page contains at least one and no more than 32 NFTokens. + * - The NFTokens on this page do not belong on a lower or higher page. + * - The NFTokens are correctly sorted on the page. + * - Each URI, if present, is not empty. + */ +class ValidNFTokenPage +{ + bool badEntry_ = false; + bool badLink_ = false; + bool badSort_ = false; + bool badURI_ = false; + bool invalidSize_ = false; + bool deletedFinalPage_ = false; + bool deletedLink_ = false; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +/** + * @brief Invariant: Validates counts of NFTokens after all transaction types. + * + * The following checks are made: + * - The number of minted or burned NFTokens can only be changed by + * NFTokenMint or NFTokenBurn transactions. + * - A successful NFTokenMint must increase the number of NFTokens. + * - A failed NFTokenMint must not change the number of minted NFTokens. + * - An NFTokenMint transaction cannot change the number of burned NFTokens. + * - A successful NFTokenBurn must increase the number of burned NFTokens. + * - A failed NFTokenBurn must not change the number of burned NFTokens. + * - An NFTokenBurn transaction cannot change the number of minted NFTokens. + */ +class NFTokenCountTracking +{ + std::uint32_t beforeMintedTotal = 0; + std::uint32_t beforeBurnedTotal = 0; + std::uint32_t afterMintedTotal = 0; + std::uint32_t afterBurnedTotal = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h new file mode 100644 index 0000000000..b4e06cd212 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace xrpl { + +class ValidPermissionedDEX +{ + bool regularOffers_ = false; + bool badHybrids_ = false; + hash_set domains_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h new file mode 100644 index 0000000000..f6c902ecb2 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Invariants: Permissioned Domains must have some rules and + * AcceptedCredentials must have length between 1 and 10 inclusive. + * + * Since only permissions constitute rules, an empty credentials list + * means that there are no rules and the invariant is violated. + * + * Credentials must be sorted and no duplicates allowed + * + */ +class ValidPermissionedDomain +{ + struct SleStatus + { + std::size_t credentialsSize_{0}; + bool isSorted_ = false; + bool isUnique_ = false; + bool isDelete_ = false; + }; + std::vector sleStatus_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/VaultInvariant.h b/include/xrpl/tx/invariants/VaultInvariant.h new file mode 100644 index 0000000000..1e1ded6fa1 --- /dev/null +++ b/include/xrpl/tx/invariants/VaultInvariant.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +/* + * @brief Invariants: Vault object and MPTokenIssuance for vault shares + * + * - vault deleted and vault created is empty + * - vault created must be linked to pseudo-account for shares and assets + * - vault must have MPTokenIssuance for shares + * - vault without shares outstanding must have no shares + * - loss unrealized does not exceed the difference between assets total and + * assets available + * - assets available do not exceed assets total + * - vault deposit increases assets and share issuance, and adds to: + * total assets, assets available, shares outstanding + * - vault withdrawal and clawback reduce assets and share issuance, and + * subtracts from: total assets, assets available, shares outstanding + * - vault set must not alter the vault assets or shares balance + * - no vault transaction can change loss unrealized (it's updated by loan + * transactions) + * + */ +class ValidVault +{ + Number static constexpr zero{}; + + struct Vault final + { + uint256 key = beast::zero; + Asset asset = {}; + AccountID pseudoId = {}; + AccountID owner = {}; + uint192 shareMPTID = beast::zero; + Number assetsTotal = 0; + Number assetsAvailable = 0; + Number assetsMaximum = 0; + Number lossUnrealized = 0; + + Vault static make(SLE const&); + }; + + struct Shares final + { + MPTIssue share = {}; + std::uint64_t sharesTotal = 0; + std::uint64_t sharesMaximum = 0; + + Shares static make(SLE const&); + }; + +public: + struct DeltaInfo final + { + Number delta = numZero; + std::optional scale; + }; + +private: + std::vector afterVault_ = {}; + std::vector afterMPTs_ = {}; + std::vector beforeVault_ = {}; + std::vector beforeMPTs_ = {}; + std::unordered_map deltas_ = {}; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); + + // Compute the coarsest scale required to represent all numbers + [[nodiscard]] static std::int32_t + computeMinScale(Asset const& asset, std::vector const& numbers); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/Lending/LendingHelpers.h b/include/xrpl/tx/transactors/Lending/LendingHelpers.h index 1d77c2158e..8dd6866ac3 100644 --- a/include/xrpl/tx/transactors/Lending/LendingHelpers.h +++ b/include/xrpl/tx/transactors/Lending/LendingHelpers.h @@ -171,7 +171,7 @@ getAssetsTotalScale(SLE::const_ref vaultSle) { if (!vaultSle) return Number::minExponent - 1; // LCOV_EXCL_LINE - return vaultSle->at(sfAssetsTotal).scale(vaultSle->at(sfAsset)); + return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset)); } TER diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000000..1d907f4d87 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,140 @@ +{ pkgs, ... }: +let + commonPackages = with pkgs; [ + ccache + cmake + conan + gcovr + git + gnumake + llvmPackages_21.clang-tools + ninja + perl # needed for openssl + pkg-config + pre-commit + python314 + ]; + + # Supported compiler versions + gccVersion = pkgs.lib.range 13 15; + clangVersions = pkgs.lib.range 18 21; + + defaultCompiler = if pkgs.stdenv.isDarwin then "apple-clang" else "gcc"; + defaultGccVersion = pkgs.lib.last gccVersion; + defaultClangVersion = pkgs.lib.last clangVersions; + + strToCompilerEnv = + compiler: version: + ( + if compiler == "gcc" then + let + gccPkg = pkgs."gcc${toString version}Stdenv" or null; + in + if gccPkg != null && builtins.elem version gccVersion then + gccPkg + else + throw "Invalid GCC version: ${toString version}. Must be one of: ${toString gccVersion}" + else if compiler == "clang" then + let + clangPkg = pkgs."llvmPackages_${toString version}".stdenv or null; + in + if clangPkg != null && builtins.elem version clangVersions then + clangPkg + else + throw "Invalid Clang version: ${toString version}. Must be one of: ${toString clangVersions}" + else if compiler == "apple-clang" || compiler == "none" then + pkgs.stdenvNoCC + else + throw "Invalid compiler: ${compiler}. Must be one of: gcc, clang, apple-clang, none" + ); + + # Helper function to create a shell with a specific compiler + makeShell = + { + compiler ? defaultCompiler, + version ? ( + if compiler == "gcc" then + defaultGccVersion + else if compiler == "clang" then + defaultClangVersion + else + null + ), + }: + let + compilerStdEnv = strToCompilerEnv compiler version; + + compilerName = + if compiler == "apple-clang" then + "clang" + else if compiler == "none" then + null + else + compiler; + + gccOnMacWarning = + if pkgs.stdenv.isDarwin && compiler == "gcc" then + '' + echo "WARNING: Using GCC on macOS with Conan may not work." + echo " Consider using 'nix develop .#clang' or the default shell instead." + echo "" + '' + else + ""; + + compilerVersion = + if compilerName != null then + '' + echo "Compiler: " + ${compilerName} --version + '' + else + '' + echo "No compiler specified - using system compiler" + ''; + + shellAttrs = { + packages = commonPackages; + + shellHook = '' + echo "Welcome to xrpld development shell"; + ${gccOnMacWarning}${compilerVersion} + ''; + }; + in + pkgs.mkShell.override { stdenv = compilerStdEnv; } shellAttrs; + + # Generate shells for each compiler version + gccShells = builtins.listToAttrs ( + map (version: { + name = "gcc${toString version}"; + value = makeShell { + compiler = "gcc"; + version = version; + }; + }) gccVersion + ); + + clangShells = builtins.listToAttrs ( + map (version: { + name = "clang${toString version}"; + value = makeShell { + compiler = "clang"; + version = version; + }; + }) clangVersions + ); + +in +gccShells +// clangShells +// { + # Default shells + default = makeShell { }; + gcc = makeShell { compiler = "gcc"; }; + clang = makeShell { compiler = "clang"; }; + + # No compiler + no-compiler = makeShell { compiler = "none"; }; + apple-clang = makeShell { compiler = "apple-clang"; }; +} diff --git a/nix/utils.nix b/nix/utils.nix new file mode 100644 index 0000000000..821d60a6f6 --- /dev/null +++ b/nix/utils.nix @@ -0,0 +1,19 @@ +{ nixpkgs }: +{ + forEachSystem = + function: + nixpkgs.lib.genAttrs + [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ] + ( + system: + function { + inherit system; + pkgs = import nixpkgs { inherit system; }; + } + ); +} diff --git a/src/libxrpl/basics/MallocTrim.cpp b/src/libxrpl/basics/MallocTrim.cpp new file mode 100644 index 0000000000..1b0932b39d --- /dev/null +++ b/src/libxrpl/basics/MallocTrim.cpp @@ -0,0 +1,157 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include + +#if defined(__GLIBC__) && BOOST_OS_LINUX +#include + +#include +#include + +// Require RUSAGE_THREAD for thread-scoped page fault tracking +#ifndef RUSAGE_THREAD +#error "MallocTrim rusage instrumentation requires RUSAGE_THREAD on Linux/glibc" +#endif + +namespace { + +bool +getRusageThread(struct rusage& ru) +{ + return ::getrusage(RUSAGE_THREAD, &ru) == 0; // LCOV_EXCL_LINE +} + +} // namespace +#endif + +namespace xrpl { + +namespace detail { + +// cSpell:ignore statm + +#if defined(__GLIBC__) && BOOST_OS_LINUX + +inline int +mallocTrimWithPad(std::size_t padBytes) +{ + return ::malloc_trim(padBytes); +} + +long +parseStatmRSSkB(std::string const& statm) +{ + // /proc/self/statm format: size resident shared text lib data dt + // We want the second field (resident) which is in pages + std::istringstream iss(statm); + long size, resident; + if (!(iss >> size >> resident)) + return -1; + + // Convert pages to KB + long const pageSize = ::sysconf(_SC_PAGESIZE); + if (pageSize <= 0) + return -1; + + return (resident * pageSize) / 1024; +} + +#endif // __GLIBC__ && BOOST_OS_LINUX + +} // namespace detail + +MallocTrimReport +mallocTrim(std::string_view tag, beast::Journal journal) +{ + // LCOV_EXCL_START + + MallocTrimReport report; + +#if !(defined(__GLIBC__) && BOOST_OS_LINUX) + JLOG(journal.debug()) << "malloc_trim not supported on this platform (tag=" << tag << ")"; +#else + // Keep glibc malloc_trim padding at 0 (default): 12h Mainnet tests across 0/256KB/1MB/16MB + // showed no clear, consistent benefit from custom padding—0 provided the best overall balance + // of RSS reduction and trim-latency stability without adding a tuning surface. + constexpr std::size_t TRIM_PAD = 0; + + report.supported = true; + + if (journal.debug()) + { + auto readFile = [](std::string const& path) -> std::string { + std::ifstream ifs(path, std::ios::in | std::ios::binary); + if (!ifs.is_open()) + return {}; + + // /proc files are often not seekable; read as a stream. + std::ostringstream oss; + oss << ifs.rdbuf(); + return oss.str(); + }; + + std::string const tagStr{tag}; + std::string const statmPath = "/proc/self/statm"; + + auto const statmBefore = readFile(statmPath); + long const rssBeforeKB = detail::parseStatmRSSkB(statmBefore); + + struct rusage ru0{}; + bool const have_ru0 = getRusageThread(ru0); + + auto const t0 = std::chrono::steady_clock::now(); + + report.trimResult = detail::mallocTrimWithPad(TRIM_PAD); + + auto const t1 = std::chrono::steady_clock::now(); + + struct rusage ru1{}; + bool const have_ru1 = getRusageThread(ru1); + + auto const statmAfter = readFile(statmPath); + long const rssAfterKB = detail::parseStatmRSSkB(statmAfter); + + // Populate report fields + report.rssBeforeKB = rssBeforeKB; + report.rssAfterKB = rssAfterKB; + report.durationUs = std::chrono::duration_cast(t1 - t0); + + if (have_ru0 && have_ru1) + { + report.minfltDelta = ru1.ru_minflt - ru0.ru_minflt; + report.majfltDelta = ru1.ru_majflt - ru0.ru_majflt; + } + + std::int64_t const deltaKB = (rssBeforeKB < 0 || rssAfterKB < 0) + ? 0 + : (static_cast(rssAfterKB) - static_cast(rssBeforeKB)); + + JLOG(journal.debug()) << "malloc_trim tag=" << tagStr << " result=" << report.trimResult + << " pad=" << TRIM_PAD << " bytes" + << " rss_before=" << rssBeforeKB << "kB" + << " rss_after=" << rssAfterKB << "kB" + << " delta=" << deltaKB << "kB" + << " duration_us=" << report.durationUs.count() + << " minflt_delta=" << report.minfltDelta + << " majflt_delta=" << report.majfltDelta; + } + else + { + report.trimResult = detail::mallocTrimWithPad(TRIM_PAD); + } + +#endif + + return report; + + // LCOV_EXCL_STOP +} + +} // namespace xrpl diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp index 8462a00b3d..143bc51bd8 100644 --- a/src/libxrpl/beast/insight/StatsDCollector.cpp +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp @@ -249,7 +249,7 @@ public: { m_timer.cancel(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { // ignored } diff --git a/src/libxrpl/ledger/OpenView.cpp b/src/libxrpl/ledger/OpenView.cpp index d27d755c66..5b94be5da8 100644 --- a/src/libxrpl/ledger/OpenView.cpp +++ b/src/libxrpl/ledger/OpenView.cpp @@ -72,8 +72,8 @@ OpenView::OpenView( ReadView const* base, Rules const& rules, std::shared_ptr hold) - : monotonic_resource_{std::make_unique( - initialBufferSize)} + : monotonic_resource_{ + std::make_unique(initialBufferSize)} , txs_{monotonic_resource_.get()} , rules_(rules) , header_(base->header()) @@ -88,8 +88,8 @@ OpenView::OpenView( } OpenView::OpenView(ReadView const* base, std::shared_ptr hold) - : monotonic_resource_{std::make_unique( - initialBufferSize)} + : monotonic_resource_{ + std::make_unique(initialBufferSize)} , txs_{monotonic_resource_.get()} , rules_(base->rules()) , header_(base->header()) diff --git a/src/libxrpl/nodestore/DatabaseNodeImp.cpp b/src/libxrpl/nodestore/DatabaseNodeImp.cpp index 5596cb4853..d1452dba86 100644 --- a/src/libxrpl/nodestore/DatabaseNodeImp.cpp +++ b/src/libxrpl/nodestore/DatabaseNodeImp.cpp @@ -33,7 +33,7 @@ DatabaseNodeImp::fetchNodeObject( try { - status = backend_->fetch(hash.data(), &nodeObject); + status = backend_->fetch(hash, &nodeObject); } catch (std::exception const& e) { @@ -68,18 +68,10 @@ DatabaseNodeImp::fetchBatch(std::vector const& hashes) using namespace std::chrono; auto const before = steady_clock::now(); - std::vector batch{}; - batch.reserve(hashes.size()); - for (size_t i = 0; i < hashes.size(); ++i) - { - auto const& hash = hashes[i]; - batch.push_back(&hash); - } - // Get the node objects that match the hashes from the backend. To protect // against the backends returning fewer or more results than expected, the // container is resized to the number of hashes. - auto results = backend_->fetchBatch(batch).first; + auto results = backend_->fetchBatch(hashes).first; XRPL_ASSERT( results.size() == hashes.size() || results.empty(), "number of output objects either matches number of input hashes or is empty"); diff --git a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp index 26d8c30931..e058fa76ac 100644 --- a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp +++ b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp @@ -105,7 +105,7 @@ DatabaseRotatingImp::fetchNodeObject( std::shared_ptr nodeObject; try { - status = backend->fetch(hash.data(), &nodeObject); + status = backend->fetch(hash, &nodeObject); } catch (std::exception const& e) { diff --git a/src/libxrpl/nodestore/backend/MemoryFactory.cpp b/src/libxrpl/nodestore/backend/MemoryFactory.cpp index 8ac23a0bb6..b11d90610a 100644 --- a/src/libxrpl/nodestore/backend/MemoryFactory.cpp +++ b/src/libxrpl/nodestore/backend/MemoryFactory.cpp @@ -116,10 +116,9 @@ public: //-------------------------------------------------------------------------- Status - fetch(void const* key, std::shared_ptr* pObject) override + fetch(uint256 const& hash, std::shared_ptr* pObject) override { XRPL_ASSERT(db_, "xrpl::NodeStore::MemoryBackend::fetch : non-null database"); - uint256 const hash(uint256::fromVoid(key)); std::lock_guard _(db_->mutex); @@ -134,14 +133,14 @@ public: } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { std::vector> results; results.reserve(hashes.size()); for (auto const& h : hashes) { std::shared_ptr nObj; - Status status = fetch(h->begin(), &nObj); + Status status = fetch(h, &nObj); if (status != ok) results.push_back({}); else diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp index e8efa464af..c79938bcf8 100644 --- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp @@ -83,7 +83,7 @@ public: // close can throw and we don't want the destructor to throw. close(); } - catch (nudb::system_error const&) + catch (nudb::system_error const&) // NOLINT(bugprone-empty-catch) { // Don't allow exceptions to propagate out of destructors. // close() has already logged the error. @@ -179,17 +179,17 @@ public: } Status - fetch(void const* key, std::shared_ptr* pno) override + fetch(uint256 const& hash, std::shared_ptr* pno) override { Status status; pno->reset(); nudb::error_code ec; db_.fetch( - key, - [key, pno, &status](void const* data, std::size_t size) { + hash.data(), + [&hash, pno, &status](void const* data, std::size_t size) { nudb::detail::buffer bf; auto const result = nodeobject_decompress(data, size, bf); - DecodedBlob decoded(key, result.first, result.second); + DecodedBlob decoded(hash.data(), result.first, result.second); if (!decoded.wasOk()) { status = dataCorrupt; @@ -207,14 +207,14 @@ public: } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { std::vector> results; results.reserve(hashes.size()); for (auto const& h : hashes) { std::shared_ptr nObj; - Status status = fetch(h->begin(), &nObj); + Status status = fetch(h, &nObj); if (status != ok) results.push_back({}); else diff --git a/src/libxrpl/nodestore/backend/NullFactory.cpp b/src/libxrpl/nodestore/backend/NullFactory.cpp index 4ecca46a9a..ab5b7d0117 100644 --- a/src/libxrpl/nodestore/backend/NullFactory.cpp +++ b/src/libxrpl/nodestore/backend/NullFactory.cpp @@ -36,13 +36,13 @@ public: } Status - fetch(void const*, std::shared_ptr*) override + fetch(uint256 const&, std::shared_ptr*) override { return notFound; } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { return {}; } diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index c84c5f6982..01bc74f5ed 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -244,7 +244,7 @@ public: //-------------------------------------------------------------------------- Status - fetch(void const* key, std::shared_ptr* pObject) override + fetch(uint256 const& hash, std::shared_ptr* pObject) override { XRPL_ASSERT(m_db, "xrpl::NodeStore::RocksDBBackend::fetch : non-null database"); pObject->reset(); @@ -252,7 +252,7 @@ public: Status status(ok); rocksdb::ReadOptions const options; - rocksdb::Slice const slice(static_cast(key), m_keyBytes); + rocksdb::Slice const slice(std::bit_cast(hash.data()), m_keyBytes); std::string string; @@ -260,7 +260,7 @@ public: if (getStatus.ok()) { - DecodedBlob decoded(key, string.data(), string.size()); + DecodedBlob decoded(hash.data(), string.data(), string.size()); if (decoded.wasOk()) { @@ -295,14 +295,14 @@ public: } std::pair>, Status> - fetchBatch(std::vector const& hashes) override + fetchBatch(std::vector const& hashes) override { std::vector> results; results.reserve(hashes.size()); for (auto const& h : hashes) { std::shared_ptr nObj; - Status status = fetch(h->begin(), &nObj); + Status status = fetch(h, &nObj); if (status != ok) results.push_back({}); else @@ -332,9 +332,8 @@ public: EncodedBlob encoded(e); wb.Put( - rocksdb::Slice(reinterpret_cast(encoded.getKey()), m_keyBytes), - rocksdb::Slice( - reinterpret_cast(encoded.getData()), encoded.getSize())); + rocksdb::Slice(std::bit_cast(encoded.getKey()), m_keyBytes), + rocksdb::Slice(std::bit_cast(encoded.getData()), encoded.getSize())); } rocksdb::WriteOptions const options; diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 650cc4369d..9503da57a2 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -443,6 +443,7 @@ getRate(STAmount const& offerOut, STAmount const& offerIn) { if (offerOut == beast::zero) return 0; + try { STAmount r = divide(offerIn, offerOut, noIssue()); @@ -454,12 +455,11 @@ getRate(STAmount const& offerOut, STAmount const& offerIn) std::uint64_t ret = r.exponent() + 100; return (ret << (64 - 8)) | r.mantissa(); } - catch (std::exception const&) + catch (...) { + // overflow -- very bad offer + return 0; } - - // overflow -- very bad offer - return 0; } /** diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 0c5e299702..098ca1a400 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -246,10 +246,10 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const return signingPubKey.empty() ? checkMultiSign(rules, sigObject) : checkSingleSign(sigObject); } - catch (std::exception const&) + catch (...) { + return Unexpected("Internal signature check failure."); } - return Unexpected("Internal signature check failure."); } Expected diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index 994077da56..6218bb6db6 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -133,9 +133,9 @@ STVar::constructST(SerializedTypeID id, int depth, Args&&... args) { construct(std::forward(args)...); } - else if constexpr (std::is_same_v< - std::tuple...>, - std::tuple>) + else if constexpr ( + std:: + is_same_v...>, std::tuple>) { construct(std::forward(args)..., depth); } diff --git a/src/libxrpl/tx/ApplyContext.cpp b/src/libxrpl/tx/ApplyContext.cpp index a8eca09ff2..f62c63d1e6 100644 --- a/src/libxrpl/tx/ApplyContext.cpp +++ b/src/libxrpl/tx/ApplyContext.cpp @@ -1,8 +1,9 @@ +#include +// #include #include #include -#include -#include +#include namespace xrpl { diff --git a/src/libxrpl/tx/InvariantCheck.cpp b/src/libxrpl/tx/InvariantCheck.cpp deleted file mode 100644 index c43d7b2eae..0000000000 --- a/src/libxrpl/tx/InvariantCheck.cpp +++ /dev/null @@ -1,3628 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace xrpl { - -/* -assert(enforce) - -There are several asserts (or XRPL_ASSERTs) in this file that check a variable -named `enforce` when an invariant fails. At first glance, those asserts may look -incorrect, but they are not. - -Those asserts take advantage of two facts: -1. `asserts` are not (normally) executed in release builds. -2. Invariants should *never* fail, except in tests that specifically modify - the open ledger to break them. - -This makes `assert(enforce)` sort of a second-layer of invariant enforcement -aimed at _developers_. It's designed to fire if a developer writes code that -violates an invariant, and runs it in unit tests or a develop build that _does -not have the relevant amendments enabled_. It's intentionally a pain in the neck -so that bad code gets caught and fixed as early as possible. -*/ - -enum Privilege { - noPriv = 0x0000, // The transaction can not do any of the enumerated operations - createAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object. - createPseudoAcct = 0x0002, // The transaction can create a pseudo account, - // which implies createAcct - mustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object - mayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT - // object, but does not have to - overrideFreeze = 0x0010, // The transaction can override some freeze rules - changeNFTCounts = 0x0020, // The transaction can mint or burn an NFT - createMPTIssuance = 0x0040, // The transaction can create a new MPT issuance - destroyMPTIssuance = 0x0080, // The transaction can destroy an MPT issuance - mustAuthorizeMPT = 0x0100, // The transaction MUST create or delete an MPT - // object (except by issuer) - mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT - // object (except by issuer) - mayDeleteMPT = 0x0400, // The transaction MAY delete an MPT object. May not create. - mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault - mayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault -}; -constexpr Privilege -operator|(Privilege lhs, Privilege rhs) -{ - return safe_cast( - safe_cast>(lhs) | - safe_cast>(rhs)); -} - -#pragma push_macro("TRANSACTION") -#undef TRANSACTION - -#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \ - case tag: { \ - return (privileges) & priv; \ - } - -bool -hasPrivilege(STTx const& tx, Privilege priv) -{ - switch (tx.getTxnType()) - { -#include - - // Deprecated types - default: - return false; - } -}; - -#undef TRANSACTION -#pragma pop_macro("TRANSACTION") - -void -TransactionFeeCheck::visitEntry( - bool, - std::shared_ptr const&, - std::shared_ptr const&) -{ - // nothing to do -} - -bool -TransactionFeeCheck::finalize( - STTx const& tx, - TER const, - XRPAmount const fee, - ReadView const&, - beast::Journal const& j) -{ - // We should never charge a negative fee - if (fee.drops() < 0) - { - JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops(); - return false; - } - - // We should never charge a fee that's greater than or equal to the - // entire XRP supply. - if (fee >= INITIAL_XRP) - { - JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops(); - return false; - } - - // We should never charge more for a transaction than the transaction - // authorizes. It's possible to charge less in some circumstances. - if (fee > tx.getFieldAmount(sfFee).xrp()) - { - JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops() - << " exceeds fee specified in transaction."; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -XRPNotCreated::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - /* We go through all modified ledger entries, looking only at account roots, - * escrow payments, and payment channels. We remove from the total any - * previous XRP values and add to the total any new XRP values. The net - * balance of a payment channel is computed from two fields (amount and - * balance) and deletions are ignored for paychan and escrow because the - * amount fields have not been adjusted for those in the case of deletion. - */ - if (before) - { - switch (before->getType()) - { - case ltACCOUNT_ROOT: - drops_ -= (*before)[sfBalance].xrp().drops(); - break; - case ltPAYCHAN: - drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); - break; - case ltESCROW: - if (isXRP((*before)[sfAmount])) - drops_ -= (*before)[sfAmount].xrp().drops(); - break; - default: - break; - } - } - - if (after) - { - switch (after->getType()) - { - case ltACCOUNT_ROOT: - drops_ += (*after)[sfBalance].xrp().drops(); - break; - case ltPAYCHAN: - if (!isDelete) - drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); - break; - case ltESCROW: - if (!isDelete && isXRP((*after)[sfAmount])) - drops_ += (*after)[sfAmount].xrp().drops(); - break; - default: - break; - } - } -} - -bool -XRPNotCreated::finalize( - STTx const& tx, - TER const, - XRPAmount const fee, - ReadView const&, - beast::Journal const& j) -{ - // The net change should never be positive, as this would mean that the - // transaction created XRP out of thin air. That's not possible. - if (drops_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_; - return false; - } - - // The negative of the net change should be equal to actual fee charged. - if (-drops_ != fee.drops()) - { - JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee " - << fee.drops(); - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -XRPBalanceChecks::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - auto isBad = [](STAmount const& balance) { - if (!balance.native()) - return true; - - auto const drops = balance.xrp(); - - // Can't have more than the number of drops instantiated - // in the genesis ledger. - if (drops > INITIAL_XRP) - return true; - - // Can't have a negative balance (0 is OK) - if (drops < XRPAmount{0}) - return true; - - return false; - }; - - if (before && before->getType() == ltACCOUNT_ROOT) - bad_ |= isBad((*before)[sfBalance]); - - if (after && after->getType() == ltACCOUNT_ROOT) - bad_ |= isBad((*after)[sfBalance]); -} - -bool -XRPBalanceChecks::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (bad_) - { - JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance"; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -NoBadOffers::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - auto isBad = [](STAmount const& pays, STAmount const& gets) { - // An offer should never be negative - if (pays < beast::zero) - return true; - - if (gets < beast::zero) - return true; - - // Can't have an XRP to XRP offer: - return pays.native() && gets.native(); - }; - - if (before && before->getType() == ltOFFER) - bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]); - - if (after && after->getType() == ltOFFER) - bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]); -} - -bool -NoBadOffers::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (bad_) - { - JLOG(j.fatal()) << "Invariant failed: offer with a bad amount"; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -NoZeroEscrow::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - auto isBad = [](STAmount const& amount) { - // XRP case - if (amount.native()) - { - if (amount.xrp() <= XRPAmount{0}) - return true; - - if (amount.xrp() >= INITIAL_XRP) - return true; - } - else - { - // IOU case - if (amount.holds()) - { - if (amount <= beast::zero) - return true; - - if (badCurrency() == amount.getCurrency()) - return true; - } - - // MPT case - if (amount.holds()) - { - if (amount <= beast::zero) - return true; - - if (amount.mpt() > MPTAmount{maxMPTokenAmount}) - return true; // LCOV_EXCL_LINE - } - } - return false; - }; - - if (before && before->getType() == ltESCROW) - bad_ |= isBad((*before)[sfAmount]); - - if (after && after->getType() == ltESCROW) - bad_ |= isBad((*after)[sfAmount]); - - auto checkAmount = [this](std::int64_t amount) { - if (amount > maxMPTokenAmount || amount < 0) - bad_ = true; - }; - - if (after && after->getType() == ltMPTOKEN_ISSUANCE) - { - auto const outstanding = (*after)[sfOutstandingAmount]; - checkAmount(outstanding); - if (auto const locked = (*after)[~sfLockedAmount]) - { - checkAmount(*locked); - bad_ = outstanding < *locked; - } - } - - if (after && after->getType() == ltMPTOKEN) - { - auto const mptAmount = (*after)[sfMPTAmount]; - checkAmount(mptAmount); - if (auto const locked = (*after)[~sfLockedAmount]) - { - checkAmount(*locked); - } - } -} - -bool -NoZeroEscrow::finalize( - STTx const& txn, - TER const, - XRPAmount const, - ReadView const& rv, - beast::Journal const& j) -{ - if (bad_) - { - JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount"; - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -AccountRootsNotDeleted::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const&) -{ - if (isDelete && before && before->getType() == ltACCOUNT_ROOT) - accountsDeleted_++; -} - -bool -AccountRootsNotDeleted::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - // AMM account root can be deleted as the result of AMM withdraw/delete - // transaction when the total AMM LP Tokens balance goes to 0. - // A successful AccountDelete or AMMDelete MUST delete exactly - // one account root. - if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) - { - if (accountsDeleted_ == 1) - return true; - - if (accountsDeleted_ == 0) - JLOG(j.fatal()) << "Invariant failed: account deletion " - "succeeded without deleting an account"; - else - JLOG(j.fatal()) << "Invariant failed: account deletion " - "succeeded but deleted multiple accounts!"; - return false; - } - - // A successful AMMWithdraw/AMMClawback MAY delete one account root - // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw - // deletes the AMM account, accountsDeleted_ is set if it is deleted. - if (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && accountsDeleted_ == 1) - return true; - - if (accountsDeleted_ == 0) - return true; - - JLOG(j.fatal()) << "Invariant failed: an account root was deleted"; - return false; -} - -//------------------------------------------------------------------------------ - -void -AccountRootsDeletedClean::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete && before && before->getType() == ltACCOUNT_ROOT) - accountsDeleted_.emplace_back(before, after); -} - -bool -AccountRootsDeletedClean::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Always check for objects in the ledger, but to prevent differing - // transaction processing results, however unlikely, only fail if the - // feature is enabled. Enabled, or not, though, a fatal-level message will - // be logged - [[maybe_unused]] bool const enforce = view.rules().enabled(featureInvariantsV1_1) || - view.rules().enabled(featureSingleAssetVault) || - view.rules().enabled(featureLendingProtocol); - - auto const objectExists = [&view, enforce, &j](auto const& keylet) { - (void)enforce; - if (auto const sle = view.read(keylet)) - { - // Finding the object is bad - auto const typeName = [&sle]() { - auto item = LedgerFormats::getInstance().findByType(sle->getType()); - - if (item != nullptr) - return item->getName(); - return std::to_string(sle->getType()); - }(); - - JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName - << " object"; - // The comment above starting with "assert(enforce)" explains this - // assert. - XRPL_ASSERT( - enforce, - "xrpl::AccountRootsDeletedClean::finalize::objectExists : " - "account deletion left no objects behind"); - return true; - } - return false; - }; - - for (auto const& [before, after] : accountsDeleted_) - { - auto const accountID = before->getAccountID(sfAccount); - // An account should not be deleted with a balance - if (after->at(sfBalance) != beast::zero) - { - JLOG(j.fatal()) << "Invariant failed: account deletion left " - "behind a non-zero balance"; - XRPL_ASSERT( - enforce, - "xrpl::AccountRootsDeletedClean::finalize : " - "deleted account has zero balance"); - if (enforce) - return false; - } - // An account should not be deleted with a non-zero owner count - if (after->at(sfOwnerCount) != 0) - { - JLOG(j.fatal()) << "Invariant failed: account deletion left " - "behind a non-zero owner count"; - XRPL_ASSERT( - enforce, - "xrpl::AccountRootsDeletedClean::finalize : " - "deleted account has zero owner count"); - if (enforce) - return false; - } - // Simple types - for (auto const& [keyletfunc, _, __] : directAccountKeylets) - { - if (objectExists(std::invoke(keyletfunc, accountID)) && enforce) - return false; - } - - { - // NFT pages. nftpage_min and nftpage_max were already explicitly - // checked above as entries in directAccountKeylets. This uses - // view.succ() to check for any NFT pages in between the two - // endpoints. - Keylet const first = keylet::nftpage_min(accountID); - Keylet const last = keylet::nftpage_max(accountID); - - std::optional key = view.succ(first.key, last.key.next()); - - // current page - if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce) - return false; - } - - // If the account is a pseudo account, then the linked object must - // also be deleted. e.g. AMM, Vault, etc. - for (auto const& field : getPseudoAccountFields()) - { - if (before->isFieldPresent(*field)) - { - auto const key = before->getFieldH256(*field); - if (objectExists(keylet::unchecked(key)) && enforce) - return false; - } - } - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -LedgerEntryTypesMatch::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (before && after && before->getType() != after->getType()) - typeMismatch_ = true; - - if (after) - { -#pragma push_macro("LEDGER_ENTRY") -#undef LEDGER_ENTRY - -#define LEDGER_ENTRY(tag, ...) case tag: - - switch (after->getType()) - { -#include - - break; - default: - invalidTypeAdded_ = true; - break; - } - -#undef LEDGER_ENTRY -#pragma pop_macro("LEDGER_ENTRY") - } -} - -bool -LedgerEntryTypesMatch::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if ((!typeMismatch_) && (!invalidTypeAdded_)) - return true; - - if (typeMismatch_) - { - JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch"; - } - - if (invalidTypeAdded_) - { - JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added"; - } - - return false; -} - -//------------------------------------------------------------------------------ - -void -NoXRPTrustLines::visitEntry( - bool, - std::shared_ptr const&, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltRIPPLE_STATE) - { - // checking the issue directly here instead of - // relying on .native() just in case native somehow - // were systematically incorrect - xrpTrustLine_ = after->getFieldAmount(sfLowLimit).issue() == xrpIssue() || - after->getFieldAmount(sfHighLimit).issue() == xrpIssue(); - } -} - -bool -NoXRPTrustLines::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (!xrpTrustLine_) - return true; - - JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created"; - return false; -} - -//------------------------------------------------------------------------------ - -void -NoDeepFreezeTrustLinesWithoutFreeze::visitEntry( - bool, - std::shared_ptr const&, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltRIPPLE_STATE) - { - std::uint32_t const uFlags = after->getFieldU32(sfFlags); - bool const lowFreeze = uFlags & lsfLowFreeze; - bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze; - - bool const highFreeze = uFlags & lsfHighFreeze; - bool const highDeepFreeze = uFlags & lsfHighDeepFreeze; - - deepFreezeWithoutFreeze_ = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze); - } -} - -bool -NoDeepFreezeTrustLinesWithoutFreeze::finalize( - STTx const&, - TER const, - XRPAmount const, - ReadView const&, - beast::Journal const& j) -{ - if (!deepFreezeWithoutFreeze_) - return true; - - JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag " - "without normal freeze was created"; - return false; -} - -//------------------------------------------------------------------------------ - -void -TransfersNotFrozen::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - /* - * A trust line freeze state alone doesn't determine if a transfer is - * frozen. The transfer must be examined "end-to-end" because both sides of - * the transfer may have different freeze states and freeze impact depends - * on the transfer direction. This is why first we need to track the - * transfers using IssuerChanges senders/receivers. - * - * Only in validateIssuerChanges, after we collected all changes can we - * determine if the transfer is valid. - */ - if (!isValidEntry(before, after)) - { - return; - } - - auto const balanceChange = calculateBalanceChange(before, after, isDelete); - if (balanceChange.signum() == 0) - { - return; - } - - recordBalanceChanges(after, balanceChange); -} - -bool -TransfersNotFrozen::finalize( - STTx const& tx, - TER const ter, - XRPAmount const fee, - ReadView const& view, - beast::Journal const& j) -{ - /* - * We check this invariant regardless of deep freeze amendment status, - * allowing for detection and logging of potential issues even when the - * amendment is disabled. - * - * If an exploit that allows moving frozen assets is discovered, - * we can alert operators who monitor fatal messages and trigger assert in - * debug builds for an early warning. - * - * In an unlikely event that an exploit is found, this early detection - * enables encouraging the UNL to expedite deep freeze amendment activation - * or deploy hotfixes via new amendments. In case of a new amendment, we'd - * only have to change this line setting 'enforce' variable. - * enforce = view.rules().enabled(featureDeepFreeze) || - * view.rules().enabled(fixFreezeExploit); - */ - [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze); - - for (auto const& [issue, changes] : balanceChanges_) - { - auto const issuerSle = findIssuer(issue.account, view); - // It should be impossible for the issuer to not be found, but check - // just in case so rippled doesn't crash in release. - if (!issuerSle) - { - // The comment above starting with "assert(enforce)" explains this - // assert. - XRPL_ASSERT( - enforce, - "xrpl::TransfersNotFrozen::finalize : enforce " - "invariant."); - if (enforce) - { - return false; - } - continue; - } - - if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce)) - { - return false; - } - } - - return true; -} - -bool -TransfersNotFrozen::isValidEntry( - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - // `after` can never be null, even if the trust line is deleted. - XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after."); - if (!after) - { - return false; - } - - if (after->getType() == ltACCOUNT_ROOT) - { - possibleIssuers_.emplace(after->at(sfAccount), after); - return false; - } - - /* While LedgerEntryTypesMatch invariant also checks types, all invariants - * are processed regardless of previous failures. - * - * This type check is still necessary here because it prevents potential - * issues in subsequent processing. - */ - return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE); -} - -STAmount -TransfersNotFrozen::calculateBalanceChange( - std::shared_ptr const& before, - std::shared_ptr const& after, - bool isDelete) -{ - auto const getBalance = [](auto const& line, auto const& other, bool zero) { - STAmount amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed(); - return zero ? amt.zeroed() : amt; - }; - - /* Trust lines can be created dynamically by other transactions such as - * Payment and OfferCreate that cross offers. Such trust line won't be - * created frozen, but the sender might be, so the starting balance must be - * treated as zero. - */ - auto const balanceBefore = getBalance(before, after, false); - - /* Same as above, trust lines can be dynamically deleted, and for frozen - * trust lines, payments not involving the issuer must be blocked. This is - * achieved by treating the final balance as zero when isDelete=true to - * ensure frozen line restrictions are enforced even during deletion. - */ - auto const balanceAfter = getBalance(after, before, isDelete); - - return balanceAfter - balanceBefore; -} - -void -TransfersNotFrozen::recordBalance(Issue const& issue, BalanceChange change) -{ - XRPL_ASSERT( - change.balanceChangeSign, - "xrpl::TransfersNotFrozen::recordBalance : valid trustline " - "balance sign."); - auto& changes = balanceChanges_[issue]; - if (change.balanceChangeSign < 0) - changes.senders.emplace_back(std::move(change)); - else - changes.receivers.emplace_back(std::move(change)); -} - -void -TransfersNotFrozen::recordBalanceChanges( - std::shared_ptr const& after, - STAmount const& balanceChange) -{ - auto const balanceChangeSign = balanceChange.signum(); - auto const currency = after->at(sfBalance).getCurrency(); - - // Change from low account's perspective, which is trust line default - recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign}); - - // Change from high account's perspective, which reverses the sign. - recordBalance({currency, after->at(sfLowLimit).getIssuer()}, {after, -balanceChangeSign}); -} - -std::shared_ptr -TransfersNotFrozen::findIssuer(AccountID const& issuerID, ReadView const& view) -{ - if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end()) - { - return it->second; - } - - return view.read(keylet::account(issuerID)); -} - -bool -TransfersNotFrozen::validateIssuerChanges( - std::shared_ptr const& issuer, - IssuerChanges const& changes, - STTx const& tx, - beast::Journal const& j, - bool enforce) -{ - if (!issuer) - { - return false; - } - - bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze); - if (changes.receivers.empty() || changes.senders.empty()) - { - /* If there are no receivers, then the holder(s) are returning - * their tokens to the issuer. Likewise, if there are no - * senders, then the issuer is issuing tokens to the holder(s). - * This is allowed regardless of the issuer's freeze flags. (The - * holder may have contradicting freeze flags, but that will be - * checked when the holder is treated as issuer.) - */ - return true; - } - - for (auto const& actors : {changes.senders, changes.receivers}) - { - for (auto const& change : actors) - { - bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount); - - if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze)) - { - return false; - } - } - } - return true; -} - -bool -TransfersNotFrozen::validateFrozenState( - BalanceChange const& change, - bool high, - STTx const& tx, - beast::Journal const& j, - bool enforce, - bool globalFreeze) -{ - bool const freeze = - change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); - bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze); - bool const frozen = globalFreeze || deepFreeze || freeze; - - bool const isAMMLine = change.line->isFlag(lsfAMMNode); - - if (!frozen) - { - return true; - } - - // AMMClawbacks are allowed to override some freeze rules - if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze)) - { - JLOG(j.debug()) << "Invariant check allowing funds to be moved " - << (change.balanceChangeSign > 0 ? "to" : "from") - << " a frozen trustline for AMMClawback " << tx.getTransactionID(); - return true; - } - - JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for " - << tx.getTransactionID(); - // The comment above starting with "assert(enforce)" explains this assert. - XRPL_ASSERT( - enforce, - "xrpl::TransfersNotFrozen::validateFrozenState : enforce " - "invariant."); - - if (enforce) - { - return false; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidNewAccountRoot::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (!before && after->getType() == ltACCOUNT_ROOT) - { - accountsCreated_++; - accountSeq_ = (*after)[sfSequence]; - pseudoAccount_ = isPseudoAccount(after); - flags_ = after->getFlags(); - } -} - -bool -ValidNewAccountRoot::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (accountsCreated_ == 0) - return true; - - if (accountsCreated_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: multiple accounts " - "created in a single transaction"; - return false; - } - - // From this point on we know exactly one account was created. - if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) - { - bool const pseudoAccount = - (pseudoAccount_ && - (view.rules().enabled(featureSingleAssetVault) || - view.rules().enabled(featureLendingProtocol))); - - if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct)) - { - JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " - "wrong transaction type"; - return false; - } - - std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq(); - - if (accountSeq_ != startingSeq) - { - JLOG(j.fatal()) << "Invariant failed: account created with " - "wrong starting sequence number"; - return false; - } - - if (pseudoAccount) - { - std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); - if (flags_ != expected) - { - JLOG(j.fatal()) << "Invariant failed: pseudo-account created with " - "wrong flags"; - return false; - } - } - - return true; - } - - JLOG(j.fatal()) << "Invariant failed: account root created illegally"; - return false; -} // namespace xrpl - -//------------------------------------------------------------------------------ - -void -ValidNFTokenPage::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - static constexpr uint256 const& pageBits = nft::pageMask; - static constexpr uint256 const accountBits = ~pageBits; - - if ((before && before->getType() != ltNFTOKEN_PAGE) || - (after && after->getType() != ltNFTOKEN_PAGE)) - return; - - auto check = [this, isDelete](std::shared_ptr const& sle) { - uint256 const account = sle->key() & accountBits; - uint256 const hiLimit = sle->key() & pageBits; - std::optional const prev = (*sle)[~sfPreviousPageMin]; - - // Make sure that any page links... - // 1. Are properly associated with the owning account and - // 2. The page is correctly ordered between links. - if (prev) - { - if (account != (*prev & accountBits)) - badLink_ = true; - - if (hiLimit <= (*prev & pageBits)) - badLink_ = true; - } - - if (auto const next = (*sle)[~sfNextPageMin]) - { - if (account != (*next & accountBits)) - badLink_ = true; - - if (hiLimit >= (*next & pageBits)) - badLink_ = true; - } - - { - auto const& nftokens = sle->getFieldArray(sfNFTokens); - - // An NFTokenPage should never contain too many tokens or be empty. - if (std::size_t const nftokenCount = nftokens.size(); - (!isDelete && nftokenCount == 0) || nftokenCount > dirMaxTokensPerPage) - invalidSize_ = true; - - // If prev is valid, use it to establish a lower bound for - // page entries. If prev is not valid the lower bound is zero. - uint256 const loLimit = prev ? *prev & pageBits : uint256(beast::zero); - - // Also verify that all NFTokenIDs in the page are sorted. - uint256 loCmp = loLimit; - for (auto const& obj : nftokens) - { - uint256 const tokenID = obj[sfNFTokenID]; - if (!nft::compareTokens(loCmp, tokenID)) - badSort_ = true; - loCmp = tokenID; - - // None of the NFTs on this page should belong on lower or - // higher pages. - if (uint256 const tokenPageBits = tokenID & pageBits; - tokenPageBits < loLimit || tokenPageBits >= hiLimit) - badEntry_ = true; - - if (auto uri = obj[~sfURI]; uri && uri->empty()) - badURI_ = true; - } - } - }; - - if (before) - { - check(before); - - // While an account's NFToken directory contains any NFTokens, the last - // NFTokenPage (with 96 bits of 1 in the low part of the index) should - // never be deleted. - if (isDelete && (before->key() & nft::pageMask) == nft::pageMask && - before->isFieldPresent(sfPreviousPageMin)) - { - deletedFinalPage_ = true; - } - } - - if (after) - check(after); - - if (!isDelete && before && after) - { - // If the NFTokenPage - // 1. Has a NextMinPage field in before, but loses it in after, and - // 2. This is not the last page in the directory - // Then we have identified a corruption in the links between the - // NFToken pages in the NFToken directory. - if ((before->key() & nft::pageMask) != nft::pageMask && - before->isFieldPresent(sfNextPageMin) && !after->isFieldPresent(sfNextPageMin)) - { - deletedLink_ = true; - } - } -} - -bool -ValidNFTokenPage::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (badLink_) - { - JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked."; - return false; - } - - if (badEntry_) - { - JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page."; - return false; - } - - if (badSort_) - { - JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted."; - return false; - } - - if (badURI_) - { - JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI."; - return false; - } - - if (invalidSize_) - { - JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size."; - return false; - } - - if (view.rules().enabled(fixNFTokenPageLinks)) - { - if (deletedFinalPage_) - { - JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with " - "non-empty directory."; - return false; - } - if (deletedLink_) - { - JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link."; - return false; - } - } - - return true; -} - -//------------------------------------------------------------------------------ -void -NFTokenCountTracking::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (before && before->getType() == ltACCOUNT_ROOT) - { - beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0); - beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0); - } - - if (after && after->getType() == ltACCOUNT_ROOT) - { - afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0); - afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0); - } -} - -bool -NFTokenCountTracking::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (!hasPrivilege(tx, changeNFTCounts)) - { - if (beforeMintedTotal != afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: the number of minted tokens " - "changed without a mint transaction!"; - return false; - } - - if (beforeBurnedTotal != afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: the number of burned tokens " - "changed without a burn transaction!"; - return false; - } - - return true; - } - - if (tx.getTxnType() == ttNFTOKEN_MINT) - { - if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: successful minting didn't increase " - "the number of minted tokens."; - return false; - } - - if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: failed minting changed the " - "number of minted tokens."; - return false; - } - - if (beforeBurnedTotal != afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: minting changed the number of " - "burned tokens."; - return false; - } - } - - if (tx.getTxnType() == ttNFTOKEN_BURN) - { - if (result == tesSUCCESS) - { - if (beforeBurnedTotal >= afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: successful burning didn't increase " - "the number of burned tokens."; - return false; - } - } - - if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal) - { - JLOG(j.fatal()) << "Invariant failed: failed burning changed the " - "number of burned tokens."; - return false; - } - - if (beforeMintedTotal != afterMintedTotal) - { - JLOG(j.fatal()) << "Invariant failed: burning changed the number of " - "minted tokens."; - return false; - } - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidClawback::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const&) -{ - if (before && before->getType() == ltRIPPLE_STATE) - trustlinesChanged++; - - if (before && before->getType() == ltMPTOKEN) - mptokensChanged++; -} - -bool -ValidClawback::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - if (tx.getTxnType() != ttCLAWBACK) - return true; - - if (result == tesSUCCESS) - { - if (trustlinesChanged > 1) - { - JLOG(j.fatal()) << "Invariant failed: more than one trustline changed."; - return false; - } - - if (mptokensChanged > 1) - { - JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed."; - return false; - } - - if (trustlinesChanged == 1) - { - AccountID const issuer = tx.getAccountID(sfAccount); - STAmount const& amount = tx.getFieldAmount(sfAmount); - AccountID const& holder = amount.getIssuer(); - STAmount const holderBalance = - accountHolds(view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); - - if (holderBalance.signum() < 0) - { - JLOG(j.fatal()) << "Invariant failed: trustline balance is negative"; - return false; - } - } - } - else - { - if (trustlinesChanged != 0) - { - JLOG(j.fatal()) << "Invariant failed: some trustlines were changed " - "despite failure of the transaction."; - return false; - } - - if (mptokensChanged != 0) - { - JLOG(j.fatal()) << "Invariant failed: some mptokens were changed " - "despite failure of the transaction."; - return false; - } - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidMPTIssuance::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltMPTOKEN_ISSUANCE) - { - if (isDelete) - mptIssuancesDeleted_++; - else if (!before) - mptIssuancesCreated_++; - } - - if (after && after->getType() == ltMPTOKEN) - { - if (isDelete) - mptokensDeleted_++; - else if (!before) - { - mptokensCreated_++; - MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)}; - if (mptIssue.getIssuer() == after->at(sfAccount)) - mptCreatedByIssuer_ = true; - } - } -} - -bool -ValidMPTIssuance::finalize( - STTx const& tx, - TER const result, - XRPAmount const _fee, - ReadView const& view, - beast::Journal const& j) -{ - if (result == tesSUCCESS) - { - auto const& rules = view.rules(); - [[maybe_unused]] - bool enforceCreatedByIssuer = - rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol); - if (mptCreatedByIssuer_) - { - JLOG(j.fatal()) << "Invariant failed: MPToken created for the MPT issuer"; - // The comment above starting with "assert(enforce)" explains this - // assert. - XRPL_ASSERT_PARTS( - enforceCreatedByIssuer, "xrpl::ValidMPTIssuance::finalize", "no issuer MPToken"); - if (enforceCreatedByIssuer) - return false; - } - - auto const txnType = tx.getTxnType(); - if (hasPrivilege(tx, createMPTIssuance)) - { - if (mptIssuancesCreated_ == 0) - { - JLOG(j.fatal()) << "Invariant failed: transaction " - "succeeded without creating a MPT issuance"; - } - else if (mptIssuancesDeleted_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: transaction " - "succeeded while removing MPT issuances"; - } - else if (mptIssuancesCreated_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: transaction " - "succeeded but created multiple issuances"; - } - - return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; - } - - if (hasPrivilege(tx, destroyMPTIssuance)) - { - if (mptIssuancesDeleted_ == 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " - "succeeded without removing a MPT issuance"; - } - else if (mptIssuancesCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " - "succeeded while creating MPT issuances"; - } - else if (mptIssuancesDeleted_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " - "succeeded but deleted multiple issuances"; - } - - return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; - } - - bool const lendingProtocolEnabled = view.rules().enabled(featureLendingProtocol); - // ttESCROW_FINISH may authorize an MPT, but it can't have the - // mayAuthorizeMPT privilege, because that may cause - // non-amendment-gated side effects. - bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) && - (view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled); - if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish) - { - bool const submittedByIssuer = tx.isFieldPresent(sfHolder); - - if (mptIssuancesCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize " - "succeeded but created MPT issuances"; - return false; - } - else if (mptIssuancesDeleted_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize " - "succeeded but deleted issuances"; - return false; - } - else if (lendingProtocolEnabled && mptokensCreated_ + mptokensDeleted_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " - "but created/deleted bad number mptokens"; - return false; - } - else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer " - "succeeded but created/deleted mptokens"; - return false; - } - else if ( - !submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) && - (mptokensCreated_ + mptokensDeleted_ != 1)) - { - // if the holder submitted this tx, then a mptoken must be - // either created or deleted. - JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder " - "succeeded but created/deleted bad number of mptokens"; - return false; - } - - return true; - } - if (txnType == ttESCROW_FINISH) - { - // ttESCROW_FINISH may authorize an MPT, but it can't have the - // mayAuthorizeMPT privilege, because that may cause - // non-amendment-gated side effects. - XRPL_ASSERT_PARTS( - !enforceEscrowFinish, "xrpl::ValidMPTIssuance::finalize", "not escrow finish tx"); - return true; - } - - if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && mptokensCreated_ == 0 && - mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) - return true; - } - - if (mptIssuancesCreated_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created"; - } - else if (mptIssuancesDeleted_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted"; - } - else if (mptokensCreated_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPToken was created"; - } - else if (mptokensDeleted_ != 0) - { - JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted"; - } - - return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && - mptokensDeleted_ == 0; -} - -//------------------------------------------------------------------------------ - -void -ValidPermissionedDomain::visitEntry( - bool isDel, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (before && before->getType() != ltPERMISSIONED_DOMAIN) - return; - if (after && after->getType() != ltPERMISSIONED_DOMAIN) - return; - - auto check = [isDel](std::vector& sleStatus, std::shared_ptr const& sle) { - auto const& credentials = sle->getFieldArray(sfAcceptedCredentials); - auto const sorted = credentials::makeSorted(credentials); - - SleStatus ss{credentials.size(), false, !sorted.empty(), isDel}; - - // If array have duplicates then all the other checks are invalid - if (ss.isUnique_) - { - unsigned i = 0; - for (auto const& cred : sorted) - { - auto const& credTx = credentials[i++]; - ss.isSorted_ = - (cred.first == credTx[sfIssuer]) && (cred.second == credTx[sfCredentialType]); - if (!ss.isSorted_) - break; - } - } - sleStatus.emplace_back(std::move(ss)); - }; - - if (after) - check(sleStatus_, after); -} - -bool -ValidPermissionedDomain::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - auto check = [](SleStatus const& sleStatus, beast::Journal const& j) { - if (!sleStatus.credentialsSize_) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain with " - "no rules."; - return false; - } - - if (sleStatus.credentialsSize_ > maxPermissionedDomainCredentialsArraySize) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain bad " - "credentials size " - << sleStatus.credentialsSize_; - return false; - } - - if (!sleStatus.isUnique_) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " - "aren't unique"; - return false; - } - - if (!sleStatus.isSorted_) - { - JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " - "aren't sorted"; - return false; - } - - return true; - }; - - if (view.rules().enabled(fixPermissionedDomainInvariant)) - { - // No permissioned domains should be affected if the transaction failed - if (result != tesSUCCESS) - // If nothing changed, all is good. If there were changes, that's - // bad. - return sleStatus_.empty(); - - if (sleStatus_.size() > 1) - { - JLOG(j.fatal()) << "Invariant failed: transaction affected more " - "than 1 permissioned domain entry."; - return false; - } - - switch (tx.getTxnType()) - { - case ttPERMISSIONED_DOMAIN_SET: { - if (sleStatus_.empty()) - { - JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " - "PermissionedDomainSet"; - return false; - } - - auto const& sleStatus = sleStatus_[0]; - if (sleStatus.isDelete_) - { - JLOG(j.fatal()) << "Invariant failed: domain object " - "deleted by PermissionedDomainSet"; - return false; - } - return check(sleStatus, j); - } - case ttPERMISSIONED_DOMAIN_DELETE: { - if (sleStatus_.empty()) - { - JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " - "PermissionedDomainDelete"; - return false; - } - - if (!sleStatus_[0].isDelete_) - { - JLOG(j.fatal()) << "Invariant failed: domain object " - "modified, but not deleted by " - "PermissionedDomainDelete"; - return false; - } - return true; - } - default: { - if (!sleStatus_.empty()) - { - JLOG(j.fatal()) << "Invariant failed: " << sleStatus_.size() - << " domain object(s) affected by an " - "unauthorized transaction. " - << tx.getTxnType(); - return false; - } - return true; - } - } - } - else - { - if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS || - sleStatus_.empty()) - return true; - return check(sleStatus_[0], j); - } -} - -//------------------------------------------------------------------------------ - -void -ValidPseudoAccounts::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete) - // Deletion is ignored - return; - - if (after && after->getType() == ltACCOUNT_ROOT) - { - bool const isPseudo = [&]() { - // isPseudoAccount checks that any of the pseudo-account fields are - // set. - if (isPseudoAccount(after)) - return true; - // Not all pseudo-accounts have a zero sequence, but all accounts - // with a zero sequence had better be pseudo-accounts. - if (after->at(sfSequence) == 0) - return true; - - return false; - }(); - if (isPseudo) - { - // Pseudo accounts must have the following properties: - // 1. Exactly one of the pseudo-account fields is set. - // 2. The sequence number is not changed. - // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth - // flags are set. - // 4. The RegularKey is not set. - { - std::vector const& fields = getPseudoAccountFields(); - - auto const numFields = - std::count_if(fields.begin(), fields.end(), [&after](SField const* sf) -> bool { - return after->isFieldPresent(*sf); - }); - if (numFields != 1) - { - std::stringstream error; - error << "pseudo-account has " << numFields << " pseudo-account fields set"; - errors_.emplace_back(error.str()); - } - } - if (before && before->at(sfSequence) != after->at(sfSequence)) - { - errors_.emplace_back("pseudo-account sequence changed"); - } - if (!after->isFlag(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)) - { - errors_.emplace_back("pseudo-account flags are not set"); - } - if (after->isFieldPresent(sfRegularKey)) - { - errors_.emplace_back("pseudo-account has a regular key"); - } - } - } -} - -bool -ValidPseudoAccounts::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - bool const enforce = view.rules().enabled(featureSingleAssetVault); - XRPL_ASSERT( - errors_.empty() || enforce, - "xrpl::ValidPseudoAccounts::finalize : no bad " - "changes or enforce invariant"); - if (!errors_.empty()) - { - for (auto const& error : errors_) - { - JLOG(j.fatal()) << "Invariant failed: " << error; - } - if (enforce) - return false; - } - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidPermissionedDEX::visitEntry( - bool, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltDIR_NODE) - { - if (after->isFieldPresent(sfDomainID)) - domains_.insert(after->getFieldH256(sfDomainID)); - } - - if (after && after->getType() == ltOFFER) - { - if (after->isFieldPresent(sfDomainID)) - domains_.insert(after->getFieldH256(sfDomainID)); - else - regularOffers_ = true; - - // if a hybrid offer is missing domain or additional book, there's - // something wrong - if (after->isFlag(lsfHybrid) && - (!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) || - after->getFieldArray(sfAdditionalBooks).size() > 1)) - badHybrids_ = true; - } -} - -bool -ValidPermissionedDEX::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - auto const txType = tx.getTxnType(); - if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || result != tesSUCCESS) - return true; - - // For each offercreate transaction, check if - // permissioned offers are valid - if (txType == ttOFFER_CREATE && badHybrids_) - { - JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed"; - return false; - } - - if (!tx.isFieldPresent(sfDomainID)) - return true; - - auto const domain = tx.getFieldH256(sfDomainID); - - if (!view.exists(keylet::permissionedDomain(domain))) - { - JLOG(j.fatal()) << "Invariant failed: domain doesn't exist"; - return false; - } - - // for both payment and offercreate, there shouldn't be another domain - // that's different from the domain specified - for (auto const& d : domains_) - { - if (d != domain) - { - JLOG(j.fatal()) << "Invariant failed: transaction" - " consumed wrong domains"; - return false; - } - } - - if (regularOffers_) - { - JLOG(j.fatal()) << "Invariant failed: domain transaction" - " affected regular offers"; - return false; - } - - return true; -} - -void -ValidAMM::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete) - return; - - if (after) - { - auto const type = after->getType(); - // AMM object changed - if (type == ltAMM) - { - ammAccount_ = after->getAccountID(sfAccount); - lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); - } - // AMM pool changed - else if ( - (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || - (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) - { - ammPoolChanged_ = true; - } - } - - if (before) - { - // AMM object changed - if (before->getType() == ltAMM) - { - lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); - } - } -} - -static bool -validBalances( - STAmount const& amount, - STAmount const& amount2, - STAmount const& lptAMMBalance, - ValidAMM::ZeroAllowed zeroAllowed) -{ - bool const positive = - amount > beast::zero && amount2 > beast::zero && lptAMMBalance > beast::zero; - if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) - return positive || - (amount == beast::zero && amount2 == beast::zero && lptAMMBalance == beast::zero); - return positive; -} - -bool -ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const -{ - if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) - { - // LPTokens and the pool can not change on vote - // LCOV_EXCL_START - JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{}) - << " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " " - << ammPoolChanged_; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const -{ - if (ammPoolChanged_) - { - // The pool can not change on bid - // LCOV_EXCL_START - JLOG(j.error()) << "AMMBid invariant failed: pool changed"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - // LPTokens are burnt, therefore there should be fewer LPTokens - else if ( - lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && - (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero)) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " " - << *lptAMMBalanceAfter_; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::finalizeCreate( - STTx const& tx, - ReadView const& view, - bool enforce, - beast::Journal const& j) const -{ - if (!ammAccount_) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - else - { - auto const [amount, amount2] = ammPoolHolds( - view, - *ammAccount_, - tx[sfAmount].get(), - tx[sfAmount2].get(), - fhIGNORE_FREEZE, - j); - // Create invariant: - // sqrt(amount * amount2) == LPTokens - // all balances are greater than zero - if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || - ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_) - { - JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " " - << *lptAMMBalanceAfter_; - if (enforce) - return false; - } - } - - return true; -} - -bool -ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const -{ - if (ammAccount_) - { - // LCOV_EXCL_START - std::string const msg = (res == tesSUCCESS) ? "AMM object is not deleted on tesSUCCESS" - : "AMM object is changed on tecINCOMPLETE"; - JLOG(j.error()) << "AMMDelete invariant failed: " << msg; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const -{ - if (ammAccount_) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - - return true; -} - -bool -ValidAMM::generalInvariant( - xrpl::STTx const& tx, - xrpl::ReadView const& view, - ZeroAllowed zeroAllowed, - beast::Journal const& j) const -{ - auto const [amount, amount2] = ammPoolHolds( - view, - *ammAccount_, - tx[sfAsset].get(), - tx[sfAsset2].get(), - fhIGNORE_FREEZE, - j); - // Deposit and Withdrawal invariant: - // sqrt(amount * amount2) >= LPTokens - // all balances are greater than zero - // unless on last withdrawal - auto const poolProductMean = root2(amount * amount2); - bool const nonNegativeBalances = - validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); - bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; - // Allow for a small relative error if strongInvariantCheck fails - auto weakInvariantCheck = [&]() { - return *lptAMMBalanceAfter_ != beast::zero && - withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); - }; - if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck())) - { - JLOG(j.error()) << "AMM " << tx.getTxnType() - << " invariant failed: " << tx.getHash(HashPrefix::transactionID) << " " - << ammPoolChanged_ << " " << amount << " " << amount2 << " " - << poolProductMean << " " << lptAMMBalanceAfter_->getText() << " " - << ((*lptAMMBalanceAfter_ == beast::zero) - ? Number{1} - : ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean)); - return false; - } - - return true; -} - -bool -ValidAMM::finalizeDeposit( - xrpl::STTx const& tx, - xrpl::ReadView const& view, - bool enforce, - beast::Journal const& j) const -{ - if (!ammAccount_) - { - // LCOV_EXCL_START - JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; - if (enforce) - return false; - // LCOV_EXCL_STOP - } - else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) - return false; - - return true; -} - -bool -ValidAMM::finalizeWithdraw( - xrpl::STTx const& tx, - xrpl::ReadView const& view, - bool enforce, - beast::Journal const& j) const -{ - if (!ammAccount_) - { - // Last Withdraw or Clawback deleted AMM - } - else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) - { - if (enforce) - return false; - } - - return true; -} - -bool -ValidAMM::finalize( - STTx const& tx, - TER const result, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Delete may return tecINCOMPLETE if there are too many - // trustlines to delete. - if (result != tesSUCCESS && result != tecINCOMPLETE) - return true; - - bool const enforce = view.rules().enabled(fixAMMv1_3); - - switch (tx.getTxnType()) - { - case ttAMM_CREATE: - return finalizeCreate(tx, view, enforce, j); - case ttAMM_DEPOSIT: - return finalizeDeposit(tx, view, enforce, j); - case ttAMM_CLAWBACK: - case ttAMM_WITHDRAW: - return finalizeWithdraw(tx, view, enforce, j); - case ttAMM_BID: - return finalizeBid(enforce, j); - case ttAMM_VOTE: - return finalizeVote(enforce, j); - case ttAMM_DELETE: - return finalizeDelete(enforce, result, j); - case ttCHECK_CASH: - case ttOFFER_CREATE: - case ttPAYMENT: - return finalizeDEX(enforce, j); - default: - break; - } - - return true; -} - -//------------------------------------------------------------------------------ - -void -NoModifiedUnmodifiableFields::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete || !before) - // Creation and deletion are ignored - return; - - changedEntries_.emplace(before, after); -} - -bool -NoModifiedUnmodifiableFields::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - static auto const fieldChanged = [](auto const& before, auto const& after, auto const& field) { - bool const beforeField = before->isFieldPresent(field); - bool const afterField = after->isFieldPresent(field); - return beforeField != afterField || (afterField && before->at(field) != after->at(field)); - }; - for (auto const& slePair : changedEntries_) - { - auto const& before = slePair.first; - auto const& after = slePair.second; - auto const type = after->getType(); - bool bad = false; - [[maybe_unused]] bool enforce = false; - switch (type) - { - case ltLOAN_BROKER: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = fieldChanged(before, after, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex) || - fieldChanged(before, after, sfSequence) || - fieldChanged(before, after, sfOwnerNode) || - fieldChanged(before, after, sfVaultNode) || - fieldChanged(before, after, sfVaultID) || - fieldChanged(before, after, sfAccount) || - fieldChanged(before, after, sfOwner) || - fieldChanged(before, after, sfManagementFeeRate) || - fieldChanged(before, after, sfCoverRateMinimum) || - fieldChanged(before, after, sfCoverRateLiquidation); - break; - case ltLOAN: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = fieldChanged(before, after, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex) || - fieldChanged(before, after, sfSequence) || - fieldChanged(before, after, sfOwnerNode) || - fieldChanged(before, after, sfLoanBrokerNode) || - fieldChanged(before, after, sfLoanBrokerID) || - fieldChanged(before, after, sfBorrower) || - fieldChanged(before, after, sfLoanOriginationFee) || - fieldChanged(before, after, sfLoanServiceFee) || - fieldChanged(before, after, sfLatePaymentFee) || - fieldChanged(before, after, sfClosePaymentFee) || - fieldChanged(before, after, sfOverpaymentFee) || - fieldChanged(before, after, sfInterestRate) || - fieldChanged(before, after, sfLateInterestRate) || - fieldChanged(before, after, sfCloseInterestRate) || - fieldChanged(before, after, sfOverpaymentInterestRate) || - fieldChanged(before, after, sfStartDate) || - fieldChanged(before, after, sfPaymentInterval) || - fieldChanged(before, after, sfGracePeriod) || - fieldChanged(before, after, sfLoanScale); - break; - default: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - * - * We use the lending protocol as a gate, even though - * all transactions are affected because that's when it - * was added. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = fieldChanged(before, after, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex); - } - XRPL_ASSERT( - !bad || enforce, - "xrpl::NoModifiedUnmodifiableFields::finalize : no bad " - "changes or enforce invariant"); - if (bad) - { - JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for " - << tx.getTransactionID(); - if (enforce) - return false; - } - } - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidLoanBroker::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after) - { - if (after->getType() == ltLOAN_BROKER) - { - auto& broker = brokers_[after->key()]; - broker.brokerBefore = before; - broker.brokerAfter = after; - } - else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = after->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - else if (after->getType() == ltRIPPLE_STATE) - { - lines_.emplace_back(after); - } - else if (after->getType() == ltMPTOKEN) - { - mpts_.emplace_back(after); - } - } -} - -bool -ValidLoanBroker::goodZeroDirectory( - ReadView const& view, - SLE::const_ref dir, - beast::Journal const& j) const -{ - auto const next = dir->at(~sfIndexNext); - auto const prev = dir->at(~sfIndexPrevious); - if ((prev && *prev) || (next && *next)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " - "OwnerCount has multiple directory pages"; - return false; - } - auto indexes = dir->getFieldV256(sfIndexes); - if (indexes.size() > 1) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " - "OwnerCount has multiple indexes in the Directory root"; - return false; - } - if (indexes.size() == 1) - { - auto const index = indexes.value().front(); - auto const sle = view.read(keylet::unchecked(index)); - if (!sle) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt"; - return false; - } - if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " - "OwnerCount has an unexpected entry in the directory"; - return false; - } - } - - return true; -} - -bool -ValidLoanBroker::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Loan Brokers will not exist on ledger if the Lending Protocol amendment - // is not enabled, so there's no need to check it. - - for (auto const& line : lines_) - { - for (auto const& field : {&sfLowLimit, &sfHighLimit}) - { - auto const account = view.read(keylet::account(line->at(*field).getIssuer())); - // This Invariant doesn't know about the rules for Trust Lines, so - // if the account is missing, don't treat it as an error. This - // loop is only concerned with finding Broker pseudo-accounts - if (account && account->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = account->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - } - } - for (auto const& mpt : mpts_) - { - auto const account = view.read(keylet::account(mpt->at(sfAccount))); - // This Invariant doesn't know about the rules for MPTokens, so - // if the account is missing, don't treat is as an error. This - // loop is only concerned with finding Broker pseudo-accounts - if (account && account->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = account->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - } - - for (auto const& [brokerID, broker] : brokers_) - { - auto const& after = - broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID)); - - if (!after) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker missing"; - return false; - } - - auto const& before = broker.brokerBefore; - - // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants - // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most - // one node (the root), which will only hold entries for `RippleState` - // or `MPToken` objects. - if (after->at(sfOwnerCount) == 0) - { - auto const dir = view.read(keylet::ownerDir(after->at(sfAccount))); - if (dir) - { - if (!goodZeroDirectory(view, dir, j)) - { - return false; - } - } - } - if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number " - "decreased"; - return false; - } - if (after->at(sfDebtTotal) < 0) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative"; - return false; - } - if (after->at(sfCoverAvailable) < 0) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative"; - return false; - } - auto const vault = view.read(keylet::vault(after->at(sfVaultID))); - if (!vault) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid"; - return false; - } - auto const& vaultAsset = vault->at(sfAsset); - if (after->at(sfCoverAvailable) < accountHolds( - view, - after->at(sfAccount), - vaultAsset, - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available " - "is less than pseudo-account asset balance"; - return false; - } - } - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidLoan::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltLOAN) - { - loans_.emplace_back(before, after); - } -} - -bool -ValidLoan::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Loans will not exist on ledger if the Lending Protocol amendment - // is not enabled, so there's no need to check it. - - for (auto const& [before, after] : loans_) - { - // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants - // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off - if (after->at(sfPaymentRemaining) == 0 && - (after->at(sfTotalValueOutstanding) != beast::zero || - after->at(sfPrincipalOutstanding) != beast::zero || - after->at(sfManagementFeeOutstanding) != beast::zero)) - { - JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " - "remaining has not been paid off"; - return false; - } - // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid - // off - if (after->at(sfPaymentRemaining) != 0 && - after->at(sfTotalValueOutstanding) == beast::zero && - after->at(sfPrincipalOutstanding) == beast::zero && - after->at(sfManagementFeeOutstanding) == beast::zero) - { - JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " - "remaining has not been paid off"; - return false; - } - if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment))) - { - JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed"; - return false; - } - // Must not be negative - STNumber - for (auto const field : - {&sfLoanServiceFee, - &sfLatePaymentFee, - &sfClosePaymentFee, - &sfPrincipalOutstanding, - &sfTotalValueOutstanding, - &sfManagementFeeOutstanding}) - { - if (after->at(*field) < 0) - { - JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative "; - return false; - } - } - // Must be positive - STNumber - for (auto const field : { - &sfPeriodicPayment, - }) - { - if (after->at(*field) <= 0) - { - JLOG(j.fatal()) << "Invariant failed: " << field->getName() - << " is zero or negative "; - return false; - } - } - } - return true; -} - -ValidVault::Vault -ValidVault::Vault::make(SLE const& from) -{ - XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object"); - - ValidVault::Vault self; - self.key = from.key(); - self.asset = from.at(sfAsset); - self.pseudoId = from.getAccountID(sfAccount); - self.owner = from.at(sfOwner); - self.shareMPTID = from.getFieldH192(sfShareMPTID); - self.assetsTotal = from.at(sfAssetsTotal); - self.assetsAvailable = from.at(sfAssetsAvailable); - self.assetsMaximum = from.at(sfAssetsMaximum); - self.lossUnrealized = from.at(sfLossUnrealized); - return self; -} - -ValidVault::Shares -ValidVault::Shares::make(SLE const& from) -{ - XRPL_ASSERT( - from.getType() == ltMPTOKEN_ISSUANCE, - "ValidVault::Shares::make : from MPTokenIssuance object"); - - ValidVault::Shares self; - self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer))); - self.sharesTotal = from.at(sfOutstandingAmount); - self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount); - return self; -} - -void -ValidVault::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - // If `before` is empty, this means an object is being created, in which - // case `isDelete` must be false. Otherwise `before` and `after` are set and - // `isDelete` indicates whether an object is being deleted or modified. - XRPL_ASSERT( - after != nullptr && (before != nullptr || !isDelete), - "xrpl::ValidVault::visitEntry : some object is available"); - - // Number balanceDelta will capture the difference (delta) between "before" - // state (zero if created) and "after" state (zero if destroyed), so the - // invariants can validate that the change in account balances matches the - // balanceDelta captures the difference (delta) between "before" - // state (zero if created) and "after" state (zero if destroyed), and - // preserves value scale (exponent) to round values to the same scale during - // validation. It is used to validate that the change in account - // balances matches the change in vault balances, stored to deltas_ at the - // end of this function. - DeltaInfo balanceDelta{numZero, std::nullopt}; - - std::int8_t sign = 0; - if (before) - { - switch (before->getType()) - { - case ltVAULT: - beforeVault_.push_back(Vault::make(*before)); - break; - case ltMPTOKEN_ISSUANCE: - // At this moment we have no way of telling if this object holds - // vault shares or something else. Save it for finalize. - beforeMPTs_.push_back(Shares::make(*before)); - balanceDelta.delta = - static_cast(before->getFieldU64(sfOutstandingAmount)); - // MPTs are ints, so the scale is always 0. - balanceDelta.scale = 0; - sign = 1; - break; - case ltMPTOKEN: - balanceDelta.delta = static_cast(before->getFieldU64(sfMPTAmount)); - // MPTs are ints, so the scale is always 0. - balanceDelta.scale = 0; - sign = -1; - break; - case ltACCOUNT_ROOT: - balanceDelta.delta = before->getFieldAmount(sfBalance); - // Account balance is XRP, which is an int, so the scale is - // always 0. - balanceDelta.scale = 0; - sign = -1; - break; - case ltRIPPLE_STATE: { - auto const amount = before->getFieldAmount(sfBalance); - balanceDelta.delta = amount; - // Trust Line balances are STAmounts, so we can use the exponent - // directly to get the scale. - balanceDelta.scale = amount.exponent(); - sign = -1; - break; - } - default:; - } - } - - if (!isDelete && after) - { - switch (after->getType()) - { - case ltVAULT: - afterVault_.push_back(Vault::make(*after)); - break; - case ltMPTOKEN_ISSUANCE: - // At this moment we have no way of telling if this object holds - // vault shares or something else. Save it for finalize. - afterMPTs_.push_back(Shares::make(*after)); - balanceDelta.delta -= - Number(static_cast(after->getFieldU64(sfOutstandingAmount))); - // MPTs are ints, so the scale is always 0. - balanceDelta.scale = 0; - sign = 1; - break; - case ltMPTOKEN: - balanceDelta.delta -= - Number(static_cast(after->getFieldU64(sfMPTAmount))); - // MPTs are ints, so the scale is always 0. - balanceDelta.scale = 0; - sign = -1; - break; - case ltACCOUNT_ROOT: - balanceDelta.delta -= Number(after->getFieldAmount(sfBalance)); - // Account balance is XRP, which is an int, so the scale is - // always 0. - balanceDelta.scale = 0; - sign = -1; - break; - case ltRIPPLE_STATE: { - auto const amount = after->getFieldAmount(sfBalance); - balanceDelta.delta -= Number(amount); - // Trust Line balances are STAmounts, so we can use the exponent - // directly to get the scale. - if (amount.exponent() > balanceDelta.scale) - balanceDelta.scale = amount.exponent(); - sign = -1; - break; - } - default:; - } - } - - uint256 const key = (before ? before->key() : after->key()); - // Append to deltas if sign is non-zero, i.e. an object of an interesting - // type has been updated. A transaction may update an object even when - // its balance has not changed, e.g. transaction fee equals the amount - // transferred to the account. We intentionally do not compare balanceDelta - // against zero, to avoid missing such updates. - if (sign != 0) - { - XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized"); - balanceDelta.delta *= sign; - deltas_[key] = balanceDelta; - } -} - -bool -ValidVault::finalize( - STTx const& tx, - TER const ret, - XRPAmount const fee, - ReadView const& view, - beast::Journal const& j) -{ - bool const enforce = view.rules().enabled(featureSingleAssetVault); - - if (!isTesSuccess(ret)) - return true; // Do not perform checks - - if (afterVault_.empty() && beforeVault_.empty()) - { - if (hasPrivilege(tx, mustModifyVault)) - { - JLOG(j.fatal()) << // - "Invariant failed: vault operation succeeded without modifying " - "a vault"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant"); - return !enforce; - } - - return true; // Not a vault operation - } - else if (!(hasPrivilege(tx, mustModifyVault) || hasPrivilege(tx, mayModifyVault))) - { - JLOG(j.fatal()) << // - "Invariant failed: vault updated by a wrong transaction type"; - XRPL_ASSERT( - enforce, - "xrpl::ValidVault::finalize : illegal vault transaction " - "invariant"); - return !enforce; // Also not a vault operation - } - - if (beforeVault_.size() > 1 || afterVault_.size() > 1) - { - JLOG(j.fatal()) << // - "Invariant failed: vault operation updated more than single vault"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant"); - return !enforce; // That's all we can do here - } - - auto const txnType = tx.getTxnType(); - - // We do special handling for ttVAULT_DELETE first, because it's the only - // vault-modifying transaction without an "after" state of the vault - if (afterVault_.empty()) - { - if (txnType != ttVAULT_DELETE) - { - JLOG(j.fatal()) << // - "Invariant failed: vault deleted by a wrong transaction type"; - XRPL_ASSERT( - enforce, - "xrpl::ValidVault::finalize : illegal vault deletion " - "invariant"); - return !enforce; // That's all we can do here - } - - // Note, if afterVault_ is empty then we know that beforeVault_ is not - // empty, as enforced at the top of this function - auto const& beforeVault = beforeVault_[0]; - - // At this moment we only know a vault is being deleted and there - // might be some MPTokenIssuance objects which are deleted in the - // same transaction. Find the one matching this vault. - auto const deletedShares = [&]() -> std::optional { - for (auto const& e : beforeMPTs_) - { - if (e.share.getMptID() == beforeVault.shareMPTID) - return std::move(e); - } - return std::nullopt; - }(); - - if (!deletedShares) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must also " - "delete shares"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant"); - return !enforce; // That's all we can do here - } - - bool result = true; - if (deletedShares->sharesTotal != 0) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " - "shares outstanding"; - result = false; - } - if (beforeVault.assetsTotal != zero) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " - "assets outstanding"; - result = false; - } - if (beforeVault.assetsAvailable != zero) - { - JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " - "assets available"; - result = false; - } - - return result; - } - else if (txnType == ttVAULT_DELETE) - { - JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without " - "deleting a vault"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant"); - return !enforce; // That's all we can do here - } - - // Note, `afterVault_.empty()` is handled above - auto const& afterVault = afterVault_[0]; - XRPL_ASSERT( - beforeVault_.empty() || beforeVault_[0].key == afterVault.key, - "xrpl::ValidVault::finalize : single vault operation"); - - auto const updatedShares = [&]() -> std::optional { - // At this moment we only know that a vault is being updated and there - // might be some MPTokenIssuance objects which are also updated in the - // same transaction. Find the one matching the shares to this vault. - // Note, we expect updatedMPTs collection to be extremely small. For - // such collections linear search is faster than lookup. - for (auto const& e : afterMPTs_) - { - if (e.share.getMptID() == afterVault.shareMPTID) - return e; - } - - auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID)); - - return sleShares ? std::optional(Shares::make(*sleShares)) : std::nullopt; - }(); - - bool result = true; - - // Universal transaction checks - if (!beforeVault_.empty()) - { - auto const& beforeVault = beforeVault_[0]; - if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId || - afterVault.shareMPTID != beforeVault.shareMPTID) - { - JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data"; - result = false; - } - } - - if (!updatedShares) - { - JLOG(j.fatal()) << "Invariant failed: updated vault must have shares"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant"); - return !enforce; // That's all we can do here - } - - if (updatedShares->sharesTotal == 0) - { - if (afterVault.assetsTotal != zero) - { - JLOG(j.fatal()) << "Invariant failed: updated zero sized " - "vault must have no assets outstanding"; - result = false; - } - if (afterVault.assetsAvailable != zero) - { - JLOG(j.fatal()) << "Invariant failed: updated zero sized " - "vault must have no assets available"; - result = false; - } - } - else if (updatedShares->sharesTotal > updatedShares->sharesMaximum) - { - JLOG(j.fatal()) // - << "Invariant failed: updated shares must not exceed maximum " - << updatedShares->sharesMaximum; - result = false; - } - - if (afterVault.assetsAvailable < zero) - { - JLOG(j.fatal()) << "Invariant failed: assets available must be positive"; - result = false; - } - - if (afterVault.assetsAvailable > afterVault.assetsTotal) - { - JLOG(j.fatal()) << "Invariant failed: assets available must " - "not be greater than assets outstanding"; - result = false; - } - else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable) - { - JLOG(j.fatal()) // - << "Invariant failed: loss unrealized must not exceed " - "the difference between assets outstanding and available"; - result = false; - } - - if (afterVault.assetsTotal < zero) - { - JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive"; - result = false; - } - - if (afterVault.assetsMaximum < zero) - { - JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive"; - result = false; - } - - // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when - // enforcing invariants on transaction types other than ttVAULT_CREATE - if (beforeVault_.empty() && txnType != ttVAULT_CREATE) - { - JLOG(j.fatal()) << // - "Invariant failed: vault created by a wrong transaction type"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant"); - return !enforce; // That's all we can do here - } - - if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized && - txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY) - { - JLOG(j.fatal()) << // - "Invariant failed: vault transaction must not change loss " - "unrealized"; - result = false; - } - - auto const beforeShares = [&]() -> std::optional { - if (beforeVault_.empty()) - return std::nullopt; - auto const& beforeVault = beforeVault_[0]; - - for (auto const& e : beforeMPTs_) - { - if (e.share.getMptID() == beforeVault.shareMPTID) - return std::move(e); - } - return std::nullopt; - }(); - - if (!beforeShares && - (tx.getTxnType() == ttVAULT_DEPOSIT || // - tx.getTxnType() == ttVAULT_WITHDRAW || // - tx.getTxnType() == ttVAULT_CLAWBACK)) - { - JLOG(j.fatal()) << "Invariant failed: vault operation succeeded " - "without updating shares"; - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant"); - return !enforce; // That's all we can do here - } - - auto const& vaultAsset = afterVault.asset; - auto const deltaAssets = [&](AccountID const& id) -> std::optional { - auto const get = // - [&](auto const& it, std::int8_t sign = 1) -> std::optional { - if (it == deltas_.end()) - return std::nullopt; - - return DeltaInfo{it->second.delta * sign, it->second.scale}; - }; - - return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) - { - if (isXRP(issue)) - return get(deltas_.find(keylet::account(id).key)); - return get( - deltas_.find(keylet::line(id, issue).key), id > issue.getIssuer() ? -1 : 1); - } - else if constexpr (std::is_same_v) - { - return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key)); - } - }, - vaultAsset.value()); - }; - auto const deltaAssetsTxAccount = [&]() -> std::optional { - auto ret = deltaAssets(tx[sfAccount]); - // Nothing returned or not XRP transaction - if (!ret.has_value() || !vaultAsset.native()) - return ret; - - // Delegated transaction; no need to compensate for fees - if (auto const delegate = tx[~sfDelegate]; - delegate.has_value() && *delegate != tx[sfAccount]) - return ret; - - ret->delta += fee.drops(); - if (ret->delta == zero) - return std::nullopt; - - return ret; - }; - auto const deltaShares = [&](AccountID const& id) -> std::optional { - auto const it = [&]() { - if (id == afterVault.pseudoId) - return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key); - return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key); - }(); - - return it != deltas_.end() ? std::optional(it->second) : std::nullopt; - }; - - auto const vaultHoldsNoAssets = [&](Vault const& vault) { - return vault.assetsAvailable == 0 && vault.assetsTotal == 0; - }; - - // Technically this does not need to be a lambda, but it's more - // convenient thanks to early "return false"; the not-so-nice - // alternatives are several layers of nested if/else or more complex - // (i.e. brittle) if statements. - result &= [&]() { - switch (txnType) - { - case ttVAULT_CREATE: { - bool result = true; - - if (!beforeVault_.empty()) - { - JLOG(j.fatal()) // - << "Invariant failed: create operation must not have " - "updated a vault"; - result = false; - } - - if (afterVault.assetsAvailable != zero || afterVault.assetsTotal != zero || - afterVault.lossUnrealized != zero || updatedShares->sharesTotal != 0) - { - JLOG(j.fatal()) // - << "Invariant failed: created vault must be empty"; - result = false; - } - - if (afterVault.pseudoId != updatedShares->share.getIssuer()) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer and vault " - "pseudo-account must be the same"; - result = false; - } - - auto const sleSharesIssuer = - view.read(keylet::account(updatedShares->share.getIssuer())); - if (!sleSharesIssuer) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer must exist"; - return false; - } - - if (!isPseudoAccount(sleSharesIssuer)) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer must be a " - "pseudo-account"; - result = false; - } - - if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; - !vaultId || *vaultId != afterVault.key) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer pseudo-account " - "must point back to the vault"; - result = false; - } - - return result; - } - case ttVAULT_SET: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault"); - auto const& beforeVault = beforeVault_[0]; - - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); - if (vaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change vault balance"; - result = false; - } - - if (beforeVault.assetsTotal != afterVault.assetsTotal) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change assets " - "outstanding"; - result = false; - } - - if (afterVault.assetsMaximum > zero && - afterVault.assetsTotal > afterVault.assetsMaximum) - { - JLOG(j.fatal()) << // - "Invariant failed: set assets outstanding must not " - "exceed assets maximum"; - result = false; - } - - if (beforeVault.assetsAvailable != afterVault.assetsAvailable) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change assets " - "available"; - result = false; - } - - if (beforeShares && updatedShares && - beforeShares->sharesTotal != updatedShares->sharesTotal) - { - JLOG(j.fatal()) << // - "Invariant failed: set must not change shares " - "outstanding"; - result = false; - } - - return result; - } - case ttVAULT_DEPOSIT: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault"); - auto const& beforeVault = beforeVault_[0]; - - auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); - if (!maybeVaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change vault balance"; - return false; // That's all we can do - } - - // Get the coarsest scale to round calculations to - DeltaInfo totalDelta{ - afterVault.assetsTotal - beforeVault.assetsTotal, - std::max( - afterVault.assetsTotal.scale(vaultAsset), - beforeVault.assetsTotal.scale(vaultAsset))}; - DeltaInfo availableDelta{ - afterVault.assetsAvailable - beforeVault.assetsAvailable, - std::max( - afterVault.assetsAvailable.scale(vaultAsset), - beforeVault.assetsAvailable.scale(vaultAsset))}; - auto const minScale = computeMinScale( - vaultAsset, - { - *maybeVaultDeltaAssets, - totalDelta, - availableDelta, - }); - - auto const vaultDeltaAssets = - roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale); - auto const txAmount = roundToAsset(vaultAsset, tx[sfAmount], minScale); - - if (vaultDeltaAssets > txAmount) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must not change vault " - "balance by more than deposited amount"; - result = false; - } - - if (vaultDeltaAssets <= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must increase vault balance"; - result = false; - } - - // Any payments (including deposits) made by the issuer - // do not change their balance, but create funds instead. - bool const issuerDeposit = [&]() -> bool { - if (vaultAsset.native()) - return false; - return tx[sfAccount] == vaultAsset.getIssuer(); - }(); - - if (!issuerDeposit) - { - auto const maybeAccDeltaAssets = deltaAssetsTxAccount(); - if (!maybeAccDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change depositor " - "balance"; - return false; - } - auto const localMinScale = - std::max(minScale, computeMinScale(vaultAsset, {*maybeAccDeltaAssets})); - - auto const accountDeltaAssets = - roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale); - auto const localVaultDeltaAssets = - roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale); - - if (accountDeltaAssets >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must decrease depositor " - "balance"; - result = false; - } - - if (localVaultDeltaAssets * -1 != accountDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change vault and " - "depositor balance by equal amount"; - result = false; - } - } - - if (afterVault.assetsMaximum > zero && - afterVault.assetsTotal > afterVault.assetsMaximum) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit assets outstanding must not " - "exceed assets maximum"; - result = false; - } - - auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]); - if (!maybeAccDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change depositor " - "shares"; - return false; // That's all we can do - } - // We don't need to round shares, they are integral MPT - auto const& accountDeltaShares = *maybeAccDeltaShares; - if (accountDeltaShares.delta <= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must increase depositor " - "shares"; - result = false; - } - - auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == zero) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change vault shares"; - return false; // That's all we can do - } - - // We don't need to round shares, they are integral MPT - auto const& vaultDeltaShares = *maybeVaultDeltaShares; - if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta) - { - JLOG(j.fatal()) << // - "Invariant failed: deposit must change depositor and " - "vault shares by equal amount"; - result = false; - } - - auto const assetTotalDelta = roundToAsset( - vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale); - if (assetTotalDelta != vaultDeltaAssets) - { - JLOG(j.fatal()) << "Invariant failed: deposit and assets " - "outstanding must add up"; - result = false; - } - - auto const assetAvailableDelta = roundToAsset( - vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale); - if (assetAvailableDelta != vaultDeltaAssets) - { - JLOG(j.fatal()) << "Invariant failed: deposit and assets " - "available must add up"; - result = false; - } - - return result; - } - case ttVAULT_WITHDRAW: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), - "xrpl::ValidVault::finalize : withdrawal updated a " - "vault"); - auto const& beforeVault = beforeVault_[0]; - - auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); - - if (!maybeVaultDeltaAssets) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal must " - "change vault balance"; - return false; // That's all we can do - } - - // Get the most coarse scale to round calculations to - auto const totalDelta = DeltaInfo{ - afterVault.assetsTotal - beforeVault.assetsTotal, - std::max( - afterVault.assetsTotal.scale(vaultAsset), - beforeVault.assetsTotal.scale(vaultAsset))}; - auto const availableDelta = DeltaInfo{ - afterVault.assetsAvailable - beforeVault.assetsAvailable, - std::max( - afterVault.assetsAvailable.scale(vaultAsset), - beforeVault.assetsAvailable.scale(vaultAsset))}; - auto const minScale = computeMinScale( - vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta}); - - auto const vaultPseudoDeltaAssets = - roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale); - - if (vaultPseudoDeltaAssets >= zero) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal must " - "decrease vault balance"; - result = false; - } - // Any payments (including withdrawal) going to the issuer - // do not change their balance, but destroy funds instead. - bool const issuerWithdrawal = [&]() -> bool { - if (vaultAsset.native()) - return false; - auto const destination = tx[~sfDestination].value_or(tx[sfAccount]); - return destination == vaultAsset.getIssuer(); - }(); - - if (!issuerWithdrawal) - { - auto const maybeAccDelta = deltaAssetsTxAccount(); - auto const maybeOtherAccDelta = [&]() -> std::optional { - if (auto const destination = tx[~sfDestination]; - destination && *destination != tx[sfAccount]) - return deltaAssets(*destination); - return std::nullopt; - }(); - - if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value()) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change one " - "destination balance"; - return false; - } - - auto const destinationDelta = // - maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta; - - // the scale of destinationDelta can be coarser than - // minScale, so we take that into account when rounding - auto const localMinScale = - std::max(minScale, computeMinScale(vaultAsset, {destinationDelta})); - - auto const roundedDestinationDelta = - roundToAsset(vaultAsset, destinationDelta.delta, localMinScale); - - if (roundedDestinationDelta <= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must increase " - "destination balance"; - result = false; - } - - auto const localPseudoDeltaAssets = - roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale); - if (localPseudoDeltaAssets * -1 != roundedDestinationDelta) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change vault " - "and destination balance by equal amount"; - result = false; - } - } - // We don't need to round shares, they are integral MPT - auto const accountDeltaShares = deltaShares(tx[sfAccount]); - if (!accountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change depositor " - "shares"; - return false; - } - - if (accountDeltaShares->delta >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must decrease depositor " - "shares"; - result = false; - } - // We don't need to round shares, they are integral MPT - auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares || vaultDeltaShares->delta == zero) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change vault shares"; - return false; // That's all we can do - } - - if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta) - { - JLOG(j.fatal()) << // - "Invariant failed: withdrawal must change depositor " - "and vault shares by equal amount"; - result = false; - } - - auto const assetTotalDelta = roundToAsset( - vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale); - // Note, vaultBalance is negative (see check above) - if (assetTotalDelta != vaultPseudoDeltaAssets) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal and " - "assets outstanding must add up"; - result = false; - } - - auto const assetAvailableDelta = roundToAsset( - vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale); - - if (assetAvailableDelta != vaultPseudoDeltaAssets) - { - JLOG(j.fatal()) << "Invariant failed: withdrawal and " - "assets available must add up"; - result = false; - } - - return result; - } - case ttVAULT_CLAWBACK: { - bool result = true; - - XRPL_ASSERT( - !beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault"); - auto const& beforeVault = beforeVault_[0]; - - if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount]) - { - // The owner can use clawback to force-burn shares when the - // vault is empty but there are outstanding shares - if (!(beforeShares && beforeShares->sharesTotal > 0 && - vaultHoldsNoAssets(beforeVault) && beforeVault.owner == tx[sfAccount])) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback may only be performed " - "by the asset issuer, or by the vault owner of an " - "empty vault"; - return false; // That's all we can do - } - } - - auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); - if (maybeVaultDeltaAssets) - { - auto const totalDelta = DeltaInfo{ - afterVault.assetsTotal - beforeVault.assetsTotal, - std::max( - afterVault.assetsTotal.scale(vaultAsset), - beforeVault.assetsTotal.scale(vaultAsset))}; - auto const availableDelta = DeltaInfo{ - afterVault.assetsAvailable - beforeVault.assetsAvailable, - std::max( - afterVault.assetsAvailable.scale(vaultAsset), - beforeVault.assetsAvailable.scale(vaultAsset))}; - auto const minScale = computeMinScale( - vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta}); - auto const vaultDeltaAssets = - roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale); - if (vaultDeltaAssets >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must decrease vault " - "balance"; - result = false; - } - - auto const assetsTotalDelta = roundToAsset( - vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale); - if (assetsTotalDelta != vaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback and assets outstanding " - "must add up"; - result = false; - } - - auto const assetAvailableDelta = roundToAsset( - vaultAsset, - afterVault.assetsAvailable - beforeVault.assetsAvailable, - minScale); - if (assetAvailableDelta != vaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback and assets available " - "must add up"; - result = false; - } - } - else if (!vaultHoldsNoAssets(beforeVault)) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change vault balance"; - return false; // That's all we can do - } - - // We don't need to round shares, they are integral MPT - auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]); - if (!maybeAccountDeltaShares) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change holder shares"; - return false; // That's all we can do - } - if (maybeAccountDeltaShares->delta >= zero) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must decrease holder " - "shares"; - result = false; - } - - // We don't need to round shares, they are integral MPT - auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares || vaultDeltaShares->delta == zero) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change vault shares"; - return false; // That's all we can do - } - - if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback must change holder and " - "vault shares by equal amount"; - result = false; - } - - return result; - } - - case ttLOAN_SET: - case ttLOAN_MANAGE: - case ttLOAN_PAY: { - // TBD - return true; - } - - default: - // LCOV_EXCL_START - UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type"); - return false; - // LCOV_EXCL_STOP - } - }(); - - if (!result) - { - // The comment at the top of this file starting with "assert(enforce)" - // explains this assert. - XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants"); - return !enforce; - } - - return true; -} - -[[nodiscard]] std::int32_t -ValidVault::computeMinScale(Asset const& asset, std::vector const& numbers) -{ - if (numbers.size() == 0) - return 0; - - auto const max = - std::max_element(numbers.begin(), numbers.end(), [](auto const& a, auto const& b) -> bool { - return a.scale < b.scale; - }); - XRPL_ASSERT_PARTS( - max->scale, "xrpl::ValidVault::computeMinScale", "scale set for destinationDelta"); - return max->scale.value_or(STAmount::cMaxOffset); -} -} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp b/src/libxrpl/tx/invariants/AMMInvariant.cpp new file mode 100644 index 0000000000..d98c0a6f50 --- /dev/null +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp @@ -0,0 +1,305 @@ +#include +// +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidAMM::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + return; + + if (after) + { + auto const type = after->getType(); + // AMM object changed + if (type == ltAMM) + { + ammAccount_ = after->getAccountID(sfAccount); + lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); + } + // AMM pool changed + else if ( + (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || + (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) + { + ammPoolChanged_ = true; + } + } + + if (before) + { + // AMM object changed + if (before->getType() == ltAMM) + { + lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); + } + } +} + +static bool +validBalances( + STAmount const& amount, + STAmount const& amount2, + STAmount const& lptAMMBalance, + ValidAMM::ZeroAllowed zeroAllowed) +{ + bool const positive = + amount > beast::zero && amount2 > beast::zero && lptAMMBalance > beast::zero; + if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) + return positive || + (amount == beast::zero && amount2 == beast::zero && lptAMMBalance == beast::zero); + return positive; +} + +bool +ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const +{ + if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) + { + // LPTokens and the pool can not change on vote + // LCOV_EXCL_START + JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{}) + << " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " " + << ammPoolChanged_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const +{ + if (ammPoolChanged_) + { + // The pool can not change on bid + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: pool changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + // LPTokens are burnt, therefore there should be fewer LPTokens + else if ( + lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && + (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero)) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " " + << *lptAMMBalanceAfter_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeCreate( + STTx const& tx, + ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else + { + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAmount].get(), + tx[sfAmount2].get(), + fhIGNORE_FREEZE, + j); + // Create invariant: + // sqrt(amount * amount2) == LPTokens + // all balances are greater than zero + if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || + ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_) + { + JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " " + << *lptAMMBalanceAfter_; + if (enforce) + return false; + } + } + + return true; +} + +bool +ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + std::string const msg = (res == tesSUCCESS) ? "AMM object is not deleted on tesSUCCESS" + : "AMM object is changed on tecINCOMPLETE"; + JLOG(j.error()) << "AMMDelete invariant failed: " << msg; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::generalInvariant( + xrpl::STTx const& tx, + xrpl::ReadView const& view, + ZeroAllowed zeroAllowed, + beast::Journal const& j) const +{ + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAsset].get(), + tx[sfAsset2].get(), + fhIGNORE_FREEZE, + j); + // Deposit and Withdrawal invariant: + // sqrt(amount * amount2) >= LPTokens + // all balances are greater than zero + // unless on last withdrawal + auto const poolProductMean = root2(amount * amount2); + bool const nonNegativeBalances = + validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); + bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; + // Allow for a small relative error if strongInvariantCheck fails + auto weakInvariantCheck = [&]() { + return *lptAMMBalanceAfter_ != beast::zero && + withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); + }; + if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck())) + { + JLOG(j.error()) << "AMM " << tx.getTxnType() + << " invariant failed: " << tx.getHash(HashPrefix::transactionID) << " " + << ammPoolChanged_ << " " << amount << " " << amount2 << " " + << poolProductMean << " " << lptAMMBalanceAfter_->getText() << " " + << ((*lptAMMBalanceAfter_ == beast::zero) + ? Number{1} + : ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean)); + return false; + } + + return true; +} + +bool +ValidAMM::finalizeDeposit( + xrpl::STTx const& tx, + xrpl::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) + return false; + + return true; +} + +bool +ValidAMM::finalizeWithdraw( + xrpl::STTx const& tx, + xrpl::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // Last Withdraw or Clawback deleted AMM + } + else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + { + if (enforce) + return false; + } + + return true; +} + +bool +ValidAMM::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Delete may return tecINCOMPLETE if there are too many + // trustlines to delete. + if (result != tesSUCCESS && result != tecINCOMPLETE) + return true; + + bool const enforce = view.rules().enabled(fixAMMv1_3); + + switch (tx.getTxnType()) + { + case ttAMM_CREATE: + return finalizeCreate(tx, view, enforce, j); + case ttAMM_DEPOSIT: + return finalizeDeposit(tx, view, enforce, j); + case ttAMM_CLAWBACK: + case ttAMM_WITHDRAW: + return finalizeWithdraw(tx, view, enforce, j); + case ttAMM_BID: + return finalizeBid(enforce, j); + case ttAMM_VOTE: + return finalizeVote(enforce, j); + case ttAMM_DELETE: + return finalizeDelete(enforce, result, j); + case ttCHECK_CASH: + case ttOFFER_CREATE: + case ttPAYMENT: + return finalizeDEX(enforce, j); + default: + break; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp b/src/libxrpl/tx/invariants/FreezeInvariant.cpp new file mode 100644 index 0000000000..858c4cdcb8 --- /dev/null +++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp @@ -0,0 +1,278 @@ +#include +// +#include +#include +#include +#include +#include + +namespace xrpl { + +void +TransfersNotFrozen::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + /* + * A trust line freeze state alone doesn't determine if a transfer is + * frozen. The transfer must be examined "end-to-end" because both sides of + * the transfer may have different freeze states and freeze impact depends + * on the transfer direction. This is why first we need to track the + * transfers using IssuerChanges senders/receivers. + * + * Only in validateIssuerChanges, after we collected all changes can we + * determine if the transfer is valid. + */ + if (!isValidEntry(before, after)) + { + return; + } + + auto const balanceChange = calculateBalanceChange(before, after, isDelete); + if (balanceChange.signum() == 0) + { + return; + } + + recordBalanceChanges(after, balanceChange); +} + +bool +TransfersNotFrozen::finalize( + STTx const& tx, + TER const ter, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j) +{ + /* + * We check this invariant regardless of deep freeze amendment status, + * allowing for detection and logging of potential issues even when the + * amendment is disabled. + * + * If an exploit that allows moving frozen assets is discovered, + * we can alert operators who monitor fatal messages and trigger assert in + * debug builds for an early warning. + * + * In an unlikely event that an exploit is found, this early detection + * enables encouraging the UNL to expedite deep freeze amendment activation + * or deploy hotfixes via new amendments. In case of a new amendment, we'd + * only have to change this line setting 'enforce' variable. + * enforce = view.rules().enabled(featureDeepFreeze) || + * view.rules().enabled(fixFreezeExploit); + */ + [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze); + + for (auto const& [issue, changes] : balanceChanges_) + { + auto const issuerSle = findIssuer(issue.account, view); + // It should be impossible for the issuer to not be found, but check + // just in case so rippled doesn't crash in release. + if (!issuerSle) + { + // The comment above starting with "assert(enforce)" explains this + // assert. + XRPL_ASSERT( + enforce, + "xrpl::TransfersNotFrozen::finalize : enforce " + "invariant."); + if (enforce) + { + return false; + } + continue; + } + + if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce)) + { + return false; + } + } + + return true; +} + +bool +TransfersNotFrozen::isValidEntry( + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + // `after` can never be null, even if the trust line is deleted. + XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after."); + if (!after) + { + return false; + } + + if (after->getType() == ltACCOUNT_ROOT) + { + possibleIssuers_.emplace(after->at(sfAccount), after); + return false; + } + + /* While LedgerEntryTypesMatch invariant also checks types, all invariants + * are processed regardless of previous failures. + * + * This type check is still necessary here because it prevents potential + * issues in subsequent processing. + */ + return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE); +} + +STAmount +TransfersNotFrozen::calculateBalanceChange( + std::shared_ptr const& before, + std::shared_ptr const& after, + bool isDelete) +{ + auto const getBalance = [](auto const& line, auto const& other, bool zero) { + STAmount amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed(); + return zero ? amt.zeroed() : amt; + }; + + /* Trust lines can be created dynamically by other transactions such as + * Payment and OfferCreate that cross offers. Such trust line won't be + * created frozen, but the sender might be, so the starting balance must be + * treated as zero. + */ + auto const balanceBefore = getBalance(before, after, false); + + /* Same as above, trust lines can be dynamically deleted, and for frozen + * trust lines, payments not involving the issuer must be blocked. This is + * achieved by treating the final balance as zero when isDelete=true to + * ensure frozen line restrictions are enforced even during deletion. + */ + auto const balanceAfter = getBalance(after, before, isDelete); + + return balanceAfter - balanceBefore; +} + +void +TransfersNotFrozen::recordBalance(Issue const& issue, BalanceChange change) +{ + XRPL_ASSERT( + change.balanceChangeSign, + "xrpl::TransfersNotFrozen::recordBalance : valid trustline " + "balance sign."); + auto& changes = balanceChanges_[issue]; + if (change.balanceChangeSign < 0) + changes.senders.emplace_back(std::move(change)); + else + changes.receivers.emplace_back(std::move(change)); +} + +void +TransfersNotFrozen::recordBalanceChanges( + std::shared_ptr const& after, + STAmount const& balanceChange) +{ + auto const balanceChangeSign = balanceChange.signum(); + auto const currency = after->at(sfBalance).getCurrency(); + + // Change from low account's perspective, which is trust line default + recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign}); + + // Change from high account's perspective, which reverses the sign. + recordBalance({currency, after->at(sfLowLimit).getIssuer()}, {after, -balanceChangeSign}); +} + +std::shared_ptr +TransfersNotFrozen::findIssuer(AccountID const& issuerID, ReadView const& view) +{ + if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end()) + { + return it->second; + } + + return view.read(keylet::account(issuerID)); +} + +bool +TransfersNotFrozen::validateIssuerChanges( + std::shared_ptr const& issuer, + IssuerChanges const& changes, + STTx const& tx, + beast::Journal const& j, + bool enforce) +{ + if (!issuer) + { + return false; + } + + bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze); + if (changes.receivers.empty() || changes.senders.empty()) + { + /* If there are no receivers, then the holder(s) are returning + * their tokens to the issuer. Likewise, if there are no + * senders, then the issuer is issuing tokens to the holder(s). + * This is allowed regardless of the issuer's freeze flags. (The + * holder may have contradicting freeze flags, but that will be + * checked when the holder is treated as issuer.) + */ + return true; + } + + for (auto const& actors : {changes.senders, changes.receivers}) + { + for (auto const& change : actors) + { + bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount); + + if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze)) + { + return false; + } + } + } + return true; +} + +bool +TransfersNotFrozen::validateFrozenState( + BalanceChange const& change, + bool high, + STTx const& tx, + beast::Journal const& j, + bool enforce, + bool globalFreeze) +{ + bool const freeze = + change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); + bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze); + bool const frozen = globalFreeze || deepFreeze || freeze; + + bool const isAMMLine = change.line->isFlag(lsfAMMNode); + + if (!frozen) + { + return true; + } + + // AMMClawbacks are allowed to override some freeze rules + if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze)) + { + JLOG(j.debug()) << "Invariant check allowing funds to be moved " + << (change.balanceChangeSign > 0 ? "to" : "from") + << " a frozen trustline for AMMClawback " << tx.getTransactionID(); + return true; + } + + JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for " + << tx.getTransactionID(); + // The comment above starting with "assert(enforce)" explains this assert. + XRPL_ASSERT( + enforce, + "xrpl::TransfersNotFrozen::validateFrozenState : enforce " + "invariant."); + + if (enforce) + { + return false; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp new file mode 100644 index 0000000000..79c593c57c --- /dev/null +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -0,0 +1,1009 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \ + case tag: { \ + return (privileges) & priv; \ + } + +bool +hasPrivilege(STTx const& tx, Privilege priv) +{ + switch (tx.getTxnType()) + { +#include + + // Deprecated types + default: + return false; + } +}; + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + +void +TransactionFeeCheck::visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ + // nothing to do +} + +bool +TransactionFeeCheck::finalize( + STTx const& tx, + TER const, + XRPAmount const fee, + ReadView const&, + beast::Journal const& j) +{ + // We should never charge a negative fee + if (fee.drops() < 0) + { + JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops(); + return false; + } + + // We should never charge a fee that's greater than or equal to the + // entire XRP supply. + if (fee >= INITIAL_XRP) + { + JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops(); + return false; + } + + // We should never charge more for a transaction than the transaction + // authorizes. It's possible to charge less in some circumstances. + if (fee > tx.getFieldAmount(sfFee).xrp()) + { + JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops() + << " exceeds fee specified in transaction."; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +XRPNotCreated::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + /* We go through all modified ledger entries, looking only at account roots, + * escrow payments, and payment channels. We remove from the total any + * previous XRP values and add to the total any new XRP values. The net + * balance of a payment channel is computed from two fields (amount and + * balance) and deletions are ignored for paychan and escrow because the + * amount fields have not been adjusted for those in the case of deletion. + */ + if (before) + { + switch (before->getType()) + { + case ltACCOUNT_ROOT: + drops_ -= (*before)[sfBalance].xrp().drops(); + break; + case ltPAYCHAN: + drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); + break; + case ltESCROW: + if (isXRP((*before)[sfAmount])) + drops_ -= (*before)[sfAmount].xrp().drops(); + break; + default: + break; + } + } + + if (after) + { + switch (after->getType()) + { + case ltACCOUNT_ROOT: + drops_ += (*after)[sfBalance].xrp().drops(); + break; + case ltPAYCHAN: + if (!isDelete) + drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); + break; + case ltESCROW: + if (!isDelete && isXRP((*after)[sfAmount])) + drops_ += (*after)[sfAmount].xrp().drops(); + break; + default: + break; + } + } +} + +bool +XRPNotCreated::finalize( + STTx const& tx, + TER const, + XRPAmount const fee, + ReadView const&, + beast::Journal const& j) +{ + // The net change should never be positive, as this would mean that the + // transaction created XRP out of thin air. That's not possible. + if (drops_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_; + return false; + } + + // The negative of the net change should be equal to actual fee charged. + if (-drops_ != fee.drops()) + { + JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee " + << fee.drops(); + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +XRPBalanceChecks::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto isBad = [](STAmount const& balance) { + if (!balance.native()) + return true; + + auto const drops = balance.xrp(); + + // Can't have more than the number of drops instantiated + // in the genesis ledger. + if (drops > INITIAL_XRP) + return true; + + // Can't have a negative balance (0 is OK) + if (drops < XRPAmount{0}) + return true; + + return false; + }; + + if (before && before->getType() == ltACCOUNT_ROOT) + bad_ |= isBad((*before)[sfBalance]); + + if (after && after->getType() == ltACCOUNT_ROOT) + bad_ |= isBad((*after)[sfBalance]); +} + +bool +XRPBalanceChecks::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (bad_) + { + JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance"; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +NoBadOffers::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto isBad = [](STAmount const& pays, STAmount const& gets) { + // An offer should never be negative + if (pays < beast::zero) + return true; + + if (gets < beast::zero) + return true; + + // Can't have an XRP to XRP offer: + return pays.native() && gets.native(); + }; + + if (before && before->getType() == ltOFFER) + bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]); + + if (after && after->getType() == ltOFFER) + bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]); +} + +bool +NoBadOffers::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (bad_) + { + JLOG(j.fatal()) << "Invariant failed: offer with a bad amount"; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +NoZeroEscrow::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto isBad = [](STAmount const& amount) { + // XRP case + if (amount.native()) + { + if (amount.xrp() <= XRPAmount{0}) + return true; + + if (amount.xrp() >= INITIAL_XRP) + return true; + } + else + { + // IOU case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; + + if (badCurrency() == amount.getCurrency()) + return true; + } + + // MPT case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; + + if (amount.mpt() > MPTAmount{maxMPTokenAmount}) + return true; // LCOV_EXCL_LINE + } + } + return false; + }; + + if (before && before->getType() == ltESCROW) + bad_ |= isBad((*before)[sfAmount]); + + if (after && after->getType() == ltESCROW) + bad_ |= isBad((*after)[sfAmount]); + + auto checkAmount = [this](std::int64_t amount) { + if (amount > maxMPTokenAmount || amount < 0) + bad_ = true; + }; + + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + auto const outstanding = (*after)[sfOutstandingAmount]; + checkAmount(outstanding); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + bad_ = outstanding < *locked; + } + } + + if (after && after->getType() == ltMPTOKEN) + { + auto const mptAmount = (*after)[sfMPTAmount]; + checkAmount(mptAmount); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + } + } +} + +bool +NoZeroEscrow::finalize( + STTx const& txn, + TER const, + XRPAmount const, + ReadView const& rv, + beast::Journal const& j) +{ + if (bad_) + { + JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount"; + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +AccountRootsNotDeleted::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const&) +{ + if (isDelete && before && before->getType() == ltACCOUNT_ROOT) + accountsDeleted_++; +} + +bool +AccountRootsNotDeleted::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + // AMM account root can be deleted as the result of AMM withdraw/delete + // transaction when the total AMM LP Tokens balance goes to 0. + // A successful AccountDelete or AMMDelete MUST delete exactly + // one account root. + if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) + { + if (accountsDeleted_ == 1) + return true; + + if (accountsDeleted_ == 0) + JLOG(j.fatal()) << "Invariant failed: account deletion " + "succeeded without deleting an account"; + else + JLOG(j.fatal()) << "Invariant failed: account deletion " + "succeeded but deleted multiple accounts!"; + return false; + } + + // A successful AMMWithdraw/AMMClawback MAY delete one account root + // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw + // deletes the AMM account, accountsDeleted_ is set if it is deleted. + if (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && accountsDeleted_ == 1) + return true; + + if (accountsDeleted_ == 0) + return true; + + JLOG(j.fatal()) << "Invariant failed: an account root was deleted"; + return false; +} + +//------------------------------------------------------------------------------ + +void +AccountRootsDeletedClean::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete && before && before->getType() == ltACCOUNT_ROOT) + accountsDeleted_.emplace_back(before, after); +} + +bool +AccountRootsDeletedClean::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Always check for objects in the ledger, but to prevent differing + // transaction processing results, however unlikely, only fail if the + // feature is enabled. Enabled, or not, though, a fatal-level message will + // be logged + [[maybe_unused]] bool const enforce = view.rules().enabled(featureInvariantsV1_1) || + view.rules().enabled(featureSingleAssetVault) || + view.rules().enabled(featureLendingProtocol); + + auto const objectExists = [&view, enforce, &j](auto const& keylet) { + (void)enforce; + if (auto const sle = view.read(keylet)) + { + // Finding the object is bad + auto const typeName = [&sle]() { + auto item = LedgerFormats::getInstance().findByType(sle->getType()); + + if (item != nullptr) + return item->getName(); + return std::to_string(sle->getType()); + }(); + + JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName + << " object"; + // The comment above starting with "assert(enforce)" explains this + // assert. + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize::objectExists : " + "account deletion left no objects behind"); + return true; + } + return false; + }; + + for (auto const& [before, after] : accountsDeleted_) + { + auto const accountID = before->getAccountID(sfAccount); + // An account should not be deleted with a balance + if (after->at(sfBalance) != beast::zero) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a non-zero balance"; + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize : " + "deleted account has zero balance"); + if (enforce) + return false; + } + // An account should not be deleted with a non-zero owner count + if (after->at(sfOwnerCount) != 0) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a non-zero owner count"; + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize : " + "deleted account has zero owner count"); + if (enforce) + return false; + } + // Simple types + for (auto const& [keyletfunc, _, __] : directAccountKeylets) + { + if (objectExists(std::invoke(keyletfunc, accountID)) && enforce) + return false; + } + + { + // NFT pages. nftpage_min and nftpage_max were already explicitly + // checked above as entries in directAccountKeylets. This uses + // view.succ() to check for any NFT pages in between the two + // endpoints. + Keylet const first = keylet::nftpage_min(accountID); + Keylet const last = keylet::nftpage_max(accountID); + + std::optional key = view.succ(first.key, last.key.next()); + + // current page + if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce) + return false; + } + + // If the account is a pseudo account, then the linked object must + // also be deleted. e.g. AMM, Vault, etc. + for (auto const& field : getPseudoAccountFields()) + { + if (before->isFieldPresent(*field)) + { + auto const key = before->getFieldH256(*field); + if (objectExists(keylet::unchecked(key)) && enforce) + return false; + } + } + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +LedgerEntryTypesMatch::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (before && after && before->getType() != after->getType()) + typeMismatch_ = true; + + if (after) + { +#pragma push_macro("LEDGER_ENTRY") +#undef LEDGER_ENTRY + +#define LEDGER_ENTRY(tag, ...) case tag: + + switch (after->getType()) + { +#include + + break; + default: + invalidTypeAdded_ = true; + break; + } + +#undef LEDGER_ENTRY +#pragma pop_macro("LEDGER_ENTRY") + } +} + +bool +LedgerEntryTypesMatch::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if ((!typeMismatch_) && (!invalidTypeAdded_)) + return true; + + if (typeMismatch_) + { + JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch"; + } + + if (invalidTypeAdded_) + { + JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added"; + } + + return false; +} + +//------------------------------------------------------------------------------ + +void +NoXRPTrustLines::visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltRIPPLE_STATE) + { + // checking the issue directly here instead of + // relying on .native() just in case native somehow + // were systematically incorrect + xrpTrustLine_ = after->getFieldAmount(sfLowLimit).issue() == xrpIssue() || + after->getFieldAmount(sfHighLimit).issue() == xrpIssue(); + } +} + +bool +NoXRPTrustLines::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (!xrpTrustLine_) + return true; + + JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created"; + return false; +} + +//------------------------------------------------------------------------------ + +void +NoDeepFreezeTrustLinesWithoutFreeze::visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltRIPPLE_STATE) + { + std::uint32_t const uFlags = after->getFieldU32(sfFlags); + bool const lowFreeze = uFlags & lsfLowFreeze; + bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze; + + bool const highFreeze = uFlags & lsfHighFreeze; + bool const highDeepFreeze = uFlags & lsfHighDeepFreeze; + + deepFreezeWithoutFreeze_ = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze); + } +} + +bool +NoDeepFreezeTrustLinesWithoutFreeze::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (!deepFreezeWithoutFreeze_) + return true; + + JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag " + "without normal freeze was created"; + return false; +} + +//------------------------------------------------------------------------------ + +void +ValidNewAccountRoot::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (!before && after->getType() == ltACCOUNT_ROOT) + { + accountsCreated_++; + accountSeq_ = (*after)[sfSequence]; + pseudoAccount_ = isPseudoAccount(after); + flags_ = after->getFlags(); + } +} + +bool +ValidNewAccountRoot::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (accountsCreated_ == 0) + return true; + + if (accountsCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: multiple accounts " + "created in a single transaction"; + return false; + } + + // From this point on we know exactly one account was created. + if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) + { + bool const pseudoAccount = + (pseudoAccount_ && + (view.rules().enabled(featureSingleAssetVault) || + view.rules().enabled(featureLendingProtocol))); + + if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct)) + { + JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " + "wrong transaction type"; + return false; + } + + std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq(); + + if (accountSeq_ != startingSeq) + { + JLOG(j.fatal()) << "Invariant failed: account created with " + "wrong starting sequence number"; + return false; + } + + if (pseudoAccount) + { + std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + if (flags_ != expected) + { + JLOG(j.fatal()) << "Invariant failed: pseudo-account created with " + "wrong flags"; + return false; + } + } + + return true; + } + + JLOG(j.fatal()) << "Invariant failed: account root created illegally"; + return false; +} // namespace xrpl + +//------------------------------------------------------------------------------ + +void +ValidClawback::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const&) +{ + if (before && before->getType() == ltRIPPLE_STATE) + trustlinesChanged++; + + if (before && before->getType() == ltMPTOKEN) + mptokensChanged++; +} + +bool +ValidClawback::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (tx.getTxnType() != ttCLAWBACK) + return true; + + if (result == tesSUCCESS) + { + if (trustlinesChanged > 1) + { + JLOG(j.fatal()) << "Invariant failed: more than one trustline changed."; + return false; + } + + if (mptokensChanged > 1) + { + JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed."; + return false; + } + + if (trustlinesChanged == 1) + { + AccountID const issuer = tx.getAccountID(sfAccount); + STAmount const& amount = tx.getFieldAmount(sfAmount); + AccountID const& holder = amount.getIssuer(); + STAmount const holderBalance = + accountHolds(view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j); + + if (holderBalance.signum() < 0) + { + JLOG(j.fatal()) << "Invariant failed: trustline balance is negative"; + return false; + } + } + } + else + { + if (trustlinesChanged != 0) + { + JLOG(j.fatal()) << "Invariant failed: some trustlines were changed " + "despite failure of the transaction."; + return false; + } + + if (mptokensChanged != 0) + { + JLOG(j.fatal()) << "Invariant failed: some mptokens were changed " + "despite failure of the transaction."; + return false; + } + } + + return true; +} + +//------------------------------------------------------------------------------ + +void +ValidPseudoAccounts::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + // Deletion is ignored + return; + + if (after && after->getType() == ltACCOUNT_ROOT) + { + bool const isPseudo = [&]() { + // isPseudoAccount checks that any of the pseudo-account fields are + // set. + if (isPseudoAccount(after)) + return true; + // Not all pseudo-accounts have a zero sequence, but all accounts + // with a zero sequence had better be pseudo-accounts. + if (after->at(sfSequence) == 0) + return true; + + return false; + }(); + if (isPseudo) + { + // Pseudo accounts must have the following properties: + // 1. Exactly one of the pseudo-account fields is set. + // 2. The sequence number is not changed. + // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth + // flags are set. + // 4. The RegularKey is not set. + { + std::vector const& fields = getPseudoAccountFields(); + + auto const numFields = + std::count_if(fields.begin(), fields.end(), [&after](SField const* sf) -> bool { + return after->isFieldPresent(*sf); + }); + if (numFields != 1) + { + std::stringstream error; + error << "pseudo-account has " << numFields << " pseudo-account fields set"; + errors_.emplace_back(error.str()); + } + } + if (before && before->at(sfSequence) != after->at(sfSequence)) + { + errors_.emplace_back("pseudo-account sequence changed"); + } + if (!after->isFlag(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)) + { + errors_.emplace_back("pseudo-account flags are not set"); + } + if (after->isFieldPresent(sfRegularKey)) + { + errors_.emplace_back("pseudo-account has a regular key"); + } + } + } +} + +bool +ValidPseudoAccounts::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + bool const enforce = view.rules().enabled(featureSingleAssetVault); + XRPL_ASSERT( + errors_.empty() || enforce, + "xrpl::ValidPseudoAccounts::finalize : no bad " + "changes or enforce invariant"); + if (!errors_.empty()) + { + for (auto const& error : errors_) + { + JLOG(j.fatal()) << "Invariant failed: " << error; + } + if (enforce) + return false; + } + return true; +} + +//------------------------------------------------------------------------------ + +void +NoModifiedUnmodifiableFields::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete || !before) + // Creation and deletion are ignored + return; + + changedEntries_.emplace(before, after); +} + +bool +NoModifiedUnmodifiableFields::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + static auto const fieldChanged = [](auto const& before, auto const& after, auto const& field) { + bool const beforeField = before->isFieldPresent(field); + bool const afterField = after->isFieldPresent(field); + return beforeField != afterField || (afterField && before->at(field) != after->at(field)); + }; + for (auto const& slePair : changedEntries_) + { + auto const& before = slePair.first; + auto const& after = slePair.second; + auto const type = after->getType(); + bool bad = false; + [[maybe_unused]] bool enforce = false; + switch (type) + { + case ltLOAN_BROKER: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex) || + fieldChanged(before, after, sfSequence) || + fieldChanged(before, after, sfOwnerNode) || + fieldChanged(before, after, sfVaultNode) || + fieldChanged(before, after, sfVaultID) || + fieldChanged(before, after, sfAccount) || + fieldChanged(before, after, sfOwner) || + fieldChanged(before, after, sfManagementFeeRate) || + fieldChanged(before, after, sfCoverRateMinimum) || + fieldChanged(before, after, sfCoverRateLiquidation); + break; + case ltLOAN: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex) || + fieldChanged(before, after, sfSequence) || + fieldChanged(before, after, sfOwnerNode) || + fieldChanged(before, after, sfLoanBrokerNode) || + fieldChanged(before, after, sfLoanBrokerID) || + fieldChanged(before, after, sfBorrower) || + fieldChanged(before, after, sfLoanOriginationFee) || + fieldChanged(before, after, sfLoanServiceFee) || + fieldChanged(before, after, sfLatePaymentFee) || + fieldChanged(before, after, sfClosePaymentFee) || + fieldChanged(before, after, sfOverpaymentFee) || + fieldChanged(before, after, sfInterestRate) || + fieldChanged(before, after, sfLateInterestRate) || + fieldChanged(before, after, sfCloseInterestRate) || + fieldChanged(before, after, sfOverpaymentInterestRate) || + fieldChanged(before, after, sfStartDate) || + fieldChanged(before, after, sfPaymentInterval) || + fieldChanged(before, after, sfGracePeriod) || + fieldChanged(before, after, sfLoanScale); + break; + default: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + * + * We use the lending protocol as a gate, even though + * all transactions are affected because that's when it + * was added. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex); + } + XRPL_ASSERT( + !bad || enforce, + "xrpl::NoModifiedUnmodifiableFields::finalize : no bad " + "changes or enforce invariant"); + if (bad) + { + JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for " + << tx.getTransactionID(); + if (enforce) + return false; + } + } + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp b/src/libxrpl/tx/invariants/LoanInvariant.cpp new file mode 100644 index 0000000000..01c4da46ac --- /dev/null +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp @@ -0,0 +1,278 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidLoanBroker::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after) + { + if (after->getType() == ltLOAN_BROKER) + { + auto& broker = brokers_[after->key()]; + broker.brokerBefore = before; + broker.brokerAfter = after; + } + else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = after->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + brokers_.emplace(loanBrokerID, BrokerInfo{}); + } + else if (after->getType() == ltRIPPLE_STATE) + { + lines_.emplace_back(after); + } + else if (after->getType() == ltMPTOKEN) + { + mpts_.emplace_back(after); + } + } +} + +bool +ValidLoanBroker::goodZeroDirectory( + ReadView const& view, + SLE::const_ref dir, + beast::Journal const& j) const +{ + auto const next = dir->at(~sfIndexNext); + auto const prev = dir->at(~sfIndexPrevious); + if ((prev && *prev) || (next && *next)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " + "OwnerCount has multiple directory pages"; + return false; + } + auto indexes = dir->getFieldV256(sfIndexes); + if (indexes.size() > 1) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " + "OwnerCount has multiple indexes in the Directory root"; + return false; + } + if (indexes.size() == 1) + { + auto const index = indexes.value().front(); + auto const sle = view.read(keylet::unchecked(index)); + if (!sle) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt"; + return false; + } + if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " + "OwnerCount has an unexpected entry in the directory"; + return false; + } + } + + return true; +} + +bool +ValidLoanBroker::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Loan Brokers will not exist on ledger if the Lending Protocol amendment + // is not enabled, so there's no need to check it. + + for (auto const& line : lines_) + { + for (auto const& field : {&sfLowLimit, &sfHighLimit}) + { + auto const account = view.read(keylet::account(line->at(*field).getIssuer())); + // This Invariant doesn't know about the rules for Trust Lines, so + // if the account is missing, don't treat it as an error. This + // loop is only concerned with finding Broker pseudo-accounts + if (account && account->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = account->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + brokers_.emplace(loanBrokerID, BrokerInfo{}); + } + } + } + for (auto const& mpt : mpts_) + { + auto const account = view.read(keylet::account(mpt->at(sfAccount))); + // This Invariant doesn't know about the rules for MPTokens, so + // if the account is missing, don't treat is as an error. This + // loop is only concerned with finding Broker pseudo-accounts + if (account && account->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = account->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + brokers_.emplace(loanBrokerID, BrokerInfo{}); + } + } + + for (auto const& [brokerID, broker] : brokers_) + { + auto const& after = + broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID)); + + if (!after) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker missing"; + return false; + } + + auto const& before = broker.brokerBefore; + + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants + // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most + // one node (the root), which will only hold entries for `RippleState` + // or `MPToken` objects. + if (after->at(sfOwnerCount) == 0) + { + auto const dir = view.read(keylet::ownerDir(after->at(sfAccount))); + if (dir) + { + if (!goodZeroDirectory(view, dir, j)) + { + return false; + } + } + } + if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number " + "decreased"; + return false; + } + if (after->at(sfDebtTotal) < 0) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative"; + return false; + } + if (after->at(sfCoverAvailable) < 0) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative"; + return false; + } + auto const vault = view.read(keylet::vault(after->at(sfVaultID))); + if (!vault) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid"; + return false; + } + auto const& vaultAsset = vault->at(sfAsset); + if (after->at(sfCoverAvailable) < accountHolds( + view, + after->at(sfAccount), + vaultAsset, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available " + "is less than pseudo-account asset balance"; + return false; + } + } + return true; +} + +//------------------------------------------------------------------------------ + +void +ValidLoan::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltLOAN) + { + loans_.emplace_back(before, after); + } +} + +bool +ValidLoan::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Loans will not exist on ledger if the Lending Protocol amendment + // is not enabled, so there's no need to check it. + + for (auto const& [before, after] : loans_) + { + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants + // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off + if (after->at(sfPaymentRemaining) == 0 && + (after->at(sfTotalValueOutstanding) != beast::zero || + after->at(sfPrincipalOutstanding) != beast::zero || + after->at(sfManagementFeeOutstanding) != beast::zero)) + { + JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " + "remaining has not been paid off"; + return false; + } + // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid + // off + if (after->at(sfPaymentRemaining) != 0 && + after->at(sfTotalValueOutstanding) == beast::zero && + after->at(sfPrincipalOutstanding) == beast::zero && + after->at(sfManagementFeeOutstanding) == beast::zero) + { + JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " + "remaining has not been paid off"; + return false; + } + if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment))) + { + JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed"; + return false; + } + // Must not be negative - STNumber + for (auto const field : + {&sfLoanServiceFee, + &sfLatePaymentFee, + &sfClosePaymentFee, + &sfPrincipalOutstanding, + &sfTotalValueOutstanding, + &sfManagementFeeOutstanding}) + { + if (after->at(*field) < 0) + { + JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative "; + return false; + } + } + // Must be positive - STNumber + for (auto const field : { + &sfPeriodicPayment, + }) + { + if (after->at(*field) <= 0) + { + JLOG(j.fatal()) << "Invariant failed: " << field->getName() + << " is zero or negative "; + return false; + } + } + } + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp new file mode 100644 index 0000000000..20957b8d43 --- /dev/null +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -0,0 +1,192 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidMPTIssuance::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + if (isDelete) + mptIssuancesDeleted_++; + else if (!before) + mptIssuancesCreated_++; + } + + if (after && after->getType() == ltMPTOKEN) + { + if (isDelete) + mptokensDeleted_++; + else if (!before) + { + mptokensCreated_++; + MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)}; + if (mptIssue.getIssuer() == after->at(sfAccount)) + mptCreatedByIssuer_ = true; + } + } +} + +bool +ValidMPTIssuance::finalize( + STTx const& tx, + TER const result, + XRPAmount const _fee, + ReadView const& view, + beast::Journal const& j) +{ + if (result == tesSUCCESS) + { + auto const& rules = view.rules(); + [[maybe_unused]] + bool enforceCreatedByIssuer = + rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol); + if (mptCreatedByIssuer_) + { + JLOG(j.fatal()) << "Invariant failed: MPToken created for the MPT issuer"; + // The comment above starting with "assert(enforce)" explains this + // assert. + XRPL_ASSERT_PARTS( + enforceCreatedByIssuer, "xrpl::ValidMPTIssuance::finalize", "no issuer MPToken"); + if (enforceCreatedByIssuer) + return false; + } + + auto const txnType = tx.getTxnType(); + if (hasPrivilege(tx, createMPTIssuance)) + { + if (mptIssuancesCreated_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: transaction " + "succeeded without creating a MPT issuance"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: transaction " + "succeeded while removing MPT issuances"; + } + else if (mptIssuancesCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: transaction " + "succeeded but created multiple issuances"; + } + + return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; + } + + if (hasPrivilege(tx, destroyMPTIssuance)) + { + if (mptIssuancesDeleted_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded without removing a MPT issuance"; + } + else if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded while creating MPT issuances"; + } + else if (mptIssuancesDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion " + "succeeded but deleted multiple issuances"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; + } + + bool const lendingProtocolEnabled = view.rules().enabled(featureLendingProtocol); + // ttESCROW_FINISH may authorize an MPT, but it can't have the + // mayAuthorizeMPT privilege, because that may cause + // non-amendment-gated side effects. + bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) && + (view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled); + if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish) + { + bool const submittedByIssuer = tx.isFieldPresent(sfHolder); + + if (mptIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but created MPT issuances"; + return false; + } + else if (mptIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize " + "succeeded but deleted issuances"; + return false; + } + else if (lendingProtocolEnabled && mptokensCreated_ + mptokensDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " + "but created/deleted bad number mptokens"; + return false; + } + else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer " + "succeeded but created/deleted mptokens"; + return false; + } + else if ( + !submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) && + (mptokensCreated_ + mptokensDeleted_ != 1)) + { + // if the holder submitted this tx, then a mptoken must be + // either created or deleted. + JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder " + "succeeded but created/deleted bad number of mptokens"; + return false; + } + + return true; + } + if (txnType == ttESCROW_FINISH) + { + // ttESCROW_FINISH may authorize an MPT, but it can't have the + // mayAuthorizeMPT privilege, because that may cause + // non-amendment-gated side effects. + XRPL_ASSERT_PARTS( + !enforceEscrowFinish, "xrpl::ValidMPTIssuance::finalize", "not escrow finish tx"); + return true; + } + + if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && mptokensCreated_ == 0 && + mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) + return true; + } + + if (mptIssuancesCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created"; + } + else if (mptIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted"; + } + else if (mptokensCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was created"; + } + else if (mptokensDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted"; + } + + return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && + mptokensDeleted_ == 0; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/NFTInvariant.cpp b/src/libxrpl/tx/invariants/NFTInvariant.cpp new file mode 100644 index 0000000000..db06896023 --- /dev/null +++ b/src/libxrpl/tx/invariants/NFTInvariant.cpp @@ -0,0 +1,274 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidNFTokenPage::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + static constexpr uint256 const& pageBits = nft::pageMask; + static constexpr uint256 const accountBits = ~pageBits; + + if ((before && before->getType() != ltNFTOKEN_PAGE) || + (after && after->getType() != ltNFTOKEN_PAGE)) + return; + + auto check = [this, isDelete](std::shared_ptr const& sle) { + uint256 const account = sle->key() & accountBits; + uint256 const hiLimit = sle->key() & pageBits; + std::optional const prev = (*sle)[~sfPreviousPageMin]; + + // Make sure that any page links... + // 1. Are properly associated with the owning account and + // 2. The page is correctly ordered between links. + if (prev) + { + if (account != (*prev & accountBits)) + badLink_ = true; + + if (hiLimit <= (*prev & pageBits)) + badLink_ = true; + } + + if (auto const next = (*sle)[~sfNextPageMin]) + { + if (account != (*next & accountBits)) + badLink_ = true; + + if (hiLimit >= (*next & pageBits)) + badLink_ = true; + } + + { + auto const& nftokens = sle->getFieldArray(sfNFTokens); + + // An NFTokenPage should never contain too many tokens or be empty. + if (std::size_t const nftokenCount = nftokens.size(); + (!isDelete && nftokenCount == 0) || nftokenCount > dirMaxTokensPerPage) + invalidSize_ = true; + + // If prev is valid, use it to establish a lower bound for + // page entries. If prev is not valid the lower bound is zero. + uint256 const loLimit = prev ? *prev & pageBits : uint256(beast::zero); + + // Also verify that all NFTokenIDs in the page are sorted. + uint256 loCmp = loLimit; + for (auto const& obj : nftokens) + { + uint256 const tokenID = obj[sfNFTokenID]; + if (!nft::compareTokens(loCmp, tokenID)) + badSort_ = true; + loCmp = tokenID; + + // None of the NFTs on this page should belong on lower or + // higher pages. + if (uint256 const tokenPageBits = tokenID & pageBits; + tokenPageBits < loLimit || tokenPageBits >= hiLimit) + badEntry_ = true; + + if (auto uri = obj[~sfURI]; uri && uri->empty()) + badURI_ = true; + } + } + }; + + if (before) + { + check(before); + + // While an account's NFToken directory contains any NFTokens, the last + // NFTokenPage (with 96 bits of 1 in the low part of the index) should + // never be deleted. + if (isDelete && (before->key() & nft::pageMask) == nft::pageMask && + before->isFieldPresent(sfPreviousPageMin)) + { + deletedFinalPage_ = true; + } + } + + if (after) + check(after); + + if (!isDelete && before && after) + { + // If the NFTokenPage + // 1. Has a NextMinPage field in before, but loses it in after, and + // 2. This is not the last page in the directory + // Then we have identified a corruption in the links between the + // NFToken pages in the NFToken directory. + if ((before->key() & nft::pageMask) != nft::pageMask && + before->isFieldPresent(sfNextPageMin) && !after->isFieldPresent(sfNextPageMin)) + { + deletedLink_ = true; + } + } +} + +bool +ValidNFTokenPage::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (badLink_) + { + JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked."; + return false; + } + + if (badEntry_) + { + JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page."; + return false; + } + + if (badSort_) + { + JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted."; + return false; + } + + if (badURI_) + { + JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI."; + return false; + } + + if (invalidSize_) + { + JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size."; + return false; + } + + if (view.rules().enabled(fixNFTokenPageLinks)) + { + if (deletedFinalPage_) + { + JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with " + "non-empty directory."; + return false; + } + if (deletedLink_) + { + JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link."; + return false; + } + } + + return true; +} + +//------------------------------------------------------------------------------ +void +NFTokenCountTracking::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (before && before->getType() == ltACCOUNT_ROOT) + { + beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0); + beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0); + } + + if (after && after->getType() == ltACCOUNT_ROOT) + { + afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0); + afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0); + } +} + +bool +NFTokenCountTracking::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (!hasPrivilege(tx, changeNFTCounts)) + { + if (beforeMintedTotal != afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: the number of minted tokens " + "changed without a mint transaction!"; + return false; + } + + if (beforeBurnedTotal != afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: the number of burned tokens " + "changed without a burn transaction!"; + return false; + } + + return true; + } + + if (tx.getTxnType() == ttNFTOKEN_MINT) + { + if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: successful minting didn't increase " + "the number of minted tokens."; + return false; + } + + if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: failed minting changed the " + "number of minted tokens."; + return false; + } + + if (beforeBurnedTotal != afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: minting changed the number of " + "burned tokens."; + return false; + } + } + + if (tx.getTxnType() == ttNFTOKEN_BURN) + { + if (result == tesSUCCESS) + { + if (beforeBurnedTotal >= afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: successful burning didn't increase " + "the number of burned tokens."; + return false; + } + } + + if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal) + { + JLOG(j.fatal()) << "Invariant failed: failed burning changed the " + "number of burned tokens."; + return false; + } + + if (beforeMintedTotal != afterMintedTotal) + { + JLOG(j.fatal()) << "Invariant failed: burning changed the number of " + "minted tokens."; + return false; + } + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp new file mode 100644 index 0000000000..2ece1f3fc0 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp @@ -0,0 +1,93 @@ +#include +// +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidPermissionedDEX::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltDIR_NODE) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + } + + if (after && after->getType() == ltOFFER) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + else + regularOffers_ = true; + + // if a hybrid offer is missing domain or additional book, there's + // something wrong + if (after->isFlag(lsfHybrid) && + (!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) || + after->getFieldArray(sfAdditionalBooks).size() > 1)) + badHybrids_ = true; + } +} + +bool +ValidPermissionedDEX::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + auto const txType = tx.getTxnType(); + if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || result != tesSUCCESS) + return true; + + // For each offercreate transaction, check if + // permissioned offers are valid + if (txType == ttOFFER_CREATE && badHybrids_) + { + JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed"; + return false; + } + + if (!tx.isFieldPresent(sfDomainID)) + return true; + + auto const domain = tx.getFieldH256(sfDomainID); + + if (!view.exists(keylet::permissionedDomain(domain))) + { + JLOG(j.fatal()) << "Invariant failed: domain doesn't exist"; + return false; + } + + // for both payment and offercreate, there shouldn't be another domain + // that's different from the domain specified + for (auto const& d : domains_) + { + if (d != domain) + { + JLOG(j.fatal()) << "Invariant failed: transaction" + " consumed wrong domains"; + return false; + } + } + + if (regularOffers_) + { + JLOG(j.fatal()) << "Invariant failed: domain transaction" + " affected regular offers"; + return false; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp new file mode 100644 index 0000000000..77acbe12c6 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp @@ -0,0 +1,162 @@ +#include +// +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidPermissionedDomain::visitEntry( + bool isDel, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (before && before->getType() != ltPERMISSIONED_DOMAIN) + return; + if (after && after->getType() != ltPERMISSIONED_DOMAIN) + return; + + auto check = [isDel](std::vector& sleStatus, std::shared_ptr const& sle) { + auto const& credentials = sle->getFieldArray(sfAcceptedCredentials); + auto const sorted = credentials::makeSorted(credentials); + + SleStatus ss{credentials.size(), false, !sorted.empty(), isDel}; + + // If array have duplicates then all the other checks are invalid + if (ss.isUnique_) + { + unsigned i = 0; + for (auto const& cred : sorted) + { + auto const& credTx = credentials[i++]; + ss.isSorted_ = + (cred.first == credTx[sfIssuer]) && (cred.second == credTx[sfCredentialType]); + if (!ss.isSorted_) + break; + } + } + sleStatus.emplace_back(std::move(ss)); + }; + + if (after) + check(sleStatus_, after); +} + +bool +ValidPermissionedDomain::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + auto check = [](SleStatus const& sleStatus, beast::Journal const& j) { + if (!sleStatus.credentialsSize_) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain with " + "no rules."; + return false; + } + + if (sleStatus.credentialsSize_ > maxPermissionedDomainCredentialsArraySize) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain bad " + "credentials size " + << sleStatus.credentialsSize_; + return false; + } + + if (!sleStatus.isUnique_) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " + "aren't unique"; + return false; + } + + if (!sleStatus.isSorted_) + { + JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials " + "aren't sorted"; + return false; + } + + return true; + }; + + if (view.rules().enabled(fixPermissionedDomainInvariant)) + { + // No permissioned domains should be affected if the transaction failed + if (result != tesSUCCESS) + // If nothing changed, all is good. If there were changes, that's + // bad. + return sleStatus_.empty(); + + if (sleStatus_.size() > 1) + { + JLOG(j.fatal()) << "Invariant failed: transaction affected more " + "than 1 permissioned domain entry."; + return false; + } + + switch (tx.getTxnType()) + { + case ttPERMISSIONED_DOMAIN_SET: { + if (sleStatus_.empty()) + { + JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " + "PermissionedDomainSet"; + return false; + } + + auto const& sleStatus = sleStatus_[0]; + if (sleStatus.isDelete_) + { + JLOG(j.fatal()) << "Invariant failed: domain object " + "deleted by PermissionedDomainSet"; + return false; + } + return check(sleStatus, j); + } + case ttPERMISSIONED_DOMAIN_DELETE: { + if (sleStatus_.empty()) + { + JLOG(j.fatal()) << "Invariant failed: no domain objects affected by " + "PermissionedDomainDelete"; + return false; + } + + if (!sleStatus_[0].isDelete_) + { + JLOG(j.fatal()) << "Invariant failed: domain object " + "modified, but not deleted by " + "PermissionedDomainDelete"; + return false; + } + return true; + } + default: { + if (!sleStatus_.empty()) + { + JLOG(j.fatal()) << "Invariant failed: " << sleStatus_.size() + << " domain object(s) affected by an " + "unauthorized transaction. " + << tx.getTxnType(); + return false; + } + return true; + } + } + } + else + { + if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS || + sleStatus_.empty()) + return true; + return check(sleStatus_[0], j); + } +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp new file mode 100644 index 0000000000..69d29448d3 --- /dev/null +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -0,0 +1,1072 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +ValidVault::Vault +ValidVault::Vault::make(SLE const& from) +{ + XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object"); + + ValidVault::Vault self; + self.key = from.key(); + self.asset = from.at(sfAsset); + self.pseudoId = from.getAccountID(sfAccount); + self.owner = from.at(sfOwner); + self.shareMPTID = from.getFieldH192(sfShareMPTID); + self.assetsTotal = from.at(sfAssetsTotal); + self.assetsAvailable = from.at(sfAssetsAvailable); + self.assetsMaximum = from.at(sfAssetsMaximum); + self.lossUnrealized = from.at(sfLossUnrealized); + return self; +} + +ValidVault::Shares +ValidVault::Shares::make(SLE const& from) +{ + XRPL_ASSERT( + from.getType() == ltMPTOKEN_ISSUANCE, + "ValidVault::Shares::make : from MPTokenIssuance object"); + + ValidVault::Shares self; + self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer))); + self.sharesTotal = from.at(sfOutstandingAmount); + self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount); + return self; +} + +void +ValidVault::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + // If `before` is empty, this means an object is being created, in which + // case `isDelete` must be false. Otherwise `before` and `after` are set and + // `isDelete` indicates whether an object is being deleted or modified. + XRPL_ASSERT( + after != nullptr && (before != nullptr || !isDelete), + "xrpl::ValidVault::visitEntry : some object is available"); + + // Number balanceDelta will capture the difference (delta) between "before" + // state (zero if created) and "after" state (zero if destroyed), and + // preserves value scale (exponent) to round values to the same scale during + // validation. It is used to validate that the change in account + // balances matches the change in vault balances, stored to deltas_ at the + // end of this function. + DeltaInfo balanceDelta{numZero, std::nullopt}; + + std::int8_t sign = 0; + if (before) + { + switch (before->getType()) + { + case ltVAULT: + beforeVault_.push_back(Vault::make(*before)); + break; + case ltMPTOKEN_ISSUANCE: + // At this moment we have no way of telling if this object holds + // vault shares or something else. Save it for finalize. + beforeMPTs_.push_back(Shares::make(*before)); + balanceDelta.delta = + static_cast(before->getFieldU64(sfOutstandingAmount)); + // MPTs are ints, so the scale is always 0. + balanceDelta.scale = 0; + sign = 1; + break; + case ltMPTOKEN: + balanceDelta.delta = static_cast(before->getFieldU64(sfMPTAmount)); + // MPTs are ints, so the scale is always 0. + balanceDelta.scale = 0; + sign = -1; + break; + case ltACCOUNT_ROOT: + balanceDelta.delta = before->getFieldAmount(sfBalance); + // Account balance is XRP, which is an int, so the scale is + // always 0. + balanceDelta.scale = 0; + sign = -1; + break; + case ltRIPPLE_STATE: { + auto const amount = before->getFieldAmount(sfBalance); + balanceDelta.delta = amount; + // Trust Line balances are STAmounts, so we can use the exponent + // directly to get the scale. + balanceDelta.scale = amount.exponent(); + sign = -1; + break; + } + default:; + } + } + + if (!isDelete && after) + { + switch (after->getType()) + { + case ltVAULT: + afterVault_.push_back(Vault::make(*after)); + break; + case ltMPTOKEN_ISSUANCE: + // At this moment we have no way of telling if this object holds + // vault shares or something else. Save it for finalize. + afterMPTs_.push_back(Shares::make(*after)); + balanceDelta.delta -= + Number(static_cast(after->getFieldU64(sfOutstandingAmount))); + // MPTs are ints, so the scale is always 0. + balanceDelta.scale = 0; + sign = 1; + break; + case ltMPTOKEN: + balanceDelta.delta -= + Number(static_cast(after->getFieldU64(sfMPTAmount))); + // MPTs are ints, so the scale is always 0. + balanceDelta.scale = 0; + sign = -1; + break; + case ltACCOUNT_ROOT: + balanceDelta.delta -= Number(after->getFieldAmount(sfBalance)); + // Account balance is XRP, which is an int, so the scale is + // always 0. + balanceDelta.scale = 0; + sign = -1; + break; + case ltRIPPLE_STATE: { + auto const amount = after->getFieldAmount(sfBalance); + balanceDelta.delta -= Number(amount); + // Trust Line balances are STAmounts, so we can use the exponent + // directly to get the scale. + if (amount.exponent() > balanceDelta.scale) + balanceDelta.scale = amount.exponent(); + sign = -1; + break; + } + default:; + } + } + + uint256 const key = (before ? before->key() : after->key()); + // Append to deltas if sign is non-zero, i.e. an object of an interesting + // type has been updated. A transaction may update an object even when + // its balance has not changed, e.g. transaction fee equals the amount + // transferred to the account. We intentionally do not compare balanceDelta + // against zero, to avoid missing such updates. + if (sign != 0) + { + XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized"); + balanceDelta.delta *= sign; + deltas_[key] = balanceDelta; + } +} + +bool +ValidVault::finalize( + STTx const& tx, + TER const ret, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j) +{ + bool const enforce = view.rules().enabled(featureSingleAssetVault); + + if (!isTesSuccess(ret)) + return true; // Do not perform checks + + if (afterVault_.empty() && beforeVault_.empty()) + { + if (hasPrivilege(tx, mustModifyVault)) + { + JLOG(j.fatal()) << // + "Invariant failed: vault operation succeeded without modifying " + "a vault"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant"); + return !enforce; + } + + return true; // Not a vault operation + } + else if (!(hasPrivilege(tx, mustModifyVault) || hasPrivilege(tx, mayModifyVault))) + { + JLOG(j.fatal()) << // + "Invariant failed: vault updated by a wrong transaction type"; + XRPL_ASSERT( + enforce, + "xrpl::ValidVault::finalize : illegal vault transaction " + "invariant"); + return !enforce; // Also not a vault operation + } + + if (beforeVault_.size() > 1 || afterVault_.size() > 1) + { + JLOG(j.fatal()) << // + "Invariant failed: vault operation updated more than single vault"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant"); + return !enforce; // That's all we can do here + } + + auto const txnType = tx.getTxnType(); + + // We do special handling for ttVAULT_DELETE first, because it's the only + // vault-modifying transaction without an "after" state of the vault + if (afterVault_.empty()) + { + if (txnType != ttVAULT_DELETE) + { + JLOG(j.fatal()) << // + "Invariant failed: vault deleted by a wrong transaction type"; + XRPL_ASSERT( + enforce, + "xrpl::ValidVault::finalize : illegal vault deletion " + "invariant"); + return !enforce; // That's all we can do here + } + + // Note, if afterVault_ is empty then we know that beforeVault_ is not + // empty, as enforced at the top of this function + auto const& beforeVault = beforeVault_[0]; + + // At this moment we only know a vault is being deleted and there + // might be some MPTokenIssuance objects which are deleted in the + // same transaction. Find the one matching this vault. + auto const deletedShares = [&]() -> std::optional { + for (auto const& e : beforeMPTs_) + { + if (e.share.getMptID() == beforeVault.shareMPTID) + return std::move(e); + } + return std::nullopt; + }(); + + if (!deletedShares) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must also " + "delete shares"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant"); + return !enforce; // That's all we can do here + } + + bool result = true; + if (deletedShares->sharesTotal != 0) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "shares outstanding"; + result = false; + } + if (beforeVault.assetsTotal != zero) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "assets outstanding"; + result = false; + } + if (beforeVault.assetsAvailable != zero) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "assets available"; + result = false; + } + + return result; + } + else if (txnType == ttVAULT_DELETE) + { + JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without " + "deleting a vault"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant"); + return !enforce; // That's all we can do here + } + + // Note, `afterVault_.empty()` is handled above + auto const& afterVault = afterVault_[0]; + XRPL_ASSERT( + beforeVault_.empty() || beforeVault_[0].key == afterVault.key, + "xrpl::ValidVault::finalize : single vault operation"); + + auto const updatedShares = [&]() -> std::optional { + // At this moment we only know that a vault is being updated and there + // might be some MPTokenIssuance objects which are also updated in the + // same transaction. Find the one matching the shares to this vault. + // Note, we expect updatedMPTs collection to be extremely small. For + // such collections linear search is faster than lookup. + for (auto const& e : afterMPTs_) + { + if (e.share.getMptID() == afterVault.shareMPTID) + return e; + } + + auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID)); + + return sleShares ? std::optional(Shares::make(*sleShares)) : std::nullopt; + }(); + + bool result = true; + + // Universal transaction checks + if (!beforeVault_.empty()) + { + auto const& beforeVault = beforeVault_[0]; + if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId || + afterVault.shareMPTID != beforeVault.shareMPTID) + { + JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data"; + result = false; + } + } + + if (!updatedShares) + { + JLOG(j.fatal()) << "Invariant failed: updated vault must have shares"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant"); + return !enforce; // That's all we can do here + } + + if (updatedShares->sharesTotal == 0) + { + if (afterVault.assetsTotal != zero) + { + JLOG(j.fatal()) << "Invariant failed: updated zero sized " + "vault must have no assets outstanding"; + result = false; + } + if (afterVault.assetsAvailable != zero) + { + JLOG(j.fatal()) << "Invariant failed: updated zero sized " + "vault must have no assets available"; + result = false; + } + } + else if (updatedShares->sharesTotal > updatedShares->sharesMaximum) + { + JLOG(j.fatal()) // + << "Invariant failed: updated shares must not exceed maximum " + << updatedShares->sharesMaximum; + result = false; + } + + if (afterVault.assetsAvailable < zero) + { + JLOG(j.fatal()) << "Invariant failed: assets available must be positive"; + result = false; + } + + if (afterVault.assetsAvailable > afterVault.assetsTotal) + { + JLOG(j.fatal()) << "Invariant failed: assets available must " + "not be greater than assets outstanding"; + result = false; + } + else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable) + { + JLOG(j.fatal()) // + << "Invariant failed: loss unrealized must not exceed " + "the difference between assets outstanding and available"; + result = false; + } + + if (afterVault.assetsTotal < zero) + { + JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive"; + result = false; + } + + if (afterVault.assetsMaximum < zero) + { + JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive"; + result = false; + } + + // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when + // enforcing invariants on transaction types other than ttVAULT_CREATE + if (beforeVault_.empty() && txnType != ttVAULT_CREATE) + { + JLOG(j.fatal()) << // + "Invariant failed: vault created by a wrong transaction type"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant"); + return !enforce; // That's all we can do here + } + + if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized && + txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY) + { + JLOG(j.fatal()) << // + "Invariant failed: vault transaction must not change loss " + "unrealized"; + result = false; + } + + auto const beforeShares = [&]() -> std::optional { + if (beforeVault_.empty()) + return std::nullopt; + auto const& beforeVault = beforeVault_[0]; + + for (auto const& e : beforeMPTs_) + { + if (e.share.getMptID() == beforeVault.shareMPTID) + return std::move(e); + } + return std::nullopt; + }(); + + if (!beforeShares && + (tx.getTxnType() == ttVAULT_DEPOSIT || // + tx.getTxnType() == ttVAULT_WITHDRAW || // + tx.getTxnType() == ttVAULT_CLAWBACK)) + { + JLOG(j.fatal()) << "Invariant failed: vault operation succeeded " + "without updating shares"; + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant"); + return !enforce; // That's all we can do here + } + + auto const& vaultAsset = afterVault.asset; + auto const deltaAssets = [&](AccountID const& id) -> std::optional { + auto const get = // + [&](auto const& it, std::int8_t sign = 1) -> std::optional { + if (it == deltas_.end()) + return std::nullopt; + + return DeltaInfo{it->second.delta * sign, it->second.scale}; + }; + + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + { + if (isXRP(issue)) + return get(deltas_.find(keylet::account(id).key)); + return get( + deltas_.find(keylet::line(id, issue).key), id > issue.getIssuer() ? -1 : 1); + } + else if constexpr (std::is_same_v) + { + return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key)); + } + }, + vaultAsset.value()); + }; + auto const deltaAssetsTxAccount = [&]() -> std::optional { + auto ret = deltaAssets(tx[sfAccount]); + // Nothing returned or not XRP transaction + if (!ret.has_value() || !vaultAsset.native()) + return ret; + + // Delegated transaction; no need to compensate for fees + if (auto const delegate = tx[~sfDelegate]; + delegate.has_value() && *delegate != tx[sfAccount]) + return ret; + + ret->delta += fee.drops(); + if (ret->delta == zero) + return std::nullopt; + + return ret; + }; + auto const deltaShares = [&](AccountID const& id) -> std::optional { + auto const it = [&]() { + if (id == afterVault.pseudoId) + return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key); + return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key); + }(); + + return it != deltas_.end() ? std::optional(it->second) : std::nullopt; + }; + + auto const vaultHoldsNoAssets = [&](Vault const& vault) { + return vault.assetsAvailable == 0 && vault.assetsTotal == 0; + }; + + // Technically this does not need to be a lambda, but it's more + // convenient thanks to early "return false"; the not-so-nice + // alternatives are several layers of nested if/else or more complex + // (i.e. brittle) if statements. + result &= [&]() { + switch (txnType) + { + case ttVAULT_CREATE: { + bool result = true; + + if (!beforeVault_.empty()) + { + JLOG(j.fatal()) // + << "Invariant failed: create operation must not have " + "updated a vault"; + result = false; + } + + if (afterVault.assetsAvailable != zero || afterVault.assetsTotal != zero || + afterVault.lossUnrealized != zero || updatedShares->sharesTotal != 0) + { + JLOG(j.fatal()) // + << "Invariant failed: created vault must be empty"; + result = false; + } + + if (afterVault.pseudoId != updatedShares->share.getIssuer()) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer and vault " + "pseudo-account must be the same"; + result = false; + } + + auto const sleSharesIssuer = + view.read(keylet::account(updatedShares->share.getIssuer())); + if (!sleSharesIssuer) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer must exist"; + return false; + } + + if (!isPseudoAccount(sleSharesIssuer)) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer must be a " + "pseudo-account"; + result = false; + } + + if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; + !vaultId || *vaultId != afterVault.key) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer pseudo-account " + "must point back to the vault"; + result = false; + } + + return result; + } + case ttVAULT_SET: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + if (vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change vault balance"; + result = false; + } + + if (beforeVault.assetsTotal != afterVault.assetsTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change assets " + "outstanding"; + result = false; + } + + if (afterVault.assetsMaximum > zero && + afterVault.assetsTotal > afterVault.assetsMaximum) + { + JLOG(j.fatal()) << // + "Invariant failed: set assets outstanding must not " + "exceed assets maximum"; + result = false; + } + + if (beforeVault.assetsAvailable != afterVault.assetsAvailable) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change assets " + "available"; + result = false; + } + + if (beforeShares && updatedShares && + beforeShares->sharesTotal != updatedShares->sharesTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change shares " + "outstanding"; + result = false; + } + + return result; + } + case ttVAULT_DEPOSIT: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); + if (!maybeVaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault balance"; + return false; // That's all we can do + } + + // Get the coarsest scale to round calculations to + DeltaInfo totalDelta{ + afterVault.assetsTotal - beforeVault.assetsTotal, + std::max( + scale(afterVault.assetsTotal, vaultAsset), + scale(beforeVault.assetsTotal, vaultAsset))}; + DeltaInfo availableDelta{ + afterVault.assetsAvailable - beforeVault.assetsAvailable, + std::max( + scale(afterVault.assetsAvailable, vaultAsset), + scale(beforeVault.assetsAvailable, vaultAsset))}; + auto const minScale = computeMinScale( + vaultAsset, + { + *maybeVaultDeltaAssets, + totalDelta, + availableDelta, + }); + + auto const vaultDeltaAssets = + roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale); + auto const txAmount = roundToAsset(vaultAsset, tx[sfAmount], minScale); + + if (vaultDeltaAssets > txAmount) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must not change vault " + "balance by more than deposited amount"; + result = false; + } + + if (vaultDeltaAssets <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must increase vault balance"; + result = false; + } + + // Any payments (including deposits) made by the issuer + // do not change their balance, but create funds instead. + bool const issuerDeposit = [&]() -> bool { + if (vaultAsset.native()) + return false; + return tx[sfAccount] == vaultAsset.getIssuer(); + }(); + + if (!issuerDeposit) + { + auto const maybeAccDeltaAssets = deltaAssetsTxAccount(); + if (!maybeAccDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor " + "balance"; + return false; + } + auto const localMinScale = + std::max(minScale, computeMinScale(vaultAsset, {*maybeAccDeltaAssets})); + + auto const accountDeltaAssets = + roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale); + auto const localVaultDeltaAssets = + roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale); + + if (accountDeltaAssets >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must decrease depositor " + "balance"; + result = false; + } + + if (localVaultDeltaAssets * -1 != accountDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault and " + "depositor balance by equal amount"; + result = false; + } + } + + if (afterVault.assetsMaximum > zero && + afterVault.assetsTotal > afterVault.assetsMaximum) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit assets outstanding must not " + "exceed assets maximum"; + result = false; + } + + auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]); + if (!maybeAccDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor " + "shares"; + return false; // That's all we can do + } + // We don't need to round shares, they are integral MPT + auto const& accountDeltaShares = *maybeAccDeltaShares; + if (accountDeltaShares.delta <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must increase depositor " + "shares"; + result = false; + } + + auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault shares"; + return false; // That's all we can do + } + + // We don't need to round shares, they are integral MPT + auto const& vaultDeltaShares = *maybeVaultDeltaShares; + if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor and " + "vault shares by equal amount"; + result = false; + } + + auto const assetTotalDelta = roundToAsset( + vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale); + if (assetTotalDelta != vaultDeltaAssets) + { + JLOG(j.fatal()) << "Invariant failed: deposit and assets " + "outstanding must add up"; + result = false; + } + + auto const assetAvailableDelta = roundToAsset( + vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale); + if (assetAvailableDelta != vaultDeltaAssets) + { + JLOG(j.fatal()) << "Invariant failed: deposit and assets " + "available must add up"; + result = false; + } + + return result; + } + case ttVAULT_WITHDRAW: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), + "xrpl::ValidVault::finalize : withdrawal updated a " + "vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); + + if (!maybeVaultDeltaAssets) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal must " + "change vault balance"; + return false; // That's all we can do + } + + // Get the most coarse scale to round calculations to + auto const totalDelta = DeltaInfo{ + afterVault.assetsTotal - beforeVault.assetsTotal, + std::max( + scale(afterVault.assetsTotal, vaultAsset), + scale(beforeVault.assetsTotal, vaultAsset))}; + auto const availableDelta = DeltaInfo{ + afterVault.assetsAvailable - beforeVault.assetsAvailable, + std::max( + scale(afterVault.assetsAvailable, vaultAsset), + scale(beforeVault.assetsAvailable, vaultAsset))}; + auto const minScale = computeMinScale( + vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta}); + + auto const vaultPseudoDeltaAssets = + roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale); + + if (vaultPseudoDeltaAssets >= zero) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal must " + "decrease vault balance"; + result = false; + } + + // Any payments (including withdrawal) going to the issuer + // do not change their balance, but destroy funds instead. + bool const issuerWithdrawal = [&]() -> bool { + if (vaultAsset.native()) + return false; + auto const destination = tx[~sfDestination].value_or(tx[sfAccount]); + return destination == vaultAsset.getIssuer(); + }(); + + if (!issuerWithdrawal) + { + auto const maybeAccDelta = deltaAssetsTxAccount(); + auto const maybeOtherAccDelta = [&]() -> std::optional { + if (auto const destination = tx[~sfDestination]; + destination && *destination != tx[sfAccount]) + return deltaAssets(*destination); + return std::nullopt; + }(); + + if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value()) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change one " + "destination balance"; + return false; + } + + auto const destinationDelta = // + maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta; + + // the scale of destinationDelta can be coarser than + // minScale, so we take that into account when rounding + auto const localMinScale = + std::max(minScale, computeMinScale(vaultAsset, {destinationDelta})); + + auto const roundedDestinationDelta = + roundToAsset(vaultAsset, destinationDelta.delta, localMinScale); + + if (roundedDestinationDelta <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must increase " + "destination balance"; + result = false; + } + + auto const localPseudoDeltaAssets = + roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale); + if (localPseudoDeltaAssets * -1 != roundedDestinationDelta) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change vault " + "and destination balance by equal amount"; + result = false; + } + } + + // We don't need to round shares, they are integral MPT + auto const accountDeltaShares = deltaShares(tx[sfAccount]); + if (!accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change depositor " + "shares"; + return false; + } + + if (accountDeltaShares->delta >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must decrease depositor " + "shares"; + result = false; + } + + // We don't need to round shares, they are integral MPT + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares || vaultDeltaShares->delta == zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change vault shares"; + return false; // That's all we can do + } + + if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change depositor " + "and vault shares by equal amount"; + result = false; + } + + auto const assetTotalDelta = roundToAsset( + vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale); + // Note, vaultBalance is negative (see check above) + if (assetTotalDelta != vaultPseudoDeltaAssets) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal and " + "assets outstanding must add up"; + result = false; + } + + auto const assetAvailableDelta = roundToAsset( + vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale); + + if (assetAvailableDelta != vaultPseudoDeltaAssets) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal and " + "assets available must add up"; + result = false; + } + + return result; + } + case ttVAULT_CLAWBACK: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount]) + { + // The owner can use clawback to force-burn shares when the + // vault is empty but there are outstanding shares + if (!(beforeShares && beforeShares->sharesTotal > 0 && + vaultHoldsNoAssets(beforeVault) && beforeVault.owner == tx[sfAccount])) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback may only be performed " + "by the asset issuer, or by the vault owner of an " + "empty vault"; + return false; // That's all we can do + } + } + + auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId); + if (maybeVaultDeltaAssets) + { + auto const totalDelta = DeltaInfo{ + afterVault.assetsTotal - beforeVault.assetsTotal, + std::max( + scale(afterVault.assetsTotal, vaultAsset), + scale(beforeVault.assetsTotal, vaultAsset))}; + auto const availableDelta = DeltaInfo{ + afterVault.assetsAvailable - beforeVault.assetsAvailable, + std::max( + scale(afterVault.assetsAvailable, vaultAsset), + scale(beforeVault.assetsAvailable, vaultAsset))}; + auto const minScale = computeMinScale( + vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta}); + auto const vaultDeltaAssets = + roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale); + if (vaultDeltaAssets >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must decrease vault " + "balance"; + result = false; + } + + auto const assetsTotalDelta = roundToAsset( + vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale); + if (assetsTotalDelta != vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback and assets outstanding " + "must add up"; + result = false; + } + + auto const assetAvailableDelta = roundToAsset( + vaultAsset, + afterVault.assetsAvailable - beforeVault.assetsAvailable, + minScale); + if (assetAvailableDelta != vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback and assets available " + "must add up"; + result = false; + } + } + else if (!vaultHoldsNoAssets(beforeVault)) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change vault balance"; + return false; // That's all we can do + } + + // We don't need to round shares, they are integral MPT + auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]); + if (!maybeAccountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change holder shares"; + return false; // That's all we can do + } + if (maybeAccountDeltaShares->delta >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must decrease holder " + "shares"; + result = false; + } + + // We don't need to round shares, they are integral MPT + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares || vaultDeltaShares->delta == zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change vault shares"; + return false; // That's all we can do + } + + if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change holder and " + "vault shares by equal amount"; + result = false; + } + + return result; + } + + case ttLOAN_SET: + case ttLOAN_MANAGE: + case ttLOAN_PAY: { + // TBD + return true; + } + + default: + // LCOV_EXCL_START + UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type"); + return false; + // LCOV_EXCL_STOP + } + }(); + + if (!result) + { + // The comment at the top of this file starting with "assert(enforce)" + // explains this assert. + XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants"); + return !enforce; + } + + return true; +} + +[[nodiscard]] std::int32_t +ValidVault::computeMinScale(Asset const& asset, std::vector const& numbers) +{ + if (numbers.size() == 0) + return 0; + + auto const max = + std::max_element(numbers.begin(), numbers.end(), [](auto const& a, auto const& b) -> bool { + return a.scale < b.scale; + }); + XRPL_ASSERT_PARTS( + max->scale, "xrpl::ValidVault::computeMinScale", "scale set for destinationDelta"); + return max->scale.value_or(STAmount::cMaxOffset); +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/AMM/AMMUtils.cpp b/src/libxrpl/tx/transactors/AMM/AMMUtils.cpp index ac67e861f1..55d71ced4f 100644 --- a/src/libxrpl/tx/transactors/AMM/AMMUtils.cpp +++ b/src/libxrpl/tx/transactors/AMM/AMMUtils.cpp @@ -180,8 +180,9 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Issue const if (auto const sle = view.read(keylet::account(ammAccountID))) return (*sle)[sfBalance]; } - else if (auto const sle = view.read(keylet::line(ammAccountID, issue.account, issue.currency)); - sle && !isFrozen(view, ammAccountID, issue.currency, issue.account)) + else if ( + auto const sle = view.read(keylet::line(ammAccountID, issue.account, issue.currency)); + sle && !isFrozen(view, ammAccountID, issue.currency, issue.account)) { auto amount = (*sle)[sfBalance]; if (ammAccountID > issue.account) diff --git a/src/libxrpl/tx/transactors/AMM/AMMVote.cpp b/src/libxrpl/tx/transactors/AMM/AMMVote.cpp index f40015ac08..549d9705b5 100644 --- a/src/libxrpl/tx/transactors/AMM/AMMVote.cpp +++ b/src/libxrpl/tx/transactors/AMM/AMMVote.cpp @@ -42,8 +42,9 @@ AMMVote::preclaim(PreclaimContext const& ctx) } else if (ammSle->getFieldAmount(sfLPTokenBalance) == beast::zero) return tecAMM_EMPTY; - else if (auto const lpTokensNew = ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j); - lpTokensNew == beast::zero) + else if ( + auto const lpTokensNew = ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j); + lpTokensNew == beast::zero) { JLOG(ctx.j.debug()) << "AMM Vote: account is not LP."; return tecAMM_INVALID_TOKENS; diff --git a/src/libxrpl/tx/transactors/Lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/Lending/LoanBrokerCoverWithdraw.cpp index 07f81d3c23..075f615210 100644 --- a/src/libxrpl/tx/transactors/Lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/Lending/LoanBrokerCoverWithdraw.cpp @@ -121,7 +121,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) return roundToAsset( vaultAsset, tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))), - currentDebtTotal.scale(vaultAsset)); + scale(currentDebtTotal, vaultAsset)); }(); if (coverAvail < amount) return tecINSUFFICIENT_FUNDS; diff --git a/src/libxrpl/tx/transactors/Lending/LoanSet.cpp b/src/libxrpl/tx/transactors/Lending/LoanSet.cpp index 82a64bc89b..5e45bd5a9a 100644 --- a/src/libxrpl/tx/transactors/Lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/Lending/LoanSet.cpp @@ -84,11 +84,12 @@ LoanSet::preflight(PreflightContext const& ctx) !validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval)) return temINVALID; // 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)) + else if ( + auto const gracePeriod = tx[~sfGracePeriod]; // + !validNumericRange( + gracePeriod, + paymentInterval.value_or(LoanSet::defaultPaymentInterval), + defaultGracePeriod)) return temINVALID; // Copied from preflight2 diff --git a/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainDelete.cpp b/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainDelete.cpp index e013bd8d2b..861bb934be 100644 --- a/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainDelete.cpp +++ b/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainDelete.cpp @@ -18,7 +18,7 @@ TER PermissionedDomainDelete::preclaim(PreclaimContext const& ctx) { auto const domain = ctx.tx.getFieldH256(sfDomainID); - auto const sleDomain = ctx.view.read({ltPERMISSIONED_DOMAIN, domain}); + auto const sleDomain = ctx.view.read(keylet::permissionedDomain(domain)); if (!sleDomain) return tecNO_ENTRY; @@ -40,7 +40,7 @@ PermissionedDomainDelete::doApply() ctx_.tx.isFieldPresent(sfDomainID), "xrpl::PermissionedDomainDelete::doApply : required field present"); - auto const slePd = view().peek({ltPERMISSIONED_DOMAIN, ctx_.tx.at(sfDomainID)}); + auto const slePd = view().peek(keylet::permissionedDomain(ctx_.tx.at(sfDomainID))); auto const page = (*slePd)[sfOwnerNode]; if (!view().dirRemove(keylet::ownerDir(account_), page, slePd->key(), true)) diff --git a/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp index 51ff4d8217..15bb79b239 100644 --- a/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.cpp @@ -1,10 +1,9 @@ +#include +// #include #include #include #include -#include - -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp index 0b3aef19a8..2562672041 100644 --- a/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/Vault/VaultDelete.cpp @@ -18,6 +18,13 @@ VaultDelete::preflight(PreflightContext const& ctx) return temMALFORMED; } + if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(fixLendingProtocolV1_1)) + return temDISABLED; + + // The sfMemoData field is an optional field used to record the deletion reason. + if (auto const data = ctx.tx[~sfMemoData]; data && !validDataLength(data, maxDataPayloadLength)) + return temMALFORMED; + return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/XChainBridge.cpp b/src/libxrpl/tx/transactors/XChainBridge.cpp index 30fc9f59e1..64daa6d1ee 100644 --- a/src/libxrpl/tx/transactors/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/XChainBridge.cpp @@ -1126,8 +1126,8 @@ toClaim(STTx const& tx) } catch (...) { + return std::nullopt; } - return std::nullopt; } template diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 12a38eb672..918b525d90 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -3816,7 +3817,7 @@ class Invariants_test : public beast::unit_test::suite NumberMantissaScaleGuard g{MantissaRange::large}; auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo { - return {n, n.scale(vaultAsset.raw())}; + return {n, scale(n, vaultAsset.raw())}; }; auto const testCases = std::vector{ @@ -3833,12 +3834,14 @@ class Invariants_test : public beast::unit_test::suite { .name = "Mixed scales", .expectedMinScale = -17, - .values = {makeDelta(Number{1, -2}), makeDelta(Number{5, -3}), makeDelta(Number{3, -2})}, + .values = + {makeDelta(Number{1, -2}), makeDelta(Number{5, -3}), makeDelta(Number{3, -2})}, }, { .name = "Equal scales", .expectedMinScale = -16, - .values = {makeDelta(Number{1, -1}), makeDelta(Number{5, -1}), makeDelta(Number{1, -1})}, + .values = + {makeDelta(Number{1, -1}), makeDelta(Number{5, -1}), makeDelta(Number{1, -1})}, }, { .name = "Mixed mantissa sizes", @@ -3859,7 +3862,8 @@ class Invariants_test : public beast::unit_test::suite BEAST_EXPECTS( actualScale == tc.expectedMinScale, - "expected: " + std::to_string(tc.expectedMinScale) + ", actual: " + std::to_string(actualScale)); + "expected: " + std::to_string(tc.expectedMinScale) + + ", actual: " + std::to_string(actualScale)); for (auto const& num : tc.values) { // None of these scales are far enough apart that rounding the @@ -3868,8 +3872,8 @@ class Invariants_test : public beast::unit_test::suite auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale); BEAST_EXPECTS( actualRounded == num.delta, - "number " + to_string(num.delta) + " rounded to scale " + std::to_string(actualScale) + " is " + - to_string(actualRounded)); + "number " + to_string(num.delta) + " rounded to scale " + + std::to_string(actualScale) + " is " + to_string(actualRounded)); } } @@ -3896,7 +3900,8 @@ class Invariants_test : public beast::unit_test::suite BEAST_EXPECTS( actualScale == tc.expectedMinScale, - "expected: " + std::to_string(tc.expectedMinScale) + ", actual: " + std::to_string(actualScale)); + "expected: " + std::to_string(tc.expectedMinScale) + + ", actual: " + std::to_string(actualScale)); std::optional first; Number firstRounded; for (auto const& num : tc.values) diff --git a/src/test/app/Loan/Loan_test.cpp b/src/test/app/Loan/Loan_test.cpp index c409474dd0..cae38febbb 100644 --- a/src/test/app/Loan/Loan_test.cpp +++ b/src/test/app/Loan/Loan_test.cpp @@ -1250,7 +1250,7 @@ protected: env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecNO_PERMISSION)); }); -#if LOANTODO +#if LOAN_TODO // TODO /* @@ -3924,7 +3924,7 @@ protected: } } -#if LOANTODO +#if LOAN_TODO void testLoanPayLateFullPaymentBypassesPenalties() { @@ -5709,7 +5709,7 @@ public: void run() override { -#if LOANTODO +#if LOAN_TODO testLoanPayLateFullPaymentBypassesPenalties(); testLoanCoverMinimumRoundingExploit(); #endif diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index a790584ac2..294d5210d9 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -71,7 +71,7 @@ public: { setupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } @@ -81,7 +81,7 @@ public: { cleanupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index c2ae7e0687..fef16cad71 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1064,14 +1064,16 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - auto testCase = [this](std::function test) { + auto testCase = [this]( + std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; @@ -1353,13 +1355,14 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - auto testCase = [this](std::function test) { + auto testCase = [this]( + std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; @@ -5338,20 +5341,77 @@ class Vault_test : public beast::unit_test::suite env.close(); // 2. Mantissa larger than uint64 max + env.set_parse_failure_expected(true); try { tx[sfAssetsMaximum] = "18446744073709551617e5"; // uint64 max + 1 env(tx, THISLINE); - BEAST_EXPECT(false); + BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max"); } catch (parse_error const& e) { using namespace std::string_literals; BEAST_EXPECT( - e.what() == - "invalidParamsField 'tx_json.AssetsMaximum' has invalid " - "data."s); + e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s); } + env.set_parse_failure_expected(false); + } + } + + void + testVaultDeleteData() + { + using namespace test::jtx; + + Env env{*this}; + + Account const owner{"owner"}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault vault{env}; + + auto const keylet = keylet::vault(owner.id(), 1); + auto delTx = vault.del({.owner = owner, .id = keylet.key}); + + // Test VaultDelete with fixLendingProtocolV1_1 disabled + // Transaction fails if the data field is provided + { + testcase("VaultDelete data fixLendingProtocolV1_1 disabled"); + env.disableFeature(fixLendingProtocolV1_1); + delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength, 'A')); + env(delTx, ter(temDISABLED), THISLINE); + env.close(); + env.enableFeature(fixLendingProtocolV1_1); + } + + // Transaction fails if the data field is too large + { + testcase("VaultDelete data fixLendingProtocolV1_1 enabled data too large"); + delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength + 1, 'A')); + env(delTx, ter(temMALFORMED), THISLINE); + env.close(); + } + + // Transaction fails if the data field is set, but is empty + { + testcase("VaultDelete data fixLendingProtocolV1_1 enabled data empty"); + delTx[sfMemoData] = strHex(std::string(0, 'A')); + env(delTx, ter(temMALFORMED), THISLINE); + env.close(); + } + + { + testcase("VaultDelete data fixLendingProtocolV1_1 enabled data valid"); + PrettyAsset const xrpAsset = xrpIssue(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(tx, ter(tesSUCCESS), THISLINE); + env.close(); + // Recreate the transaction as the vault keylet changed + auto delTx = vault.del({.owner = owner, .id = keylet.key}); + delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength, 'A')); + env(delTx, ter(tesSUCCESS), THISLINE); + env.close(); } } @@ -5376,6 +5436,7 @@ public: testVaultClawbackBurnShares(); testVaultClawbackAssets(); testAssetsMaximum(); + testVaultDeleteData(); } }; diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp index a06193ae86..66b368176d 100644 --- a/src/test/core/SociDB_test.cpp +++ b/src/test/core/SociDB_test.cpp @@ -58,7 +58,7 @@ public: { setupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } @@ -68,7 +68,7 @@ public: { cleanupDatabaseDir(getDatabasePath()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 9caf257aa1..2ac0ca7435 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -393,6 +394,48 @@ public: return close(std::chrono::seconds(5)); } + /** Close and advance the ledger, then synchronize with the server's + io_context to ensure all async operations initiated by the close have + been started. + + This function performs the same ledger close as close(), but additionally + ensures that all tasks posted to the server's io_context (such as + WebSocket subscription message sends) have been initiated before returning. + + What it guarantees: + - All async operations posted before syncClose() have been STARTED + - For WebSocket sends: async_write_some() has been called + - The actual I/O completion may still be pending (async) + + What it does NOT guarantee: + - Async operations have COMPLETED + - WebSocket messages have been received by clients + - However, for localhost connections, the remaining latency is typically + microseconds, making tests reliable + + Use this instead of close() when: + - Test code immediately checks for subscription messages + - Race conditions between test and worker threads must be avoided + - Deterministic test behavior is required + + @param timeout Maximum time to wait for the barrier task to execute + @return true if close succeeded and barrier executed within timeout, + false otherwise + */ + [[nodiscard]] bool + syncClose(std::chrono::steady_clock::duration timeout = std::chrono::seconds{1}) + { + XRPL_ASSERT( + app().getNumberOfThreads() == 1, + "syncClose() is only useful on an application with a single thread"); + auto const result = close(); + auto serverBarrier = std::make_shared>(); + auto future = serverBarrier->get_future(); + boost::asio::post(app().getIOContext(), [serverBarrier]() { serverBarrier->set_value(); }); + auto const status = future.wait_for(timeout); + return result && status == std::future_status::ready; + } + /** Turn on JSON tracing. With no arguments, trace all */ diff --git a/src/test/jtx/envconfig.h b/src/test/jtx/envconfig.h index f2f67f935b..e4a1975e74 100644 --- a/src/test/jtx/envconfig.h +++ b/src/test/jtx/envconfig.h @@ -73,6 +73,8 @@ std::unique_ptr admin_localnet(std::unique_ptr); std::unique_ptr secure_gateway_localnet(std::unique_ptr); +std::unique_ptr single_thread_io(std::unique_ptr); + /// @brief adjust configuration with params needed to be a validator /// /// this is intended for use with envconfig, as in diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index df86aaa2e4..4dfd2f2b38 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -587,10 +587,10 @@ Env::st(JTx const& jt) { return sterilize(STTx{std::move(*obj)}); } - catch (std::exception const&) + catch (...) { + return nullptr; } - return nullptr; } std::shared_ptr @@ -613,10 +613,10 @@ Env::ust(JTx const& jt) { return std::make_shared(std::move(*obj)); } - catch (std::exception const&) + catch (...) { + return nullptr; } - return nullptr; } Json::Value diff --git a/src/test/jtx/impl/Oracle.cpp b/src/test/jtx/impl/Oracle.cpp index c9d8c0ce27..302880c972 100644 --- a/src/test/jtx/impl/Oracle.cpp +++ b/src/test/jtx/impl/Oracle.cpp @@ -339,8 +339,8 @@ validDocumentID(AnyValue const& v) } catch (...) { + return false; } - return false; } } // namespace oracle diff --git a/src/test/jtx/impl/WSClient.cpp b/src/test/jtx/impl/WSClient.cpp index 2b92eb5ec3..84424be222 100644 --- a/src/test/jtx/impl/WSClient.cpp +++ b/src/test/jtx/impl/WSClient.cpp @@ -107,6 +107,7 @@ class WSClientImpl : public WSClient { stream_.cancel(); } + // NOLINTNEXTLINE(bugprone-empty-catch) catch (boost::system::system_error const&) { // ignored diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index 31034f3b63..e31e687c3d 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -87,6 +87,12 @@ secure_gateway_localnet(std::unique_ptr cfg) (*cfg)[PORT_WS].set("secure_gateway", "127.0.0.0/8"); return cfg; } +std::unique_ptr +single_thread_io(std::unique_ptr cfg) +{ + cfg->IO_WORKERS = 1; + return cfg; +} auto constexpr defaultseed = "shUwVw52ofnCUX5m7kPTKzJdr4HEH"; diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h index 4a4d21002e..cb2a8e3bd5 100644 --- a/src/test/nodestore/TestBase.h +++ b/src/test/nodestore/TestBase.h @@ -138,7 +138,7 @@ public: { std::shared_ptr object; - Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object); + Status const status = backend.fetch(batch[i]->getHash(), &object); BEAST_EXPECT(status == ok); @@ -158,7 +158,7 @@ public: { std::shared_ptr object; - Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object); + Status const status = backend.fetch(batch[i]->getHash(), &object); BEAST_EXPECT(status == notFound); } diff --git a/src/test/nodestore/Timing_test.cpp b/src/test/nodestore/Timing_test.cpp index dae131e5e7..b537e3abb7 100644 --- a/src/test/nodestore/Timing_test.cpp +++ b/src/test/nodestore/Timing_test.cpp @@ -314,7 +314,7 @@ public: std::shared_ptr obj; std::shared_ptr result; obj = seq1_.obj(dist_(gen_)); - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(result && isSame(result, obj)); } catch (std::exception const& e) @@ -377,9 +377,9 @@ public: { try { - auto const key = seq2_.key(i); + auto const hash = seq2_.key(i); std::shared_ptr result; - backend_.fetch(key.data(), &result); + backend_.fetch(hash, &result); suite_.expect(!result); } catch (std::exception const& e) @@ -449,9 +449,9 @@ public: { if (rand_(gen_) < missingNodePercent) { - auto const key = seq2_.key(dist_(gen_)); + auto const hash = seq2_.key(dist_(gen_)); std::shared_ptr result; - backend_.fetch(key.data(), &result); + backend_.fetch(hash, &result); suite_.expect(!result); } else @@ -459,7 +459,7 @@ public: std::shared_ptr obj; std::shared_ptr result; obj = seq1_.obj(dist_(gen_)); - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(result && isSame(result, obj)); } } @@ -540,8 +540,7 @@ public: std::shared_ptr result; auto const j = older_(gen_); obj = seq1_.obj(j); - std::shared_ptr result1; - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(result != nullptr); suite_.expect(isSame(result, obj)); } @@ -559,7 +558,7 @@ public: std::shared_ptr result; auto const j = recent_(gen_); obj = seq1_.obj(j); - backend_.fetch(obj->getHash().data(), &result); + backend_.fetch(obj->getHash(), &result); suite_.expect(!result || isSame(result, obj)); break; } diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index d83711324d..414bceefd7 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -26,7 +26,7 @@ public: { using namespace std::chrono_literals; using namespace jtx; - Env env(*this); + Env env{*this, single_thread_io(envconfig())}; auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -92,7 +92,7 @@ public: { using namespace std::chrono_literals; using namespace jtx; - Env env(*this); + Env env{*this, single_thread_io(envconfig())}; auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -114,7 +114,7 @@ public: { // Accept a ledger - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -125,7 +125,7 @@ public: { // Accept another ledger - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -150,7 +150,7 @@ public: { using namespace std::chrono_literals; using namespace jtx; - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto baseFee = env.current()->fees().base.drops(); auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -171,7 +171,7 @@ public: { env.fund(XRP(10000), "alice"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update for payment transaction BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -195,7 +195,7 @@ public: })); env.fund(XRP(10000), "bob"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update for payment transaction BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -249,12 +249,12 @@ public: { // Transaction that does not affect stream env.fund(XRP(10000), "carol"); - env.close(); + BEAST_EXPECT(env.syncClose()); BEAST_EXPECT(!wsc->getMsg(10ms)); // Transactions concerning alice env.trust(Account("bob")["USD"](100), "alice"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream updates BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -288,6 +288,7 @@ public: using namespace jtx; Env env(*this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = 10; + cfg = single_thread_io(std::move(cfg)); return cfg; })); auto wsc = makeWSClient(env.app().config()); @@ -310,7 +311,7 @@ public: { env.fund(XRP(10000), "alice"); - env.close(); + BEAST_EXPECT(env.syncClose()); // Check stream update for payment transaction BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -360,7 +361,7 @@ public: testManifests() { using namespace jtx; - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wsc = makeWSClient(env.app().config()); Json::Value stream; @@ -394,7 +395,7 @@ public: { using namespace jtx; - Env env{*this, envconfig(validator, ""), features}; + Env env{*this, single_thread_io(envconfig(validator, "")), features}; auto& cfg = env.app().config(); if (!BEAST_EXPECT(cfg.section(SECTION_VALIDATION_SEED).empty())) return; @@ -483,7 +484,7 @@ public: // at least one flag ledger. while (env.closed()->header().seq < 300) { - env.close(); + BEAST_EXPECT(env.syncClose()); using namespace std::chrono_literals; BEAST_EXPECT(wsc->findMsg(5s, validValidationFields)); } @@ -505,7 +506,7 @@ public: { using namespace jtx; testcase("Subscribe by url"); - Env env{*this}; + Env env{*this, single_thread_io(envconfig())}; Json::Value jv; jv[jss::url] = "http://localhost/events"; @@ -536,7 +537,7 @@ public: auto const method = subscribe ? "subscribe" : "unsubscribe"; testcase << "Error cases for " << method; - Env env{*this}; + Env env{*this, single_thread_io(envconfig())}; auto wsc = makeWSClient(env.app().config()); { @@ -572,7 +573,7 @@ public: } { - Env env_nonadmin{*this, no_admin(envconfig())}; + Env env_nonadmin{*this, single_thread_io(no_admin(envconfig()))}; Json::Value jv; jv[jss::url] = "no-url"; auto jr = env_nonadmin.rpc("json", method, to_string(jv))[jss::result]; @@ -834,12 +835,13 @@ public: * send payments between the two accounts a and b, * and close ledgersToClose ledgers */ - auto sendPayments = [](Env& env, - Account const& a, - Account const& b, - int newTxns, - std::uint32_t ledgersToClose, - int numXRP = 10) { + auto sendPayments = [this]( + Env& env, + Account const& a, + Account const& b, + int newTxns, + std::uint32_t ledgersToClose, + int numXRP = 10) { env.memoize(a); env.memoize(b); for (int i = 0; i < newTxns; ++i) @@ -852,7 +854,7 @@ public: jtx::sig(jtx::autofill)); } for (int i = 0; i < ledgersToClose; ++i) - env.close(); + BEAST_EXPECT(env.syncClose()); return newTxns; }; @@ -945,7 +947,7 @@ public: * * also test subscribe to the account before it is created */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wscTxHistory = makeWSClient(env.app().config()); Json::Value request; request[jss::account_history_tx_stream] = Json::objectValue; @@ -988,7 +990,7 @@ public: * subscribe genesis account tx history without txns * subscribe to bob's account after it is created */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wscTxHistory = makeWSClient(env.app().config()); Json::Value request; request[jss::account_history_tx_stream] = Json::objectValue; @@ -998,6 +1000,7 @@ public: if (!BEAST_EXPECT(goodSubRPC(jv))) return; IdxHashVec genesisFullHistoryVec; + BEAST_EXPECT(env.syncClose()); if (!BEAST_EXPECT(!getTxHash(*wscTxHistory, genesisFullHistoryVec, 1).first)) return; @@ -1016,6 +1019,7 @@ public: if (!BEAST_EXPECT(goodSubRPC(jv))) return; IdxHashVec bobFullHistoryVec; + BEAST_EXPECT(env.syncClose()); r = getTxHash(*wscTxHistory, bobFullHistoryVec, 1); if (!BEAST_EXPECT(r.first && r.second)) return; @@ -1050,6 +1054,7 @@ public: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; jv = wscTxHistory->invoke("subscribe", request); genesisFullHistoryVec.clear(); + BEAST_EXPECT(env.syncClose()); BEAST_EXPECT(getTxHash(*wscTxHistory, genesisFullHistoryVec, 31).second); jv = wscTxHistory->invoke("unsubscribe", request); @@ -1062,13 +1067,13 @@ public: * subscribe account and subscribe account tx history * and compare txns streamed */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto wscAccount = makeWSClient(env.app().config()); auto wscTxHistory = makeWSClient(env.app().config()); std::array accounts = {alice, bob}; env.fund(XRP(222222), accounts); - env.close(); + BEAST_EXPECT(env.syncClose()); // subscribe account Json::Value stream = Json::objectValue; @@ -1131,18 +1136,18 @@ public: * alice issues USD to carol * mix USD and XRP payments */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); auto const USD_a = alice["USD"]; std::array accounts = {alice, carol}; env.fund(XRP(333333), accounts); env.trust(USD_a(20000), carol); - env.close(); + BEAST_EXPECT(env.syncClose()); auto mixedPayments = [&]() -> int { sendPayments(env, alice, carol, 1, 0); env(pay(alice, carol, USD_a(100))); - env.close(); + BEAST_EXPECT(env.syncClose()); return 2; }; @@ -1152,6 +1157,7 @@ public: request[jss::account_history_tx_stream][jss::account] = carol.human(); auto ws = makeWSClient(env.app().config()); auto jv = ws->invoke("subscribe", request); + BEAST_EXPECT(env.syncClose()); { // take out existing txns from the stream IdxHashVec tempVec; @@ -1169,10 +1175,10 @@ public: /* * long transaction history */ - Env env(*this); + Env env(*this, single_thread_io(envconfig())); std::array accounts = {alice, carol}; env.fund(XRP(444444), accounts); - env.close(); + BEAST_EXPECT(env.syncClose()); // many payments, and close lots of ledgers auto oneRound = [&](int numPayments) { @@ -1185,6 +1191,7 @@ public: request[jss::account_history_tx_stream][jss::account] = carol.human(); auto wscLong = makeWSClient(env.app().config()); auto jv = wscLong->invoke("subscribe", request); + BEAST_EXPECT(env.syncClose()); { // take out existing txns from the stream IdxHashVec tempVec; @@ -1222,7 +1229,7 @@ public: jtx::testable_amendments() | featurePermissionedDomains | featureCredentials | featurePermissionedDEX}; - Env env(*this, all); + Env env(*this, single_thread_io(envconfig()), all); PermissionedDEX permDex(env); auto const alice = permDex.alice; auto const bob = permDex.bob; @@ -1241,10 +1248,10 @@ public: if (!BEAST_EXPECT(jv[jss::status] == "success")) return; env(offer(alice, XRP(10), USD(10)), domain(domainID), txflags(tfHybrid)); - env.close(); + BEAST_EXPECT(env.syncClose()); env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5)), domain(domainID)); - env.close(); + BEAST_EXPECT(env.syncClose()); BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { if (jv[jss::changes].size() != 1) @@ -1284,9 +1291,9 @@ public: Account const bob{"bob"}; Account const broker{"broker"}; - Env env{*this, features}; + Env env{*this, single_thread_io(envconfig()), features}; env.fund(XRP(10000), alice, bob, broker); - env.close(); + BEAST_EXPECT(env.syncClose()); auto wsc = test::makeWSClient(env.app().config()); Json::Value stream; @@ -1350,12 +1357,12 @@ public: // Verify the NFTokenIDs are correct in the NFTokenMint tx meta uint256 const nftId1{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId1); uint256 const nftId2{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId2); // Alice creates one sell offer for each NFT @@ -1363,32 +1370,32 @@ public: // meta uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId1, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex1); uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId2, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex2); // Alice cancels two offers she created // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx // meta env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2})); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenIDsInCancelOffer({nftId1, nftId2}); // Bobs creates a buy offer for nftId1 // Verify the offer id is correct in the NFTokenCreateOffer tx meta auto const bobBuyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key; env(token::createOffer(bob, nftId1, drops(1)), token::owner(alice)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(bobBuyOfferIndex); // Alice accepts bob's buy offer // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta env(token::acceptBuyOffer(alice, bobBuyOfferIndex)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId1); } @@ -1397,7 +1404,7 @@ public: // Alice mints a NFT uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId); // Alice creates sell offer and set broker as destination @@ -1405,18 +1412,18 @@ public: env(token::createOffer(alice, nftId, drops(1)), token::destination(broker), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(offerAliceToBroker); // Bob creates buy offer uint256 const offerBobToBroker = keylet::nftoffer(bob, env.seq(bob)).key; env(token::createOffer(bob, nftId, drops(1)), token::owner(alice)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(offerBobToBroker); // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode env(token::brokerOffers(broker, offerBobToBroker, offerAliceToBroker)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId); } @@ -1426,24 +1433,24 @@ public: // Alice mints a NFT uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)}; env(token::mint(alice, 0u), txflags(tfTransferable)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenID(nftId); // Alice creates 2 sell offers for the same NFT uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex1); uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken)); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex2); // Make sure the metadata only has 1 nft id, since both offers are // for the same nft env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2})); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenIDsInCancelOffer({nftId}); } @@ -1451,7 +1458,7 @@ public: { uint256 const aliceMintWithOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::mint(alice), token::amount(XRP(0))); - env.close(); + BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceMintWithOfferIndex1); } } diff --git a/src/tests/libxrpl/basics/MallocTrim.cpp b/src/tests/libxrpl/basics/MallocTrim.cpp new file mode 100644 index 0000000000..f01bd91bbf --- /dev/null +++ b/src/tests/libxrpl/basics/MallocTrim.cpp @@ -0,0 +1,209 @@ +#include + +#include + +#include + +using namespace xrpl; + +// cSpell:ignore statm + +#if defined(__GLIBC__) && BOOST_OS_LINUX +namespace xrpl::detail { +long +parseStatmRSSkB(std::string const& statm); +} // namespace xrpl::detail +#endif + +TEST(MallocTrimReport, structure) +{ + // Test default construction + MallocTrimReport report; + EXPECT_EQ(report.supported, false); + EXPECT_EQ(report.trimResult, -1); + EXPECT_EQ(report.rssBeforeKB, -1); + EXPECT_EQ(report.rssAfterKB, -1); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); + EXPECT_EQ(report.deltaKB(), 0); + + // Test deltaKB calculation - memory freed + report.rssBeforeKB = 1000; + report.rssAfterKB = 800; + EXPECT_EQ(report.deltaKB(), -200); + + // Test deltaKB calculation - memory increased + report.rssBeforeKB = 500; + report.rssAfterKB = 600; + EXPECT_EQ(report.deltaKB(), 100); + + // Test deltaKB calculation - no change + report.rssBeforeKB = 1234; + report.rssAfterKB = 1234; + EXPECT_EQ(report.deltaKB(), 0); +} + +#if defined(__GLIBC__) && BOOST_OS_LINUX +TEST(parseStatmRSSkB, standard_format) +{ + using xrpl::detail::parseStatmRSSkB; + + // Test standard format: size resident shared text lib data dt + // Assuming 4KB page size: resident=1000 pages = 4000 KB + { + std::string statm = "25365 1000 2377 0 0 5623 0"; + long result = parseStatmRSSkB(statm); + // Note: actual result depends on system page size + // On most systems it's 4KB, so 1000 pages = 4000 KB + EXPECT_GT(result, 0); + } + + // Test with newline + { + std::string statm = "12345 2000 1234 0 0 3456 0\n"; + long result = parseStatmRSSkB(statm); + EXPECT_GT(result, 0); + } + + // Test with tabs + { + std::string statm = "12345\t2000\t1234\t0\t0\t3456\t0"; + long result = parseStatmRSSkB(statm); + EXPECT_GT(result, 0); + } + + // Test zero resident pages + { + std::string statm = "25365 0 2377 0 0 5623 0"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, 0); + } + + // Test with extra whitespace + { + std::string statm = " 25365 1000 2377 "; + long result = parseStatmRSSkB(statm); + EXPECT_GT(result, 0); + } + + // Test empty string + { + std::string statm = ""; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } + + // Test malformed data (only one field) + { + std::string statm = "25365"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } + + // Test malformed data (non-numeric) + { + std::string statm = "abc def ghi"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } + + // Test malformed data (second field non-numeric) + { + std::string statm = "25365 abc 2377"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } +} +#endif + +TEST(mallocTrim, without_debug_logging) +{ + beast::Journal journal{beast::Journal::getNullSink()}; + + MallocTrimReport report = mallocTrim("without_debug", journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); +#else + EXPECT_EQ(report.supported, false); + EXPECT_EQ(report.trimResult, -1); + EXPECT_EQ(report.rssBeforeKB, -1); + EXPECT_EQ(report.rssAfterKB, -1); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); +#endif +} + +TEST(mallocTrim, empty_tag) +{ + beast::Journal journal{beast::Journal::getNullSink()}; + MallocTrimReport report = mallocTrim("", journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); +#else + EXPECT_EQ(report.supported, false); +#endif +} + +TEST(mallocTrim, with_debug_logging) +{ + struct DebugSink : public beast::Journal::Sink + { + DebugSink() : Sink(beast::severities::kDebug, false) + { + } + void + write(beast::severities::Severity, std::string const&) override + { + } + void + writeAlways(beast::severities::Severity, std::string const&) override + { + } + }; + + DebugSink sink; + beast::Journal journal{sink}; + + MallocTrimReport report = mallocTrim("debug_test", journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); + EXPECT_GE(report.durationUs.count(), 0); + EXPECT_GE(report.minfltDelta, 0); + EXPECT_GE(report.majfltDelta, 0); +#else + EXPECT_EQ(report.supported, false); + EXPECT_EQ(report.trimResult, -1); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); +#endif +} + +TEST(mallocTrim, repeated_calls) +{ + beast::Journal journal{beast::Journal::getNullSink()}; + + // Call malloc_trim multiple times to ensure it's safe + for (int i = 0; i < 5; ++i) + { + MallocTrimReport report = mallocTrim("iteration_" + std::to_string(i), journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); +#else + EXPECT_EQ(report.supported, false); +#endif + } +} diff --git a/src/tests/libxrpl/basics/scope.cpp b/src/tests/libxrpl/basics/scope.cpp index 309a41ec04..8efa4a84b1 100644 --- a/src/tests/libxrpl/basics/scope.cpp +++ b/src/tests/libxrpl/basics/scope.cpp @@ -35,7 +35,7 @@ TEST(scope, scope_exit) scope_exit x{[&i]() { i = 5; }}; throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -47,7 +47,7 @@ TEST(scope, scope_exit) x.release(); throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -85,7 +85,7 @@ TEST(scope, scope_fail) scope_fail x{[&i]() { i = 5; }}; throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -97,7 +97,7 @@ TEST(scope, scope_fail) x.release(); throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -135,7 +135,7 @@ TEST(scope, scope_success) scope_success x{[&i]() { i = 5; }}; throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } @@ -147,7 +147,7 @@ TEST(scope, scope_success) x.release(); throw 1; } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index a8ae530bde..e17437d64f 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -241,7 +241,7 @@ public: newNode->getHash().as_uint256(), std::make_shared(s.begin(), s.end())); } } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 8072b619e1..64bdf04df1 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -1637,7 +1637,7 @@ LedgerMaster::getLedgerBySeq(std::uint32_t index) if (hash) return mLedgerHistory.getLedgerByHash(*hash); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { // Missing nodes are already handled } diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp index 0fb1239c49..2191ef965a 100644 --- a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp @@ -127,7 +127,7 @@ SkipListAcquire::processData( return; } } - catch (...) + catch (...) // NOLINT(bugprone-empty-catch) { } diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 91cc387d54..3e3d87dcd5 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -1053,6 +1054,8 @@ public: << "; size after: " << cachedSLEs_.size(); } + mallocTrim("doSweep", m_journal); + // Set timer to do another sweep later. setSweepTimer(); } @@ -1069,6 +1072,12 @@ public: return trapTxID_; } + size_t + getNumberOfThreads() const override + { + return get_number_of_threads(); + } + private: // For a newly-started validator, this is the greatest persisted ledger // and new validations must be greater than this. diff --git a/src/xrpld/app/main/Application.h b/src/xrpld/app/main/Application.h index 433992bcda..0000ae010b 100644 --- a/src/xrpld/app/main/Application.h +++ b/src/xrpld/app/main/Application.h @@ -157,6 +157,10 @@ public: * than the last ledger it persisted. */ virtual LedgerIndex getMaxDisallowedLedger() = 0; + + /** Returns the number of io_context (I/O worker) threads used by the application. */ + virtual size_t + getNumberOfThreads() const = 0; }; std::unique_ptr diff --git a/src/xrpld/app/main/BasicApp.h b/src/xrpld/app/main/BasicApp.h index 278c255af3..19f07d1e5b 100644 --- a/src/xrpld/app/main/BasicApp.h +++ b/src/xrpld/app/main/BasicApp.h @@ -23,4 +23,10 @@ public: { return io_context_; } + + size_t + get_number_of_threads() const + { + return threads_.size(); + } }; diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index ced252cb71..c6b5c91e14 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -29,7 +29,7 @@ getEndpoint(std::string const& peer) if (endpoint) return beast::IP::to_asio_endpoint(endpoint.value()); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } return {}; diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp b/src/xrpld/app/misc/detail/ValidatorSite.cpp index fb68bf5ef4..c4077a1b8b 100644 --- a/src/xrpld/app/misc/detail/ValidatorSite.cpp +++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp @@ -177,7 +177,7 @@ ValidatorSite::stop() { timer_.cancel(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { } stopping_ = false; @@ -222,7 +222,7 @@ ValidatorSite::makeRequest( { timer_.cancel_one(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { } }; diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index c9361a2a5d..ac0743e936 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -252,7 +252,7 @@ ConnectAttempt::cancelTimer() timer_.cancel(); stepTimer_.cancel(); } - catch (boost::system::system_error const&) + catch (boost::system::system_error const&) // NOLINT(bugprone-empty-catch) { // ignored } diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index 7b65daa839..134cbb34f8 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -1479,7 +1479,7 @@ rpcClient( setup = setup_ServerHandler( config, beast::logstream{logs.journal("HTTPClient").warn()}); } - catch (std::exception const&) + catch (std::exception const&) // NOLINT(bugprone-empty-catch) { // ignore any exceptions, so the command // line client works without a config file