mirror of
https://github.com/XRPLF/rippled.git
synced 2026-02-28 17:52:31 +00:00
Compare commits
2 Commits
copilot/fi
...
a1q123456/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78d8f89267 | ||
|
|
34c49ff166 |
309
.clang-tidy
309
.clang-tidy
@@ -1,143 +1,105 @@
|
||||
---
|
||||
Checks: "-*,
|
||||
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-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-argument-comment
|
||||
"
|
||||
# ---
|
||||
# checks that have some issues that need to be resolved:
|
||||
#
|
||||
# bugprone-empty-catch,
|
||||
# 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-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-reserved-identifier,
|
||||
# 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-unused-local-non-trivial-variable,
|
||||
# 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-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-unused-local-non-trivial-variable,
|
||||
# bugprone-unused-raii,
|
||||
# bugprone-unused-return-value,
|
||||
# bugprone-use-after-move,
|
||||
# bugprone-unhandled-self-assignment,
|
||||
# bugprone-unused-raii,
|
||||
#
|
||||
# cppcoreguidelines-misleading-capture-default-by-value,
|
||||
# bugprone-virtual-near-miss,
|
||||
# cppcoreguidelines-init-variables,
|
||||
# cppcoreguidelines-misleading-capture-default-by-value,
|
||||
# cppcoreguidelines-no-suspend-with-lock,
|
||||
# cppcoreguidelines-pro-type-member-init,
|
||||
# cppcoreguidelines-pro-type-static-cast-downcast,
|
||||
# cppcoreguidelines-use-default-member-init,
|
||||
# cppcoreguidelines-rvalue-reference-param-not-moved,
|
||||
#
|
||||
# cppcoreguidelines-use-default-member-init,
|
||||
# cppcoreguidelines-virtual-class-destructor,
|
||||
# hicpp-ignored-remove-result,
|
||||
# llvm-namespace-comment,
|
||||
# misc-const-correctness,
|
||||
# misc-definitions-in-headers,
|
||||
# misc-header-include-cycle,
|
||||
# misc-include-cleaner,
|
||||
# misc-misplaced-const,
|
||||
# misc-redundant-expression,
|
||||
#
|
||||
# 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,
|
||||
#
|
||||
# misc-static-assert,
|
||||
# misc-throw-by-value-catch-by-reference,
|
||||
# misc-unused-alias-decls,
|
||||
# misc-unused-using-decls,
|
||||
# modernize-concat-nested-namespaces,
|
||||
# modernize-deprecated-headers,
|
||||
# modernize-make-shared,
|
||||
# modernize-make-unique,
|
||||
# modernize-pass-by-value,
|
||||
# modernize-type-traits,
|
||||
# modernize-use-designated-initializers,
|
||||
@@ -149,50 +111,79 @@ 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'
|
||||
#
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -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@56de1bdf19639e009639a50b8d17c28ca954f267
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@320be44621ca2a080f05aeb15817c44b84518108
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'
|
||||
|
||||
17
.github/workflows/reusable-build-test-config.yml
vendored
17
.github/workflows/reusable-build-test-config.yml
vendored
@@ -101,7 +101,7 @@ jobs:
|
||||
steps:
|
||||
- name: Cleanup workspace (macOS and Windows)
|
||||
if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }}
|
||||
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
|
||||
uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -229,21 +229,8 @@ jobs:
|
||||
env:
|
||||
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
|
||||
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}"
|
||||
|
||||
- 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: |
|
||||
|
||||
@@ -78,9 +78,9 @@ jobs:
|
||||
id: run_clang_tidy
|
||||
continue-on-error: true
|
||||
env:
|
||||
TARGETS: ${{ inputs.files != '' && inputs.files || 'src tests' }}
|
||||
FILES: ${{ inputs.files }}
|
||||
run: |
|
||||
run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" ${TARGETS} 2>&1 | tee clang-tidy-output.txt
|
||||
run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "$BUILD_DIR" $FILES 2>&1 | tee clang-tidy-output.txt
|
||||
|
||||
- name: Upload clang-tidy output
|
||||
if: steps.run_clang_tidy.outcome != 'success'
|
||||
|
||||
14
.github/workflows/reusable-clang-tidy.yml
vendored
14
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -22,8 +22,7 @@ jobs:
|
||||
if: ${{ inputs.check_only_changed }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
clang_tidy_config_changed: ${{ steps.changed_clang_tidy.outputs.any_changed }}
|
||||
any_cpp_changed: ${{ steps.changed_files.outputs.any_changed }}
|
||||
any_changed: ${{ steps.changed_files.outputs.any_changed }}
|
||||
all_changed_files: ${{ steps.changed_files.outputs.all_changed_files }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -39,17 +38,10 @@ 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_cpp_changed == 'true' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }}
|
||||
if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_changed == 'true') }}
|
||||
uses: ./.github/workflows/reusable-clang-tidy-files.yml
|
||||
with:
|
||||
files: ${{ (needs.determine-files.outputs.clang_tidy_config_changed == 'true' && '') || (inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '') }}
|
||||
files: ${{ inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '' }}
|
||||
create_issue_on_failure: ${{ inputs.create_issue_on_failure }}
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
steps:
|
||||
- name: Cleanup workspace (macOS and Windows)
|
||||
if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }}
|
||||
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
|
||||
uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -80,3 +80,7 @@ DerivedData
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
# Auto-generated protocol wrapper classes (generated at CMake configure time)
|
||||
/include/xrpl/protocol_autogen/transactions/
|
||||
/include/xrpl/protocol_autogen/ledger_objects/
|
||||
|
||||
@@ -61,15 +61,7 @@ repos:
|
||||
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
|
||||
' --
|
||||
entry: nix --extra-experimental-features 'nix-command flakes' fmt
|
||||
language: system
|
||||
types:
|
||||
- nix
|
||||
|
||||
@@ -75,10 +75,6 @@ This release contains bug fixes only and no API changes.
|
||||
|
||||
[Version 2.5.0](https://github.com/XRPLF/rippled/releases/tag/2.5.0) was released on Jun 24, 2025.
|
||||
|
||||
### Breaking changes in 2.5.0
|
||||
|
||||
- `feature`: In admin-mode responses, the `vetoed` field is now always a boolean. Disabled obsolete amendments now have `"vetoed": true` and a new `"obsolete": true` field, instead of the previous `"vetoed": "Obsolete"` string value. This change improves type safety for API clients while avoiding changes to non-admin responses.
|
||||
|
||||
### Additions and bugfixes in 2.5.0
|
||||
|
||||
- `tx`: Added `ctid` field to the response and improved error handling. ([#4738](https://github.com/XRPLF/rippled/pull/4738))
|
||||
|
||||
@@ -251,29 +251,6 @@ 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,
|
||||
|
||||
@@ -74,15 +74,23 @@ add_module(xrpl protocol)
|
||||
target_link_libraries(xrpl.libxrpl.protocol PUBLIC xrpl.libxrpl.crypto xrpl.libxrpl.json)
|
||||
|
||||
# Level 05
|
||||
add_module(xrpl protocol_autogen)
|
||||
target_link_libraries(xrpl.libxrpl.protocol_autogen PUBLIC xrpl.libxrpl.protocol)
|
||||
|
||||
# Set up code generation for protocol_autogen module
|
||||
include(XrplProtocolAutogen)
|
||||
setup_protocol_autogen()
|
||||
|
||||
# Level 06
|
||||
add_module(xrpl core)
|
||||
target_link_libraries(xrpl.libxrpl.core PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.json
|
||||
xrpl.libxrpl.protocol)
|
||||
|
||||
# Level 06
|
||||
# Level 07
|
||||
add_module(xrpl resource)
|
||||
target_link_libraries(xrpl.libxrpl.resource PUBLIC xrpl.libxrpl.protocol)
|
||||
|
||||
# Level 07
|
||||
# Level 08
|
||||
add_module(xrpl net)
|
||||
target_link_libraries(xrpl.libxrpl.net PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.json
|
||||
xrpl.libxrpl.protocol xrpl.libxrpl.resource)
|
||||
@@ -140,6 +148,7 @@ target_link_modules(
|
||||
net
|
||||
nodestore
|
||||
protocol
|
||||
protocol_autogen
|
||||
rdb
|
||||
resource
|
||||
server
|
||||
|
||||
@@ -29,6 +29,7 @@ install(TARGETS common
|
||||
xrpl.libxrpl.net
|
||||
xrpl.libxrpl.nodestore
|
||||
xrpl.libxrpl.protocol
|
||||
xrpl.libxrpl.protocol_autogen
|
||||
xrpl.libxrpl.resource
|
||||
xrpl.libxrpl.server
|
||||
xrpl.libxrpl.shamap
|
||||
|
||||
132
cmake/XrplProtocolAutogen.cmake
Normal file
132
cmake/XrplProtocolAutogen.cmake
Normal file
@@ -0,0 +1,132 @@
|
||||
#[===================================================================[
|
||||
Protocol Autogen - Code generation for protocol wrapper classes
|
||||
#]===================================================================]
|
||||
|
||||
# Function to set up code generation for protocol_autogen module
|
||||
# This runs at configure time to generate C++ wrapper classes from macro files
|
||||
function (setup_protocol_autogen)
|
||||
# Directory paths
|
||||
set(MACRO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/xrpl/protocol/detail")
|
||||
set(AUTOGEN_HEADER_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/xrpl/protocol_autogen")
|
||||
set(SCRIPTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/scripts")
|
||||
|
||||
# Input macro files
|
||||
set(TRANSACTIONS_MACRO "${MACRO_DIR}/transactions.macro")
|
||||
set(LEDGER_ENTRIES_MACRO "${MACRO_DIR}/ledger_entries.macro")
|
||||
set(SFIELDS_MACRO "${MACRO_DIR}/sfields.macro")
|
||||
|
||||
# Python scripts
|
||||
set(GENERATE_TX_SCRIPT "${SCRIPTS_DIR}/generate_tx_classes.py")
|
||||
set(GENERATE_LEDGER_SCRIPT "${SCRIPTS_DIR}/generate_ledger_classes.py")
|
||||
set(REQUIREMENTS_FILE "${SCRIPTS_DIR}/requirements.txt")
|
||||
|
||||
# Create output directories
|
||||
file(MAKE_DIRECTORY "${AUTOGEN_HEADER_DIR}/transactions")
|
||||
file(MAKE_DIRECTORY "${AUTOGEN_HEADER_DIR}/ledger_objects")
|
||||
|
||||
# Find Python3 - check if already found by Conan or find it ourselves
|
||||
if (NOT Python3_EXECUTABLE)
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
endif ()
|
||||
|
||||
if (NOT Python3_EXECUTABLE)
|
||||
# Try finding python3 executable directly
|
||||
find_program(Python3_EXECUTABLE NAMES python3 python)
|
||||
endif ()
|
||||
|
||||
if (NOT Python3_EXECUTABLE)
|
||||
message(FATAL_ERROR "Python3 not found. Code generation cannot proceed.")
|
||||
return()
|
||||
endif ()
|
||||
|
||||
message(STATUS "Using Python3 for code generation: ${Python3_EXECUTABLE}")
|
||||
|
||||
# Set up Python virtual environment for code generation
|
||||
set(VENV_DIR "${CMAKE_CURRENT_BINARY_DIR}/codegen_venv")
|
||||
|
||||
# Determine the Python executable path in the venv
|
||||
if (WIN32)
|
||||
set(VENV_PYTHON "${VENV_DIR}/Scripts/python.exe")
|
||||
set(VENV_PIP "${VENV_DIR}/Scripts/pip.exe")
|
||||
else ()
|
||||
set(VENV_PYTHON "${VENV_DIR}/bin/python")
|
||||
set(VENV_PIP "${VENV_DIR}/bin/pip")
|
||||
endif ()
|
||||
|
||||
# Check if venv needs to be created or updated
|
||||
set(VENV_NEEDS_UPDATE FALSE)
|
||||
if (NOT EXISTS "${VENV_PYTHON}")
|
||||
set(VENV_NEEDS_UPDATE TRUE)
|
||||
message(STATUS "Creating Python virtual environment for code generation...")
|
||||
elseif ("${REQUIREMENTS_FILE}" IS_NEWER_THAN "${VENV_DIR}/.requirements_installed")
|
||||
set(VENV_NEEDS_UPDATE TRUE)
|
||||
message(STATUS "Updating Python virtual environment (requirements changed)...")
|
||||
endif ()
|
||||
|
||||
# Create/update virtual environment if needed
|
||||
if (VENV_NEEDS_UPDATE)
|
||||
message(STATUS "Setting up Python virtual environment at ${VENV_DIR}")
|
||||
execute_process(COMMAND ${Python3_EXECUTABLE} -m venv "${VENV_DIR}"
|
||||
RESULT_VARIABLE VENV_RESULT ERROR_VARIABLE VENV_ERROR)
|
||||
if (NOT VENV_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to create virtual environment: ${VENV_ERROR}")
|
||||
endif ()
|
||||
|
||||
message(STATUS "Installing Python dependencies...")
|
||||
execute_process(COMMAND ${VENV_PIP} install --upgrade pip RESULT_VARIABLE PIP_UPGRADE_RESULT
|
||||
OUTPUT_QUIET ERROR_VARIABLE PIP_UPGRADE_ERROR)
|
||||
if (NOT PIP_UPGRADE_RESULT EQUAL 0)
|
||||
message(WARNING "Failed to upgrade pip: ${PIP_UPGRADE_ERROR}")
|
||||
endif ()
|
||||
|
||||
execute_process(COMMAND ${VENV_PIP} install -r "${REQUIREMENTS_FILE}"
|
||||
RESULT_VARIABLE PIP_INSTALL_RESULT ERROR_VARIABLE PIP_INSTALL_ERROR)
|
||||
if (NOT PIP_INSTALL_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to install Python dependencies: ${PIP_INSTALL_ERROR}")
|
||||
endif ()
|
||||
|
||||
# Mark requirements as installed
|
||||
file(TOUCH "${VENV_DIR}/.requirements_installed")
|
||||
message(STATUS "Python virtual environment ready")
|
||||
endif ()
|
||||
|
||||
# Generate transaction classes at configure time
|
||||
message(STATUS "Generating transaction classes from transactions.macro...")
|
||||
execute_process(COMMAND ${VENV_PYTHON} "${GENERATE_TX_SCRIPT}" "${TRANSACTIONS_MACRO}"
|
||||
--header-dir "${AUTOGEN_HEADER_DIR}/transactions" --sfields-macro
|
||||
"${SFIELDS_MACRO}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
RESULT_VARIABLE TX_GEN_RESULT
|
||||
OUTPUT_VARIABLE TX_GEN_OUTPUT
|
||||
ERROR_VARIABLE TX_GEN_ERROR)
|
||||
if (NOT TX_GEN_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to generate transaction classes:\n${TX_GEN_ERROR}")
|
||||
else ()
|
||||
message(STATUS "Transaction classes generated successfully")
|
||||
endif ()
|
||||
|
||||
# Generate ledger entry classes at configure time
|
||||
message(STATUS "Generating ledger entry classes from ledger_entries.macro...")
|
||||
execute_process(COMMAND ${VENV_PYTHON} "${GENERATE_LEDGER_SCRIPT}" "${LEDGER_ENTRIES_MACRO}"
|
||||
--header-dir "${AUTOGEN_HEADER_DIR}/ledger_objects" --sfields-macro
|
||||
"${SFIELDS_MACRO}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
RESULT_VARIABLE LEDGER_GEN_RESULT
|
||||
OUTPUT_VARIABLE LEDGER_GEN_OUTPUT
|
||||
ERROR_VARIABLE LEDGER_GEN_ERROR)
|
||||
if (NOT LEDGER_GEN_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to generate ledger entry classes:\n${LEDGER_GEN_ERROR}")
|
||||
else ()
|
||||
message(STATUS "Ledger entry classes generated successfully")
|
||||
endif ()
|
||||
|
||||
# Add the generated header directory to the module's include path
|
||||
target_include_directories(
|
||||
xrpl.libxrpl.protocol_autogen PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
|
||||
|
||||
# Install generated headers
|
||||
install(DIRECTORY "${AUTOGEN_HEADER_DIR}/"
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/xrpl/protocol_autogen" FILES_MATCHING
|
||||
PATTERN "*.h")
|
||||
endfunction ()
|
||||
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
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
|
||||
@@ -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 hash The hash of the object.
|
||||
@param key A pointer to the key data.
|
||||
@param pObject [out] The created object if successful.
|
||||
@return The result of the operation.
|
||||
*/
|
||||
virtual Status
|
||||
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pObject) = 0;
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) = 0;
|
||||
|
||||
/** Fetch a batch synchronously. */
|
||||
virtual std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256> const& hashes) = 0;
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) = 0;
|
||||
|
||||
/** Store a single object.
|
||||
Depending on the implementation this may happen immediately
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
|
||||
@@ -32,7 +31,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::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
|
||||
// Check flags in Credential transactions
|
||||
|
||||
@@ -449,7 +449,6 @@ JSS(node_write_retries); // out: GetCounts
|
||||
JSS(node_writes_delayed); // out::GetCounts
|
||||
JSS(nth); // out: RPC server_definitions
|
||||
JSS(obligations); // out: GatewayBalances
|
||||
JSS(obsolete); // out: AmendmentTableImpl
|
||||
JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe
|
||||
JSS(offer_id); // out: insertNFTokenOfferID
|
||||
JSS(offline); // in: TransactionSign
|
||||
|
||||
133
include/xrpl/protocol_autogen/LedgerEntryBase.h
Normal file
133
include/xrpl/protocol_autogen/LedgerEntryBase.h
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
/**
|
||||
* @brief Base class for type-safe ledger entry wrappers.
|
||||
*
|
||||
* This class provides common functionality for all ledger entry types,
|
||||
* including access to common fields (sfLedgerIndex, sfLedgerEntryType, sfFlags).
|
||||
*
|
||||
* This is an immutable wrapper around SLE (Serialized Ledger Entry).
|
||||
* Use the corresponding Builder classes to construct new ledger entries.
|
||||
*/
|
||||
class LedgerEntryBase
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a ledger entry wrapper from an existing SLE object.
|
||||
* @param sle The underlying serialized ledger entry to wrap
|
||||
*/
|
||||
explicit LedgerEntryBase(SLE const& sle) : sle_(sle)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the ledger entry type.
|
||||
* @return The type of this ledger entry
|
||||
*/
|
||||
[[nodiscard]]
|
||||
LedgerEntryType
|
||||
getType() const
|
||||
{
|
||||
return sle_.getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the key (index) of this ledger entry.
|
||||
*
|
||||
* The key uniquely identifies this ledger entry in the ledger state.
|
||||
* @return A constant reference to the 256-bit key
|
||||
*/
|
||||
[[nodiscard]]
|
||||
uint256 const&
|
||||
getKey() const
|
||||
{
|
||||
return sle_.key();
|
||||
}
|
||||
|
||||
// Common field getters (from LedgerFormats.cpp commonFields)
|
||||
|
||||
/**
|
||||
* @brief Get the ledger index (sfLedgerIndex).
|
||||
*
|
||||
* This field is OPTIONAL and represents the index of the ledger entry.
|
||||
* @return The ledger index if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint256>
|
||||
getLedgerIndex() const
|
||||
{
|
||||
if (sle_.isFieldPresent(sfLedgerIndex))
|
||||
{
|
||||
return sle_.at(sfLedgerIndex);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the ledger entry has a ledger index.
|
||||
* @return true if sfLedgerIndex is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasLedgerIndex() const
|
||||
{
|
||||
return sle_.isFieldPresent(sfLedgerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the ledger entry type field (sfLedgerEntryType).
|
||||
*
|
||||
* This field is REQUIRED for all ledger entries and indicates the type
|
||||
* of the ledger entry (e.g., AccountRoot, RippleState, Offer, etc.).
|
||||
* @return The ledger entry type as a 16-bit unsigned integer
|
||||
*/
|
||||
[[nodiscard]]
|
||||
uint16_t
|
||||
getLedgerEntryType() const
|
||||
{
|
||||
return sle_.at(sfLedgerEntryType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the flags field (sfFlags).
|
||||
*
|
||||
* This field is REQUIRED for all ledger entries and contains
|
||||
* type-specific flags that modify the behavior of the ledger entry.
|
||||
* @return The flags value as a 32-bit unsigned integer
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::uint32_t
|
||||
getFlags() const
|
||||
{
|
||||
return sle_.at(sfFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the underlying SLE object.
|
||||
*
|
||||
* Provides direct access to the wrapped serialized ledger entry object
|
||||
* for cases where the type-safe accessors are insufficient.
|
||||
* @return A constant reference to the underlying SLE object
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SLE const&
|
||||
getSle() const
|
||||
{
|
||||
return sle_;
|
||||
}
|
||||
|
||||
protected:
|
||||
/** @brief The underlying serialized ledger entry being wrapped. */
|
||||
SLE const& sle_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::ledger_entries
|
||||
75
include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h
Normal file
75
include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
/**
|
||||
* Base class for all ledger entry builders.
|
||||
* Provides common field setters that are available for all ledger entry types.
|
||||
*/
|
||||
template <typename Derived>
|
||||
class LedgerEntryBuilderBase
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Set the ledger index.
|
||||
* @param value Ledger index
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setLedgerIndex(uint256 const& value)
|
||||
{
|
||||
object_[sfLedgerIndex] = value;
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flags.
|
||||
* @param value Flags value
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setFlags(uint32_t value)
|
||||
{
|
||||
object_.setFieldU32(sfFlags, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Factory method to create a new instance of the derived builder.
|
||||
*
|
||||
* Creates a default-constructed builder instance. It is recommended to use
|
||||
* this factory method instead of directly constructing the derived type to
|
||||
* avoid creating unnecessary temporary objects.
|
||||
* @return A new instance of the derived builder type
|
||||
*/
|
||||
static Derived
|
||||
create()
|
||||
{
|
||||
return Derived{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Factory method to create an instance of the derived builder from an existing SLE.
|
||||
*
|
||||
* Creates a builder instance initialized with data from an existing serialized
|
||||
* ledger entry. It is recommended to use this factory method instead of directly
|
||||
* constructing the derived type to avoid creating unnecessary temporary objects.
|
||||
* @param sle The existing serialized ledger entry to initialize from
|
||||
* @return A new instance of the derived builder type initialized with the SLE data
|
||||
*/
|
||||
static Derived
|
||||
create(SLE const& sle)
|
||||
{
|
||||
return Derived{sle};
|
||||
}
|
||||
|
||||
protected:
|
||||
STObject object_{sfLedgerEntry};
|
||||
};
|
||||
|
||||
} // namespace xrpl::ledger_entries
|
||||
64
include/xrpl/protocol_autogen/README.md
Normal file
64
include/xrpl/protocol_autogen/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- cspell:words pyparsing -->
|
||||
|
||||
# Protocol Autogen
|
||||
|
||||
This directory contains auto-generated C++ wrapper classes for XRP Ledger protocol types.
|
||||
|
||||
## Generated Files
|
||||
|
||||
The files in this directory are automatically generated at **CMake configure time** from macro definition files:
|
||||
|
||||
- **Transaction classes** (in `transactions/`): Generated from `include/xrpl/protocol/detail/transactions.macro` by `scripts/generate_tx_classes.py`
|
||||
- **Ledger entry classes** (in `ledger_objects/`): Generated from `include/xrpl/protocol/detail/ledger_entries.macro` by `scripts/generate_ledger_classes.py`
|
||||
|
||||
## Generation Process
|
||||
|
||||
The generation happens automatically when you **configure** the project (not during build). When you run CMake, the system:
|
||||
|
||||
1. Creates a Python virtual environment in the build directory (`codegen_venv`)
|
||||
2. Installs Python dependencies from `scripts/requirements.txt` into the venv (only if needed)
|
||||
3. Runs the Python generation scripts using the venv Python interpreter
|
||||
4. Parses the macro files to extract type definitions
|
||||
5. Generates type-safe C++ wrapper classes using Jinja2 templates
|
||||
6. Places the generated headers in this directory
|
||||
|
||||
### When Regeneration Happens
|
||||
|
||||
The code is regenerated when:
|
||||
|
||||
- You run CMake configure for the first time
|
||||
- The Python virtual environment doesn't exist
|
||||
- `scripts/requirements.txt` has been modified
|
||||
|
||||
To force regeneration, delete the build directory and reconfigure.
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
The code generation requires the following Python packages (automatically installed):
|
||||
|
||||
- `pcpp` - C preprocessor for Python
|
||||
- `pyparsing` - Parser combinator library
|
||||
- `Jinja2` - Template engine
|
||||
|
||||
These are isolated in a virtual environment and won't affect your system Python installation.
|
||||
|
||||
## Version Control
|
||||
|
||||
The generated `.h` files are **not checked into version control** - they are listed in `.gitignore`.
|
||||
This means:
|
||||
|
||||
- Every developer needs Python 3 installed to configure the project
|
||||
- CI/CD systems must run CMake configure to generate the files
|
||||
- Generated files are always fresh and match the current macro definitions
|
||||
|
||||
## Modifying Generated Code
|
||||
|
||||
**Do not manually edit files in this directory.** Any changes will be overwritten the next time CMake configure runs.
|
||||
|
||||
To modify the generated classes:
|
||||
|
||||
- Edit the macro files in `include/xrpl/protocol/detail/`
|
||||
- Edit the Jinja2 templates in `scripts/templates/`
|
||||
- Edit the generation scripts in `scripts/`
|
||||
- Update Python dependencies in `scripts/requirements.txt`
|
||||
- Run CMake configure to regenerate
|
||||
442
include/xrpl/protocol_autogen/TransactionBase.h
Normal file
442
include/xrpl/protocol_autogen/TransactionBase.h
Normal file
@@ -0,0 +1,442 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
/**
|
||||
* @brief Base class for all transaction wrapper types.
|
||||
*
|
||||
* Provides type-safe read-only accessors for common transaction fields.
|
||||
* This is an immutable wrapper around STTx. Use the corresponding Builder classes
|
||||
* to construct new transactions.
|
||||
*/
|
||||
class TransactionBase
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a transaction wrapper from an existing STTx object.
|
||||
* @param tx The underlying transaction object to wrap
|
||||
*/
|
||||
explicit TransactionBase(STTx const& tx) : tx_(tx)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Validate the transaction using passesLocalChecks.
|
||||
* @param reason Output parameter for validation failure reason
|
||||
* @return true if validation passes, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
validate(std::string& reason) const
|
||||
{
|
||||
return passesLocalChecks(tx_, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the transaction type.
|
||||
* @return The type of this transaction
|
||||
*/
|
||||
[[nodiscard]]
|
||||
xrpl::TxType
|
||||
getTransactionType() const
|
||||
{
|
||||
return tx_.getTxnType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the account initiating the transaction (sfAccount).
|
||||
*
|
||||
* This field is REQUIRED for all transactions.
|
||||
* @return The account ID of the transaction sender
|
||||
*/
|
||||
[[nodiscard]]
|
||||
AccountID
|
||||
getAccount() const
|
||||
{
|
||||
return tx_.at(sfAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the sequence number of the transaction (sfSequence).
|
||||
*
|
||||
* This field is REQUIRED for all transactions.
|
||||
* @return The sequence number
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::uint32_t
|
||||
getSequence() const
|
||||
{
|
||||
return tx_.at(sfSequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the transaction fee (sfFee).
|
||||
*
|
||||
* This field is REQUIRED for all transactions.
|
||||
* @return The fee amount
|
||||
*/
|
||||
[[nodiscard]]
|
||||
STAmount
|
||||
getFee() const
|
||||
{
|
||||
return tx_.at(sfFee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the signing public key (sfSigningPubKey).
|
||||
*
|
||||
* This field is REQUIRED for all transactions.
|
||||
* @return The public key used for signing as a blob
|
||||
*/
|
||||
[[nodiscard]]
|
||||
Blob
|
||||
getSigningPubKey() const
|
||||
{
|
||||
return tx_.getFieldVL(sfSigningPubKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the transaction flags (sfFlags).
|
||||
*
|
||||
* This field is OPTIONAL.
|
||||
* @return The flags value if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint32_t>
|
||||
getFlags() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfFlags))
|
||||
return tx_.at(sfFlags);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has flags set.
|
||||
* @return true if sfFlags is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasFlags() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the source tag (sfSourceTag).
|
||||
*
|
||||
* This field is OPTIONAL and used to identify the source of a payment.
|
||||
* @return The source tag value if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint32_t>
|
||||
getSourceTag() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfSourceTag))
|
||||
return tx_.at(sfSourceTag);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has a source tag.
|
||||
* @return true if sfSourceTag is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasSourceTag() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfSourceTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the previous transaction ID (sfPreviousTxnID).
|
||||
*
|
||||
* This field is OPTIONAL and used for transaction chaining.
|
||||
* @return The previous transaction ID if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint256>
|
||||
getPreviousTxnID() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfPreviousTxnID))
|
||||
return tx_.at(sfPreviousTxnID);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has a previous transaction ID.
|
||||
* @return true if sfPreviousTxnID is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasPreviousTxnID() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfPreviousTxnID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the last ledger sequence (sfLastLedgerSequence).
|
||||
*
|
||||
* This field is OPTIONAL and specifies the latest ledger sequence
|
||||
* in which this transaction can be included.
|
||||
* @return The last ledger sequence if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint32_t>
|
||||
getLastLedgerSequence() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfLastLedgerSequence))
|
||||
return tx_.at(sfLastLedgerSequence);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has a last ledger sequence.
|
||||
* @return true if sfLastLedgerSequence is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasLastLedgerSequence() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfLastLedgerSequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the account transaction ID (sfAccountTxnID).
|
||||
*
|
||||
* This field is OPTIONAL and used to track transaction sequences.
|
||||
* @return The account transaction ID if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint256>
|
||||
getAccountTxnID() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfAccountTxnID))
|
||||
return tx_.at(sfAccountTxnID);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has an account transaction ID.
|
||||
* @return true if sfAccountTxnID is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAccountTxnID() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfAccountTxnID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the operation limit (sfOperationLimit).
|
||||
*
|
||||
* This field is OPTIONAL and limits the number of operations in a transaction.
|
||||
* @return The operation limit if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint32_t>
|
||||
getOperationLimit() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfOperationLimit))
|
||||
return tx_.at(sfOperationLimit);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has an operation limit.
|
||||
* @return true if sfOperationLimit is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasOperationLimit() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfOperationLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the memos array (sfMemos).
|
||||
*
|
||||
* This field is OPTIONAL and contains arbitrary data attached to the transaction.
|
||||
* @note This is an untyped field (STArray).
|
||||
* @return A reference wrapper to the memos array if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<std::reference_wrapper<STArray const>>
|
||||
getMemos() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfMemos))
|
||||
return tx_.getFieldArray(sfMemos);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has memos.
|
||||
* @return true if sfMemos is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasMemos() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfMemos);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the ticket sequence (sfTicketSequence).
|
||||
*
|
||||
* This field is OPTIONAL and used when consuming a ticket instead of a sequence number.
|
||||
* @return The ticket sequence if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint32_t>
|
||||
getTicketSequence() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfTicketSequence))
|
||||
return tx_.at(sfTicketSequence);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has a ticket sequence.
|
||||
* @return true if sfTicketSequence is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasTicketSequence() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfTicketSequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the transaction signature (sfTxnSignature).
|
||||
*
|
||||
* This field is OPTIONAL and contains the signature for single-signed transactions.
|
||||
* @return The transaction signature as a blob if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<Blob>
|
||||
getTxnSignature() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfTxnSignature))
|
||||
return tx_.getFieldVL(sfTxnSignature);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has a transaction signature.
|
||||
* @return true if sfTxnSignature is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasTxnSignature() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfTxnSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the signers array (sfSigners).
|
||||
*
|
||||
* This field is OPTIONAL and contains the list of signers for multi-signed transactions.
|
||||
* @note This is an untyped field (STArray).
|
||||
* @return A reference wrapper to the signers array if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<std::reference_wrapper<STArray const>>
|
||||
getSigners() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfSigners))
|
||||
return tx_.getFieldArray(sfSigners);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has signers.
|
||||
* @return true if sfSigners is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasSigners() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfSigners);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the network ID (sfNetworkID).
|
||||
*
|
||||
* This field is OPTIONAL and identifies the network this transaction is intended for.
|
||||
* @return The network ID if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<uint32_t>
|
||||
getNetworkID() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfNetworkID))
|
||||
return tx_.at(sfNetworkID);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has a network ID.
|
||||
* @return true if sfNetworkID is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasNetworkID() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfNetworkID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the delegate account (sfDelegate).
|
||||
*
|
||||
* This field is OPTIONAL and specifies a delegate account for the transaction.
|
||||
* @return The delegate account ID if present, std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::optional<AccountID>
|
||||
getDelegate() const
|
||||
{
|
||||
if (tx_.isFieldPresent(sfDelegate))
|
||||
return tx_.at(sfDelegate);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the transaction has a delegate account.
|
||||
* @return true if sfDelegate is present, false otherwise
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasDelegate() const
|
||||
{
|
||||
return tx_.isFieldPresent(sfDelegate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the underlying STTx object.
|
||||
*
|
||||
* Provides direct access to the wrapped transaction object for cases
|
||||
* where the type-safe accessors are insufficient.
|
||||
* @return A constant reference to the underlying STTx object
|
||||
*/
|
||||
[[nodiscard]]
|
||||
STTx const&
|
||||
getSTTx() const
|
||||
{
|
||||
return tx_;
|
||||
}
|
||||
|
||||
protected:
|
||||
/** @brief The underlying transaction object being wrapped. */
|
||||
STTx const& tx_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
128
include/xrpl/protocol_autogen/TransactionBuilderBase.h
Normal file
128
include/xrpl/protocol_autogen/TransactionBuilderBase.h
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
/**
|
||||
* Base class for all transaction builders.
|
||||
* Provides common field setters that are available for all transaction types.
|
||||
*/
|
||||
template <typename Derived>
|
||||
class TransactionBuilderBase
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Set the account that is sending the transaction.
|
||||
* @param value Account address (typically as a string)
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setAccount(AccountID const& value)
|
||||
{
|
||||
set(object_, sfAccount, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the transaction fee.
|
||||
* @param value Fee in drops (typically as a string or number)
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setFee(STAmount const& value)
|
||||
{
|
||||
set(object_, sfFee, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sequence number.
|
||||
* @param value Sequence number
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setSequence(std::uint32_t const& value)
|
||||
{
|
||||
set(object_, sfSequence, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the signing public key.
|
||||
* @param value Public key (typically as a hex string)
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setSigningPubKey(Blob const& value)
|
||||
{
|
||||
set(object_, sfSigningPubKey, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set transaction flags.
|
||||
* @param value Flags value
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setFlags(std::uint32_t const& value)
|
||||
{
|
||||
set(object_, sfFlags, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source tag.
|
||||
* @param value Source tag
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setSourceTag(std::uint32_t const& value)
|
||||
{
|
||||
set(object_, sfSourceTag, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last ledger sequence.
|
||||
* @param value Last ledger sequence number
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setLastLedgerSequence(std::uint32_t const& value)
|
||||
{
|
||||
set(object_, sfLastLedgerSequence, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the account transaction ID.
|
||||
* @param value Account transaction ID (typically as a hex string)
|
||||
* @return Reference to the derived builder for method chaining.
|
||||
*/
|
||||
Derived&
|
||||
setAccountTxnID(STUInt256 const& value)
|
||||
{
|
||||
set(object_, sfAccountTxnID, value);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Factory method to create an instance of the derived builder.
|
||||
*
|
||||
* @return A new instance of the derived builder type
|
||||
*/
|
||||
static Derived
|
||||
create()
|
||||
{
|
||||
return Derived{};
|
||||
}
|
||||
|
||||
protected:
|
||||
STObject object_{sfTransaction};
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
732
include/xrpl/tx/InvariantCheck.h
Normal file
732
include/xrpl/tx/InvariantCheck.h
Normal file
@@ -0,0 +1,732 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
|
||||
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<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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, after>. 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::pair<std::shared_ptr<SLE const>, std::shared_ptr<SLE const>>> accountsDeleted_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const line;
|
||||
int const balanceChangeSign;
|
||||
};
|
||||
|
||||
struct IssuerChanges
|
||||
{
|
||||
std::vector<BalanceChange> senders;
|
||||
std::vector<BalanceChange> receivers;
|
||||
};
|
||||
|
||||
using ByIssuer = std::map<Issue, IssuerChanges>;
|
||||
ByIssuer balanceChanges_;
|
||||
|
||||
std::map<AccountID, std::shared_ptr<SLE const> const> possibleIssuers_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
|
||||
private:
|
||||
bool
|
||||
isValidEntry(std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
|
||||
|
||||
STAmount
|
||||
calculateBalanceChange(
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after,
|
||||
bool isDelete);
|
||||
|
||||
void
|
||||
recordBalance(Issue const& issue, BalanceChange change);
|
||||
|
||||
void
|
||||
recordBalanceChanges(std::shared_ptr<SLE const> const& after, STAmount const& balanceChange);
|
||||
|
||||
std::shared_ptr<SLE const>
|
||||
findIssuer(AccountID const& issuerID, ReadView const& view);
|
||||
|
||||
bool
|
||||
validateIssuerChanges(
|
||||
std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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> sleStatus_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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<std::string> errors_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
class ValidPermissionedDEX
|
||||
{
|
||||
bool regularOffers_ = false;
|
||||
bool badHybrids_ = false;
|
||||
hash_set<uint256> domains_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
class ValidAMM
|
||||
{
|
||||
std::optional<AccountID> ammAccount_;
|
||||
std::optional<STAmount> lptAMMBalanceAfter_;
|
||||
std::optional<STAmount> lptAMMBalanceBefore_;
|
||||
bool ammPoolChanged_;
|
||||
|
||||
public:
|
||||
enum class ZeroAllowed : bool { No = false, Yes = true };
|
||||
|
||||
ValidAMM() : ammPoolChanged_{false}
|
||||
{
|
||||
}
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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 <before, after>.
|
||||
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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<uint256, BrokerInfo> brokers_;
|
||||
// Collect all the modified trust lines. Their high and low accounts will be
|
||||
// loaded to look for LoanBroker pseudo-accounts.
|
||||
std::vector<SLE::const_pointer> lines_;
|
||||
// Collect all the modified MPTokens. Their accounts will be loaded to look
|
||||
// for LoanBroker pseudo-accounts.
|
||||
std::vector<SLE::const_pointer> mpts_;
|
||||
|
||||
bool
|
||||
goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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 <before, after>. After is used for most of the checks, except
|
||||
// those that check changed values.
|
||||
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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&);
|
||||
};
|
||||
|
||||
std::vector<Vault> afterVault_ = {};
|
||||
std::vector<Shares> afterMPTs_ = {};
|
||||
std::vector<Vault> beforeVault_ = {};
|
||||
std::vector<Shares> beforeMPTs_ = {};
|
||||
std::unordered_map<uint256, Number> deltas_ = {};
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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
|
||||
@@ -1,53 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ValidAMM
|
||||
{
|
||||
std::optional<AccountID> ammAccount_;
|
||||
std::optional<STAmount> lptAMMBalanceAfter_;
|
||||
std::optional<STAmount> lptAMMBalanceBefore_;
|
||||
bool ammPoolChanged_;
|
||||
|
||||
public:
|
||||
enum class ZeroAllowed : bool { No = false, Yes = true };
|
||||
|
||||
ValidAMM() : ammPoolChanged_{false}
|
||||
{
|
||||
}
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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
|
||||
@@ -1,84 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
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<SLE const> const line;
|
||||
int const balanceChangeSign;
|
||||
};
|
||||
|
||||
struct IssuerChanges
|
||||
{
|
||||
std::vector<BalanceChange> senders;
|
||||
std::vector<BalanceChange> receivers;
|
||||
};
|
||||
|
||||
using ByIssuer = std::map<Issue, IssuerChanges>;
|
||||
ByIssuer balanceChanges_;
|
||||
|
||||
std::map<AccountID, std::shared_ptr<SLE const> const> possibleIssuers_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
|
||||
private:
|
||||
bool
|
||||
isValidEntry(std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
|
||||
|
||||
STAmount
|
||||
calculateBalanceChange(
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after,
|
||||
bool isDelete);
|
||||
|
||||
void
|
||||
recordBalance(Issue const& issue, BalanceChange change);
|
||||
|
||||
void
|
||||
recordBalanceChanges(std::shared_ptr<SLE const> const& after, STAmount const& balanceChange);
|
||||
|
||||
std::shared_ptr<SLE const>
|
||||
findIssuer(AccountID const& issuerID, ReadView const& view);
|
||||
|
||||
bool
|
||||
validateIssuerChanges(
|
||||
std::shared_ptr<SLE const> 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
|
||||
@@ -1,385 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
#include <xrpl/tx/invariants/FreezeInvariant.h>
|
||||
#include <xrpl/tx/invariants/LoanInvariant.h>
|
||||
#include <xrpl/tx/invariants/MPTInvariant.h>
|
||||
#include <xrpl/tx/invariants/NFTInvariant.h>
|
||||
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
|
||||
#include <xrpl/tx/invariants/PermissionedDomainInvariant.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariant.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
|
||||
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<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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, after>. 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::pair<std::shared_ptr<SLE const>, std::shared_ptr<SLE const>>> accountsDeleted_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> 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<std::string> errors_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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 <before, after>.
|
||||
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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
|
||||
@@ -1,60 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
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<Privilege>(
|
||||
safe_cast<std::underlying_type_t<Privilege>>(lhs) |
|
||||
safe_cast<std::underlying_type_t<Privilege>>(rhs));
|
||||
}
|
||||
|
||||
bool
|
||||
hasPrivilege(STTx const& tx, Privilege priv);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,75 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
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<uint256, BrokerInfo> brokers_;
|
||||
// Collect all the modified trust lines. Their high and low accounts will be
|
||||
// loaded to look for LoanBroker pseudo-accounts.
|
||||
std::vector<SLE::const_pointer> lines_;
|
||||
// Collect all the modified MPTokens. Their accounts will be loaded to look
|
||||
// for LoanBroker pseudo-accounts.
|
||||
std::vector<SLE::const_pointer> mpts_;
|
||||
|
||||
bool
|
||||
goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> 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 <before, after>. After is used for most of the checks, except
|
||||
// those that check changed values.
|
||||
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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<SLE const> const&, std::shared_ptr<SLE const> 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<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ValidPermissionedDEX
|
||||
{
|
||||
bool regularOffers_ = false;
|
||||
bool badHybrids_ = false;
|
||||
hash_set<uint256> domains_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
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> sleStatus_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,77 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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&);
|
||||
};
|
||||
|
||||
std::vector<Vault> afterVault_ = {};
|
||||
std::vector<Shares> afterMPTs_ = {};
|
||||
std::vector<Vault> beforeVault_ = {};
|
||||
std::vector<Shares> beforeMPTs_ = {};
|
||||
std::unordered_map<uint256, Number> deltas_ = {};
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
216
scripts/generate_ledger_classes.py
Normal file
216
scripts/generate_ledger_classes.py
Normal file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate C++ wrapper classes for XRP Ledger entry types from ledger_entries.macro.
|
||||
|
||||
This script parses the ledger_entries.macro file and generates type-safe wrapper
|
||||
classes for each ledger entry type, similar to the transaction wrapper classes.
|
||||
|
||||
Uses pcpp to preprocess the macro file and pyparsing to parse the DSL.
|
||||
"""
|
||||
|
||||
# cspell:words sfields
|
||||
|
||||
import io
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import pyparsing as pp
|
||||
|
||||
# Import common utilities
|
||||
from macro_parser_common import CppCleaner, parse_sfields_macro, parse_field_list
|
||||
|
||||
|
||||
def create_ledger_entry_parser():
|
||||
"""Create a pyparsing parser for LEDGER_ENTRY macros.
|
||||
|
||||
This parser extracts the full LEDGER_ENTRY macro call and parses its arguments
|
||||
using pyparsing's nesting-aware delimited list parsing.
|
||||
"""
|
||||
# Match the exact words
|
||||
ledger_entry = pp.Keyword("LEDGER_ENTRY") | pp.Keyword("LEDGER_ENTRY_DUPLICATE")
|
||||
|
||||
# Define nested structures so pyparsing protects them
|
||||
nested_braces = pp.original_text_for(pp.nested_expr("{", "}"))
|
||||
nested_parens = pp.original_text_for(pp.nested_expr("(", ")"))
|
||||
|
||||
# Define standard text (anything that isn't a comma, parens, or braces)
|
||||
plain_text = pp.Word(pp.printables + " \t\n", exclude_chars=",{}()")
|
||||
|
||||
# A single argument is any combination of the above
|
||||
single_arg = pp.Combine(pp.OneOrMore(nested_braces | nested_parens | plain_text))
|
||||
single_arg.set_parse_action(lambda t: t[0].strip())
|
||||
|
||||
# The arguments are a delimited list
|
||||
args_list = pp.DelimitedList(single_arg)
|
||||
|
||||
# The full macro: LEDGER_ENTRY(args) or LEDGER_ENTRY_DUPLICATE(args)
|
||||
macro_parser = (
|
||||
ledger_entry + pp.Suppress("(") + pp.Group(args_list)("args") + pp.Suppress(")")
|
||||
)
|
||||
|
||||
return macro_parser
|
||||
|
||||
|
||||
def parse_ledger_entry_args(args_list):
|
||||
"""Parse the arguments of a LEDGER_ENTRY macro call.
|
||||
|
||||
Args:
|
||||
args_list: A list of parsed arguments from pyparsing, e.g.,
|
||||
['ltACCOUNT_ROOT', '0x0061', 'AccountRoot', 'account', '({...})']
|
||||
|
||||
Returns:
|
||||
A dict with parsed ledger entry information.
|
||||
"""
|
||||
if len(args_list) < 5:
|
||||
raise ValueError(
|
||||
f"Expected at least 5 parts in LEDGER_ENTRY, got {len(args_list)}: {args_list}"
|
||||
)
|
||||
|
||||
tag = args_list[0]
|
||||
value = args_list[1]
|
||||
name = args_list[2]
|
||||
rpc_name = args_list[3]
|
||||
fields_str = args_list[4]
|
||||
|
||||
# Parse fields: ({field1, field2, ...})
|
||||
fields = parse_field_list(fields_str)
|
||||
|
||||
return {
|
||||
"tag": tag,
|
||||
"value": value,
|
||||
"name": name,
|
||||
"rpc_name": rpc_name,
|
||||
"fields": fields,
|
||||
}
|
||||
|
||||
|
||||
def parse_macro_file(file_path):
|
||||
"""Parse the ledger_entries.macro file and return a list of ledger entry definitions.
|
||||
|
||||
Uses pcpp to preprocess the file and pyparsing to parse the LEDGER_ENTRY macros.
|
||||
"""
|
||||
with open(file_path, "r") as f:
|
||||
c_code = f.read()
|
||||
|
||||
# Step 1: Clean the C++ code using pcpp
|
||||
cleaner = CppCleaner("LEDGER_ENTRY_INCLUDE")
|
||||
cleaner.parse(c_code)
|
||||
|
||||
out = io.StringIO()
|
||||
cleaner.write(out)
|
||||
clean_text = out.getvalue()
|
||||
|
||||
# Step 2: Parse the clean text using pyparsing
|
||||
parser = create_ledger_entry_parser()
|
||||
entries = []
|
||||
|
||||
for match, _, _ in parser.scan_string(clean_text):
|
||||
# Extract the macro name and arguments
|
||||
raw_args = match.args
|
||||
|
||||
# Parse the arguments
|
||||
entry_data = parse_ledger_entry_args(raw_args)
|
||||
entries.append(entry_data)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def generate_cpp_class(entry_info, header_dir, jinja_env, field_types):
|
||||
"""Generate C++ header file for a ledger entry type."""
|
||||
# Enrich field information with type data
|
||||
for field in entry_info["fields"]:
|
||||
field_name = field["name"]
|
||||
if field_name in field_types:
|
||||
field["typed"] = field_types[field_name]["typed"]
|
||||
field["stiSuffix"] = field_types[field_name]["stiSuffix"]
|
||||
field["typeData"] = field_types[field_name]["typeData"]
|
||||
else:
|
||||
# Unknown field - assume typed for safety
|
||||
field["typed"] = True
|
||||
field["stiSuffix"] = None
|
||||
field["typeData"] = None
|
||||
|
||||
template = jinja_env.get_template("LedgerEntry.h.jinja2")
|
||||
|
||||
# Render the template
|
||||
header_content = template.render(
|
||||
name=entry_info["name"],
|
||||
tag=entry_info["tag"],
|
||||
value=entry_info["value"],
|
||||
rpc_name=entry_info["rpc_name"],
|
||||
fields=entry_info["fields"],
|
||||
)
|
||||
|
||||
# Write header file
|
||||
header_path = Path(header_dir) / f"{entry_info['name']}.h"
|
||||
with open(header_path, "w") as f:
|
||||
f.write(header_content)
|
||||
|
||||
print(f"Generated {header_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate C++ ledger entry classes from ledger_entries.macro"
|
||||
)
|
||||
parser.add_argument("macro_path", help="Path to ledger_entries.macro")
|
||||
parser.add_argument(
|
||||
"--header-dir",
|
||||
help="Output directory for header files",
|
||||
default="include/xrpl/protocol/ledger_objects",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sfields-macro",
|
||||
help="Path to sfields.macro (default: auto-detect from macro_path)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Auto-detect sfields.macro path if not provided
|
||||
if args.sfields_macro:
|
||||
sfields_path = Path(args.sfields_macro)
|
||||
else:
|
||||
# Assume sfields.macro is in the same directory as ledger_entries.macro
|
||||
macro_path = Path(args.macro_path)
|
||||
sfields_path = macro_path.parent / "sfields.macro"
|
||||
|
||||
# Parse sfields.macro to get field type information
|
||||
print(f"Parsing {sfields_path}...")
|
||||
field_types = parse_sfields_macro(sfields_path)
|
||||
print(
|
||||
f"Found {len(field_types)} field definitions ({sum(1 for f in field_types.values() if f['typed'])} typed, {sum(1 for f in field_types.values() if not f['typed'])} untyped)\n"
|
||||
)
|
||||
|
||||
# Parse the file
|
||||
entries = parse_macro_file(args.macro_path)
|
||||
|
||||
print(f"Found {len(entries)} ledger entries\n")
|
||||
|
||||
for entry in entries:
|
||||
print(f"Ledger Entry: {entry['name']}")
|
||||
print(f" Tag: {entry['tag']}")
|
||||
print(f" Value: {entry['value']}")
|
||||
print(f" RPC Name: {entry['rpc_name']}")
|
||||
print(f" Fields: {len(entry['fields'])}")
|
||||
for field in entry["fields"]:
|
||||
mpt_info = f" ({field['mpt_support']})" if "mpt_support" in field else ""
|
||||
print(f" - {field['name']}: {field['requirement']}{mpt_info}")
|
||||
print()
|
||||
|
||||
# Set up Jinja2 environment
|
||||
script_dir = Path(__file__).parent
|
||||
template_dir = script_dir / "templates"
|
||||
jinja_env = Environment(loader=FileSystemLoader(str(template_dir)))
|
||||
|
||||
# Generate C++ classes
|
||||
header_dir = Path(args.header_dir)
|
||||
header_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for entry in entries:
|
||||
generate_cpp_class(entry, header_dir, jinja_env, field_types)
|
||||
|
||||
print(f"\nGenerated {len(entries)} ledger entry classes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
226
scripts/generate_tx_classes.py
Normal file
226
scripts/generate_tx_classes.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parse transactions.macro file to extract transaction information
|
||||
and generate C++ classes for each transaction type.
|
||||
|
||||
Uses pcpp to preprocess the macro file and pyparsing to parse the DSL.
|
||||
"""
|
||||
# cspell:words sfields
|
||||
|
||||
import io
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import pyparsing as pp
|
||||
|
||||
# Import common utilities
|
||||
from macro_parser_common import CppCleaner, parse_sfields_macro, parse_field_list
|
||||
|
||||
|
||||
def create_transaction_parser():
|
||||
"""Create a pyparsing parser for TRANSACTION macros.
|
||||
|
||||
This parser extracts the full TRANSACTION macro call and parses its arguments
|
||||
using pyparsing's nesting-aware delimited list parsing.
|
||||
"""
|
||||
# Define nested structures so pyparsing protects them
|
||||
nested_braces = pp.original_text_for(pp.nested_expr("{", "}"))
|
||||
nested_parens = pp.original_text_for(pp.nested_expr("(", ")"))
|
||||
|
||||
# Define standard text (anything that isn't a comma, parens, or braces)
|
||||
plain_text = pp.Word(pp.printables + " \t\n", exclude_chars=",{}()")
|
||||
|
||||
# A single argument is any combination of the above
|
||||
single_arg = pp.Combine(pp.OneOrMore(nested_braces | nested_parens | plain_text))
|
||||
single_arg.set_parse_action(lambda t: t[0].strip())
|
||||
|
||||
# The arguments are a delimited list
|
||||
args_list = pp.DelimitedList(single_arg)
|
||||
|
||||
# The full macro: TRANSACTION(args)
|
||||
macro_parser = (
|
||||
pp.Keyword("TRANSACTION")
|
||||
+ pp.Suppress("(")
|
||||
+ pp.Group(args_list)("args")
|
||||
+ pp.Suppress(")")
|
||||
)
|
||||
|
||||
return macro_parser
|
||||
|
||||
|
||||
def parse_transaction_args(args_list):
|
||||
"""Parse the arguments of a TRANSACTION macro call.
|
||||
|
||||
Args:
|
||||
args_list: A list of parsed arguments from pyparsing, e.g.,
|
||||
['ttPAYMENT', '0', 'Payment', 'Delegation::delegable',
|
||||
'uint256{}', 'createAcct', '({...})']
|
||||
|
||||
Returns:
|
||||
A dict with parsed transaction information.
|
||||
"""
|
||||
if len(args_list) < 7:
|
||||
raise ValueError(
|
||||
f"Expected at least 7 parts in TRANSACTION, got {len(args_list)}: {args_list}"
|
||||
)
|
||||
|
||||
tag = args_list[0]
|
||||
value = args_list[1]
|
||||
name = args_list[2]
|
||||
delegable = args_list[3]
|
||||
amendments = args_list[4]
|
||||
privileges = args_list[5]
|
||||
fields_str = args_list[6]
|
||||
|
||||
# Parse fields: ({field1, field2, ...})
|
||||
fields = parse_field_list(fields_str)
|
||||
|
||||
return {
|
||||
"tag": tag,
|
||||
"value": value,
|
||||
"name": name,
|
||||
"delegable": delegable,
|
||||
"amendments": amendments,
|
||||
"privileges": privileges,
|
||||
"fields": fields,
|
||||
}
|
||||
|
||||
|
||||
def parse_macro_file(filepath):
|
||||
"""Parse the transactions.macro file.
|
||||
|
||||
Uses pcpp to preprocess the file and pyparsing to parse the TRANSACTION macros.
|
||||
"""
|
||||
with open(filepath, "r") as f:
|
||||
c_code = f.read()
|
||||
|
||||
# Step 1: Clean the C++ code using pcpp
|
||||
cleaner = CppCleaner("TRANSACTION_INCLUDE")
|
||||
cleaner.parse(c_code)
|
||||
|
||||
out = io.StringIO()
|
||||
cleaner.write(out)
|
||||
clean_text = out.getvalue()
|
||||
|
||||
# Step 2: Parse the clean text using pyparsing
|
||||
parser = create_transaction_parser()
|
||||
transactions = []
|
||||
|
||||
for match, _, _ in parser.scan_string(clean_text):
|
||||
# Extract the macro name and arguments
|
||||
raw_args = match.args
|
||||
|
||||
# Parse the arguments
|
||||
tx_data = parse_transaction_args(raw_args)
|
||||
transactions.append(tx_data)
|
||||
|
||||
return transactions
|
||||
|
||||
|
||||
def generate_cpp_class(tx_info, header_dir, jinja_env, field_types):
|
||||
"""Generate a header-only template class for a transaction using Jinja2 templates."""
|
||||
class_name = tx_info["name"]
|
||||
|
||||
# Enrich field information with type data
|
||||
for field in tx_info["fields"]:
|
||||
field_name = field["name"]
|
||||
if field_name in field_types:
|
||||
field["typed"] = field_types[field_name]["typed"]
|
||||
field["stiSuffix"] = field_types[field_name]["stiSuffix"]
|
||||
field["typeData"] = field_types[field_name]["typeData"]
|
||||
else:
|
||||
# Unknown field - assume typed for safety
|
||||
field["typed"] = True
|
||||
field["stiSuffix"] = None
|
||||
field["typeData"] = None
|
||||
|
||||
# Load template
|
||||
header_template = jinja_env.get_template("Transaction.h.jinja2")
|
||||
|
||||
# Render header
|
||||
header_content = header_template.render(tx_info)
|
||||
header_path = header_dir / f"{class_name}.h"
|
||||
with open(header_path, "w") as f:
|
||||
f.write(header_content)
|
||||
|
||||
return header_path
|
||||
|
||||
|
||||
# TransactionBase is a static file in the repository at:
|
||||
# - include/xrpl/protocol/TransactionBase.h
|
||||
# - src/libxrpl/protocol/TransactionBase.cpp
|
||||
# It is NOT generated by this script.
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate C++ transaction classes from transactions.macro"
|
||||
)
|
||||
parser.add_argument("macro_path", help="Path to transactions.macro")
|
||||
parser.add_argument(
|
||||
"--header-dir",
|
||||
help="Output directory for header files",
|
||||
default="include/xrpl/protocol/transactions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sfields-macro",
|
||||
help="Path to sfields.macro (default: auto-detect from macro_path)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Auto-detect sfields.macro path if not provided
|
||||
if args.sfields_macro:
|
||||
sfields_path = Path(args.sfields_macro)
|
||||
else:
|
||||
# Assume sfields.macro is in the same directory as transactions.macro
|
||||
macro_path = Path(args.macro_path)
|
||||
sfields_path = macro_path.parent / "sfields.macro"
|
||||
|
||||
# Parse sfields.macro to get field type information
|
||||
print(f"Parsing {sfields_path}...")
|
||||
field_types = parse_sfields_macro(sfields_path)
|
||||
print(
|
||||
f"Found {len(field_types)} field definitions ({sum(1 for f in field_types.values() if f['typed'])} typed, {sum(1 for f in field_types.values() if not f['typed'])} untyped)\n"
|
||||
)
|
||||
|
||||
# Parse the file
|
||||
transactions = parse_macro_file(args.macro_path)
|
||||
|
||||
print(f"Found {len(transactions)} transactions\n")
|
||||
|
||||
for tx in transactions:
|
||||
print(f"Transaction: {tx['name']}")
|
||||
print(f" Tag: {tx['tag']}")
|
||||
print(f" Value: {tx['value']}")
|
||||
print(f" Fields: {len(tx['fields'])}")
|
||||
for field in tx["fields"]:
|
||||
print(f" - {field['name']}: {field['requirement']}")
|
||||
print()
|
||||
|
||||
# Set up output directory
|
||||
header_dir = Path(args.header_dir)
|
||||
header_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"\nGenerating header-only template classes...")
|
||||
print(f" Headers: {header_dir}\n")
|
||||
|
||||
# Set up Jinja2 environment
|
||||
script_dir = Path(__file__).parent
|
||||
template_dir = script_dir / "templates"
|
||||
jinja_env = Environment(loader=FileSystemLoader(template_dir))
|
||||
|
||||
generated_files = []
|
||||
for tx_info in transactions:
|
||||
header_path = generate_cpp_class(tx_info, header_dir, jinja_env, field_types)
|
||||
generated_files.append(header_path)
|
||||
print(f" Generated: {tx_info['name']}.h")
|
||||
|
||||
print(
|
||||
f"\n✓ Successfully generated {len(transactions)} transaction classes ({len(generated_files)} header files)"
|
||||
)
|
||||
print(f" Headers: {header_dir.absolute()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
193
scripts/macro_parser_common.py
Normal file
193
scripts/macro_parser_common.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Common utilities for parsing XRP Ledger macro files.
|
||||
|
||||
This module provides shared functionality for parsing transactions.macro
|
||||
and ledger_entries.macro files using pcpp and pyparsing.
|
||||
"""
|
||||
# cspell:words sfields
|
||||
|
||||
import re
|
||||
import pyparsing as pp
|
||||
from pcpp import Preprocessor
|
||||
|
||||
|
||||
class CppCleaner(Preprocessor):
|
||||
"""C preprocessor that removes C++ noise while preserving macro calls."""
|
||||
|
||||
def __init__(self, macro_include_name):
|
||||
"""
|
||||
Initialize the preprocessor.
|
||||
|
||||
Args:
|
||||
macro_include_name: The name of the include flag to set to 0
|
||||
(e.g., "TRANSACTION_INCLUDE" or "LEDGER_ENTRY_INCLUDE")
|
||||
"""
|
||||
super(CppCleaner, self).__init__()
|
||||
# Define flags so #if blocks evaluate correctly
|
||||
# We set the include flag to 0 so includes are skipped
|
||||
self.define(f"{macro_include_name} 0")
|
||||
# Suppress line directives
|
||||
self.line_directive = None
|
||||
|
||||
def on_error(self, file, line, msg):
|
||||
# Ignore #error directives
|
||||
pass
|
||||
|
||||
def on_include_not_found(
|
||||
self, is_malformed, is_system_include, curdir, includepath
|
||||
):
|
||||
# Ignore missing headers
|
||||
pass
|
||||
|
||||
|
||||
def parse_sfields_macro(sfields_path):
|
||||
"""
|
||||
Parse sfields.macro to determine which fields are typed vs untyped.
|
||||
|
||||
Returns a dict mapping field names to their type information:
|
||||
{
|
||||
'sfMemos': {'typed': False, 'stiSuffix': 'ARRAY', 'typeData': {...}},
|
||||
'sfAmount': {'typed': True, 'stiSuffix': 'AMOUNT', 'typeData': {...}},
|
||||
...
|
||||
}
|
||||
"""
|
||||
# Mapping from STI suffix to C++ type for untyped fields
|
||||
UNTYPED_TYPE_MAP = {
|
||||
"ARRAY": {
|
||||
"getter_method": "getFieldArray",
|
||||
"setter_method": "setFieldArray",
|
||||
"setter_use_brackets": False,
|
||||
"setter_type": "STArray const&",
|
||||
"return_type": "STArray const&",
|
||||
"return_type_optional": "std::optional<std::reference_wrapper<STArray const>>",
|
||||
},
|
||||
"OBJECT": {
|
||||
"getter_method": "getFieldObject",
|
||||
"setter_method": "setFieldObject",
|
||||
"setter_use_brackets": False,
|
||||
"setter_type": "STObject const&",
|
||||
"return_type": "STObject",
|
||||
"return_type_optional": "std::optional<STObject>",
|
||||
},
|
||||
"PATHSET": {
|
||||
"getter_method": "getFieldPathSet",
|
||||
"setter_method": "setFieldPathSet",
|
||||
"setter_use_brackets": False,
|
||||
"setter_type": "STPathSet const&",
|
||||
"return_type": "STPathSet const&",
|
||||
"return_type_optional": "std::optional<std::reference_wrapper<STPathSet const>>",
|
||||
},
|
||||
}
|
||||
|
||||
field_info = {}
|
||||
|
||||
with open(sfields_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse TYPED_SFIELD entries
|
||||
# Format: TYPED_SFIELD(sfName, stiSuffix, fieldValue, ...)
|
||||
typed_pattern = r"TYPED_SFIELD\s*\(\s*(\w+)\s*,\s*(\w+)\s*,"
|
||||
for match in re.finditer(typed_pattern, content):
|
||||
field_name = match.group(1)
|
||||
sti_suffix = match.group(2)
|
||||
field_info[field_name] = {
|
||||
"typed": True,
|
||||
"stiSuffix": sti_suffix,
|
||||
"typeData": {
|
||||
"getter_method": "at",
|
||||
"setter_method": "",
|
||||
"setter_use_brackets": True,
|
||||
"setter_type": f"SF_{sti_suffix}::type::value_type const&",
|
||||
"return_type": f"SF_{sti_suffix}::type::value_type",
|
||||
"return_type_optional": f"std::optional<SF_{sti_suffix}::type::value_type>",
|
||||
},
|
||||
}
|
||||
|
||||
# Parse UNTYPED_SFIELD entries
|
||||
# Format: UNTYPED_SFIELD(sfName, stiSuffix, fieldValue, ...)
|
||||
untyped_pattern = r"UNTYPED_SFIELD\s*\(\s*(\w+)\s*,\s*(\w+)\s*,"
|
||||
for match in re.finditer(untyped_pattern, content):
|
||||
field_name = match.group(1)
|
||||
sti_suffix = match.group(2)
|
||||
type_data = UNTYPED_TYPE_MAP.get(
|
||||
sti_suffix, UNTYPED_TYPE_MAP.get("OBJECT")
|
||||
) # Default to OBJECT
|
||||
field_info[field_name] = {
|
||||
"typed": False,
|
||||
"stiSuffix": sti_suffix,
|
||||
"typeData": type_data,
|
||||
}
|
||||
|
||||
return field_info
|
||||
|
||||
|
||||
def create_field_list_parser():
|
||||
"""Create a pyparsing parser for field lists like '({...})'."""
|
||||
# A field identifier (e.g., sfDestination, soeREQUIRED, soeMPTSupported)
|
||||
field_identifier = pp.Word(pp.alphas + "_", pp.alphanums + "_")
|
||||
|
||||
# A single field definition: {sfName, soeREQUIRED, ...}
|
||||
# Allow optional trailing comma inside the braces
|
||||
field_def = (
|
||||
pp.Suppress("{")
|
||||
+ pp.Group(pp.DelimitedList(field_identifier) + pp.Optional(pp.Suppress(",")))(
|
||||
"parts"
|
||||
)
|
||||
+ pp.Suppress("}")
|
||||
)
|
||||
|
||||
# The field list: ({field1, field2, ...}) or ({}) for empty lists
|
||||
# Allow optional trailing comma after the last field definition
|
||||
field_list = (
|
||||
pp.Suppress("(")
|
||||
+ pp.Suppress("{")
|
||||
+ pp.Group(
|
||||
pp.Optional(pp.DelimitedList(field_def) + pp.Optional(pp.Suppress(",")))
|
||||
)("fields")
|
||||
+ pp.Suppress("}")
|
||||
+ pp.Suppress(")")
|
||||
)
|
||||
|
||||
return field_list
|
||||
|
||||
|
||||
def parse_field_list(fields_str):
|
||||
"""Parse a field list string like '({...})' using pyparsing.
|
||||
|
||||
Args:
|
||||
fields_str: A string like '({
|
||||
{sfDestination, soeREQUIRED},
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported}
|
||||
})'
|
||||
|
||||
Returns:
|
||||
A list of field dicts with 'name', 'requirement', 'flags', and 'supports_mpt'.
|
||||
"""
|
||||
parser = create_field_list_parser()
|
||||
|
||||
try:
|
||||
result = parser.parse_string(fields_str, parse_all=True)
|
||||
fields = []
|
||||
|
||||
for field_parts in result.fields:
|
||||
if len(field_parts) < 2:
|
||||
continue
|
||||
|
||||
field_name = field_parts[0]
|
||||
requirement = field_parts[1]
|
||||
flags = list(field_parts[2:]) if len(field_parts) > 2 else []
|
||||
supports_mpt = "soeMPTSupported" in flags
|
||||
|
||||
fields.append(
|
||||
{
|
||||
"name": field_name,
|
||||
"requirement": requirement,
|
||||
"flags": flags,
|
||||
"supports_mpt": supports_mpt,
|
||||
}
|
||||
)
|
||||
|
||||
return fields
|
||||
except pp.ParseException as e:
|
||||
raise ValueError(f"Failed to parse field list: {e}")
|
||||
14
scripts/requirements.txt
Normal file
14
scripts/requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
# Python dependencies for XRP Ledger code generation scripts
|
||||
#
|
||||
# These packages are required to run the code generation scripts that
|
||||
# parse macro files and generate C++ wrapper classes.
|
||||
# cspell:words pyparsing
|
||||
|
||||
# C preprocessor for Python - used to preprocess macro files
|
||||
pcpp>=1.30
|
||||
|
||||
# Parser combinator library - used to parse the macro DSL
|
||||
pyparsing>=3.0.0
|
||||
|
||||
# Template engine - used to generate C++ code from templates
|
||||
Jinja2>=3.0.0
|
||||
176
scripts/templates/LedgerEntry.h.jinja2
Normal file
176
scripts/templates/LedgerEntry.h.jinja2
Normal file
@@ -0,0 +1,176 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
// Forward declaration
|
||||
class {{ name }}Builder;
|
||||
|
||||
/**
|
||||
* Ledger Entry: {{ name }}
|
||||
* Type: {{ tag }} ({{ value }})
|
||||
* RPC Name: {{ rpc_name }}
|
||||
*
|
||||
* Immutable wrapper around SLE providing type-safe field access.
|
||||
* Use {{ name }}Builder to construct new ledger entries.
|
||||
*/
|
||||
class {{ name }} : public LedgerEntryBase
|
||||
{
|
||||
public:
|
||||
static constexpr LedgerEntryType entryType = {{ tag }};
|
||||
|
||||
/**
|
||||
* Construct a {{ name }} ledger entry wrapper from an existing SLE object.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
explicit {{ name }}(SLE const& sle)
|
||||
: LedgerEntryBase(sle)
|
||||
{
|
||||
// Verify ledger entry type
|
||||
if (sle.getType() != entryType)
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for {{ name }}");
|
||||
}
|
||||
}
|
||||
|
||||
// Ledger entry-specific field getters
|
||||
{%- for field in fields %}
|
||||
{%- if field.typed %}
|
||||
|
||||
/**
|
||||
* Get {{ field.name }} ({{ field.requirement }})
|
||||
{%- if field.mpt_support %}
|
||||
* MPT Support: {{ field.mpt_support }}
|
||||
{%- endif %}
|
||||
*/
|
||||
{%- if field.requirement == 'soeREQUIRED' %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->sle_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
}
|
||||
{%- else %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type_optional }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
if (has{{ field.name[2:] }}())
|
||||
return this->sle_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->sle_.isFieldPresent({{ field.name }});
|
||||
}
|
||||
{%- endif %}
|
||||
{%- else %}
|
||||
|
||||
/**
|
||||
* Get {{ field.name }} ({{ field.requirement }})
|
||||
{%- if field.mpt_support %}
|
||||
* MPT Support: {{ field.mpt_support }}
|
||||
{%- endif %}
|
||||
* Note: This is an untyped field ({{ field.cppType }}).
|
||||
*/
|
||||
{%- if field.requirement == 'soeREQUIRED' %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->sle_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
}
|
||||
{%- else %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type_optional }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
if (this->sle_.isFieldPresent({{ field.name }}))
|
||||
return this->sle_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->sle_.isFieldPresent({{ field.name }});
|
||||
}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {{ name }} ledger entries.
|
||||
* Provides a fluent interface for constructing ledger entries with method chaining.
|
||||
* Uses Json::Value internally for flexible ledger entry construction.
|
||||
* Inherits common field setters from LedgerEntryBuilderBase.
|
||||
*/
|
||||
class {{ name }}Builder : public LedgerEntryBuilderBase<{{ name }}Builder>
|
||||
{
|
||||
public:
|
||||
{{ name }}Builder()
|
||||
{
|
||||
// Initialize with ledger entry type
|
||||
object_[sfLedgerEntryType] = {{ tag }};
|
||||
}
|
||||
|
||||
{{ name }}Builder(SLE const& sle)
|
||||
{
|
||||
if (object_[sfLedgerEntryType] != {{ tag }})
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for {{ name }}");
|
||||
}
|
||||
object_ = sle;
|
||||
}
|
||||
|
||||
// Ledger entry-specific field setters
|
||||
{%- for field in fields %}
|
||||
|
||||
/**
|
||||
* Set {{ field.name }} ({{ field.requirement }})
|
||||
{%- if field.mpt_support %}
|
||||
* MPT Support: {{ field.mpt_support }}
|
||||
{%- endif %}
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
{{ name }}Builder&
|
||||
set{{ field.name[2:] }}({{ field.typeData.setter_type }} value)
|
||||
{
|
||||
{%- if field.stiSuffix == 'ISSUE' %}
|
||||
object_[{{ field.name }}] = STIssue({{ field.name }}, value);
|
||||
{%- elif field.typeData.setter_use_brackets %}
|
||||
object_[{{ field.name }}] = value;
|
||||
{%- else %}
|
||||
object_.{{ field.typeData.setter_method }}({{ field.name }}, value);
|
||||
{%- endif %}
|
||||
return *this;
|
||||
}
|
||||
{%- endfor %}
|
||||
|
||||
/**
|
||||
* Build and return the completed {{ name }} wrapper.
|
||||
* @return The constructed ledger entry wrapper.
|
||||
* @throws std::runtime_error if the JSON cannot be parsed into a valid ledger entry.
|
||||
*/
|
||||
{{ name }}
|
||||
build(uint256 const& index)
|
||||
{
|
||||
return {{ name }}{SLE(object_, index)};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::ledger_entries
|
||||
179
scripts/templates/Transaction.h.jinja2
Normal file
179
scripts/templates/Transaction.h.jinja2
Normal file
@@ -0,0 +1,179 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
// Forward declaration
|
||||
class {{ name }}Builder;
|
||||
|
||||
/**
|
||||
* Transaction: {{ name }}
|
||||
* Type: {{ tag }} ({{ value }})
|
||||
* Delegable: {{ delegable }}
|
||||
* Amendment: {{ amendments }}
|
||||
* Privileges: {{ privileges }}
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use {{ name }}Builder to construct new transactions.
|
||||
*/
|
||||
class {{ name }} : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = {{ tag }};
|
||||
|
||||
/**
|
||||
* Construct a {{ name }} transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit {{ name }}(STTx const& tx)
|
||||
: TransactionBase(tx)
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx.getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for {{ name }}");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
{%- for field in fields %}
|
||||
{%- if field.typed %}
|
||||
|
||||
/**
|
||||
* Get {{ field.name }} ({{ field.requirement }})
|
||||
{%- if field.supports_mpt %}
|
||||
* Note: This field supports MPT (Multi-Purpose Token) amounts.
|
||||
{%- endif %}
|
||||
*/
|
||||
{%- if field.requirement == 'soeREQUIRED' %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->tx_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
}
|
||||
{%- else %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type_optional }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
if (has{{ field.name[2:] }}())
|
||||
{
|
||||
return this->tx_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->tx_.isFieldPresent({{ field.name }});
|
||||
}
|
||||
{%- endif %}
|
||||
{%- else %}
|
||||
/**
|
||||
* Get {{ field.name }} ({{ field.requirement }})
|
||||
{%- if field.supports_mpt %}
|
||||
* Note: This field supports MPT (Multi-Purpose Token) amounts.
|
||||
{%- endif %}
|
||||
* Note: This is an untyped field
|
||||
*/
|
||||
{%- if field.requirement == 'soeREQUIRED' %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->tx_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
}
|
||||
{%- else %}
|
||||
[[nodiscard]]
|
||||
{{ field.typeData.return_type_optional }}
|
||||
get{{ field.name[2:] }}() const
|
||||
{
|
||||
if (this->tx_.isFieldPresent({{ field.name }}))
|
||||
return this->tx_.{{ field.typeData.getter_method }}({{ field.name }});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has{{ field.name[2:] }}() const
|
||||
{
|
||||
return this->tx_.isFieldPresent({{ field.name }});
|
||||
}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {{ name }} transactions.
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses Json::Value internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class {{ name }}Builder : public TransactionBuilderBase<{{ name }}Builder>
|
||||
{
|
||||
public:
|
||||
{{ name }}Builder()
|
||||
{
|
||||
// Initialize with transaction type
|
||||
object_[sfTransactionType] = {{ tag }};
|
||||
}
|
||||
|
||||
{{ name }}Builder(STTx const& tx)
|
||||
{
|
||||
if (tx.getTxnType() != {{ tag }})
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for {{ name }}Builder");
|
||||
}
|
||||
object_ = tx;
|
||||
}
|
||||
|
||||
// Transaction-specific field setters
|
||||
{%- for field in fields %}
|
||||
|
||||
/**
|
||||
* Set {{ field.name }} ({{ field.requirement }})
|
||||
{%- if field.supports_mpt %}
|
||||
* Note: This field supports MPT (Multi-Purpose Token) amounts.
|
||||
{%- endif %}
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
{{ name }}Builder&
|
||||
set{{ field.name[2:] }}({{ field.typeData.setter_type }} value)
|
||||
{
|
||||
{%- if field.stiSuffix == 'ISSUE' %}
|
||||
object_[{{ field.name }}] = STIssue({{ field.name }}, value);
|
||||
{%- elif field.typeData.setter_use_brackets %}
|
||||
object_[{{ field.name }}] = value;
|
||||
{%- else %}
|
||||
object_.{{ field.typeData.setter_method }}({{ field.name }}, value);
|
||||
{%- endif %}
|
||||
return *this;
|
||||
}
|
||||
{%- endfor %}
|
||||
|
||||
/**
|
||||
* Build and return the completed {{ name }} wrapper.
|
||||
* @return The constructed transaction wrapper.
|
||||
* @throws std::runtime_error if the JSON cannot be parsed into a valid transaction.
|
||||
*/
|
||||
{{ name }}
|
||||
build()
|
||||
{
|
||||
return {{ name }}(STTx(std::move(object_)));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -1,157 +0,0 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MallocTrim.h>
|
||||
|
||||
#include <boost/predef.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#if defined(__GLIBC__) && BOOST_OS_LINUX
|
||||
#include <sys/resource.h>
|
||||
|
||||
#include <malloc.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// 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<std::chrono::microseconds>(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<std::int64_t>(rssAfterKB) - static_cast<std::int64_t>(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
|
||||
@@ -33,7 +33,7 @@ DatabaseNodeImp::fetchNodeObject(
|
||||
|
||||
try
|
||||
{
|
||||
status = backend_->fetch(hash, &nodeObject);
|
||||
status = backend_->fetch(hash.data(), &nodeObject);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
@@ -68,10 +68,18 @@ DatabaseNodeImp::fetchBatch(std::vector<uint256> const& hashes)
|
||||
using namespace std::chrono;
|
||||
auto const before = steady_clock::now();
|
||||
|
||||
std::vector<uint256 const*> 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(hashes).first;
|
||||
auto results = backend_->fetchBatch(batch).first;
|
||||
XRPL_ASSERT(
|
||||
results.size() == hashes.size() || results.empty(),
|
||||
"number of output objects either matches number of input hashes or is empty");
|
||||
|
||||
@@ -105,7 +105,7 @@ DatabaseRotatingImp::fetchNodeObject(
|
||||
std::shared_ptr<NodeObject> nodeObject;
|
||||
try
|
||||
{
|
||||
status = backend->fetch(hash, &nodeObject);
|
||||
status = backend->fetch(hash.data(), &nodeObject);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
|
||||
@@ -116,9 +116,10 @@ public:
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
Status
|
||||
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pObject) override
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) override
|
||||
{
|
||||
XRPL_ASSERT(db_, "xrpl::NodeStore::MemoryBackend::fetch : non-null database");
|
||||
uint256 const hash(uint256::fromVoid(key));
|
||||
|
||||
std::lock_guard _(db_->mutex);
|
||||
|
||||
@@ -133,14 +134,14 @@ public:
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256> const& hashes) override
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
{
|
||||
std::vector<std::shared_ptr<NodeObject>> results;
|
||||
results.reserve(hashes.size());
|
||||
for (auto const& h : hashes)
|
||||
{
|
||||
std::shared_ptr<NodeObject> nObj;
|
||||
Status status = fetch(h, &nObj);
|
||||
Status status = fetch(h->begin(), &nObj);
|
||||
if (status != ok)
|
||||
results.push_back({});
|
||||
else
|
||||
|
||||
@@ -179,17 +179,17 @@ public:
|
||||
}
|
||||
|
||||
Status
|
||||
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pno) override
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pno) override
|
||||
{
|
||||
Status status;
|
||||
pno->reset();
|
||||
nudb::error_code ec;
|
||||
db_.fetch(
|
||||
hash.data(),
|
||||
[&hash, pno, &status](void const* data, std::size_t size) {
|
||||
key,
|
||||
[key, pno, &status](void const* data, std::size_t size) {
|
||||
nudb::detail::buffer bf;
|
||||
auto const result = nodeobject_decompress(data, size, bf);
|
||||
DecodedBlob decoded(hash.data(), result.first, result.second);
|
||||
DecodedBlob decoded(key, result.first, result.second);
|
||||
if (!decoded.wasOk())
|
||||
{
|
||||
status = dataCorrupt;
|
||||
@@ -207,14 +207,14 @@ public:
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256> const& hashes) override
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
{
|
||||
std::vector<std::shared_ptr<NodeObject>> results;
|
||||
results.reserve(hashes.size());
|
||||
for (auto const& h : hashes)
|
||||
{
|
||||
std::shared_ptr<NodeObject> nObj;
|
||||
Status status = fetch(h, &nObj);
|
||||
Status status = fetch(h->begin(), &nObj);
|
||||
if (status != ok)
|
||||
results.push_back({});
|
||||
else
|
||||
|
||||
@@ -36,13 +36,13 @@ public:
|
||||
}
|
||||
|
||||
Status
|
||||
fetch(uint256 const&, std::shared_ptr<NodeObject>*) override
|
||||
fetch(void const*, std::shared_ptr<NodeObject>*) override
|
||||
{
|
||||
return notFound;
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256> const& hashes) override
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ public:
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
Status
|
||||
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pObject) override
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* 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(std::bit_cast<char const*>(hash.data()), m_keyBytes);
|
||||
rocksdb::Slice const slice(static_cast<char const*>(key), m_keyBytes);
|
||||
|
||||
std::string string;
|
||||
|
||||
@@ -260,7 +260,7 @@ public:
|
||||
|
||||
if (getStatus.ok())
|
||||
{
|
||||
DecodedBlob decoded(hash.data(), string.data(), string.size());
|
||||
DecodedBlob decoded(key, string.data(), string.size());
|
||||
|
||||
if (decoded.wasOk())
|
||||
{
|
||||
@@ -295,14 +295,14 @@ public:
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256> const& hashes) override
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
{
|
||||
std::vector<std::shared_ptr<NodeObject>> results;
|
||||
results.reserve(hashes.size());
|
||||
for (auto const& h : hashes)
|
||||
{
|
||||
std::shared_ptr<NodeObject> nObj;
|
||||
Status status = fetch(h, &nObj);
|
||||
Status status = fetch(h->begin(), &nObj);
|
||||
if (status != ok)
|
||||
results.push_back({});
|
||||
else
|
||||
@@ -332,8 +332,9 @@ public:
|
||||
EncodedBlob encoded(e);
|
||||
|
||||
wb.Put(
|
||||
rocksdb::Slice(std::bit_cast<char const*>(encoded.getKey()), m_keyBytes),
|
||||
rocksdb::Slice(std::bit_cast<char const*>(encoded.getData()), encoded.getSize()));
|
||||
rocksdb::Slice(reinterpret_cast<char const*>(encoded.getKey()), m_keyBytes),
|
||||
rocksdb::Slice(
|
||||
reinterpret_cast<char const*>(encoded.getData()), encoded.getSize()));
|
||||
}
|
||||
|
||||
rocksdb::WriteOptions const options;
|
||||
|
||||
1
src/libxrpl/protocol_autogen/placeholder.cpp
Normal file
1
src/libxrpl/protocol_autogen/placeholder.cpp
Normal file
@@ -0,0 +1 @@
|
||||
// This file is a placeholder to ensure the protocol_autogen module can be built.
|
||||
@@ -1,9 +1,8 @@
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheck.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/InvariantCheck.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
|
||||
3483
src/libxrpl/tx/InvariantCheck.cpp
Normal file
3483
src/libxrpl/tx/InvariantCheck.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,305 +0,0 @@
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/transactors/AMM/AMMHelpers.h>
|
||||
#include <xrpl/tx/transactors/AMM/AMMUtils.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidAMM::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<Issue>(),
|
||||
tx[sfAmount2].get<Issue>(),
|
||||
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<Issue>(),
|
||||
tx[sfAsset2].get<Issue>(),
|
||||
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
|
||||
@@ -1,278 +0,0 @@
|
||||
#include <xrpl/tx/invariants/FreezeInvariant.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
TransfersNotFrozen::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<SLE const> 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<SLE const>
|
||||
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<SLE const> 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,278 +0,0 @@
|
||||
#include <xrpl/tx/invariants/LoanInvariant.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidLoanBroker::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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
|
||||
@@ -1,192 +0,0 @@
|
||||
#include <xrpl/tx/invariants/MPTInvariant.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidMPTIssuance::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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
|
||||
@@ -1,274 +0,0 @@
|
||||
#include <xrpl/tx/invariants/NFTInvariant.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/nftPageMask.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
#include <xrpl/tx/transactors/NFT/NFTokenUtils.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidNFTokenPage::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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<SLE const> const& sle) {
|
||||
uint256 const account = sle->key() & accountBits;
|
||||
uint256 const hiLimit = sle->key() & pageBits;
|
||||
std::optional<uint256> 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<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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
|
||||
@@ -1,93 +0,0 @@
|
||||
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidPermissionedDEX::visitEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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
|
||||
@@ -1,162 +0,0 @@
|
||||
#include <xrpl/tx/invariants/PermissionedDomainInvariant.h>
|
||||
//
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidPermissionedDomain::visitEntry(
|
||||
bool isDel,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
{
|
||||
if (before && before->getType() != ltPERMISSIONED_DOMAIN)
|
||||
return;
|
||||
if (after && after->getType() != ltPERMISSIONED_DOMAIN)
|
||||
return;
|
||||
|
||||
auto check = [isDel](std::vector<SleStatus>& sleStatus, std::shared_ptr<SLE const> 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
|
||||
@@ -1,926 +0,0 @@
|
||||
#include <xrpl/tx/invariants/VaultInvariant.h>
|
||||
//
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
|
||||
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<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> 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
|
||||
// change in vault balances, stored to deltas_ at the end of this function.
|
||||
Number balanceDelta{};
|
||||
|
||||
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 = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltRIPPLE_STATE:
|
||||
balanceDelta = before->getFieldAmount(sfBalance);
|
||||
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 -=
|
||||
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltRIPPLE_STATE:
|
||||
balanceDelta -= Number(after->getFieldAmount(sfBalance));
|
||||
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)
|
||||
deltas_[key] = balanceDelta * sign;
|
||||
}
|
||||
|
||||
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<Shares> {
|
||||
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<Shares> {
|
||||
// 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>(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<Shares> {
|
||||
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<Number> {
|
||||
auto const get = //
|
||||
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
|
||||
if (it == deltas_.end())
|
||||
return std::nullopt;
|
||||
|
||||
return it->second * sign;
|
||||
};
|
||||
|
||||
return std::visit(
|
||||
[&]<typename TIss>(TIss const& issue) {
|
||||
if constexpr (std::is_same_v<TIss, Issue>)
|
||||
{
|
||||
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<TIss, MPTIssue>)
|
||||
{
|
||||
return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key));
|
||||
}
|
||||
},
|
||||
vaultAsset.value());
|
||||
};
|
||||
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
|
||||
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 += fee.drops();
|
||||
if (*ret == zero)
|
||||
return std::nullopt;
|
||||
|
||||
return ret;
|
||||
};
|
||||
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
|
||||
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<Number>(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 vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
|
||||
if (!vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets > tx[sfAmount])
|
||||
{
|
||||
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 accountDeltaAssets = deltaAssetsTxAccount();
|
||||
if (!accountDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor "
|
||||
"balance";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*accountDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must decrease depositor "
|
||||
"balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
|
||||
{
|
||||
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 accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!accountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor "
|
||||
"shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*accountDeltaShares <= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must increase depositor "
|
||||
"shares";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!vaultDeltaShares || *vaultDeltaShares == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor and "
|
||||
"vault shares by equal amount";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
|
||||
"outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
|
||||
{
|
||||
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 vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
|
||||
if (!vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
|
||||
"change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets >= 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 accountDeltaAssets = deltaAssetsTxAccount();
|
||||
auto const otherAccountDelta = [&]() -> std::optional<Number> {
|
||||
if (auto const destination = tx[~sfDestination];
|
||||
destination && *destination != tx[sfAccount])
|
||||
return deltaAssets(*destination);
|
||||
return std::nullopt;
|
||||
}();
|
||||
|
||||
if (accountDeltaAssets.has_value() == otherAccountDelta.has_value())
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change one "
|
||||
"destination balance";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const destinationDelta = //
|
||||
accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta;
|
||||
|
||||
if (destinationDelta <= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must increase "
|
||||
"destination balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets * -1 != destinationDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change vault "
|
||||
"and destination balance by equal amount";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!accountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change depositor "
|
||||
"shares";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*accountDeltaShares >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must decrease depositor "
|
||||
"shares";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!vaultDeltaShares || *vaultDeltaShares == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change depositor "
|
||||
"and vault shares by equal amount";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// Note, vaultBalance is negative (see check above)
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
|
||||
"assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
|
||||
{
|
||||
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 vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (vaultDeltaAssets)
|
||||
{
|
||||
if (*vaultDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease vault "
|
||||
"balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets outstanding "
|
||||
"must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
|
||||
afterVault.assetsAvailable)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
|
||||
if (!accountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change holder shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*accountDeltaShares >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease holder "
|
||||
"shares";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!vaultDeltaShares || *vaultDeltaShares == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,9 +1,10 @@
|
||||
#include <xrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.h>
|
||||
//
|
||||
#include <xrpl/ledger/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
|
||||
@@ -5340,20 +5340,20 @@ 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_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
|
||||
BEAST_EXPECT(false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ public:
|
||||
{
|
||||
std::shared_ptr<NodeObject> object;
|
||||
|
||||
Status const status = backend.fetch(batch[i]->getHash(), &object);
|
||||
Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object);
|
||||
|
||||
BEAST_EXPECT(status == ok);
|
||||
|
||||
@@ -158,7 +158,7 @@ public:
|
||||
{
|
||||
std::shared_ptr<NodeObject> object;
|
||||
|
||||
Status const status = backend.fetch(batch[i]->getHash(), &object);
|
||||
Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object);
|
||||
|
||||
BEAST_EXPECT(status == notFound);
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ public:
|
||||
std::shared_ptr<NodeObject> obj;
|
||||
std::shared_ptr<NodeObject> result;
|
||||
obj = seq1_.obj(dist_(gen_));
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
backend_.fetch(obj->getHash().data(), &result);
|
||||
suite_.expect(result && isSame(result, obj));
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
@@ -377,9 +377,9 @@ public:
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const hash = seq2_.key(i);
|
||||
auto const key = seq2_.key(i);
|
||||
std::shared_ptr<NodeObject> result;
|
||||
backend_.fetch(hash, &result);
|
||||
backend_.fetch(key.data(), &result);
|
||||
suite_.expect(!result);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
@@ -449,9 +449,9 @@ public:
|
||||
{
|
||||
if (rand_(gen_) < missingNodePercent)
|
||||
{
|
||||
auto const hash = seq2_.key(dist_(gen_));
|
||||
auto const key = seq2_.key(dist_(gen_));
|
||||
std::shared_ptr<NodeObject> result;
|
||||
backend_.fetch(hash, &result);
|
||||
backend_.fetch(key.data(), &result);
|
||||
suite_.expect(!result);
|
||||
}
|
||||
else
|
||||
@@ -459,7 +459,7 @@ public:
|
||||
std::shared_ptr<NodeObject> obj;
|
||||
std::shared_ptr<NodeObject> result;
|
||||
obj = seq1_.obj(dist_(gen_));
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
backend_.fetch(obj->getHash().data(), &result);
|
||||
suite_.expect(result && isSame(result, obj));
|
||||
}
|
||||
}
|
||||
@@ -540,7 +540,8 @@ public:
|
||||
std::shared_ptr<NodeObject> result;
|
||||
auto const j = older_(gen_);
|
||||
obj = seq1_.obj(j);
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
std::shared_ptr<NodeObject> result1;
|
||||
backend_.fetch(obj->getHash().data(), &result);
|
||||
suite_.expect(result != nullptr);
|
||||
suite_.expect(isSame(result, obj));
|
||||
}
|
||||
@@ -558,7 +559,7 @@ public:
|
||||
std::shared_ptr<NodeObject> result;
|
||||
auto const j = recent_(gen_);
|
||||
obj = seq1_.obj(j);
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
backend_.fetch(obj->getHash().data(), &result);
|
||||
suite_.expect(!result || isSame(result, obj));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -148,20 +148,12 @@ class Feature_test : public beast::unit_test::suite
|
||||
feature.isMember(jss::enabled) && !feature[jss::enabled].asBool(),
|
||||
feature[jss::name].asString() + " enabled");
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::vetoed) && feature[jss::vetoed].isBool(),
|
||||
feature[jss::name].asString() + " vetoed is bool");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::vetoed].asBool() == (expectVeto || expectObsolete),
|
||||
feature[jss::name].asString() + " vetoed value");
|
||||
if (expectObsolete)
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::obsolete) && feature[jss::obsolete].isBool() &&
|
||||
feature[jss::obsolete].asBool() == true,
|
||||
feature[jss::name].asString() + " obsolete");
|
||||
else
|
||||
BEAST_EXPECTS(
|
||||
!feature.isMember(jss::obsolete),
|
||||
feature[jss::name].asString() + " no obsolete");
|
||||
feature.isMember(jss::vetoed) && feature[jss::vetoed].isBool() == !expectObsolete &&
|
||||
(!feature[jss::vetoed].isBool() ||
|
||||
feature[jss::vetoed].asBool() == expectVeto) &&
|
||||
(feature[jss::vetoed].isBool() ||
|
||||
feature[jss::vetoed].asString() == "Obsolete"),
|
||||
feature[jss::name].asString() + " vetoed");
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::supported) && feature[jss::supported].asBool(),
|
||||
feature[jss::name].asString() + " supported");
|
||||
@@ -261,7 +253,6 @@ class Feature_test : public beast::unit_test::suite
|
||||
(*it)[jss::supported].asBool() == expectSupported,
|
||||
(*it)[jss::name].asString() + " supported");
|
||||
BEAST_EXPECT(!(*it).isMember(jss::vetoed));
|
||||
BEAST_EXPECT(!(*it).isMember(jss::obsolete));
|
||||
BEAST_EXPECT(!(*it).isMember(jss::majority));
|
||||
BEAST_EXPECT(!(*it).isMember(jss::count));
|
||||
BEAST_EXPECT(!(*it).isMember(jss::validations));
|
||||
@@ -324,23 +315,13 @@ class Feature_test : public beast::unit_test::suite
|
||||
BEAST_EXPECTS(
|
||||
!(*it).isMember(jss::vetoed), (*it)[jss::name].asString() + " vetoed");
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
(*it).isMember(jss::vetoed) && (*it)[jss::vetoed].isBool(),
|
||||
(*it)[jss::name].asString() + " vetoed is bool");
|
||||
BEAST_EXPECTS(
|
||||
(*it)[jss::vetoed].asBool() == (expectVeto || expectObsolete),
|
||||
(*it)[jss::name].asString() + " vetoed value");
|
||||
if (expectObsolete)
|
||||
BEAST_EXPECTS(
|
||||
(*it).isMember(jss::obsolete) && (*it)[jss::obsolete].isBool() &&
|
||||
(*it)[jss::obsolete].asBool() == true,
|
||||
(*it)[jss::name].asString() + " obsolete");
|
||||
else
|
||||
BEAST_EXPECTS(
|
||||
!(*it).isMember(jss::obsolete),
|
||||
(*it)[jss::name].asString() + " no obsolete");
|
||||
}
|
||||
(*it).isMember(jss::vetoed) && (*it)[jss::vetoed].isBool() == !expectObsolete &&
|
||||
(!(*it)[jss::vetoed].isBool() ||
|
||||
(*it)[jss::vetoed].asBool() == expectVeto) &&
|
||||
((*it)[jss::vetoed].isBool() ||
|
||||
(*it)[jss::vetoed].asString() == "Obsolete"),
|
||||
(*it)[jss::name].asString() + " vetoed");
|
||||
BEAST_EXPECTS(
|
||||
(*it).isMember(jss::supported) && (*it)[jss::supported].asBool() == expectSupported,
|
||||
(*it)[jss::name].asString() + " supported");
|
||||
@@ -408,20 +389,12 @@ class Feature_test : public beast::unit_test::suite
|
||||
(expectVeto || expectObsolete) ^ feature.isMember(jss::majority),
|
||||
feature[jss::name].asString() + " majority");
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::vetoed) && feature[jss::vetoed].isBool(),
|
||||
feature[jss::name].asString() + " vetoed is bool");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::vetoed].asBool() == (expectVeto || expectObsolete),
|
||||
feature[jss::name].asString() + " vetoed value");
|
||||
if (expectObsolete)
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::obsolete) && feature[jss::obsolete].isBool() &&
|
||||
feature[jss::obsolete].asBool() == true,
|
||||
feature[jss::name].asString() + " obsolete");
|
||||
else
|
||||
BEAST_EXPECTS(
|
||||
!feature.isMember(jss::obsolete),
|
||||
feature[jss::name].asString() + " no obsolete");
|
||||
feature.isMember(jss::vetoed) && feature[jss::vetoed].isBool() == !expectObsolete &&
|
||||
(!feature[jss::vetoed].isBool() ||
|
||||
feature[jss::vetoed].asBool() == expectVeto) &&
|
||||
(feature[jss::vetoed].isBool() ||
|
||||
feature[jss::vetoed].asString() == "Obsolete"),
|
||||
feature[jss::name].asString() + " vetoed");
|
||||
BEAST_EXPECTS(feature.isMember(jss::count), feature[jss::name].asString() + " count");
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::threshold), feature[jss::name].asString() + " threshold");
|
||||
@@ -512,9 +485,8 @@ class Feature_test : public beast::unit_test::suite
|
||||
auto feature = *(jrr.begin());
|
||||
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::vetoed].isBool() && feature[jss::vetoed].asBool() == true, "vetoed");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::obsolete].isBool() && feature[jss::obsolete].asBool() == true, "obsolete");
|
||||
feature[jss::vetoed].isString() && feature[jss::vetoed].asString() == "Obsolete",
|
||||
"vetoed");
|
||||
|
||||
jrr = env.rpc("feature", featureName, "reject")[jss::result];
|
||||
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
|
||||
@@ -525,9 +497,8 @@ class Feature_test : public beast::unit_test::suite
|
||||
feature = *(jrr.begin());
|
||||
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::vetoed].isBool() && feature[jss::vetoed].asBool() == true, "vetoed");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::obsolete].isBool() && feature[jss::obsolete].asBool() == true, "obsolete");
|
||||
feature[jss::vetoed].isString() && feature[jss::vetoed].asString() == "Obsolete",
|
||||
"vetoed");
|
||||
|
||||
jrr = env.rpc("feature", featureName, "accept")[jss::result];
|
||||
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
|
||||
@@ -538,9 +509,8 @@ class Feature_test : public beast::unit_test::suite
|
||||
feature = *(jrr.begin());
|
||||
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::vetoed].isBool() && feature[jss::vetoed].asBool() == true, "vetoed");
|
||||
BEAST_EXPECTS(
|
||||
feature[jss::obsolete].isBool() && feature[jss::obsolete].asBool() == true, "obsolete");
|
||||
feature[jss::vetoed].isString() && feature[jss::vetoed].asString() == "Obsolete",
|
||||
"vetoed");
|
||||
|
||||
// anything other than accept or reject is an error
|
||||
jrr = env.rpc("feature", featureName, "maybe");
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
#include <xrpl/basics/MallocTrim.h>
|
||||
|
||||
#include <boost/predef.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <xrpld/shamap/NodeFamily.h>
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/MallocTrim.h>
|
||||
#include <xrpl/basics/ResolverAsio.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/asio/io_latency_probe.h>
|
||||
@@ -1054,8 +1053,6 @@ public:
|
||||
<< "; size after: " << cachedSLEs_.size();
|
||||
}
|
||||
|
||||
mallocTrim("doSweep", m_journal);
|
||||
|
||||
// Set timer to do another sweep later.
|
||||
setSweepTimer();
|
||||
}
|
||||
|
||||
@@ -931,10 +931,7 @@ AmendmentTableImpl::injectJson(
|
||||
if (!fs.enabled && isAdmin)
|
||||
{
|
||||
if (fs.vote == AmendmentVote::obsolete)
|
||||
{
|
||||
v[jss::vetoed] = true;
|
||||
v[jss::obsolete] = true;
|
||||
}
|
||||
v[jss::vetoed] = "Obsolete";
|
||||
else
|
||||
v[jss::vetoed] = fs.vote == AmendmentVote::down;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user