mirror of
https://github.com/XRPLF/rippled.git
synced 2026-03-01 18:22:34 +00:00
Compare commits
19 Commits
ximinez/nu
...
legleux/tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8585055d6e | ||
|
|
a35308399d | ||
|
|
8068a7b513 | ||
|
|
1a7f824b89 | ||
|
|
b58c681189 | ||
|
|
404f35d556 | ||
|
|
2e595b6031 | ||
|
|
3a8a18c2ca | ||
|
|
65e63ebef3 | ||
|
|
bdd106d992 | ||
|
|
24cbaf76a5 | ||
|
|
3a805cc646 | ||
|
|
0fd237d707 | ||
|
|
3542daa4cc | ||
|
|
fd9f57ec97 | ||
|
|
625becff18 | ||
|
|
4bcbc6e50f | ||
|
|
0bc4a0cfe8 | ||
|
|
cb54adefed |
311
.clang-tidy
311
.clang-tidy
@@ -1,105 +1,143 @@
|
||||
---
|
||||
Checks: "-*,
|
||||
bugprone-argument-comment
|
||||
bugprone-argument-comment,
|
||||
bugprone-assert-side-effect,
|
||||
bugprone-bad-signal-to-kill-thread,
|
||||
bugprone-bool-pointer-implicit-conversion,
|
||||
bugprone-casting-through-void,
|
||||
bugprone-chained-comparison,
|
||||
bugprone-compare-pointer-to-member-virtual-function,
|
||||
bugprone-copy-constructor-init,
|
||||
bugprone-dangling-handle,
|
||||
bugprone-dynamic-static-initializers,
|
||||
bugprone-fold-init-type,
|
||||
bugprone-forward-declaration-namespace,
|
||||
bugprone-inaccurate-erase,
|
||||
bugprone-incorrect-enable-if,
|
||||
bugprone-incorrect-roundings,
|
||||
bugprone-infinite-loop,
|
||||
bugprone-integer-division,
|
||||
bugprone-lambda-function-name,
|
||||
bugprone-macro-parentheses,
|
||||
bugprone-macro-repeated-side-effects,
|
||||
bugprone-misplaced-operator-in-strlen-in-alloc,
|
||||
bugprone-misplaced-pointer-arithmetic-in-alloc,
|
||||
bugprone-misplaced-widening-cast,
|
||||
bugprone-multi-level-implicit-pointer-conversion,
|
||||
bugprone-multiple-new-in-one-expression,
|
||||
bugprone-multiple-statement-macro,
|
||||
bugprone-no-escape,
|
||||
bugprone-non-zero-enum-to-bool-conversion,
|
||||
bugprone-parent-virtual-call,
|
||||
bugprone-posix-return,
|
||||
bugprone-redundant-branch-condition,
|
||||
bugprone-shared-ptr-array-mismatch,
|
||||
bugprone-signal-handler,
|
||||
bugprone-signed-char-misuse,
|
||||
bugprone-sizeof-container,
|
||||
bugprone-spuriously-wake-up-functions,
|
||||
bugprone-standalone-empty,
|
||||
bugprone-string-constructor,
|
||||
bugprone-string-integer-assignment,
|
||||
bugprone-string-literal-with-embedded-nul,
|
||||
bugprone-stringview-nullptr,
|
||||
bugprone-suspicious-enum-usage,
|
||||
bugprone-suspicious-include,
|
||||
bugprone-suspicious-memory-comparison,
|
||||
bugprone-suspicious-memset-usage,
|
||||
bugprone-suspicious-realloc-usage,
|
||||
bugprone-suspicious-semicolon,
|
||||
bugprone-suspicious-string-compare,
|
||||
bugprone-swapped-arguments,
|
||||
bugprone-terminating-continue,
|
||||
bugprone-throw-keyword-missing,
|
||||
bugprone-undefined-memory-manipulation,
|
||||
bugprone-undelegated-constructor,
|
||||
bugprone-unhandled-exception-at-new,
|
||||
bugprone-unique-ptr-array-mismatch,
|
||||
bugprone-unsafe-functions,
|
||||
bugprone-virtual-near-miss,
|
||||
cppcoreguidelines-no-suspend-with-lock,
|
||||
cppcoreguidelines-virtual-class-destructor,
|
||||
hicpp-ignored-remove-result,
|
||||
misc-definitions-in-headers,
|
||||
misc-header-include-cycle,
|
||||
misc-misplaced-const,
|
||||
misc-static-assert,
|
||||
misc-throw-by-value-catch-by-reference,
|
||||
misc-unused-alias-decls,
|
||||
misc-unused-using-decls,
|
||||
readability-duplicate-include,
|
||||
readability-enum-initial-value,
|
||||
readability-misleading-indentation,
|
||||
readability-non-const-parameter,
|
||||
readability-redundant-declaration,
|
||||
readability-reference-to-constructed-temporary,
|
||||
modernize-deprecated-headers,
|
||||
modernize-make-shared,
|
||||
modernize-make-unique,
|
||||
performance-implicit-conversion-in-loop,
|
||||
performance-move-constructor-init,
|
||||
performance-trivially-destructible
|
||||
"
|
||||
# bugprone-assert-side-effect,
|
||||
# bugprone-bad-signal-to-kill-thread,
|
||||
# bugprone-bool-pointer-implicit-conversion,
|
||||
# bugprone-casting-through-void,
|
||||
# bugprone-chained-comparison,
|
||||
# bugprone-compare-pointer-to-member-virtual-function,
|
||||
# bugprone-copy-constructor-init,
|
||||
# bugprone-crtp-constructor-accessibility,
|
||||
# bugprone-dangling-handle,
|
||||
# bugprone-dynamic-static-initializers,
|
||||
# ---
|
||||
# checks that have some issues that need to be resolved:
|
||||
#
|
||||
# bugprone-empty-catch,
|
||||
# bugprone-fold-init-type,
|
||||
# bugprone-forward-declaration-namespace,
|
||||
# bugprone-inaccurate-erase,
|
||||
# bugprone-crtp-constructor-accessibility,
|
||||
# bugprone-inc-dec-in-conditions,
|
||||
# bugprone-incorrect-enable-if,
|
||||
# bugprone-incorrect-roundings,
|
||||
# bugprone-infinite-loop,
|
||||
# bugprone-integer-division,
|
||||
# bugprone-lambda-function-name,
|
||||
# bugprone-macro-parentheses,
|
||||
# bugprone-macro-repeated-side-effects,
|
||||
# bugprone-misplaced-operator-in-strlen-in-alloc,
|
||||
# bugprone-misplaced-pointer-arithmetic-in-alloc,
|
||||
# bugprone-misplaced-widening-cast,
|
||||
# bugprone-move-forwarding-reference,
|
||||
# bugprone-multi-level-implicit-pointer-conversion,
|
||||
# bugprone-multiple-new-in-one-expression,
|
||||
# bugprone-multiple-statement-macro,
|
||||
# bugprone-no-escape,
|
||||
# bugprone-non-zero-enum-to-bool-conversion,
|
||||
# bugprone-optional-value-conversion,
|
||||
# bugprone-parent-virtual-call,
|
||||
# bugprone-pointer-arithmetic-on-polymorphic-object,
|
||||
# bugprone-posix-return,
|
||||
# bugprone-redundant-branch-condition,
|
||||
# bugprone-reserved-identifier,
|
||||
# bugprone-return-const-ref-from-parameter,
|
||||
# bugprone-shared-ptr-array-mismatch,
|
||||
# bugprone-signal-handler,
|
||||
# bugprone-signed-char-misuse,
|
||||
# bugprone-sizeof-container,
|
||||
# bugprone-sizeof-expression,
|
||||
# bugprone-spuriously-wake-up-functions,
|
||||
# bugprone-standalone-empty,
|
||||
# bugprone-string-constructor,
|
||||
# bugprone-string-integer-assignment,
|
||||
# bugprone-string-literal-with-embedded-nul,
|
||||
# bugprone-stringview-nullptr,
|
||||
# bugprone-suspicious-enum-usage,
|
||||
# bugprone-suspicious-include,
|
||||
# bugprone-suspicious-memory-comparison,
|
||||
# bugprone-suspicious-memset-usage,
|
||||
# bugprone-suspicious-missing-comma,
|
||||
# bugprone-suspicious-realloc-usage,
|
||||
# bugprone-suspicious-semicolon,
|
||||
# bugprone-suspicious-string-compare,
|
||||
# bugprone-suspicious-stringview-data-usage,
|
||||
# bugprone-swapped-arguments,
|
||||
# bugprone-switch-missing-default-case,
|
||||
# bugprone-terminating-continue,
|
||||
# bugprone-throw-keyword-missing,
|
||||
# bugprone-too-small-loop-variable,
|
||||
# bugprone-undefined-memory-manipulation,
|
||||
# bugprone-undelegated-constructor,
|
||||
# bugprone-unhandled-exception-at-new,
|
||||
# bugprone-unhandled-self-assignment,
|
||||
# bugprone-unique-ptr-array-mismatch,
|
||||
# bugprone-unsafe-functions,
|
||||
# bugprone-move-forwarding-reference,
|
||||
# bugprone-unused-local-non-trivial-variable,
|
||||
# bugprone-unused-raii,
|
||||
# bugprone-return-const-ref-from-parameter,
|
||||
# bugprone-switch-missing-default-case,
|
||||
# bugprone-sizeof-expression,
|
||||
# bugprone-suspicious-stringview-data-usage,
|
||||
# bugprone-suspicious-missing-comma,
|
||||
# bugprone-pointer-arithmetic-on-polymorphic-object,
|
||||
# bugprone-optional-value-conversion,
|
||||
# bugprone-too-small-loop-variable,
|
||||
# bugprone-unused-return-value,
|
||||
# bugprone-use-after-move,
|
||||
# bugprone-virtual-near-miss,
|
||||
# cppcoreguidelines-init-variables,
|
||||
# bugprone-unhandled-self-assignment,
|
||||
# bugprone-unused-raii,
|
||||
#
|
||||
# cppcoreguidelines-misleading-capture-default-by-value,
|
||||
# cppcoreguidelines-no-suspend-with-lock,
|
||||
# cppcoreguidelines-init-variables,
|
||||
# cppcoreguidelines-pro-type-member-init,
|
||||
# cppcoreguidelines-pro-type-static-cast-downcast,
|
||||
# cppcoreguidelines-rvalue-reference-param-not-moved,
|
||||
# cppcoreguidelines-use-default-member-init,
|
||||
# cppcoreguidelines-virtual-class-destructor,
|
||||
# hicpp-ignored-remove-result,
|
||||
# cppcoreguidelines-rvalue-reference-param-not-moved,
|
||||
#
|
||||
# llvm-namespace-comment,
|
||||
# misc-const-correctness,
|
||||
# misc-definitions-in-headers,
|
||||
# misc-header-include-cycle,
|
||||
# misc-include-cleaner,
|
||||
# misc-misplaced-const,
|
||||
# misc-redundant-expression,
|
||||
# misc-static-assert,
|
||||
# misc-throw-by-value-catch-by-reference,
|
||||
# misc-unused-alias-decls,
|
||||
# misc-unused-using-decls,
|
||||
#
|
||||
# readability-avoid-nested-conditional-operator,
|
||||
# readability-avoid-return-with-void-value,
|
||||
# readability-braces-around-statements,
|
||||
# readability-container-contains,
|
||||
# readability-container-size-empty,
|
||||
# readability-convert-member-functions-to-static,
|
||||
# readability-const-return-type,
|
||||
# readability-else-after-return,
|
||||
# readability-implicit-bool-conversion,
|
||||
# readability-inconsistent-declaration-parameter-name,
|
||||
# readability-identifier-naming,
|
||||
# readability-make-member-function-const,
|
||||
# readability-math-missing-parentheses,
|
||||
# readability-redundant-inline-specifier,
|
||||
# readability-redundant-member-init,
|
||||
# readability-redundant-casting,
|
||||
# readability-redundant-string-init,
|
||||
# readability-simplify-boolean-expr,
|
||||
# readability-static-definition-in-anonymous-namespace,
|
||||
# readability-suspicious-call-argument,
|
||||
# readability-use-std-min-max,
|
||||
# readability-static-accessed-through-instance,
|
||||
#
|
||||
# modernize-concat-nested-namespaces,
|
||||
# modernize-deprecated-headers,
|
||||
# modernize-make-shared,
|
||||
# modernize-make-unique,
|
||||
# modernize-pass-by-value,
|
||||
# modernize-type-traits,
|
||||
# modernize-use-designated-initializers,
|
||||
@@ -111,79 +149,50 @@ Checks: "-*,
|
||||
# modernize-use-starts-ends-with,
|
||||
# modernize-use-std-numbers,
|
||||
# modernize-use-using,
|
||||
#
|
||||
# performance-faster-string-find,
|
||||
# performance-for-range-copy,
|
||||
# performance-implicit-conversion-in-loop,
|
||||
# performance-inefficient-vector-operation,
|
||||
# performance-move-const-arg,
|
||||
# performance-move-constructor-init,
|
||||
# performance-no-automatic-move,
|
||||
# performance-trivially-destructible,
|
||||
# readability-avoid-nested-conditional-operator,
|
||||
# readability-avoid-return-with-void-value,
|
||||
# readability-braces-around-statements,
|
||||
# readability-const-return-type,
|
||||
# readability-container-contains,
|
||||
# readability-container-size-empty,
|
||||
# readability-convert-member-functions-to-static,
|
||||
# readability-duplicate-include,
|
||||
# readability-else-after-return,
|
||||
# readability-enum-initial-value,
|
||||
# readability-implicit-bool-conversion,
|
||||
# readability-inconsistent-declaration-parameter-name,
|
||||
# readability-identifier-naming,
|
||||
# readability-make-member-function-const,
|
||||
# readability-math-missing-parentheses,
|
||||
# readability-misleading-indentation,
|
||||
# readability-non-const-parameter,
|
||||
# readability-redundant-casting,
|
||||
# readability-redundant-declaration,
|
||||
# readability-redundant-inline-specifier,
|
||||
# readability-redundant-member-init,
|
||||
# readability-redundant-string-init,
|
||||
# readability-reference-to-constructed-temporary,
|
||||
# readability-simplify-boolean-expr,
|
||||
# readability-static-accessed-through-instance,
|
||||
# readability-static-definition-in-anonymous-namespace,
|
||||
# readability-suspicious-call-argument,
|
||||
# readability-use-std-min-max
|
||||
# ---
|
||||
#
|
||||
# CheckOptions:
|
||||
# readability-braces-around-statements.ShortStatementLines: 2
|
||||
# readability-identifier-naming.MacroDefinitionCase: UPPER_CASE
|
||||
# readability-identifier-naming.ClassCase: CamelCase
|
||||
# readability-identifier-naming.StructCase: CamelCase
|
||||
# readability-identifier-naming.UnionCase: CamelCase
|
||||
# readability-identifier-naming.EnumCase: CamelCase
|
||||
# readability-identifier-naming.EnumConstantCase: CamelCase
|
||||
# readability-identifier-naming.ScopedEnumConstantCase: CamelCase
|
||||
# readability-identifier-naming.GlobalConstantCase: UPPER_CASE
|
||||
# readability-identifier-naming.GlobalConstantPrefix: "k"
|
||||
# readability-identifier-naming.GlobalVariableCase: CamelCase
|
||||
# readability-identifier-naming.GlobalVariablePrefix: "g"
|
||||
# readability-identifier-naming.ConstexprFunctionCase: camelBack
|
||||
# readability-identifier-naming.ConstexprMethodCase: camelBack
|
||||
# readability-identifier-naming.ClassMethodCase: camelBack
|
||||
# readability-identifier-naming.ClassMemberCase: camelBack
|
||||
# readability-identifier-naming.ClassConstantCase: UPPER_CASE
|
||||
# readability-identifier-naming.ClassConstantPrefix: "k"
|
||||
# readability-identifier-naming.StaticConstantCase: UPPER_CASE
|
||||
# readability-identifier-naming.StaticConstantPrefix: "k"
|
||||
# readability-identifier-naming.StaticVariableCase: UPPER_CASE
|
||||
# readability-identifier-naming.StaticVariablePrefix: "k"
|
||||
# readability-identifier-naming.ConstexprVariableCase: UPPER_CASE
|
||||
# readability-identifier-naming.ConstexprVariablePrefix: "k"
|
||||
# readability-identifier-naming.LocalConstantCase: camelBack
|
||||
# readability-identifier-naming.LocalVariableCase: camelBack
|
||||
# readability-identifier-naming.TemplateParameterCase: CamelCase
|
||||
# readability-identifier-naming.ParameterCase: camelBack
|
||||
# readability-identifier-naming.FunctionCase: camelBack
|
||||
# readability-identifier-naming.MemberCase: camelBack
|
||||
# readability-identifier-naming.PrivateMemberSuffix: _
|
||||
# readability-identifier-naming.ProtectedMemberSuffix: _
|
||||
# readability-identifier-naming.PublicMemberSuffix: ""
|
||||
# readability-identifier-naming.FunctionIgnoredRegexp: ".*tag_invoke.*"
|
||||
# bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true
|
||||
CheckOptions:
|
||||
# readability-braces-around-statements.ShortStatementLines: 2
|
||||
# readability-identifier-naming.MacroDefinitionCase: UPPER_CASE
|
||||
# readability-identifier-naming.ClassCase: CamelCase
|
||||
# readability-identifier-naming.StructCase: CamelCase
|
||||
# readability-identifier-naming.UnionCase: CamelCase
|
||||
# readability-identifier-naming.EnumCase: CamelCase
|
||||
# readability-identifier-naming.EnumConstantCase: CamelCase
|
||||
# readability-identifier-naming.ScopedEnumConstantCase: CamelCase
|
||||
# readability-identifier-naming.GlobalConstantCase: UPPER_CASE
|
||||
# readability-identifier-naming.GlobalConstantPrefix: "k"
|
||||
# readability-identifier-naming.GlobalVariableCase: CamelCase
|
||||
# readability-identifier-naming.GlobalVariablePrefix: "g"
|
||||
# readability-identifier-naming.ConstexprFunctionCase: camelBack
|
||||
# readability-identifier-naming.ConstexprMethodCase: camelBack
|
||||
# readability-identifier-naming.ClassMethodCase: camelBack
|
||||
# readability-identifier-naming.ClassMemberCase: camelBack
|
||||
# readability-identifier-naming.ClassConstantCase: UPPER_CASE
|
||||
# readability-identifier-naming.ClassConstantPrefix: "k"
|
||||
# readability-identifier-naming.StaticConstantCase: UPPER_CASE
|
||||
# readability-identifier-naming.StaticConstantPrefix: "k"
|
||||
# readability-identifier-naming.StaticVariableCase: UPPER_CASE
|
||||
# readability-identifier-naming.StaticVariablePrefix: "k"
|
||||
# readability-identifier-naming.ConstexprVariableCase: UPPER_CASE
|
||||
# readability-identifier-naming.ConstexprVariablePrefix: "k"
|
||||
# readability-identifier-naming.LocalConstantCase: camelBack
|
||||
# readability-identifier-naming.LocalVariableCase: camelBack
|
||||
# readability-identifier-naming.TemplateParameterCase: CamelCase
|
||||
# readability-identifier-naming.ParameterCase: camelBack
|
||||
# readability-identifier-naming.FunctionCase: camelBack
|
||||
# readability-identifier-naming.MemberCase: camelBack
|
||||
# readability-identifier-naming.PrivateMemberSuffix: _
|
||||
# readability-identifier-naming.ProtectedMemberSuffix: _
|
||||
# readability-identifier-naming.PublicMemberSuffix: ""
|
||||
# readability-identifier-naming.FunctionIgnoredRegexp: ".*tag_invoke.*"
|
||||
bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true
|
||||
# bugprone-unused-return-value.CheckedReturnTypes: ::std::error_code;::std::error_condition;::std::errc
|
||||
# misc-include-cleaner.IgnoreHeaders: '.*/(detail|impl)/.*;.*(expected|unexpected).*;.*ranges_lower_bound\.h;time.h;stdlib.h;__chrono/.*;fmt/chrono.h;boost/uuid/uuid_hash.hpp'
|
||||
#
|
||||
|
||||
42
.github/scripts/strategy-matrix/generate.py
vendored
42
.github/scripts/strategy-matrix/generate.py
vendored
@@ -32,10 +32,13 @@ We will further set additional CMake arguments as follows:
|
||||
"""
|
||||
|
||||
|
||||
def generate_strategy_matrix(all: bool, config: Config) -> list:
|
||||
def generate_strategy_matrix(all: bool, config: Config, distro: str = "") -> list:
|
||||
configurations = []
|
||||
os_entries = config.os
|
||||
if distro:
|
||||
os_entries = [o for o in os_entries if o["distro_name"] == distro]
|
||||
for architecture, os, build_type, cmake_args in itertools.product(
|
||||
config.architecture, config.os, config.build_type, config.cmake_args
|
||||
config.architecture, os_entries, config.build_type, config.cmake_args
|
||||
):
|
||||
# The default CMake target is 'all' for Linux and MacOS and 'install'
|
||||
# for Windows, but it can get overridden for certain configurations.
|
||||
@@ -223,7 +226,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
|
||||
if (n := os["compiler_version"]) != "":
|
||||
config_name += f"-{n}"
|
||||
config_name += (
|
||||
f"-{architecture['platform'][architecture['platform'].find('/')+1:]}"
|
||||
f"-{architecture['platform'][architecture['platform'].find('/') + 1 :]}"
|
||||
)
|
||||
config_name += f"-{build_type.lower()}"
|
||||
if "-Dcoverage=ON" in cmake_args:
|
||||
@@ -236,23 +239,21 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
|
||||
# names get truncated.
|
||||
# Add Address and Thread (both coupled with UB) sanitizers for specific bookworm distros.
|
||||
# GCC-Asan rippled-embedded tests are failing because of https://github.com/google/sanitizers/issues/856
|
||||
if os[
|
||||
"distro_version"
|
||||
] == "bookworm" and f"{os['compiler_name']}-{os['compiler_version']}" in [
|
||||
"clang-20",
|
||||
"gcc-13",
|
||||
]:
|
||||
if (
|
||||
os["distro_version"] == "bookworm"
|
||||
and f"{os['compiler_name']}-{os['compiler_version']}" == "clang-20"
|
||||
):
|
||||
# Add ASAN + UBSAN configuration.
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name + "-asan",
|
||||
"config_name": config_name + "-asan-ubsan",
|
||||
"cmake_args": cmake_args,
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
"sanitizers": "address",
|
||||
"sanitizers": "address,undefinedbehavior",
|
||||
}
|
||||
)
|
||||
# TSAN is deactivated due to seg faults with latest compilers.
|
||||
@@ -315,21 +316,32 @@ if __name__ == "__main__":
|
||||
required=False,
|
||||
type=Path,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--distro",
|
||||
help="Filter OS entries to only include those with this distro_name (e.g. 'debian', 'rhel', 'ubuntu').",
|
||||
required=False,
|
||||
type=str,
|
||||
default="",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
matrix = []
|
||||
if args.config is None or args.config == "":
|
||||
matrix += generate_strategy_matrix(
|
||||
args.all, read_config(THIS_DIR / "linux.json")
|
||||
args.all, read_config(THIS_DIR / "linux.json"), args.distro
|
||||
)
|
||||
matrix += generate_strategy_matrix(
|
||||
args.all, read_config(THIS_DIR / "macos.json")
|
||||
args.all, read_config(THIS_DIR / "macos.json"), args.distro
|
||||
)
|
||||
matrix += generate_strategy_matrix(
|
||||
args.all, read_config(THIS_DIR / "windows.json")
|
||||
args.all, read_config(THIS_DIR / "windows.json"), args.distro
|
||||
)
|
||||
else:
|
||||
matrix += generate_strategy_matrix(args.all, read_config(args.config))
|
||||
matrix += generate_strategy_matrix(
|
||||
args.all, read_config(args.config), args.distro
|
||||
)
|
||||
|
||||
# Generate the strategy matrix.
|
||||
print(f"matrix={json.dumps({'include': matrix})}")
|
||||
# print(json.dumps(matrix, indent=2))
|
||||
|
||||
17
.github/workflows/on-pr.yml
vendored
17
.github/workflows/on-pr.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Determine changed files
|
||||
# This step checks whether any files have changed that should
|
||||
# cause the next jobs to run. We do it this way rather than
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# that Github considers any skipped jobs to have passed, and in
|
||||
# turn the required checks as well.
|
||||
id: changes
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
with:
|
||||
files: |
|
||||
# These paths are unique to `on-pr.yml`.
|
||||
@@ -128,12 +128,23 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [linux, macos, windows]
|
||||
include:
|
||||
- os: linux
|
||||
distro: debian
|
||||
- os: linux
|
||||
distro: rhel
|
||||
- os: linux
|
||||
distro: ubuntu
|
||||
- os: macos
|
||||
distro: ""
|
||||
- os: windows
|
||||
distro: ""
|
||||
with:
|
||||
# Enable ccache only for events targeting the XRPLF repository, since
|
||||
# other accounts will not have access to our remote cache storage.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' }}
|
||||
os: ${{ matrix.os }}
|
||||
distro: ${{ matrix.distro }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
13
.github/workflows/on-trigger.yml
vendored
13
.github/workflows/on-trigger.yml
vendored
@@ -77,7 +77,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: ${{ github.event_name == 'merge_group' }}
|
||||
matrix:
|
||||
os: [linux, macos, windows]
|
||||
include:
|
||||
- os: linux
|
||||
distro: debian
|
||||
- os: linux
|
||||
distro: rhel
|
||||
- os: linux
|
||||
distro: ubuntu
|
||||
- os: macos
|
||||
distro: ""
|
||||
- os: windows
|
||||
distro: ""
|
||||
with:
|
||||
# Enable ccache only for events targeting the XRPLF repository, since
|
||||
# other accounts will not have access to our remote cache storage.
|
||||
@@ -86,6 +96,7 @@ jobs:
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
os: ${{ matrix.os }}
|
||||
distro: ${{ matrix.distro }}
|
||||
strategy_matrix: ${{ github.event_name == 'schedule' && 'all' || 'minimal' }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
4
.github/workflows/pre-commit.yml
vendored
4
.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@320be44621ca2a080f05aeb15817c44b84518108
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@56de1bdf19639e009639a50b8d17c28ca954f267
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-ab4d1f0" }'
|
||||
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'
|
||||
|
||||
20
.github/workflows/publish-docs.yml
vendored
20
.github/workflows/publish-docs.yml
vendored
@@ -4,6 +4,18 @@ name: Build and publish documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
paths:
|
||||
- ".github/workflows/publish-docs.yml"
|
||||
- "*.md"
|
||||
- "**/*.md"
|
||||
- "docs/**"
|
||||
- "include/**"
|
||||
- "src/libxrpl/**"
|
||||
- "src/xrpld/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/publish-docs.yml"
|
||||
- "*.md"
|
||||
@@ -23,7 +35,9 @@ defaults:
|
||||
|
||||
env:
|
||||
BUILD_DIR: build
|
||||
NPROC_SUBTRACT: 2
|
||||
# ubuntu-latest has only 2 CPUs for private repositories
|
||||
# https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for--private-repositories
|
||||
NPROC_SUBTRACT: ${{ github.event.repository.private && '1' || '2' }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
@@ -33,7 +47,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Get number of processors
|
||||
uses: XRPLF/actions/get-nproc@cf0433aa74563aead044a1e395610c96d65a37cf
|
||||
@@ -65,7 +79,7 @@ jobs:
|
||||
cmake --build . --target docs --parallel ${BUILD_NPROC}
|
||||
|
||||
- name: Publish documentation
|
||||
if: ${{ github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch }}
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
46
.github/workflows/reusable-build-test-config.yml
vendored
46
.github/workflows/reusable-build-test-config.yml
vendored
@@ -76,8 +76,7 @@ jobs:
|
||||
name: ${{ inputs.config_name }}
|
||||
runs-on: ${{ fromJSON(inputs.runs_on) }}
|
||||
container: ${{ inputs.image != '' && inputs.image || null }}
|
||||
# Sanitizer builds on GCC are taking longer than 60mins. Hence increasing the timeout to 90mins.
|
||||
timeout-minutes: ${{ inputs.sanitizers != '' && 360 || 60 }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
# Use a namespace to keep the objects separate for each configuration.
|
||||
CCACHE_NAMESPACE: ${{ inputs.config_name }}
|
||||
@@ -102,10 +101,10 @@ jobs:
|
||||
steps:
|
||||
- name: Cleanup workspace (macOS and Windows)
|
||||
if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }}
|
||||
uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf
|
||||
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
|
||||
@@ -178,7 +177,7 @@ jobs:
|
||||
|
||||
- name: Upload the binary (Linux)
|
||||
if: ${{ github.repository_owner == 'XRPLF' && runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: xrpld-${{ inputs.config_name }}
|
||||
path: ${{ env.BUILD_DIR }}/xrpld
|
||||
@@ -206,22 +205,14 @@ jobs:
|
||||
- name: Set sanitizer options
|
||||
if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }}
|
||||
run: |
|
||||
ASAN_OPTS="include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-asan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp"
|
||||
if [[ "${{ inputs.config_name }}" == *gcc* ]]; then
|
||||
ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0"
|
||||
fi
|
||||
echo "ASAN_OPTIONS=${ASAN_OPTS}" >> ${GITHUB_ENV}
|
||||
echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >> ${GITHUB_ENV}
|
||||
echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >> ${GITHUB_ENV}
|
||||
echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >> ${GITHUB_ENV}
|
||||
echo "ASAN_OPTIONS=print_stacktrace=1:detect_container_overflow=0:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp" >> ${GITHUB_ENV}
|
||||
echo "TSAN_OPTIONS=second_deadlock_stack=1:halt_on_error=0:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >> ${GITHUB_ENV}
|
||||
echo "UBSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >> ${GITHUB_ENV}
|
||||
echo "LSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >> ${GITHUB_ENV}
|
||||
|
||||
- name: Run the separate tests
|
||||
# We continue on error here because we want to try the Embedded tests before
|
||||
# failing. This will give us details on all the failures at once.
|
||||
continue-on-error: true
|
||||
if: ${{ !inputs.build_only }}
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
id: separate_tests
|
||||
# Windows locks some of the build files while running tests, and parallel jobs can collide
|
||||
env:
|
||||
BUILD_TYPE: ${{ inputs.build_type }}
|
||||
@@ -237,15 +228,22 @@ jobs:
|
||||
working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
|
||||
env:
|
||||
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
|
||||
PARALLELISM: ${{ env.SANITIZERS_ENABLED == 'true' && steps.nproc.outputs.nproc || steps.nproc.outputs.nproc }}
|
||||
run: |
|
||||
./xrpld --unittest --unittest-jobs "${PARALLELISM}"
|
||||
set -o pipefail
|
||||
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
|
||||
|
||||
# Pipeline should fail if the separate tests failed.
|
||||
- name: Check results of the SeparateTests
|
||||
if: ${{ !inputs.build_only && steps.separate_tests.outcome == 'failure' }}
|
||||
run: exit 1
|
||||
- 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: |
|
||||
@@ -269,7 +267,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage report
|
||||
if: ${{ github.repository_owner == 'XRPLF' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
disable_search: true
|
||||
disable_telem: true
|
||||
|
||||
8
.github/workflows/reusable-build-test.yml
vendored
8
.github/workflows/reusable-build-test.yml
vendored
@@ -26,6 +26,12 @@ on:
|
||||
type: string
|
||||
default: "minimal"
|
||||
|
||||
distro:
|
||||
description: 'Filter to only include configs for this distro (e.g. "debian", "rhel", "ubuntu"). Leave empty for no filtering.'
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: "The Codecov token to use for uploading coverage reports."
|
||||
@@ -38,9 +44,11 @@ jobs:
|
||||
with:
|
||||
os: ${{ inputs.os }}
|
||||
strategy_matrix: ${{ inputs.strategy_matrix }}
|
||||
distro: ${{ inputs.distro }}
|
||||
|
||||
# Build and test the binary for each configuration.
|
||||
build-test-config:
|
||||
name: ${{ matrix.config_name }}
|
||||
needs:
|
||||
- generate-matrix
|
||||
uses: ./.github/workflows/reusable-build-test-config.yml
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Check levelization
|
||||
run: .github/scripts/levelization/generate.sh
|
||||
- name: Check for differences
|
||||
|
||||
2
.github/workflows/reusable-check-rename.yml
vendored
2
.github/workflows/reusable-check-rename.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Check definitions
|
||||
run: .github/scripts/rename/definitions.sh .
|
||||
- name: Check copyright notices
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
|
||||
@@ -78,13 +78,13 @@ jobs:
|
||||
id: run_clang_tidy
|
||||
continue-on-error: true
|
||||
env:
|
||||
FILES: ${{ inputs.files }}
|
||||
TARGETS: ${{ inputs.files != '' && inputs.files || 'src tests' }}
|
||||
run: |
|
||||
run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "$BUILD_DIR" $FILES 2>&1 | tee clang-tidy-output.txt
|
||||
run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" ${TARGETS} 2>&1 | tee clang-tidy-output.txt
|
||||
|
||||
- name: Upload clang-tidy output
|
||||
if: steps.run_clang_tidy.outcome != 'success'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: clang-tidy-results
|
||||
path: clang-tidy-output.txt
|
||||
|
||||
16
.github/workflows/reusable-clang-tidy.yml
vendored
16
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -22,7 +22,8 @@ jobs:
|
||||
if: ${{ inputs.check_only_changed }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed_files.outputs.any_changed }}
|
||||
clang_tidy_config_changed: ${{ steps.changed_clang_tidy.outputs.any_changed }}
|
||||
any_cpp_changed: ${{ steps.changed_files.outputs.any_changed }}
|
||||
all_changed_files: ${{ steps.changed_files.outputs.all_changed_files }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -30,7 +31,7 @@ jobs:
|
||||
|
||||
- name: Get changed C++ files
|
||||
id: changed_files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
with:
|
||||
files: |
|
||||
**/*.cpp
|
||||
@@ -38,10 +39,17 @@ jobs:
|
||||
**/*.ipp
|
||||
separator: " "
|
||||
|
||||
- name: Get changed clang-tidy configuration
|
||||
id: changed_clang_tidy
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
with:
|
||||
files: |
|
||||
.clang-tidy
|
||||
|
||||
run-clang-tidy:
|
||||
needs: [determine-files]
|
||||
if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_changed == 'true') }}
|
||||
if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.any_cpp_changed == 'true' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }}
|
||||
uses: ./.github/workflows/reusable-clang-tidy-files.yml
|
||||
with:
|
||||
files: ${{ inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '' }}
|
||||
files: ${{ (needs.determine-files.outputs.clang_tidy_config_changed == 'true' && '') || (inputs.check_only_changed && needs.determine-files.outputs.all_changed_files || '') }}
|
||||
create_issue_on_failure: ${{ inputs.create_issue_on_failure }}
|
||||
|
||||
12
.github/workflows/reusable-strategy-matrix.yml
vendored
12
.github/workflows/reusable-strategy-matrix.yml
vendored
@@ -13,6 +13,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: "minimal"
|
||||
distro:
|
||||
description: 'Filter to only include configs for this distro (e.g. "debian", "rhel", "ubuntu"). Leave empty for no filtering.'
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
outputs:
|
||||
matrix:
|
||||
description: "The generated strategy matrix."
|
||||
@@ -29,10 +34,10 @@ jobs:
|
||||
matrix: ${{ steps.generate.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
@@ -42,4 +47,5 @@ jobs:
|
||||
env:
|
||||
GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }}
|
||||
GENERATE_OPTION: ${{ inputs.strategy_matrix == 'all' && '--all' || '' }}
|
||||
run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >> "${GITHUB_OUTPUT}"
|
||||
GENERATE_DISTRO: ${{ inputs.distro != '' && format('--distro={0}', inputs.distro) || '' }}
|
||||
run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} ${GENERATE_DISTRO} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
2
.github/workflows/reusable-upload-recipe.yml
vendored
2
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13-sha-5dd7158
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate build version number
|
||||
id: version
|
||||
|
||||
4
.github/workflows/upload-conan-deps.yml
vendored
4
.github/workflows/upload-conan-deps.yml
vendored
@@ -64,10 +64,10 @@ jobs:
|
||||
steps:
|
||||
- name: Cleanup workspace (macOS and Windows)
|
||||
if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }}
|
||||
uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf
|
||||
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -42,6 +42,9 @@ gmon.out
|
||||
# Locally patched Conan recipes
|
||||
external/conan-center-index/
|
||||
|
||||
# Local conan directory
|
||||
.conan
|
||||
|
||||
# XCode IDE.
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
@@ -72,5 +75,8 @@ DerivedData
|
||||
/.claude
|
||||
/CLAUDE.md
|
||||
|
||||
# Direnv's directory
|
||||
/.direnv
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
@@ -57,6 +57,24 @@ repos:
|
||||
- .git/COMMIT_EDITMSG
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: nix-fmt
|
||||
name: Format Nix files
|
||||
entry: |
|
||||
bash -c '
|
||||
if command -v nix &> /dev/null || [ "$GITHUB_ACTIONS" = "true" ]; then
|
||||
nix --extra-experimental-features "nix-command flakes" fmt "$@"
|
||||
else
|
||||
echo "Skipping nix-fmt: nix not installed and not in GitHub Actions"
|
||||
exit 0
|
||||
fi
|
||||
' --
|
||||
language: system
|
||||
types:
|
||||
- nix
|
||||
pass_filenames: true
|
||||
|
||||
exclude: |
|
||||
(?x)^(
|
||||
external/.*|
|
||||
|
||||
@@ -251,6 +251,29 @@ pip3 install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## Clang-tidy
|
||||
|
||||
All code must pass `clang-tidy` checks according to the settings in [`.clang-tidy`](./.clang-tidy).
|
||||
|
||||
There is a Continuous Integration job that runs clang-tidy on pull requests. The CI will check:
|
||||
|
||||
- All changed C++ files (`.cpp`, `.h`, `.ipp`) when only code files are modified
|
||||
- **All files in the repository** when the `.clang-tidy` configuration file is changed
|
||||
|
||||
This ensures that configuration changes don't introduce new warnings across the codebase.
|
||||
|
||||
### Running clang-tidy locally
|
||||
|
||||
Before running clang-tidy, you must build the project to generate required files (particularly protobuf headers). Refer to [`BUILD.md`](./BUILD.md) for build instructions.
|
||||
|
||||
Then run clang-tidy on your local changes:
|
||||
|
||||
```
|
||||
run-clang-tidy -p build src tests
|
||||
```
|
||||
|
||||
This will check all source files in the `src` and `tests` directories using the compile commands from your `build` directory.
|
||||
|
||||
## Contracts and instrumentation
|
||||
|
||||
We are using [Antithesis](https://antithesis.com/) for continuous fuzzing,
|
||||
|
||||
@@ -17,10 +17,12 @@ find_dependency(Boost
|
||||
chrono
|
||||
container
|
||||
context
|
||||
coroutine
|
||||
date_time
|
||||
filesystem
|
||||
program_options
|
||||
regex
|
||||
system
|
||||
thread)
|
||||
#[=========================================================[
|
||||
OpenSSL
|
||||
|
||||
@@ -22,7 +22,7 @@ target_compile_definitions(
|
||||
BOOST_FILESYSTEM_NO_DEPRECATED
|
||||
>
|
||||
$<$<NOT:$<BOOL:${boost_show_deprecated}>>:
|
||||
BOOST_COROUTINES2_NO_DEPRECATION_WARNING
|
||||
BOOST_COROUTINES_NO_DEPRECATION_WARNING
|
||||
BOOST_BEAST_ALLOW_DEPRECATED
|
||||
BOOST_FILESYSTEM_DEPRECATED
|
||||
>
|
||||
|
||||
@@ -4,12 +4,13 @@ include(XrplSanitizers)
|
||||
find_package(Boost REQUIRED
|
||||
COMPONENTS chrono
|
||||
container
|
||||
context
|
||||
coroutine
|
||||
date_time
|
||||
filesystem
|
||||
json
|
||||
program_options
|
||||
regex
|
||||
system
|
||||
thread)
|
||||
|
||||
add_library(xrpl_boost INTERFACE)
|
||||
@@ -20,7 +21,7 @@ target_link_libraries(
|
||||
INTERFACE Boost::headers
|
||||
Boost::chrono
|
||||
Boost::container
|
||||
Boost::context
|
||||
Boost::coroutine
|
||||
Boost::date_time
|
||||
Boost::filesystem
|
||||
Boost::json
|
||||
@@ -31,26 +32,6 @@ target_link_libraries(
|
||||
if (Boost_COMPILER)
|
||||
target_link_libraries(xrpl_boost INTERFACE Boost::disable_autolinking)
|
||||
endif ()
|
||||
|
||||
# GCC 14+ has a false positive -Wuninitialized warning in Boost.Coroutine2's
|
||||
# state.hpp when compiled with -O3. This is due to GCC's intentional behavior
|
||||
# change (Bug #98871, #119388) where warnings from inlined system header code
|
||||
# are no longer suppressed by -isystem. The warning occurs in operator|= in
|
||||
# boost/coroutine2/detail/state.hpp when inlined from push_control_block::destroy().
|
||||
# See: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119388
|
||||
if (is_gcc AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 14)
|
||||
target_compile_options(xrpl_boost INTERFACE -Wno-uninitialized)
|
||||
endif ()
|
||||
|
||||
# Boost.Context's ucontext backend has ASAN fiber-switching annotations
|
||||
# (start/finish_switch_fiber) that are compiled in when BOOST_USE_ASAN is defined.
|
||||
# This tells ASAN about coroutine stack switches, preventing false positive
|
||||
# stack-use-after-scope errors. BOOST_USE_UCONTEXT ensures the ucontext backend
|
||||
# is selected (fcontext does not support ASAN annotations).
|
||||
# These defines must match what Boost was compiled with (see conan/profiles/sanitizers).
|
||||
if (enable_asan)
|
||||
target_compile_definitions(xrpl_boost INTERFACE BOOST_USE_ASAN BOOST_USE_UCONTEXT)
|
||||
endif ()
|
||||
if (SANITIZERS_ENABLED AND is_clang)
|
||||
# TODO: gcc does not support -fsanitize-blacklist...can we do something else for gcc ?
|
||||
if (NOT Boost_INCLUDE_DIRS AND TARGET Boost::headers)
|
||||
|
||||
@@ -7,21 +7,16 @@ include(default)
|
||||
{% if compiler == "gcc" %}
|
||||
{% if "address" in sanitizers or "thread" in sanitizers or "undefinedbehavior" in sanitizers %}
|
||||
{% set sanitizer_list = [] %}
|
||||
{% set defines = [] %}
|
||||
{% set model_code = "" %}
|
||||
{% set extra_cxxflags = ["-fno-omit-frame-pointer", "-O1", "-Wno-stringop-overflow"] %}
|
||||
|
||||
{% if "address" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("address") %}
|
||||
{% set model_code = "-mcmodel=large" %}
|
||||
{% set _ = defines.append("BOOST_USE_ASAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% elif "thread" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("thread") %}
|
||||
{% set model_code = "-mcmodel=medium" %}
|
||||
{% set _ = extra_cxxflags.append("-Wno-tsan") %}
|
||||
{% set _ = defines.append("BOOST_USE_TSAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% endif %}
|
||||
|
||||
{% if "undefinedbehavior" in sanitizers %}
|
||||
@@ -34,22 +29,16 @@ include(default)
|
||||
tools.build:cxxflags+=['{{sanitizer_flags}} {{" ".join(extra_cxxflags)}}']
|
||||
tools.build:sharedlinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:exelinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:defines+={{defines}}
|
||||
{% endif %}
|
||||
{% elif compiler == "apple-clang" or compiler == "clang" %}
|
||||
{% if "address" in sanitizers or "thread" in sanitizers or "undefinedbehavior" in sanitizers %}
|
||||
{% set sanitizer_list = [] %}
|
||||
{% set defines = [] %}
|
||||
{% set extra_cxxflags = ["-fno-omit-frame-pointer", "-O1"] %}
|
||||
|
||||
{% if "address" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("address") %}
|
||||
{% set _ = defines.append("BOOST_USE_ASAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% elif "thread" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("thread") %}
|
||||
{% set _ = defines.append("BOOST_USE_TSAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% endif %}
|
||||
|
||||
{% if "undefinedbehavior" in sanitizers %}
|
||||
@@ -63,24 +52,8 @@ include(default)
|
||||
tools.build:cxxflags+=['{{sanitizer_flags}} {{" ".join(extra_cxxflags)}}']
|
||||
tools.build:sharedlinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:exelinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:defines+={{defines}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"]
|
||||
|
||||
[options]
|
||||
{% if sanitizers %}
|
||||
{% if "address" in sanitizers %}
|
||||
# Build Boost.Context with ucontext backend (not fcontext) so that
|
||||
# ASAN fiber-switching annotations (__sanitizer_start/finish_switch_fiber)
|
||||
# are compiled into the library. fcontext (assembly) has no ASAN support.
|
||||
# define=BOOST_USE_ASAN=1 is critical: it must be defined when building
|
||||
# Boost.Context itself so the ucontext backend compiles in the ASAN annotations.
|
||||
boost/*:extra_b2_flags=context-impl=ucontext address-sanitizer=on define=BOOST_USE_ASAN=1
|
||||
boost/*:without_context=False
|
||||
# Boost stacktrace fails to build with some sanitizers
|
||||
boost/*:without_stacktrace=True
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags"]
|
||||
|
||||
15
conanfile.py
15
conanfile.py
@@ -1,5 +1,4 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
|
||||
|
||||
@@ -58,9 +57,6 @@ class Xrpl(ConanFile):
|
||||
"tests": False,
|
||||
"unity": False,
|
||||
"xrpld": False,
|
||||
"boost/*:without_context": False,
|
||||
"boost/*:without_coroutine": True,
|
||||
"boost/*:without_coroutine2": False,
|
||||
"date/*:header_only": True,
|
||||
"ed25519/*:shared": False,
|
||||
"grpc/*:shared": False,
|
||||
@@ -129,14 +125,6 @@ class Xrpl(ConanFile):
|
||||
self.options["boost"].visibility = "global"
|
||||
if self.settings.compiler in ["clang", "gcc"]:
|
||||
self.options["boost"].without_cobalt = True
|
||||
self.options["boost"].without_context = False
|
||||
self.options["boost"].without_coroutine = True
|
||||
self.options["boost"].without_coroutine2 = False
|
||||
# Check if environment variable exists
|
||||
if "SANITIZERS" in os.environ:
|
||||
sanitizers = os.environ["SANITIZERS"]
|
||||
if "address" in sanitizers.lower():
|
||||
self.default_options["fPIC"] = False
|
||||
|
||||
def requirements(self):
|
||||
# Conan 2 requires transitive headers to be specified
|
||||
@@ -208,8 +196,7 @@ class Xrpl(ConanFile):
|
||||
"boost::headers",
|
||||
"boost::chrono",
|
||||
"boost::container",
|
||||
"boost::context",
|
||||
"boost::coroutine2",
|
||||
"boost::coroutine",
|
||||
"boost::date_time",
|
||||
"boost::filesystem",
|
||||
"boost::json",
|
||||
|
||||
@@ -99,10 +99,8 @@ words:
|
||||
- endmacro
|
||||
- exceptioned
|
||||
- Falco
|
||||
- fcontext
|
||||
- finalizers
|
||||
- firewalled
|
||||
- flackiness
|
||||
- fmtdur
|
||||
- fsanitize
|
||||
- funclets
|
||||
@@ -113,7 +111,6 @@ words:
|
||||
- gpgcheck
|
||||
- gpgkey
|
||||
- hotwallet
|
||||
- hwaddress
|
||||
- hwrap
|
||||
- ifndef
|
||||
- inequation
|
||||
@@ -176,8 +173,10 @@ words:
|
||||
- nftokens
|
||||
- nftpage
|
||||
- nikb
|
||||
- nixfmt
|
||||
- nixos
|
||||
- nixpkgs
|
||||
- nonxrp
|
||||
- norecover
|
||||
- noripple
|
||||
- nudb
|
||||
- nullptr
|
||||
@@ -237,7 +236,6 @@ words:
|
||||
- soci
|
||||
- socidb
|
||||
- sslws
|
||||
- stackful
|
||||
- statsd
|
||||
- STATSDCOLLECTOR
|
||||
- stissue
|
||||
|
||||
2
docs/build/environment.md
vendored
2
docs/build/environment.md
vendored
@@ -3,6 +3,8 @@ environment complete with Git, Python, Conan, CMake, and a C++ compiler.
|
||||
This document exists to help readers set one up on any of the Big Three
|
||||
platforms: Linux, macOS, or Windows.
|
||||
|
||||
As an alternative to system packages, the Nix development shell can be used to provide a development environment. See [using nix development shell](./nix.md) for more details.
|
||||
|
||||
[BUILD.md]: ../../BUILD.md
|
||||
|
||||
## Linux
|
||||
|
||||
95
docs/build/nix.md
vendored
Normal file
95
docs/build/nix.md
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
# Using Nix Development Shell for xrpld Development
|
||||
|
||||
This guide explains how to use Nix to set up a reproducible development environment for xrpld. Using Nix eliminates the need to manually install utilities and ensures consistent tooling across different machines.
|
||||
|
||||
## Benefits of Using Nix
|
||||
|
||||
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
|
||||
- **No system pollution**: Dependencies are isolated and don't affect your system packages
|
||||
- **Multiple compiler versions**: Easily switch between different GCC and Clang versions
|
||||
- **Quick setup**: Get started with a single command
|
||||
- **Works on Linux and macOS**: Consistent experience across platforms
|
||||
|
||||
## Install Nix
|
||||
|
||||
Please follow [the official installation instructions of nix package manager](https://nixos.org/download/) for your system.
|
||||
|
||||
## Entering the Development Shell
|
||||
|
||||
### Basic Usage
|
||||
|
||||
From the root of the xrpld repository, enter the default development shell:
|
||||
|
||||
```bash
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
- Configure the appropriate compiler for your platform:
|
||||
- **macOS**: Apple Clang (default system compiler)
|
||||
- **Linux**: GCC 15
|
||||
|
||||
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
|
||||
|
||||
> [!TIP]
|
||||
> To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`:
|
||||
>
|
||||
> ```bash
|
||||
> mkdir -p ~/.config/nix
|
||||
> echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
|
||||
> ```
|
||||
>
|
||||
> After this, you can simply use `nix develop` instead.
|
||||
|
||||
> [!NOTE]
|
||||
> The examples below assume you've enabled flakes in your config. If you haven't, add `--experimental-features 'nix-command flakes'` after each `nix` command.
|
||||
|
||||
### Choosing a different compiler
|
||||
|
||||
A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`.
|
||||
Use `nix flake show` to see all the available development shells.
|
||||
|
||||
Use `nix develop .#no_compiler` to use the compiler from your system.
|
||||
|
||||
### Example Usage
|
||||
|
||||
```bash
|
||||
# Use GCC 14
|
||||
nix develop .#gcc14
|
||||
|
||||
# Use Clang 19
|
||||
nix develop .#clang19
|
||||
|
||||
# Use default for your platform
|
||||
nix develop
|
||||
```
|
||||
|
||||
### Using a different shell
|
||||
|
||||
`nix develop` opens bash by default. If you want to use another shell this could be done by adding `-c` flag. For example:
|
||||
|
||||
```bash
|
||||
nix develop -c zsh
|
||||
```
|
||||
|
||||
## Building xrpld with Nix
|
||||
|
||||
Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.).
|
||||
|
||||
## Automatic Activation with direnv
|
||||
|
||||
[direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory.
|
||||
|
||||
## Conan and Prebuilt Packages
|
||||
|
||||
Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source:
|
||||
|
||||
```bash
|
||||
conan install .. --output-folder . --build '*' --settings build_type=Release
|
||||
```
|
||||
|
||||
## Updating `flake.lock` file
|
||||
|
||||
To update `flake.lock` to the latest revision use `nix flake update` command.
|
||||
8
docs/build/sanitizers.md
vendored
8
docs/build/sanitizers.md
vendored
@@ -89,8 +89,8 @@ cmake --build . --parallel 4
|
||||
**IMPORTANT**: ASAN with Boost produces many false positives. Use these options:
|
||||
|
||||
```bash
|
||||
export ASAN_OPTIONS="include=sanitizers/suppressions/runtime-asan-options.txt:suppressions=sanitizers/suppressions/asan.supp"
|
||||
export LSAN_OPTIONS="include=sanitizers/suppressions/runtime-lsan-options.txt:suppressions=sanitizers/suppressions/lsan.supp"
|
||||
export ASAN_OPTIONS="print_stacktrace=1:detect_container_overflow=0:suppressions=path/to/asan.supp:halt_on_error=0:log_path=asan.log"
|
||||
export LSAN_OPTIONS="suppressions=path/to/lsan.supp:halt_on_error=0:log_path=lsan.log"
|
||||
|
||||
# Run tests
|
||||
./xrpld --unittest --unittest-jobs=5
|
||||
@@ -108,7 +108,7 @@ export LSAN_OPTIONS="include=sanitizers/suppressions/runtime-lsan-options.txt:su
|
||||
### ThreadSanitizer (TSan)
|
||||
|
||||
```bash
|
||||
export TSAN_OPTIONS="include=sanitizers/suppressions/runtime-tsan-options.txt:suppressions=sanitizers/suppressions/tsan.supp"
|
||||
export TSAN_OPTIONS="suppressions=path/to/tsan.supp halt_on_error=0 log_path=tsan.log"
|
||||
|
||||
# Run tests
|
||||
./xrpld --unittest --unittest-jobs=5
|
||||
@@ -129,7 +129,7 @@ More details [here](https://github.com/google/sanitizers/wiki/AddressSanitizerLe
|
||||
### UndefinedBehaviorSanitizer (UBSan)
|
||||
|
||||
```bash
|
||||
export UBSAN_OPTIONS="include=sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=sanitizers/suppressions/ubsan.supp"
|
||||
export UBSAN_OPTIONS="suppressions=path/to/ubsan.supp:print_stacktrace=1:halt_on_error=0:log_path=ubsan.log"
|
||||
|
||||
# Run tests
|
||||
./xrpld --unittest --unittest-jobs=5
|
||||
|
||||
26
flake.lock
generated
Normal file
26
flake.lock
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769461804,
|
||||
"narHash": "sha256-6h5sROT/3CTHvzPy9koKBmoCa2eJKh4fzQK8eYFEgl8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b579d443b37c9c5373044201ea77604e37e748c8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
16
flake.nix
Normal file
16
flake.nix
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
description = "Nix related things for xrpld";
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }:
|
||||
let
|
||||
forEachSystem = (import ./nix/utils.nix { inherit nixpkgs; }).forEachSystem;
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (import ./nix/devshell.nix);
|
||||
formatter = forEachSystem ({ pkgs, ... }: pkgs.nixfmt);
|
||||
};
|
||||
}
|
||||
73
include/xrpl/basics/MallocTrim.h
Normal file
73
include/xrpl/basics/MallocTrim.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#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
|
||||
@@ -9,10 +9,6 @@
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
#endif // !defined(_MSC_VER)
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class Number;
|
||||
@@ -20,37 +16,18 @@ class Number;
|
||||
std::string
|
||||
to_string(Number const& amount);
|
||||
|
||||
/** Returns a rough estimate of log10(value).
|
||||
*
|
||||
* The return value is a pair (log, rem), where log is the estimated log10,
|
||||
* and rem is value divided by 10^log. If rem is 1, then value is an exact
|
||||
* power of ten, and log is the exact log10(value).
|
||||
*
|
||||
* This function only works for positive values.
|
||||
*/
|
||||
template <typename T>
|
||||
constexpr std::pair<int, T>
|
||||
logTenEstimate(T value)
|
||||
{
|
||||
int log = 0;
|
||||
T remainder = value;
|
||||
while (value >= 10)
|
||||
{
|
||||
if (value % 10 == 0)
|
||||
remainder = remainder / 10;
|
||||
value /= 10;
|
||||
++log;
|
||||
}
|
||||
return {log, remainder};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr std::optional<int>
|
||||
logTen(T value)
|
||||
{
|
||||
auto const est = logTenEstimate(value);
|
||||
if (est.second == 1)
|
||||
return est.first;
|
||||
int log = 0;
|
||||
while (value >= 10 && value % 10 == 0)
|
||||
{
|
||||
value /= 10;
|
||||
++log;
|
||||
}
|
||||
if (value == 1)
|
||||
return log;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -64,10 +41,12 @@ isPowerOfTen(T value)
|
||||
/** MantissaRange defines a range for the mantissa of a normalized Number.
|
||||
*
|
||||
* The mantissa is in the range [min, max], where
|
||||
* * min is a power of 10, and
|
||||
* * max = min * 10 - 1.
|
||||
*
|
||||
* The mantissa_scale enum indicates whether the range is "small" or "large".
|
||||
* This intentionally restricts the number of MantissaRanges that can be
|
||||
* used to two: one for each scale.
|
||||
* instantiated to two: one for each scale.
|
||||
*
|
||||
* The "small" scale is based on the behavior of STAmount for IOUs. It has a min
|
||||
* value of 10^15, and a max value of 10^16-1. This was sufficient for
|
||||
@@ -81,8 +60,8 @@ isPowerOfTen(T value)
|
||||
* "large" scale.
|
||||
*
|
||||
* The "large" scale is intended to represent all values that can be represented
|
||||
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 2^63/10+1
|
||||
* (truncated), and a max value of 2^63-1.
|
||||
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
|
||||
* value of 10^19-1.
|
||||
*
|
||||
* Note that if the mentioned amendments are eventually retired, this class
|
||||
* should be left in place, but the "small" scale option should be removed. This
|
||||
@@ -94,50 +73,25 @@ struct MantissaRange
|
||||
enum mantissa_scale { small, large };
|
||||
|
||||
explicit constexpr MantissaRange(mantissa_scale scale_)
|
||||
: max(getMax(scale_))
|
||||
, min(computeMin(max))
|
||||
, referenceMin(getReferenceMin(scale_, min))
|
||||
, log(computeLog(min))
|
||||
, scale(scale_)
|
||||
: min(getMin(scale_)), max(min * 10 - 1), log(logTen(min).value_or(-1)), scale(scale_)
|
||||
{
|
||||
// Since this is constexpr, if any of these throw, it won't compile
|
||||
if (min * 10 <= max)
|
||||
throw std::out_of_range("min * 10 <= max");
|
||||
if (max / 10 >= min)
|
||||
throw std::out_of_range("max / 10 >= min");
|
||||
if ((min - 1) * 10 > max)
|
||||
throw std::out_of_range("(min - 1) * 10 > max");
|
||||
// This is a little hacky
|
||||
if ((max + 10) / 10 < min)
|
||||
throw std::out_of_range("(max + 10) / 10 < min");
|
||||
}
|
||||
|
||||
// Explicitly delete copy and move operations
|
||||
MantissaRange(MantissaRange const&) = delete;
|
||||
MantissaRange(MantissaRange&&) = delete;
|
||||
MantissaRange&
|
||||
operator=(MantissaRange const&) = delete;
|
||||
MantissaRange&
|
||||
operator=(MantissaRange&&) = delete;
|
||||
|
||||
rep max;
|
||||
rep min;
|
||||
// This is not a great name. Used to determine if mantissas are in range,
|
||||
// but have fewer digits than max
|
||||
rep referenceMin;
|
||||
rep max;
|
||||
int log;
|
||||
mantissa_scale scale;
|
||||
|
||||
private:
|
||||
static constexpr rep
|
||||
getMax(mantissa_scale scale)
|
||||
getMin(mantissa_scale scale_)
|
||||
{
|
||||
switch (scale)
|
||||
switch (scale_)
|
||||
{
|
||||
case small:
|
||||
return 9'999'999'999'999'999ULL;
|
||||
return 1'000'000'000'000'000ULL;
|
||||
case large:
|
||||
return std::numeric_limits<std::int64_t>::max();
|
||||
return 1'000'000'000'000'000'000ULL;
|
||||
default:
|
||||
// Since this can never be called outside a non-constexpr
|
||||
// context, this throw assures that the build fails if an
|
||||
@@ -145,52 +99,12 @@ private:
|
||||
throw std::runtime_error("Unknown mantissa scale");
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr rep
|
||||
computeMin(rep max)
|
||||
{
|
||||
return max / 10 + 1;
|
||||
}
|
||||
|
||||
static constexpr rep
|
||||
getReferenceMin(mantissa_scale scale, rep min)
|
||||
{
|
||||
switch (scale)
|
||||
{
|
||||
case large:
|
||||
return 1'000'000'000'000'000'000ULL;
|
||||
default:
|
||||
if (isPowerOfTen(min))
|
||||
return min;
|
||||
throw std::runtime_error("Unknown/bad mantissa scale");
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr rep
|
||||
computeLog(rep min)
|
||||
{
|
||||
auto const estimate = logTenEstimate(min);
|
||||
return estimate.first + (estimate.second == 1 ? 0 : 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Like std::integral, but only 64-bit integral types.
|
||||
template <class T>
|
||||
concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>;
|
||||
|
||||
namespace detail {
|
||||
#ifdef _MSC_VER
|
||||
using uint128_t = boost::multiprecision::uint128_t;
|
||||
using int128_t = boost::multiprecision::int128_t;
|
||||
#else // !defined(_MSC_VER)
|
||||
using uint128_t = __uint128_t;
|
||||
using int128_t = __int128_t;
|
||||
#endif // !defined(_MSC_VER)
|
||||
|
||||
template <class T>
|
||||
concept UnsignedMantissa = std::is_unsigned_v<T> || std::is_same_v<T, uint128_t>;
|
||||
} // namespace detail
|
||||
|
||||
/** Number is a floating point type that can represent a wide range of values.
|
||||
*
|
||||
* It can represent all values that can be represented by an STAmount -
|
||||
@@ -218,7 +132,9 @@ concept UnsignedMantissa = std::is_unsigned_v<T> || std::is_same_v<T, uint128_t>
|
||||
* 1. Normalization can be disabled by using the "unchecked" ctor tag. This
|
||||
* should only be used at specific conversion points, some constexpr
|
||||
* values, and in unit tests.
|
||||
* 2. The max of the "large" range, 2^63-1, TODO: explain the large range.
|
||||
* 2. The max of the "large" range, 10^19-1, is the largest 10^X-1 value that
|
||||
* fits in an unsigned 64-bit number. (10^19-1 < 2^64-1 and
|
||||
* 10^20-1 > 2^64-1). This avoids under- and overflows.
|
||||
*
|
||||
* ---- External Interface ----
|
||||
*
|
||||
@@ -232,7 +148,7 @@ concept UnsignedMantissa = std::is_unsigned_v<T> || std::is_same_v<T, uint128_t>
|
||||
*
|
||||
* Note:
|
||||
* 1. 2^63-1 is between 10^18 and 10^19-1, which are the limits of the "large"
|
||||
* mantissa range. TODO: update this explanation.
|
||||
* mantissa range.
|
||||
* 2. The functions mantissa() and exponent() return the external view of the
|
||||
* Number value, specifically using a signed 63-bit mantissa. This may
|
||||
* require altering the internal representation to fit into that range
|
||||
@@ -292,7 +208,8 @@ class Number
|
||||
using rep = std::int64_t;
|
||||
using internalrep = MantissaRange::rep;
|
||||
|
||||
rep mantissa_{0};
|
||||
bool negative_{false};
|
||||
internalrep mantissa_{0};
|
||||
int exponent_{std::numeric_limits<int>::lowest()};
|
||||
|
||||
public:
|
||||
@@ -300,11 +217,9 @@ public:
|
||||
constexpr static int minExponent = -32768;
|
||||
constexpr static int maxExponent = 32768;
|
||||
|
||||
#if MAXREP
|
||||
constexpr static internalrep maxRep = std::numeric_limits<rep>::max();
|
||||
static_assert(maxRep == 9'223'372'036'854'775'807);
|
||||
static_assert(-maxRep == std::numeric_limits<rep>::min() + 1);
|
||||
#endif
|
||||
|
||||
// May need to make unchecked private
|
||||
struct unchecked
|
||||
@@ -383,7 +298,8 @@ public:
|
||||
friend constexpr bool
|
||||
operator==(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
return x.mantissa_ == y.mantissa_ && x.exponent_ == y.exponent_;
|
||||
return x.negative_ == y.negative_ && x.mantissa_ == y.mantissa_ &&
|
||||
x.exponent_ == y.exponent_;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
@@ -397,8 +313,8 @@ public:
|
||||
{
|
||||
// If the two amounts have different signs (zero is treated as positive)
|
||||
// then the comparison is true iff the left is negative.
|
||||
bool const lneg = x.mantissa_ < 0;
|
||||
bool const rneg = y.mantissa_ < 0;
|
||||
bool const lneg = x.negative_;
|
||||
bool const rneg = y.negative_;
|
||||
|
||||
if (lneg != rneg)
|
||||
return lneg;
|
||||
@@ -426,7 +342,7 @@ public:
|
||||
constexpr int
|
||||
signum() const noexcept
|
||||
{
|
||||
return mantissa_ < 0 ? -1 : (mantissa_ ? 1 : 0);
|
||||
return negative_ ? -1 : (mantissa_ ? 1 : 0);
|
||||
}
|
||||
|
||||
Number
|
||||
@@ -465,9 +381,6 @@ public:
|
||||
friend Number
|
||||
root2(Number f);
|
||||
|
||||
friend Number
|
||||
power(Number const& f, unsigned n, unsigned d);
|
||||
|
||||
// Thread local rounding control. Default is to_nearest
|
||||
enum rounding_mode { to_nearest, towards_zero, downward, upward };
|
||||
static rounding_mode
|
||||
@@ -532,48 +445,22 @@ private:
|
||||
static_assert(isPowerOfTen(smallRange.min));
|
||||
static_assert(smallRange.min == 1'000'000'000'000'000LL);
|
||||
static_assert(smallRange.max == 9'999'999'999'999'999LL);
|
||||
static_assert(smallRange.referenceMin == smallRange.min);
|
||||
static_assert(smallRange.log == 15);
|
||||
#if MAXREP
|
||||
static_assert(smallRange.min < maxRep);
|
||||
static_assert(smallRange.max < maxRep);
|
||||
#endif
|
||||
constexpr static MantissaRange largeRange{MantissaRange::large};
|
||||
static_assert(!isPowerOfTen(largeRange.min));
|
||||
static_assert(largeRange.min == 922'337'203'685'477'581ULL);
|
||||
static_assert(largeRange.max == internalrep(9'223'372'036'854'775'807ULL));
|
||||
static_assert(largeRange.max == std::numeric_limits<rep>::max());
|
||||
static_assert(largeRange.referenceMin == 1'000'000'000'000'000'000ULL);
|
||||
static_assert(isPowerOfTen(largeRange.min));
|
||||
static_assert(largeRange.min == 1'000'000'000'000'000'000ULL);
|
||||
static_assert(largeRange.max == internalrep(9'999'999'999'999'999'999ULL));
|
||||
static_assert(largeRange.log == 18);
|
||||
// There are 2 values that will not fit in largeRange without some extra
|
||||
// work
|
||||
// * 9223372036854775808
|
||||
// * 9223372036854775809
|
||||
// They both end up < min, but with a leftover. If they round up, everything
|
||||
// will be fine. If they don't, well need to bring them up into range.
|
||||
// Guard::bringIntoRange handles this situation.
|
||||
|
||||
#if MAXREP
|
||||
static_assert(largeRange.min < maxRep);
|
||||
static_assert(largeRange.max > maxRep);
|
||||
#endif
|
||||
|
||||
// The range for the mantissa when normalized.
|
||||
// Use reference_wrapper to avoid making copies, and prevent accidentally
|
||||
// changing the values inside the range.
|
||||
static thread_local std::reference_wrapper<MantissaRange const> range_;
|
||||
|
||||
// And one is needed because it needs to choose between oneSmall and
|
||||
// oneLarge based on the current range
|
||||
static Number
|
||||
one(MantissaRange const& range);
|
||||
|
||||
static Number
|
||||
root(MantissaRange const& range, Number f, unsigned d);
|
||||
|
||||
void
|
||||
normalize(MantissaRange const& range);
|
||||
|
||||
void
|
||||
normalize();
|
||||
|
||||
@@ -596,14 +483,11 @@ private:
|
||||
friend void
|
||||
doNormalize(
|
||||
bool& negative,
|
||||
T& mantissa,
|
||||
int& exponent,
|
||||
T& mantissa_,
|
||||
int& exponent_,
|
||||
MantissaRange::rep const& minMantissa,
|
||||
MantissaRange::rep const& maxMantissa);
|
||||
|
||||
bool
|
||||
isnormal(MantissaRange const& range) const noexcept;
|
||||
|
||||
bool
|
||||
isnormal() const noexcept;
|
||||
|
||||
@@ -620,56 +504,7 @@ private:
|
||||
static internalrep
|
||||
externalToInternal(rep mantissa);
|
||||
|
||||
/** Breaks down the number into components, potentially de-normalizing it.
|
||||
*
|
||||
* Ensures that the mantissa always has range_.log + 1 digits.
|
||||
*
|
||||
*/
|
||||
template <detail::UnsignedMantissa Rep = internalrep>
|
||||
std::tuple<bool, Rep, int>
|
||||
toInternal(MantissaRange const& range) const;
|
||||
|
||||
/** Breaks down the number into components, potentially de-normalizing it.
|
||||
*
|
||||
* Ensures that the mantissa always has range_.log + 1 digits.
|
||||
*
|
||||
*/
|
||||
template <detail::UnsignedMantissa Rep = internalrep>
|
||||
std::tuple<bool, Rep, int>
|
||||
toInternal() const;
|
||||
|
||||
/** Rebuilds the number from components.
|
||||
*
|
||||
* If "normalized" is true, the values are expected to be normalized - all
|
||||
* in their valid ranges.
|
||||
*
|
||||
* If "normalized" is false, the values are expected to be "near
|
||||
* normalized", meaning that the mantissa has to be modified at most once to
|
||||
* bring it back into range.
|
||||
*
|
||||
*/
|
||||
template <bool expectNormal = true, detail::UnsignedMantissa Rep = internalrep>
|
||||
void
|
||||
fromInternal(bool negative, Rep mantissa, int exponent, MantissaRange const* pRange);
|
||||
|
||||
/** Rebuilds the number from components.
|
||||
*
|
||||
* If "normalized" is true, the values are expected to be normalized - all
|
||||
* in their valid ranges.
|
||||
*
|
||||
* If "normalized" is false, the values are expected to be "near
|
||||
* normalized", meaning that the mantissa has to be modified at most once to
|
||||
* bring it back into range.
|
||||
*
|
||||
*/
|
||||
template <bool expectNormal = true, detail::UnsignedMantissa Rep = internalrep>
|
||||
void
|
||||
fromInternal(bool negative, Rep mantissa, int exponent);
|
||||
|
||||
class Guard;
|
||||
|
||||
public:
|
||||
constexpr static internalrep largestMantissa = largeRange.max;
|
||||
};
|
||||
|
||||
inline constexpr Number::Number(
|
||||
@@ -677,7 +512,7 @@ inline constexpr Number::Number(
|
||||
internalrep mantissa,
|
||||
int exponent,
|
||||
unchecked) noexcept
|
||||
: mantissa_{(negative ? -1 : 1) * static_cast<rep>(mantissa)}, exponent_{exponent}
|
||||
: negative_(negative), mantissa_{mantissa}, exponent_{exponent}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -688,6 +523,12 @@ inline constexpr Number::Number(internalrep mantissa, int exponent, unchecked) n
|
||||
|
||||
constexpr static Number numZero{};
|
||||
|
||||
inline Number::Number(bool negative, internalrep mantissa, int exponent, normalized)
|
||||
: Number(negative, mantissa, exponent, unchecked{})
|
||||
{
|
||||
normalize();
|
||||
}
|
||||
|
||||
inline Number::Number(internalrep mantissa, int exponent, normalized)
|
||||
: Number(false, mantissa, exponent, normalized{})
|
||||
{
|
||||
@@ -710,7 +551,17 @@ inline Number::Number(rep mantissa) : Number{mantissa, 0}
|
||||
inline constexpr Number::rep
|
||||
Number::mantissa() const noexcept
|
||||
{
|
||||
return mantissa_;
|
||||
auto m = mantissa_;
|
||||
if (m > maxRep)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
!isnormal() || (m % 10 == 0 && m / 10 <= maxRep),
|
||||
"xrpl::Number::mantissa",
|
||||
"large normalized mantissa has no remainder");
|
||||
m /= 10;
|
||||
}
|
||||
auto const sign = negative_ ? -1 : 1;
|
||||
return sign * static_cast<Number::rep>(m);
|
||||
}
|
||||
|
||||
/** Returns the exponent of the external view of the Number.
|
||||
@@ -721,7 +572,16 @@ Number::mantissa() const noexcept
|
||||
inline constexpr int
|
||||
Number::exponent() const noexcept
|
||||
{
|
||||
return exponent_;
|
||||
auto e = exponent_;
|
||||
if (mantissa_ > maxRep)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
!isnormal() || (mantissa_ % 10 == 0 && mantissa_ / 10 <= maxRep),
|
||||
"xrpl::Number::exponent",
|
||||
"large normalized mantissa has no remainder");
|
||||
++e;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
inline constexpr Number
|
||||
@@ -736,7 +596,7 @@ Number::operator-() const noexcept
|
||||
if (mantissa_ == 0)
|
||||
return Number{};
|
||||
auto x = *this;
|
||||
x.mantissa_ = -1 * x.mantissa_;
|
||||
x.negative_ = !x.negative_;
|
||||
return x;
|
||||
}
|
||||
|
||||
@@ -817,61 +677,42 @@ Number::min() noexcept
|
||||
inline Number
|
||||
Number::max() noexcept
|
||||
{
|
||||
return Number{false, range_.get().max, maxExponent, unchecked{}};
|
||||
return Number{false, std::min(range_.get().max, maxRep), maxExponent, unchecked{}};
|
||||
}
|
||||
|
||||
inline Number
|
||||
Number::lowest() noexcept
|
||||
{
|
||||
return Number{true, range_.get().max, maxExponent, unchecked{}};
|
||||
}
|
||||
|
||||
inline bool
|
||||
Number::isnormal(MantissaRange const& range) const noexcept
|
||||
{
|
||||
auto const abs_m = mantissa_ < 0 ? -mantissa_ : mantissa_;
|
||||
|
||||
return *this == Number{} ||
|
||||
(range.min <= abs_m && abs_m <= range.max && //
|
||||
minExponent <= exponent_ && exponent_ <= maxExponent);
|
||||
return Number{true, std::min(range_.get().max, maxRep), maxExponent, unchecked{}};
|
||||
}
|
||||
|
||||
inline bool
|
||||
Number::isnormal() const noexcept
|
||||
{
|
||||
return isnormal(range_);
|
||||
MantissaRange const& range = range_;
|
||||
auto const abs_m = mantissa_;
|
||||
return *this == Number{} ||
|
||||
(range.min <= abs_m && abs_m <= range.max && (abs_m <= maxRep || abs_m % 10 == 0) &&
|
||||
minExponent <= exponent_ && exponent_ <= maxExponent);
|
||||
}
|
||||
|
||||
template <Integral64 T>
|
||||
std::pair<T, int>
|
||||
Number::normalizeToRange(T minMantissa, T maxMantissa) const
|
||||
{
|
||||
bool negative = mantissa_ < 0;
|
||||
auto const sign = negative ? -1 : 1;
|
||||
internalrep mantissa = sign * mantissa_;
|
||||
bool negative = negative_;
|
||||
internalrep mantissa = mantissa_;
|
||||
int exponent = exponent_;
|
||||
|
||||
if constexpr (std::is_unsigned_v<T>)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
!negative,
|
||||
"xrpl::Number::normalizeToRange",
|
||||
"Number is non-negative for unsigned range.");
|
||||
// To avoid logical errors in release builds, throw if the Number is
|
||||
// negative for an unsigned range.
|
||||
if (negative)
|
||||
throw std::runtime_error(
|
||||
"Number::normalizeToRange: Number is negative for "
|
||||
"unsigned range.");
|
||||
}
|
||||
Number::normalize(negative, mantissa, exponent, minMantissa, maxMantissa);
|
||||
|
||||
// Cast mantissa to signed type first (if T is a signed type) to avoid
|
||||
// unsigned integer overflow when multiplying by negative sign
|
||||
T signedMantissa = static_cast<T>(mantissa);
|
||||
if (negative)
|
||||
signedMantissa = -signedMantissa;
|
||||
return std::make_pair(signedMantissa, exponent);
|
||||
auto const sign = negative ? -1 : 1;
|
||||
return std::make_pair(static_cast<T>(sign * mantissa), exponent);
|
||||
}
|
||||
|
||||
inline constexpr Number
|
||||
|
||||
@@ -359,7 +359,6 @@ public:
|
||||
base_uint&
|
||||
operator&=(base_uint const& b)
|
||||
{
|
||||
XRPL_ASSERT(WIDTH == b.WIDTH, "input size mismatch");
|
||||
for (int i = 0; i < WIDTH; i++)
|
||||
data_[i] &= b.data_[i];
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/sanitizers.h>
|
||||
#include <xrpl/beast/type_name.h>
|
||||
|
||||
#include <exception>
|
||||
@@ -25,7 +24,7 @@ LogThrow(std::string const& title);
|
||||
control to the next matching exception handler, if any.
|
||||
Otherwise, std::terminate will be called.
|
||||
*/
|
||||
[[noreturn]] XRPL_NO_SANITIZE_ADDRESS inline void
|
||||
[[noreturn]] inline void
|
||||
Rethrow()
|
||||
{
|
||||
LogThrow("Re-throwing exception");
|
||||
@@ -33,7 +32,7 @@ Rethrow()
|
||||
}
|
||||
|
||||
template <class E, class... Args>
|
||||
[[noreturn]] XRPL_NO_SANITIZE_ADDRESS inline void
|
||||
[[noreturn]] inline void
|
||||
Throw(Args&&... args)
|
||||
{
|
||||
static_assert(
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Helper to disable ASan/HwASan for specific functions
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define XRPL_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address", "hwaddress")))
|
||||
#else
|
||||
#define XRPL_NO_SANITIZE_ADDRESS
|
||||
#endif
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <class F>
|
||||
@@ -8,15 +10,17 @@ JobQueue::Coro::Coro(Coro_create_t, JobQueue& jq, JobType type, std::string cons
|
||||
, type_(type)
|
||||
, name_(name)
|
||||
, running_(false)
|
||||
, coro_([this, fn = std::forward<F>(f)](
|
||||
boost::coroutines2::asymmetric_coroutine<void>::push_type& do_yield) {
|
||||
yield_ = &do_yield;
|
||||
yield();
|
||||
fn(shared_from_this());
|
||||
, coro_(
|
||||
[this, fn = std::forward<F>(f)](
|
||||
boost::coroutines::asymmetric_coroutine<void>::push_type& do_yield) {
|
||||
yield_ = &do_yield;
|
||||
yield();
|
||||
fn(shared_from_this());
|
||||
#ifndef NDEBUG
|
||||
finished_ = true;
|
||||
finished_ = true;
|
||||
#endif
|
||||
})
|
||||
},
|
||||
boost::coroutines::attributes(megabytes(1)))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -76,7 +80,6 @@ JobQueue::Coro::resume()
|
||||
coro_();
|
||||
detail::getLocalValues().release();
|
||||
detail::getLocalValues().reset(saved);
|
||||
|
||||
std::lock_guard lk(mutex_run_);
|
||||
running_ = false;
|
||||
cv_.notify_all();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include <xrpl/core/detail/Workers.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <boost/coroutine2/all.hpp>
|
||||
#include <boost/coroutine/all.hpp>
|
||||
|
||||
#include <set>
|
||||
|
||||
@@ -48,8 +48,8 @@ public:
|
||||
std::mutex mutex_;
|
||||
std::mutex mutex_run_;
|
||||
std::condition_variable cv_;
|
||||
boost::coroutines2::coroutine<void>::pull_type coro_;
|
||||
boost::coroutines2::coroutine<void>::push_type* yield_;
|
||||
boost::coroutines::asymmetric_coroutine<void>::pull_type coro_;
|
||||
boost::coroutines::asymmetric_coroutine<void>::push_type* yield_;
|
||||
#ifndef NDEBUG
|
||||
bool finished_ = false;
|
||||
#endif
|
||||
|
||||
@@ -64,11 +64,6 @@ public:
|
||||
std::shared_ptr<SLE const>
|
||||
read(ReadView const& base, Keylet const& k) const;
|
||||
|
||||
/** Check only local items without delegating to base.
|
||||
Returns std::nullopt if key not found locally. */
|
||||
std::optional<std::shared_ptr<SLE const>>
|
||||
readLocal(Keylet const& k) const;
|
||||
|
||||
std::shared_ptr<SLE>
|
||||
peek(ReadView const& base, Keylet const& k);
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ public:
|
||||
bool sslVerify,
|
||||
beast::Journal j);
|
||||
|
||||
static void
|
||||
cleanupSSLContext();
|
||||
|
||||
static void
|
||||
get(bool bSSL,
|
||||
boost::asio::io_context& io_context,
|
||||
|
||||
@@ -77,16 +77,16 @@ public:
|
||||
If the object is not found or an error is encountered, the
|
||||
result will indicate the condition.
|
||||
@note This will be called concurrently.
|
||||
@param key A pointer to the key data.
|
||||
@param hash The hash of the object.
|
||||
@param pObject [out] The created object if successful.
|
||||
@return The result of the operation.
|
||||
*/
|
||||
virtual Status
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) = 0;
|
||||
fetch(uint256 const& hash, 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*> const& hashes) = 0;
|
||||
fetchBatch(std::vector<uint256> const& hashes) = 0;
|
||||
|
||||
/** Store a single object.
|
||||
Depending on the implementation this may happen immediately
|
||||
|
||||
@@ -234,7 +234,7 @@ missing_field_error(std::string const& name)
|
||||
}
|
||||
|
||||
inline Json::Value
|
||||
missing_field_error(Json::StaticString const& name)
|
||||
missing_field_error(Json::StaticString name)
|
||||
{
|
||||
return missing_field_error(std::string(name));
|
||||
}
|
||||
@@ -252,7 +252,7 @@ object_field_error(std::string const& name)
|
||||
}
|
||||
|
||||
inline Json::Value
|
||||
object_field_error(Json::StaticString const& name)
|
||||
object_field_error(Json::StaticString name)
|
||||
{
|
||||
return object_field_error(std::string(name));
|
||||
}
|
||||
@@ -264,7 +264,7 @@ invalid_field_message(std::string const& name)
|
||||
}
|
||||
|
||||
inline std::string
|
||||
invalid_field_message(Json::StaticString const& name)
|
||||
invalid_field_message(Json::StaticString name)
|
||||
{
|
||||
return invalid_field_message(std::string(name));
|
||||
}
|
||||
@@ -276,7 +276,7 @@ invalid_field_error(std::string const& name)
|
||||
}
|
||||
|
||||
inline Json::Value
|
||||
invalid_field_error(Json::StaticString const& name)
|
||||
invalid_field_error(Json::StaticString name)
|
||||
{
|
||||
return invalid_field_error(std::string(name));
|
||||
}
|
||||
@@ -288,7 +288,7 @@ expected_field_message(std::string const& name, std::string const& type)
|
||||
}
|
||||
|
||||
inline std::string
|
||||
expected_field_message(Json::StaticString const& name, std::string const& type)
|
||||
expected_field_message(Json::StaticString name, std::string const& type)
|
||||
{
|
||||
return expected_field_message(std::string(name), type);
|
||||
}
|
||||
@@ -300,7 +300,7 @@ expected_field_error(std::string const& name, std::string const& type)
|
||||
}
|
||||
|
||||
inline Json::Value
|
||||
expected_field_error(Json::StaticString const& name, std::string const& type)
|
||||
expected_field_error(Json::StaticString name, std::string const& type)
|
||||
{
|
||||
return expected_field_error(std::string(name), type);
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024;
|
||||
|
||||
/** The maximum amount of MPTokenIssuance */
|
||||
std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull;
|
||||
static_assert(Number::largestMantissa >= maxMPTokenAmount);
|
||||
static_assert(Number::maxRep >= maxMPTokenAmount);
|
||||
|
||||
/** The maximum length of Data payload */
|
||||
std::size_t constexpr maxDataPayloadLength = 256;
|
||||
|
||||
@@ -539,8 +539,6 @@ STAmount::fromNumber(A const& a, Number const& number)
|
||||
return STAmount{asset, intValue, 0, negative};
|
||||
}
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
working.signum() >= 0, "xrpl::STAmount::fromNumber", "non-negative Number to normalize");
|
||||
auto const [mantissa, exponent] = working.normalizeToRange(cMinValue, cMaxValue);
|
||||
|
||||
return STAmount{asset, mantissa, exponent, negative};
|
||||
|
||||
@@ -23,7 +23,7 @@ systemName()
|
||||
/** Number of drops in the genesis account. */
|
||||
constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP};
|
||||
static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000);
|
||||
static_assert(Number::largestMantissa >= INITIAL_XRP.drops());
|
||||
static_assert(Number::maxRep >= INITIAL_XRP.drops());
|
||||
|
||||
/** Returns true if the amount does not exceed the initial XRP in existence. */
|
||||
inline bool
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
|
||||
// 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::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
|
||||
@@ -31,7 +32,7 @@ XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo
|
||||
XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Batch, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
|
||||
// Check flags in Credential transactions
|
||||
|
||||
@@ -578,7 +578,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
|
||||
// The unrounded true total value of the loan.
|
||||
//
|
||||
// - TrueTotalPrincipalOutstanding can be computed using the algorithm
|
||||
// in the xrpl::detail::loanPrincipalFromPeriodicPayment function.
|
||||
// in the ripple::detail::loanPrincipalFromPeriodicPayment function.
|
||||
//
|
||||
// - TrueTotalInterestOutstanding = TrueTotalLoanValue -
|
||||
// TrueTotalPrincipalOutstanding
|
||||
|
||||
@@ -46,17 +46,6 @@ public:
|
||||
return id_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SHAMapNodeID of a child node at the specified branch.
|
||||
*
|
||||
* @param m The branch number (0-15) indicating which child to descend to.
|
||||
* In the SHAMap's 16-way radix tree, each inner node has up to
|
||||
* 16 children, indexed by the corresponding nibble (4 bits) of
|
||||
* the key at the current depth.
|
||||
* @return SHAMapNodeID of the child node at branch m.
|
||||
* @throws std::logic_error if this node is at the maximum leaf depth (64)
|
||||
* or if the node's id doesn't match its depth mask.
|
||||
*/
|
||||
SHAMapNodeID
|
||||
getChildNodeID(unsigned int m) const;
|
||||
|
||||
|
||||
@@ -1,732 +0,0 @@
|
||||
#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
|
||||
53
include/xrpl/tx/invariants/AMMInvariant.h
Normal file
53
include/xrpl/tx/invariants/AMMInvariant.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#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
|
||||
84
include/xrpl/tx/invariants/FreezeInvariant.h
Normal file
84
include/xrpl/tx/invariants/FreezeInvariant.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#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
|
||||
385
include/xrpl/tx/invariants/InvariantCheck.h
Normal file
385
include/xrpl/tx/invariants/InvariantCheck.h
Normal file
@@ -0,0 +1,385 @@
|
||||
#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
|
||||
60
include/xrpl/tx/invariants/InvariantCheckPrivilege.h
Normal file
60
include/xrpl/tx/invariants/InvariantCheckPrivilege.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#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
|
||||
75
include/xrpl/tx/invariants/LoanInvariant.h
Normal file
75
include/xrpl/tx/invariants/LoanInvariant.h
Normal file
@@ -0,0 +1,75 @@
|
||||
#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
|
||||
31
include/xrpl/tx/invariants/MPTInvariant.h
Normal file
31
include/xrpl/tx/invariants/MPTInvariant.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#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
|
||||
70
include/xrpl/tx/invariants/NFTInvariant.h
Normal file
70
include/xrpl/tx/invariants/NFTInvariant.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#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
|
||||
25
include/xrpl/tx/invariants/PermissionedDEXInvariant.h
Normal file
25
include/xrpl/tx/invariants/PermissionedDEXInvariant.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#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
|
||||
41
include/xrpl/tx/invariants/PermissionedDomainInvariant.h
Normal file
41
include/xrpl/tx/invariants/PermissionedDomainInvariant.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#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
|
||||
77
include/xrpl/tx/invariants/VaultInvariant.h
Normal file
77
include/xrpl/tx/invariants/VaultInvariant.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#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
|
||||
140
nix/devshell.nix
Normal file
140
nix/devshell.nix
Normal file
@@ -0,0 +1,140 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
commonPackages = with pkgs; [
|
||||
ccache
|
||||
cmake
|
||||
conan
|
||||
gcovr
|
||||
git
|
||||
gnumake
|
||||
llvmPackages_21.clang-tools
|
||||
ninja
|
||||
perl # needed for openssl
|
||||
pkg-config
|
||||
pre-commit
|
||||
python314
|
||||
];
|
||||
|
||||
# Supported compiler versions
|
||||
gccVersion = pkgs.lib.range 13 15;
|
||||
clangVersions = pkgs.lib.range 18 21;
|
||||
|
||||
defaultCompiler = if pkgs.stdenv.isDarwin then "apple-clang" else "gcc";
|
||||
defaultGccVersion = pkgs.lib.last gccVersion;
|
||||
defaultClangVersion = pkgs.lib.last clangVersions;
|
||||
|
||||
strToCompilerEnv =
|
||||
compiler: version:
|
||||
(
|
||||
if compiler == "gcc" then
|
||||
let
|
||||
gccPkg = pkgs."gcc${toString version}Stdenv" or null;
|
||||
in
|
||||
if gccPkg != null && builtins.elem version gccVersion then
|
||||
gccPkg
|
||||
else
|
||||
throw "Invalid GCC version: ${toString version}. Must be one of: ${toString gccVersion}"
|
||||
else if compiler == "clang" then
|
||||
let
|
||||
clangPkg = pkgs."llvmPackages_${toString version}".stdenv or null;
|
||||
in
|
||||
if clangPkg != null && builtins.elem version clangVersions then
|
||||
clangPkg
|
||||
else
|
||||
throw "Invalid Clang version: ${toString version}. Must be one of: ${toString clangVersions}"
|
||||
else if compiler == "apple-clang" || compiler == "none" then
|
||||
pkgs.stdenvNoCC
|
||||
else
|
||||
throw "Invalid compiler: ${compiler}. Must be one of: gcc, clang, apple-clang, none"
|
||||
);
|
||||
|
||||
# Helper function to create a shell with a specific compiler
|
||||
makeShell =
|
||||
{
|
||||
compiler ? defaultCompiler,
|
||||
version ? (
|
||||
if compiler == "gcc" then
|
||||
defaultGccVersion
|
||||
else if compiler == "clang" then
|
||||
defaultClangVersion
|
||||
else
|
||||
null
|
||||
),
|
||||
}:
|
||||
let
|
||||
compilerStdEnv = strToCompilerEnv compiler version;
|
||||
|
||||
compilerName =
|
||||
if compiler == "apple-clang" then
|
||||
"clang"
|
||||
else if compiler == "none" then
|
||||
null
|
||||
else
|
||||
compiler;
|
||||
|
||||
gccOnMacWarning =
|
||||
if pkgs.stdenv.isDarwin && compiler == "gcc" then
|
||||
''
|
||||
echo "WARNING: Using GCC on macOS with Conan may not work."
|
||||
echo " Consider using 'nix develop .#clang' or the default shell instead."
|
||||
echo ""
|
||||
''
|
||||
else
|
||||
"";
|
||||
|
||||
compilerVersion =
|
||||
if compilerName != null then
|
||||
''
|
||||
echo "Compiler: "
|
||||
${compilerName} --version
|
||||
''
|
||||
else
|
||||
''
|
||||
echo "No compiler specified - using system compiler"
|
||||
'';
|
||||
|
||||
shellAttrs = {
|
||||
packages = commonPackages;
|
||||
|
||||
shellHook = ''
|
||||
echo "Welcome to xrpld development shell";
|
||||
${gccOnMacWarning}${compilerVersion}
|
||||
'';
|
||||
};
|
||||
in
|
||||
pkgs.mkShell.override { stdenv = compilerStdEnv; } shellAttrs;
|
||||
|
||||
# Generate shells for each compiler version
|
||||
gccShells = builtins.listToAttrs (
|
||||
map (version: {
|
||||
name = "gcc${toString version}";
|
||||
value = makeShell {
|
||||
compiler = "gcc";
|
||||
version = version;
|
||||
};
|
||||
}) gccVersion
|
||||
);
|
||||
|
||||
clangShells = builtins.listToAttrs (
|
||||
map (version: {
|
||||
name = "clang${toString version}";
|
||||
value = makeShell {
|
||||
compiler = "clang";
|
||||
version = version;
|
||||
};
|
||||
}) clangVersions
|
||||
);
|
||||
|
||||
in
|
||||
gccShells
|
||||
// clangShells
|
||||
// {
|
||||
# Default shells
|
||||
default = makeShell { };
|
||||
gcc = makeShell { compiler = "gcc"; };
|
||||
clang = makeShell { compiler = "clang"; };
|
||||
|
||||
# No compiler
|
||||
no-compiler = makeShell { compiler = "none"; };
|
||||
apple-clang = makeShell { compiler = "apple-clang"; };
|
||||
}
|
||||
19
nix/utils.nix
Normal file
19
nix/utils.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{ nixpkgs }:
|
||||
{
|
||||
forEachSystem =
|
||||
function:
|
||||
nixpkgs.lib.genAttrs
|
||||
[
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
]
|
||||
(
|
||||
system:
|
||||
function {
|
||||
inherit system;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppresions.
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppressions.
|
||||
#
|
||||
# ASAN_OPTIONS="suppressions=sanitizers/suppressions/asan.supp:include=sanitizers/suppressions/runtime-asan-options.txt"
|
||||
# ASAN_OPTIONS="print_stacktrace=1:detect_container_overflow=0:suppressions=sanitizers/suppressions/asan.supp:halt_on_error=0"
|
||||
#
|
||||
# Boost coroutines cause multiple ASAN false positives due to swapcontext/fiber stack switching.
|
||||
# ASAN cannot correctly track stack memory across coroutine context switches, leading to:
|
||||
# - stack-use-after-return errors
|
||||
# - stack-use-after-scope errors
|
||||
# - stack-buffer-overflow errors in seemingly unrelated code (e.g., std::chrono::steady_clock::now())
|
||||
# - stack-buffer-underflow errors in seemingly unrelated code (e.g., xxhasher::retrieveHash(), clock_gettime)
|
||||
# - bad-free errors in boost::context::basic_fixedsize_stack::deallocate (ASan loses track of
|
||||
# malloc allocations after fiber/context switches, reporting "free on address not malloc()-ed")
|
||||
# The detect_container_overflow=0 option disables false positives from:
|
||||
# - Boost intrusive containers (slist_iterator.hpp, hashtable.hpp, aged_unordered_container.h)
|
||||
# - Boost context/coroutine stack switching (Workers.cpp, thread.h)
|
||||
#
|
||||
# These are now handled by:
|
||||
# 1. Using Boost.Coroutine2 with the ucontext backend (BOOST_USE_ASAN + BOOST_USE_UCONTEXT)
|
||||
# 2. Runtime options in runtime-asan-options.txt:
|
||||
# - alloc_dealloc_mismatch=0: Suppresses false "bad-free" errors from fiber stack deallocation, on GCC. For Clang we don't instrument boost
|
||||
# - detect_stack_use_after_return=0: Prevents false positives from fake stack tracking
|
||||
# - use_sigaltstack=0: Avoids conflicts with coroutine stack switching
|
||||
interceptor_via_fun:swapcontext
|
||||
interceptor_via_fun:makecontext
|
||||
interceptor_via_fun:boost::context::fiber::~fiber
|
||||
interceptor_name:boost/context/fiber_ucontext.hpp
|
||||
interceptor_name:boost/context/fixedsize_stack.hpp
|
||||
interceptor_name:Coro.ipp
|
||||
# See: https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow
|
||||
|
||||
interceptor_name:clock_gettime
|
||||
# Boost
|
||||
interceptor_name:boost/asio
|
||||
|
||||
# Leaks in Doctest tests: xrpl.test.*
|
||||
interceptor_name:src/libxrpl/net/HTTPClient.cpp
|
||||
interceptor_name:src/libxrpl/net/RegisterSSLCerts.cpp
|
||||
interceptor_name:src/tests/libxrpl/net/HTTPClient.cpp
|
||||
interceptor_name:xrpl/net/AutoSocket.h
|
||||
interceptor_name:xrpl/net/HTTPClient.h
|
||||
interceptor_name:xrpl/net/HTTPClientSSLContext.h
|
||||
interceptor_name:xrpl/net/RegisterSSLCerts.h
|
||||
|
||||
# Suppress false positive stack-buffer errors in thread stack allocation
|
||||
# Related to ASan's __asan_handle_no_return warnings (github.com/google/sanitizers/issues/189)
|
||||
# These occur during multi-threaded test initialization on macOS
|
||||
interceptor_name:memcpy
|
||||
interceptor_name:__bzero
|
||||
interceptor_name:nudb
|
||||
interceptor_name:__asan_memset
|
||||
interceptor_name:__asan_memcpy
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppresions.
|
||||
|
||||
# Suppress leaks detected by asan in rippled code.
|
||||
leak:src/libxrpl/net/HTTPClient.cpp
|
||||
leak:src/libxrpl/net/RegisterSSLCerts.cpp
|
||||
leak:src/tests/libxrpl/net/HTTPClient.cpp
|
||||
leak:xrpl/net/AutoSocket.h
|
||||
leak:xrpl/net/HTTPClient.h
|
||||
leak:xrpl/net/HTTPClientSSLContext.h
|
||||
leak:xrpl/net/RegisterSSLCerts.h
|
||||
leak:ripple::HTTPClient
|
||||
leak:ripple::HTTPClientImp
|
||||
|
||||
# Suppress leaks detected by asan in boost code.
|
||||
# These are false positives from Boost.Asio SSL internals that use OpenSSL BIO structures.
|
||||
# The BIO structures are managed by OpenSSL's internal reference counting and freed at process exit.
|
||||
|
||||
#leak:boost::asio
|
||||
#leak:boost/asio
|
||||
|
||||
# OpenSSL BIO memory is managed internally and freed at process exit
|
||||
leak:CRYPTO_malloc
|
||||
leak:bio_make_pair
|
||||
leak:BIO_new_bio_pair
|
||||
leak:boost::asio
|
||||
leak:boost/asio
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
detect_container_overflow=0
|
||||
detect_stack_use_after_return=0
|
||||
halt_on_error=0
|
||||
print_summary=true
|
||||
use_sigaltstack=0
|
||||
@@ -1 +0,0 @@
|
||||
halt_on_error=0
|
||||
@@ -1,2 +0,0 @@
|
||||
halt_on_error=0
|
||||
second_deadlock_stack=1
|
||||
@@ -1 +0,0 @@
|
||||
halt_on_error=0
|
||||
@@ -20,15 +20,10 @@ signal:test/beast/beast_PropertyStream_test.cpp
|
||||
signal:xrpld/core/detail/Workers.cpp
|
||||
signal:xrpld/core/JobQueue.cpp
|
||||
|
||||
# src:beast/utility/beast_Journal.cpp
|
||||
# src:beast/utility/beast_PropertyStream.cpp
|
||||
# src:core/detail/Workers.cpp
|
||||
# src:core/JobQueue.cpp
|
||||
# src:libxrpl/beast/utility/beast_Journal.cpp
|
||||
# src:test/beast/beast_PropertyStream_test.cpp
|
||||
# src:src/test/app/Invariants_test.cpp
|
||||
|
||||
# Boost coroutines cause false positive stack-buffer-underflow in xxhasher
|
||||
# This is a known ASAN limitation with stackful coroutines
|
||||
# See: https://github.com/google/sanitizers/issues/189
|
||||
src:beast/hash/xxhasher.h
|
||||
src:beast/utility/beast_Journal.cpp
|
||||
src:beast/utility/beast_PropertyStream.cpp
|
||||
src:core/detail/Workers.cpp
|
||||
src:core/JobQueue.cpp
|
||||
src:libxrpl/beast/utility/beast_Journal.cpp
|
||||
src:test/beast/beast_PropertyStream_test.cpp
|
||||
src:src/test/app/Invariants_test.cpp
|
||||
|
||||
@@ -140,7 +140,6 @@ unsigned-integer-overflow:src/libxrpl/protocol/tokens.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/shamap/SHAMap.cpp
|
||||
unsigned-integer-overflow:src/test/app/Batch_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/Invariants_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/Loan_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/NFToken_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/Offer_test.cpp
|
||||
unsigned-integer-overflow:src/test/app/Path_test.cpp
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#include <xrpl/basics/LocalValue.h>
|
||||
|
||||
namespace xrpl {
|
||||
namespace detail {
|
||||
|
||||
} // namespace detail
|
||||
} // namespace xrpl
|
||||
157
src/libxrpl/basics/MallocTrim.cpp
Normal file
157
src/libxrpl/basics/MallocTrim.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#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
|
||||
@@ -11,16 +11,18 @@
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma message("Using boost::multiprecision::uint128_t and int128_t")
|
||||
#endif
|
||||
|
||||
using uint128_t = xrpl::detail::uint128_t;
|
||||
using int128_t = xrpl::detail::int128_t;
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
using uint128_t = boost::multiprecision::uint128_t;
|
||||
using int128_t = boost::multiprecision::int128_t;
|
||||
#else // !defined(_MSC_VER)
|
||||
using uint128_t = __uint128_t;
|
||||
using int128_t = __int128_t;
|
||||
#endif // !defined(_MSC_VER)
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -59,6 +61,9 @@ Number::setMantissaScale(MantissaRange::mantissa_scale scale)
|
||||
// precision to an operation. This enables the final result
|
||||
// to be correctly rounded to the internal precision of Number.
|
||||
|
||||
template <class T>
|
||||
concept UnsignedMantissa = std::is_unsigned_v<T> || std::is_same_v<T, uint128_t>;
|
||||
|
||||
class Number::Guard
|
||||
{
|
||||
std::uint64_t digits_; // 16 decimal guard digits
|
||||
@@ -94,7 +99,7 @@ public:
|
||||
round() noexcept;
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
template <detail::UnsignedMantissa T>
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
doRoundUp(
|
||||
bool& negative,
|
||||
@@ -102,22 +107,22 @@ public:
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
std::string_view location);
|
||||
std::string location);
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
template <detail::UnsignedMantissa T>
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa);
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
void
|
||||
doRound(rep& drops, std::string_view location);
|
||||
doRound(rep& drops, std::string location);
|
||||
|
||||
private:
|
||||
void
|
||||
doPush(unsigned d) noexcept;
|
||||
|
||||
template <detail::UnsignedMantissa T>
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa);
|
||||
};
|
||||
@@ -204,7 +209,7 @@ Number::Guard::round() noexcept
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <detail::UnsignedMantissa T>
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
Number::Guard::bringIntoRange(
|
||||
bool& negative,
|
||||
@@ -223,13 +228,13 @@ Number::Guard::bringIntoRange(
|
||||
{
|
||||
constexpr Number zero = Number{};
|
||||
|
||||
negative = false;
|
||||
negative = zero.negative_;
|
||||
mantissa = zero.mantissa_;
|
||||
exponent = zero.exponent_;
|
||||
}
|
||||
}
|
||||
|
||||
template <detail::UnsignedMantissa T>
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
Number::Guard::doRoundUp(
|
||||
bool& negative,
|
||||
@@ -237,7 +242,7 @@ Number::Guard::doRoundUp(
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
std::string_view location)
|
||||
std::string location)
|
||||
{
|
||||
auto r = round();
|
||||
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
|
||||
@@ -245,7 +250,7 @@ Number::Guard::doRoundUp(
|
||||
++mantissa;
|
||||
// Ensure mantissa after incrementing fits within both the
|
||||
// min/maxMantissa range and is a valid "rep".
|
||||
if (mantissa > maxMantissa)
|
||||
if (mantissa > maxMantissa || mantissa > maxRep)
|
||||
{
|
||||
mantissa /= 10;
|
||||
++exponent;
|
||||
@@ -253,10 +258,10 @@ Number::Guard::doRoundUp(
|
||||
}
|
||||
bringIntoRange(negative, mantissa, exponent, minMantissa);
|
||||
if (exponent > maxExponent)
|
||||
Throw<std::overflow_error>(std::string{location});
|
||||
throw std::overflow_error(location);
|
||||
}
|
||||
|
||||
template <detail::UnsignedMantissa T>
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
Number::Guard::doRoundDown(
|
||||
bool& negative,
|
||||
@@ -279,13 +284,12 @@ Number::Guard::doRoundDown(
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
void
|
||||
Number::Guard::doRound(rep& drops, std::string_view location)
|
||||
Number::Guard::doRound(rep& drops, std::string location)
|
||||
{
|
||||
auto r = round();
|
||||
if (r == 1 || (r == 0 && (drops & 1) == 1))
|
||||
{
|
||||
auto const& range = range_.get();
|
||||
if (drops >= range.max)
|
||||
if (drops >= maxRep)
|
||||
{
|
||||
static_assert(sizeof(internalrep) == sizeof(rep));
|
||||
// This should be impossible, because it's impossible to represent
|
||||
@@ -294,7 +298,7 @@ Number::Guard::doRound(rep& drops, std::string_view location)
|
||||
// or "(maxRep + 1) / 10", neither of which will round up when
|
||||
// converting to rep, though the latter might overflow _before_
|
||||
// rounding.
|
||||
Throw<std::overflow_error>(std::string{location}); // LCOV_EXCL_LINE
|
||||
throw std::overflow_error(location); // LCOV_EXCL_LINE
|
||||
}
|
||||
++drops;
|
||||
}
|
||||
@@ -314,131 +318,23 @@ Number::externalToInternal(rep mantissa)
|
||||
// If the mantissa is already positive, just return it
|
||||
if (mantissa >= 0)
|
||||
return mantissa;
|
||||
// If the mantissa is negative, but fits within the positive range of rep,
|
||||
// return it negated
|
||||
if (mantissa >= -std::numeric_limits<rep>::max())
|
||||
return -mantissa;
|
||||
|
||||
// Cast to unsigned before negating to avoid undefined behavior
|
||||
// when v == INT64_MIN (negating INT64_MIN in signed is UB)
|
||||
return -static_cast<internalrep>(mantissa);
|
||||
}
|
||||
|
||||
/** Breaks down the number into components, potentially de-normalizing it.
|
||||
*
|
||||
* Ensures that the mantissa always has range_.log digits.
|
||||
*
|
||||
*/
|
||||
template <detail::UnsignedMantissa Rep>
|
||||
std::tuple<bool, Rep, int>
|
||||
Number::toInternal(MantissaRange const& range) const
|
||||
{
|
||||
auto exponent = exponent_;
|
||||
bool const negative = mantissa_ < 0;
|
||||
auto const sign = negative ? -1 : 1;
|
||||
Rep mantissa = static_cast<Rep>(sign * mantissa_);
|
||||
|
||||
auto const referenceMin = range.referenceMin;
|
||||
auto const minMantissa = range.min;
|
||||
|
||||
if (mantissa != 0 && mantissa >= minMantissa && mantissa < referenceMin)
|
||||
{
|
||||
// Ensure the mantissa has the correct number of digits
|
||||
mantissa *= 10;
|
||||
--exponent;
|
||||
XRPL_ASSERT_PARTS(
|
||||
mantissa >= referenceMin && mantissa < referenceMin * 10,
|
||||
"xrpl::Number::toInternal()",
|
||||
"Number is within reference range and has 'log' digits");
|
||||
}
|
||||
|
||||
return {negative, mantissa, exponent};
|
||||
}
|
||||
|
||||
/** Breaks down the number into components, potentially de-normalizing it.
|
||||
*
|
||||
* Ensures that the mantissa always has range_.log digits.
|
||||
*
|
||||
*/
|
||||
template <detail::UnsignedMantissa Rep>
|
||||
std::tuple<bool, Rep, int>
|
||||
Number::toInternal() const
|
||||
{
|
||||
return toInternal(range_);
|
||||
}
|
||||
|
||||
/** Rebuilds the number from components.
|
||||
*
|
||||
* If "normalized" is true, the values are expected to be normalized - all
|
||||
* in their valid ranges.
|
||||
*
|
||||
* If "normalized" is false, the values are expected to be "near
|
||||
* normalized", meaning that the mantissa has to be modified at most once to
|
||||
* bring it back into range.
|
||||
*
|
||||
*/
|
||||
template <bool expectNormal, detail::UnsignedMantissa Rep>
|
||||
void
|
||||
Number::fromInternal(bool negative, Rep mantissa, int exponent, MantissaRange const* pRange)
|
||||
{
|
||||
if constexpr (std::is_same_v<std::bool_constant<expectNormal>, std::false_type>)
|
||||
{
|
||||
if (!pRange)
|
||||
throw std::runtime_error("Missing range to Number::fromInternal!");
|
||||
auto const& range = *pRange;
|
||||
|
||||
auto const maxMantissa = range.max;
|
||||
auto const minMantissa = range.min;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
mantissa >= minMantissa, "xrpl::Number::fromInternal", "mantissa large enough");
|
||||
|
||||
if (mantissa > maxMantissa || mantissa < minMantissa)
|
||||
{
|
||||
normalize(negative, mantissa, exponent, range.min, maxMantissa);
|
||||
}
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
mantissa >= minMantissa && mantissa <= maxMantissa,
|
||||
"xrpl::Number::fromInternal",
|
||||
"mantissa in range");
|
||||
}
|
||||
|
||||
auto const sign = negative ? -1 : 1;
|
||||
|
||||
mantissa_ = sign * static_cast<rep>(mantissa);
|
||||
exponent_ = exponent;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
(pRange && isnormal(*pRange)) || isnormal(),
|
||||
"xrpl::Number::fromInternal",
|
||||
"Number is normalized");
|
||||
}
|
||||
|
||||
/** Rebuilds the number from components.
|
||||
*
|
||||
* If "normalized" is true, the values are expected to be normalized - all in
|
||||
* their valid ranges.
|
||||
*
|
||||
* If "normalized" is false, the values are expected to be "near normalized",
|
||||
* meaning that the mantissa has to be modified at most once to bring it back
|
||||
* into range.
|
||||
*
|
||||
*/
|
||||
template <bool expectNormal, detail::UnsignedMantissa Rep>
|
||||
void
|
||||
Number::fromInternal(bool negative, Rep mantissa, int exponent)
|
||||
{
|
||||
MantissaRange const* pRange = nullptr;
|
||||
if constexpr (std::is_same_v<std::bool_constant<expectNormal>, std::false_type>)
|
||||
{
|
||||
pRange = &Number::range_.get();
|
||||
}
|
||||
|
||||
fromInternal(negative, mantissa, exponent, pRange);
|
||||
// If the mantissa doesn't fit within the positive range, convert to
|
||||
// int128_t, negate that, and cast it back down to the internalrep
|
||||
// In practice, this is only going to cover the case of
|
||||
// std::numeric_limits<rep>::min().
|
||||
int128_t temp = mantissa;
|
||||
return static_cast<internalrep>(-temp);
|
||||
}
|
||||
|
||||
constexpr Number
|
||||
Number::oneSmall()
|
||||
{
|
||||
return Number{
|
||||
false, Number::smallRange.referenceMin, -Number::smallRange.log, Number::unchecked{}};
|
||||
return Number{false, Number::smallRange.min, -Number::smallRange.log, Number::unchecked{}};
|
||||
};
|
||||
|
||||
constexpr Number oneSml = Number::oneSmall();
|
||||
@@ -446,89 +342,103 @@ constexpr Number oneSml = Number::oneSmall();
|
||||
constexpr Number
|
||||
Number::oneLarge()
|
||||
{
|
||||
return Number{
|
||||
false, Number::largeRange.referenceMin, -Number::largeRange.log, Number::unchecked{}};
|
||||
return Number{false, Number::largeRange.min, -Number::largeRange.log, Number::unchecked{}};
|
||||
};
|
||||
|
||||
constexpr Number oneLrg = Number::oneLarge();
|
||||
|
||||
Number
|
||||
Number::one(MantissaRange const& range)
|
||||
Number::one()
|
||||
{
|
||||
if (&range == &smallRange)
|
||||
if (&range_.get() == &smallRange)
|
||||
return oneSml;
|
||||
XRPL_ASSERT(&range == &largeRange, "Number::one() : valid range");
|
||||
XRPL_ASSERT(&range_.get() == &largeRange, "Number::one() : valid range_");
|
||||
return oneLrg;
|
||||
}
|
||||
|
||||
Number
|
||||
Number::one()
|
||||
{
|
||||
return one(range_);
|
||||
}
|
||||
|
||||
// Use the member names in this static function for now so the diff is cleaner
|
||||
// TODO: Rename the function parameters to get rid of the "_" suffix
|
||||
template <class T>
|
||||
void
|
||||
doNormalize(
|
||||
bool& negative,
|
||||
T& mantissa,
|
||||
int& exponent,
|
||||
T& mantissa_,
|
||||
int& exponent_,
|
||||
MantissaRange::rep const& minMantissa,
|
||||
MantissaRange::rep const& maxMantissa)
|
||||
{
|
||||
auto constexpr minExponent = Number::minExponent;
|
||||
auto constexpr maxExponent = Number::maxExponent;
|
||||
auto constexpr maxRep = Number::maxRep;
|
||||
|
||||
using Guard = Number::Guard;
|
||||
|
||||
constexpr Number zero = Number{};
|
||||
if (mantissa == 0 || (mantissa < minMantissa && exponent <= minExponent))
|
||||
if (mantissa_ == 0)
|
||||
{
|
||||
mantissa = zero.mantissa_;
|
||||
exponent = zero.exponent_;
|
||||
negative = false;
|
||||
mantissa_ = zero.mantissa_;
|
||||
exponent_ = zero.exponent_;
|
||||
negative = zero.negative_;
|
||||
return;
|
||||
}
|
||||
|
||||
auto m = mantissa;
|
||||
while ((m < minMantissa) && (exponent > minExponent))
|
||||
auto m = mantissa_;
|
||||
while ((m < minMantissa) && (exponent_ > minExponent))
|
||||
{
|
||||
m *= 10;
|
||||
--exponent;
|
||||
--exponent_;
|
||||
}
|
||||
Guard g;
|
||||
if (negative)
|
||||
g.set_negative();
|
||||
while (m > maxMantissa)
|
||||
{
|
||||
if (exponent >= maxExponent)
|
||||
if (exponent_ >= maxExponent)
|
||||
throw std::overflow_error("Number::normalize 1");
|
||||
g.push(m % 10);
|
||||
m /= 10;
|
||||
++exponent;
|
||||
++exponent_;
|
||||
}
|
||||
if ((exponent < minExponent) || (m == 0))
|
||||
if ((exponent_ < minExponent) || (m < minMantissa))
|
||||
{
|
||||
mantissa = zero.mantissa_;
|
||||
exponent = zero.exponent_;
|
||||
negative = false;
|
||||
mantissa_ = zero.mantissa_;
|
||||
exponent_ = zero.exponent_;
|
||||
negative = zero.negative_;
|
||||
return;
|
||||
}
|
||||
|
||||
XRPL_ASSERT_PARTS(m <= maxMantissa, "xrpl::doNormalize", "intermediate mantissa fits in int64");
|
||||
mantissa = m;
|
||||
|
||||
g.doRoundUp(negative, mantissa, exponent, minMantissa, maxMantissa, "Number::normalize 2");
|
||||
// When using the largeRange, "m" needs fit within an int64, even if
|
||||
// the final mantissa_ is going to end up larger to fit within the
|
||||
// MantissaRange. Cut it down here so that the rounding will be done while
|
||||
// it's smaller.
|
||||
//
|
||||
// Example: 9,900,000,000,000,123,456 > 9,223,372,036,854,775,807,
|
||||
// so "m" will be modified to 990,000,000,000,012,345. Then that value
|
||||
// will be rounded to 990,000,000,000,012,345 or
|
||||
// 990,000,000,000,012,346, depending on the rounding mode. Finally,
|
||||
// mantissa_ will be "m*10" so it fits within the range, and end up as
|
||||
// 9,900,000,000,000,123,450 or 9,900,000,000,000,123,460.
|
||||
// mantissa() will return mantissa_ / 10, and exponent() will return
|
||||
// exponent_ + 1.
|
||||
if (m > maxRep)
|
||||
{
|
||||
if (exponent_ >= maxExponent)
|
||||
throw std::overflow_error("Number::normalize 1.5");
|
||||
g.push(m % 10);
|
||||
m /= 10;
|
||||
++exponent_;
|
||||
}
|
||||
// Before modification, m should be within the min/max range. After
|
||||
// modification, it must be less than maxRep. In other words, the original
|
||||
// value should have been no more than maxRep * 10.
|
||||
// (maxRep * 10 > maxMantissa)
|
||||
XRPL_ASSERT_PARTS(m <= maxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64");
|
||||
mantissa_ = m;
|
||||
|
||||
g.doRoundUp(negative, mantissa_, exponent_, minMantissa, maxMantissa, "Number::normalize 2");
|
||||
XRPL_ASSERT_PARTS(
|
||||
mantissa >= minMantissa && mantissa <= maxMantissa,
|
||||
mantissa_ >= minMantissa && mantissa_ <= maxMantissa,
|
||||
"xrpl::doNormalize",
|
||||
"final mantissa fits in range");
|
||||
XRPL_ASSERT_PARTS(
|
||||
exponent >= minExponent && exponent <= maxExponent,
|
||||
"xrpl::doNormalize",
|
||||
"final exponent fits in range");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -567,20 +477,11 @@ Number::normalize<unsigned long>(
|
||||
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa);
|
||||
}
|
||||
|
||||
void
|
||||
Number::normalize(MantissaRange const& range)
|
||||
{
|
||||
auto [negative, mantissa, exponent] = toInternal(range);
|
||||
|
||||
normalize(negative, mantissa, exponent, range.min, range.max);
|
||||
|
||||
fromInternal(negative, mantissa, exponent, &range);
|
||||
}
|
||||
|
||||
void
|
||||
Number::normalize()
|
||||
{
|
||||
normalize(range_);
|
||||
auto const& range = range_.get();
|
||||
normalize(negative_, mantissa_, exponent_, range.min, range.max);
|
||||
}
|
||||
|
||||
// Copy the number, but set a new exponent. Because the mantissa doesn't change,
|
||||
@@ -590,33 +491,21 @@ Number
|
||||
Number::shiftExponent(int exponentDelta) const
|
||||
{
|
||||
XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::shiftExponent", "normalized");
|
||||
|
||||
Number result = *this;
|
||||
|
||||
result.exponent_ += exponentDelta;
|
||||
|
||||
if (result.exponent_ >= maxExponent)
|
||||
auto const newExponent = exponent_ + exponentDelta;
|
||||
if (newExponent >= maxExponent)
|
||||
throw std::overflow_error("Number::shiftExponent");
|
||||
if (result.exponent_ < minExponent)
|
||||
if (newExponent < minExponent)
|
||||
{
|
||||
return Number{};
|
||||
}
|
||||
|
||||
Number const result{negative_, mantissa_, newExponent, unchecked{}};
|
||||
XRPL_ASSERT_PARTS(result.isnormal(), "xrpl::Number::shiftExponent", "result is normalized");
|
||||
return result;
|
||||
}
|
||||
|
||||
Number::Number(bool negative, internalrep mantissa, int exponent, normalized)
|
||||
{
|
||||
auto const& range = range_.get();
|
||||
normalize(negative, mantissa, exponent, range.min, range.max);
|
||||
fromInternal(negative, mantissa, exponent, &range);
|
||||
}
|
||||
|
||||
Number&
|
||||
Number::operator+=(Number const& y)
|
||||
{
|
||||
auto const& range = range_.get();
|
||||
|
||||
constexpr Number zero = Number{};
|
||||
if (y == zero)
|
||||
return *this;
|
||||
@@ -631,8 +520,7 @@ Number::operator+=(Number const& y)
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
isnormal(range) && y.isnormal(range), "xrpl::Number::operator+=(Number) : is normal");
|
||||
XRPL_ASSERT(isnormal() && y.isnormal(), "xrpl::Number::operator+=(Number) : is normal");
|
||||
// *n = negative
|
||||
// *s = sign
|
||||
// *m = mantissa
|
||||
@@ -640,10 +528,13 @@ Number::operator+=(Number const& y)
|
||||
|
||||
// Need to use uint128_t, because large mantissas can overflow when added
|
||||
// together.
|
||||
auto [xn, xm, xe] = toInternal<uint128_t>(range);
|
||||
|
||||
auto [yn, ym, ye] = y.toInternal<uint128_t>(range);
|
||||
bool xn = negative_;
|
||||
uint128_t xm = mantissa_;
|
||||
auto xe = exponent_;
|
||||
|
||||
bool yn = y.negative_;
|
||||
uint128_t ym = y.mantissa_;
|
||||
auto ye = y.exponent_;
|
||||
Guard g;
|
||||
if (xe < ye)
|
||||
{
|
||||
@@ -668,13 +559,14 @@ Number::operator+=(Number const& y)
|
||||
} while (xe > ye);
|
||||
}
|
||||
|
||||
auto const& range = range_.get();
|
||||
auto const& minMantissa = range.min;
|
||||
auto const& maxMantissa = range.max;
|
||||
|
||||
if (xn == yn)
|
||||
{
|
||||
xm += ym;
|
||||
if (xm > maxMantissa)
|
||||
if (xm > maxMantissa || xm > maxRep)
|
||||
{
|
||||
g.push(xm % 10);
|
||||
xm /= 10;
|
||||
@@ -694,7 +586,7 @@ Number::operator+=(Number const& y)
|
||||
xe = ye;
|
||||
xn = yn;
|
||||
}
|
||||
while (xm < minMantissa)
|
||||
while (xm < minMantissa && xm * 10 <= maxRep)
|
||||
{
|
||||
xm *= 10;
|
||||
xm -= g.pop();
|
||||
@@ -703,8 +595,10 @@ Number::operator+=(Number const& y)
|
||||
g.doRoundDown(xn, xm, xe, minMantissa);
|
||||
}
|
||||
|
||||
normalize(xn, xm, xe, minMantissa, maxMantissa);
|
||||
fromInternal(xn, xm, xe, &range);
|
||||
negative_ = xn;
|
||||
mantissa_ = static_cast<internalrep>(xm);
|
||||
exponent_ = xe;
|
||||
normalize();
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -739,8 +633,6 @@ divu10(uint128_t& u)
|
||||
Number&
|
||||
Number::operator*=(Number const& y)
|
||||
{
|
||||
auto const& range = range_.get();
|
||||
|
||||
constexpr Number zero = Number{};
|
||||
if (*this == zero)
|
||||
return *this;
|
||||
@@ -754,11 +646,15 @@ Number::operator*=(Number const& y)
|
||||
// *m = mantissa
|
||||
// *e = exponent
|
||||
|
||||
auto [xn, xm, xe] = toInternal(range);
|
||||
bool xn = negative_;
|
||||
int xs = xn ? -1 : 1;
|
||||
internalrep xm = mantissa_;
|
||||
auto xe = exponent_;
|
||||
|
||||
auto [yn, ym, ye] = y.toInternal(range);
|
||||
bool yn = y.negative_;
|
||||
int ys = yn ? -1 : 1;
|
||||
internalrep ym = y.mantissa_;
|
||||
auto ye = y.exponent_;
|
||||
|
||||
auto zm = uint128_t(xm) * uint128_t(ym);
|
||||
auto ze = xe + ye;
|
||||
@@ -768,10 +664,11 @@ Number::operator*=(Number const& y)
|
||||
if (zn)
|
||||
g.set_negative();
|
||||
|
||||
auto const& range = range_.get();
|
||||
auto const& minMantissa = range.min;
|
||||
auto const& maxMantissa = range.max;
|
||||
|
||||
while (zm > maxMantissa)
|
||||
while (zm > maxMantissa || zm > maxRep)
|
||||
{
|
||||
// The following is optimization for:
|
||||
// g.push(static_cast<unsigned>(zm % 10));
|
||||
@@ -788,17 +685,17 @@ Number::operator*=(Number const& y)
|
||||
minMantissa,
|
||||
maxMantissa,
|
||||
"Number::multiplication overflow : exponent is " + std::to_string(xe));
|
||||
negative_ = zn;
|
||||
mantissa_ = xm;
|
||||
exponent_ = xe;
|
||||
|
||||
normalize(zn, xm, xe, minMantissa, maxMantissa);
|
||||
fromInternal(zn, xm, xe, &range);
|
||||
normalize();
|
||||
return *this;
|
||||
}
|
||||
|
||||
Number&
|
||||
Number::operator/=(Number const& y)
|
||||
{
|
||||
auto const& range = range_.get();
|
||||
|
||||
constexpr Number zero = Number{};
|
||||
if (y == zero)
|
||||
throw std::overflow_error("Number: divide by 0");
|
||||
@@ -811,12 +708,17 @@ Number::operator/=(Number const& y)
|
||||
// *m = mantissa
|
||||
// *e = exponent
|
||||
|
||||
auto [np, nm, ne] = toInternal(range);
|
||||
bool np = negative_;
|
||||
int ns = (np ? -1 : 1);
|
||||
auto nm = mantissa_;
|
||||
auto ne = exponent_;
|
||||
|
||||
auto [dp, dm, de] = y.toInternal(range);
|
||||
bool dp = y.negative_;
|
||||
int ds = (dp ? -1 : 1);
|
||||
auto dm = y.mantissa_;
|
||||
auto de = y.exponent_;
|
||||
|
||||
auto const& range = range_.get();
|
||||
auto const& minMantissa = range.min;
|
||||
auto const& maxMantissa = range.max;
|
||||
|
||||
@@ -828,7 +730,7 @@ Number::operator/=(Number const& y)
|
||||
// f can be up to 10^(38-19) = 10^19 safely
|
||||
static_assert(smallRange.log == 15);
|
||||
static_assert(largeRange.log == 18);
|
||||
bool small = range.scale == MantissaRange::small;
|
||||
bool small = Number::getMantissaScale() == MantissaRange::small;
|
||||
uint128_t const f = small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL;
|
||||
XRPL_ASSERT_PARTS(f >= minMantissa * 10, "Number::operator/=", "factor expected size");
|
||||
|
||||
@@ -878,8 +780,10 @@ Number::operator/=(Number const& y)
|
||||
}
|
||||
}
|
||||
normalize(zn, zm, ze, minMantissa, maxMantissa);
|
||||
fromInternal(zn, zm, ze, &range);
|
||||
XRPL_ASSERT_PARTS(isnormal(range), "xrpl::Number::operator/=", "result is normalized");
|
||||
negative_ = zn;
|
||||
mantissa_ = static_cast<internalrep>(zm);
|
||||
exponent_ = ze;
|
||||
XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::operator/=", "result is normalized");
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -892,7 +796,7 @@ operator rep() const
|
||||
Guard g;
|
||||
if (drops != 0)
|
||||
{
|
||||
if (drops < 0)
|
||||
if (negative_)
|
||||
{
|
||||
g.set_negative();
|
||||
drops = -drops;
|
||||
@@ -904,7 +808,7 @@ operator rep() const
|
||||
}
|
||||
for (; offset > 0; --offset)
|
||||
{
|
||||
if (drops >= largeRange.min)
|
||||
if (drops > maxRep / 10)
|
||||
throw std::overflow_error("Number::operator rep() overflow");
|
||||
drops *= 10;
|
||||
}
|
||||
@@ -934,22 +838,19 @@ Number::truncate() const noexcept
|
||||
std::string
|
||||
to_string(Number const& amount)
|
||||
{
|
||||
auto const& range = Number::range_.get();
|
||||
|
||||
// keep full internal accuracy, but make more human friendly if possible
|
||||
constexpr Number zero = Number{};
|
||||
if (amount == zero)
|
||||
return "0";
|
||||
|
||||
// The mantissa must have a set number of decimal places for this to work
|
||||
auto [negative, mantissa, exponent] = amount.toInternal(range);
|
||||
auto exponent = amount.exponent_;
|
||||
auto mantissa = amount.mantissa_;
|
||||
bool const negative = amount.negative_;
|
||||
|
||||
// Use scientific notation for exponents that are too small or too large
|
||||
auto const rangeLog = range.log;
|
||||
if (((exponent != 0 && amount.exponent() != 0) &&
|
||||
((exponent < -(rangeLog + 10)) || (exponent > -(rangeLog - 10)))))
|
||||
auto const rangeLog = Number::mantissaLog();
|
||||
if (((exponent != 0) && ((exponent < -(rangeLog + 10)) || (exponent > -(rangeLog - 10)))))
|
||||
{
|
||||
// Remove trailing zeroes from the mantissa.
|
||||
while (mantissa != 0 && mantissa % 10 == 0 && exponent < Number::maxExponent)
|
||||
{
|
||||
mantissa /= 10;
|
||||
@@ -957,11 +858,8 @@ to_string(Number const& amount)
|
||||
}
|
||||
std::string ret = negative ? "-" : "";
|
||||
ret.append(std::to_string(mantissa));
|
||||
if (exponent != 0)
|
||||
{
|
||||
ret.append(1, 'e');
|
||||
ret.append(std::to_string(exponent));
|
||||
}
|
||||
ret.append(1, 'e');
|
||||
ret.append(std::to_string(exponent));
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1045,11 +943,20 @@ power(Number const& f, unsigned n)
|
||||
return r;
|
||||
}
|
||||
|
||||
// Returns f^(1/d)
|
||||
// Uses Newton–Raphson iterations until the result stops changing
|
||||
// to find the non-negative root of the polynomial g(x) = x^d - f
|
||||
|
||||
// This function, and power(Number f, unsigned n, unsigned d)
|
||||
// treat corner cases such as 0 roots as advised by Annex F of
|
||||
// the C standard, which itself is consistent with the IEEE
|
||||
// floating point standards.
|
||||
|
||||
Number
|
||||
Number::root(MantissaRange const& range, Number f, unsigned d)
|
||||
root(Number f, unsigned d)
|
||||
{
|
||||
constexpr Number zero = Number{};
|
||||
auto const one = Number::one(range);
|
||||
auto const one = Number::one();
|
||||
|
||||
if (f == one || d == 1)
|
||||
return f;
|
||||
@@ -1066,28 +973,21 @@ Number::root(MantissaRange const& range, Number f, unsigned d)
|
||||
if (f == zero)
|
||||
return f;
|
||||
|
||||
auto const [e, di] = [&]() {
|
||||
auto const [negative, mantissa, exponent] = f.toInternal(range);
|
||||
|
||||
// Scale f into the range (0, 1) such that the scale change (e) is a
|
||||
// multiple of the root (d)
|
||||
auto e = exponent + range.log + 1;
|
||||
auto const di = static_cast<int>(d);
|
||||
auto ex = [e = e, di = di]() // Euclidean remainder of e/d
|
||||
{
|
||||
int k = (e >= 0 ? e : e - (di - 1)) / di;
|
||||
int k2 = e - k * di;
|
||||
if (k2 == 0)
|
||||
return 0;
|
||||
return di - k2;
|
||||
}();
|
||||
e += ex;
|
||||
f = f.shiftExponent(-e); // f /= 10^e;
|
||||
return std::make_tuple(e, di);
|
||||
// Scale f into the range (0, 1) such that f's exponent is a multiple of d
|
||||
auto e = f.exponent_ + Number::mantissaLog() + 1;
|
||||
auto const di = static_cast<int>(d);
|
||||
auto ex = [e = e, di = di]() // Euclidean remainder of e/d
|
||||
{
|
||||
int k = (e >= 0 ? e : e - (di - 1)) / di;
|
||||
int k2 = e - k * di;
|
||||
if (k2 == 0)
|
||||
return 0;
|
||||
return di - k2;
|
||||
}();
|
||||
e += ex;
|
||||
f = f.shiftExponent(-e); // f /= 10^e;
|
||||
|
||||
XRPL_ASSERT_PARTS(e % di == 0, "xrpl::root(Number, unsigned)", "e is divisible by d");
|
||||
XRPL_ASSERT_PARTS(f.isnormal(range), "xrpl::root(Number, unsigned)", "f is normalized");
|
||||
XRPL_ASSERT_PARTS(f.isnormal(), "xrpl::root(Number, unsigned)", "f is normalized");
|
||||
bool neg = false;
|
||||
if (f < zero)
|
||||
{
|
||||
@@ -1120,33 +1020,15 @@ Number::root(MantissaRange const& range, Number f, unsigned d)
|
||||
|
||||
// return r * 10^(e/d) to reverse scaling
|
||||
auto const result = r.shiftExponent(e / di);
|
||||
XRPL_ASSERT_PARTS(
|
||||
result.isnormal(range), "xrpl::root(Number, unsigned)", "result is normalized");
|
||||
XRPL_ASSERT_PARTS(result.isnormal(), "xrpl::root(Number, unsigned)", "result is normalized");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns f^(1/d)
|
||||
// Uses Newton–Raphson iterations until the result stops changing
|
||||
// to find the non-negative root of the polynomial g(x) = x^d - f
|
||||
|
||||
// This function, and power(Number f, unsigned n, unsigned d)
|
||||
// treat corner cases such as 0 roots as advised by Annex F of
|
||||
// the C standard, which itself is consistent with the IEEE
|
||||
// floating point standards.
|
||||
|
||||
Number
|
||||
root(Number f, unsigned d)
|
||||
{
|
||||
auto const& range = Number::range_.get();
|
||||
return Number::root(range, f, d);
|
||||
}
|
||||
|
||||
Number
|
||||
root2(Number f)
|
||||
{
|
||||
auto const& range = Number::range_.get();
|
||||
constexpr Number zero = Number{};
|
||||
auto const one = Number::one(range);
|
||||
auto const one = Number::one();
|
||||
|
||||
if (f == one)
|
||||
return f;
|
||||
@@ -1155,18 +1037,12 @@ root2(Number f)
|
||||
if (f == zero)
|
||||
return f;
|
||||
|
||||
auto const e = [&]() {
|
||||
auto const [negative, mantissa, exponent] = f.toInternal(range);
|
||||
|
||||
// Scale f into the range (0, 1) such that f's exponent is a
|
||||
// multiple of d
|
||||
auto e = exponent + range.log + 1;
|
||||
if (e % 2 != 0)
|
||||
++e;
|
||||
f = f.shiftExponent(-e); // f /= 10^e;
|
||||
return e;
|
||||
}();
|
||||
XRPL_ASSERT_PARTS(f.isnormal(range), "xrpl::root2(Number)", "f is normalized");
|
||||
// Scale f into the range (0, 1) such that f's exponent is a multiple of d
|
||||
auto e = f.exponent_ + Number::mantissaLog() + 1;
|
||||
if (e % 2 != 0)
|
||||
++e;
|
||||
f = f.shiftExponent(-e); // f /= 10^e;
|
||||
XRPL_ASSERT_PARTS(f.isnormal(), "xrpl::root2(Number)", "f is normalized");
|
||||
|
||||
// Quadratic least squares curve fit of f^(1/d) in the range [0, 1]
|
||||
auto const D = 105;
|
||||
@@ -1188,7 +1064,7 @@ root2(Number f)
|
||||
|
||||
// return r * 10^(e/2) to reverse scaling
|
||||
auto const result = r.shiftExponent(e / 2);
|
||||
XRPL_ASSERT_PARTS(result.isnormal(range), "xrpl::root2(Number)", "result is normalized");
|
||||
XRPL_ASSERT_PARTS(result.isnormal(), "xrpl::root2(Number)", "result is normalized");
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1198,10 +1074,8 @@ root2(Number f)
|
||||
Number
|
||||
power(Number const& f, unsigned n, unsigned d)
|
||||
{
|
||||
auto const& range = Number::range_.get();
|
||||
|
||||
constexpr Number zero = Number{};
|
||||
auto const one = Number::one(range);
|
||||
auto const one = Number::one();
|
||||
|
||||
if (f == one)
|
||||
return f;
|
||||
@@ -1223,7 +1097,7 @@ power(Number const& f, unsigned n, unsigned d)
|
||||
d /= g;
|
||||
if ((n % 2) == 1 && (d % 2) == 0 && f < zero)
|
||||
throw std::overflow_error("Number::power nan");
|
||||
return Number::root(range, power(f, n), d);
|
||||
return root(power(f, n), d);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -193,17 +193,17 @@ Value::Value(ValueType type) : type_(type), allocated_(0)
|
||||
}
|
||||
}
|
||||
|
||||
Value::Value(Int value) : type_(intValue), allocated_(0)
|
||||
Value::Value(Int value) : type_(intValue)
|
||||
{
|
||||
value_.int_ = value;
|
||||
}
|
||||
|
||||
Value::Value(UInt value) : type_(uintValue), allocated_(0)
|
||||
Value::Value(UInt value) : type_(uintValue)
|
||||
{
|
||||
value_.uint_ = value;
|
||||
}
|
||||
|
||||
Value::Value(double value) : type_(realValue), allocated_(0)
|
||||
Value::Value(double value) : type_(realValue)
|
||||
{
|
||||
value_.real_ = value;
|
||||
}
|
||||
@@ -230,7 +230,7 @@ Value::Value(StaticString const& value) : type_(stringValue), allocated_(false)
|
||||
value_.string_ = const_cast<char*>(value.c_str());
|
||||
}
|
||||
|
||||
Value::Value(bool value) : type_(booleanValue), allocated_(0)
|
||||
Value::Value(bool value) : type_(booleanValue)
|
||||
{
|
||||
value_.bool_ = value;
|
||||
}
|
||||
|
||||
@@ -335,28 +335,6 @@ ApplyStateTable::read(ReadView const& base, Keylet const& k) const
|
||||
return sle;
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<SLE const>>
|
||||
ApplyStateTable::readLocal(Keylet const& k) const
|
||||
{
|
||||
auto const iter = items_.find(k.key);
|
||||
if (iter == items_.end())
|
||||
return std::nullopt;
|
||||
auto const& item = iter->second;
|
||||
auto const& sle = item.second;
|
||||
switch (item.first)
|
||||
{
|
||||
case Action::erase:
|
||||
return nullptr;
|
||||
case Action::cache:
|
||||
case Action::insert:
|
||||
case Action::modify:
|
||||
break;
|
||||
};
|
||||
if (!k.check(*sle))
|
||||
return nullptr;
|
||||
return sle;
|
||||
}
|
||||
|
||||
std::shared_ptr<SLE>
|
||||
ApplyStateTable::peek(ReadView const& base, Keylet const& k)
|
||||
{
|
||||
|
||||
@@ -49,24 +49,7 @@ ApplyViewBase::succ(key_type const& key, std::optional<key_type> const& last) co
|
||||
std::shared_ptr<SLE const>
|
||||
ApplyViewBase::read(Keylet const& k) const
|
||||
{
|
||||
// Iteratively walk up the chain of ApplyViewBase layers
|
||||
// instead of recursing through items_.read(*base_, k).
|
||||
auto const* current = this;
|
||||
while (current)
|
||||
{
|
||||
if (auto result = current->items_.readLocal(k))
|
||||
return *result;
|
||||
|
||||
// Check if the base is another ApplyViewBase layer
|
||||
auto const* next = dynamic_cast<ApplyViewBase const*>(current->base_);
|
||||
if (!next)
|
||||
return current->base_->read(k);
|
||||
current = next;
|
||||
}
|
||||
// Unreachable: current starts as `this` (non-null) and the loop
|
||||
// always returns before current could become null.
|
||||
UNREACHABLE("xrpl::ApplyViewBase::read : unreachable");
|
||||
return nullptr;
|
||||
return items_.read(*base_, k);
|
||||
}
|
||||
|
||||
auto
|
||||
|
||||
@@ -26,12 +26,6 @@ HTTPClient::initializeSSLContext(
|
||||
httpClientSSLContext.emplace(sslVerifyDir, sslVerifyFile, sslVerify, j);
|
||||
}
|
||||
|
||||
void
|
||||
HTTPClient::cleanupSSLContext()
|
||||
{
|
||||
httpClientSSLContext.reset();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Fetch a web page via http or https.
|
||||
|
||||
@@ -33,7 +33,7 @@ DatabaseNodeImp::fetchNodeObject(
|
||||
|
||||
try
|
||||
{
|
||||
status = backend_->fetch(hash.data(), &nodeObject);
|
||||
status = backend_->fetch(hash, &nodeObject);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
@@ -68,18 +68,10 @@ DatabaseNodeImp::fetchBatch(std::vector<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(batch).first;
|
||||
auto results = backend_->fetchBatch(hashes).first;
|
||||
XRPL_ASSERT(
|
||||
results.size() == hashes.size() || results.empty(),
|
||||
"number of output objects either matches number of input hashes or is empty");
|
||||
|
||||
@@ -105,7 +105,7 @@ DatabaseRotatingImp::fetchNodeObject(
|
||||
std::shared_ptr<NodeObject> nodeObject;
|
||||
try
|
||||
{
|
||||
status = backend->fetch(hash.data(), &nodeObject);
|
||||
status = backend->fetch(hash, &nodeObject);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
|
||||
@@ -116,10 +116,9 @@ public:
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
Status
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) override
|
||||
fetch(uint256 const& hash, 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);
|
||||
|
||||
@@ -134,14 +133,14 @@ public:
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
fetchBatch(std::vector<uint256> 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->begin(), &nObj);
|
||||
Status status = fetch(h, &nObj);
|
||||
if (status != ok)
|
||||
results.push_back({});
|
||||
else
|
||||
|
||||
@@ -179,17 +179,17 @@ public:
|
||||
}
|
||||
|
||||
Status
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pno) override
|
||||
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pno) override
|
||||
{
|
||||
Status status;
|
||||
pno->reset();
|
||||
nudb::error_code ec;
|
||||
db_.fetch(
|
||||
key,
|
||||
[key, pno, &status](void const* data, std::size_t size) {
|
||||
hash.data(),
|
||||
[&hash, pno, &status](void const* data, std::size_t size) {
|
||||
nudb::detail::buffer bf;
|
||||
auto const result = nodeobject_decompress(data, size, bf);
|
||||
DecodedBlob decoded(key, result.first, result.second);
|
||||
DecodedBlob decoded(hash.data(), result.first, result.second);
|
||||
if (!decoded.wasOk())
|
||||
{
|
||||
status = dataCorrupt;
|
||||
@@ -207,14 +207,14 @@ public:
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
fetchBatch(std::vector<uint256> 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->begin(), &nObj);
|
||||
Status status = fetch(h, &nObj);
|
||||
if (status != ok)
|
||||
results.push_back({});
|
||||
else
|
||||
|
||||
@@ -36,13 +36,13 @@ public:
|
||||
}
|
||||
|
||||
Status
|
||||
fetch(void const*, std::shared_ptr<NodeObject>*) override
|
||||
fetch(uint256 const&, std::shared_ptr<NodeObject>*) override
|
||||
{
|
||||
return notFound;
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
fetchBatch(std::vector<uint256> const& hashes) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ public:
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
Status
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) override
|
||||
fetch(uint256 const& hash, 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(static_cast<char const*>(key), m_keyBytes);
|
||||
rocksdb::Slice const slice(std::bit_cast<char const*>(hash.data()), m_keyBytes);
|
||||
|
||||
std::string string;
|
||||
|
||||
@@ -260,7 +260,7 @@ public:
|
||||
|
||||
if (getStatus.ok())
|
||||
{
|
||||
DecodedBlob decoded(key, string.data(), string.size());
|
||||
DecodedBlob decoded(hash.data(), string.data(), string.size());
|
||||
|
||||
if (decoded.wasOk())
|
||||
{
|
||||
@@ -295,14 +295,14 @@ public:
|
||||
}
|
||||
|
||||
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
|
||||
fetchBatch(std::vector<uint256 const*> const& hashes) override
|
||||
fetchBatch(std::vector<uint256> 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->begin(), &nObj);
|
||||
Status status = fetch(h, &nObj);
|
||||
if (status != ok)
|
||||
results.push_back({});
|
||||
else
|
||||
@@ -332,9 +332,8 @@ public:
|
||||
EncodedBlob encoded(e);
|
||||
|
||||
wb.Put(
|
||||
rocksdb::Slice(reinterpret_cast<char const*>(encoded.getKey()), m_keyBytes),
|
||||
rocksdb::Slice(
|
||||
reinterpret_cast<char const*>(encoded.getData()), encoded.getSize()));
|
||||
rocksdb::Slice(std::bit_cast<char const*>(encoded.getKey()), m_keyBytes),
|
||||
rocksdb::Slice(std::bit_cast<char const*>(encoded.getData()), encoded.getSize()));
|
||||
}
|
||||
|
||||
rocksdb::WriteOptions const options;
|
||||
|
||||
@@ -1003,12 +1003,7 @@ amountFromJson(SField const& name, Json::Value const& v)
|
||||
else if (v.isString())
|
||||
{
|
||||
std::string val = v.asString();
|
||||
// Pre-allocate to avoid reallocation during split. This function is often
|
||||
// called deep in the RPC stack (via JSON parsing) where stack space is
|
||||
// limited. ASAN detected stack-buffer-overflow here at ~95% coroutine
|
||||
// stack usage (1001376/1048576 bytes). Coroutine stack increased to 2MB.
|
||||
std::vector<std::string> elements;
|
||||
elements.reserve(3);
|
||||
boost::split(elements, val, boost::is_any_of("\t\n\r ,/"));
|
||||
|
||||
if (elements.size() > 3)
|
||||
|
||||
@@ -69,7 +69,7 @@ make_name(std::string const& object, std::string const& field)
|
||||
if (field.empty())
|
||||
return object;
|
||||
|
||||
return {object + "." + field};
|
||||
return object + "." + field;
|
||||
}
|
||||
|
||||
static inline Json::Value
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Returns a depth mask by value to avoid potential lifetime issues in
|
||||
// multi-threaded contexts. Returning by const reference to a static member
|
||||
// could trigger stack-use-after-scope errors when the reference is used in
|
||||
// temporary expressions with operator& in concurrent coroutine scenarios.
|
||||
static uint256 const
|
||||
static uint256 const&
|
||||
depthMask(unsigned int depth)
|
||||
{
|
||||
enum { mask_size = 65 };
|
||||
@@ -76,8 +72,7 @@ SHAMapNodeID::getChildNodeID(unsigned int m) const
|
||||
if (depth_ >= SHAMap::leafDepth)
|
||||
Throw<std::logic_error>("Request for child node ID of " + to_string(*this));
|
||||
|
||||
auto const idAtDepth = id_ & depthMask(depth_);
|
||||
if (id_ != idAtDepth)
|
||||
if (id_ != (id_ & depthMask(depth_)))
|
||||
Throw<std::logic_error>("Incorrect mask for " + to_string(*this));
|
||||
|
||||
SHAMapNodeID node{depth_ + 1, id_};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#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/ApplyContext.h>
|
||||
#include <xrpl/tx/InvariantCheck.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheck.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
305
src/libxrpl/tx/invariants/AMMInvariant.cpp
Normal file
305
src/libxrpl/tx/invariants/AMMInvariant.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
#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
|
||||
278
src/libxrpl/tx/invariants/FreezeInvariant.cpp
Normal file
278
src/libxrpl/tx/invariants/FreezeInvariant.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#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
|
||||
1009
src/libxrpl/tx/invariants/InvariantCheck.cpp
Normal file
1009
src/libxrpl/tx/invariants/InvariantCheck.cpp
Normal file
File diff suppressed because it is too large
Load Diff
278
src/libxrpl/tx/invariants/LoanInvariant.cpp
Normal file
278
src/libxrpl/tx/invariants/LoanInvariant.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#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
|
||||
192
src/libxrpl/tx/invariants/MPTInvariant.cpp
Normal file
192
src/libxrpl/tx/invariants/MPTInvariant.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
#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
|
||||
274
src/libxrpl/tx/invariants/NFTInvariant.cpp
Normal file
274
src/libxrpl/tx/invariants/NFTInvariant.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
#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
|
||||
93
src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp
Normal file
93
src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#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
|
||||
162
src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp
Normal file
162
src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#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
|
||||
926
src/libxrpl/tx/invariants/VaultInvariant.cpp
Normal file
926
src/libxrpl/tx/invariants/VaultInvariant.cpp
Normal file
@@ -0,0 +1,926 @@
|
||||
#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,10 +1,9 @@
|
||||
#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 {
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ private:
|
||||
std::vector<std::string> emptyCfgKeys;
|
||||
struct publisher
|
||||
{
|
||||
publisher(FetchListConfig const& c) : cfg{c}, isRetry{false}
|
||||
publisher(FetchListConfig const& c) : cfg{c}
|
||||
{
|
||||
}
|
||||
std::shared_ptr<TrustedPublisherServer> server;
|
||||
|
||||
@@ -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_EXPECT(false);
|
||||
BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
|
||||
}
|
||||
catch (parse_error const& e)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
BEAST_EXPECT(
|
||||
e.what() ==
|
||||
"invalidParamsField 'tx_json.AssetsMaximum' has invalid "
|
||||
"data."s);
|
||||
e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s);
|
||||
}
|
||||
env.set_parse_failure_expected(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,9 @@ public:
|
||||
test_limits()
|
||||
{
|
||||
auto const scale = Number::getMantissaScale();
|
||||
auto const minMantissa = Number::minMantissa();
|
||||
|
||||
testcase << "test_limits " << to_string(scale) << ", " << minMantissa;
|
||||
testcase << "test_limits " << to_string(scale);
|
||||
bool caught = false;
|
||||
auto const minMantissa = Number::minMantissa();
|
||||
try
|
||||
{
|
||||
Number x = Number{false, minMantissa * 10, 32768, Number::normalized{}};
|
||||
@@ -59,9 +58,8 @@ public:
|
||||
__LINE__);
|
||||
test(Number{false, minMantissa, -32769, Number::normalized{}}, Number{}, __LINE__);
|
||||
test(
|
||||
// Use 1501 to force rounding up
|
||||
Number{false, minMantissa, 32000, Number::normalized{}} * 1'000 +
|
||||
Number{false, 1'501, 32000, Number::normalized{}},
|
||||
Number{false, 1'500, 32000, Number::normalized{}},
|
||||
Number{false, minMantissa + 2, 32003, Number::normalized{}},
|
||||
__LINE__);
|
||||
// 9,223,372,036,854,775,808
|
||||
@@ -170,12 +168,8 @@ public:
|
||||
{Number{true, 9'999'999'999'999'999'999ULL, -37, Number::normalized{}},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::normalized{}}},
|
||||
{Number{Number::largestMantissa},
|
||||
Number{6, -1},
|
||||
Number{Number::largestMantissa / 10, 1}},
|
||||
{Number{Number::largestMantissa - 1},
|
||||
Number{1, 0},
|
||||
Number{Number::largestMantissa}},
|
||||
{Number{Number::maxRep}, Number{6, -1}, Number{Number::maxRep / 10, 1}},
|
||||
{Number{Number::maxRep - 1}, Number{1, 0}, Number{Number::maxRep}},
|
||||
// Test extremes
|
||||
{
|
||||
// Each Number operand rounds up, so the actual mantissa is
|
||||
@@ -185,18 +179,11 @@ public:
|
||||
Number{2, 19},
|
||||
},
|
||||
{
|
||||
// Does not round. Mantissas are going to be >
|
||||
// largestMantissa, so if added together as uint64_t's, the
|
||||
// result will overflow. With addition using uint128_t,
|
||||
// there's no problem. After normalizing, the resulting
|
||||
// mantissa ends up less than largestMantissa.
|
||||
Number{false, Number::largestMantissa, 0, Number::normalized{}},
|
||||
Number{false, Number::largestMantissa, 0, Number::normalized{}},
|
||||
Number{false, Number::largestMantissa * 2, 0, Number::normalized{}},
|
||||
},
|
||||
{
|
||||
// These mantissas round down, so adding them together won't
|
||||
// have any consequences.
|
||||
// Does not round. Mantissas are going to be > maxRep, so if
|
||||
// added together as uint64_t's, the result will overflow.
|
||||
// With addition using uint128_t, there's no problem. After
|
||||
// normalizing, the resulting mantissa ends up less than
|
||||
// maxRep.
|
||||
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::normalized{}},
|
||||
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::normalized{}},
|
||||
Number{false, 1'999'999'999'999'999'998ULL, 1, Number::normalized{}},
|
||||
@@ -285,16 +272,14 @@ public:
|
||||
{Number{1'000'000'000'000'000'001, -18},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{1'000'000'000'000'000'000, -36}},
|
||||
{Number{Number::largestMantissa},
|
||||
Number{6, -1},
|
||||
Number{Number::largestMantissa - 1}},
|
||||
{Number{false, Number::largestMantissa + 1, 0, Number::normalized{}},
|
||||
{Number{Number::maxRep}, Number{6, -1}, Number{Number::maxRep - 1}},
|
||||
{Number{false, Number::maxRep + 1, 0, Number::normalized{}},
|
||||
Number{1, 0},
|
||||
Number{Number::largestMantissa / 10 + 1, 1}},
|
||||
{Number{false, Number::largestMantissa + 1, 0, Number::normalized{}},
|
||||
Number{Number::maxRep / 10 + 1, 1}},
|
||||
{Number{false, Number::maxRep + 1, 0, Number::normalized{}},
|
||||
Number{3, 0},
|
||||
Number{Number::largestMantissa}},
|
||||
{power(2, 63), Number{3, 0}, Number{Number::largestMantissa}},
|
||||
Number{Number::maxRep}},
|
||||
{power(2, 63), Number{3, 0}, Number{Number::maxRep}},
|
||||
});
|
||||
auto test = [this](auto const& c) {
|
||||
for (auto const& [x, y, z] : c)
|
||||
@@ -317,15 +302,14 @@ public:
|
||||
auto const scale = Number::getMantissaScale();
|
||||
testcase << "test_mul " << to_string(scale);
|
||||
|
||||
// Case: Factor 1, Factor 2, Expected product, Line number
|
||||
using Case = std::tuple<Number, Number, Number, int>;
|
||||
using Case = std::tuple<Number, Number, Number>;
|
||||
auto test = [this](auto const& c) {
|
||||
for (auto const& [x, y, z, line] : c)
|
||||
for (auto const& [x, y, z] : c)
|
||||
{
|
||||
auto const result = x * y;
|
||||
std::stringstream ss;
|
||||
ss << x << " * " << y << " = " << result << ". Expected: " << z;
|
||||
BEAST_EXPECTS(result == z, ss.str() + " line: " + std::to_string(line));
|
||||
BEAST_EXPECTS(result == z, ss.str());
|
||||
}
|
||||
};
|
||||
auto tests = [&](auto const& cSmall, auto const& cLarge) {
|
||||
@@ -335,100 +319,70 @@ public:
|
||||
test(cLarge);
|
||||
};
|
||||
auto const maxMantissa = Number::maxMantissa();
|
||||
auto const maxInternalMantissa = static_cast<std::uint64_t>(static_cast<std::int64_t>(
|
||||
power(10, Number::mantissaLog()))) *
|
||||
10 -
|
||||
1;
|
||||
|
||||
saveNumberRoundMode save{Number::setround(Number::to_nearest)};
|
||||
{
|
||||
auto const cSmall = std::to_array<Case>({
|
||||
{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{2000000000000000, -15},
|
||||
__LINE__},
|
||||
Number{2000000000000000, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-2000000000000000, -15},
|
||||
__LINE__},
|
||||
Number{-2000000000000000, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{2000000000000000, -15},
|
||||
__LINE__},
|
||||
Number{2000000000000000, -15}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{1000000000000000, -14},
|
||||
__LINE__},
|
||||
{Number{1000000000000000, -32768},
|
||||
Number{1000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
Number{1000000000000000, -14}},
|
||||
{Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}},
|
||||
// Maximum mantissa range
|
||||
{Number{9'999'999'999'999'999, 0},
|
||||
Number{9'999'999'999'999'999, 0},
|
||||
Number{9'999'999'999'999'998, 16},
|
||||
__LINE__},
|
||||
Number{9'999'999'999'999'998, 16}},
|
||||
});
|
||||
auto const cLarge = std::to_array<Case>({
|
||||
// Note that items with extremely large mantissas need to be
|
||||
// calculated, because otherwise they overflow uint64. Items
|
||||
// from C with larger mantissa
|
||||
{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{1999999999999999862, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999862, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-1999999999999999862, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999862, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{1999999999999999862, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999862, -18}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{false, 9'999'999'999'999'999'579ULL, -18, Number::normalized{}},
|
||||
__LINE__},
|
||||
Number{false, 9'999'999'999'999'999'579ULL, -18, Number::normalized{}}},
|
||||
{Number{1000000000000000000, -32768},
|
||||
Number{1000000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
Number{0}},
|
||||
// Items from cSmall expanded for the larger mantissa,
|
||||
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2
|
||||
// with higher precision
|
||||
{Number{1414213562373095049, -18},
|
||||
Number{1414213562373095049, -18},
|
||||
Number{2000000000000000001, -18},
|
||||
__LINE__},
|
||||
Number{2000000000000000001, -18}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{1414213562373095048, -18},
|
||||
Number{-1999999999999999998, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999998, -18}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{-1414213562373095049, -18},
|
||||
Number{1999999999999999999, -18},
|
||||
__LINE__},
|
||||
{Number{3214285714285714278, -18},
|
||||
Number{3111111111111111119, -18},
|
||||
Number{10, 0},
|
||||
__LINE__},
|
||||
// Maximum internal mantissa range - rounds up to 1e19
|
||||
{Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{1, 38},
|
||||
__LINE__},
|
||||
// Maximum actual mantissa range - same as int64 range
|
||||
Number{1999999999999999999, -18}},
|
||||
{Number{3214285714285714278, -18}, Number{3111111111111111119, -18}, Number{10, 0}},
|
||||
// Maximum mantissa range - rounds up to 1e19
|
||||
{Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{85'070'591'730'234'615'85, 19},
|
||||
__LINE__},
|
||||
Number{1, 38}},
|
||||
// Maximum int64 range
|
||||
{Number{Number::largestMantissa, 0},
|
||||
Number{Number::largestMantissa, 0},
|
||||
Number{85'070'591'730'234'615'85, 19},
|
||||
__LINE__},
|
||||
{Number{Number::maxRep, 0},
|
||||
Number{Number::maxRep, 0},
|
||||
Number{85'070'591'730'234'615'85, 19}},
|
||||
});
|
||||
tests(cSmall, cLarge);
|
||||
}
|
||||
@@ -436,90 +390,66 @@ public:
|
||||
testcase << "test_mul " << to_string(Number::getMantissaScale()) << " towards_zero";
|
||||
{
|
||||
auto const cSmall = std::to_array<Case>(
|
||||
{{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{1999999999999999, -15},
|
||||
__LINE__},
|
||||
Number{1999999999999999, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-1999999999999999, -15},
|
||||
__LINE__},
|
||||
Number{-1999999999999999, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{1999999999999999, -15},
|
||||
__LINE__},
|
||||
Number{1999999999999999, -15}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{9999999999999999, -15},
|
||||
__LINE__},
|
||||
{Number{1000000000000000, -32768},
|
||||
Number{1000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__}});
|
||||
Number{9999999999999999, -15}},
|
||||
{Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}});
|
||||
auto const cLarge = std::to_array<Case>(
|
||||
// Note that items with extremely large mantissas need to be
|
||||
// calculated, because otherwise they overflow uint64. Items
|
||||
// from C with larger mantissa
|
||||
{
|
||||
{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{1999999999999999861, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999861, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-1999999999999999861, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999861, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{1999999999999999861, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999861, -18}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{false, 9999999999999999579ULL, -18, Number::normalized{}},
|
||||
__LINE__},
|
||||
Number{false, 9999999999999999579ULL, -18, Number::normalized{}}},
|
||||
{Number{1000000000000000000, -32768},
|
||||
Number{1000000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
Number{0}},
|
||||
// Items from cSmall expanded for the larger mantissa,
|
||||
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2
|
||||
// with higher precision
|
||||
{Number{1414213562373095049, -18},
|
||||
Number{1414213562373095049, -18},
|
||||
Number{2, 0},
|
||||
__LINE__},
|
||||
Number{2, 0}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{1414213562373095048, -18},
|
||||
Number{-1999999999999999997, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999997, -18}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{-1414213562373095049, -18},
|
||||
Number{1999999999999999999, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999999, -18}},
|
||||
{Number{3214285714285714278, -18},
|
||||
Number{3111111111111111119, -18},
|
||||
Number{10, 0},
|
||||
__LINE__},
|
||||
// Maximum internal mantissa range - rounds down to
|
||||
// maxMantissa/10e1
|
||||
Number{10, 0}},
|
||||
// Maximum mantissa range - rounds down to maxMantissa/10e1
|
||||
// 99'999'999'999'999'999'800'000'000'000'000'000'100
|
||||
{Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxInternalMantissa / 10 - 1, 20, Number::normalized{}},
|
||||
__LINE__},
|
||||
// Maximum actual mantissa range - same as int64
|
||||
{Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{85'070'591'730'234'615'84, 19},
|
||||
__LINE__},
|
||||
Number{false, maxMantissa / 10 - 1, 20, Number::normalized{}}},
|
||||
// Maximum int64 range
|
||||
// 85'070'591'730'234'615'847'396'907'784'232'501'249
|
||||
{Number{Number::largestMantissa, 0},
|
||||
Number{Number::largestMantissa, 0},
|
||||
Number{85'070'591'730'234'615'84, 19},
|
||||
__LINE__},
|
||||
{Number{Number::maxRep, 0},
|
||||
Number{Number::maxRep, 0},
|
||||
Number{85'070'591'730'234'615'84, 19}},
|
||||
});
|
||||
tests(cSmall, cLarge);
|
||||
}
|
||||
@@ -527,90 +457,66 @@ public:
|
||||
testcase << "test_mul " << to_string(Number::getMantissaScale()) << " downward";
|
||||
{
|
||||
auto const cSmall = std::to_array<Case>(
|
||||
{{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{1999999999999999, -15},
|
||||
__LINE__},
|
||||
Number{1999999999999999, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-2000000000000000, -15},
|
||||
__LINE__},
|
||||
Number{-2000000000000000, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{1999999999999999, -15},
|
||||
__LINE__},
|
||||
Number{1999999999999999, -15}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{9999999999999999, -15},
|
||||
__LINE__},
|
||||
{Number{1000000000000000, -32768},
|
||||
Number{1000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__}});
|
||||
Number{9999999999999999, -15}},
|
||||
{Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}});
|
||||
auto const cLarge = std::to_array<Case>(
|
||||
// Note that items with extremely large mantissas need to be
|
||||
// calculated, because otherwise they overflow uint64. Items
|
||||
// from C with larger mantissa
|
||||
{
|
||||
{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{1999999999999999861, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999861, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-1999999999999999862, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999862, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{1999999999999999861, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999861, -18}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{false, 9'999'999'999'999'999'579ULL, -18, Number::normalized{}},
|
||||
__LINE__},
|
||||
Number{false, 9'999'999'999'999'999'579ULL, -18, Number::normalized{}}},
|
||||
{Number{1000000000000000000, -32768},
|
||||
Number{1000000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
Number{0}},
|
||||
// Items from cSmall expanded for the larger mantissa,
|
||||
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2
|
||||
// with higher precision
|
||||
{Number{1414213562373095049, -18},
|
||||
Number{1414213562373095049, -18},
|
||||
Number{2, 0},
|
||||
__LINE__},
|
||||
Number{2, 0}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{1414213562373095048, -18},
|
||||
Number{-1999999999999999998, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999998, -18}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{-1414213562373095049, -18},
|
||||
Number{1999999999999999999, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999999, -18}},
|
||||
{Number{3214285714285714278, -18},
|
||||
Number{3111111111111111119, -18},
|
||||
Number{10, 0},
|
||||
__LINE__},
|
||||
// Maximum internal mantissa range - rounds down to
|
||||
// maxMantissa/10-1
|
||||
Number{10, 0}},
|
||||
// Maximum mantissa range - rounds down to maxMantissa/10e1
|
||||
// 99'999'999'999'999'999'800'000'000'000'000'000'100
|
||||
{Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxInternalMantissa / 10 - 1, 20, Number::normalized{}},
|
||||
__LINE__},
|
||||
// Maximum mantissa range - same as int64
|
||||
{Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{85'070'591'730'234'615'84, 19},
|
||||
__LINE__},
|
||||
Number{false, maxMantissa / 10 - 1, 20, Number::normalized{}}},
|
||||
// Maximum int64 range
|
||||
// 85'070'591'730'234'615'847'396'907'784'232'501'249
|
||||
{Number{Number::largestMantissa, 0},
|
||||
Number{Number::largestMantissa, 0},
|
||||
Number{85'070'591'730'234'615'84, 19},
|
||||
__LINE__},
|
||||
{Number{Number::maxRep, 0},
|
||||
Number{Number::maxRep, 0},
|
||||
Number{85'070'591'730'234'615'84, 19}},
|
||||
});
|
||||
tests(cSmall, cLarge);
|
||||
}
|
||||
@@ -618,89 +524,66 @@ public:
|
||||
testcase << "test_mul " << to_string(Number::getMantissaScale()) << " upward";
|
||||
{
|
||||
auto const cSmall = std::to_array<Case>(
|
||||
{{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{2000000000000000, -15},
|
||||
__LINE__},
|
||||
Number{2000000000000000, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-1999999999999999, -15},
|
||||
__LINE__},
|
||||
Number{-1999999999999999, -15}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{2000000000000000, -15},
|
||||
__LINE__},
|
||||
Number{2000000000000000, -15}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{1000000000000000, -14},
|
||||
__LINE__},
|
||||
{Number{1000000000000000, -32768},
|
||||
Number{1000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__}});
|
||||
Number{1000000000000000, -14}},
|
||||
{Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}});
|
||||
auto const cLarge = std::to_array<Case>(
|
||||
// Note that items with extremely large mantissas need to be
|
||||
// calculated, because otherwise they overflow uint64. Items
|
||||
// from C with larger mantissa
|
||||
{
|
||||
{Number{7}, Number{8}, Number{56}, __LINE__},
|
||||
{Number{7}, Number{8}, Number{56}},
|
||||
{Number{1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{1999999999999999862, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999862, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{1414213562373095, -15},
|
||||
Number{-1999999999999999861, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999861, -18}},
|
||||
{Number{-1414213562373095, -15},
|
||||
Number{-1414213562373095, -15},
|
||||
Number{1999999999999999862, -18},
|
||||
__LINE__},
|
||||
Number{1999999999999999862, -18}},
|
||||
{Number{3214285714285706, -15},
|
||||
Number{3111111111111119, -15},
|
||||
Number{999999999999999958, -17},
|
||||
__LINE__},
|
||||
Number{999999999999999958, -17}},
|
||||
{Number{1000000000000000000, -32768},
|
||||
Number{1000000000000000000, -32768},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
Number{0}},
|
||||
// Items from cSmall expanded for the larger mantissa,
|
||||
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2
|
||||
// with higher precision
|
||||
{Number{1414213562373095049, -18},
|
||||
Number{1414213562373095049, -18},
|
||||
Number{2000000000000000001, -18},
|
||||
__LINE__},
|
||||
Number{2000000000000000001, -18}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{1414213562373095048, -18},
|
||||
Number{-1999999999999999997, -18},
|
||||
__LINE__},
|
||||
Number{-1999999999999999997, -18}},
|
||||
{Number{-1414213562373095048, -18},
|
||||
Number{-1414213562373095049, -18},
|
||||
Number{2, 0},
|
||||
__LINE__},
|
||||
Number{2, 0}},
|
||||
{Number{3214285714285714278, -18},
|
||||
Number{3111111111111111119, -18},
|
||||
Number{1000000000000000001, -17},
|
||||
__LINE__},
|
||||
// Maximum internal mantissa range - rounds up to
|
||||
// minMantissa*10 1e19*1e19=1e38
|
||||
{Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxInternalMantissa, 0, Number::normalized{}},
|
||||
Number{1, 38},
|
||||
__LINE__},
|
||||
// Maximum mantissa range - same as int64
|
||||
Number{1000000000000000001, -17}},
|
||||
// Maximum mantissa range - rounds up to minMantissa*10
|
||||
// 1e19*1e19=1e38
|
||||
{Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{false, maxMantissa, 0, Number::normalized{}},
|
||||
Number{85'070'591'730'234'615'85, 19},
|
||||
__LINE__},
|
||||
Number{1, 38}},
|
||||
// Maximum int64 range
|
||||
// 85'070'591'730'234'615'847'396'907'784'232'501'249
|
||||
{Number{Number::largestMantissa, 0},
|
||||
Number{Number::largestMantissa, 0},
|
||||
Number{85'070'591'730'234'615'85, 19},
|
||||
__LINE__},
|
||||
{Number{Number::maxRep, 0},
|
||||
Number{Number::maxRep, 0},
|
||||
Number{85'070'591'730'234'615'85, 19}},
|
||||
});
|
||||
tests(cSmall, cLarge);
|
||||
}
|
||||
@@ -931,11 +814,6 @@ public:
|
||||
};
|
||||
*/
|
||||
|
||||
auto const maxInternalMantissa = static_cast<std::uint64_t>(static_cast<std::int64_t>(
|
||||
power(10, Number::mantissaLog()))) *
|
||||
10 -
|
||||
1;
|
||||
|
||||
auto const cSmall = std::to_array<Case>(
|
||||
{{Number{2}, 2, Number{1414213562373095049, -18}},
|
||||
{Number{2'000'000}, 2, Number{1414213562373095049, -15}},
|
||||
@@ -947,16 +825,16 @@ public:
|
||||
{Number{0}, 5, Number{0}},
|
||||
{Number{5625, -4}, 2, Number{75, -2}}});
|
||||
auto const cLarge = std::to_array<Case>({
|
||||
{Number{false, maxInternalMantissa - 9, -1, Number::normalized{}},
|
||||
{Number{false, Number::maxMantissa() - 9, -1, Number::normalized{}},
|
||||
2,
|
||||
Number{false, 999'999'999'999'999'999, -9, Number::normalized{}}},
|
||||
{Number{false, maxInternalMantissa - 9, 0, Number::normalized{}},
|
||||
{Number{false, Number::maxMantissa() - 9, 0, Number::normalized{}},
|
||||
2,
|
||||
Number{false, 3'162'277'660'168'379'330, -9, Number::normalized{}}},
|
||||
{Number{Number::largestMantissa},
|
||||
{Number{Number::maxRep},
|
||||
2,
|
||||
Number{false, 3'037'000'499'976049692, -9, Number::normalized{}}},
|
||||
{Number{Number::largestMantissa},
|
||||
{Number{Number::maxRep},
|
||||
4,
|
||||
Number{false, 55'108'98747006743627, -14, Number::normalized{}}},
|
||||
});
|
||||
@@ -1005,8 +883,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
auto const maxInternalMantissa = power(10, Number::mantissaLog()) * 10 - 1;
|
||||
|
||||
auto const cSmall = std::to_array<Number>({
|
||||
Number{2},
|
||||
Number{2'000'000},
|
||||
@@ -1016,10 +892,7 @@ public:
|
||||
Number{5, -1},
|
||||
Number{0},
|
||||
Number{5625, -4},
|
||||
Number{Number::largestMantissa},
|
||||
maxInternalMantissa,
|
||||
Number{Number::minMantissa(), 0, Number::unchecked{}},
|
||||
Number{Number::maxMantissa(), 0, Number::unchecked{}},
|
||||
Number{Number::maxRep},
|
||||
});
|
||||
test(cSmall);
|
||||
bool caught = false;
|
||||
@@ -1370,18 +1243,18 @@ public:
|
||||
case MantissaRange::large:
|
||||
// Test the edges
|
||||
// ((exponent < -(28)) || (exponent > -(8)))))
|
||||
test(Number::min(), "922337203685477581e-32768");
|
||||
test(Number::min(), "1e-32750");
|
||||
test(Number::max(), "9223372036854775807e32768");
|
||||
test(Number::lowest(), "-9223372036854775807e32768");
|
||||
{
|
||||
NumberRoundModeGuard mg(Number::towards_zero);
|
||||
|
||||
auto const maxMantissa = Number::maxMantissa();
|
||||
BEAST_EXPECT(maxMantissa == 9'223'372'036'854'775'807ULL);
|
||||
BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL);
|
||||
test(
|
||||
Number{false, maxMantissa, 0, Number::normalized{}}, "9223372036854775807");
|
||||
Number{false, maxMantissa, 0, Number::normalized{}}, "9999999999999999990");
|
||||
test(
|
||||
Number{true, maxMantissa, 0, Number::normalized{}}, "-9223372036854775807");
|
||||
Number{true, maxMantissa, 0, Number::normalized{}}, "-9999999999999999990");
|
||||
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::max(), 0}, "9223372036854775807");
|
||||
@@ -1617,7 +1490,7 @@ public:
|
||||
Number const initalXrp{INITIAL_XRP};
|
||||
BEAST_EXPECT(initalXrp.exponent() > 0);
|
||||
|
||||
Number const maxInt64{Number::largestMantissa};
|
||||
Number const maxInt64{Number::maxRep};
|
||||
BEAST_EXPECT(maxInt64.exponent() > 0);
|
||||
// 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits
|
||||
BEAST_EXPECT((power(maxInt64, 2) == Number{85'070'591'730'234'62, 22}));
|
||||
@@ -1634,217 +1507,21 @@ public:
|
||||
Number const initalXrp{INITIAL_XRP};
|
||||
BEAST_EXPECT(initalXrp.exponent() <= 0);
|
||||
|
||||
Number const maxInt64{Number::largestMantissa};
|
||||
Number const maxInt64{Number::maxRep};
|
||||
BEAST_EXPECT(maxInt64.exponent() <= 0);
|
||||
// 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits
|
||||
BEAST_EXPECT((power(maxInt64, 2) == Number{85'070'591'730'234'615'85, 19}));
|
||||
|
||||
NumberRoundModeGuard mg(Number::towards_zero);
|
||||
|
||||
{
|
||||
auto const maxInternalMantissa =
|
||||
static_cast<std::uint64_t>(
|
||||
static_cast<std::int64_t>(power(10, Number::mantissaLog()))) *
|
||||
10 -
|
||||
1;
|
||||
|
||||
// Rounds down to fit under 2^63
|
||||
Number const max = Number{false, maxInternalMantissa, 0, Number::normalized{}};
|
||||
// No alterations by the accessors
|
||||
BEAST_EXPECT(max.mantissa() == maxInternalMantissa / 10);
|
||||
BEAST_EXPECT(max.exponent() == 1);
|
||||
// 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38
|
||||
// digits
|
||||
BEAST_EXPECT(
|
||||
(power(max, 2) ==
|
||||
Number{false, maxInternalMantissa / 10 - 1, 20, Number::normalized{}}));
|
||||
}
|
||||
|
||||
{
|
||||
auto const maxMantissa = Number::maxMantissa();
|
||||
Number const max = Number{false, maxMantissa, 0, Number::normalized{}};
|
||||
// No alterations by the accessors
|
||||
BEAST_EXPECT(max.mantissa() == maxMantissa);
|
||||
BEAST_EXPECT(max.exponent() == 0);
|
||||
// 85'070'591'730'234'615'847'396'907'784'232'501'249 - also 38
|
||||
// digits
|
||||
BEAST_EXPECT(
|
||||
(power(max, 2) ==
|
||||
Number{false, 85'070'591'730'234'615'84, 19, Number::normalized{}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testNormalizeToRange()
|
||||
{
|
||||
// Test edge-cases of normalizeToRange
|
||||
auto const scale = Number::getMantissaScale();
|
||||
testcase << "normalizeToRange " << to_string(scale);
|
||||
|
||||
auto test = [this](
|
||||
Number const& n,
|
||||
auto const rangeMin,
|
||||
auto const rangeMax,
|
||||
auto const expectedMantissa,
|
||||
auto const expectedExponent,
|
||||
auto const line) {
|
||||
auto const normalized = n.normalizeToRange(rangeMin, rangeMax);
|
||||
BEAST_EXPECTS(
|
||||
normalized.first == expectedMantissa,
|
||||
"Number " + to_string(n) + " scaled to " + std::to_string(rangeMax) +
|
||||
". Expected mantissa:" + std::to_string(expectedMantissa) +
|
||||
", got: " + std::to_string(normalized.first) + " @ " + std::to_string(line));
|
||||
BEAST_EXPECTS(
|
||||
normalized.second == expectedExponent,
|
||||
"Number " + to_string(n) + " scaled to " + std::to_string(rangeMax) +
|
||||
". Expected exponent:" + std::to_string(expectedExponent) +
|
||||
", got: " + std::to_string(normalized.second) + " @ " + std::to_string(line));
|
||||
};
|
||||
|
||||
std::int64_t constexpr iRangeMin = 100;
|
||||
std::int64_t constexpr iRangeMax = 999;
|
||||
|
||||
std::uint64_t constexpr uRangeMin = 100;
|
||||
std::uint64_t constexpr uRangeMax = 999;
|
||||
|
||||
constexpr static MantissaRange largeRange{MantissaRange::large};
|
||||
|
||||
std::int64_t constexpr iBigMin = largeRange.min;
|
||||
std::int64_t constexpr iBigMax = largeRange.max;
|
||||
|
||||
auto const testSuite = [&](Number const& n,
|
||||
auto const expectedSmallMantissa,
|
||||
auto const expectedSmallExponent,
|
||||
auto const expectedLargeMantissa,
|
||||
auto const expectedLargeExponent,
|
||||
auto const line) {
|
||||
test(n, iRangeMin, iRangeMax, expectedSmallMantissa, expectedSmallExponent, line);
|
||||
test(n, iBigMin, iBigMax, expectedLargeMantissa, expectedLargeExponent, line);
|
||||
|
||||
// Only test non-negative. testing a negative number with an
|
||||
// unsigned range will assert, and asserts can't be tested.
|
||||
if (n.signum() >= 0)
|
||||
{
|
||||
test(n, uRangeMin, uRangeMax, expectedSmallMantissa, expectedSmallExponent, line);
|
||||
test(
|
||||
n,
|
||||
largeRange.min,
|
||||
largeRange.max,
|
||||
expectedLargeMantissa,
|
||||
expectedLargeExponent,
|
||||
line);
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
// zero
|
||||
Number const n{0};
|
||||
|
||||
testSuite(
|
||||
n,
|
||||
0,
|
||||
std::numeric_limits<int>::lowest(),
|
||||
0,
|
||||
std::numeric_limits<int>::lowest(),
|
||||
__LINE__);
|
||||
}
|
||||
{
|
||||
// Small positive number
|
||||
Number const n{2};
|
||||
|
||||
testSuite(n, 200, -2, 2'000'000'000'000'000'000, -18, __LINE__);
|
||||
}
|
||||
{
|
||||
// Negative number
|
||||
Number const n{-2};
|
||||
|
||||
testSuite(n, -200, -2, -2'000'000'000'000'000'000, -18, __LINE__);
|
||||
}
|
||||
{
|
||||
// Biggest valid mantissa
|
||||
Number const n{Number::largestMantissa, 0, Number::normalized{}};
|
||||
|
||||
if (scale == MantissaRange::small)
|
||||
// With the small mantissa range, the value rounds up. Because
|
||||
// it rounds up, when scaling up to the full int64 range, it
|
||||
// can't go over the max, so it is one digit smaller than the
|
||||
// full value.
|
||||
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
|
||||
else
|
||||
testSuite(n, 922, 16, Number::largestMantissa, 0, __LINE__);
|
||||
}
|
||||
{
|
||||
// Biggest valid mantissa + 1
|
||||
Number const n{Number::largestMantissa + 1, 0, Number::normalized{}};
|
||||
|
||||
if (scale == MantissaRange::small)
|
||||
// With the small mantissa range, the value rounds up. Because
|
||||
// it rounds up, when scaling up to the full int64 range, it
|
||||
// can't go over the max, so it is one digit smaller than the
|
||||
// full value.
|
||||
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
|
||||
else
|
||||
testSuite(n, 922, 16, 922'337'203'685'477'581, 1, __LINE__);
|
||||
}
|
||||
{
|
||||
// Biggest valid mantissa + 2
|
||||
Number const n{Number::largestMantissa + 2, 0, Number::normalized{}};
|
||||
|
||||
if (scale == MantissaRange::small)
|
||||
// With the small mantissa range, the value rounds up. Because
|
||||
// it rounds up, when scaling up to the full int64 range, it
|
||||
// can't go over the max, so it is one digit smaller than the
|
||||
// full value.
|
||||
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
|
||||
else
|
||||
testSuite(n, 922, 16, 922'337'203'685'477'581, 1, __LINE__);
|
||||
}
|
||||
{
|
||||
// Biggest valid mantissa + 3
|
||||
Number const n{Number::largestMantissa + 3, 0, Number::normalized{}};
|
||||
|
||||
if (scale == MantissaRange::small)
|
||||
// With the small mantissa range, the value rounds up. Because
|
||||
// it rounds up, when scaling up to the full int64 range, it
|
||||
// can't go over the max, so it is one digit smaller than the
|
||||
// full value.
|
||||
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
|
||||
else
|
||||
testSuite(n, 922, 16, 922'337'203'685'477'581, 1, __LINE__);
|
||||
}
|
||||
{
|
||||
// int64 min
|
||||
Number const n{std::numeric_limits<std::int64_t>::min(), 0};
|
||||
|
||||
if (scale == MantissaRange::small)
|
||||
testSuite(n, -922, 16, -922'337'203'685'477'600, 1, __LINE__);
|
||||
else
|
||||
testSuite(n, -922, 16, -922'337'203'685'477'581, 1, __LINE__);
|
||||
}
|
||||
{
|
||||
// int64 min + 1
|
||||
Number const n{std::numeric_limits<std::int64_t>::min() + 1, 0};
|
||||
|
||||
if (scale == MantissaRange::small)
|
||||
testSuite(n, -922, 16, -922'337'203'685'477'600, 1, __LINE__);
|
||||
else
|
||||
testSuite(n, -922, 16, -9'223'372'036'854'775'807, 0, __LINE__);
|
||||
}
|
||||
{
|
||||
// int64 min - 1
|
||||
// Need to cast to uint, even though we're dealing with a negative
|
||||
// number to avoid overflow and UB
|
||||
Number const n{
|
||||
true,
|
||||
static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::min()) + 1,
|
||||
0,
|
||||
Number::normalized{}};
|
||||
|
||||
if (scale == MantissaRange::small)
|
||||
testSuite(n, -922, 16, -922'337'203'685'477'600, 1, __LINE__);
|
||||
else
|
||||
testSuite(n, -922, 16, -922'337'203'685'477'581, 1, __LINE__);
|
||||
auto const maxMantissa = Number::maxMantissa();
|
||||
Number const max = Number{false, maxMantissa, 0, Number::normalized{}};
|
||||
BEAST_EXPECT(max.mantissa() == maxMantissa / 10);
|
||||
BEAST_EXPECT(max.exponent() == 1);
|
||||
// 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38
|
||||
// digits
|
||||
BEAST_EXPECT(
|
||||
(power(max, 2) == Number{false, maxMantissa / 10 - 1, 20, Number::normalized{}}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1875,7 +1552,6 @@ public:
|
||||
test_truncate();
|
||||
testRounding();
|
||||
testInt64();
|
||||
testNormalizeToRange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,7 +138,7 @@ public:
|
||||
{
|
||||
std::shared_ptr<NodeObject> object;
|
||||
|
||||
Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object);
|
||||
Status const status = backend.fetch(batch[i]->getHash(), &object);
|
||||
|
||||
BEAST_EXPECT(status == ok);
|
||||
|
||||
@@ -158,7 +158,7 @@ public:
|
||||
{
|
||||
std::shared_ptr<NodeObject> object;
|
||||
|
||||
Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object);
|
||||
Status const status = backend.fetch(batch[i]->getHash(), &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().data(), &result);
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
suite_.expect(result && isSame(result, obj));
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
@@ -377,9 +377,9 @@ public:
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const key = seq2_.key(i);
|
||||
auto const hash = seq2_.key(i);
|
||||
std::shared_ptr<NodeObject> result;
|
||||
backend_.fetch(key.data(), &result);
|
||||
backend_.fetch(hash, &result);
|
||||
suite_.expect(!result);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
@@ -449,9 +449,9 @@ public:
|
||||
{
|
||||
if (rand_(gen_) < missingNodePercent)
|
||||
{
|
||||
auto const key = seq2_.key(dist_(gen_));
|
||||
auto const hash = seq2_.key(dist_(gen_));
|
||||
std::shared_ptr<NodeObject> result;
|
||||
backend_.fetch(key.data(), &result);
|
||||
backend_.fetch(hash, &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().data(), &result);
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
suite_.expect(result && isSame(result, obj));
|
||||
}
|
||||
}
|
||||
@@ -540,8 +540,7 @@ public:
|
||||
std::shared_ptr<NodeObject> result;
|
||||
auto const j = older_(gen_);
|
||||
obj = seq1_.obj(j);
|
||||
std::shared_ptr<NodeObject> result1;
|
||||
backend_.fetch(obj->getHash().data(), &result);
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
suite_.expect(result != nullptr);
|
||||
suite_.expect(isSame(result, obj));
|
||||
}
|
||||
@@ -559,7 +558,7 @@ public:
|
||||
std::shared_ptr<NodeObject> result;
|
||||
auto const j = recent_(gen_);
|
||||
obj = seq1_.obj(j);
|
||||
backend_.fetch(obj->getHash().data(), &result);
|
||||
backend_.fetch(obj->getHash(), &result);
|
||||
suite_.expect(!result || isSame(result, obj));
|
||||
break;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user