diff --git a/.clang-tidy b/.clang-tidy index 5a1ba7c321..68fc9e75fc 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -9,37 +9,24 @@ Checks: "-*, cppcoreguidelines-*, -cppcoreguidelines-avoid-c-arrays, - -cppcoreguidelines-avoid-capturing-lambda-coroutines, -cppcoreguidelines-avoid-const-or-ref-data-members, -cppcoreguidelines-avoid-do-while, - -cppcoreguidelines-avoid-goto, -cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-avoid-non-const-global-variables, - -cppcoreguidelines-avoid-reference-coroutine-parameters, -cppcoreguidelines-c-copy-assignment-signature, - -cppcoreguidelines-explicit-virtual-functions, -cppcoreguidelines-interfaces-global-init, - -cppcoreguidelines-macro-to-enum, -cppcoreguidelines-macro-usage, -cppcoreguidelines-missing-std-forward, -cppcoreguidelines-narrowing-conversions, - -cppcoreguidelines-no-malloc, - -cppcoreguidelines-noexcept-destructor, -cppcoreguidelines-noexcept-move-operations, - -cppcoreguidelines-noexcept-swap, -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-owning-memory, - -cppcoreguidelines-prefer-member-initializer, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-pro-bounds-pointer-arithmetic, - -cppcoreguidelines-pro-type-const-cast, - -cppcoreguidelines-pro-type-cstyle-cast, -cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-union-access, - -cppcoreguidelines-pro-type-vararg, - -cppcoreguidelines-slicing, -cppcoreguidelines-special-member-functions, llvm-namespace-comment, @@ -63,44 +50,23 @@ Checks: "-*, performance-*, -performance-avoid-endl, -performance-enum-size, - -performance-inefficient-algorithm, - -performance-inefficient-string-concatenation, - -performance-no-int-to-ptr, - -performance-noexcept-destructor, -performance-noexcept-move-constructor, - -performance-noexcept-swap, - -performance-type-promotion-in-math-fn, -performance-unnecessary-copy-initialization, -performance-unnecessary-value-param, readability-*, -readability-avoid-const-params-in-decls, - -readability-avoid-unconditional-preprocessor-if, -readability-container-data-pointer, - -readability-delete-null-pointer, -readability-function-cognitive-complexity, - -readability-function-size, -readability-identifier-length, -readability-inconsistent-declaration-parameter-name, -readability-isolate-declaration, -readability-magic-numbers, - -readability-misplaced-array-index, -readability-named-parameter, - -readability-operators-representation, -readability-qualified-auto, -readability-redundant-access-specifiers, - -readability-redundant-control-flow, - -readability-redundant-function-ptr-dereference, - -readability-redundant-preprocessor, - -readability-redundant-smartptr-get, - -readability-redundant-string-cstr, - -readability-simplify-subscript-expr, -readability-static-accessed-through-instance, - -readability-string-compare, - -readability-uniqueptr-delete-release, - -readability-uppercase-literal-suffix, - -readability-use-anyofallof, - -readability-use-concise-preprocessor-directives + -readability-uppercase-literal-suffix " # --- # bugprone-narrowing-conversions, # This will break a lot of code but we should enable it in the future because it can eliminate a lot of bugs diff --git a/.cspell.config.yaml b/.cspell.config.yaml index c558fc0984..13da132b90 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -65,6 +65,7 @@ words: - Btrfs - Buildx - canonicality + - canonicalised - changespq - checkme - choco @@ -72,6 +73,7 @@ words: - citardauq - clawback - clawbacks + - clippy - cmaketoolchain - coeffs - coldwallet @@ -259,6 +261,7 @@ words: - rocksdb - Rohrs - roundings + - rustc - sahyadri - Satoshi - scons @@ -279,6 +282,8 @@ words: - sles - soci - socidb + - sponsee + - sponsees - SRPMS - sslws - statsd @@ -300,6 +305,7 @@ words: - takerpays - ters - TMEndpointv2 + - tparam - trixie - tx - txid @@ -327,6 +333,7 @@ words: - unserviced - unshareable - unshares + - unsponsored - unsquelch - unsquelched - unsquelching diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000..3550a30f2d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 9c9adeefe2..b31e6ae961 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -72,7 +72,6 @@ test.app > xrpl.server test.app > xrpl.shamap test.app > xrpl.tx test.basics > test.jtx -test.basics > test.unit_test test.basics > xrpl.basics test.basics > xrpl.core test.basics > xrpld.rpc @@ -160,6 +159,7 @@ test.peerfinder > xrpl.protocol test.protocol > test.jtx test.protocol > test.unit_test test.protocol > xrpl.basics +test.protocol > xrpld.core test.protocol > xrpl.json test.protocol > xrpl.protocol test.rpc > test.jtx diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index a269cb25d4..c783f32fb7 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -25,24 +25,16 @@ def get_cmake_args(build_type: str, extra_args: str) -> str: return " ".join(args) -def runs_on_event(exclude_event_types: list[str], event: str | None) -> bool: - """Whether a config should run for the current event. - - 'exclude_event_types' is a list of GitHub event names (e.g. - ["pull_request"]) on which the config should NOT run; an empty list means - the config runs on every event. When no event is given (event is None), no - filtering is applied. - """ - if event is None: - return True - return event not in exclude_event_types - - # --------------------------------------------------------------------------- # Input types — shapes of the JSON config files # --------------------------------------------------------------------------- +# Every config must declare 'minimal'. Minimal configs form the reduced matrix +# built for pull requests by default; the full matrix adds the rest. Packaging +# configs declare it too, but packaging is gated in the workflow, not by it. + + @dataclasses.dataclass class LinuxConfig: """One entry in linux.json's 'configs' or 'package_configs' arrays.""" @@ -50,13 +42,11 @@ class LinuxConfig: compiler: list[str] build_type: list[str] arch: list[str] + minimal: bool sanitizers: list[str] = dataclasses.field(default_factory=list) suffix: str = "" extra_cmake_args: str = "" image: str = "" # only used by package_configs entries - # List of GitHub event names (e.g. "pull_request") on which this config - # should NOT run. Empty means it runs on every event. - exclude_event_types: list[str] = dataclasses.field(default_factory=list) @dataclasses.dataclass @@ -89,11 +79,9 @@ class PlatformConfig: """One entry in macos.json's or windows.json's 'configs' array.""" build_type: list[str] + minimal: bool build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug) extra_cmake_args: str = "" - # List of GitHub event names (e.g. "pull_request") on which this config - # should NOT run. Empty means it runs on every event. - exclude_event_types: list[str] = dataclasses.field(default_factory=list) def __post_init__(self) -> None: if isinstance(self.build_type, str): @@ -168,20 +156,18 @@ _ARCHS: dict[str, Architecture] = { } -def expand_linux_matrix( - linux: LinuxFile, event: str | None = None -) -> list[MatrixEntry]: +def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]: """Expand a LinuxFile into a flat list of matrix entries. Each config entry is expanded over the cross-product of its - compiler, build_type, sanitizers, and architecture lists. Configs that - exclude the current event are skipped. + compiler, build_type, sanitizers, and architecture lists. When 'minimal' is + true, only configs flagged as minimal are included. """ entries: list[MatrixEntry] = [] for distro, configs in linux.configs.items(): for cfg in configs: - if not runs_on_event(cfg.exclude_event_types, event): + if minimal and not cfg.minimal: continue # An empty sanitizers list means "one entry with no sanitizer". effective_sanitizers = cfg.sanitizers or [""] @@ -240,19 +226,17 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]: return entries -def expand_platform_matrix( - pf: PlatformFile, event: str | None = None -) -> list[MatrixEntry]: +def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry]: """Expand a PlatformFile (macOS or Windows) into matrix entries. - Configs that exclude the current event are skipped. + When 'minimal' is true, only configs flagged as minimal are included. """ platform_name, arch = pf.platform.split("/") is_windows = platform_name == "windows" entries: list[MatrixEntry] = [] for cfg in pf.configs: - if not runs_on_event(cfg.exclude_event_types, event): + if minimal and not cfg.minimal: continue for build_type in cfg.build_type: entries.append( @@ -292,12 +276,12 @@ if __name__ == "__main__": action="store_true", ) parser.add_argument( - "-e", - "--event", - help="The GitHub event name that triggered the workflow (e.g. 'push', " - "'pull_request'). Configs are filtered by their 'event_type'. If " - "omitted, no filtering is applied.", - default=None, + "-m", + "--minimal", + help="Emit only the minimal matrix (the configs flagged 'minimal'), " + "used for pull requests by default. If omitted, the full matrix is " + "emitted.", + action="store_true", ) args = parser.parse_args() @@ -308,15 +292,15 @@ if __name__ == "__main__": else: if args.config in ("linux", None): matrix += expand_linux_matrix( - LinuxFile.load(THIS_DIR / "linux.json"), args.event + LinuxFile.load(THIS_DIR / "linux.json"), args.minimal ) if args.config in ("macos", None): matrix += expand_platform_matrix( - PlatformFile.load(THIS_DIR / "macos.json"), args.event + PlatformFile.load(THIS_DIR / "macos.json"), args.minimal ) if args.config in ("windows", None): matrix += expand_platform_matrix( - PlatformFile.load(THIS_DIR / "windows.json"), args.event + PlatformFile.load(THIS_DIR / "windows.json"), args.minimal ) print(f"matrix={json.dumps({'include': [dataclasses.asdict(e) for e in matrix]})}") diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 863b910dda..03ac1c6334 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -2,16 +2,30 @@ "image_tag": "sha-e29b523", "configs": { "ubuntu": [ + { + "compiler": ["clang"], + "build_type": ["Release"], + "arch": ["amd64"], + "minimal": true + }, + { + "compiler": ["gcc"], + "build_type": ["Release"], + "arch": ["amd64"], + "minimal": false + }, { "compiler": ["gcc", "clang"], "build_type": ["Debug", "Release"], - "arch": ["amd64", "arm64"] + "arch": ["arm64"], + "minimal": false }, { "compiler": ["gcc", "clang"], "build_type": ["Debug", "Release"], "arch": ["amd64"], + "minimal": false, "sanitizers": ["address", "undefinedbehavior"] }, @@ -19,6 +33,7 @@ "compiler": ["gcc"], "build_type": ["Debug"], "arch": ["amd64"], + "minimal": true, "suffix": "coverage", "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0" }, @@ -26,6 +41,7 @@ "compiler": ["clang"], "build_type": ["Debug"], "arch": ["amd64"], + "minimal": false, "suffix": "voidstar", "extra_cmake_args": "-Dvoidstar=ON" }, @@ -33,6 +49,7 @@ "compiler": ["clang"], "build_type": ["Release"], "arch": ["amd64"], + "minimal": false, "suffix": "reffee", "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=1000" }, @@ -40,9 +57,9 @@ "compiler": ["gcc"], "build_type": ["Debug"], "arch": ["amd64"], + "minimal": false, "suffix": "unity", - "extra_cmake_args": "-Dunity=ON", - "exclude_event_types": ["pull_request"] + "extra_cmake_args": "-Dunity=ON" } ], @@ -50,7 +67,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "minimal": false } ], @@ -58,7 +76,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "minimal": false } ] }, @@ -68,6 +87,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], + "minimal": false, "image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745" } ], @@ -77,6 +97,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], + "minimal": false, "image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745" } ] diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index 2d3cc75c7b..98e0f13141 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -4,13 +4,14 @@ "configs": [ { "build_type": "Release", - "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "minimal": true }, { "build_type": "Debug", "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", "build_only": true, - "exclude_event_types": ["pull_request"] + "minimal": false } ] } diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index 370e9f5bc7..6b926e85f5 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -2,11 +2,11 @@ "platform": "windows/amd64", "runner": ["self-hosted", "Windows", "dev-box-windows-2026"], "configs": [ - { "build_type": "Release" }, + { "build_type": "Release", "minimal": true }, { "build_type": "Debug", "build_only": true, - "exclude_event_types": ["pull_request"] + "minimal": false } ] } diff --git a/.github/workflows/conflicting-pr.yml b/.github/workflows/conflicting-pr.yml index 772d46fd7d..cf65640954 100644 --- a/.github/workflows/conflicting-pr.yml +++ b/.github/workflows/conflicting-pr.yml @@ -14,6 +14,7 @@ permissions: jobs: main: + if: ${{ !contains(github.event.pull_request.labels.*.name, 'IgnoreConflicts') }} runs-on: ubuntu-latest steps: - name: Check if PRs are dirty diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 19fb170b92..442a202a44 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -1,7 +1,11 @@ -# This workflow runs all workflows to check, build and test the project on -# various Linux flavors, as well as on MacOS and Windows, on every push to a -# user branch. However, it will not run if the pull request is a draft unless it -# has the 'DraftRunCI' label. For commits to PRs that target a release branch, +# This workflow runs workflows to check, build and test the project +# on every meaningful change on pull_request. +# However, it will not run if the PR is a draft +# unless it has the 'DraftRunCI' or 'Full CI build' label. +# +# By default a PR builds only a minimal matrix. +# The full matrix runs once the PR is labeled "Ready to merge" or "Full CI build". +# For commits to PRs that target a release branch, # it also uploads the libxrpl recipe to the Conan remote. name: PR @@ -15,8 +19,16 @@ on: - reopened - synchronize - ready_for_review + # Trigger on label changes so toggling "Ready to merge" or "Full CI build" + # switches between the minimal and full matrix without needing a new push. + - labeled + - unlabeled concurrency: + # A single per-ref group with cancel-in-progress means any newer run (a push + # or a label change) supersedes the in-progress one for that ref. Keeping + # exactly one authoritative run per ref ensures a fast do-nothing run can never + # mask a real build's checks. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -25,11 +37,18 @@ defaults: shell: bash jobs: - # This job determines whether the rest of the workflow should run. It runs - # when the PR is not a draft (which should also cover merge-group) or - # has the 'DraftRunCI' label. + # This job determines whether the rest of the workflow should run at all, + # based on the current set of labels: it runs when the PR is not a draft + # (which should also cover merge-group) or has the 'DraftRunCI' or + # 'Full CI build' label. Whether a build then happens, and whether it is the + # minimal or full matrix, is decided further below and in the strategy matrix. should-run: - if: ${{ !github.event.pull_request.draft || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }} + if: >- + ${{ + !github.event.pull_request.draft + || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') + || contains(github.event.pull_request.labels.*.name, 'Full CI build') + }} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -91,15 +110,17 @@ jobs: # least one of: # * Any of the files checked in the `changes` step were modified # * The PR is NOT a draft and is labeled "Ready to merge" + # * The PR is labeled "Full CI build" (draft or not) # * The workflow is running from the merge queue id: go env: FILES: ${{ steps.changes.outputs.any_changed }} DRAFT: ${{ github.event.pull_request.draft }} READY: ${{ contains(github.event.pull_request.labels.*.name, 'Ready to merge') }} + FULL: ${{ contains(github.event.pull_request.labels.*.name, 'Full CI build') }} MERGE: ${{ github.event_name == 'merge_group' }} run: | - echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}" + echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FULL == 'true' || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}" cat "${GITHUB_OUTPUT}" outputs: go: ${{ steps.go.outputs.go == 'true' }} @@ -142,7 +163,10 @@ jobs: package: needs: [should-run, build-test] - if: ${{ needs.should-run.outputs.go == 'true' }} + # Packaging consumes the debian/rhel release binaries, which are only built + # by the full matrix. Skip it for pull requests that ran only the minimal + # matrix (i.e. not yet labeled "Ready to merge" or "Full CI build"). + if: ${{ needs.should-run.outputs.go == 'true' && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'Ready to merge') || contains(github.event.pull_request.labels.*.name, 'Full CI build')) }} uses: ./.github/workflows/reusable-package.yml upload-recipe: diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index e7a88a0e66..b2327f67ea 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -124,7 +124,7 @@ jobs: - name: Check tools env: CHECK_TOOLS_SKIP_CLONE: "1" - run: ./bin/check-tools.sh + run: ./bin/check-tools.sh || true - name: Print build environment uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574 diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index 690aa3d423..b6091b99d9 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -35,5 +35,8 @@ jobs: id: generate env: GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}', inputs.os) || '' }} - GENERATE_EVENT: ${{ github.event_name }} - run: ./generate.py ${GENERATE_CONFIG} --event="${GENERATE_EVENT}" >>"${GITHUB_OUTPUT}" + # Run only the minimal matrix for pull requests that are not yet + # labeled "Ready to merge" or "Full CI build". Any other event (merge + # queue, push, schedule, manual dispatch) runs the full matrix. + GENERATE_MINIMAL: ${{ (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'Ready to merge') && !contains(github.event.pull_request.labels.*.name, 'Full CI build')) && '--minimal' || '' }} + run: ./generate.py ${GENERATE_CONFIG} ${GENERATE_MINIMAL} >>"${GITHUB_OUTPUT}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 910bda8d4c..8a689c0f8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,6 +32,11 @@ repos: # as standalone translation units, so they have no compile_commands.json # entry to lint (verify_headers checks them transitively). exclude: '^include/xrpl/protocol_autogen|\.ipp$' + # run-clang-tidy --fix may edit headers included by files it is not run on, + # so pre-commit must not split the files across parallel hook invocations. + # The script determines the staged files itself and lets run-clang-tidy + # handle parallelism internally. + pass_filenames: false - id: fix-include-style name: fix include style entry: ./bin/pre-commit/fix_include_style.py @@ -43,6 +48,11 @@ repos: language: python entry: ./bin/pre-commit/fix_pragma_once.py files: \.(h|hpp)$ + - id: check-doxygen-style + name: check Doxygen comment style + entry: ./bin/pre-commit/check_doxygen_style.py + language: python + types_or: [c++, c] - repo: https://github.com/pre-commit/mirrors-clang-format rev: dd18dad857d6133e90bbe478f4f2f22ec0030269 # frozen: v22.1.5 diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index 56a45c132a..a04f265328 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -28,6 +28,9 @@ This section contains changes targeting a future version. ### Additions +- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`. + When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present. + - `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681)) - `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)): diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc93223925..9929b2eb39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,7 +84,9 @@ If you create new source files, they must be organized as follows: - All other non-test files must go under `src/xrpld`. - All test source files must go under `src/test`. -The source must be formatted according to the style guide below. +The source must be formatted according to the style guide below. The easiest +way to satisfy this is to install the [`pre-commit`](#pre-commit-hooks) hooks, +which format and lint your changes automatically on every commit. Header includes must be [levelized](.github/scripts/levelization). @@ -212,13 +214,61 @@ This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent rather than a set of _thou shalt not_ commandments. +## Pre-commit hooks + +We use the [`pre-commit`](https://pre-commit.com/) framework to run the +formatting and linting tools that keep the codebase consistent. `pre-commit` +runs each tool configured in +[`.pre-commit-config.yaml`](./.pre-commit-config.yaml) in its own isolated +environment, so you don't need to install most of the individual tools +yourself. The version of each hook sourced from an external repository +(`clang-format`, `gersemi`, etc.) is pinned in that file, so running the hooks +locally uses exactly the same versions as CI. A few `local` hooks — most notably +`clang-tidy` — run tools from your own environment; see +[Installing clang-tidy](#installing-clang-tidy) for how to get those. + +To get started, install `pre-commit` and enable the git hook scripts: + +```bash +pip install pre-commit +pre-commit install +``` + +Once installed, the hooks run automatically on your staged files every time you +`git commit`. You can also run them on demand: + +```bash +# Run all hooks against only the staged files +pre-commit run + +# Run all hooks against every file in the repository +pre-commit run --all-files + +# Run a single hook (e.g. clang-format) against all files +pre-commit run clang-format --all-files +``` + +The hooks configured in this repository include, among others: + +- `clang-format` — C++/proto formatting (see [Formatting](#formatting)) +- `clang-tidy` — C++ static analysis (see [Clang-tidy](#clang-tidy)); opt in with `TIDY=1` +- `fix-include-style`, `fix-pragma-once`, `check-doxygen-style` — C++ hygiene +- `gersemi` — CMake formatting +- `prettier`, `black`, `shfmt` — formatting for JavaScript/JSON/Markdown, Python, and shell +- `cspell` — spell checking + +The same hooks run in CI on every pull request, so running them locally before +you push helps you avoid CI failures. + ## Formatting -All code must conform to `clang-format` version 22, -according to the settings in [`.clang-format`](./.clang-format), -unless the result would be unreasonably difficult to read or maintain. -To demarcate lines that should be left as-is, surround them with comments like -this: +All code must conform to `clang-format`, according to the settings in +[`.clang-format`](./.clang-format), unless the result would be unreasonably +difficult to read or maintain. The `clang-format` version is pinned in +[`.pre-commit-config.yaml`](./.pre-commit-config.yaml), so the +[`pre-commit`](#pre-commit-hooks) hook always formats with the same version as +CI. To demarcate lines that should be left as-is, surround them with comments +like this: ``` // clang-format off @@ -226,9 +276,21 @@ this: // clang-format on ``` -You can format individual files in place by running `clang-format -i ...` +The easiest way to format your changes is to let the `pre-commit` hook run +automatically on commit, or to run it manually: + +```bash +pre-commit run clang-format --all-files +``` + +You can also format individual files in place by running `clang-format -i ...` from any directory within this project. +> [!NOTE] +> This uses whatever `clang-format` version is installed locally, which may +> differ from the pinned version used by `pre-commit` and CI, so the results +> can vary. + There is a Continuous Integration job that runs clang-format on pull requests. If the code doesn't comply, a patch file that corrects auto-fixable formatting issues is generated. To download the patch file: @@ -239,13 +301,6 @@ To download the patch file: 4. Download the zip file and extract it to your local git repository. Run `git apply [patch-file-name]`. 5. Commit and push. -You can install a pre-commit hook to automatically run `clang-format` before every commit: - -``` -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). @@ -267,7 +322,7 @@ Before running clang-tidy, you must build the project to generate required files #### Via pre-commit (recommended) -If you have already installed the pre-commit hooks (see above), you can run clang-tidy on your staged files using: +If you have already installed the [`pre-commit`](#pre-commit-hooks) hooks, you can run clang-tidy on your staged files using: ``` TIDY=1 pre-commit run clang-tidy diff --git a/bin/check-tools.sh b/bin/check-tools.sh index 808f384d5b..7886bcf8b0 100755 --- a/bin/check-tools.sh +++ b/bin/check-tools.sh @@ -110,6 +110,23 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then fi fi +# Rust toolchain. Part of the Nix commonPackages, so available on both Linux +# and macOS. The cargo plugins are invoked through cargo (`cargo `), which +# resolves the matching `cargo-` binary on PATH; `--version` is offline and +# does not need a Cargo project. +if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then + echo + echo "Rust toolchain:" + check cargo + check cargo-audit cargo audit --version + check cargo-llvm-cov cargo llvm-cov --version + check cargo-nextest cargo nextest --version + check clippy clippy-driver --version + check rust-analyzer + check rustc + check rustfmt +fi + # GCC is the default compiler on Linux. macOS uses the system Apple Clang # instead, so GCC/g++/gcov are not expected there. if [ "${os}" = "linux" ]; then diff --git a/bin/pre-commit/check_doxygen_style.py b/bin/pre-commit/check_doxygen_style.py new file mode 100755 index 0000000000..6a9af9399f --- /dev/null +++ b/bin/pre-commit/check_doxygen_style.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +""" +Check C++ Doxygen comment style. + +Enforces the house convention for documentation comments: + + * Use ``/** ... */`` blocks, not ``///``, ``//!`` or ``/*! ... */``; a plain + ``/* ... */`` that contains Doxygen commands is a doc comment missing its + second star. Trailing member-after comments use ``///<`` (not ``//!<``, + ``/*!< ... */`` or ``/**< ... */`` -- the block forms get reflowed and + mis-attached by clang-format on packed enum values, the line form does not). + * ``/**`` sits alone on its line; the closing ``*/`` sits alone on its line. + * Every content line is prefixed with `` * `` (no bare-indented continuation). + * The first content line is flush (not over-indented). + * Doxygen commands use the ``@cmd`` form, not ``\\cmd``. + * Use ``@return`` / ``@throws`` rather than prose ``Returns:`` / ``Throws:``. + * A plain ``//`` comment carrying a block-level ``@command`` (``@param``, + ``@return``, ``@see``, ...) is documentation and must be a ``/** ... */`` + block (Doxygen ignores ``//``). + * Use canonical command spellings: ``@return`` (not ``@returns``), + ``@throws`` (not ``@throw``), ``@see`` (not ``@sa``). + * Order block tags ``@tparam`` -> ``@param`` -> ``@return``. (Whether + ``@param`` order matches the signature is not checked here -- too fragile to + parse; Doxygen's WARN_IF_DOC_ERROR covers name mismatches.) + * One-liners are expanded to three lines, EXCEPT bare markers ``@{`` / ``@}`` + / ``@cond [label]`` / ``@endcond`` / ``@file [name]`` which stay on one line. + +Left intentionally alone (recognized, valid Doxygen that is not this style's +concern): + + * ``///<`` trailing "member-after" comments (the house form). + * Divider lines made only of slashes (``//////////``). + * Plain ``/* ... */`` (non-Doxygen) comments. + +Usage: + check_doxygen_style.py [FILE ...] # explicit files + check_doxygen_style.py # default: src/ and include/ trees + +Exit status is non-zero if any violation is found. +""" + +import argparse +import re +import sys +from collections.abc import Iterable, Iterator +from dataclasses import dataclass +from enum import Enum +from pathlib import Path + + +class Category(Enum): + """A kind of style violation: a printed ``label`` and its ``description``. + + The description is the default message; a few categories whose wording + depends on the offending text (see ``Finding.detail``) override it. + """ + + def __init__(self, label: str, description: str) -> None: + self.label = label + self.description = description + + BACKSLASH_COMMAND = ("backslash-command", "use the @cmd form, not \\cmd") + WRONG_COMMAND = ("wrong-command", "use the canonical command spelling") + TRIPLE_SLASH = ("triple-slash", "use a /** ... */ block instead of ///") + QT_MEMBER = ("qt-member", "use ///< instead of //!<") + QT_LINE = ("qt-line", "use a /** ... */ block instead of //!") + BLOCK_MEMBER = ("block-member", "use ///< instead of /**<") + QT_BLOCK_MEMBER = ("qt-block-member", "use ///< instead of /*!<") + DOC_IN_LINE_COMMENT = ( + "doc-in-line-comment", + "use a /** ... */ block for documentation, not //", + ) + QT_COMMENT = ("qt-comment", "use /** instead of /*!") + SINGLE_LINE_BLOCK = ( + "single-line-block", + "expand one-line /** ... */ to a multi-line block " + "(markers @{ @} @cond @endcond @file may stay)", + ) + TEXT_ON_OPENER = ("text-on-opener", "move text off the /** opener line") + BARE_CONTINUATION = ("bare-continuation", 'prefix continuation lines with " * "') + OVER_INDENTED = ("over-indented", "first content line is over-indented") + OVER_INDENTED_TAG = ( + "over-indented-tag", + 'Doxygen tag over-indented; use a single space after "*"', + ) + COMBINED_MARKER = ( + "combined-marker", + "scope marker @{ / @} should be its own single-line /** @{ */ block", + ) + PROSE_LABEL = ("prose-label", "use a Doxygen tag instead of a prose label") + CONTENT_ON_CLOSER = ("content-on-closer", "move content off the closing */ line") + PLAIN_BLOCK_DOC = ( + "plain-block-doc", + "documentation comment must open with /** not /*", + ) + TAG_ORDER = ( + "tag-order", + "block tags out of order; expected @tparam, then @param, then @return", + ) + + +@dataclass(frozen=True) +class Finding: + """A single style violation at a 1-based line number. + + ``detail`` overrides the category's default description when the message + depends on the offending text (e.g. which command was misspelled). + """ + + line: int + category: Category + detail: str | None = None + + @property + def message(self) -> str: + return self.detail if self.detail is not None else self.category.description + + +DEFAULT_ROOTS = ("src", "include") +EXTS = {".h", ".hpp", ".cpp", ".ipp", ".cxx", ".cc"} + +# Every Doxygen command we recognize when written with a backslash (\cmd). +_ALL_COMMANDS = ( + "brief|param|tparam|return|returns|retval|note|warning|pre|post|see|sa|ref|" + "throw|throws|exception|deprecated|details|code|endcode|verbatim|endverbatim|" + "li|arg|c|internal|since|todo|attention|remark|remarks|ingroup|defgroup" +) +# Block-level tags whose over-indentation we flag inside a block body. +_BLOCK_TAGS = ( + "param|tparam|returns?|retval|brief|throws?|note|warning|" + "pre|post|see|sa|details|deprecated" +) +# Tags that, appearing anywhere in a comment, mark it as documentation. +_ANY_DOC_TAGS = ( + "param|tparam|returns?|retval|brief|throws?|note|warning|pre|post|see|sa" +) +# Tags that make a plain // comment a mis-styled doc comment. +_LINE_DOC_TAGS = "brief|param|tparam|returns?|retval|throws?|note|see|pre|post" + +# \cmd that should be @cmd. +RE_BACKSLASH_CMD = re.compile(r"\\(" + _ALL_COMMANDS + r")\b") +# Bare markers that may legitimately stay on a single line. +RE_MARKER = re.compile(r"^@(\{|\}|cond(\s.*)?|endcond|file(\s.*)?)$") +# Prose section labels that should be Doxygen tags. +RE_PROSE_LABEL = re.compile(r"^\*\s(Returns|Throws|Exceptions):\s*$") +# An over-indented block tag: "*" followed by 2+ spaces then the tag. +RE_OVERINDENTED_TAG = re.compile(r"^\*\s{2,}@(" + _BLOCK_TAGS + r")\b") +# Any documentation tag (used to spot a doc comment hiding in a plain /* */). +RE_ANY_DOC_TAG = re.compile(r"@(" + _ANY_DOC_TAGS + r")\b") +# A documentation tag inside a // comment. +RE_LINE_DOC_TAG = re.compile(r"@(" + _LINE_DOC_TAGS + r")\b") +# Order-relevant tags, for the @tparam -> @param -> @return ordering check. +RE_ORDER_TAG = re.compile(r"^\*\s*@(param|tparam|returns?|retval)\b") +# First content line indented by 2+ spaces after the "*". +RE_FIRST_OVERINDENT = re.compile(r"^\s*\*\s{2,}\S") +# A scope marker @{ / @} sharing a comment with other text. +RE_COMBINED_MARKER = re.compile(r"^\*\s*@[{}]\s*$") + +# Non-canonical command spellings -> the house spelling (bare command names). +# Used both to flag a wrong @form and to suggest the right @form for a \wrong. +CANONICAL_COMMAND = {"returns": "return", "throw": "throws", "sa": "see"} +WRONG_SPELLINGS = [ + (re.compile(rf"@{wrong}\b"), f"@{right}") + for wrong, right in CANONICAL_COMMAND.items() +] + +# Order block tags should appear in; a body out of this order is a violation. +EXPECTED_TAG_ORDER = ("tparam", "param", "return") + + +def is_doxy_open(stripped: str) -> bool: + """True for a line-start Doxygen block opener we should normalize.""" + if stripped.startswith("/*!"): # Qt-style Doxygen + return not stripped.startswith("/*!<") # member-after, leave inline + return ( + stripped.startswith("/**") + and not stripped.startswith("/***") + and not stripped.startswith("/**/") + and not stripped.startswith("/**<") + ) + + +def _flag_commands(raw_line: str, stripped: str, index: int) -> list[Finding]: + """Flag \\cmd and misspelled @cmd on a comment line (opener, body, or closer).""" + if not stripped.startswith(("*", "//", "/*")): + return [] + findings: list[Finding] = [] + backslash = RE_BACKSLASH_CMD.search(raw_line) + if backslash: + command = backslash.group(1) + canonical = CANONICAL_COMMAND.get(command, command) + findings.append( + Finding( + index + 1, + Category.BACKSLASH_COMMAND, + f"use @{canonical} instead of \\{command}", + ) + ) + for pattern, replacement in WRONG_SPELLINGS: + wrong = pattern.search(raw_line) + if wrong: + findings.append( + Finding( + index + 1, + Category.WRONG_COMMAND, + f"use {replacement} instead of {wrong.group(0)}", + ) + ) + return findings + + +def _flag_line_comment(raw_line: str, stripped: str, index: int) -> Finding | None: + """Return the finding for a single-line comment form (///, //!, /**<, ...), else None.""" + if stripped.startswith("///") and not stripped.startswith(("////", "///<")): + return Finding(index + 1, Category.TRIPLE_SLASH) + if "//!<" in raw_line: + return Finding(index + 1, Category.QT_MEMBER) + if stripped.startswith("//!"): + return Finding(index + 1, Category.QT_LINE) + if "/**<" in raw_line: + return Finding(index + 1, Category.BLOCK_MEMBER) + if "/*!<" in raw_line: + return Finding(index + 1, Category.QT_BLOCK_MEMBER) + if stripped.startswith("//") and RE_LINE_DOC_TAG.search(stripped): + return Finding(index + 1, Category.DOC_IN_LINE_COMMENT) + return None + + +def _flag_single_line_block(stripped: str, line_no: int, is_qt: bool) -> list[Finding]: + """Findings for a whole /** ... */ or /*! ... */ block on one line.""" + inner = re.sub(r"^/\*[*!]", "", stripped) + inner = re.sub(r"\*/\s*$", "", inner).strip() + findings: list[Finding] = [] + if is_qt: + findings.append(Finding(line_no, Category.QT_COMMENT)) + if inner and not RE_MARKER.match(inner): + findings.append(Finding(line_no, Category.SINGLE_LINE_BLOCK)) + return findings + + +def _canonical_order_tag(body: str) -> str | None: + """The order-relevant tag (tparam/param/return) a body line opens with, if any.""" + match = RE_ORDER_TAG.match(body) + if match is None: + return None + command = match.group(1) + return "return" if command in ("return", "returns", "retval") else command + + +def _flag_body_line( + body_line: str, line_no: int, is_first_content: bool +) -> list[Finding]: + """Findings for one interior line of a multi-line block.""" + body = body_line.strip() + findings: list[Finding] = [] + if body and not body.startswith("*"): + findings.append(Finding(line_no, Category.BARE_CONTINUATION)) + if body.startswith("*"): + if is_first_content and RE_FIRST_OVERINDENT.match(body_line): + findings.append(Finding(line_no, Category.OVER_INDENTED)) + if RE_OVERINDENTED_TAG.match(body): + findings.append(Finding(line_no, Category.OVER_INDENTED_TAG)) + if RE_COMBINED_MARKER.match(body): + findings.append(Finding(line_no, Category.COMBINED_MARKER)) + label = RE_PROSE_LABEL.match(body) + if label: + suggested_tag = "@return" if label.group(1) == "Returns" else "@throws" + findings.append( + Finding( + line_no, + Category.PROSE_LABEL, + f'use {suggested_tag} instead of prose "{label.group(1)}:"', + ) + ) + return findings + + +def _flag_closer(closer_line: str, line_no: int) -> list[Finding]: + """Findings for content sharing the closing */ line.""" + before = closer_line[: closer_line.index("*/")].strip() + if before and before != "*": + return [Finding(line_no, Category.CONTENT_ON_CLOSER)] + return [] + + +def _flag_tag_order(first_tag_line: dict[str, int]) -> list[Finding]: + """One finding if the present block tags are not in EXPECTED_TAG_ORDER.""" + tag_lines = [ + first_tag_line[tag] for tag in EXPECTED_TAG_ORDER if tag in first_tag_line + ] + if tag_lines != sorted(tag_lines): + return [Finding(min(tag_lines), Category.TAG_ORDER)] + return [] + + +def _flag_doxy_block(lines: list[str], start: int) -> tuple[int, list[Finding]]: + """Handle a /** or /*! block opening at ``start``; return (next index, findings).""" + raw_line = lines[start] + stripped = raw_line.lstrip() + open_pos = raw_line.index("/*") + is_qt = stripped.startswith("/*!") + + # A whole block on one line: /** ... */. + if "*/" in raw_line[open_pos + 2 :]: + return start + 1, _flag_single_line_block(stripped, start + 1, is_qt) + + # Multi-line block: opener, then scan the body to the closer. + findings: list[Finding] = [] + if is_qt: + findings.append(Finding(start + 1, Category.QT_COMMENT)) + if raw_line[open_pos + 3 :].strip(): + findings.append(Finding(start + 1, Category.TEXT_ON_OPENER)) + + line_count = len(lines) + cursor = start + 1 + is_first_content = True + first_tag_line: dict[str, int] = {} # canonical tag -> 1-based first line + while cursor < line_count and "*/" not in lines[cursor]: + body_line = lines[cursor] + body = body_line.strip() + findings.extend(_flag_commands(body_line, body, cursor)) + tag = _canonical_order_tag(body) + if tag is not None: + first_tag_line.setdefault(tag, cursor + 1) + findings.extend(_flag_body_line(body_line, cursor + 1, is_first_content)) + if body.startswith("*"): + is_first_content = False + cursor += 1 + + if cursor < line_count: + closer_line = lines[cursor] + findings.extend(_flag_commands(closer_line, closer_line.strip(), cursor)) + findings.extend(_flag_closer(closer_line, cursor + 1)) + findings.extend(_flag_tag_order(first_tag_line)) + + return cursor + 1, findings + + +def _flag_plain_block(lines: list[str], start: int) -> tuple[int, list[Finding]]: + """Handle a line-start plain /* ... */ block; return (next index, findings). + + Only flagged when it hides a documentation command (a missing second star). + """ + line_count = len(lines) + cursor = start + while cursor < line_count and "*/" not in lines[cursor]: + cursor += 1 + findings: list[Finding] = [] + # The opener (start) is command-checked by check_file; check the rest here. + for i in range(start + 1, min(cursor + 1, line_count)): + findings.extend(_flag_commands(lines[i], lines[i].strip(), i)) + block_text = "\n".join( + lines[start : cursor + 1] if cursor < line_count else lines[start:] + ) + if RE_ANY_DOC_TAG.search(block_text): + findings.append(Finding(start + 1, Category.PLAIN_BLOCK_DOC)) + next_index = cursor + 1 if cursor < line_count else line_count + return next_index, findings + + +def check_source(text: str) -> list[Finding]: + """Return all style violations found in the given source text.""" + lines = text.split("\n") + findings: list[Finding] = [] + line_count = len(lines) + index = 0 + in_plain_block = False # inside a mid-line, non-Doxygen /* ... */ + while index < line_count: + raw_line = lines[index] + stripped = raw_line.lstrip() + + # Skip the interior of a plain block opened on an earlier line. + if in_plain_block: + in_plain_block = "*/" not in raw_line + index += 1 + continue + + findings.extend(_flag_commands(raw_line, stripped, index)) + + line_finding = _flag_line_comment(raw_line, stripped, index) + if line_finding is not None: + findings.append(line_finding) + index += 1 + elif is_doxy_open(stripped): + index, block_findings = _flag_doxy_block(lines, index) + findings.extend(block_findings) + elif stripped.startswith("/*"): + index, block_findings = _flag_plain_block(lines, index) + findings.extend(block_findings) + else: + # A /* that opens mid-line without closing starts a plain block. + if "/*" in raw_line and not stripped.startswith("//"): + if "*/" not in raw_line[raw_line.index("/*") + 2 :]: + in_plain_block = True + index += 1 + return findings + + +def check_file(path: Path) -> list[Finding]: + """Return all style violations found in one file.""" + return check_source(path.read_text(encoding="utf-8")) + + +def iter_files(paths: Iterable[str]) -> Iterator[Path]: + """Yield every C++ source file among the given files and directories.""" + for raw_path in paths: + path = Path(raw_path) + if path.is_dir(): + for candidate in path.rglob("*"): + if candidate.is_file() and candidate.suffix in EXTS: + yield candidate + elif path.suffix in EXTS: + yield path + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check Doxygen comment style.") + parser.add_argument( + "files", nargs="*", help="files or directories (default: src/ include/)" + ) + parser.add_argument( + "-q", "--quiet", action="store_true", help="only print the summary count" + ) + args = parser.parse_args() + roots = args.files or [root for root in DEFAULT_ROOTS if Path(root).is_dir()] + + total = 0 + for path in sorted(set(iter_files(roots)), key=str): + for finding in check_file(path): + total += 1 + if not args.quiet: + print( + f"{path}:{finding.line}: {finding.category.label}: {finding.message}" + ) + print(f"\n{total} doxygen-style violation(s)", file=sys.stderr) + return 1 if total else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/pre-commit/clang_tidy_check.py b/bin/pre-commit/clang_tidy_check.py index 5b5792b405..cf4808d2ea 100755 --- a/bin/pre-commit/clang_tidy_check.py +++ b/bin/pre-commit/clang_tidy_check.py @@ -1,27 +1,46 @@ #!/usr/bin/env python3 -"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy. +"""Pre-commit hook that runs clang-tidy on staged files using run-clang-tidy. -The set of files is chosen by pre-commit (see .pre-commit-config.yaml), which -filters to C/C++ sources and excludes `.ipp` fragments. Headers are linted -directly: the `verify_headers` build option (ON by default) compiles every -`.h`/`.hpp` on its own, so each header is the main file of its own -compile_commands.json entry and run-clang-tidy can analyse it just like a -`.cpp`. +The script determines the staged files itself (see `pass_filenames: false` in +.pre-commit-config.yaml) so run-clang-tidy is run once and handles parallelism +internally: pre-commit would otherwise split the files across parallel hook +invocations that race when fixes edit a shared header. + +Fixes are collected with `-export-fixes` and applied by clang-apply-replacements +in a separate step rather than with run-clang-tidy's `-fix`. The `add_module` +build isolates each module's headers behind a per-module symlink directory +(build/modules//...), so a header reachable from several translation +units is referenced through different paths that all resolve to the same source +file. clang-apply-replacements deduplicates identical replacements by their +literal path, so those paths must be canonicalised to the real source path +first; otherwise the same fix is applied once per path and corrupts the header. """ from __future__ import annotations import os +import re import shutil import subprocess import sys +import tempfile from pathlib import Path CLANG_TIDY_VERSION = 22 +# Extensions run-clang-tidy can analyse: `.cpp` translation units and, thanks to +# the `verify_headers` build option, `.h`/`.hpp` headers (each has its own +# compile_commands.json entry). `.ipp` fragments have no entry and are skipped. +TIDY_EXTENSIONS = {".cpp", ".h", ".hpp"} -def find_run_clang_tidy() -> str | None: - for candidate in (f"run-clang-tidy-{CLANG_TIDY_VERSION}", "run-clang-tidy"): +# A single-quoted `FilePath:` entry in an -export-fixes YAML file, allowing the +# `- ` marker that precedes it inside a `Replacements:` sequence. clang-tidy +# emits paths single-quoted and doubles any embedded quote per YAML rules. +FILEPATH_RE = re.compile(r"^(\s*(?:-\s+)?FilePath:\s*)'((?:[^']|'')*)'\s*$") + + +def find_tool(name: str) -> str | None: + for candidate in (f"{name}-{CLANG_TIDY_VERSION}", name): if path := shutil.which(candidate): return path return None @@ -35,23 +54,43 @@ def find_build_dir(repo_root: Path) -> Path | None: return None +def staged_files(repo_root: Path) -> list[Path]: + """Return absolute paths of staged, lint-able C/C++ files. + + `--diff-filter=d` excludes deletions so we never lint a removed file. + """ + output = subprocess.check_output( + ["git", "diff", "--staged", "--name-only", "--diff-filter=d", "--"] + + [f"*{ext}" for ext in TIDY_EXTENSIONS], + text=True, + cwd=repo_root, + ) + return [repo_root / rel for rel in output.splitlines() if rel] + + +def canonicalize_fix_paths(fixes_dir: Path) -> None: + """Rewrite every `FilePath` in the exported fixes to its real source path. + + A header included through a module's isolation symlink is recorded under that + symlink's path; collapsing all paths to the same real file lets + clang-apply-replacements recognise the per-translation-unit duplicates and + apply each fix once. + """ + for yaml in fixes_dir.glob("*.yaml"): + lines = [] + for line in yaml.read_text().splitlines(): + if m := FILEPATH_RE.match(line): + path = m.group(2).replace("''", "'") + real = os.path.realpath(path).replace("'", "''") + line = f"{m.group(1)}'{real}'" + lines.append(line) + yaml.write_text("\n".join(lines) + "\n") + + def main(): if not os.environ.get("TIDY"): return 0 - files = sys.argv[1:] - if not files: - return 0 - - run_clang_tidy = find_run_clang_tidy() - if not run_clang_tidy: - print( - f"clang-tidy check failed: TIDY is enabled but neither " - f"'run-clang-tidy-{CLANG_TIDY_VERSION}' nor 'run-clang-tidy' was found in PATH.", - file=sys.stderr, - ) - return 1 - repo_root = Path( subprocess.check_output( ["git", "rev-parse", "--show-toplevel"], @@ -59,6 +98,29 @@ def main(): text=True, ).strip() ) + + files = staged_files(repo_root) + if not files: + return 0 + + run_clang_tidy = find_tool("run-clang-tidy") + clang_apply_replacements = find_tool("clang-apply-replacements") + missing = [ + name + for name, path in ( + ("run-clang-tidy", run_clang_tidy), + ("clang-apply-replacements", clang_apply_replacements), + ) + if not path + ] + if missing: + print( + f"clang-tidy check failed: TIDY is enabled but {' and '.join(missing)} " + f"was not found in PATH (tried the '-{CLANG_TIDY_VERSION}' suffix too).", + file=sys.stderr, + ) + return 1 + build_dir = find_build_dir(repo_root) if not build_dir: print( @@ -68,11 +130,23 @@ def main(): ) return 1 - result = subprocess.run( - [run_clang_tidy, "-quiet", "-p", str(build_dir), "-fix", "-allow-no-checks"] - + files - ) - return result.returncode + with tempfile.TemporaryDirectory() as fixes_dir: + result = subprocess.run( + [ + run_clang_tidy, + "-quiet", + "-p", + build_dir, + "-export-fixes", + fixes_dir, + "-allow-no-checks", + ] + + files + ) + canonicalize_fix_paths(Path(fixes_dir)) + applied = subprocess.run([clang_apply_replacements, fixes_dir]) + + return result.returncode or applied.returncode if __name__ == "__main__": diff --git a/bin/pre-commit/test_check_doxygen_style.py b/bin/pre-commit/test_check_doxygen_style.py new file mode 100755 index 0000000000..861414f46d --- /dev/null +++ b/bin/pre-commit/test_check_doxygen_style.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 +""" +Tests for check_doxygen_style.py. + +Run directly (no test framework needed): + ./bin/pre-commit/test_check_doxygen_style.py +or under pytest: + pytest bin/pre-commit/test_check_doxygen_style.py +""" + +import sys +import textwrap + +from check_doxygen_style import Finding, check_source + + +def findings_for(text: str) -> list[Finding]: + """Return the style violations for the given source text. + + The text is dedented and its leading newline stripped, so fixtures can be + written as indented triple-quoted here-docs while keeping honest 1-based + line numbers. + """ + text = textwrap.dedent(text).lstrip("\n") + return check_source(text) + + +def labels_for(text: str) -> list[str]: + return [f.category.label for f in findings_for(text)] + + +def messages_for(text: str) -> list[str]: + return [f.message for f in findings_for(text)] + + +# --- well-formed input produces nothing ------------------------------------- + + +def test_clean_block_ok() -> None: + code = """ + /** + * Brief. + * + * @tparam T a type + * @param x the x + * @return the result + */ + """ + assert findings_for(code) == [] + + +def test_blank_lines_inside_block_ok() -> None: + code = """ + /** + * a + * + * b + */ + """ + assert findings_for(code) == [] + + +def test_member_and_divider_allowed() -> None: + assert findings_for("int x; ///< ok member\n") == [] + assert findings_for("//////////\n") == [] + assert findings_for("//// text\n") == [] + + +# --- line-comment forms ------------------------------------------------------ + + +def test_triple_slash() -> None: + code = "/// doc\n" + assert labels_for(code) == ["triple-slash"] + + +def test_qt_line() -> None: + code = "//! doc\n" + assert labels_for(code) == ["qt-line"] + + +def test_qt_member() -> None: + code = "int x; //!< doc\n" + assert labels_for(code) == ["qt-member"] + + +def test_block_member() -> None: + code = "int x; /**< doc */\n" + assert labels_for(code) == ["block-member"] + + +def test_qt_block_member() -> None: + code = "int x; /*!< doc */\n" + assert labels_for(code) == ["qt-block-member"] + + +def test_doc_in_line_comment() -> None: + code = "// @param x\n" + assert labels_for(code) == ["doc-in-line-comment"] + + +# --- block forms ------------------------------------------------------------- + + +def test_qt_comment() -> None: + code = """ + /*! + * brief + */ + """ + assert labels_for(code) == ["qt-comment"] + + +def test_qt_comment_single_line() -> None: + # /*! ... */ on one line -> qt-comment (plus single-line-block) + code = "/*! brief */\n" + assert labels_for(code) == ["qt-comment", "single-line-block"] + + +def test_single_line_block() -> None: + code = "/** brief */\n" + assert labels_for(code) == ["single-line-block"] + + +def test_single_line_markers_allowed() -> None: + for marker in ("@{", "@}", "@cond LABEL", "@endcond", "@file foo.h"): + code = f"/** {marker} */\n" + assert findings_for(code) == [], marker + + +def test_text_on_opener() -> None: + code = """ + /** text here + * more + */ + """ + assert labels_for(code) == ["text-on-opener"] + + +def test_bare_continuation() -> None: + code = """ + /** + * a + bare line + */ + """ + assert labels_for(code) == ["bare-continuation"] + + +def test_over_indented_first_line() -> None: + code = """ + /** + * over + */ + """ + assert labels_for(code) == ["over-indented"] + + +def test_over_indented_tag() -> None: + # a flush first line consumes "first content", isolating the tag check + code = """ + /** + * brief + * @param x + */ + """ + assert labels_for(code) == ["over-indented-tag"] + + +def test_combined_marker() -> None: + code = """ + /** + * @{ + */ + """ + assert labels_for(code) == ["combined-marker"] + + +def test_prose_label() -> None: + for word in ("Returns", "Throws", "Exceptions"): + code = f""" + /** + * {word}: + */ + """ + assert labels_for(code) == ["prose-label"], word + + +def test_content_on_closer() -> None: + code = """ + /** + * a + * b */ + """ + assert labels_for(code) == ["content-on-closer"] + + +def test_plain_block_doc() -> None: + assert labels_for("/* @param x */\n") == ["plain-block-doc"] + assert findings_for("/* just an ordinary note */\n") == [] + + +def test_tag_order() -> None: + out_of_order = """ + /** + * @param x + * @tparam T + */ + """ + assert labels_for(out_of_order) == ["tag-order"] + + correct = """ + /** + * @tparam T + * @param x + * @return r + */ + """ + assert findings_for(correct) == [] + + single = """ + /** + * @param x + */ + """ + assert findings_for(single) == [] # single tag: never out of order + + +# --- command spelling (must work on body/closer lines, not just the opener) -- + + +def test_backslash_command_on_body_line() -> None: + code = r""" + /** + * \brief x + */ + """ + assert labels_for(code) == ["backslash-command"] + + +def test_backslash_command_suggests_canonical_spelling() -> None: + # a backslash + non-canonical spelling is fixed in one pass, not two: + # \sa -> @see (not @sa), \returns -> @return (not @returns) + sa = r""" + /** + * \sa other + */ + """ + assert messages_for(sa) == [r"use @see instead of \sa"] + + returns = r""" + /** + * \returns x + */ + """ + assert messages_for(returns) == [r"use @return instead of \returns"] + + +def test_wrong_command_on_body_line() -> None: + code = """ + /** + * @returns x + */ + """ + assert labels_for(code) == ["wrong-command"] + + +def test_body_line_commands_regression() -> None: + # regression: these live on body lines of a multi-line block + code = r""" + /** + * @returns bad + * @throw ex + * @sa other + * \param y + */ + """ + assert labels_for(code) == [ + "wrong-command", + "wrong-command", + "wrong-command", + "backslash-command", + ] + + +def test_command_on_closer_line() -> None: + code = """ + /** + * a + * @sa b */ + """ + assert labels_for(code) == ["wrong-command", "content-on-closer"] + + +def test_no_double_count_across_opener_body_closer() -> None: + code = """ + /** @returns opener + * @throw body + * @sa closer */ + """ + assert labels_for(code).count("wrong-command") == 3 + + +def test_code_with_word_allowed() -> None: + # @code{.cpp} is valid Doxygen and must not be flagged + code = """ + /** + * @code{.cpp} + * int x; + * @endcode + */ + """ + assert findings_for(code) == [] + + +# --- rendered message text --------------------------------------------------- + + +def test_message_uses_category_description() -> None: + # a static category renders its default description + code = "/// doc\n" + assert messages_for(code) == ["use a /** ... */ block instead of ///"] + + +def test_message_detail_overrides() -> None: + # dynamic categories render the offending text via Finding.detail + backslash = r""" + /** + * \param y + */ + """ + assert messages_for(backslash) == [r"use @param instead of \param"] + + wrong = """ + /** + * @returns x + */ + """ + assert messages_for(wrong) == ["use @return instead of @returns"] + + prose = """ + /** + * Throws: + */ + """ + assert messages_for(prose) == ['use @throws instead of prose "Throws:"'] + + +# --- robustness -------------------------------------------------------------- + + +def test_empty_file_no_crash() -> None: + assert findings_for("") == [] + + +def test_mid_line_plain_block_skipped() -> None: + # a /* opened mid-line (after code) and spanning lines is skipped, so its + # comment-like contents are not analyzed + code = """ + int x = 0; /* note: @returns is not a real tag here + * @param also not real + */ + int y = 0; + """ + assert findings_for(code) == [] + + +def test_unclosed_block_scanned_to_eof() -> None: + # an unterminated /** block is still scanned to EOF (no crash, body checked) + code = """ + /** + * @returns x + """ + assert labels_for(code) == ["wrong-command"] + + +def test_banner_and_empty_comment_not_flagged() -> None: + code = """ + /*** + * banner + ***/ + """ + assert findings_for(code) == [] + assert findings_for("/**/\n") == [] + + +def main() -> int: + tests = sorted( + (name, fn) + for name, fn in globals().items() + if name.startswith("test_") and callable(fn) + ) + failed = 0 + for name, fn in tests: + try: + fn() + print(f"PASS {name}") + except AssertionError as exc: + failed += 1 + print(f"FAIL {name}: {exc!r}") + print(f"\n{len(tests) - failed}/{len(tests)} passed") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cmake/scripts/codegen/templates/LedgerEntry.h.mako b/cmake/scripts/codegen/templates/LedgerEntry.h.mako index 63f5f39ef9..c799903b21 100644 --- a/cmake/scripts/codegen/templates/LedgerEntry.h.mako +++ b/cmake/scripts/codegen/templates/LedgerEntry.h.mako @@ -177,7 +177,9 @@ ${field['typeData']['setter_type']} ${field['paramName']}${',' if i < len(requir object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ % for field in fields: /** diff --git a/cmake/scripts/codegen/templates/Transaction.h.mako b/cmake/scripts/codegen/templates/Transaction.h.mako index d3b303d9d6..49e2e4a5cd 100644 --- a/cmake/scripts/codegen/templates/Transaction.h.mako +++ b/cmake/scripts/codegen/templates/Transaction.h.mako @@ -185,7 +185,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ % for field in fields: /** diff --git a/conan.lock b/conan.lock index b6ddfa4e58..9dfbb86960 100644 --- a/conan.lock +++ b/conan.lock @@ -10,22 +10,22 @@ "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713", "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897", "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933", - "openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886", + "openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288", "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166", - "mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828", + "mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355", "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188", "libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732", "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892", "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228", "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979", - "grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616", + "grpc/1.81.1#f729f6d75992d20f9c72828e9142d62f%1783945160.094135", "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562", "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492", "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654", "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1782392402.296732", "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1782392419.475605", - "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833" + "abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047" ], "build_requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708", @@ -38,7 +38,7 @@ "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226", "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56", "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86", - "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833" + "abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047" ], "python_requires": [], "overrides": { diff --git a/conanfile.py b/conanfile.py index db12dcb585..f0b10cf34b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -134,7 +134,7 @@ class Xrpl(ConanFile): if self.options.jemalloc: self.requires("jemalloc/5.3.1") self.requires("lz4/1.10.0", force=True) - self.requires("mpt-crypto/0.4.0-rc2", transitive_headers=True) + self.requires("mpt-crypto/0.4.0-rc4", transitive_headers=True) self.requires("protobuf/6.33.5", force=True) if self.options.rocksdb: self.requires("rocksdb/10.5.1") diff --git a/docs/build/environment.md b/docs/build/environment.md index 2cca608567..e639ed2d5f 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -33,9 +33,10 @@ with a single command and without installing anything system-wide: nix --experimental-features 'nix-command flakes' develop ``` -On **Linux**, Nix also provides the compiler (GCC). On **macOS**, the shell uses -your **system-wide Apple Clang** as the compiler, so you still need to manage -its version (see below). +On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides +Clang. If you instead opt to use your system-wide Apple Clang (via +`nix develop .#apple-clang`), you need to manage its version yourself (see +below). See [Using the Nix development shell](./nix.md) for installation and usage details, including how to select a different compiler. @@ -48,10 +49,10 @@ details, including how to select a different compiler. ### macOS: managing the Apple Clang version -Because the Nix shell uses the system-wide Apple Clang on macOS, the compiler -version is whatever your installed Xcode (or Command Line Tools) provides. The -following command should return a version greater than or equal to the -[minimum required](#tested-compiler-versions): +If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`), +the compiler version is whatever your installed Xcode (or Command Line Tools) +provides. The following command should return a version greater than or equal to +the [minimum required](#tested-compiler-versions): ```bash clang --version diff --git a/docs/build/nix.md b/docs/build/nix.md index 2ae483aefe..d6e53a254a 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -9,7 +9,7 @@ This guide explains how to use Nix to set up a reproducible development environm - **Reproducible environment**: Everyone gets the same versions of tools and compilers - **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment - **No system pollution**: Dependencies are isolated and don't affect your system packages -- **Multiple compiler versions**: Easily switch between different GCC and Clang versions +- **Consistent compilers**: The GCC and Clang shells use the same versions as CI - **Quick setup**: Get started with a single command - **Works on Linux and macOS**: Consistent experience across platforms @@ -31,8 +31,8 @@ This will: - Download and set up all required development tools (CMake, Ninja, Conan, etc.) - Configure the appropriate compiler for your platform: - - **Linux**: GCC 15.2 (provided by Nix) - - **macOS**: Apple Clang (your system compiler) + - **Linux**: GCC (provided by Nix) + - **macOS**: Clang (provided by Nix) 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. @@ -40,12 +40,12 @@ The first time you run this command, it will take a few minutes to download and - **Linux**: `nix develop` gives you a shell with all the tooling necessary to develop xrpld and with GCC 15.2 (also provided by Nix). There are no caveats. -- **macOS**: `nix develop` gives you a full environment too. The compiler is - your system-wide Apple Clang, while every other tool — including Conan — is - provided by Nix. Conan has no binary in the Nix cache for macOS, so it is - built from source the first time you enter the shell, which makes the initial - setup slower (this is handled automatically; see - [`nix/devshell.nix`](../../nix/devshell.nix)). +- **macOS**: `nix develop` gives you a full environment too, with Clang (and + every other tool, including Conan) provided by Nix. To use your system-wide + Apple Clang instead, enter `nix develop .#apple-clang`. Conan has no binary in + the Nix cache for macOS, so it is built from source the first time you enter + the shell, which makes the initial setup slower (this is handled + automatically; see [`nix/devshell.nix`](../../nix/devshell.nix)). > [!TIP] > To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`: @@ -62,7 +62,9 @@ The first time you run this command, it will take a few minutes to download and ### Choosing a different compiler -A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`. +A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#clang`. +The `.#gcc` and `.#clang` shells provide the same GCC and Clang versions used in CI +(pinned in [`nix/packages.nix`](../../nix/packages.nix)). Use `nix flake show` to see all the available development shells. Use `nix develop .#no-compiler` to use the compiler from your system. @@ -70,11 +72,11 @@ Use `nix develop .#no-compiler` to use the compiler from your system. ### Example Usage ```bash -# Use GCC 14 -nix develop .#gcc14 +# Use GCC (same version as CI) +nix develop .#gcc -# Use Clang 19 -nix develop .#clang19 +# Use Clang (same version as CI) +nix develop .#clang # Use default for your platform nix develop @@ -112,7 +114,15 @@ Once inside the Nix development shell, follow the standard [build instructions]( [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. -This is also the most robust way to use the environment from **any shell** (bash, zsh, fish, …): direnv stays in your current shell and loads the environment _after_ your shell's startup files have run, so the Nix-provided tools take precedence over anything your shell configuration adds to `$PATH`. To use it, install direnv for your shell, then add an `.envrc` containing `use flake` at the repository root and run `direnv allow`. +This is also the most robust way to use the environment from **any shell** (bash, zsh, fish, …): direnv stays in your current shell and loads the environment _after_ your shell's startup files have run, so the Nix-provided tools take precedence over anything your shell configuration adds to `$PATH`. + +The repository already ships an `.envrc` at its root that activates the Nix flake development shell, so you don't need to create one. To use it: + +1. [Install direnv](https://direnv.net/docs/installation.html) and [hook it into your shell](https://direnv.net/docs/hook.html) (bash, zsh, fish, …). Installing [nix-direnv](https://github.com/nix-community/nix-direnv) as well is recommended: it caches the shell so that activation is near-instant after the first run. +2. Run `direnv allow` once in the repository root. direnv will then load (and reload) the Nix development shell automatically whenever you enter the directory. + +> [!NOTE] +> direnv only caches the `.direnv` directory (already listed in `.gitignore`); no other repository files are affected. ## Conan and Prebuilt Packages diff --git a/include/xrpl/basics/Archive.h b/include/xrpl/basics/Archive.h index 58e12bbb71..66d6a019af 100644 --- a/include/xrpl/basics/Archive.h +++ b/include/xrpl/basics/Archive.h @@ -4,13 +4,14 @@ namespace xrpl { -/** Extract a tar archive compressed with lz4 - - @param src the path of the archive to be extracted - @param dst the directory to extract to - - @throws runtime_error -*/ +/** + * Extract a tar archive compressed with lz4 + * + * @param src the path of the archive to be extracted + * @param dst the directory to extract to + * + * @throws runtime_error + */ void extractTarLz4(boost::filesystem::path const& src, boost::filesystem::path const& dst); diff --git a/include/xrpl/basics/Blob.h b/include/xrpl/basics/Blob.h index ee0d6cf3b5..bfb8e5a697 100644 --- a/include/xrpl/basics/Blob.h +++ b/include/xrpl/basics/Blob.h @@ -4,9 +4,10 @@ namespace xrpl { -/** Storage for linear binary data. - Blocks of binary data appear often in various idioms and structures. -*/ +/** + * Storage for linear binary data. + * Blocks of binary data appear often in various idioms and structures. + */ using Blob = std::vector; } // namespace xrpl diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index c0ae8ef56e..05af6c409a 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -10,9 +10,10 @@ namespace xrpl { -/** Like std::vector but better. - Meets the requirements of BufferFactory. -*/ +/** + * Like std::vector but better. + * Meets the requirements of BufferFactory. + */ class Buffer { private: @@ -24,30 +25,37 @@ public: Buffer() = default; - /** Create an uninitialized buffer with the given size. */ + /** + * Create an uninitialized buffer with the given size. + */ explicit Buffer(std::size_t size) : p_((size != 0u) ? new std::uint8_t[size] : nullptr), size_(size) { } - /** Create a buffer as a copy of existing memory. - - @param data a pointer to the existing memory. If - size is non-zero, it must not be null. - @param size size of the existing memory block. - */ + /** + * Create a buffer as a copy of existing memory. + * + * @param data a pointer to the existing memory. If + * size is non-zero, it must not be null. + * @param size size of the existing memory block. + */ Buffer(void const* data, std::size_t size) : Buffer(size) { if (size != 0u) std::memcpy(p_.get(), data, size); } - /** Copy-construct */ + /** + * Copy-construct + */ Buffer(Buffer const& other) : Buffer(other.p_.get(), other.size_) { } - /** Copy assign */ + /** + * Copy assign + */ Buffer& operator=(Buffer const& other) { @@ -59,17 +67,19 @@ public: return *this; } - /** Move-construct. - The other buffer is reset. - */ + /** + * Move-construct. + * The other buffer is reset. + */ Buffer(Buffer&& other) noexcept : p_(std::move(other.p_)), size_(other.size_) { other.size_ = 0; } - /** Move-assign. - The other buffer is reset. - */ + /** + * Move-assign. + * The other buffer is reset. + */ Buffer& operator=(Buffer&& other) noexcept { @@ -82,12 +92,16 @@ public: return *this; } - /** Construct from a slice */ + /** + * Construct from a slice + */ explicit Buffer(Slice s) : Buffer(s.data(), s.size()) { } - /** Assign from slice */ + /** + * Assign from slice + */ Buffer& operator=(Slice s) { @@ -101,7 +115,9 @@ public: return *this; } - /** Returns the number of bytes in the buffer. */ + /** + * Returns the number of bytes in the buffer. + */ [[nodiscard]] std::size_t size() const noexcept { @@ -121,10 +137,11 @@ public: return Slice{p_.get(), size_}; } - /** Return a pointer to beginning of the storage. - @note The return type is guaranteed to be a pointer - to a single byte, to facilitate pointer arithmetic. - */ + /** + * Return a pointer to beginning of the storage. + * @note The return type is guaranteed to be a pointer + * to a single byte, to facilitate pointer arithmetic. + */ /** @{ */ [[nodiscard]] std::uint8_t const* data() const noexcept @@ -139,9 +156,10 @@ public: } /** @} */ - /** Reset the buffer. - All memory is deallocated. The resulting size is 0. - */ + /** + * Reset the buffer. + * All memory is deallocated. The resulting size is 0. + */ void clear() noexcept { @@ -149,9 +167,10 @@ public: size_ = 0; } - /** Reallocate the storage. - Existing data, if any, is discarded. - */ + /** + * Reallocate the storage. + * Existing data, if any, is discarded. + */ std::uint8_t* alloc(std::size_t n) { diff --git a/include/xrpl/basics/CompressionAlgorithms.h b/include/xrpl/basics/CompressionAlgorithms.h index a5ec8645b6..316acb14ac 100644 --- a/include/xrpl/basics/CompressionAlgorithms.h +++ b/include/xrpl/basics/CompressionAlgorithms.h @@ -12,7 +12,8 @@ namespace xrpl::compression_algorithms { -/** LZ4 block compression. +/** + * LZ4 block compression. * @tparam BufferFactory Callable object or lambda. * Takes the requested buffer size and returns allocated buffer pointer. * @param in Data to compress @@ -80,7 +81,8 @@ lz4Decompress( return decompressedSize; } -/** LZ4 block decompression. +/** + * LZ4 block decompression. * @tparam InputStream ZeroCopyInputStream * @param in Input source stream * @param inSize Size of compressed data diff --git a/include/xrpl/basics/CountedObject.h b/include/xrpl/basics/CountedObject.h index 275894673e..bb7b0d8877 100644 --- a/include/xrpl/basics/CountedObject.h +++ b/include/xrpl/basics/CountedObject.h @@ -9,7 +9,9 @@ namespace xrpl { -/** Manages all counted object types. */ +/** + * Manages all counted object types. + */ class CountedObjects { public: @@ -23,10 +25,11 @@ public: getCounts(int minimumThreshold) const; public: - /** Implementation for @ref CountedObject. - - @internal - */ + /** + * Implementation for @ref CountedObject. + * + * @internal + */ class Counter { public: @@ -94,13 +97,14 @@ private: //------------------------------------------------------------------------------ -/** Tracks the number of instances of an object. - - Derived classes have their instances counted automatically. This is used - for reporting purposes. - - @ingroup basics -*/ +/** + * Tracks the number of instances of an object. + * + * Derived classes have their instances counted automatically. This is used + * for reporting purposes. + * + * @ingroup basics + */ template class CountedObject { diff --git a/include/xrpl/basics/DecayingSample.h b/include/xrpl/basics/DecayingSample.h index 86a8baa62e..1b05770734 100644 --- a/include/xrpl/basics/DecayingSample.h +++ b/include/xrpl/basics/DecayingSample.h @@ -6,9 +6,10 @@ namespace xrpl { -/** Sampling function using exponential decay to provide a continuous value. - @tparam The number of seconds in the decay window. -*/ +/** + * Sampling function using exponential decay to provide a continuous value. + * @tparam The number of seconds in the decay window. + */ template class DecayingSample { @@ -19,15 +20,16 @@ public: DecayingSample() = delete; /** - @param now Start time of DecayingSample. - */ + * @param now Start time of DecayingSample. + */ explicit DecayingSample(time_point now) : value_(value_type()), when_(now) { } - /** Add a new sample. - The value is first aged according to the specified time. - */ + /** + * Add a new sample. + * The value is first aged according to the specified time. + */ value_type add(value_type value, time_point now) { @@ -36,9 +38,10 @@ public: return value_ / Window; } - /** Retrieve the current value in normalized units. - The samples are first aged according to the specified time. - */ + /** + * Retrieve the current value in normalized units. + * The samples are first aged according to the specified time. + */ value_type value(time_point now) { @@ -87,9 +90,10 @@ private: //------------------------------------------------------------------------------ -/** Sampling function using exponential decay to provide a continuous value. - @tparam HalfLife The half life of a sample, in seconds. -*/ +/** + * Sampling function using exponential decay to provide a continuous value. + * @tparam HalfLife The half life of a sample, in seconds. + */ template class DecayWindow { diff --git a/include/xrpl/basics/IntrusivePointer.h b/include/xrpl/basics/IntrusivePointer.h index c23d6afb85..59853ad4d0 100644 --- a/include/xrpl/basics/IntrusivePointer.h +++ b/include/xrpl/basics/IntrusivePointer.h @@ -10,33 +10,37 @@ namespace xrpl { //------------------------------------------------------------------------------ -/** Tag to create an intrusive pointer from another intrusive pointer by using a - static cast. This is useful to create an intrusive pointer to a derived - class from an intrusive pointer to a base class. -*/ +/** + * Tag to create an intrusive pointer from another intrusive pointer by using a + * static cast. This is useful to create an intrusive pointer to a derived + * class from an intrusive pointer to a base class. + */ struct StaticCastTagSharedIntrusive { }; -/** Tag to create an intrusive pointer from another intrusive pointer by using a - dynamic cast. This is useful to create an intrusive pointer to a derived - class from an intrusive pointer to a base class. If the cast fails an empty - (null) intrusive pointer is created. -*/ +/** + * Tag to create an intrusive pointer from another intrusive pointer by using a + * dynamic cast. This is useful to create an intrusive pointer to a derived + * class from an intrusive pointer to a base class. If the cast fails an empty + * (null) intrusive pointer is created. + */ struct DynamicCastTagSharedIntrusive { }; -/** When creating or adopting a raw pointer, controls whether the strong count - is incremented or not. Use this tag to increment the strong count. -*/ +/** + * When creating or adopting a raw pointer, controls whether the strong count + * is incremented or not. Use this tag to increment the strong count. + */ struct SharedIntrusiveAdoptIncrementStrongTag { }; -/** When creating or adopting a raw pointer, controls whether the strong count - is incremented or not. Use this tag to leave the strong count unchanged. -*/ +/** + * When creating or adopting a raw pointer, controls whether the strong count + * is incremented or not. Use this tag to leave the strong count unchanged. + */ struct SharedIntrusiveAdoptNoIncrementTag { }; @@ -50,20 +54,21 @@ concept CAdoptTag = std::is_same_v || //------------------------------------------------------------------------------ -/** A shared intrusive pointer class that supports weak pointers. - - This is meant to be used for SHAMapInnerNodes, but may be useful for other - cases. Since the reference counts are stored on the pointee, the pointee is - not destroyed until both the strong _and_ weak pointer counts go to zero. - When the strong pointer count goes to zero, the "partialDestructor" is - called. This can be used to destroy as much of the object as possible while - still retaining the reference counts. For example, for SHAMapInnerNodes the - children may be reset in that function. Note that std::shared_pointer WILL - run the destructor when the strong count reaches zero, but may not free the - memory used by the object until the weak count reaches zero. In xrpld, we - typically allocate shared pointers with the `make_shared` function. When - that is used, the memory is not reclaimed until the weak count reaches zero. -*/ +/** + * A shared intrusive pointer class that supports weak pointers. + * + * This is meant to be used for SHAMapInnerNodes, but may be useful for other + * cases. Since the reference counts are stored on the pointee, the pointee is + * not destroyed until both the strong _and_ weak pointer counts go to zero. + * When the strong pointer count goes to zero, the "partialDestructor" is + * called. This can be used to destroy as much of the object as possible while + * still retaining the reference counts. For example, for SHAMapInnerNodes the + * children may be reset in that function. Note that std::shared_pointer WILL + * run the destructor when the strong count reaches zero, but may not free the + * memory used by the object until the weak count reaches zero. In xrpld, we + * typically allocate shared pointers with the `make_shared` function. When + * that is used, the memory is not reclaimed until the weak count reaches zero. + */ template class SharedIntrusive { @@ -111,8 +116,9 @@ public: operator=( SharedIntrusive&& rhs); // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) - /** Adopt the raw pointer. The strong reference may or may not be - incremented, depending on the TAdoptTag + /** + * Adopt the raw pointer. The strong reference may or may not be + * incremented, depending on the TAdoptTag */ template void @@ -120,27 +126,31 @@ public: ~SharedIntrusive(); - /** Create a new SharedIntrusive by statically casting the pointer - controlled by the rhs param. - */ + /** + * Create a new SharedIntrusive by statically casting the pointer + * controlled by the rhs param. + */ template SharedIntrusive(StaticCastTagSharedIntrusive, SharedIntrusive const& rhs); - /** Create a new SharedIntrusive by statically casting the pointer - controlled by the rhs param. - */ + /** + * Create a new SharedIntrusive by statically casting the pointer + * controlled by the rhs param. + */ template SharedIntrusive(StaticCastTagSharedIntrusive, SharedIntrusive&& rhs); - /** Create a new SharedIntrusive by dynamically casting the pointer - controlled by the rhs param. - */ + /** + * Create a new SharedIntrusive by dynamically casting the pointer + * controlled by the rhs param. + */ template SharedIntrusive(DynamicCastTagSharedIntrusive, SharedIntrusive const& rhs); - /** Create a new SharedIntrusive by dynamically casting the pointer - controlled by the rhs param. - */ + /** + * Create a new SharedIntrusive by dynamically casting the pointer + * controlled by the rhs param. + */ template SharedIntrusive(DynamicCastTagSharedIntrusive, SharedIntrusive&& rhs); @@ -153,17 +163,22 @@ public: explicit operator bool() const noexcept; - /** Set the pointer to null, decrement the strong count, and run the - appropriate release action. - */ + /** + * Set the pointer to null, decrement the strong count, and run the + * appropriate release action. + */ void reset(); - /** Get the raw pointer */ + /** + * Get the raw pointer + */ [[nodiscard]] T* get() const; - /** Return the strong count */ + /** + * Return the strong count + */ [[nodiscard]] std::size_t useCount() const; @@ -181,43 +196,51 @@ public: friend class WeakIntrusive; private: - /** Return the raw pointer held by this object. */ + /** + * Return the raw pointer held by this object. + */ [[nodiscard]] T* unsafeGetRawPtr() const; - /** Exchange the current raw pointer held by this object with the given - pointer. Decrement the strong count of the raw pointer previously held - by this object and run the appropriate release action. + /** + * Exchange the current raw pointer held by this object with the given + * pointer. Decrement the strong count of the raw pointer previously held + * by this object and run the appropriate release action. */ void unsafeReleaseAndStore(T* next); - /** Set the raw pointer directly. This is wrapped in a function so the class - can support both atomic and non-atomic pointers in a future patch. + /** + * Set the raw pointer directly. This is wrapped in a function so the class + * can support both atomic and non-atomic pointers in a future patch. */ void unsafeSetRawPtr(T* p); - /** Exchange the raw pointer directly. - This sets the raw pointer to the given value and returns the previous - value. This is wrapped in a function so the class can support both - atomic and non-atomic pointers in a future patch. + /** + * Exchange the raw pointer directly. + * This sets the raw pointer to the given value and returns the previous + * value. This is wrapped in a function so the class can support both + * atomic and non-atomic pointers in a future patch. */ T* unsafeExchange(T* p); - /** pointer to the type with an intrusive count */ + /** + * pointer to the type with an intrusive count + */ T* ptr_{nullptr}; }; //------------------------------------------------------------------------------ -/** A weak intrusive pointer class for the SharedIntrusive pointer class. - -Note that this weak pointer class asks differently from normal weak pointer -classes. When the strong pointer count goes to zero, the "partialDestructor" -is called. See the comment on SharedIntrusive for a fuller explanation. -*/ +/** + * A weak intrusive pointer class for the SharedIntrusive pointer class. + * + * Note that this weak pointer class asks differently from normal weak pointer + * classes. When the strong pointer count goes to zero, the "partialDestructor" + * is called. See the comment on SharedIntrusive for a fuller explanation. + */ template class WeakIntrusive { @@ -247,54 +270,62 @@ public: WeakIntrusive& operator=(SharedIntrusive const& rhs); - /** Adopt the raw pointer and increment the weak count. */ + /** + * Adopt the raw pointer and increment the weak count. + */ void adopt(T* ptr); ~WeakIntrusive(); - /** Get a strong pointer from the weak pointer, if possible. This will - only return a seated pointer if the strong count on the raw pointer - is non-zero before locking. + /** + * Get a strong pointer from the weak pointer, if possible. This will + * only return a seated pointer if the strong count on the raw pointer + * is non-zero before locking. */ SharedIntrusive lock() const; - /** Return true if the strong count is zero. */ + /** + * Return true if the strong count is zero. + */ [[nodiscard]] bool expired() const; - /** Set the pointer to null and decrement the weak count. - - Note: This may run the destructor if the strong count is zero. - */ + /** + * Set the pointer to null and decrement the weak count. + * + * Note: This may run the destructor if the strong count is zero. + */ void reset(); private: T* ptr_ = nullptr; - /** Decrement the weak count. This does _not_ set the raw pointer to - null. - - Note: This may run the destructor if the strong count is zero. - */ + /** + * Decrement the weak count. This does _not_ set the raw pointer to + * null. + * + * Note: This may run the destructor if the strong count is zero. + */ void unsafeReleaseNoStore(); }; //------------------------------------------------------------------------------ -/** A combination of a strong and a weak intrusive pointer stored in the - space of a single pointer. - - This class is similar to a `std::variant` - with some optimizations. In particular, it uses a low-order bit to - determine if the raw pointer represents a strong pointer or a weak - pointer. It can also be quickly switched between its strong pointer and - weak pointer representations. This class is useful for storing intrusive - pointers in tagged caches. - */ +/** + * A combination of a strong and a weak intrusive pointer stored in the + * space of a single pointer. + * + * This class is similar to a `std::variant` + * with some optimizations. In particular, it uses a low-order bit to + * determine if the raw pointer represents a strong pointer or a weak + * pointer. It can also be quickly switched between its strong pointer and + * weak pointer representations. This class is useful for storing intrusive + * pointers in tagged caches. + */ template class SharedWeakUnion @@ -336,69 +367,83 @@ public: ~SharedWeakUnion(); - /** Return a strong pointer if this is already a strong pointer (i.e. - don't lock the weak pointer. Use the `lock` method if that's what's - needed) + /** + * Return a strong pointer if this is already a strong pointer (i.e. + * don't lock the weak pointer. Use the `lock` method if that's what's + * needed) */ [[nodiscard]] SharedIntrusive getStrong() const; - /** Return true if this is a strong pointer and the strong pointer is - seated. + /** + * Return true if this is a strong pointer and the strong pointer is + * seated. */ explicit operator bool() const noexcept; - /** Set the pointer to null, decrement the appropriate ref count, and - run the appropriate release action. + /** + * Set the pointer to null, decrement the appropriate ref count, and + * run the appropriate release action. */ void reset(); - /** If this is a strong pointer, return the raw pointer. Otherwise - return null. + /** + * If this is a strong pointer, return the raw pointer. Otherwise + * return null. */ [[nodiscard]] T* get() const; - /** If this is a strong pointer, return the strong count. Otherwise + /** + * If this is a strong pointer, return the strong count. Otherwise * return 0 */ [[nodiscard]] std::size_t useCount() const; - /** Return true if there is a non-zero strong count. */ + /** + * Return true if there is a non-zero strong count. + */ [[nodiscard]] bool expired() const; - /** If this is a strong pointer, return the strong pointer. Otherwise - attempt to lock the weak pointer. + /** + * If this is a strong pointer, return the strong pointer. Otherwise + * attempt to lock the weak pointer. */ [[nodiscard]] SharedIntrusive lock() const; - /** Return true is this represents a strong pointer. */ + /** + * Return true is this represents a strong pointer. + */ [[nodiscard]] bool isStrong() const; - /** Return true is this represents a weak pointer. */ + /** + * Return true is this represents a weak pointer. + */ [[nodiscard]] bool isWeak() const; - /** If this is a weak pointer, attempt to convert it to a strong - pointer. - - @return true if successfully converted to a strong pointer (or was - already a strong pointer). Otherwise false. - */ + /** + * If this is a weak pointer, attempt to convert it to a strong + * pointer. + * + * @return true if successfully converted to a strong pointer (or was + * already a strong pointer). Otherwise false. + */ bool convertToStrong(); - /** If this is a strong pointer, attempt to convert it to a weak - pointer. - - @return false if the pointer is null. Otherwise return true. - */ + /** + * If this is a strong pointer, attempt to convert it to a weak + * pointer. + * + * @return false if the pointer is null. Otherwise return true. + */ bool convertToWeak(); @@ -411,23 +456,27 @@ private: static constexpr std::uintptr_t kPtrMask = ~kTagMask; private: - /** Return the raw pointer held by this object. + /** + * Return the raw pointer held by this object. */ [[nodiscard]] T* unsafeGetRawPtr() const; enum class RefStrength { Strong, Weak }; - /** Set the raw pointer and tag bit directly. + /** + * Set the raw pointer and tag bit directly. */ void unsafeSetRawPtr(T* p, RefStrength rs); - /** Set the raw pointer and tag bit to all zeros (strong null pointer). + /** + * Set the raw pointer and tag bit to all zeros (strong null pointer). */ void unsafeSetRawPtr(std::nullptr_t); - /** Decrement the appropriate ref count, and run the appropriate release - action. Note: this does _not_ set the raw pointer to null. + /** + * Decrement the appropriate ref count, and run the appropriate release + * action. Note: this does _not_ set the raw pointer to null. */ void unsafeReleaseNoStore(); @@ -435,12 +484,13 @@ private: //------------------------------------------------------------------------------ -/** Create a shared intrusive pointer. - - Note: unlike std::shared_ptr, where there is an advantage of allocating - the pointer and control block together, there is no benefit for intrusive - pointers. -*/ +/** + * Create a shared intrusive pointer. + * + * Note: unlike std::shared_ptr, where there is an advantage of allocating + * the pointer and control block together, there is no benefit for intrusive + * pointers. + */ template SharedIntrusive makeSharedIntrusive(Args&&... args) diff --git a/include/xrpl/basics/IntrusivePointer.ipp b/include/xrpl/basics/IntrusivePointer.ipp index 8344a3e613..67d43b05d6 100644 --- a/include/xrpl/basics/IntrusivePointer.ipp +++ b/include/xrpl/basics/IntrusivePointer.ipp @@ -641,6 +641,9 @@ template T* SharedWeakUnion::unsafeGetRawPtr() const { + // tp_ packs a raw pointer together with a strength bit; recovering the + // pointer inherently requires an integer-to-pointer cast. + // NOLINTNEXTLINE(performance-no-int-to-ptr) return reinterpret_cast(tp_ & kPtrMask); } diff --git a/include/xrpl/basics/IntrusiveRefCounts.h b/include/xrpl/basics/IntrusiveRefCounts.h index 5eb1422541..caa06ed786 100644 --- a/include/xrpl/basics/IntrusiveRefCounts.h +++ b/include/xrpl/basics/IntrusiveRefCounts.h @@ -8,35 +8,38 @@ namespace xrpl { -/** Action to perform when releasing a strong pointer. - - noop: Do nothing. For example, a `noop` action will occur when a count is - decremented to a non-zero value. - - partialDestroy: Run the `partialDestructor`. This action will happen when a - strong count is decremented to zero and the weak count is non-zero. - - destroy: Run the destructor. This action will occur when either the strong - count or weak count is decremented and the other count is also zero. +/** + * Action to perform when releasing a strong pointer. + * + * noop: Do nothing. For example, a `noop` action will occur when a count is + * decremented to a non-zero value. + * + * partialDestroy: Run the `partialDestructor`. This action will happen when a + * strong count is decremented to zero and the weak count is non-zero. + * + * destroy: Run the destructor. This action will occur when either the strong + * count or weak count is decremented and the other count is also zero. */ enum class ReleaseStrongRefAction { NoOp, PartialDestroy, Destroy }; -/** Action to perform when releasing a weak pointer. - - noop: Do nothing. For example, a `noop` action will occur when a count is - decremented to a non-zero value. - - destroy: Run the destructor. This action will occur when either the strong - count or weak count is decremented and the other count is also zero. +/** + * Action to perform when releasing a weak pointer. + * + * noop: Do nothing. For example, a `noop` action will occur when a count is + * decremented to a non-zero value. + * + * destroy: Run the destructor. This action will occur when either the strong + * count or weak count is decremented and the other count is also zero. */ enum class ReleaseWeakRefAction { NoOp, Destroy }; -/** Implement the strong count, weak count, and bit flags for an intrusive - pointer. - - A class can satisfy the requirements of an xrpl::IntrusivePointer by - inheriting from this class. - */ +/** + * Implement the strong count, weak count, and bit flags for an intrusive + * pointer. + * + * A class can satisfy the requirements of an xrpl::IntrusivePointer by + * inheriting from this class. + */ struct IntrusiveRefCounts { virtual ~IntrusiveRefCounts() noexcept; @@ -105,109 +108,123 @@ private: static constexpr size_t kFieldTypeBits = sizeof(FieldType) * 8; static constexpr FieldType kOne = 1; - /** `refCounts` consists of four fields that are treated atomically: - - 1. Strong count. This is a count of the number of shared pointers that - hold a reference to this object. When the strong counts goes to zero, - if the weak count is zero, the destructor is run. If the weak count is - non-zero when the strong count goes to zero then the partialDestructor - is run. - - 2. Weak count. This is a count of the number of weak pointer that hold - a reference to this object. When the weak count goes to zero and the - strong count is also zero, then the destructor is run. - - 3. Partial destroy started bit. This bit is set if the - `partialDestructor` function has been started (or is about to be - started). This is used to prevent the destructor from running - concurrently with the partial destructor. This can easily happen when - the last strong pointer release its reference in one thread and starts - the partialDestructor, while in another thread the last weak pointer - goes out of scope and starts the destructor while the partialDestructor - is still running. Both a start and finished bit is needed to handle a - corner-case where the last strong pointer goes out of scope, then then - last `weakPointer` goes out of scope, but this happens before the - `partialDestructor` bit is set. It would be possible to use a single - bit if it could also be set atomically when the strong count goes to - zero and the weak count is non-zero, but that would add complexity (and - likely slow down common cases as well). - - 4. Partial destroy finished bit. This bit is set when the - `partialDestructor` has finished running. See (3) above for more - information. - - */ + /** + * `refCounts` consists of four fields that are treated atomically: + * + * 1. Strong count. This is a count of the number of shared pointers that + * hold a reference to this object. When the strong counts goes to zero, + * if the weak count is zero, the destructor is run. If the weak count is + * non-zero when the strong count goes to zero then the partialDestructor + * is run. + * + * 2. Weak count. This is a count of the number of weak pointer that hold + * a reference to this object. When the weak count goes to zero and the + * strong count is also zero, then the destructor is run. + * + * 3. Partial destroy started bit. This bit is set if the + * `partialDestructor` function has been started (or is about to be + * started). This is used to prevent the destructor from running + * concurrently with the partial destructor. This can easily happen when + * the last strong pointer release its reference in one thread and starts + * the partialDestructor, while in another thread the last weak pointer + * goes out of scope and starts the destructor while the partialDestructor + * is still running. Both a start and finished bit is needed to handle a + * corner-case where the last strong pointer goes out of scope, then then + * last `weakPointer` goes out of scope, but this happens before the + * `partialDestructor` bit is set. It would be possible to use a single + * bit if it could also be set atomically when the strong count goes to + * zero and the weak count is non-zero, but that would add complexity (and + * likely slow down common cases as well). + * + * 4. Partial destroy finished bit. This bit is set when the + * `partialDestructor` has finished running. See (3) above for more + * information. + */ mutable std::atomic refCounts_{kStrongDelta}; - /** Amount to change the strong count when adding or releasing a reference - - Note: The strong count is stored in the low `StrongCountNumBits` bits - of refCounts - */ + /** + * Amount to change the strong count when adding or releasing a reference + * + * Note: The strong count is stored in the low `StrongCountNumBits` bits + * of refCounts + */ static constexpr FieldType kStrongDelta = 1; - /** Amount to change the weak count when adding or releasing a reference - - Note: The weak count is stored in the high `WeakCountNumBits` bits of - refCounts - */ + /** + * Amount to change the weak count when adding or releasing a reference + * + * Note: The weak count is stored in the high `WeakCountNumBits` bits of + * refCounts + */ static constexpr FieldType kWeakDelta = (kOne << kStrongCountNumBits); - /** Flag that is set when the partialDestroy function has started running - (or is about to start running). - - See description of the `refCounts` field for a fuller description of - this field. - */ + /** + * Flag that is set when the partialDestroy function has started running + * (or is about to start running). + * + * See description of the `refCounts` field for a fuller description of + * this field. + */ static constexpr FieldType kPartialDestroyStartedMask = (kOne << (kFieldTypeBits - 1)); - /** Flag that is set when the partialDestroy function has finished running - - See description of the `refCounts` field for a fuller description of - this field. - */ + /** + * Flag that is set when the partialDestroy function has finished running + * + * See description of the `refCounts` field for a fuller description of + * this field. + */ static constexpr FieldType kPartialDestroyFinishedMask = (kOne << (kFieldTypeBits - 2)); - /** Mask that will zero out all the `count` bits and leave the tag bits - unchanged. - */ + /** + * Mask that will zero out all the `count` bits and leave the tag bits + * unchanged. + */ static constexpr FieldType kTagMask = kPartialDestroyStartedMask | kPartialDestroyFinishedMask; - /** Mask that will zero out the `tag` bits and leave the count bits - unchanged. - */ + /** + * Mask that will zero out the `tag` bits and leave the count bits + * unchanged. + */ static constexpr FieldType kValueMask = ~kTagMask; - /** Mask that will zero out everything except the strong count. + /** + * Mask that will zero out everything except the strong count. */ static constexpr FieldType kStrongMask = ((kOne << kStrongCountNumBits) - 1) & kValueMask; - /** Mask that will zero out everything except the weak count. + /** + * Mask that will zero out everything except the weak count. */ static constexpr FieldType kWeakMask = (((kOne << kWeakCountNumBits) - 1) << kStrongCountNumBits) & kValueMask; - /** Unpack the count and tag fields from the packed atomic integer form. */ + /** + * Unpack the count and tag fields from the packed atomic integer form. + */ struct RefCountPair { CountType strong; CountType weak; - /** The `partialDestroyStartedBit` is set to on when the partial - destroy function is started. It is not a boolean; it is a uint32 - with all bits zero with the possible exception of the - `partialDestroyStartedMask` bit. This is done so it can be directly - masked into the `combinedValue`. + /** + * The `partialDestroyStartedBit` is set to on when the partial + * destroy function is started. It is not a boolean; it is a uint32 + * with all bits zero with the possible exception of the + * `partialDestroyStartedMask` bit. This is done so it can be directly + * masked into the `combinedValue`. */ FieldType partialDestroyStartedBit{0}; - /** The `partialDestroyFinishedBit` is set to on when the partial - destroy function has finished. + /** + * The `partialDestroyFinishedBit` is set to on when the partial + * destroy function has finished. */ FieldType partialDestroyFinishedBit{0}; RefCountPair(FieldType v) noexcept; RefCountPair(CountType s, CountType w) noexcept; - /** Convert back to the packed integer form. */ + /** + * Convert back to the packed integer form. + */ [[nodiscard]] FieldType combinedValue() const noexcept; @@ -215,9 +232,10 @@ private: static_cast((kOne << kStrongCountNumBits) - 1); static constexpr CountType kMaxWeakValue = static_cast((kOne << kWeakCountNumBits) - 1); - /** Put an extra margin to detect when running up against limits. - This is only used in debug code, and is useful if we reduce the - number of bits in the strong and weak counts (to 16 and 14 bits). + /** + * Put an extra margin to detect when running up against limits. + * This is only used in debug code, and is useful if we reduce the + * number of bits in the strong and weak counts (to 16 and 14 bits). */ static constexpr CountType kCheckStrongMaxValue = kMaxStrongValue - 32; static constexpr CountType kCheckWeakMaxValue = kMaxWeakValue - 32; diff --git a/include/xrpl/basics/LocalValue.h b/include/xrpl/basics/LocalValue.h index 1c2a657a18..c5e544a343 100644 --- a/include/xrpl/basics/LocalValue.h +++ b/include/xrpl/basics/LocalValue.h @@ -70,11 +70,15 @@ public: { } - /** Stores instance of T specific to the calling coroutine or thread. */ + /** + * Stores instance of T specific to the calling coroutine or thread. + */ T& operator*(); - /** Stores instance of T specific to the calling coroutine or thread. */ + /** + * Stores instance of T specific to the calling coroutine or thread. + */ T* operator->() { diff --git a/include/xrpl/basics/Log.h b/include/xrpl/basics/Log.h index 4e3437fe71..945dc1b4ec 100644 --- a/include/xrpl/basics/Log.h +++ b/include/xrpl/basics/Log.h @@ -16,7 +16,9 @@ namespace xrpl { -/** Manages partitions for logging. */ +/** + * Manages partitions for logging. + */ class Logs { private: @@ -40,69 +42,81 @@ private: writeAlways(beast::Severity level, std::string const& text) override; }; - /** Manages a system file containing logged output. - The system file remains open during program execution. Interfaces - are provided for interoperating with standard log management - tools like logrotate(8): - http://linuxcommand.org/man_pages/logrotate8.html - @note None of the listed interfaces are thread-safe. - */ + /** + * Manages a system file containing logged output. + * The system file remains open during program execution. Interfaces + * are provided for interoperating with standard log management + * tools like logrotate(8): + * http://linuxcommand.org/man_pages/logrotate8.html + * @note None of the listed interfaces are thread-safe. + */ class File { public: - /** Construct with no associated system file. - A system file may be associated later with @ref open. - @see open - */ + /** + * Construct with no associated system file. + * A system file may be associated later with @ref open. + * @see open + */ File(); - /** Destroy the object. - If a system file is associated, it will be flushed and closed. - */ + /** + * Destroy the object. + * If a system file is associated, it will be flushed and closed. + */ ~File() = default; - /** Determine if a system file is associated with the log. - @return `true` if a system file is associated and opened for - writing. - */ + /** + * Determine if a system file is associated with the log. + * @return `true` if a system file is associated and opened for + * writing. + */ [[nodiscard]] bool isOpen() const noexcept; - /** Associate a system file with the log. - If the file does not exist an attempt is made to create it - and open it for writing. If the file already exists an attempt is - made to open it for appending. - If a system file is already associated with the log, it is closed - first. - @return `true` if the file was opened. - */ + /** + * Associate a system file with the log. + * If the file does not exist an attempt is made to create it + * and open it for writing. If the file already exists an attempt is + * made to open it for appending. + * If a system file is already associated with the log, it is closed + * first. + * @return `true` if the file was opened. + */ bool open(boost::filesystem::path const& path); - /** Close and re-open the system file associated with the log - This assists in interoperating with external log management tools. - @return `true` if the file was opened. - */ + /** + * Close and re-open the system file associated with the log + * This assists in interoperating with external log management tools. + * @return `true` if the file was opened. + */ bool closeAndReopen(); - /** Close the system file if it is open. */ + /** + * Close the system file if it is open. + */ void close(); - /** write to the log file. - Does nothing if there is no associated system file. - */ + /** + * write to the log file. + * Does nothing if there is no associated system file. + */ void write(char const* text); - /** write to the log file and append an end of line marker. - Does nothing if there is no associated system file. - */ + /** + * write to the log file and append an end of line marker. + * Does nothing if there is no associated system file. + */ void writeln(char const* text); - /** Write to the log file using std::string. */ + /** + * Write to the log file using std::string. + */ /** @{ */ void write(std::string const& str) @@ -223,19 +237,21 @@ private: //------------------------------------------------------------------------------ // Debug logging: -/** Set the sink for the debug journal. - - @param sink unique_ptr to new debug Sink. - @return unique_ptr to the previous Sink. nullptr if there was no Sink. -*/ +/** + * Set the sink for the debug journal. + * + * @param sink unique_ptr to new debug Sink. + * @return unique_ptr to the previous Sink. nullptr if there was no Sink. + */ std::unique_ptr setDebugLogSink(std::unique_ptr sink); -/** Returns a debug journal. - The journal may drain to a null sink, so its output - may never be seen. Never use it for critical - information. -*/ +/** + * Returns a debug journal. + * The journal may drain to a null sink, so its output + * may never be seen. Never use it for critical + * information. + */ beast::Journal debugLog(); diff --git a/include/xrpl/basics/MathUtilities.h b/include/xrpl/basics/MathUtilities.h index 4552b335e1..78f5c76988 100644 --- a/include/xrpl/basics/MathUtilities.h +++ b/include/xrpl/basics/MathUtilities.h @@ -6,7 +6,8 @@ namespace xrpl { -/** Calculate one number divided by another number in percentage. +/** + * Calculate one number divided by another number in percentage. * The result is rounded up to the next integer, and capped in the range [0,100] * E.g. calculatePercent(1, 100) = 1 because 1/100 = 0.010000 * calculatePercent(1, 99) = 2 because 1/99 = 0.010101 @@ -19,7 +20,7 @@ namespace xrpl { * @return the percentage, in [0, 100] * * @note total cannot be zero. - * */ + */ constexpr std::size_t calculatePercent(std::size_t count, std::size_t total) { diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 073da12f89..f90800c715 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -14,7 +14,6 @@ #include #include #include -#include #include namespace xrpl { @@ -48,46 +47,54 @@ isPowerOfTen(T value) namespace detail { -/** Builds a table of the powers of 10 +/** + * Builds a table of the powers of 10 * * This function is marked consteval, so it can only be run in * a constexpr context. This assures that it is and can only be run at * compile time. Doing it at runtime would be pretty wasteful and * inefficient. */ -constexpr std::size_t kInt64Digits = 20; -consteval std::array +constexpr std::size_t kUint64Digits = 20; +[[maybe_unused]] constexpr std::size_t kUint128Digits = 39; + +template +consteval std::array buildPowersOfTen() { - std::array result{}; + std::array result{}; - std::uint64_t power = 1; + T power = 1; std::size_t exponent = 0; // end the loop early so it doesn't overflow; for (; exponent < result.size() - 1; ++exponent, power *= 10) { result[exponent] = power; - if (power > std::numeric_limits::max() / 10) + if (power > std::numeric_limits::max() / 10) throw std::logic_error("Power of 10 table is too big"); } result[exponent] = power; - if (power < std::numeric_limits::max() / 10) - throw std::logic_error("Power of 10 table is not big enough for the uint64_t type"); + if (power < std::numeric_limits::max() / 10) + throw std::logic_error("Power of 10 table is not big enough for the given type"); return result; } } // namespace detail -constexpr std::array kPowerOfTen = detail::buildPowersOfTen(); +template +constexpr std::array kPowerOfTenImpl = detail::buildPowersOfTen(); + +constexpr auto kPowerOfTen = kPowerOfTenImpl; static_assert(kPowerOfTen[0] == 1); static_assert(kPowerOfTen[1] == 10); static_assert(kPowerOfTen[10] == 10'000'000'000); static_assert( - isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1); + isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kUint64Digits - 1); -/** MantissaRange defines a range for the mantissa of a normalized Number. +/** + * 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 @@ -124,17 +131,37 @@ struct MantissaRange final { using rep = std::uint64_t; + // NOLINTBEGIN(readability-enum-initial-value) + // The values don't matter, except for Large enum class MantissaScale { + // Small can be removed when either featureSingleAssetVault or featureLendingProtocol are + // retired Small, // LargeLegacy can be removed when fixCleanup3_2_0 is retired LargeLegacy, - Large, + // Large320 can be removed when fixCleanup3_3_0 is retired + Large320, + // If Large330 is ever the only remaining "Large*" entry, it can be renamed to just "Large". + Large330, + // Large is a de-facto alias for "the latest", and is only here for backward compatibility + // in the extremely unlikely case that a downstream project made use of it. Note that + // because the behavior changed, this may still be a breaking change. + Large = Large330, }; + // NOLINTEND(readability-enum-initial-value) - // This entire enum can be removed when fixCleanup3_2_0 is retired - enum class CuspRoundingFix : bool { - Disabled = false, - Enabled = true, + // This entire enum can be removed when the last relevant amendment is retired + enum class CuspRoundingFix : std::uint8_t { + // Disabled can be removed when fixCleanup3_2_0 is retired + Disabled = 0, + // Enabled320 can be removed when fixCleanup3_3_0 is retired + Enabled320 = 1, + // If we ever get to the point that there's only one entry, remove the entire enum + Enabled330 = 2, + // Enabled is a de-facto alias for "the latest", and is only here for backward compatibility + // in the extremely unlikely case that a downstream project made use of it. Note that + // because the behavior changed, this may still be a breaking change. + Enabled = Enabled330, }; explicit constexpr MantissaRange(MantissaScale sc) : scale(sc) @@ -145,13 +172,27 @@ struct MantissaRange final int const log{getExponent(scale)}; rep const min{getMin(scale, log)}; rep const max{(min * 10) - 1}; - CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)}; - - static MantissaRange const& - getMantissaRange(MantissaScale scale); + CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)}; static std::set const& - getAllScales(); + getAllScales() + { + static std::set const kScales = { + MantissaRange::MantissaScale::Small, + MantissaRange::MantissaScale::LargeLegacy, + MantissaRange::MantissaScale::Large320, + MantissaRange::MantissaScale::Large330, + }; + return kScales; + } + + class Access + { + static constexpr MantissaRange const& + mantissaRange(MantissaScale scale); + + friend Number; + }; private: static constexpr int @@ -162,7 +203,8 @@ private: case MantissaScale::Small: return 15; case MantissaScale::LargeLegacy: - case MantissaScale::Large: + case MantissaScale::Large320: + case MantissaScale::Large330: return 18; // LCOV_EXCL_START default: @@ -191,24 +233,24 @@ private: case MantissaScale::Small: case MantissaScale::LargeLegacy: return CuspRoundingFix::Disabled; - case MantissaScale::Large: - return CuspRoundingFix::Enabled; + case MantissaScale::Large320: + return CuspRoundingFix::Enabled320; + case MantissaScale::Large330: + return CuspRoundingFix::Enabled330; default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE } } - - static std::unordered_map const& - getRanges(); }; // Like std::integral, but only 64-bit integral types. template concept Integral64 = std::is_same_v || std::is_same_v; -/** Number is a floating point type that can represent a wide range of values. +/** + * Number is a floating point type that can represent a wide range of values. * * It can represent all values that can be represented by an STAmount - * regardless of asset type - XRPAmount, MPTAmount, and IOUAmount, with at least @@ -304,7 +346,6 @@ concept Integral64 = std::is_same_v || std::is_same_v::max(); static_assert(kMaxRep == 9'223'372'036'854'775'807); static_assert(-kMaxRep == std::numeric_limits::min() + 1); + static constexpr internalrep kMaxRepUp = ((kMaxRep / 10) + 1) * 10; + static_assert(kMaxRepUp == 9'223'372'036'854'775'810ULL); // May need to make unchecked private struct Unchecked @@ -390,10 +433,11 @@ public: static Number lowest() noexcept; - /** Conversions to Number are implicit and conversions away from Number - * are explicit. This design encourages and facilitates the use of Number - * as the preferred type for floating point arithmetic as it makes - * "mixed mode" more convenient, e.g. MPTAmount + Number. + /** + * Conversions to Number are implicit and conversions away from Number + * are explicit. This design encourages and facilitates the use of Number + * as the preferred type for floating point arithmetic as it makes + * "mixed mode" more convenient, e.g. MPTAmount + Number. */ explicit operator rep() const; // round to nearest, even on tie @@ -448,7 +492,9 @@ public: return l.mantissa_ < r.mantissa_; } - /** Return the sign of the amount */ + /** + * Return the sign of the amount + */ [[nodiscard]] constexpr int signum() const noexcept { @@ -502,14 +548,16 @@ public: static RoundingMode setround(RoundingMode inMode); - /** Returns which mantissa scale is currently in use for normalization. + /** + * Returns which mantissa scale is currently in use for normalization. * * If you think you need to call this outside of unit tests, no you don't. */ static MantissaRange::MantissaScale getMantissaScale(); - /** Changes which mantissa scale is used for normalization. + /** + * Changes which mantissa scale is used for normalization. * * If you think you need to call this outside of unit tests, no you don't. */ @@ -545,6 +593,13 @@ public: std::pair normalizeToRange() const; + // Safely convert rep (int64) mantissa to internalrep (uint64). If the rep + // is negative, returns the positive value. This takes a little extra work + // because converting std::numeric_limits::min() flirts with + // UB, and can vary across compilers. + static internalrep + externalToInternal(rep mantissa); + private: static thread_local RoundingMode mode; // The available ranges for mantissa @@ -554,10 +609,17 @@ private: // changing the values inside the range. static thread_local std::reference_wrapper kRange; + class Guard; + void normalize(MantissaRange const& range); - /** Normalize Number components to an arbitrary range. + // Guard has the fields that we need, as well as MantissaRange, so if we have a guard, use that + void + normalize(Guard const& guard); + + /** + * Normalize Number components to an arbitrary range. * * min/maxMantissa are parameters because this function is used by both * normalize(), which reads from kRange, and by normalizeToRange, @@ -571,7 +633,7 @@ private: int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); + MantissaRange::CuspRoundingFix cuspRoundingFix); template friend void @@ -581,7 +643,7 @@ private: int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped); [[nodiscard]] bool @@ -592,15 +654,6 @@ private: // exponent could go out of range, so it will be checked. [[nodiscard]] Number shiftExponent(int exponentDelta) const; - - // Safely convert rep (int64) mantissa to internalrep (uint64). If the rep - // is negative, returns the positive value. This takes a little extra work - // because converting std::numeric_limits::min() flirts with - // UB, and can vary across compilers. - static internalrep - externalToInternal(rep mantissa); - - class Guard; }; constexpr Number::Number(bool negative, internalrep mantissa, int exponent, Unchecked) noexcept @@ -635,7 +688,8 @@ inline Number::Number(rep mantissa) : Number{mantissa, 0} { } -/** Returns the mantissa of the external view of the Number. +/** + * Returns the mantissa of the external view of the Number. * * Please see the "---- External Interface ----" section of the class * documentation for an explanation of why the internal value may be modified. @@ -656,7 +710,8 @@ Number::mantissa() const noexcept return sign * static_cast(m); } -/** Returns the exponent of the external view of the Number. +/** + * Returns the exponent of the external view of the Number. * * Please see the "---- External Interface ----" section of the class * documentation for an explanation of why the internal value may be modified. @@ -862,21 +917,11 @@ squelch(Number const& x, Number const& limit) noexcept return x; } -inline std::string -to_string(MantissaRange::MantissaScale const& scale) -{ - switch (scale) - { - case MantissaRange::MantissaScale::Small: - return "small"; - case MantissaRange::MantissaScale::LargeLegacy: - return "largeLegacy"; - case MantissaRange::MantissaScale::Large: - return "large"; - default: - throw std::runtime_error("Bad scale"); - } -} +std::string +to_string(MantissaRange::MantissaScale const& scale); + +std::string +to_string(Number::RoundingMode const& round); class SaveNumberRoundMode { @@ -915,10 +960,10 @@ public: operator=(NumberRoundModeGuard const&) = delete; }; -/** Sets the new scale and restores the old scale when it leaves scope. +/** + * Sets the new scale and restores the old scale when it leaves scope. * * If you think you need to use this class outside of unit tests, no you don't. - * */ class NumberMantissaScaleGuard { diff --git a/include/xrpl/basics/RangeSet.h b/include/xrpl/basics/RangeSet.h index 2ed543b376..3de882979e 100644 --- a/include/xrpl/basics/RangeSet.h +++ b/include/xrpl/basics/RangeSet.h @@ -13,23 +13,25 @@ namespace xrpl { -/** A closed interval over the domain T. - - For an instance ClosedInterval c, this represents the closed interval - (c.first(), c.last()). A single element interval has c.first() == c.last(). - - This is simply a type-alias for boost interval container library interval - set, so users should consult that documentation for available supporting - member and free functions. -*/ +/** + * A closed interval over the domain T. + * + * For an instance ClosedInterval c, this represents the closed interval + * (c.first(), c.last()). A single element interval has c.first() == c.last(). + * + * This is simply a type-alias for boost interval container library interval + * set, so users should consult that documentation for available supporting + * member and free functions. + */ template using ClosedInterval = boost::icl::closed_interval; -/** Create a closed range interval - - Helper function to create a closed range interval without having to qualify - the template argument. -*/ +/** + * Create a closed range interval + * + * Helper function to create a closed range interval without having to qualify + * the template argument. + */ template ClosedInterval range(T low, T high) @@ -37,28 +39,30 @@ range(T low, T high) return ClosedInterval(low, high); } -/** A set of closed intervals over the domain T. - - Represents a set of values of the domain T using the minimum number - of disjoint ClosedInterval. This is useful to represent ranges of - T where a few instances are missing, e.g. the set 1-5,8-9,11-14. - - This is simply a type-alias for boost interval container library interval - set, so users should consult that documentation for available supporting - member and free functions. -*/ +/** + * A set of closed intervals over the domain T. + * + * Represents a set of values of the domain T using the minimum number + * of disjoint ClosedInterval. This is useful to represent ranges of + * T where a few instances are missing, e.g. the set 1-5,8-9,11-14. + * + * This is simply a type-alias for boost interval container library interval + * set, so users should consult that documentation for available supporting + * member and free functions. + */ template using RangeSet = boost::icl::interval_set>; -/** Convert a ClosedInterval to a styled string - - The styled string is - "c.first()-c.last()" if c.first() != c.last() - "c.first()" if c.first() == c.last() - - @param ci The closed interval to convert - @return The style string -*/ +/** + * Convert a ClosedInterval to a styled string + * + * The styled string is + * "c.first()-c.last()" if c.first() != c.last() + * "c.first()" if c.first() == c.last() + * + * @param ci The closed interval to convert + * @return The style string + */ template std::string to_string(ClosedInterval const& ci) @@ -68,14 +72,15 @@ to_string(ClosedInterval const& ci) return std::to_string(ci.first()) + "-" + std::to_string(ci.last()); } -/** Convert the given RangeSet to a styled string. - - The styled string representation is the set of disjoint intervals joined - by commas. The string "empty" is returned if the set is empty. - - @param rs The rangeset to convert - @return The styled string -*/ +/** + * Convert the given RangeSet to a styled string. + * + * The styled string representation is the set of disjoint intervals joined + * by commas. The string "empty" is returned if the set is empty. + * + * @param rs The rangeset to convert + * @return The styled string + */ template std::string to_string(RangeSet const& rs) @@ -91,15 +96,16 @@ to_string(RangeSet const& rs) return s; } -/** Convert the given styled string to a RangeSet. - - The styled string representation is the set - of disjoint intervals joined by commas. - - @param rs The set to be populated - @param s The styled string to convert - @return True on successfully converting styled string -*/ +/** + * Convert the given styled string to a RangeSet. + * + * The styled string representation is the set + * of disjoint intervals joined by commas. + * + * @param rs The set to be populated + * @param s The styled string to convert + * @return True on successfully converting styled string + */ template [[nodiscard]] bool fromString(RangeSet& rs, std::string const& s) @@ -161,14 +167,15 @@ fromString(RangeSet& rs, std::string const& s) return result; } -/** Find the largest value not in the set that is less than a given value. - - @param rs The set of interest - @param t The value that must be larger than the result - @param minVal (Default is 0) The smallest allowed value - @return The largest v such that minV <= v < t and !contains(rs, v) or - std::nullopt if no such v exists. -*/ +/** + * Find the largest value not in the set that is less than a given value. + * + * @param rs The set of interest + * @param t The value that must be larger than the result + * @param minVal (Default is 0) The smallest allowed value + * @return The largest v such that minV <= v < t and !contains(rs, v) or + * std::nullopt if no such v exists. + */ template std::optional prevMissing(RangeSet const& rs, T t, T minVal = 0) diff --git a/include/xrpl/basics/Resolver.h b/include/xrpl/basics/Resolver.h index d48958b76d..239eb9630e 100644 --- a/include/xrpl/basics/Resolver.h +++ b/include/xrpl/basics/Resolver.h @@ -15,22 +15,29 @@ public: virtual ~Resolver() = 0; - /** Issue an asynchronous stop request. */ + /** + * Issue an asynchronous stop request. + */ virtual void stopAsync() = 0; - /** Issue a synchronous stop request. */ + /** + * Issue a synchronous stop request. + */ virtual void stop() = 0; - /** Issue a synchronous start request. */ + /** + * Issue a synchronous start request. + */ virtual void start() = 0; - /** resolve all hostnames on the list - @param names the names to be resolved - @param handler the handler to call - */ + /** + * resolve all hostnames on the list + * @param names the names to be resolved + * @param handler the handler to call + */ /** @{ */ template void diff --git a/include/xrpl/basics/SharedWeakCachePointer.h b/include/xrpl/basics/SharedWeakCachePointer.h index a143647a1e..1b78af2fae 100644 --- a/include/xrpl/basics/SharedWeakCachePointer.h +++ b/include/xrpl/basics/SharedWeakCachePointer.h @@ -7,13 +7,14 @@ namespace xrpl { -/** A combination of a std::shared_ptr and a std::weak_pointer. - - -This class is a wrapper to a `std::variant` -This class is useful for storing intrusive pointers in tagged caches using less -memory than storing both pointers directly. -*/ +/** + * A combination of a std::shared_ptr and a std::weak_pointer. + * + * + * This class is a wrapper to a `std::variant` + * This class is useful for storing intrusive pointers in tagged caches using less + * memory than storing both pointers directly. + */ template class SharedWeakCachePointer @@ -48,65 +49,79 @@ public: ~SharedWeakCachePointer(); - /** Return a strong pointer if this is already a strong pointer (i.e. don't - lock the weak pointer. Use the `lock` method if that's what's needed) + /** + * Return a strong pointer if this is already a strong pointer (i.e. don't + * lock the weak pointer. Use the `lock` method if that's what's needed) */ [[nodiscard]] std::shared_ptr const& getStrong() const; - /** Return true if this is a strong pointer and the strong pointer is - seated. + /** + * Return true if this is a strong pointer and the strong pointer is + * seated. */ explicit operator bool() const noexcept; - /** Set the pointer to null, decrement the appropriate ref count, and run - the appropriate release action. + /** + * Set the pointer to null, decrement the appropriate ref count, and run + * the appropriate release action. */ void reset(); - /** If this is a strong pointer, return the raw pointer. Otherwise return - null. + /** + * If this is a strong pointer, return the raw pointer. Otherwise return + * null. */ [[nodiscard]] T* get() const; - /** If this is a strong pointer, return the strong count. Otherwise return 0 + /** + * If this is a strong pointer, return the strong count. Otherwise return 0 */ [[nodiscard]] std::size_t useCount() const; - /** Return true if there is a non-zero strong count. */ + /** + * Return true if there is a non-zero strong count. + */ [[nodiscard]] bool expired() const; - /** If this is a strong pointer, return the strong pointer. Otherwise - attempt to lock the weak pointer. + /** + * If this is a strong pointer, return the strong pointer. Otherwise + * attempt to lock the weak pointer. */ [[nodiscard]] std::shared_ptr lock() const; - /** Return true is this represents a strong pointer. */ + /** + * Return true is this represents a strong pointer. + */ [[nodiscard]] bool isStrong() const; - /** Return true is this represents a weak pointer. */ + /** + * Return true is this represents a weak pointer. + */ [[nodiscard]] bool isWeak() const; - /** If this is a weak pointer, attempt to convert it to a strong pointer. - - @return true if successfully converted to a strong pointer (or was - already a strong pointer). Otherwise false. - */ + /** + * If this is a weak pointer, attempt to convert it to a strong pointer. + * + * @return true if successfully converted to a strong pointer (or was + * already a strong pointer). Otherwise false. + */ bool convertToStrong(); - /** If this is a strong pointer, attempt to convert it to a weak pointer. - - @return false if the pointer is null. Otherwise return true. - */ + /** + * If this is a strong pointer, attempt to convert it to a weak pointer. + * + * @return false if the pointer is null. Otherwise return true. + */ bool convertToWeak(); diff --git a/include/xrpl/basics/SlabAllocator.h b/include/xrpl/basics/SlabAllocator.h index 8e741991f6..7b6e88e8bc 100644 --- a/include/xrpl/basics/SlabAllocator.h +++ b/include/xrpl/basics/SlabAllocator.h @@ -33,7 +33,9 @@ class SlabAllocator static_assert(alignof(Type) == 8 || alignof(Type) == 4); - /** A block of memory that is owned by a slab allocator */ + /** + * A block of memory that is owned by a slab allocator + */ struct SlabBlock { // A mutex to protect the freelist for this block: @@ -80,7 +82,9 @@ class SlabAllocator SlabBlock& operator=(SlabBlock&& other) = delete; - /** Determines whether the given pointer belongs to this allocator */ + /** + * Determines whether the given pointer belongs to this allocator + */ bool own(std::uint8_t const* pIn) const noexcept { @@ -107,14 +111,15 @@ class SlabAllocator return ret; } - /** Return an item to this allocator's freelist. - - @param ptr The pointer to the chunk of memory being deallocated. - - @note This is a dangerous, private interface; the item being - returned should belong to this allocator. Debug builds - will check and assert if this is not the case. Release - builds will not. + /** + * Return an item to this allocator's freelist. + * + * @param ptr The pointer to the chunk of memory being deallocated. + * + * @note This is a dangerous, private interface; the item being + * returned should belong to this allocator. Debug builds + * will check and assert if this is not the case. Release + * builds will not. */ void deallocate(std::uint8_t* ptr) noexcept @@ -145,13 +150,14 @@ private: std::size_t const slabSize_; public: - /** Constructs a slab allocator able to allocate objects of a fixed size - - @param count the number of items the slab allocator can allocate; note - that a count of 0 is valid and means that the allocator - is, effectively, disabled. This can be very useful in some - contexts (e.g. when minimal memory usage is needed) and - allows for graceful failure. + /** + * Constructs a slab allocator able to allocate objects of a fixed size + * + * @param count the number of items the slab allocator can allocate; note + * that a count of 0 is valid and means that the allocator + * is, effectively, disabled. This can be very useful in some + * contexts (e.g. when minimal memory usage is needed) and + * allows for graceful failure. */ constexpr explicit SlabAllocator( std::size_t extra, @@ -179,17 +185,20 @@ public: // shutdown process up could make this possible. ~SlabAllocator() = default; - /** Returns the size of the memory block this allocator returns. */ + /** + * Returns the size of the memory block this allocator returns. + */ [[nodiscard]] constexpr std::size_t size() const noexcept { return itemSize_; } - /** Returns a suitably aligned pointer, if one is available. - - @return a pointer to a block of memory from the allocator, or - nullptr if the allocator can't satisfy this request. + /** + * Returns a suitably aligned pointer, if one is available. + * + * @return a pointer to a block of memory from the allocator, or + * nullptr if the allocator can't satisfy this request. */ std::uint8_t* allocate() noexcept @@ -250,12 +259,13 @@ public: return slab->allocate(); } - /** Returns the memory block to the allocator. - - @param ptr A pointer to a memory block. - @param size If non-zero, a hint as to the size of the block. - @return true if this memory block belonged to the allocator and has - been released; false otherwise. + /** + * Returns the memory block to the allocator. + * + * @param ptr A pointer to a memory block. + * @param size If non-zero, a hint as to the size of the block. + * @return true if this memory block belonged to the allocator and has + * been released; false otherwise. */ bool deallocate(std::uint8_t* ptr) noexcept @@ -278,7 +288,9 @@ public: } }; -/** A collection of slab allocators of various sizes for a given type. */ +/** + * A collection of slab allocators of various sizes for a given type. + */ template class SlabAllocatorSet { @@ -345,13 +357,14 @@ public: ~SlabAllocatorSet() = default; - /** Returns a suitably aligned pointer, if one is available. - - @param extra The number of extra bytes, above and beyond the size of - the object, that should be returned by the allocator. - - @return a pointer to a block of memory, or nullptr if the allocator - can't satisfy this request. + /** + * Returns a suitably aligned pointer, if one is available. + * + * @param extra The number of extra bytes, above and beyond the size of + * the object, that should be returned by the allocator. + * + * @return a pointer to a block of memory, or nullptr if the allocator + * can't satisfy this request. */ std::uint8_t* allocate(std::size_t extra) noexcept @@ -368,12 +381,13 @@ public: return nullptr; } - /** Returns the memory block to the allocator. - - @param ptr A pointer to a memory block. - - @return true if this memory block belonged to one of the allocators - in this set and has been released; false otherwise. + /** + * Returns the memory block to the allocator. + * + * @param ptr A pointer to a memory block. + * + * @return true if this memory block belonged to one of the allocators + * in this set and has been released; false otherwise. */ bool deallocate(std::uint8_t* ptr) noexcept diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index f87ca063b8..36e7615c3a 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -16,12 +16,13 @@ namespace xrpl { -/** An immutable linear range of bytes. - - A fully constructed Slice is guaranteed to be in a valid state. - A Slice is lightweight and copyable, it retains no ownership - of the underlying memory. -*/ +/** + * An immutable linear range of bytes. + * + * A fully constructed Slice is guaranteed to be in a valid state. + * A Slice is lightweight and copyable, it retains no ownership + * of the underlying memory. + */ class Slice { private: @@ -32,30 +33,37 @@ public: using value_type = std::uint8_t; using const_iterator = value_type const*; - /** Default constructed Slice has length 0. */ + /** + * Default constructed Slice has length 0. + */ Slice() noexcept = default; Slice(Slice const&) noexcept = default; Slice& operator=(Slice const&) noexcept = default; - /** Create a slice pointing to existing memory. */ + /** + * Create a slice pointing to existing memory. + */ Slice(void const* data, std::size_t size) noexcept : data_(reinterpret_cast(data)), size_(size) { } - /** Return `true` if the byte range is empty. */ + /** + * Return `true` if the byte range is empty. + */ [[nodiscard]] bool empty() const noexcept { return size_ == 0; } - /** Returns the number of bytes in the storage. - - This may be zero for an empty range. - */ + /** + * Returns the number of bytes in the storage. + * + * This may be zero for an empty range. + */ /** @{ */ [[nodiscard]] std::size_t size() const noexcept @@ -70,17 +78,20 @@ public: } /** @} */ - /** Return a pointer to beginning of the storage. - @note The return type is guaranteed to be a pointer - to a single byte, to facilitate pointer arithmetic. - */ + /** + * Return a pointer to beginning of the storage. + * @note The return type is guaranteed to be a pointer + * to a single byte, to facilitate pointer arithmetic. + */ [[nodiscard]] std::uint8_t const* data() const noexcept { return data_; } - /** Access raw bytes. */ + /** + * Access raw bytes. + */ std::uint8_t operator[](std::size_t i) const noexcept { @@ -88,7 +99,9 @@ public: return data_[i]; } - /** Advance the buffer. */ + /** + * Advance the buffer. + */ /** @{ */ Slice& operator+=(std::size_t n) @@ -108,7 +121,9 @@ public: } /** @} */ - /** Shrinks the slice by moving its start forward by n characters. */ + /** + * Shrinks the slice by moving its start forward by n characters. + */ void removePrefix(std::size_t n) { @@ -116,7 +131,9 @@ public: size_ -= n; } - /** Shrinks the slice by moving its end backward by n characters. */ + /** + * Shrinks the slice by moving its end backward by n characters. + */ void removeSuffix(std::size_t n) { @@ -147,16 +164,17 @@ public: return data_ + size_; } - /** Return a "sub slice" of given length starting at the given position - - Note that the subslice encompasses the range [pos, pos + rCount), - where rCount is the smaller of count and size() - pos. - - @param pos position of the first character - @count requested length - - @returns The requested subslice, if the request is valid. - @throws std::out_of_range if pos > size() + /** + * Return a "sub slice" of given length starting at the given position + * + * Note that the subslice encompasses the range [pos, pos + rCount), + * where rCount is the smaller of count and size() - pos. + * + * @param pos position of the first character + * @count requested length + * + * @return The requested subslice, if the request is valid. + * @throws std::out_of_range if pos > size() */ [[nodiscard]] Slice substr(std::size_t pos, std::size_t count = std::numeric_limits::max()) const diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index 97df43d68f..2b360d2fda 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -17,15 +17,16 @@ namespace xrpl { -/** Format arbitrary binary data as an SQLite "blob literal". - - In SQLite, blob literals must be encoded when used in a query. Per - https://sqlite.org/lang_expr.html#literal_values_constants_ they are - encoded as string literals containing hexadecimal data and preceded - by a single 'X' character. - - @param blob An arbitrary blob of binary data - @return The input, encoded as a blob literal. +/** + * Format arbitrary binary data as an SQLite "blob literal". + * + * In SQLite, blob literals must be encoded when used in a query. Per + * https://sqlite.org/lang_expr.html#literal_values_constants_ they are + * encoded as string literals containing hexadecimal data and preceded + * by a single 'X' character. + * + * @param blob An arbitrary blob of binary data + * @return The input, encoded as a blob literal. */ std::string sqlBlobLiteral(Blob const& blob); @@ -130,11 +131,12 @@ trimWhitespace(std::string str); std::optional toUInt64(std::string const& s); -/** Determines if the given string looks like a TOML-file hosting domain. - - Do not use this function to determine if a particular string is a valid - domain, as this function may reject domains that are otherwise valid and - doesn't check whether the TLD is valid. +/** + * Determines if the given string looks like a TOML-file hosting domain. + * + * Do not use this function to determine if a particular string is a valid + * domain, as this function may reject domains that are otherwise valid and + * doesn't check whether the TLD is valid. */ bool isProperlyFormedTomlDomain(std::string_view domain); diff --git a/include/xrpl/basics/TaggedCache.h b/include/xrpl/basics/TaggedCache.h index 16c87cf833..7bb2cb552b 100644 --- a/include/xrpl/basics/TaggedCache.h +++ b/include/xrpl/basics/TaggedCache.h @@ -41,18 +41,19 @@ struct ReplaceDynamically; } // namespace detail -/** Map/cache combination. - This class implements a cache and a map. The cache keeps objects alive - in the map. The map allows multiple code paths that reference objects - with the same tag to get the same actual object. - - So long as data is in the cache, it will stay in memory. - If it stays in memory even after it is ejected from the cache, - the map will track it. - - @note Callers must not modify data objects that are stored in the cache - unless they hold their own lock over all cache operations. -*/ +/** + * Map/cache combination. + * This class implements a cache and a map. The cache keeps objects alive + * in the map. The map allows multiple code paths that reference objects + * with the same tag to get the same actual object. + * + * So long as data is in the cache, it will stay in memory. + * If it stays in memory even after it is ejected from the cache, + * the map will track it. + * + * @note Callers must not modify data objects that are stored in the cache + * unless they hold their own lock over all cache operations. + */ template < class Key, class T, @@ -82,11 +83,15 @@ public: beast::insight::Collector::ptr const& collector = beast::insight::NullCollector::make()); public: - /** Return the clock associated with the cache. */ + /** + * Return the clock associated with the cache. + */ clock_type& clock(); - /** Returns the number of items in the container. */ + /** + * Returns the number of items in the container. + */ std::size_t size() const; @@ -105,9 +110,10 @@ public: void reset(); - /** Refresh the last access time on a key if present. - @return `true` If the key was found. - */ + /** + * Refresh the last access time on a key if present. + * @return `true` If the key was found. + */ template bool touchIfExists(KeyComparable const& key); @@ -130,14 +136,15 @@ private: SharedPointerType const&, SharedPointerType&>; - /** Shared implementation of the canonicalize family. - - `policy` selects how a collision is resolved when `key` already exists: - detail::ReplaceCached, detail::ReplaceClient or - detail::ReplaceDynamically. For ReplaceDynamically `replaceCallback` is - invoked with the existing strong pointer and returns whether to replace - the cached value with `data`; for the tag policies it is unused. - */ + /** + * Shared implementation of the canonicalize family. + * + * `policy` selects how a collision is resolved when `key` already exists: + * detail::ReplaceCached, detail::ReplaceClient or + * detail::ReplaceDynamically. For ReplaceDynamically `replaceCallback` is + * invoked with the existing strong pointer and returns whether to replace + * the cached value with `data`; for the tag policies it is unused. + */ template bool canonicalizeImpl( @@ -147,69 +154,73 @@ private: Callback&& replaceCallback = nullptr); public: - /** Replace aliased objects with originals. - - Due to concurrency it is possible for two separate objects with - the same content and referring to the same unique "thing" to exist. - This routine eliminates the duplicate and performs a replacement - on the callers shared pointer if needed. - - `replaceCallback` is a callable taking the existing strong pointer and - returning whether to replace the cached value with `data` (true) or to - keep the cached value and write it back into `data` (false). Because the - write-back case mutates `data`, `data` must be writable. - - @param key The key corresponding to the object - @param data A shared pointer to the data corresponding to the object. - @param replaceCallback A callable (existing strong pointer -> bool). - - @return `true` if an existing live entry was found and used; `false` if a new entry was - inserted or an expired tracked entry was re-cached. - **/ + /** + * Replace aliased objects with originals. + * + * Due to concurrency it is possible for two separate objects with + * the same content and referring to the same unique "thing" to exist. + * This routine eliminates the duplicate and performs a replacement + * on the callers shared pointer if needed. + * + * `replaceCallback` is a callable taking the existing strong pointer and + * returning whether to replace the cached value with `data` (true) or to + * keep the cached value and write it back into `data` (false). Because the + * write-back case mutates `data`, `data` must be writable. + * + * @param key The key corresponding to the object + * @param data A shared pointer to the data corresponding to the object. + * @param replaceCallback A callable (existing strong pointer -> bool). + * + * @return `true` if an existing live entry was found and used; `false` if a new entry was + * inserted or an expired tracked entry was re-cached. + */ template bool canonicalize(key_type const& key, SharedPointerType& data, Callback&& replaceCallback); - /** Insert/update the canonical entry for `key`, always replacing the - cached value with `data`. - - If an entry already exists for `key`, the cached value is unconditionally - replaced with `data`; otherwise `data` is inserted. `data` is never - written back, so it may be const. - - @param key The key corresponding to the object. - @param data A shared pointer to the data corresponding to the object. - - @return `true` if an existing live entry was found and used; `false` if a new entry was - inserted or an expired tracked entry was re-cached. - **/ + /** + * Insert/update the canonical entry for `key`, always replacing the + * cached value with `data`. + * + * If an entry already exists for `key`, the cached value is unconditionally + * replaced with `data`; otherwise `data` is inserted. `data` is never + * written back, so it may be const. + * + * @param key The key corresponding to the object. + * @param data A shared pointer to the data corresponding to the object. + * + * @return `true` if an existing live entry was found and used; `false` if a new entry was + * inserted or an expired tracked entry was re-cached. + */ bool canonicalizeReplaceCache(key_type const& key, SharedPointerType const& data); - /** Insert the canonical entry for `key`, keeping any existing cached value. - - If an entry already exists for `key`, the cached value is kept and - written back into `data` so the caller ends up with the canonical - object; otherwise `data` is inserted. Because `data` may be overwritten - it must be writable. - - @param key The key corresponding to the object. - @param data A shared pointer to the data corresponding to the object; - updated to the canonical value when one already exists. - - @return `true` if an existing live entry was found and used; `false` if a new entry was - inserted or an expired tracked entry was re-cached. - **/ + /** + * Insert the canonical entry for `key`, keeping any existing cached value. + * + * If an entry already exists for `key`, the cached value is kept and + * written back into `data` so the caller ends up with the canonical + * object; otherwise `data` is inserted. Because `data` may be overwritten + * it must be writable. + * + * @param key The key corresponding to the object. + * @param data A shared pointer to the data corresponding to the object; + * updated to the canonical value when one already exists. + * + * @return `true` if an existing live entry was found and used; `false` if a new entry was + * inserted or an expired tracked entry was re-cached. + */ bool canonicalizeReplaceClient(key_type const& key, SharedPointerType& data); SharedPointerType fetch(key_type const& key); - /** Insert the element into the container. - If the key already exists, nothing happens. - @return `true` If the element was inserted - */ + /** + * Insert the element into the container. + * If the key already exists, nothing happens. + * @return `true` If the element was inserted + */ template auto insert(key_type const& key, T const& value) -> ReturnType @@ -235,15 +246,18 @@ public: getKeys() const; // CachedSLEs functions. - /** Returns the fraction of cache hits. */ + /** + * Returns the fraction of cache hits. + */ double rate() const; - /** Fetch an item from the cache. - If the digest was not found, Handler - will be called with this signature: - SLE::const_pointer(void) - */ + /** + * Fetch an item from the cache. + * If the digest was not found, Handler + * will be called with this signature: + * SLE::const_pointer(void) + */ template SharedPointerType fetch(key_type const& digest, Handler const& h); diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp index b79561c71a..447743a7b7 100644 --- a/include/xrpl/basics/TaggedCache.ipp +++ b/include/xrpl/basics/TaggedCache.ipp @@ -3,6 +3,9 @@ #include #include // IWYU pragma: keep #include +#include + +#include namespace xrpl { @@ -601,8 +604,42 @@ TaggedCache v; { - std::scoped_lock const lock(mutex_); - v.reserve(cache_.size()); + // Keep track of how many iterations are needed. Exit the loop if the number of retries gets + // absurd. (Note that if this somehow ever happens, one more allocation will be done under + // lock, which is undesirable, but really should be almost impossible.) + std::size_t allocationIterations = 0; + std::unique_lock lock(mutex_); + for (auto size = cache_.size(); v.capacity() < size && allocationIterations < 20; + size = cache_.size()) + { + ScopeUnlock const unlock(lock); + if (allocationIterations > 0) + { + JLOG(journal_.info()) + << "getKeys(): Cache grew beyond allocated capacity after " + << allocationIterations << " prior attempt(s). Have " << v.capacity() + << ", need " << size << ". Retrying allocation"; + } + // Allocate the current size plus a little extra, in case the cache grows while + // allocating. Each time another allocation is needed, the extra also gets bigger until + // it ultimately doubles the size + 1. + constexpr std::size_t baseShift = 5; + auto const bufferOffset = std::min(allocationIterations, std::size_t{baseShift}); + auto const bufferShift = baseShift - bufferOffset; + size += (size >> bufferShift) + 1; + v.reserve(size); + ++allocationIterations; + } + if (v.capacity() < cache_.size()) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::TaggedCache::getKeys(): failed to allocate sufficient capacity"); + v.reserve(cache_.size()); + // LCOV_EXCL_STOP + } + XRPL_ASSERT(lock.owns_lock(), "xrpl::TaggedCache::getKeys(): owns lock"); + XRPL_ASSERT( + v.capacity() >= cache_.size(), "xrpl::TaggedCache::getKeys(): sufficient capacity"); for (auto const& _ : cache_) v.push_back(_.first); } diff --git a/include/xrpl/basics/ToString.h b/include/xrpl/basics/ToString.h index e9f8f43633..a54db8a8ce 100644 --- a/include/xrpl/basics/ToString.h +++ b/include/xrpl/basics/ToString.h @@ -5,10 +5,11 @@ namespace xrpl { -/** to_string() generalizes std::to_string to handle bools, chars, and strings. - - It's also possible to provide implementation of to_string for a class - which needs a string implementation. +/** + * to_string() generalizes std::to_string to handle bools, chars, and strings. + * + * It's also possible to provide implementation of to_string for a class + * which needs a string implementation. */ template diff --git a/include/xrpl/basics/UptimeClock.h b/include/xrpl/basics/UptimeClock.h index 502aae7c25..b375de4497 100644 --- a/include/xrpl/basics/UptimeClock.h +++ b/include/xrpl/basics/UptimeClock.h @@ -7,12 +7,13 @@ namespace xrpl { -/** Tracks program uptime to seconds precision. - - The timer caches the current time as a performance optimization. - This allows clients to query the current time thousands of times - per second. -*/ +/** + * Tracks program uptime to seconds precision. + * + * The timer caches the current time as a performance optimization. + * This allows clients to query the current time thousands of times + * per second. + */ class UptimeClock { diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 481a7dbd77..bee8b8b945 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -63,18 +63,19 @@ struct AlwaysFalseT : std::bool_constant } // namespace detail -/** Integers of any length that is a multiple of 32-bits - - @note This class stores its values internally in big-endian - form and that internal representation is part of the - binary protocol of the XRP Ledger and cannot be changed - arbitrarily without causing breakage. - - @tparam Bits The number of bits this integer should have; must - be at least 64 and a multiple of 32. - @tparam Tag An arbitrary type that functions as a tag and allows - the instantiation of "distinct" types that the same - number of bits. +/** + * Integers of any length that is a multiple of 32-bits + * + * @note This class stores its values internally in big-endian + * form and that internal representation is part of the + * binary protocol of the XRP Ledger and cannot be changed + * arbitrarily without causing breakage. + * + * @tparam Bits The number of bits this integer should have; must + * be at least 64 and a multiple of 32. + * @tparam Tag An arbitrary type that functions as a tag and allows + * the instantiation of "distinct" types that the same + * number of bits. */ template class BaseUInt @@ -154,21 +155,23 @@ public: return data() + kBytes; } - /** Value hashing function. - The seed prevents crafted inputs from causing degenerate parent - containers. - */ + /** + * Value hashing function. + * The seed prevents crafted inputs from causing degenerate parent + * containers. + */ using hasher = HardenedHash<>; //-------------------------------------------------------------------------- private: - /** Construct from a raw pointer. - The buffer pointed to by `data` must be at least Bits/8 bytes. - - @note the structure is used to disambiguate this from the std::uint64_t - constructor: something like base_uint(0) is ambiguous. - */ + /** + * Construct from a raw pointer. + * The buffer pointed to by `data` must be at least Bits/8 bytes. + * + * @note the structure is used to disambiguate this from the std::uint64_t + * constructor: something like base_uint(0) is ambiguous. + */ // NIKB TODO Remove the need for this constructor. struct VoidHelper { @@ -305,7 +308,9 @@ public: XRPL_ASSERT( c.size() * sizeof(typename Container::value_type) == size(), "xrpl::BaseUInt::fromRaw(Container auto) : input size match"); - std::memcpy(result.data_.data(), c.data(), size()); + std::size_t const canCopy = + std::min(size(), c.size() * sizeof(typename Container::value_type)); + std::memcpy(result.data_.data(), c.data(), canCopy); return result; } @@ -319,7 +324,11 @@ public: XRPL_ASSERT( c.size() * sizeof(typename Container::value_type) == size(), "xrpl::BaseUInt::operator=(Container auto) : input size match"); - std::memcpy(data_.data(), c.data(), size()); + std::size_t const canCopy = + std::min(size(), c.size() * sizeof(typename Container::value_type)); + if (canCopy < size()) + *this = beast::kZero; + std::memcpy(data_.data(), c.data(), canCopy); return *this; } @@ -503,13 +512,14 @@ public: h(a.data_.data(), sizeof(a.data_)); } - /** Parse a hex string into a base_uint - - The input must be precisely `2 * bytes` hexadecimal characters - long, with one exception: the value '0'. - - @param sv A null-terminated string of hexadecimal characters - @return true if the input was parsed properly; false otherwise. + /** + * Parse a hex string into a base_uint + * + * The input must be precisely `2 * bytes` hexadecimal characters + * long, with one exception: the value '0'. + * + * @param sv A null-terminated string of hexadecimal characters + * @return true if the input was parsed properly; false otherwise. */ [[nodiscard]] constexpr bool parseHex(std::string_view sv) @@ -595,7 +605,7 @@ template [[nodiscard]] constexpr bool operator==(BaseUInt const& lhs, BaseUInt const& rhs) { - return (lhs <=> rhs) == 0; + return (lhs <=> rhs) == 0; // NOLINT(modernize-use-nullptr) } //------------------------------------------------------------------------------ diff --git a/include/xrpl/basics/chrono.h b/include/xrpl/basics/chrono.h index 61246fc699..b855318524 100644 --- a/include/xrpl/basics/chrono.h +++ b/include/xrpl/basics/chrono.h @@ -21,15 +21,16 @@ using days = using weeks = std::chrono::duration>>; -/** Clock for measuring the network time. - - The epoch is January 1, 2000 - - epoch_offset - = date(2000-01-01) - date(1970-0-01) - = days(10957) - = seconds(946684800) -*/ +/** + * Clock for measuring the network time. + * + * The epoch is January 1, 2000 + * + * epoch_offset + * = date(2000-01-01) - date(1970-0-01) + * = days(10957) + * = seconds(946684800) + */ static constexpr std::chrono::seconds kEpochOffset = date::sys_days{date::year{2000} / 1 / 1} - date::sys_days{date::year{1970} / 1 / 1}; @@ -81,16 +82,21 @@ toStringIso(NetClock::time_point tp) return toStringIso(date::sys_time{tp.time_since_epoch() + kEpochOffset}); } -/** A clock for measuring elapsed time. - - The epoch is unspecified. -*/ +/** + * A clock for measuring elapsed time. + * + * The epoch is unspecified. + */ using Stopwatch = beast::AbstractClock; -/** A manual Stopwatch for unit tests. */ +/** + * A manual Stopwatch for unit tests. + */ using TestStopwatch = beast::ManualClock; -/** Returns an instance of a wall clock. */ +/** + * Returns an instance of a wall clock. + */ inline Stopwatch& stopwatch() { diff --git a/include/xrpl/basics/contract.h b/include/xrpl/basics/contract.h index 0e90687de3..6588cb5d1a 100644 --- a/include/xrpl/basics/contract.h +++ b/include/xrpl/basics/contract.h @@ -15,20 +15,23 @@ namespace xrpl { preconditions, postconditions, and invariants. */ -/** Generates and logs a call stack */ +/** + * Generates and logs a call stack + */ void logThrow(std::string const& title); -/** Rethrow the exception currently being handled. - - When called from within a catch block, it will pass - control to the next matching exception handler, if any. - Otherwise, std::terminate will be called. - - ASAN can't handle sudden jumps in control flow very well. This - function is marked as XRPL_NO_SANITIZE_ADDRESS to prevent it from - triggering false positives, since it throws. -*/ +/** + * Rethrow the exception currently being handled. + * + * When called from within a catch block, it will pass + * control to the next matching exception handler, if any. + * Otherwise, std::terminate will be called. + * + * ASAN can't handle sudden jumps in control flow very well. This + * function is marked as XRPL_NO_SANITIZE_ADDRESS to prevent it from + * triggering false positives, since it throws. + */ [[noreturn]] XRPL_NO_SANITIZE_ADDRESS inline void rethrow() { @@ -56,7 +59,9 @@ Throw(Args&&... args) throw std::move(e); } -/** Called when faulty logic causes a broken invariant. */ +/** + * Called when faulty logic causes a broken invariant. + */ [[noreturn]] void logicError(std::string const& how) noexcept; diff --git a/include/xrpl/basics/hardened_hash.h b/include/xrpl/basics/hardened_hash.h index 5a855736b3..6b8277a560 100644 --- a/include/xrpl/basics/hardened_hash.h +++ b/include/xrpl/basics/hardened_hash.h @@ -39,33 +39,33 @@ makeSeedPair() noexcept /** * Seed functor once per construction - - A std compatible hash adapter that resists adversarial inputs. - For this to work, T must implement in its own namespace: - - @code - - template - void - hash_append (Hasher& h, T const& t) noexcept - { - // hash_append each base and member that should - // participate in forming the hash - using beast::hash_append; - hash_append (h, static_cast(t)); - hash_append (h, static_cast(t)); - // ... - hash_append (h, t.member1); - hash_append (h, t.member2); - // ... - } - - @endcode - - Do not use any version of Murmur or CityHash for the Hasher - template parameter (the hashing algorithm). For details - see https://131002.net/siphash/#at -*/ + * + * A std compatible hash adapter that resists adversarial inputs. + * For this to work, T must implement in its own namespace: + * + * @code + * + * template + * void + * hash_append (Hasher& h, T const& t) noexcept + * { + * // hash_append each base and member that should + * // participate in forming the hash + * using beast::hash_append; + * hash_append (h, static_cast(t)); + * hash_append (h, static_cast(t)); + * // ... + * hash_append (h, t.member1); + * hash_append (h, t.member2); + * // ... + * } + * + * @endcode + * + * Do not use any version of Murmur or CityHash for the Hasher + * template parameter (the hashing algorithm). For details + * see https://131002.net/siphash/#at + */ template class HardenedHash diff --git a/include/xrpl/basics/make_SSLContext.h b/include/xrpl/basics/make_SSLContext.h index 45ac637c36..c8ada176f9 100644 --- a/include/xrpl/basics/make_SSLContext.h +++ b/include/xrpl/basics/make_SSLContext.h @@ -7,11 +7,15 @@ namespace xrpl { -/** Create a self-signed SSL context that allows anonymous Diffie Hellman. */ +/** + * Create a self-signed SSL context that allows anonymous Diffie Hellman. + */ std::shared_ptr makeSslContext(std::string const& cipherList); -/** Create an authenticated SSL context using the specified files. */ +/** + * Create an authenticated SSL context using the specified files. + */ std::shared_ptr makeSslContextAuthed( std::string const& keyFile, diff --git a/include/xrpl/basics/mulDiv.h b/include/xrpl/basics/mulDiv.h index 9076da62f2..38fa57294b 100644 --- a/include/xrpl/basics/mulDiv.h +++ b/include/xrpl/basics/mulDiv.h @@ -7,16 +7,16 @@ namespace xrpl { constexpr auto kMuldivMax = std::numeric_limits::max(); -/** Return value*mul/div accurately. - Computes the result of the multiplication and division in - a single step, avoiding overflow and retaining precision. - Throws: - None - Returns: - `std::optional`: - `std::nullopt` if the calculation overflows. Otherwise, `value * mul - / div`. -*/ +/** + * Return value*mul/div accurately. + * + * Computes the result of the multiplication and division in + * a single step, avoiding overflow and retaining precision. + * + * @throws None + * @return `std::nullopt` if the calculation overflows. Otherwise, + * `value * mul / div`. + */ std::optional mulDiv(std::uint64_t value, std::uint64_t mul, std::uint64_t div); diff --git a/include/xrpl/basics/partitioned_unordered_map.h b/include/xrpl/basics/partitioned_unordered_map.h index c2750a5769..e78043e252 100644 --- a/include/xrpl/basics/partitioned_unordered_map.h +++ b/include/xrpl/basics/partitioned_unordered_map.h @@ -138,11 +138,8 @@ public: { } - ConstIterator(Iterator const& orig) + ConstIterator(Iterator const& orig) : map(orig.map), ait(orig.ait), mit(orig.mit) { - map = orig.map; - ait = orig.ait; - mit = orig.mit; } const_reference @@ -231,11 +228,11 @@ private: public: PartitionedUnorderedMap(std::optional partitions = std::nullopt) - { // Set partitions to the number of hardware threads if the parameter // is either empty or set to 0. - partitions_ = - partitions && (*partitions != 0u) ? *partitions : std::thread::hardware_concurrency(); + : partitions_( + partitions && (*partitions != 0u) ? *partitions : std::thread::hardware_concurrency()) + { map_.resize(partitions_); XRPL_ASSERT( partitions_, diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h index c544e7d0c8..7aeb7d6145 100644 --- a/include/xrpl/basics/random.h +++ b/include/xrpl/basics/random.h @@ -33,16 +33,17 @@ template using is_engine = std::is_invocable_r; } // namespace detail -/** Return the default random engine. - - This engine is guaranteed to be deterministic, but by - default will be randomly seeded. It is NOT cryptographically - secure and MUST NOT be used to generate randomness that - will be used for keys, secure cookies, IVs, padding, etc. - - Each thread gets its own instance of the engine which - will be randomly seeded. -*/ +/** + * Return the default random engine. + * + * This engine is guaranteed to be deterministic, but by + * default will be randomly seeded. It is NOT cryptographically + * secure and MUST NOT be used to generate randomness that + * will be used for keys, secure cookies, IVs, padding, etc. + * + * Each thread gets its own instance of the engine which + * will be randomly seeded. + */ inline beast::xor_shift_engine& defaultPrng() { @@ -70,25 +71,26 @@ defaultPrng() return kEngine; } -/** Return a uniformly distributed random integer. - - @param min The smallest value to return. If not specified - the value defaults to 0. - @param max The largest value to return. If not specified - the value defaults to the largest value that - can be represented. - - The randomness is generated by the specified engine (or - the default engine if one is not specified). The result - is cryptographically secure only when the engine passed - into the function is cryptographically secure. - - @note The range is always a closed interval, so calling - rand_int(-5, 15) can return any integer in the - closed interval [-5, 15]; similarly, calling - rand_int(7) can return any integer in the closed - interval [0, 7]. -*/ +/** + * Return a uniformly distributed random integer. + * + * @param min The smallest value to return. If not specified + * the value defaults to 0. + * @param max The largest value to return. If not specified + * the value defaults to the largest value that + * can be represented. + * + * The randomness is generated by the specified engine (or + * the default engine if one is not specified). The result + * is cryptographically secure only when the engine passed + * into the function is cryptographically secure. + * + * @note The range is always a closed interval, so calling + * rand_int(-5, 15) can return any integer in the + * closed interval [-5, 15]; similarly, calling + * rand_int(7) can return any integer in the closed + * interval [0, 7]. + */ /** @{ */ template Integral @@ -144,7 +146,9 @@ randInt() } /** @} */ -/** Return a random byte */ +/** + * Return a random byte + */ /** @{ */ template Byte @@ -166,7 +170,9 @@ randByte() } /** @} */ -/** Return a random boolean value */ +/** + * Return a random boolean value + */ /** @{ */ template inline bool diff --git a/include/xrpl/basics/scope.h b/include/xrpl/basics/scope.h index e63bb69eb5..5821e1dacc 100644 --- a/include/xrpl/basics/scope.h +++ b/include/xrpl/basics/scope.h @@ -156,41 +156,41 @@ template ScopeSuccess(EF) -> ScopeSuccess; /** - Automatically unlocks and re-locks a unique_lock object. - - This is the reverse of a std::unique_lock object - instead of locking the - mutex for the lifetime of this object, it unlocks it. - - Make sure you don't try to unlock mutexes that aren't actually locked! - - This is essentially a less-versatile boost::reverse_lock. - - e.g. @code - - std::mutex mut; - - for (;;) - { - std::unique_lock myScopedLock{mut}; - // mut is now locked - - ... do some stuff with it locked .. - - while (xyz) - { - ... do some stuff with it locked .. - - scope_unlock unlocker{myScopedLock}; - - // mut is now unlocked for the remainder of this block, - // and re-locked at the end. - - ...do some stuff with it unlocked ... - } // mut gets locked here. - - } // mut gets unlocked here - @endcode -*/ + * Automatically unlocks and re-locks a unique_lock object. + * + * This is the reverse of a std::unique_lock object - instead of locking the + * mutex for the lifetime of this object, it unlocks it. + * + * Make sure you don't try to unlock mutexes that aren't actually locked! + * + * This is essentially a less-versatile boost::reverse_lock. + * + * e.g. @code + * + * std::mutex mut; + * + * for (;;) + * { + * std::unique_lock myScopedLock{mut}; + * // mut is now locked + * + * ... do some stuff with it locked .. + * + * while (xyz) + * { + * ... do some stuff with it locked .. + * + * scope_unlock unlocker{myScopedLock}; + * + * // mut is now unlocked for the remainder of this block, + * // and re-locked at the end. + * + * ...do some stuff with it unlocked ... + * } // mut gets locked here. + * + * } // mut gets unlocked here + * @endcode + */ template class ScopeUnlock diff --git a/include/xrpl/basics/spinlock.h b/include/xrpl/basics/spinlock.h index 2cc00efdef..87611f20ba 100644 --- a/include/xrpl/basics/spinlock.h +++ b/include/xrpl/basics/spinlock.h @@ -15,15 +15,16 @@ namespace xrpl { namespace detail { -/** Inform the processor that we are in a tight spin-wait loop. - - Spinlocks caught in tight loops can result in the processor's pipeline - filling up with comparison operations, resulting in a misprediction at - the time the lock is finally acquired, necessitating pipeline flushing - which is ridiculously expensive and results in very high latency. - - This function instructs the processor to "pause" for some architecture - specific amount of time, to prevent this. +/** + * Inform the processor that we are in a tight spin-wait loop. + * + * Spinlocks caught in tight loops can result in the processor's pipeline + * filling up with comparison operations, resulting in a misprediction at + * the time the lock is finally acquired, necessitating pipeline flushing + * which is ridiculously expensive and results in very high latency. + * + * This function instructs the processor to "pause" for some architecture + * specific amount of time, to prevent this. */ inline void spinPause() noexcept @@ -38,37 +39,39 @@ spinPause() noexcept } // namespace detail /** @{ */ -/** Classes to handle arrays of spinlocks packed into a single atomic integer: - - Packed spinlocks allow for tremendously space-efficient lock-sharding - but they come at a cost. - - First, the implementation is necessarily low-level and uses advanced - features like memory ordering and highly platform-specific tricks to - maximize performance. This imposes a significant and ongoing cost to - developers. - - Second, and perhaps most important, is that the packing of multiple - locks into a single integer which, albeit space-efficient, also has - performance implications stemming from data dependencies, increased - cache-coherency traffic between processors and heavier loads on the - processor's load/store units. - - To be sure, these locks can have advantages but they are definitely - not general purpose locks and should not be thought of or used that - way. The use cases for them are likely few and far between; without - a compelling reason to use them, backed by profiling data, it might - be best to use one of the standard locking primitives instead. Note - that in most common platforms, `std::mutex` is so heavily optimized - that it can, usually, outperform spinlocks. - - @tparam T An unsigned integral type (e.g. std::uint16_t) +/** + * Classes to handle arrays of spinlocks packed into a single atomic integer: + * + * Packed spinlocks allow for tremendously space-efficient lock-sharding + * but they come at a cost. + * + * First, the implementation is necessarily low-level and uses advanced + * features like memory ordering and highly platform-specific tricks to + * maximize performance. This imposes a significant and ongoing cost to + * developers. + * + * Second, and perhaps most important, is that the packing of multiple + * locks into a single integer which, albeit space-efficient, also has + * performance implications stemming from data dependencies, increased + * cache-coherency traffic between processors and heavier loads on the + * processor's load/store units. + * + * To be sure, these locks can have advantages but they are definitely + * not general purpose locks and should not be thought of or used that + * way. The use cases for them are likely few and far between; without + * a compelling reason to use them, backed by profiling data, it might + * be best to use one of the standard locking primitives instead. Note + * that in most common platforms, `std::mutex` is so heavily optimized + * that it can, usually, outperform spinlocks. + * + * @tparam T An unsigned integral type (e.g. std::uint16_t) */ -/** A class that grabs a single packed spinlock from an atomic integer. - - This class meets the requirements of Lockable: - https://en.cppreference.com/w/cpp/named_req/Lockable +/** + * A class that grabs a single packed spinlock from an atomic integer. + * + * This class meets the requirements of Lockable: + * https://en.cppreference.com/w/cpp/named_req/Lockable */ template class PackedSpinlock @@ -91,13 +94,14 @@ public: PackedSpinlock& operator=(PackedSpinlock const&) = delete; - /** A single spinlock packed inside the specified atomic - - @param lock The atomic integer inside which the spinlock is packed. - @param index The index of the spinlock this object acquires. - - @note For performance reasons, you should strive to have `lock` be - on a cacheline by itself. + /** + * A single spinlock packed inside the specified atomic + * + * @param lock The atomic integer inside which the spinlock is packed. + * @param index The index of the spinlock this object acquires. + * + * @note For performance reasons, you should strive to have `lock` be + * on a cacheline by itself. */ PackedSpinlock(std::atomic& lock, int index) : bits_(lock), mask_(static_cast(1) << index) { @@ -133,17 +137,18 @@ public: } }; -/** A spinlock implemented on top of an atomic integer. - - @note Using `packed_spinlock` and `spinlock` against the same underlying - atomic integer can result in `spinlock` not being able to actually - acquire the lock during periods of high contention, because of how - the two locks operate: `spinlock` will spin trying to grab all the - bits at once, whereas any given `packed_spinlock` will only try to - grab one bit at a time. Caveat emptor. - - This class meets the requirements of Lockable: - https://en.cppreference.com/w/cpp/named_req/Lockable +/** + * A spinlock implemented on top of an atomic integer. + * + * @note Using `packed_spinlock` and `spinlock` against the same underlying + * atomic integer can result in `spinlock` not being able to actually + * acquire the lock during periods of high contention, because of how + * the two locks operate: `spinlock` will spin trying to grab all the + * bits at once, whereas any given `packed_spinlock` will only try to + * grab one bit at a time. Caveat emptor. + * + * This class meets the requirements of Lockable: + * https://en.cppreference.com/w/cpp/named_req/Lockable */ template class Spinlock @@ -159,12 +164,13 @@ public: Spinlock& operator=(Spinlock const&) = delete; - /** Grabs the - - @param lock The atomic integer to spin against. - - @note For performance reasons, you should strive to have `lock` be - on a cacheline by itself. + /** + * Grabs the + * + * @param lock The atomic integer to spin against. + * + * @note For performance reasons, you should strive to have `lock` be + * on a cacheline by itself. */ Spinlock(std::atomic& lock) : lock_(lock) { diff --git a/include/xrpl/basics/tagged_integer.h b/include/xrpl/basics/tagged_integer.h index 5a088db863..2edb314a16 100644 --- a/include/xrpl/basics/tagged_integer.h +++ b/include/xrpl/basics/tagged_integer.h @@ -12,17 +12,18 @@ namespace xrpl { -/** A type-safe wrap around standard integral types - - The tag is used to implement type safety, catching mismatched types at - compile time. Multiple instantiations wrapping the same underlying integral - type are distinct types (distinguished by tag) and will not interoperate. A - tagged_integer supports all the usual assignment, arithmetic, comparison and - shifting operations defined for the underlying type - - The tag is not meant as a unit, which would require restricting the set of - allowed arithmetic operations. -*/ +/** + * A type-safe wrap around standard integral types + * + * The tag is used to implement type safety, catching mismatched types at + * compile time. Multiple instantiations wrapping the same underlying integral + * type are distinct types (distinguished by tag) and will not interoperate. A + * tagged_integer supports all the usual assignment, arithmetic, comparison and + * shifting operations defined for the underlying type + * + * The tag is not meant as a unit, which would require restricting the set of + * allowed arithmetic operations. + */ template class TaggedInteger : boost::totally_ordered< TaggedInteger, diff --git a/include/xrpl/beast/asio/io_latency_probe.h b/include/xrpl/beast/asio/io_latency_probe.h index f67ff4a692..d87bdafe45 100644 --- a/include/xrpl/beast/asio/io_latency_probe.h +++ b/include/xrpl/beast/asio/io_latency_probe.h @@ -14,7 +14,9 @@ namespace beast { -/** Measures handler latency on an io_context queue. */ +/** + * Measures handler latency on an io_context queue. + */ template class IOLatencyProbe { @@ -42,7 +44,9 @@ public: cancel(lock, true); } - /** Return the io_context associated with the latency probe. */ + /** + * Return the io_context associated with the latency probe. + */ /** @{ */ boost::asio::io_context& getIoContext() @@ -57,9 +61,10 @@ public: } /** @} */ - /** Cancel all pending i/o. - Any handlers which have already been queued will still be called. - */ + /** + * Cancel all pending i/o. + * Any handlers which have already been queued will still be called. + */ /** @{ */ void cancel() @@ -76,10 +81,11 @@ public: } /** @} */ - /** Measure one sample of i/o latency. - Handler will be called with this signature: - void Handler (Duration d); - */ + /** + * Measure one sample of i/o latency. + * Handler will be called with this signature: + * void Handler (Duration d); + */ template void sampleOne(Handler&& handler) @@ -91,10 +97,11 @@ public: ios_, SampleOp(std::forward(handler), Clock::now(), false, this)); } - /** Initiate continuous i/o latency sampling. - Handler will be called with this signature: - void Handler (std::chrono::milliseconds); - */ + /** + * Initiate continuous i/o latency sampling. + * Handler will be called with this signature: + * void Handler (std::chrono::milliseconds); + */ template void sample(Handler&& handler) diff --git a/include/xrpl/beast/clock/abstract_clock.h b/include/xrpl/beast/clock/abstract_clock.h index 15d785d138..6e23700730 100644 --- a/include/xrpl/beast/clock/abstract_clock.h +++ b/include/xrpl/beast/clock/abstract_clock.h @@ -2,34 +2,35 @@ namespace beast { -/** Abstract interface to a clock. - - This makes now() a member function instead of a static member, so - an instance of the class can be dependency injected, facilitating - unit tests where time may be controlled. - - An abstract_clock inherits all the nested types of the Clock - template parameter. - - Example: - - @code - - struct Implementation - { - using clock_type = abstract_clock ; - clock_type& clock_; - explicit Implementation (clock_type& clock) - : clock_(clock) - { - } - }; - - @endcode - - @tparam Clock A type meeting these requirements: - http://en.cppreference.com/w/cpp/concept/Clock -*/ +/** + * Abstract interface to a clock. + * + * This makes now() a member function instead of a static member, so + * an instance of the class can be dependency injected, facilitating + * unit tests where time may be controlled. + * + * An abstract_clock inherits all the nested types of the Clock + * template parameter. + * + * Example: + * + * @code + * + * struct Implementation + * { + * using clock_type = abstract_clock ; + * clock_type& clock_; + * explicit Implementation (clock_type& clock) + * : clock_(clock) + * { + * } + * }; + * + * @endcode + * + * @tparam Clock A type meeting these requirements: + * http://en.cppreference.com/w/cpp/concept/Clock + */ template class AbstractClock { @@ -46,7 +47,9 @@ public: AbstractClock() = default; AbstractClock(AbstractClock const&) = default; - /** Returns the current time. */ + /** + * Returns the current time. + */ [[nodiscard]] virtual time_point now() const = 0; }; @@ -74,11 +77,12 @@ struct AbstractClockWrapper : public AbstractClock //------------------------------------------------------------------------------ -/** Returns a global instance of an abstract clock. - @tparam Facade A type meeting these requirements: - http://en.cppreference.com/w/cpp/concept/Clock - @tparam Clock The actual concrete clock to use. -*/ +/** + * Returns a global instance of an abstract clock. + * @tparam Facade A type meeting these requirements: + * http://en.cppreference.com/w/cpp/concept/Clock + * @tparam Clock The actual concrete clock to use. + */ template AbstractClock& getAbstractClock() diff --git a/include/xrpl/beast/clock/basic_seconds_clock.h b/include/xrpl/beast/clock/basic_seconds_clock.h index 5a267e9458..dce521d0b8 100644 --- a/include/xrpl/beast/clock/basic_seconds_clock.h +++ b/include/xrpl/beast/clock/basic_seconds_clock.h @@ -4,15 +4,16 @@ namespace beast { -/** A clock whose minimum resolution is one second. - - The purpose of this class is to optimize the performance of the now() - member function call. It uses a dedicated thread that wakes up at least - once per second to sample the requested trivial clock. - - @tparam Clock A type meeting these requirements: - http://en.cppreference.com/w/cpp/concept/Clock -*/ +/** + * A clock whose minimum resolution is one second. + * + * The purpose of this class is to optimize the performance of the now() + * member function call. It uses a dedicated thread that wakes up at least + * once per second to sample the requested trivial clock. + * + * @tparam Clock A type meeting these requirements: + * http://en.cppreference.com/w/cpp/concept/Clock + */ class BasicSecondsClock { public: diff --git a/include/xrpl/beast/clock/manual_clock.h b/include/xrpl/beast/clock/manual_clock.h index 8b3e4e63c6..4dc9553644 100644 --- a/include/xrpl/beast/clock/manual_clock.h +++ b/include/xrpl/beast/clock/manual_clock.h @@ -7,15 +7,16 @@ namespace beast { -/** Manual clock implementation. - - This concrete class implements the @ref abstract_clock interface and - allows the time to be advanced manually, mainly for the purpose of - providing a clock in unit tests. - - @tparam Clock A type meeting these requirements: - http://en.cppreference.com/w/cpp/concept/Clock -*/ +/** + * Manual clock implementation. + * + * This concrete class implements the @ref abstract_clock interface and + * allows the time to be advanced manually, mainly for the purpose of + * providing a clock in unit tests. + * + * @tparam Clock A type meeting these requirements: + * http://en.cppreference.com/w/cpp/concept/Clock + */ template class ManualClock : public AbstractClock { @@ -38,7 +39,9 @@ public: return now_; } - /** Set the current time of the manual clock. */ + /** + * Set the current time of the manual clock. + */ void set(time_point const& when) { @@ -48,7 +51,9 @@ public: now_ = when; } - /** Convenience for setting the time in seconds from epoch. */ + /** + * Convenience for setting the time in seconds from epoch. + */ template void set(Integer secondsFromEpoch) @@ -56,7 +61,9 @@ public: set(time_point(duration(std::chrono::seconds(secondsFromEpoch)))); } - /** Advance the clock by a duration. */ + /** + * Advance the clock by a duration. + */ template void advance(std::chrono::duration const& elapsed) @@ -67,7 +74,9 @@ public: now_ += elapsed; } - /** Convenience for advancing the clock by one second. */ + /** + * Convenience for advancing the clock by one second. + */ ManualClock& operator++() { diff --git a/include/xrpl/beast/container/aged_container_utility.h b/include/xrpl/beast/container/aged_container_utility.h index f43e59b0f7..da3e4e0500 100644 --- a/include/xrpl/beast/container/aged_container_utility.h +++ b/include/xrpl/beast/container/aged_container_utility.h @@ -7,7 +7,9 @@ namespace beast { -/** Expire aged container items past the specified age. */ +/** + * Expire aged container items past the specified age. + */ template std::size_t expire(AgedContainer& c, std::chrono::duration const& age) diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index f739f05d2c..5b60ef7e6d 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -39,22 +39,23 @@ struct IsBoostReverseIterator> : std::tru explicit IsBoostReverseIterator() = default; }; -/** Associative container where each element is also indexed by time. - - This container mirrors the interface of the standard library ordered - associative containers, with the addition that each element is associated - with a `when` `time_point` which is obtained from the value of the clock's - `now`. The function `touch` updates the time for an element to the current - time as reported by the clock. - - An extra set of iterator types and member functions are provided in the - `chronological` memberspace that allow traversal in temporal or reverse - temporal order. This container is useful as a building block for caches - whose items expire after a certain amount of time. The chronological - iterators allow for fully customizable expiration strategies. - - @see aged_set, aged_multiset, aged_map, aged_multimap -*/ +/** + * Associative container where each element is also indexed by time. + * + * This container mirrors the interface of the standard library ordered + * associative containers, with the addition that each element is associated + * with a `when` `time_point` which is obtained from the value of the clock's + * `now`. The function `touch` updates the time for an element to the current + * time as reported by the clock. + * + * An extra set of iterator types and member functions are provided in the + * `chronological` memberspace that allow traversal in temporal or reverse + * temporal order. This container is useful as a building block for caches + * whose items expire after a certain amount of time. The chronological + * iterators allow for fully customizable expiration strategies. + * + * @see aged_set, aged_multiset, aged_map, aged_multimap + */ template < bool IsMulti, bool IsMap, @@ -359,6 +360,7 @@ private: deleteElement(Element const* p) { ElementAllocatorTraits::destroy(config_.alloc(), p); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) ElementAllocatorTraits::deallocate(config_.alloc(), const_cast(p), 1); } @@ -1794,7 +1796,9 @@ swap( lhs.swap(rhs); } -/** Expire aged container items past the specified age. */ +/** + * Expire aged container items past the specified age. + */ template < bool IsMulti, bool IsMap, diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index ea24141bfb..db10e8cc23 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -43,23 +43,24 @@ TODO namespace beast { namespace detail { -/** Associative container where each element is also indexed by time. - - This container mirrors the interface of the standard library unordered - associative containers, with the addition that each element is associated - with a `when` `time_point` which is obtained from the value of the clock's - `now`. The function `touch` updates the time for an element to the current - time as reported by the clock. - - An extra set of iterator types and member functions are provided in the - `chronological` memberspace that allow traversal in temporal or reverse - temporal order. This container is useful as a building block for caches - whose items expire after a certain amount of time. The chronological - iterators allow for fully customizable expiration strategies. - - @see aged_unordered_set, aged_unordered_multiset - @see aged_unordered_map, aged_unordered_multimap -*/ +/** + * Associative container where each element is also indexed by time. + * + * This container mirrors the interface of the standard library unordered + * associative containers, with the addition that each element is associated + * with a `when` `time_point` which is obtained from the value of the clock's + * `now`. The function `touch` updates the time for an element to the current + * time as reported by the clock. + * + * An extra set of iterator types and member functions are provided in the + * `chronological` memberspace that allow traversal in temporal or reverse + * temporal order. This container is useful as a building block for caches + * whose items expire after a certain amount of time. The chronological + * iterators allow for fully customizable expiration strategies. + * + * @see aged_unordered_set, aged_unordered_multiset + * @see aged_unordered_map, aged_unordered_multimap + */ template < bool IsMulti, bool IsMap, @@ -528,6 +529,7 @@ private: deleteElement(Element const* p) { ElementAllocatorTraits::destroy(config_.alloc(), p); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) ElementAllocatorTraits::deallocate(config_.alloc(), const_cast(p), 1); } @@ -2708,7 +2710,9 @@ swap( lhs.swap(rhs); } -/** Expire aged container items past the specified age. */ +/** + * Expire aged container items past the specified age. + */ template < bool IsMulti, bool IsMap, diff --git a/include/xrpl/beast/core/CurrentThreadName.h b/include/xrpl/beast/core/CurrentThreadName.h index 3cdfe4c678..d1f14a6f80 100644 --- a/include/xrpl/beast/core/CurrentThreadName.h +++ b/include/xrpl/beast/core/CurrentThreadName.h @@ -12,9 +12,10 @@ namespace beast { -/** Changes the name of the caller thread. - Different OSes may place different length or content limits on this name. -*/ +/** + * Changes the name of the caller thread. + * Different OSes may place different length or content limits on this name. + */ void setCurrentThreadName(std::string_view newThreadName); @@ -24,13 +25,14 @@ setCurrentThreadName(std::string_view newThreadName); // Maximum number of characters is therefore 15. constexpr std::size_t kMaxThreadNameLength = 15; -/** Sets the name of the caller thread with compile-time size checking. - @tparam N The size of the string literal including null terminator - @param newThreadName A string literal to set as the thread name - - This template overload enforces that thread names are at most 16 characters - (including null terminator) at compile time, matching Linux's limit. -*/ +/** + * Sets the name of the caller thread with compile-time size checking. + * @tparam N The size of the string literal including null terminator + * @param newThreadName A string literal to set as the thread name + * + * This template overload enforces that thread names are at most 16 characters + * (including null terminator) at compile time, matching Linux's limit. + */ template void setCurrentThreadName(char const (&newThreadName)[N]) @@ -41,14 +43,15 @@ setCurrentThreadName(char const (&newThreadName)[N]) } #endif -/** Returns the name of the caller thread. - - The name returned is the name as set by a call to setCurrentThreadName(). - If the thread name is set by an external force, then that name change - will not be reported. - - If no name has ever been set, then the empty string is returned. -*/ +/** + * Returns the name of the caller thread. + * + * The name returned is the name as set by a call to setCurrentThreadName(). + * If the thread name is set by an external force, then that name change + * will not be reported. + * + * If no name has ever been set, then the empty string is returned. + */ std::string getCurrentThreadName(); diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index 1162d83078..7cf21892bd 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -163,17 +163,19 @@ struct LexicalCast //------------------------------------------------------------------------------ -/** Thrown when a conversion is not possible with LexicalCast. - Only used in the throw variants of lexicalCast. -*/ +/** + * Thrown when a conversion is not possible with LexicalCast. + * Only used in the throw variants of lexicalCast. + */ struct BadLexicalCast : public std::bad_cast { explicit BadLexicalCast() = default; }; -/** Intelligently convert from one type to another. - @return `false` if there was a parsing or range error -*/ +/** + * Intelligently convert from one type to another. + * @return `false` if there was a parsing or range error + */ template bool lexicalCastChecked(Out& out, In in) @@ -181,12 +183,13 @@ lexicalCastChecked(Out& out, In in) return detail::LexicalCast()(out, in); } -/** Convert from one type to another, throw on error - - An exception of type BadLexicalCast is thrown if the conversion fails. - - @return The new type. -*/ +/** + * Convert from one type to another, throw on error + * + * An exception of type BadLexicalCast is thrown if the conversion fails. + * + * @return The new type. + */ template Out lexicalCastThrow(In in) @@ -197,11 +200,12 @@ lexicalCastThrow(In in) throw BadLexicalCast(); } -/** Convert from one type to another. - - @param defaultValue The value returned if parsing fails - @return The new type. -*/ +/** + * Convert from one type to another. + * + * @param defaultValue The value returned if parsing fails + * @return The new type. + */ template Out lexicalCast(In in, Out defaultValue = Out()) diff --git a/include/xrpl/beast/core/List.h b/include/xrpl/beast/core/List.h index 1eeeaa87d1..b9b6829d31 100644 --- a/include/xrpl/beast/core/List.h +++ b/include/xrpl/beast/core/List.h @@ -11,7 +11,9 @@ class List; namespace detail { -/** Copy `const` attribute from T to U if present. */ +/** + * Copy `const` attribute from T to U if present. + */ /** @{ */ template struct CopyConst @@ -153,110 +155,111 @@ private: } // namespace detail -/** Intrusive doubly linked list. - - This intrusive List is a container similar in operation to std::list in the - Standard Template Library (STL). Like all @ref intrusive containers, List - requires you to first derive your class from List<>::Node: - - @code - - struct Object : List ::Node - { - explicit Object (int value) : value_ (value) - { - } - - int value_; - }; - - @endcode - - Now we define the list, and add a couple of items. - - @code - - List list; - - list.push_back (* (new Object (1))); - list.push_back (* (new Object (2))); - - @endcode - - For compatibility with the standard containers, push_back() expects a - reference to the object. Unlike the standard container, however, push_back() - places the actual object in the list and not a copy-constructed duplicate. - - Iterating over the list follows the same idiom as the STL: - - @code - - for (List ::iterator iter = list.begin(); iter != list.end; ++iter) - std::cout << iter->value_; - - @endcode - - You can even use BOOST_FOREACH, or range based for loops: - - @code - - BOOST_FOREACH (Object& object, list) // boost only - std::cout << object.value_; - - for (Object& object : list) // C++11 only - std::cout << object.value_; - - @endcode - - Because List is mostly STL compliant, it can be passed into STL algorithms: - e.g. `std::for_each()` or `std::find_first_of()`. - - In general, objects placed into a List should be dynamically allocated - although this cannot be enforced at compile time. Since the caller provides - the storage for the object, the caller is also responsible for deleting the - object. An object still exists after being removed from a List, until the - caller deletes it. This means an element can be moved from one List to - another with practically no overhead. - - Unlike the standard containers, an object may only exist in one list at a - time, unless special preparations are made. The Tag template parameter is - used to distinguish between different list types for the same object, - allowing the object to exist in more than one list simultaneously. - - For example, consider an actor system where a global list of actors is - maintained, so that they can each be periodically receive processing - time. We wish to also maintain a list of the subset of actors that require - a domain-dependent update. To achieve this, we declare two tags, the - associated list types, and the list element thusly: - - @code - - struct Actor; // Forward declaration required - - struct ProcessTag { }; - struct UpdateTag { }; - - using ProcessList = List ; - using UpdateList = List ; - - // Derive from both node types so we can be in each list at once. - // - struct Actor : ProcessList::Node, UpdateList::Node - { - bool process (); // returns true if we need an update - void update (); - }; - - @endcode - - @tparam T The base type of element which the list will store - pointers to. - - @tparam Tag An optional unique type name used to distinguish lists and - nodes, when the object can exist in multiple lists simultaneously. - - @ingroup beast_core intrusive -*/ +/** + * Intrusive doubly linked list. + * + * This intrusive List is a container similar in operation to std::list in the + * Standard Template Library (STL). Like all @ref intrusive containers, List + * requires you to first derive your class from List<>::Node: + * + * @code + * + * struct Object : List ::Node + * { + * explicit Object (int value) : value_ (value) + * { + * } + * + * int value_; + * }; + * + * @endcode + * + * Now we define the list, and add a couple of items. + * + * @code + * + * List list; + * + * list.push_back (* (new Object (1))); + * list.push_back (* (new Object (2))); + * + * @endcode + * + * For compatibility with the standard containers, push_back() expects a + * reference to the object. Unlike the standard container, however, push_back() + * places the actual object in the list and not a copy-constructed duplicate. + * + * Iterating over the list follows the same idiom as the STL: + * + * @code + * + * for (List ::iterator iter = list.begin(); iter != list.end; ++iter) + * std::cout << iter->value_; + * + * @endcode + * + * You can even use BOOST_FOREACH, or range based for loops: + * + * @code + * + * BOOST_FOREACH (Object& object, list) // boost only + * std::cout << object.value_; + * + * for (Object& object : list) // C++11 only + * std::cout << object.value_; + * + * @endcode + * + * Because List is mostly STL compliant, it can be passed into STL algorithms: + * e.g. `std::for_each()` or `std::find_first_of()`. + * + * In general, objects placed into a List should be dynamically allocated + * although this cannot be enforced at compile time. Since the caller provides + * the storage for the object, the caller is also responsible for deleting the + * object. An object still exists after being removed from a List, until the + * caller deletes it. This means an element can be moved from one List to + * another with practically no overhead. + * + * Unlike the standard containers, an object may only exist in one list at a + * time, unless special preparations are made. The Tag template parameter is + * used to distinguish between different list types for the same object, + * allowing the object to exist in more than one list simultaneously. + * + * For example, consider an actor system where a global list of actors is + * maintained, so that they can each be periodically receive processing + * time. We wish to also maintain a list of the subset of actors that require + * a domain-dependent update. To achieve this, we declare two tags, the + * associated list types, and the list element thusly: + * + * @code + * + * struct Actor; // Forward declaration required + * + * struct ProcessTag { }; + * struct UpdateTag { }; + * + * using ProcessList = List ; + * using UpdateList = List ; + * + * // Derive from both node types so we can be in each list at once. + * // + * struct Actor : ProcessList::Node, UpdateList::Node + * { + * bool process (); // returns true if we need an update + * void update (); + * }; + * + * @endcode + * + * @tparam T The base type of element which the list will store + * pointers to. + * + * @tparam Tag An optional unique type name used to distinguish lists and + * nodes, when the object can exist in multiple lists simultaneously. + * + * @ingroup beast_core intrusive + */ template class List { @@ -274,7 +277,9 @@ public: using iterator = detail::ListIterator; using const_iterator = detail::ListIterator; - /** Create an empty list. */ + /** + * Create an empty list. + */ List() { head_.prev_ = nullptr; // identifies the head @@ -286,119 +291,133 @@ public: List& operator=(List const&) = delete; - /** Determine if the list is empty. - @return `true` if the list is empty. - */ + /** + * Determine if the list is empty. + * @return `true` if the list is empty. + */ [[nodiscard]] bool empty() const noexcept { return size() == 0; } - /** Returns the number of elements in the list. */ + /** + * Returns the number of elements in the list. + */ [[nodiscard]] size_type size() const noexcept { return size_; } - /** Obtain a reference to the first element. - @invariant The list may not be empty. - @return A reference to the first element. - */ + /** + * Obtain a reference to the first element. + * @invariant The list may not be empty. + * @return A reference to the first element. + */ reference front() noexcept { return element_from(head_.next_); } - /** Obtain a const reference to the first element. - @invariant The list may not be empty. - @return A const reference to the first element. - */ + /** + * Obtain a const reference to the first element. + * @invariant The list may not be empty. + * @return A const reference to the first element. + */ [[nodiscard]] const_reference front() const noexcept { return element_from(head_.next_); } - /** Obtain a reference to the last element. - @invariant The list may not be empty. - @return A reference to the last element. - */ + /** + * Obtain a reference to the last element. + * @invariant The list may not be empty. + * @return A reference to the last element. + */ reference back() noexcept { return element_from(tail_.prev_); } - /** Obtain a const reference to the last element. - @invariant The list may not be empty. - @return A const reference to the last element. - */ + /** + * Obtain a const reference to the last element. + * @invariant The list may not be empty. + * @return A const reference to the last element. + */ [[nodiscard]] const_reference back() const noexcept { return element_from(tail_.prev_); } - /** Obtain an iterator to the beginning of the list. - @return An iterator pointing to the beginning of the list. - */ + /** + * Obtain an iterator to the beginning of the list. + * @return An iterator pointing to the beginning of the list. + */ iterator begin() noexcept { return iterator(head_.next_); } - /** Obtain a const iterator to the beginning of the list. - @return A const iterator pointing to the beginning of the list. - */ + /** + * Obtain a const iterator to the beginning of the list. + * @return A const iterator pointing to the beginning of the list. + */ [[nodiscard]] const_iterator begin() const noexcept { return const_iterator(head_.next_); } - /** Obtain a const iterator to the beginning of the list. - @return A const iterator pointing to the beginning of the list. - */ + /** + * Obtain a const iterator to the beginning of the list. + * @return A const iterator pointing to the beginning of the list. + */ [[nodiscard]] const_iterator cbegin() const noexcept { return const_iterator(head_.next_); } - /** Obtain a iterator to the end of the list. - @return An iterator pointing to the end of the list. - */ + /** + * Obtain a iterator to the end of the list. + * @return An iterator pointing to the end of the list. + */ iterator end() noexcept { return iterator(&tail_); } - /** Obtain a const iterator to the end of the list. - @return A constiterator pointing to the end of the list. - */ + /** + * Obtain a const iterator to the end of the list. + * @return A constiterator pointing to the end of the list. + */ [[nodiscard]] const_iterator end() const noexcept { return const_iterator(&tail_); } - /** Obtain a const iterator to the end of the list - @return A constiterator pointing to the end of the list. - */ + /** + * Obtain a const iterator to the end of the list + * @return A constiterator pointing to the end of the list. + */ [[nodiscard]] const_iterator cend() const noexcept { return const_iterator(&tail_); } - /** Clear the list. - @note This does not free the elements. - */ + /** + * Clear the list. + * @note This does not free the elements. + */ void clear() noexcept { @@ -407,12 +426,13 @@ public: size_ = 0; } - /** Insert an element. - @invariant The element must not already be in the list. - @param pos The location to insert after. - @param element The element to insert. - @return An iterator pointing to the newly inserted element. - */ + /** + * Insert an element. + * @invariant The element must not already be in the list. + * @param pos The location to insert after. + * @param element The element to insert. + * @return An iterator pointing to the newly inserted element. + */ iterator insert(iterator pos, T& element) noexcept { @@ -425,11 +445,12 @@ public: return iterator(node); } - /** Insert another list into this one. - The other list is cleared. - @param pos The location to insert after. - @param other The list to insert. - */ + /** + * Insert another list into this one. + * The other list is cleared. + * @param pos The location to insert after. + * @param other The list to insert. + */ void insert(iterator pos, List& other) noexcept { @@ -445,11 +466,12 @@ public: } } - /** Remove an element. - @invariant The element must exist in the list. - @param pos An iterator pointing to the element to remove. - @return An iterator pointing to the next element after the one removed. - */ + /** + * Remove an element. + * @invariant The element must exist in the list. + * @param pos An iterator pointing to the element to remove. + * @return An iterator pointing to the next element after the one removed. + */ iterator erase(iterator pos) noexcept { @@ -461,20 +483,22 @@ public: return pos; } - /** Insert an element at the beginning of the list. - @invariant The element must not exist in the list. - @param element The element to insert. - */ + /** + * Insert an element at the beginning of the list. + * @invariant The element must not exist in the list. + * @param element The element to insert. + */ iterator pushFront(T& element) noexcept { return insert(begin(), element); } - /** Remove the element at the beginning of the list. - @invariant The list must not be empty. - @return A reference to the popped element. - */ + /** + * Remove the element at the beginning of the list. + * @invariant The list must not be empty. + * @return A reference to the popped element. + */ T& popFront() noexcept { @@ -483,20 +507,22 @@ public: return element; } - /** Append an element at the end of the list. - @invariant The element must not exist in the list. - @param element The element to append. - */ + /** + * Append an element at the end of the list. + * @invariant The element must not exist in the list. + * @param element The element to append. + */ iterator pushBack(T& element) noexcept { return insert(end(), element); } - /** Remove the element at the end of the list. - @invariant The list must not be empty. - @return A reference to the popped element. - */ + /** + * Remove the element at the end of the list. + * @invariant The list must not be empty. + * @return A reference to the popped element. + */ T& popBack() noexcept { @@ -505,7 +531,9 @@ public: return element; } - /** Swap contents with another list. */ + /** + * Swap contents with another list. + */ void swap(List& other) noexcept { @@ -515,42 +543,46 @@ public: append(temp); } - /** Insert another list at the beginning of this list. - The other list is cleared. - @param list The other list to insert. - */ + /** + * Insert another list at the beginning of this list. + * The other list is cleared. + * @param list The other list to insert. + */ iterator prepend(List& list) noexcept { return insert(begin(), list); } - /** Append another list at the end of this list. - The other list is cleared. - @param list the other list to append. - */ + /** + * Append another list at the end of this list. + * The other list is cleared. + * @param list the other list to append. + */ iterator append(List& list) noexcept { return insert(end(), list); } - /** Obtain an iterator from an element. - @invariant The element must exist in the list. - @param element The element to obtain an iterator for. - @return An iterator to the element. - */ + /** + * Obtain an iterator from an element. + * @invariant The element must exist in the list. + * @param element The element to obtain an iterator for. + * @return An iterator to the element. + */ iterator iteratorTo(T& element) const noexcept { return iterator(static_cast(&element)); } - /** Obtain a const iterator from an element. - @invariant The element must exist in the list. - @param element The element to obtain an iterator for. - @return A const iterator to the element. - */ + /** + * Obtain a const iterator from an element. + * @invariant The element must exist in the list. + * @param element The element to obtain an iterator for. + * @return A const iterator to the element. + */ [[nodiscard]] const_iterator constIteratorTo(T const& element) const noexcept { diff --git a/include/xrpl/beast/core/LockFreeStack.h b/include/xrpl/beast/core/LockFreeStack.h index dd135d2d98..849edc8fce 100644 --- a/include/xrpl/beast/core/LockFreeStack.h +++ b/include/xrpl/beast/core/LockFreeStack.h @@ -103,18 +103,19 @@ operator!=( //------------------------------------------------------------------------------ -/** Multiple Producer, Multiple Consumer (MPMC) intrusive stack. - - This stack is implemented using the same intrusive interface as List. - All mutations are lock-free. - - The caller is responsible for preventing the "ABA" problem: - http://en.wikipedia.org/wiki/ABA_problem - - @param Tag A type name used to distinguish lists and nodes, for - putting objects in multiple lists. If this parameter is - omitted, the default tag is used. -*/ +/** + * Multiple Producer, Multiple Consumer (MPMC) intrusive stack. + * + * This stack is implemented using the same intrusive interface as List. + * All mutations are lock-free. + * + * The caller is responsible for preventing the "ABA" problem: + * http://en.wikipedia.org/wiki/ABA_problem + * + * @param Tag A type name used to distinguish lists and nodes, for + * putting objects in multiple lists. If this parameter is + * omitted, the default tag is used. + */ template class LockFreeStack { @@ -162,24 +163,27 @@ public: LockFreeStack& operator=(LockFreeStack const&) = delete; - /** Returns true if the stack is empty. */ + /** + * Returns true if the stack is empty. + */ [[nodiscard]] bool empty() const { return head_.load() == &end_; } - /** Push a node onto the stack. - The caller is responsible for preventing the ABA problem. - This operation is lock-free. - Thread safety: - Safe to call from any thread. - - @param node The node to push. - - @return `true` if the stack was previously empty. If multiple threads - are attempting to push, only one will receive `true`. - */ + /** + * Push a node onto the stack. + * The caller is responsible for preventing the ABA problem. + * This operation is lock-free. + * Thread safety: + * Safe to call from any thread. + * + * @param node The node to push. + * + * @return `true` if the stack was previously empty. If multiple threads + * are attempting to push, only one will receive `true`. + */ // VFALCO NOTE Fix this, shouldn't it be a reference like intrusive list? bool pushFront(Node* node) @@ -195,15 +199,16 @@ public: return first; } - /** Pop an element off the stack. - The caller is responsible for preventing the ABA problem. - This operation is lock-free. - Thread safety: - Safe to call from any thread. - - @return The element that was popped, or `nullptr` if the stack - was empty. - */ + /** + * Pop an element off the stack. + * The caller is responsible for preventing the ABA problem. + * This operation is lock-free. + * Thread safety: + * Safe to call from any thread. + * + * @return The element that was popped, or `nullptr` if the stack + * was empty. + */ Element* popFront() { @@ -219,12 +224,13 @@ public: return static_cast(node); } - /** Return a forward iterator to the beginning or end of the stack. - Undefined behavior results if push_front or pop_front is called - while an iteration is in progress. - Thread safety: - Caller is responsible for synchronization. - */ + /** + * Return a forward iterator to the beginning or end of the stack. + * Undefined behavior results if push_front or pop_front is called + * while an iteration is in progress. + * Thread safety: + * Caller is responsible for synchronization. + */ /** @{ */ iterator begin() diff --git a/include/xrpl/beast/core/SemanticVersion.h b/include/xrpl/beast/core/SemanticVersion.h index 826a43d3f8..338942c252 100644 --- a/include/xrpl/beast/core/SemanticVersion.h +++ b/include/xrpl/beast/core/SemanticVersion.h @@ -6,13 +6,14 @@ namespace beast { -/** A Semantic Version number. - - Identifies the build of a particular version of software using - the Semantic Versioning Specification described here: - - http://semver.org/ -*/ +/** + * A Semantic Version number. + * + * Identifies the build of a particular version of software using + * the Semantic Versioning Specification described here: + * + * http://semver.org/ + */ class SemanticVersion { public: @@ -29,14 +30,17 @@ public: SemanticVersion(std::string_view version); - /** Parse a semantic version string. - The parsing is as strict as possible. - @return `true` if the string was parsed. - */ + /** + * Parse a semantic version string. + * The parsing is as strict as possible. + * @return `true` if the string was parsed. + */ bool parse(std::string_view input); - /** Produce a string from semantic version components. */ + /** + * Produce a string from semantic version components. + */ [[nodiscard]] std::string print() const; @@ -52,9 +56,10 @@ public: } }; -/** Compare two SemanticVersions against each other. - The comparison follows the rules as per the specification. -*/ +/** + * Compare two SemanticVersions against each other. + * The comparison follows the rules as per the specification. + */ int compare(SemanticVersion const& lhs, SemanticVersion const& rhs); diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h index 3592ffbfe8..c5374f95e5 100644 --- a/include/xrpl/beast/hash/hash_append.h +++ b/include/xrpl/beast/hash/hash_append.h @@ -135,19 +135,20 @@ struct IsUniquelyRepresented> explicit IsUniquelyRepresented() = default; }; -/** Metafunction returning `true` if the type can be hashed in one call. - - For `IsContiguouslyHashable::value` to be true, then for every - combination of possible values of `T` held in `x` and `y`, - if `x == y`, then it must be true that `memcmp(&x, &y, sizeof(T))` - return 0; i.e. that `x` and `y` are represented by the same bit pattern. - - For example: A two's complement `int` should be contiguously hashable. - Every bit pattern produces a unique value that does not compare equal to - any other bit pattern's value. A IEEE floating point should not be - contiguously hashable because -0. and 0. have different bit patterns, - though they compare equal. -*/ +/** + * Metafunction returning `true` if the type can be hashed in one call. + * + * For `IsContiguouslyHashable::value` to be true, then for every + * combination of possible values of `T` held in `x` and `y`, + * if `x == y`, then it must be true that `memcmp(&x, &y, sizeof(T))` + * return 0; i.e. that `x` and `y` are represented by the same bit pattern. + * + * For example: A two's complement `int` should be contiguously hashable. + * Every bit pattern produces a unique value that does not compare equal to + * any other bit pattern's value. A IEEE floating point should not be + * contiguously hashable because -0. and 0. have different bit patterns, + * though they compare equal. + */ /** @{ */ template struct IsContiguouslyHashable @@ -172,29 +173,30 @@ struct IsContiguouslyHashable //------------------------------------------------------------------------------ -/** Logically concatenate input data to a `Hasher`. - - Hasher requirements: - - `X` is the type `Hasher` - `h` is a value of type `x` - `p` is a value convertible to `void const*` - `n` is a value of type `std::size_t`, greater than zero - - Expression: - `h.append (p, n);` - Throws: - Never - Effect: - Adds the input data to the hasher state. - - Expression: - `static_cast(j)` - Throws: - Never - Effect: - Returns the resulting hash of all the input data. -*/ +/** + * Logically concatenate input data to a `Hasher`. + * + * Hasher requirements: + * + * `X` is the type `Hasher` + * `h` is a value of type `x` + * `p` is a value convertible to `void const*` + * `n` is a value of type `std::size_t`, greater than zero + * + * Expression: + * `h.append (p, n);` + * Throws: + * Never + * Effect: + * Adds the input data to the hasher state. + * + * Expression: + * `static_cast(j)` + * Throws: + * Never + * Effect: + * Returns the resulting hash of all the input data. + */ /** @{ */ // scalars diff --git a/include/xrpl/beast/insight/Collector.h b/include/xrpl/beast/insight/Collector.h index 3f83e329d4..9da2a8bb74 100644 --- a/include/xrpl/beast/insight/Collector.h +++ b/include/xrpl/beast/insight/Collector.h @@ -12,16 +12,17 @@ namespace beast::insight { -/** Interface for a manager that allows collection of metrics. - - To export metrics from a class, pass and save a shared_ptr to this - interface in the class constructor. Create the metric objects - as desired (counters, events, gauges, meters, and an optional hook) - using the interface. - - @see Counter, Event, Gauge, Hook, Meter - @see NullCollector, StatsDCollector -*/ +/** + * Interface for a manager that allows collection of metrics. + * + * To export metrics from a class, pass and save a shared_ptr to this + * interface in the class constructor. Create the metric objects + * as desired (counters, events, gauges, meters, and an optional hook) + * using the interface. + * + * @see Counter, Event, Gauge, Hook, Meter + * @see NullCollector, StatsDCollector + */ class Collector { public: @@ -29,18 +30,19 @@ public: virtual ~Collector() = 0; - /** Create a hook. - - A hook is called at each collection interval, on an implementation - defined thread. This is a convenience facility for gathering metrics - in the polling style. The typical usage is to update all the metrics - of interest in the handler. - - Handler will be called with this signature: - void handler (void) - - @see Hook - */ + /** + * Create a hook. + * + * A hook is called at each collection interval, on an implementation + * defined thread. This is a convenience facility for gathering metrics + * in the polling style. The typical usage is to update all the metrics + * of interest in the handler. + * + * Handler will be called with this signature: + * void handler (void) + * + * @see Hook + */ /** @{ */ template Hook @@ -53,9 +55,10 @@ public: makeHook(HookImpl::HandlerType const& handler) = 0; /** @} */ - /** Create a counter with the specified name. - @see Counter - */ + /** + * Create a counter with the specified name. + * @see Counter + */ /** @{ */ virtual Counter makeCounter(std::string const& name) = 0; @@ -69,9 +72,10 @@ public: } /** @} */ - /** Create an event with the specified name. - @see Event - */ + /** + * Create an event with the specified name. + * @see Event + */ /** @{ */ virtual Event makeEvent(std::string const& name) = 0; @@ -85,9 +89,10 @@ public: } /** @} */ - /** Create a gauge with the specified name. - @see Gauge - */ + /** + * Create a gauge with the specified name. + * @see Gauge + */ /** @{ */ virtual Gauge makeGauge(std::string const& name) = 0; @@ -101,9 +106,10 @@ public: } /** @} */ - /** Create a meter with the specified name. - @see Meter - */ + /** + * Create a meter with the specified name. + * @see Meter + */ /** @{ */ virtual Meter makeMeter(std::string const& name) = 0; diff --git a/include/xrpl/beast/insight/Counter.h b/include/xrpl/beast/insight/Counter.h index 482808b2c7..875fadf33a 100644 --- a/include/xrpl/beast/insight/Counter.h +++ b/include/xrpl/beast/insight/Counter.h @@ -7,34 +7,39 @@ namespace beast::insight { -/** A metric for measuring an integral value. - - A counter is a gauge calculated at the server. The owner of the counter - may increment and decrement the value by an amount. - - This is a lightweight reference wrapper which is cheap to copy and assign. - When the last reference goes away, the metric is no longer collected. -*/ +/** + * A metric for measuring an integral value. + * + * A counter is a gauge calculated at the server. The owner of the counter + * may increment and decrement the value by an amount. + * + * This is a lightweight reference wrapper which is cheap to copy and assign. + * When the last reference goes away, the metric is no longer collected. + */ class Counter final { public: using value_type = CounterImpl::value_type; - /** Create a null metric. - A null metric reports no information. - */ + /** + * Create a null metric. + * A null metric reports no information. + */ Counter() = default; - /** Create the metric reference the specified implementation. - Normally this won't be called directly. Instead, call the appropriate - factory function in the Collector interface. - @see Collector. - */ + /** + * Create the metric reference the specified implementation. + * Normally this won't be called directly. Instead, call the appropriate + * factory function in the Collector interface. + * @see Collector. + */ explicit Counter(std::shared_ptr impl) : impl_(std::move(impl)) { } - /** Increment the counter. */ + /** + * Increment the counter. + */ /** @{ */ void increment(value_type amount) const diff --git a/include/xrpl/beast/insight/Event.h b/include/xrpl/beast/insight/Event.h index afccf9baba..c3ff1a8877 100644 --- a/include/xrpl/beast/insight/Event.h +++ b/include/xrpl/beast/insight/Event.h @@ -8,35 +8,40 @@ namespace beast::insight { -/** A metric for reporting event timing. - - An event is an operation that has an associated millisecond time, or - other integral value. Because events happen at a specific moment, the - metric only supports a push-style interface. - - This is a lightweight reference wrapper which is cheap to copy and assign. - When the last reference goes away, the metric is no longer collected. -*/ +/** + * A metric for reporting event timing. + * + * An event is an operation that has an associated millisecond time, or + * other integral value. Because events happen at a specific moment, the + * metric only supports a push-style interface. + * + * This is a lightweight reference wrapper which is cheap to copy and assign. + * When the last reference goes away, the metric is no longer collected. + */ class Event final { public: using value_type = EventImpl::value_type; - /** Create a null metric. - A null metric reports no information. - */ + /** + * Create a null metric. + * A null metric reports no information. + */ Event() = default; - /** Create the metric reference the specified implementation. - Normally this won't be called directly. Instead, call the appropriate - factory function in the Collector interface. - @see Collector. - */ + /** + * Create the metric reference the specified implementation. + * Normally this won't be called directly. Instead, call the appropriate + * factory function in the Collector interface. + * @see Collector. + */ explicit Event(std::shared_ptr impl) : impl_(std::move(impl)) { } - /** Push an event notification. */ + /** + * Push an event notification. + */ template void notify(std::chrono::duration const& value) const diff --git a/include/xrpl/beast/insight/Gauge.h b/include/xrpl/beast/insight/Gauge.h index 9a23ea6665..ef62e252b3 100644 --- a/include/xrpl/beast/insight/Gauge.h +++ b/include/xrpl/beast/insight/Gauge.h @@ -7,40 +7,44 @@ namespace beast::insight { -/** A metric for measuring an integral value. - - A gauge is an instantaneous measurement of a value, like the gas gauge - in a car. The caller directly sets the value, or adjusts it by a - specified amount. The value is kept in the client rather than the collector. - - This is a lightweight reference wrapper which is cheap to copy and assign. - When the last reference goes away, the metric is no longer collected. -*/ +/** + * A metric for measuring an integral value. + * + * A gauge is an instantaneous measurement of a value, like the gas gauge + * in a car. The caller directly sets the value, or adjusts it by a + * specified amount. The value is kept in the client rather than the collector. + * + * This is a lightweight reference wrapper which is cheap to copy and assign. + * When the last reference goes away, the metric is no longer collected. + */ class Gauge final { public: using value_type = GaugeImpl::value_type; using difference_type = GaugeImpl::difference_type; - /** Create a null metric. - A null metric reports no information. - */ + /** + * Create a null metric. + * A null metric reports no information. + */ Gauge() = default; - /** Create the metric reference the specified implementation. - Normally this won't be called directly. Instead, call the appropriate - factory function in the Collector interface. - @see Collector. - */ + /** + * Create the metric reference the specified implementation. + * Normally this won't be called directly. Instead, call the appropriate + * factory function in the Collector interface. + * @see Collector. + */ explicit Gauge(std::shared_ptr impl) : impl_(std::move(impl)) { } - /** Set the value on the gauge. - A Collector implementation should combine multiple calls to value - changes into a single change if the calls occur within a single - collection interval. - */ + /** + * Set the value on the gauge. + * A Collector implementation should combine multiple calls to value + * changes into a single change if the calls occur within a single + * collection interval. + */ /** @{ */ void set(value_type value) const @@ -62,7 +66,9 @@ public: } /** @} */ - /** Adjust the value of the gauge. */ + /** + * Adjust the value of the gauge. + */ /** @{ */ void increment(difference_type amount) const diff --git a/include/xrpl/beast/insight/Group.h b/include/xrpl/beast/insight/Group.h index 3e0eb93452..ecf7709546 100644 --- a/include/xrpl/beast/insight/Group.h +++ b/include/xrpl/beast/insight/Group.h @@ -7,13 +7,17 @@ namespace beast::insight { -/** A collector front-end that manages a group of metrics. */ +/** + * A collector front-end that manages a group of metrics. + */ class Group : public Collector { public: using ptr = std::shared_ptr; - /** Returns the name of this group, for diagnostics. */ + /** + * Returns the name of this group, for diagnostics. + */ [[nodiscard]] virtual std::string const& name() const = 0; }; diff --git a/include/xrpl/beast/insight/Groups.h b/include/xrpl/beast/insight/Groups.h index cfe4d99bdc..77fc2d3336 100644 --- a/include/xrpl/beast/insight/Groups.h +++ b/include/xrpl/beast/insight/Groups.h @@ -8,13 +8,17 @@ namespace beast::insight { -/** A container for managing a set of metric groups. */ +/** + * A container for managing a set of metric groups. + */ class Groups { public: virtual ~Groups() = 0; - /** Find or create a new collector with a given name. */ + /** + * Find or create a new collector with a given name. + */ /** @{ */ virtual Group::ptr const& get(std::string const& name) = 0; @@ -27,7 +31,9 @@ public: /** @} */ }; -/** Create a group container that uses the specified collector. */ +/** + * Create a group container that uses the specified collector. + */ std::unique_ptr makeGroups(Collector::ptr const& collector); diff --git a/include/xrpl/beast/insight/Hook.h b/include/xrpl/beast/insight/Hook.h index 8dbe5a4be0..572a9ffcb4 100644 --- a/include/xrpl/beast/insight/Hook.h +++ b/include/xrpl/beast/insight/Hook.h @@ -7,20 +7,24 @@ namespace beast::insight { -/** A reference to a handler for performing polled collection. */ +/** + * A reference to a handler for performing polled collection. + */ class Hook final { public: - /** Create a null hook. - A null hook has no associated handler. - */ + /** + * Create a null hook. + * A null hook has no associated handler. + */ Hook() = default; - /** Create a hook referencing the specified implementation. - Normally this won't be called directly. Instead, call the appropriate - factory function in the Collector interface. - @see Collector. - */ + /** + * Create a hook referencing the specified implementation. + * Normally this won't be called directly. Instead, call the appropriate + * factory function in the Collector interface. + * @see Collector. + */ explicit Hook(std::shared_ptr impl) : impl_(std::move(impl)) { } diff --git a/include/xrpl/beast/insight/Meter.h b/include/xrpl/beast/insight/Meter.h index 25ffabd928..ac2f3a352c 100644 --- a/include/xrpl/beast/insight/Meter.h +++ b/include/xrpl/beast/insight/Meter.h @@ -7,33 +7,38 @@ namespace beast::insight { -/** A metric for measuring an integral value. - - A meter may be thought of as an increment-only counter. - - This is a lightweight reference wrapper which is cheap to copy and assign. - When the last reference goes away, the metric is no longer collected. -*/ +/** + * A metric for measuring an integral value. + * + * A meter may be thought of as an increment-only counter. + * + * This is a lightweight reference wrapper which is cheap to copy and assign. + * When the last reference goes away, the metric is no longer collected. + */ class Meter final { public: using value_type = MeterImpl::value_type; - /** Create a null metric. - A null metric reports no information. - */ + /** + * Create a null metric. + * A null metric reports no information. + */ Meter() = default; - /** Create the metric reference the specified implementation. - Normally this won't be called directly. Instead, call the appropriate - factory function in the Collector interface. - @see Collector. - */ + /** + * Create the metric reference the specified implementation. + * Normally this won't be called directly. Instead, call the appropriate + * factory function in the Collector interface. + * @see Collector. + */ explicit Meter(std::shared_ptr impl) : impl_(std::move(impl)) { } - /** Increment the meter. */ + /** + * Increment the meter. + */ /** @{ */ void increment(value_type amount) const diff --git a/include/xrpl/beast/insight/NullCollector.h b/include/xrpl/beast/insight/NullCollector.h index 67903420fa..ffafe6d6d5 100644 --- a/include/xrpl/beast/insight/NullCollector.h +++ b/include/xrpl/beast/insight/NullCollector.h @@ -6,7 +6,9 @@ namespace beast::insight { -/** A Collector which does not collect metrics. */ +/** + * A Collector which does not collect metrics. + */ class NullCollector : public Collector { public: diff --git a/include/xrpl/beast/insight/StatsDCollector.h b/include/xrpl/beast/insight/StatsDCollector.h index 9a438c48f1..e14d3a27ff 100644 --- a/include/xrpl/beast/insight/StatsDCollector.h +++ b/include/xrpl/beast/insight/StatsDCollector.h @@ -9,20 +9,22 @@ namespace beast::insight { -/** A Collector that reports metrics to a StatsD server. - Reference: - https://github.com/b/statsd_spec -*/ +/** + * A Collector that reports metrics to a StatsD server. + * Reference: + * https://github.com/b/statsd_spec + */ class StatsDCollector : public Collector { public: explicit StatsDCollector() = default; - /** Create a StatsD collector. - @param address The IP address and port of the StatsD server. - @param prefix A string pre-pended before each metric name. - @param journal Destination for logging output. - */ + /** + * Create a StatsD collector. + * @param address The IP address and port of the StatsD server. + * @param prefix A string pre-pended before each metric name. + * @param journal Destination for logging output. + */ static std::shared_ptr make(IP::Endpoint const& address, std::string const& prefix, Journal journal); }; diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index f4327b7b8a..4f4fb189a6 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -19,42 +19,54 @@ namespace IP { using Address = boost::asio::ip::address; -/** Returns the address represented as a string. */ +/** + * Returns the address represented as a string. + */ inline std::string to_string(Address const& addr) { return addr.to_string(); } -/** Returns `true` if this is a loopback address. */ +/** + * Returns `true` if this is a loopback address. + */ inline bool isLoopback(Address const& addr) { return addr.is_loopback(); } -/** Returns `true` if the address is unspecified. */ +/** + * Returns `true` if the address is unspecified. + */ inline bool isUnspecified(Address const& addr) { return addr.is_unspecified(); } -/** Returns `true` if the address is a multicast address. */ +/** + * Returns `true` if the address is a multicast address. + */ inline bool isMulticast(Address const& addr) { return addr.is_multicast(); } -/** Returns `true` if the address is a private unroutable address. */ +/** + * Returns `true` if the address is a private unroutable address. + */ inline bool isPrivate(Address const& addr) { return (addr.is_v4()) ? isPrivate(addr.to_v4()) : isPrivate(addr.to_v6()); } -/** Returns `true` if the address is a public routable address. */ +/** + * Returns `true` if the address is a public routable address. + */ inline bool isPublic(Address const& addr) { diff --git a/include/xrpl/beast/net/IPAddressConversion.h b/include/xrpl/beast/net/IPAddressConversion.h index b5fb697233..73777cf841 100644 --- a/include/xrpl/beast/net/IPAddressConversion.h +++ b/include/xrpl/beast/net/IPAddressConversion.h @@ -6,23 +6,29 @@ namespace beast::IP { -/** Convert to Endpoint. - The port is set to zero. -*/ +/** + * Convert to Endpoint. + * The port is set to zero. + */ Endpoint fromAsio(boost::asio::ip::address const& address); -/** Convert to Endpoint. */ +/** + * Convert to Endpoint. + */ Endpoint fromAsio(boost::asio::ip::tcp::endpoint const& endpoint); -/** Convert to asio::ip::address. - The port is ignored. -*/ +/** + * Convert to asio::ip::address. + * The port is ignored. + */ boost::asio::ip::address toAsioAddress(Endpoint const& endpoint); -/** Convert to asio::ip::tcp::endpoint. */ +/** + * Convert to asio::ip::tcp::endpoint. + */ boost::asio::ip::tcp::endpoint toAsioEndpoint(Endpoint const& endpoint); diff --git a/include/xrpl/beast/net/IPAddressV4.h b/include/xrpl/beast/net/IPAddressV4.h index 9367fbe1eb..94943af3ea 100644 --- a/include/xrpl/beast/net/IPAddressV4.h +++ b/include/xrpl/beast/net/IPAddressV4.h @@ -6,17 +6,22 @@ namespace beast::IP { using AddressV4 = boost::asio::ip::address_v4; -/** Returns `true` if the address is a private unroutable address. */ +/** + * Returns `true` if the address is a private unroutable address. + */ bool isPrivate(AddressV4 const& addr); -/** Returns `true` if the address is a public routable address. */ +/** + * Returns `true` if the address is a public routable address. + */ bool isPublic(AddressV4 const& addr); -/** Returns the address class for the given address. - @note Class 'D' represents multicast addresses (224.*.*.*). -*/ +/** + * Returns the address class for the given address. + * @note Class 'D' represents multicast addresses (224.*.*.*). + */ char getClass(AddressV4 const& address); diff --git a/include/xrpl/beast/net/IPAddressV6.h b/include/xrpl/beast/net/IPAddressV6.h index 1bfa079990..b51cb62532 100644 --- a/include/xrpl/beast/net/IPAddressV6.h +++ b/include/xrpl/beast/net/IPAddressV6.h @@ -6,11 +6,15 @@ namespace beast::IP { using AddressV6 = boost::asio::ip::address_v6; -/** Returns `true` if the address is a private unroutable address. */ +/** + * Returns `true` if the address is a private unroutable address. + */ bool isPrivate(AddressV6 const& addr); -/** Returns `true` if the address is a public routable address. */ +/** + * Returns `true` if the address is a public routable address. + */ bool isPublic(AddressV6 const& addr); diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index 0b661108f2..c4b269e9c3 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -17,51 +17,68 @@ namespace beast::IP { using Port = std::uint16_t; -/** A version-independent IP address and port combination. */ +/** + * A version-independent IP address and port combination. + */ class Endpoint { public: - /** Create an unspecified endpoint. */ + /** + * Create an unspecified endpoint. + */ Endpoint(); - /** Create an endpoint from the address and optional port. */ + /** + * Create an endpoint from the address and optional port. + */ explicit Endpoint(Address addr, Port port = 0); - /** Create an Endpoint from a string. - If the port is omitted, the endpoint will have a zero port. - @return An optional endpoint; will be `std::nullopt` on failure - */ + /** + * Create an Endpoint from a string. + * If the port is omitted, the endpoint will have a zero port. + * @return An optional endpoint; will be `std::nullopt` on failure + */ static std::optional fromStringChecked(std::string const& s); static Endpoint fromString(std::string const& s); - /** Returns a string representing the endpoint. */ + /** + * Returns a string representing the endpoint. + */ [[nodiscard]] std::string toString() const; - /** Returns the port number on the endpoint. */ + /** + * Returns the port number on the endpoint. + */ [[nodiscard]] Port port() const { return port_; } - /** Returns a new Endpoint with a different port. */ + /** + * Returns a new Endpoint with a different port. + */ [[nodiscard]] Endpoint atPort(Port port) const { return Endpoint(addr_, port); } - /** Returns the address portion of this endpoint. */ + /** + * Returns the address portion of this endpoint. + */ [[nodiscard]] Address const& address() const { return addr_; } - /** Convenience accessors for the address part. */ + /** + * Convenience accessors for the address part. + */ /** @{ */ [[nodiscard]] bool isV4() const @@ -85,7 +102,9 @@ public: } /** @} */ - /** Arithmetic comparison. */ + /** + * Arithmetic comparison. + */ /** @{ */ friend bool operator==(Endpoint const& lhs, Endpoint const& rhs); @@ -131,35 +150,45 @@ private: // Properties -/** Returns `true` if the endpoint is a loopback address. */ +/** + * Returns `true` if the endpoint is a loopback address. + */ inline bool isLoopback(Endpoint const& endpoint) { return isLoopback(endpoint.address()); } -/** Returns `true` if the endpoint is unspecified. */ +/** + * Returns `true` if the endpoint is unspecified. + */ inline bool isUnspecified(Endpoint const& endpoint) { return isUnspecified(endpoint.address()); } -/** Returns `true` if the endpoint is a multicast address. */ +/** + * Returns `true` if the endpoint is a multicast address. + */ inline bool isMulticast(Endpoint const& endpoint) { return isMulticast(endpoint.address()); } -/** Returns `true` if the endpoint is a private unroutable address. */ +/** + * Returns `true` if the endpoint is a private unroutable address. + */ inline bool isPrivate(Endpoint const& endpoint) { return isPrivate(endpoint.address()); } -/** Returns `true` if the endpoint is a public routable address. */ +/** + * Returns `true` if the endpoint is a public routable address. + */ inline bool isPublic(Endpoint const& endpoint) { @@ -168,14 +197,18 @@ isPublic(Endpoint const& endpoint) //------------------------------------------------------------------------------ -/** Returns the endpoint represented as a string. */ +/** + * Returns the endpoint represented as a string. + */ inline std::string to_string(Endpoint const& endpoint) { return endpoint.toString(); } -/** Output stream conversion. */ +/** + * Output stream conversion. + */ template OutputStream& operator<<(OutputStream& os, Endpoint const& endpoint) @@ -184,7 +217,9 @@ operator<<(OutputStream& os, Endpoint const& endpoint) return os; } -/** Input stream conversion. */ +/** + * Input stream conversion. + */ std::istream& operator>>(std::istream& is, Endpoint& endpoint); @@ -193,7 +228,9 @@ operator>>(std::istream& is, Endpoint& endpoint); //------------------------------------------------------------------------------ namespace std { -/** std::hash support. */ +/** + * std::hash support. + */ template <> struct hash<::beast::IP::Endpoint> { @@ -208,7 +245,9 @@ struct hash<::beast::IP::Endpoint> } // namespace std namespace boost { -/** boost::hash support. */ +/** + * boost::hash support. + */ template <> struct hash<::beast::IP::Endpoint> { diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h index 7c681ab140..1986568553 100644 --- a/include/xrpl/beast/rfc2616.h +++ b/include/xrpl/beast/rfc2616.h @@ -30,17 +30,20 @@ struct CiEqualPred } }; -/** Returns `true` if `c` is linear white space. - - This excludes the CRLF sequence allowed for line continuations. -*/ +/** + * Returns `true` if `c` is linear white space. + * + * This excludes the CRLF sequence allowed for line continuations. + */ inline bool isLws(char c) { return c == ' ' || c == '\t'; } -/** Returns `true` if `c` is any whitespace character. */ +/** + * Returns `true` if `c` is any whitespace character. + */ inline bool isWhite(char c) { @@ -87,14 +90,15 @@ trimRight(String const& s) } // namespace detail -/** Parse a character sequence of values separated by commas. - Double quotes and escape sequences will be converted. Excess white - space, commas, double quotes, and empty elements are not copied. - Format: - #(token|quoted-string) - Reference: - http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2 -*/ +/** + * Parse a character sequence of values separated by commas. + * Double quotes and escape sequences will be converted. Excess white + * space, commas, double quotes, and empty elements are not copied. + * Format: + * #(token|quoted-string) + * Reference: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2 + */ template < class FwdIt, class Result = std::vector::value_type>>, @@ -189,14 +193,15 @@ splitCommas(boost::beast::string_view const& s) //------------------------------------------------------------------------------ -/** Iterates through a comma separated list. - - Meets the requirements of ForwardIterator. - - List defined in rfc2616 2.1. - - @note Values returned may contain backslash escapes. -*/ +/** + * Iterates through a comma separated list. + * + * Meets the requirements of ForwardIterator. + * + * List defined in rfc2616 2.1. + * + * @note Values returned may contain backslash escapes. + */ class ListIterator { using iter_type = boost::string_ref::const_iterator; @@ -323,17 +328,20 @@ ListIterator::increment() } } } -/** Returns true if two strings are equal. - - A case-insensitive comparison is used. -*/ +/** + * Returns true if two strings are equal. + * + * A case-insensitive comparison is used. + */ inline bool ciEqual(boost::string_ref s1, boost::string_ref s2) { return boost::range::equal(s1, s2, detail::CiEqualPred{}); } -/** Returns a range representing the list. */ +/** + * Returns a range representing the list. + */ inline boost::iterator_range makeList(boost::string_ref const& field) { @@ -341,20 +349,22 @@ makeList(boost::string_ref const& field) ListIterator{field.begin(), field.end()}, ListIterator{field.end(), field.end()}}; } -/** Returns true if the specified token exists in the list. - - A case-insensitive comparison is used. -*/ +/** + * Returns true if the specified token exists in the list. + * + * A case-insensitive comparison is used. + */ template bool tokenInList(boost::string_ref const& value, boost::string_ref const& token) { - for (auto const& item : makeList(value)) - { - if (ciEqual(item, token)) - return true; - } - return false; + auto const list = makeList(value); + // ListIterator is not default-constructible, so it does not model a std::ranges + // sentinel/range; the classic std::any_of (which only needs an input iterator) + // is used instead. + // NOLINTNEXTLINE(modernize-use-ranges) + return std::any_of( + list.begin(), list.end(), [&token](auto const& item) { return ciEqual(item, token); }); } template diff --git a/include/xrpl/beast/test/yield_to.h b/include/xrpl/beast/test/yield_to.h index 1a34ec436e..b3aa482dd5 100644 --- a/include/xrpl/beast/test/yield_to.h +++ b/include/xrpl/beast/test/yield_to.h @@ -19,12 +19,13 @@ namespace beast::test { -/** Mix-in to support tests using asio coroutines. - - Derive from this class and use yield_to to launch test - functions inside coroutines. This is handy for testing - asynchronous asio code. -*/ +/** + * Mix-in to support tests using asio coroutines. + * + * Derive from this class and use yield_to to launch test + * functions inside coroutines. This is handy for testing + * asynchronous asio code. + */ class EnableYieldTo { protected: @@ -38,7 +39,9 @@ private: std::size_t running_ = 0; public: - /// The type of yield context passed to functions. + /** + * The type of yield context passed to functions. + */ using yield_context = boost::asio::yield_context; explicit EnableYieldTo(std::size_t concurrency = 1) : work_(boost::asio::make_work_guard(ios_)) @@ -57,24 +60,27 @@ public: t.join(); } - /// Return the `io_context` associated with the object + /** + * Return the `io_context` associated with the object + */ boost::asio::io_context& getIoContext() { return ios_; } - /** Run one or more functions, each in a coroutine. - - This call will block until all coroutines terminate. - - Each functions should have this signature: - @code - void f(yield_context); - @endcode - - @param fn... One or more functions to invoke. - */ + /** + * Run one or more functions, each in a coroutine. + * + * This call will block until all coroutines terminate. + * + * Each functions should have this signature: + * @code + * void f(yield_context); + * @endcode + * + * @param fn... One or more functions to invoke. + */ #if BEAST_DOXYGEN template void diff --git a/include/xrpl/beast/type_name.h b/include/xrpl/beast/type_name.h index ae7b681af9..85fd9ae6a2 100644 --- a/include/xrpl/beast/type_name.h +++ b/include/xrpl/beast/type_name.h @@ -23,6 +23,7 @@ typeName() if (auto s = abi::__cxa_demangle(name.c_str(), nullptr, nullptr, nullptr)) { name = s; + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) std::free(s); } #endif diff --git a/include/xrpl/beast/unit_test/amount.h b/include/xrpl/beast/unit_test/amount.h index 3a392f393f..c1e4357753 100644 --- a/include/xrpl/beast/unit_test/amount.h +++ b/include/xrpl/beast/unit_test/amount.h @@ -10,7 +10,9 @@ namespace beast::unit_test { -/** Utility for producing nicely composed output of amounts with units. */ +/** + * Utility for producing nicely composed output of amounts with units. + */ class Amount { private: diff --git a/include/xrpl/beast/unit_test/detail/const_container.h b/include/xrpl/beast/unit_test/detail/const_container.h index 6826bf4258..9f4646cbdb 100644 --- a/include/xrpl/beast/unit_test/detail/const_container.h +++ b/include/xrpl/beast/unit_test/detail/const_container.h @@ -6,10 +6,11 @@ namespace beast::unit_test::detail { -/** Adapter to constrain a container interface. - The interface allows for limited read only operations. Derived classes - provide additional behavior. -*/ +/** + * Adapter to constrain a container interface. + * The interface allows for limited read only operations. Derived classes + * provide additional behavior. + */ template class ConstContainer { @@ -38,21 +39,27 @@ public: using iterator = cont_type::const_iterator; using const_iterator = cont_type::const_iterator; - /** Returns `true` if the container is empty. */ + /** + * Returns `true` if the container is empty. + */ [[nodiscard]] bool empty() const { return cont_.empty(); } - /** Returns the number of items in the container. */ + /** + * Returns the number of items in the container. + */ [[nodiscard]] size_type size() const { return cont_.size(); } - /** Returns forward iterators for traversal. */ + /** + * Returns forward iterators for traversal. + */ /** @{ */ [[nodiscard]] const_iterator begin() const diff --git a/include/xrpl/beast/unit_test/global_suites.h b/include/xrpl/beast/unit_test/global_suites.h index 72ed738bdb..18e5bc3a6b 100644 --- a/include/xrpl/beast/unit_test/global_suites.h +++ b/include/xrpl/beast/unit_test/global_suites.h @@ -10,7 +10,9 @@ namespace beast::unit_test { namespace detail { -/// Holds test suites registered during static initialization. +/** + * Holds test suites registered during static initialization. + */ inline SuiteList& globalSuites() { @@ -34,7 +36,9 @@ struct InsertSuite } // namespace detail -/// Holds test suites registered during static initialization. +/** + * Holds test suites registered during static initialization. + */ inline SuiteList const& globalSuites() { diff --git a/include/xrpl/beast/unit_test/match.h b/include/xrpl/beast/unit_test/match.h index 5faeaa1100..966574b833 100644 --- a/include/xrpl/beast/unit_test/match.h +++ b/include/xrpl/beast/unit_test/match.h @@ -121,42 +121,48 @@ Selector::operator()(SuiteInfo const& s) // Utility functions for producing predicates to select suites. -/** Returns a predicate that implements a smart matching rule. - The predicate checks the suite, module, and library fields of the - SuiteInfo in that order. When it finds a match, it changes modes - depending on what was found: - - If a suite is matched first, then only the suite is selected. The - suite may be marked manual. - - If a module is matched first, then only suites from that module - and library not marked manual are selected from then on. - - If a library is matched first, then only suites from that library - not marked manual are selected from then on. - -*/ +/** + * Returns a predicate that implements a smart matching rule. + * The predicate checks the suite, module, and library fields of the + * SuiteInfo in that order. When it finds a match, it changes modes + * depending on what was found: + * + * If a suite is matched first, then only the suite is selected. The + * suite may be marked manual. + * + * If a module is matched first, then only suites from that module + * and library not marked manual are selected from then on. + * + * If a library is matched first, then only suites from that library + * not marked manual are selected from then on. + */ inline Selector matchAuto(std::string const& name) { return Selector(Selector::ModeT::Automatch, name); } -/** Return a predicate that matches all suites not marked manual. */ +/** + * Return a predicate that matches all suites not marked manual. + */ inline Selector matchAll() { return Selector(Selector::ModeT::All); } -/** Returns a predicate that matches a specific suite. */ +/** + * Returns a predicate that matches a specific suite. + */ inline Selector matchSuite(std::string const& name) { return Selector(Selector::ModeT::Suite, name); } -/** Returns a predicate that matches all suites in a library. */ +/** + * Returns a predicate that matches all suites in a library. + */ inline Selector matchLibrary(std::string const& name) { diff --git a/include/xrpl/beast/unit_test/recorder.h b/include/xrpl/beast/unit_test/recorder.h index 1b7347dc2e..fcadb63fc9 100644 --- a/include/xrpl/beast/unit_test/recorder.h +++ b/include/xrpl/beast/unit_test/recorder.h @@ -13,7 +13,9 @@ namespace beast::unit_test { -/** A test runner that stores the results. */ +/** + * A test runner that stores the results. + */ class Recorder : public Runner { private: @@ -24,7 +26,9 @@ private: public: Recorder() = default; - /** Returns a report with the results of all completed suites. */ + /** + * Returns a report with the results of all completed suites. + */ [[nodiscard]] Results const& report() const { diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h index a903d9f8c2..0fe77a7862 100644 --- a/include/xrpl/beast/unit_test/reporter.h +++ b/include/xrpl/beast/unit_test/reporter.h @@ -25,9 +25,10 @@ namespace beast::unit_test { namespace detail { -/** A simple test runner that writes everything to a stream in real time. - The totals are output when the object is destroyed. -*/ +/** + * A simple test runner that writes everything to a stream in real time. + * The totals are output when the object is destroyed. + */ template class Reporter : public Runner { diff --git a/include/xrpl/beast/unit_test/results.h b/include/xrpl/beast/unit_test/results.h index 718d764c9f..273ad5b129 100644 --- a/include/xrpl/beast/unit_test/results.h +++ b/include/xrpl/beast/unit_test/results.h @@ -13,11 +13,15 @@ namespace beast::unit_test { -/** Holds a set of test condition outcomes in a testcase. */ +/** + * Holds a set of test condition outcomes in a testcase. + */ class CaseResults { public: - /** Holds the result of evaluating one test condition. */ + /** + * Holds the result of evaluating one test condition. + */ struct Test { explicit Test(bool pass) : pass(pass) @@ -41,28 +45,36 @@ private: public: TestsT() = default; - /** Returns the total number of test conditions. */ + /** + * Returns the total number of test conditions. + */ [[nodiscard]] std::size_t total() const { return cont().size(); } - /** Returns the number of failed test conditions. */ + /** + * Returns the number of failed test conditions. + */ [[nodiscard]] std::size_t failed() const { return failed_; } - /** Register a successful test condition. */ + /** + * Register a successful test condition. + */ void pass() { cont().emplace_back(true); } - /** Register a failed test condition. */ + /** + * Register a failed test condition. + */ void fail(std::string const& reason = "") { @@ -74,7 +86,9 @@ private: class LogT : public detail::ConstContainer> { public: - /** Insert a string into the log. */ + /** + * Insert a string into the log. + */ void insert(std::string const& s) { @@ -89,23 +103,31 @@ public: { } - /** Returns the name of this testcase. */ + /** + * Returns the name of this testcase. + */ [[nodiscard]] std::string const& name() const { return name_; } - /** Memberspace for a container of test condition outcomes. */ + /** + * Memberspace for a container of test condition outcomes. + */ TestsT tests; - /** Memberspace for a container of testcase log messages. */ + /** + * Memberspace for a container of testcase log messages. + */ LogT log; }; //-------------------------------------------------------------------------- -/** Holds the set of testcase results in a suite. */ +/** + * Holds the set of testcase results in a suite. + */ class SuiteResults : public detail::ConstContainer> { private: @@ -118,28 +140,36 @@ public: { } - /** Returns the name of this suite. */ + /** + * Returns the name of this suite. + */ [[nodiscard]] std::string const& name() const { return name_; } - /** Returns the total number of test conditions. */ + /** + * Returns the total number of test conditions. + */ [[nodiscard]] std::size_t total() const { return total_; } - /** Returns the number of failures. */ + /** + * Returns the number of failures. + */ [[nodiscard]] std::size_t failed() const { return failed_; } - /** Insert a set of testcase results. */ + /** + * Insert a set of testcase results. + */ /** @{ */ void insert(CaseResults&& r) @@ -162,7 +192,9 @@ public: //------------------------------------------------------------------------------ // VFALCO TODO Make this a template class using scoped allocators -/** Holds the results of running a set of testsuites. */ +/** + * Holds the results of running a set of testsuites. + */ class Results : public detail::ConstContainer> { private: @@ -173,28 +205,36 @@ private: public: Results() = default; - /** Returns the total number of test cases. */ + /** + * Returns the total number of test cases. + */ [[nodiscard]] std::size_t cases() const { return cases_; } - /** Returns the total number of test conditions. */ + /** + * Returns the total number of test conditions. + */ [[nodiscard]] std::size_t total() const { return total_; } - /** Returns the number of failures. */ + /** + * Returns the number of failures. + */ [[nodiscard]] std::size_t failed() const { return failed_; } - /** Insert a set of suite results. */ + /** + * Insert a set of suite results. + */ /** @{ */ void insert(SuiteResults&& r) diff --git a/include/xrpl/beast/unit_test/runner.h b/include/xrpl/beast/unit_test/runner.h index b88bfc5fe1..f8f9deca48 100644 --- a/include/xrpl/beast/unit_test/runner.h +++ b/include/xrpl/beast/unit_test/runner.h @@ -13,11 +13,12 @@ namespace beast::unit_test { -/** Unit test runner interface. - - Derived classes can customize the reporting behavior. This interface is - injected into the unit_test class to receive the results of the tests. -*/ +/** + * Unit test runner interface. + * + * Derived classes can customize the reporting behavior. This interface is + * injected into the unit_test class to receive the results of the tests. + */ class Runner { std::string arg_; @@ -33,110 +34,132 @@ public: Runner& operator=(Runner const&) = delete; - /** Set the argument string. - - The argument string is available to suites and - allows for customization of the test. Each suite - defines its own syntax for the argument string. - The same argument is passed to all suites. - */ + /** + * Set the argument string. + * + * The argument string is available to suites and + * allows for customization of the test. Each suite + * defines its own syntax for the argument string. + * The same argument is passed to all suites. + */ void arg(std::string const& s) { arg_ = s; } - /** Returns the argument string. */ + /** + * Returns the argument string. + */ [[nodiscard]] std::string const& arg() const { return arg_; } - /** Run the specified suite. - @return `true` if any conditions failed. - */ + /** + * Run the specified suite. + * @return `true` if any conditions failed. + */ template bool run(SuiteInfo const& s); - /** Run a sequence of suites. - The expression - `FwdIter::value_type` - must be convertible to `SuiteInfo`. - @return `true` if any conditions failed. - */ + /** + * Run a sequence of suites. + * The expression + * `FwdIter::value_type` + * must be convertible to `SuiteInfo`. + * @return `true` if any conditions failed. + */ template bool run(FwdIter first, FwdIter last); - /** Conditionally run a sequence of suites. - pred will be called as: - @code - bool pred(SuiteInfo const&); - @endcode - @return `true` if any conditions failed. - */ + /** + * Conditionally run a sequence of suites. + * pred will be called as: + * @code + * bool pred(SuiteInfo const&); + * @endcode + * @return `true` if any conditions failed. + */ template bool runIf(FwdIter first, FwdIter last, Pred pred = Pred{}); - /** Run all suites in a container. - @return `true` if any conditions failed. - */ + /** + * Run all suites in a container. + * @return `true` if any conditions failed. + */ template bool runEach(SequenceContainer const& c); - /** Conditionally run suites in a container. - pred will be called as: - @code - bool pred(SuiteInfo const&); - @endcode - @return `true` if any conditions failed. - */ + /** + * Conditionally run suites in a container. + * pred will be called as: + * @code + * bool pred(SuiteInfo const&); + * @endcode + * @return `true` if any conditions failed. + */ template bool runEachIf(SequenceContainer const& c, Pred pred = Pred{}); protected: - /// Called when a new suite starts. + /** + * Called when a new suite starts. + */ virtual void onSuiteBegin(SuiteInfo const&) { } - /// Called when a suite ends. + /** + * Called when a suite ends. + */ virtual void onSuiteEnd() { } - /// Called when a new case starts. + /** + * Called when a new case starts. + */ virtual void onCaseBegin(std::string const&) { } - /// Called when a new case ends. + /** + * Called when a new case ends. + */ virtual void onCaseEnd() { } - /// Called for each passing condition. + /** + * Called for each passing condition. + */ virtual void onPass() { } - /// Called for each failing condition. + /** + * Called for each failing condition. + */ virtual void onFail(std::string const&) { } - /// Called when a test logs output. + /** + * Called when a test logs output. + */ virtual void onLog(std::string const&) { diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index 487663fcc5..c20fe2522c 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -41,13 +41,14 @@ class Thread; enum class AbortT { NoAbortOnFail, AbortOnFail }; -/** A testsuite class. - - Derived classes execute a series of testcases, where each testcase is - a series of pass/fail tests. To provide a unit test using this class, - derive from it and use the BEAST_DEFINE_UNIT_TEST macro in a - translation unit. -*/ +/** + * A testsuite class. + * + * Derived classes execute a series of testcases, where each testcase is + * a series of pass/fail tests. To provide a unit test using this class, + * derive from it and use the BEAST_DEFINE_UNIT_TEST macro in a + * translation unit. + */ class Suite { private: @@ -118,16 +119,17 @@ private: { } - /** Open a new testcase. - - A testcase is a series of evaluated test conditions. A test - suite may have multiple test cases. A test is associated with - the last opened testcase. When the test first runs, a default - unnamed case is opened. Tests with only one case may omit the - call to testcase. - - @param abort Determines if suite continues running after a failure. - */ + /** + * Open a new testcase. + * + * A testcase is a series of evaluated test conditions. A test + * suite may have multiple test cases. A test is associated with + * the last opened testcase. When the test first runs, a default + * unnamed case is opened. Tests with only one case may omit the + * call to testcase. + * + * @param abort Determines if suite continues running after a failure. + */ void operator()(std::string const& name, AbortT abort = AbortT::NoAbortOnFail); @@ -140,19 +142,23 @@ private: }; public: - /** Logging output stream. - - Text sent to the log output stream will be forwarded to - the output stream associated with the runner. - */ + /** + * Logging output stream. + * + * Text sent to the log output stream will be forwarded to + * the output stream associated with the runner. + */ LogOs log; - /** Memberspace for declaring test cases. */ + /** + * Memberspace for declaring test cases. + */ TestcaseT testcase; - /** Returns the "current" running suite. - If no suite is running, nullptr is returned. - */ + /** + * Returns the "current" running suite. + * If no suite is running, nullptr is returned. + */ static Suite* thisSuite() { @@ -168,30 +174,34 @@ public: Suite& operator=(Suite const&) = delete; - /** Invokes the test using the specified runner. - - Data members are set up here instead of the constructor as a - convenience to writing the derived class to avoid repetition of - forwarded constructor arguments to the base. - Normally this is called by the framework for you. - */ + /** + * Invokes the test using the specified runner. + * + * Data members are set up here instead of the constructor as a + * convenience to writing the derived class to avoid repetition of + * forwarded constructor arguments to the base. + * Normally this is called by the framework for you. + */ template void operator()(Runner& r); - /** Record a successful test condition. */ + /** + * Record a successful test condition. + */ template void pass(); - /** Record a failure. - - @param reason Optional text added to the output on a failure. - - @param file The source code file where the test failed. - - @param line The source code line number where the test failed. - */ + /** + * Record a failure. + * + * @param reason Optional text added to the output on a failure. + * + * @param file The source code file where the test failed. + * + * @param line The source code line number where the test failed. + */ /** @{ */ template void @@ -202,23 +212,24 @@ public: fail(std::string const& reason = ""); /** @} */ - /** Evaluate a test condition. - - This function provides improved logging by incorporating the - file name and line number into the reported output on failure, - as well as additional text specified by the caller. - - @param shouldBeTrue The condition to test. The condition - is evaluated in a boolean context. - - @param reason Optional added text to output on a failure. - - @param file The source code file where the test failed. - - @param line The source code line number where the test failed. - - @return `true` if the test condition indicates success. - */ + /** + * Evaluate a test condition. + * + * This function provides improved logging by incorporating the + * file name and line number into the reported output on failure, + * as well as additional text specified by the caller. + * + * @param shouldBeTrue The condition to test. The condition + * is evaluated in a boolean context. + * + * @param reason Optional added text to output on a failure. + * + * @param file The source code file where the test failed. + * + * @param line The source code line number where the test failed. + * + * @return `true` if the test condition indicates success. + */ /** @{ */ template bool @@ -275,15 +286,19 @@ public: return unexcept(f, ""); } - /** Return the argument associated with the runner. */ + /** + * Return the argument associated with the runner. + */ std::string const& arg() const { return runner_->arg(); } - // DEPRECATED - // @return `true` if the test condition indicates success(a false value) + /** + * DEPRECATED + * @return `true` if the test condition indicates success(a false value) + */ template bool unexpected(Condition shouldBeFalse, String const& reason); @@ -305,7 +320,9 @@ private: return &kPTs; } - /** Runs the suite. */ + /** + * Runs the suite. + */ virtual void run() = 0; @@ -558,18 +575,20 @@ Suite::run(Runner& r) } #ifndef BEAST_EXPECT -/** Check a precondition. - - If the condition is false, the file and line number are reported. -*/ +/** + * Check a precondition. + * + * If the condition is false, the file and line number are reported. + */ #define BEAST_EXPECT(cond) expect(cond, __FILE__, __LINE__) #endif #ifndef BEAST_EXPECTS -/** Check a precondition. - - If the condition is false, the file and line number are reported. -*/ +/** + * Check a precondition. + * + * If the condition is false, the file and line number are reported. + */ #define BEAST_EXPECTS(cond, reason) \ ((cond) ? (pass(), true) : (fail((reason), __FILE__, __LINE__), false)) #endif @@ -593,41 +612,43 @@ Suite::run(Runner& r) // #ifndef BEAST_DEFINE_TESTSUITE -/** Enables insertion of test suites into the global container. - The default is to insert all test suite definitions into the global - container. If BEAST_DEFINE_TESTSUITE is user defined, this macro - has no effect. -*/ +/** + * Enables insertion of test suites into the global container. + * The default is to insert all test suite definitions into the global + * container. If BEAST_DEFINE_TESTSUITE is user defined, this macro + * has no effect. + */ #ifndef BEAST_NO_UNIT_TEST_INLINE #define BEAST_NO_UNIT_TEST_INLINE 0 #endif -/** Define a unit test suite. - - Class The type representing the class being tested. - Module Identifies the module. - Library Identifies the library. - - The declaration for the class implementing the test should be the same - as Class ## _test. For example, if Class is aged_ordered_container, the - test class must be declared as: - - @code - - struct aged_ordered_container_test : beast::unit_test::suite - { - //... - }; - - @endcode - - The macro invocation must appear in the same namespace as the test class. - - Unit test priorities were introduced so parallel unit_test::suites would - execute faster. Suites with longer running times have higher priorities - than unit tests with shorter running times. Suites with no priorities - are assumed to run most quickly, so they run last. -*/ +/** + * Define a unit test suite. + * + * Class The type representing the class being tested. + * Module Identifies the module. + * Library Identifies the library. + * + * The declaration for the class implementing the test should be the same + * as Class ## _test. For example, if Class is aged_ordered_container, the + * test class must be declared as: + * + * @code + * + * struct aged_ordered_container_test : beast::unit_test::suite + * { + * //... + * }; + * + * @endcode + * + * The macro invocation must appear in the same namespace as the test class. + * + * Unit test priorities were introduced so parallel unit_test::suites would + * execute faster. Suites with longer running times have higher priorities + * than unit tests with shorter running times. Suites with no priorities + * are assumed to run most quickly, so they run last. + */ #if BEAST_NO_UNIT_TEST_INLINE #define BEAST_DEFINE_TESTSUITE(Class, Module, Library) diff --git a/include/xrpl/beast/unit_test/suite_info.h b/include/xrpl/beast/unit_test/suite_info.h index bda10ae7e3..c4e3496f13 100644 --- a/include/xrpl/beast/unit_test/suite_info.h +++ b/include/xrpl/beast/unit_test/suite_info.h @@ -13,7 +13,9 @@ namespace beast::unit_test { class Runner; -/** Associates a unit test type with metadata. */ +/** + * Associates a unit test type with metadata. + */ class SuiteInfo { using run_type = std::function; @@ -60,21 +62,27 @@ public: return library_; } - /// Returns `true` if this suite only runs manually. + /** + * Returns `true` if this suite only runs manually. + */ [[nodiscard]] bool manual() const { return manual_; } - /// Return the canonical suite name as a string. + /** + * Return the canonical suite name as a string. + */ [[nodiscard]] std::string fullName() const { return library_ + "." + module_ + "." + name_; } - /// Run a new instance of the associated test suite. + /** + * Run a new instance of the associated test suite. + */ void run(Runner& r) const { @@ -93,7 +101,9 @@ public: //------------------------------------------------------------------------------ -/// Convenience for producing SuiteInfo for a given test type. +/** + * Convenience for producing SuiteInfo for a given test type. + */ template SuiteInfo makeSuiteInfo(std::string name, std::string module, std::string library, bool manual, int priority) diff --git a/include/xrpl/beast/unit_test/suite_list.h b/include/xrpl/beast/unit_test/suite_list.h index 7dd0dd80f0..057a362859 100644 --- a/include/xrpl/beast/unit_test/suite_list.h +++ b/include/xrpl/beast/unit_test/suite_list.h @@ -16,7 +16,9 @@ namespace beast::unit_test { -/// A container of test suites. +/** + * A container of test suites. + */ class SuiteList : public detail::ConstContainer> { private: @@ -26,10 +28,11 @@ private: #endif public: - /** Insert a suite into the set. - - The suite must not already exist. - */ + /** + * Insert a suite into the set. + * + * The suite must not already exist. + */ template void insert(char const* name, char const* module, char const* library, bool manual, int priority); diff --git a/include/xrpl/beast/unit_test/thread.h b/include/xrpl/beast/unit_test/thread.h index 91d8cf3cab..5a5a99d149 100644 --- a/include/xrpl/beast/unit_test/thread.h +++ b/include/xrpl/beast/unit_test/thread.h @@ -14,7 +14,9 @@ namespace beast::unit_test { -/** Replacement for std::thread that handles exceptions in unit tests. */ +/** + * Replacement for std::thread that handles exceptions in unit tests. + */ class Thread { private: diff --git a/include/xrpl/beast/utility/Journal.h b/include/xrpl/beast/utility/Journal.h index 3de3cfb0e0..9f0a1ead66 100644 --- a/include/xrpl/beast/utility/Journal.h +++ b/include/xrpl/beast/utility/Journal.h @@ -10,7 +10,9 @@ namespace beast { -/** Severity level / threshold of a Journal message. */ +/** + * Severity level / threshold of a Journal message. + */ enum class Severity : std::uint8_t { All = 0, @@ -25,18 +27,19 @@ enum class Severity : std::uint8_t { None = Disabled }; -/** A generic endpoint for log messages. - - The Journal has a few simple goals: - - * To be light-weight and copied by value. - * To allow logging statements to be left in source code. - * The logging is controlled at run-time based on a logging threshold. - - It is advisable to check Journal::active(level) prior to formatting log - text. Doing so sidesteps expensive text formatting when the results - will not be sent to the log. -*/ +/** + * A generic endpoint for log messages. + * + * The Journal has a few simple goals: + * + * * To be light-weight and copied by value. + * * To allow logging statements to be left in source code. + * * The logging is controlled at run-time based on a logging threshold. + * + * It is advisable to check Journal::active(level) prior to formatting log + * text. Doing so sidesteps expensive text formatting when the results + * will not be sent to the log. + */ class Journal { public: @@ -49,7 +52,9 @@ private: public: //-------------------------------------------------------------------------- - /** Abstraction for the underlying message destination. */ + /** + * Abstraction for the underlying message destination. + */ class Sink { protected: @@ -63,36 +68,47 @@ public: Sink& operator=(Sink const& lhs) = delete; - /** Returns `true` if text at the passed severity produces output. */ + /** + * Returns `true` if text at the passed severity produces output. + */ [[nodiscard]] virtual bool active(Severity level) const; - /** Returns `true` if a message is also written to the Output Window - * (MSVC). */ + /** + * Returns `true` if a message is also written to the Output Window + * (MSVC). + */ [[nodiscard]] virtual bool console() const; - /** Set whether messages are also written to the Output Window (MSVC). + /** + * Set whether messages are also written to the Output Window (MSVC). */ virtual void console(bool output); - /** Returns the minimum severity level this sink will report. */ + /** + * Returns the minimum severity level this sink will report. + */ [[nodiscard]] virtual Severity threshold() const; - /** Set the minimum severity this sink will report. */ + /** + * Set the minimum severity this sink will report. + */ virtual void threshold(Severity thresh); - /** Write text to the sink at the specified severity. - A conforming implementation will not write the text if the passed - level is below the current threshold(). - */ + /** + * Write text to the sink at the specified severity. + * A conforming implementation will not write the text if the passed + * level is below the current threshold(). + */ virtual void write(Severity level, std::string const& text) = 0; - /** Bypass filter and write text to the sink at the specified severity. + /** + * Bypass filter and write text to the sink at the specified severity. * Always write the message, but maintain the same formatting as if * it passed through a level filter. * @@ -116,7 +132,9 @@ public: static_assert(std::is_nothrow_destructible_v); #endif - /** Returns a Sink which does nothing. */ + /** + * Returns a Sink which does nothing. + */ static Sink& getNullSink(); @@ -174,26 +192,33 @@ public: //-------------------------------------------------------------------------- public: - /** Provide a light-weight way to check active() before string formatting */ + /** + * Provide a light-weight way to check active() before string formatting + */ class Stream { public: - /** Create a stream which produces no output. */ + /** + * Create a stream which produces no output. + */ explicit Stream() : sink_(getNullSink()), level_(Severity::Disabled) { } - /** Create a stream that writes at the given level. - - Constructor is inlined so checking active() very inexpensive. - */ + /** + * Create a stream that writes at the given level. + * + * Constructor is inlined so checking active() very inexpensive. + */ Stream(Sink& sink, Severity level) : sink_(sink), level_(level) { XRPL_ASSERT( level_ < Severity::Disabled, "beast::Journal::Stream::Stream : maximum level"); } - /** Construct or copy another Stream. */ + /** + * Construct or copy another Stream. + */ Stream(Stream const& other) : Stream(other.sink_, other.level_) { } @@ -201,21 +226,27 @@ public: Stream& operator=(Stream const& other) = delete; - /** Returns the Sink that this Stream writes to. */ + /** + * Returns the Sink that this Stream writes to. + */ [[nodiscard]] Sink& sink() const { return sink_; } - /** Returns the Severity level of messages this Stream reports. */ + /** + * Returns the Severity level of messages this Stream reports. + */ [[nodiscard]] Severity level() const { return level_; } - /** Returns `true` if sink logs anything at this stream's level. */ + /** + * Returns `true` if sink logs anything at this stream's level. + */ /** @{ */ [[nodiscard]] bool active() const @@ -230,7 +261,9 @@ public: } /** @} */ - /** Output stream support. */ + /** + * Output stream support. + */ /** @{ */ ScopedStream operator<<(std::ostream& manip(std::ostream&)) const; @@ -256,39 +289,50 @@ public: //-------------------------------------------------------------------------- - /** Journal has no default constructor. */ + /** + * Journal has no default constructor. + */ Journal() = delete; - /** Create a journal that writes to the specified sink. */ + /** + * Create a journal that writes to the specified sink. + */ explicit Journal(Sink& sink) : sink_(&sink) { } - /** Returns the Sink associated with this Journal. */ + /** + * Returns the Sink associated with this Journal. + */ [[nodiscard]] Sink& sink() const { return *sink_; } - /** Returns a stream for this sink, with the specified severity level. */ + /** + * Returns a stream for this sink, with the specified severity level. + */ [[nodiscard]] Stream stream(Severity level) const { return Stream(*sink_, level); } - /** Returns `true` if any message would be logged at this severity level. - For a message to be logged, the severity must be at or above the - sink's severity threshold. - */ + /** + * Returns `true` if any message would be logged at this severity level. + * For a message to be logged, the severity must be at or above the + * sink's severity threshold. + */ [[nodiscard]] bool active(Severity level) const { return sink_->active(level); } - /** Severity stream access functions. */ + /** + * Severity stream access functions. + */ /** @{ */ [[nodiscard]] Stream trace() const diff --git a/include/xrpl/beast/utility/PropertyStream.h b/include/xrpl/beast/utility/PropertyStream.h index 3fb6df53d9..f32f5b7fef 100644 --- a/include/xrpl/beast/utility/PropertyStream.h +++ b/include/xrpl/beast/utility/PropertyStream.h @@ -12,7 +12,9 @@ namespace beast { //------------------------------------------------------------------------------ -/** Abstract stream with RAII containers that produce a property tree. */ +/** + * Abstract stream with RAII containers that produce a property tree. + */ class PropertyStream { public: @@ -306,7 +308,9 @@ public: // //------------------------------------------------------------------------------ -/** Subclasses can be called to write to a stream and have children. */ +/** + * Subclasses can be called to write to a stream and have children. + */ class PropertyStream::Source { private: @@ -324,17 +328,22 @@ public: Source& operator=(Source const&) = delete; - /** Returns the name of this source. */ + /** + * Returns the name of this source. + */ [[nodiscard]] std::string const& name() const; - /** Add a child source. */ + /** + * Add a child source. + */ void add(Source& source); - /** Add a child source by pointer. - The source pointer is returned so it can be used in ctor-initializers. - */ + /** + * Add a child source by pointer. + * The source pointer is returned so it can be used in ctor-initializers. + */ template Derived* add(Derived* child) @@ -343,45 +352,55 @@ public: return child; } - /** Remove a child source from this Source. */ + /** + * Remove a child source from this Source. + */ void remove(Source& child); - /** Remove all child sources from this Source. */ + /** + * Remove all child sources from this Source. + */ void removeAll(); - /** Write only this Source to the stream. */ + /** + * Write only this Source to the stream. + */ void writeOne(PropertyStream& stream); - /** write this source and all its children recursively to the stream. */ + /** + * write this source and all its children recursively to the stream. + */ void write(PropertyStream& stream); - /** Parse the path and write the corresponding Source and optional children. - If the source is found, it is written. If the wildcard character '*' - exists as the last character in the path, then all the children are - written recursively. - */ + /** + * Parse the path and write the corresponding Source and optional children. + * If the source is found, it is written. If the wildcard character '*' + * exists as the last character in the path, then all the children are + * written recursively. + */ void write(PropertyStream& stream, std::string const& path); - /** Parse the dot-delimited Source path and return the result. - The first value will be a pointer to the Source object corresponding - to the given path. If no Source object exists, then the first value - will be nullptr and the second value will be undefined. - The second value is a boolean indicating whether or not the path string - specifies the wildcard character '*' as the last character. - - print statement examples - "parent.child" prints child and all of its children - "parent.child." start at the parent and print down to child - "parent.grandchild" prints nothing- grandchild not direct descendent - "parent.grandchild." starts at the parent and prints down to grandchild - "parent.grandchild.*" starts at parent, print through grandchild - children - */ + /** + * Parse the dot-delimited Source path and return the result. + * The first value will be a pointer to the Source object corresponding + * to the given path. If no Source object exists, then the first value + * will be nullptr and the second value will be undefined. + * The second value is a boolean indicating whether or not the path string + * specifies the wildcard character '*' as the last character. + * + * print statement examples + * "parent.child" prints child and all of its children + * "parent.child." start at the parent and print down to child + * "parent.grandchild" prints nothing- grandchild not direct descendent + * "parent.grandchild." starts at the parent and prints down to grandchild + * "parent.grandchild.*" starts at parent, print through grandchild + * children + */ std::pair find(std::string path); @@ -401,9 +420,10 @@ public: //-------------------------------------------------------------------------- - /** Subclass override. - The default version does nothing. - */ + /** + * Subclass override. + * The default version does nothing. + */ virtual void onWrite(Map&); }; diff --git a/include/xrpl/beast/utility/WrappedSink.h b/include/xrpl/beast/utility/WrappedSink.h index a24ad595db..3ab48e1939 100644 --- a/include/xrpl/beast/utility/WrappedSink.h +++ b/include/xrpl/beast/utility/WrappedSink.h @@ -7,7 +7,9 @@ namespace beast { -/** Wraps a Journal::Sink to prefix its output with a string. */ +/** + * Wraps a Journal::Sink to prefix its output with a string. + */ // A WrappedSink both is a Sink and has a Sink: // o It inherits from Sink so it has the correct interface. diff --git a/include/xrpl/beast/utility/Zero.h b/include/xrpl/beast/utility/Zero.h index e28589760b..406921c500 100644 --- a/include/xrpl/beast/utility/Zero.h +++ b/include/xrpl/beast/utility/Zero.h @@ -4,22 +4,23 @@ namespace beast { -/** Zero allows classes to offer efficient comparisons to zero. - - Zero is a struct to allow classes to efficiently compare with zero without - requiring an rvalue construction. - - It's often the case that we have classes which combine a number and a unit. - In such cases, comparisons like t > 0 or t != 0 make sense, but comparisons - like t > 1 or t != 1 do not. - - The class Zero allows such comparisons to be easily made. - - The comparing class T either needs to have a method called signum() which - returns a positive number, 0, or a negative; or there needs to be a signum - function which resolves in the namespace which takes an instance of T and - returns a positive, zero or negative number. -*/ +/** + * Zero allows classes to offer efficient comparisons to zero. + * + * Zero is a struct to allow classes to efficiently compare with zero without + * requiring an rvalue construction. + * + * It's often the case that we have classes which combine a number and a unit. + * In such cases, comparisons like t > 0 or t != 0 make sense, but comparisons + * like t > 1 or t != 1 do not. + * + * The class Zero allows such comparisons to be easily made. + * + * The comparing class T either needs to have a method called signum() which + * returns a positive number, 0, or a negative; or there needs to be a signum + * function which resolves in the namespace which takes an instance of T and + * returns a positive, zero or negative number. + */ struct Zero { @@ -28,7 +29,9 @@ struct Zero inline constexpr Zero kZero{}; -/** Default implementation of signum calls the method on the class. */ +/** + * Default implementation of signum calls the method on the class. + */ template auto signum(T const& t) diff --git a/include/xrpl/beast/utility/maybe_const.h b/include/xrpl/beast/utility/maybe_const.h index 10b2eaf7f6..848ea86cb2 100644 --- a/include/xrpl/beast/utility/maybe_const.h +++ b/include/xrpl/beast/utility/maybe_const.h @@ -4,7 +4,9 @@ namespace beast { -/** Makes T const or non const depending on a bool. */ +/** + * Makes T const or non const depending on a bool. + */ template struct MaybeConst { @@ -13,7 +15,9 @@ struct MaybeConst conditional_t::type const, std::remove_const_t>; }; -/** Alias for omitting `typename`. */ +/** + * Alias for omitting `typename`. + */ template using maybe_const_t = MaybeConst::type; diff --git a/include/xrpl/beast/utility/temp_dir.h b/include/xrpl/beast/utility/temp_dir.h index ec661b51c4..a0ff1e6940 100644 --- a/include/xrpl/beast/utility/temp_dir.h +++ b/include/xrpl/beast/utility/temp_dir.h @@ -6,11 +6,12 @@ namespace beast { -/** RAII temporary directory. - - The directory and all its contents are deleted when - the instance of `temp_dir` is destroyed. -*/ +/** + * RAII temporary directory. + * + * The directory and all its contents are deleted when + * the instance of `temp_dir` is destroyed. + */ class TempDir { boost::filesystem::path path_; @@ -22,7 +23,9 @@ public: operator=(TempDir const&) = delete; #endif - /// Construct a temporary directory. + /** + * Construct a temporary directory. + */ TempDir() { auto const dir = boost::filesystem::temp_directory_path(); @@ -33,7 +36,9 @@ public: boost::filesystem::create_directory(path_); } - /// Destroy a temporary directory. + /** + * Destroy a temporary directory. + */ ~TempDir() { // use non-throwing calls in the destructor @@ -42,17 +47,20 @@ public: // TODO: warn/notify if ec set ? } - /// Get the native path for the temporary directory + /** + * Get the native path for the temporary directory + */ [[nodiscard]] std::string path() const { return path_.string(); } - /** Get the native path for the a file. - - The file does not need to exist. - */ + /** + * Get the native path for the a file. + * + * The file does not need to exist. + */ [[nodiscard]] std::string file(std::string const& name) const { diff --git a/include/xrpl/beast/xor_shift_engine.h b/include/xrpl/beast/xor_shift_engine.h index 45baecf101..6a7272c195 100644 --- a/include/xrpl/beast/xor_shift_engine.h +++ b/include/xrpl/beast/xor_shift_engine.h @@ -85,14 +85,15 @@ XorShiftEngine::murmurhash3(result_type x) -> result_type } // namespace detail -/** XOR-shift Generator. - - Meets the requirements of UniformRandomNumberGenerator. - - Simple and fast RNG based on: - http://xorshift.di.unimi.it/xorshift128plus.c - does not accept seed==0 -*/ +/** + * XOR-shift Generator. + * + * Meets the requirements of UniformRandomNumberGenerator. + * + * Simple and fast RNG based on: + * http://xorshift.di.unimi.it/xorshift128plus.c + * does not accept seed==0 + */ using xor_shift_engine = detail::XorShiftEngine<>; } // namespace beast diff --git a/include/xrpl/conditions/Condition.h b/include/xrpl/conditions/Condition.h index 3c798663d7..365a41a087 100644 --- a/include/xrpl/conditions/Condition.h +++ b/include/xrpl/conditions/Condition.h @@ -24,42 +24,49 @@ enum class Type : std::uint8_t { class Condition { public: - /** The largest binary condition we support. - - @note This value will be increased in the future, but it - must never decrease, as that could cause conditions - that were previously considered valid to no longer - be allowed. - */ + /** + * The largest binary condition we support. + * + * @note This value will be increased in the future, but it + * must never decrease, as that could cause conditions + * that were previously considered valid to no longer + * be allowed. + */ static constexpr std::size_t kMaxSerializedCondition = 128; - /** Load a condition from its binary form - - @param s The buffer containing the fulfillment to load. - @param ec Set to the error, if any occurred. - - The binary format for a condition is specified in the - cryptoconditions RFC. See: - - https://tools.ietf.org/html/draft-thomas-crypto-conditions-02#section-7.2 - */ + /** + * Load a condition from its binary form + * + * @param s The buffer containing the fulfillment to load. + * @param ec Set to the error, if any occurred. + * + * The binary format for a condition is specified in the + * cryptoconditions RFC. See: + * + * https://tools.ietf.org/html/draft-thomas-crypto-conditions-02#section-7.2 + */ static std::unique_ptr deserialize(Slice s, std::error_code& ec); public: Type type; - /** An identifier for this condition. - - This fingerprint is meant to be unique only with - respect to other conditions of the same type. - */ + /** + * An identifier for this condition. + * + * This fingerprint is meant to be unique only with + * respect to other conditions of the same type. + */ Buffer fingerprint; - /** The cost associated with this condition. */ + /** + * The cost associated with this condition. + */ std::uint32_t cost; - /** For compound conditions, set of conditions includes */ + /** + * For compound conditions, set of conditions includes + */ std::set subtypes; Condition(Type t, std::uint32_t c, Slice fp) : type(t), fingerprint(fp), cost(c) diff --git a/include/xrpl/conditions/Fulfillment.h b/include/xrpl/conditions/Fulfillment.h index a3001b2620..11f3165a58 100644 --- a/include/xrpl/conditions/Fulfillment.h +++ b/include/xrpl/conditions/Fulfillment.h @@ -14,64 +14,73 @@ namespace xrpl::cryptoconditions { struct Fulfillment { public: - /** The largest binary fulfillment we support. - - @note This value will be increased in the future, but it - must never decrease, as that could cause fulfillments - that were previously considered valid to no longer - be allowed. - */ + /** + * The largest binary fulfillment we support. + * + * @note This value will be increased in the future, but it + * must never decrease, as that could cause fulfillments + * that were previously considered valid to no longer + * be allowed. + */ static constexpr std::size_t kMaxSerializedFulfillment = 256; - /** Load a fulfillment from its binary form - - @param s The buffer containing the fulfillment to load. - @param ec Set to the error, if any occurred. - - The binary format for a fulfillment is specified in the - cryptoconditions RFC. See: - - https://tools.ietf.org/html/draft-thomas-crypto-conditions-02#section-7.3 - */ + /** + * Load a fulfillment from its binary form + * + * @param s The buffer containing the fulfillment to load. + * @param ec Set to the error, if any occurred. + * + * The binary format for a fulfillment is specified in the + * cryptoconditions RFC. See: + * + * https://tools.ietf.org/html/draft-thomas-crypto-conditions-02#section-7.3 + */ static std::unique_ptr deserialize(Slice s, std::error_code& ec); public: virtual ~Fulfillment() = default; - /** Returns the fulfillment's fingerprint: - - The fingerprint is an octet string uniquely - representing this fulfillment's condition - with respect to other conditions of the - same type. - */ + /** + * Returns the fulfillment's fingerprint: + * + * The fingerprint is an octet string uniquely + * representing this fulfillment's condition + * with respect to other conditions of the + * same type. + */ [[nodiscard]] virtual Buffer fingerprint() const = 0; - /** Returns the type of this condition. */ + /** + * Returns the type of this condition. + */ [[nodiscard]] virtual Type type() const = 0; - /** Validates a fulfillment. */ + /** + * Validates a fulfillment. + */ [[nodiscard]] virtual bool validate(Slice data) const = 0; - /** Calculates the cost associated with this fulfillment. * - - The cost function is deterministic and depends on the - type and properties of the condition and the fulfillment - that the condition is generated from. - */ + /** + * Calculates the cost associated with this fulfillment. * + * + * The cost function is deterministic and depends on the + * type and properties of the condition and the fulfillment + * that the condition is generated from. + */ [[nodiscard]] virtual std::uint32_t cost() const = 0; - /** Returns the condition associated with the given fulfillment. - - This process is completely deterministic. All implementations - will, if compliant, produce the identical condition for the - same fulfillment. - */ + /** + * Returns the condition associated with the given fulfillment. + * + * This process is completely deterministic. All implementations + * will, if compliant, produce the identical condition for the + * same fulfillment. + */ [[nodiscard]] virtual Condition condition() const = 0; }; @@ -90,36 +99,40 @@ operator!=(Fulfillment const& lhs, Fulfillment const& rhs) return !(lhs == rhs); } -/** Determine whether the given fulfillment and condition match */ +/** + * Determine whether the given fulfillment and condition match + */ bool match(Fulfillment const& f, Condition const& c); -/** Verify if the given message satisfies the fulfillment. - - @param f The fulfillment - @param c The condition - @param m The message - - @note the message is not relevant for some conditions - and a fulfillment will successfully satisfy its - condition for any given message. -*/ +/** + * Verify if the given message satisfies the fulfillment. + * + * @param f The fulfillment + * @param c The condition + * @param m The message + * + * @note the message is not relevant for some conditions + * and a fulfillment will successfully satisfy its + * condition for any given message. + */ bool validate(Fulfillment const& f, Condition const& c, Slice m); -/** Verify a cryptoconditional trigger. - - A cryptoconditional trigger is a cryptocondition with - an empty message. - - When using such triggers, it is recommended that the - trigger be of type preimage, prefix or threshold. If - a signature type is used (i.e. Ed25519 or RSA-SHA256) - then the Ed25519 or RSA keys should be single-use keys. - - @param f The fulfillment - @param c The condition -*/ +/** + * Verify a cryptoconditional trigger. + * + * A cryptoconditional trigger is a cryptocondition with + * an empty message. + * + * When using such triggers, it is recommended that the + * trigger be of type preimage, prefix or threshold. If + * a signature type is used (i.e. Ed25519 or RSA-SHA256) + * then the Ed25519 or RSA keys should be single-use keys. + * + * @param f The fulfillment + * @param c The condition + */ bool validate(Fulfillment const& f, Condition const& c); diff --git a/include/xrpl/conditions/detail/PreimageSha256.h b/include/xrpl/conditions/detail/PreimageSha256.h index 0973a52e4a..007588a0b5 100644 --- a/include/xrpl/conditions/detail/PreimageSha256.h +++ b/include/xrpl/conditions/detail/PreimageSha256.h @@ -18,23 +18,25 @@ namespace xrpl::cryptoconditions { class PreimageSha256 final : public Fulfillment { public: - /** The maximum allowed length of a preimage. - - The specification does not specify a minimum supported - length, nor does it require all conditions to support - the same minimum length. - - While future versions of this code will never lower - this limit, they may opt to raise it. - */ + /** + * The maximum allowed length of a preimage. + * + * The specification does not specify a minimum supported + * length, nor does it require all conditions to support + * the same minimum length. + * + * While future versions of this code will never lower + * this limit, they may opt to raise it. + */ static constexpr std::size_t kMaxPreimageLength = 128; - /** Parse the payload for a PreimageSha256 condition - - @param s A slice containing the DER encoded payload - @param ec indicates success or failure of the operation - @return the preimage, if successful; empty pointer otherwise. - */ + /** + * Parse the payload for a PreimageSha256 condition + * + * @param s A slice containing the DER encoded payload + * @param ec indicates success or failure of the operation + * @return the preimage, if successful; empty pointer otherwise. + */ static std::unique_ptr deserialize(Slice s, std::error_code& ec) { diff --git a/include/xrpl/config/BasicConfig.h b/include/xrpl/config/BasicConfig.h index 5680b51fe7..607a0c3e5f 100644 --- a/include/xrpl/config/BasicConfig.h +++ b/include/xrpl/config/BasicConfig.h @@ -21,9 +21,10 @@ using IniFileSections = std::unordered_map //------------------------------------------------------------------------------ -/** Holds a collection of configuration values. - A configuration file contains zero or more sections. -*/ +/** + * Holds a collection of configuration values. + * A configuration file contains zero or more sections. + */ class Section { private: @@ -36,28 +37,34 @@ private: using const_iterator = decltype(lookup_)::const_iterator; public: - /** Create an empty section. */ + /** + * Create an empty section. + */ explicit Section(std::string name = ""); - /** Returns the name of this section. */ + /** + * Returns the name of this section. + */ [[nodiscard]] std::string const& name() const { return name_; } - /** Returns all the lines in the section. - This includes everything. - */ + /** + * Returns all the lines in the section. + * This includes everything. + */ [[nodiscard]] std::vector const& lines() const { return lines_; } - /** Returns all the values in the section. - Values are non-empty lines which are not key/value pairs. - */ + /** + * Returns all the values in the section. + * Values are non-empty lines which are not key/value pairs. + */ [[nodiscard]] std::vector const& values() const { @@ -84,7 +91,7 @@ public: * Get the legacy value for this section. * * @return The retrieved value. A section with an empty legacy value returns - an empty string. + * an empty string. */ [[nodiscard]] std::string legacy() const @@ -99,28 +106,34 @@ public: return lines_[0]; } - /** Set a key/value pair. - The previous value is discarded. - */ + /** + * Set a key/value pair. + * The previous value is discarded. + */ void set(std::string const& key, std::string const& value); - /** Append a set of lines to this section. - Lines containing key/value pairs are added to the map, - else they are added to the values list. Everything is - added to the lines list. - */ + /** + * Append a set of lines to this section. + * Lines containing key/value pairs are added to the map, + * else they are added to the values list. Everything is + * added to the lines list. + */ void append(std::vector const& lines); - /** Append a line to this section. */ + /** + * Append a line to this section. + */ void append(std::string const& line) { append(std::vector{line}); } - /** Returns `true` if a key with the given name exists. */ + /** + * Returns `true` if a key with the given name exists. + */ [[nodiscard]] bool exists(std::string const& name) const; @@ -134,7 +147,9 @@ public: return boost::lexical_cast(iter->second); } - /// Returns a value if present, else another value. + /** + * Returns a value if present, else another value. + */ template [[nodiscard]] T valueOr(std::string const& name, T const& other) const @@ -199,23 +214,27 @@ public: //------------------------------------------------------------------------------ -/** Holds unparsed configuration information. - The raw data sections are processed with intermediate parsers specific - to each module instead of being all parsed in a central location. -*/ +/** + * Holds unparsed configuration information. + * The raw data sections are processed with intermediate parsers specific + * to each module instead of being all parsed in a central location. + */ class BasicConfig { private: std::unordered_map map_; public: - /** Returns `true` if a section with the given name exists. */ + /** + * Returns `true` if a section with the given name exists. + */ [[nodiscard]] bool exists(std::string const& name) const; - /** Returns the section with the given name. - If the section does not exist, an empty section is returned. - */ + /** + * Returns the section with the given name. + * If the section does not exist, an empty section is returned. + */ /** @{ */ Section& section(std::string const& name); @@ -236,37 +255,39 @@ public: } /** @} */ - /** Overwrite a key/value pair with a command line argument - If the section does not exist it is created. - The previous value, if any, is overwritten. - */ + /** + * Overwrite a key/value pair with a command line argument + * If the section does not exist it is created. + * The previous value, if any, is overwritten. + */ void overwrite(std::string const& section, std::string const& key, std::string const& value); - /** Remove all the key/value pairs from the section. + /** + * Remove all the key/value pairs from the section. */ void deprecatedClearSection(std::string const& section); /** - * Set a value that is not a key/value pair. + * Set a value that is not a key/value pair. * - * The value is stored as the section's first value and may be retrieved - * through section::legacy. + * The value is stored as the section's first value and may be retrieved + * through section::legacy. * - * @param section Name of the section to modify. - * @param value Contents of the legacy value. + * @param section Name of the section to modify. + * @param value Contents of the legacy value. */ void legacy(std::string const& section, std::string value); /** - * Get the legacy value of a section. A section with a - * single-line value may be retrieved as a legacy value. + * Get the legacy value of a section. A section with a + * single-line value may be retrieved as a legacy value. * - * @param sectionName Retrieve the contents of this section's - * legacy value. - * @return Contents of the legacy value. + * @param sectionName Retrieve the contents of this section's + * legacy value. + * @return Contents of the legacy value. */ [[nodiscard]] std::string legacy(std::string const& sectionName) const; @@ -289,11 +310,12 @@ protected: //------------------------------------------------------------------------------ -/** Set a value from a configuration Section - If the named value is not found or doesn't parse as a T, - the variable is unchanged. - @return `true` if value was set. -*/ +/** + * Set a value from a configuration Section + * If the named value is not found or doesn't parse as a T, + * the variable is unchanged. + * @return `true` if value was set. + */ template bool set(T& target, std::string const& name, Section const& section) @@ -312,11 +334,12 @@ set(T& target, std::string const& name, Section const& section) return foundAndValid; } -/** Set a value from a configuration Section - If the named value is not found or doesn't cast to T, - the variable is assigned the default. - @return `true` if the named value was found and is valid. -*/ +/** + * Set a value from a configuration Section + * If the named value is not found or doesn't cast to T, + * the variable is assigned the default. + * @return `true` if the named value was found and is valid. + */ template bool set(T& target, T const& defaultValue, std::string const& name, Section const& section) @@ -327,10 +350,11 @@ set(T& target, T const& defaultValue, std::string const& name, Section const& se return foundAndValid; } -/** Retrieve a key/value pair from a section. - @return The value string converted to T if it exists - and can be parsed, or else defaultValue. -*/ +/** + * Retrieve a key/value pair from a section. + * @return The value string converted to T if it exists + * and can be parsed, or else defaultValue. + */ // NOTE This routine might be more clumsy than the previous two template T diff --git a/include/xrpl/core/ClosureCounter.h b/include/xrpl/core/ClosureCounter.h index ed15db032e..33899d671b 100644 --- a/include/xrpl/core/ClosureCounter.h +++ b/include/xrpl/core/ClosureCounter.h @@ -30,8 +30,8 @@ namespace xrpl { * the caller that they should drop the closure and cancel their operation. * `join` blocks until all existing closure substitutes are destroyed. * - * \tparam Ret The return type of the closure. - * \tparam Args The argument types of the closure. + * @tparam Ret The return type of the closure. + * @tparam Args The argument types of the closure. */ template class ClosureCounter @@ -131,18 +131,21 @@ public: ClosureCounter& operator=(ClosureCounter const&) = delete; - /** Destructor verifies all in-flight closures are complete. */ + /** + * Destructor verifies all in-flight closures are complete. + */ ~ClosureCounter() { using namespace std::chrono_literals; join("ClosureCounter", 1s, debugLog()); } - /** Returns once all counted in-flight closures are destroyed. - - @param name Name reported if join time exceeds wait. - @param wait If join() exceeds this duration report to Journal. - @param j Journal written to if wait is exceeded. + /** + * Returns once all counted in-flight closures are destroyed. + * + * @param name Name reported if join time exceeds wait. + * @param wait If join() exceeds this duration report to Journal. + * @param j Journal written to if wait is exceeded. */ void join(char const* name, std::chrono::milliseconds wait, beast::Journal j) @@ -160,13 +163,14 @@ public: } } - /** Wrap the passed closure with a reference counter. - - @param closure Closure that accepts Args parameters and returns Ret. - @return If join() has been called returns std::nullopt. Otherwise - returns a std::optional that wraps closure with a - reference counter. - */ + /** + * Wrap the passed closure with a reference counter. + * + * @param closure Closure that accepts Args parameters and returns Ret. + * @return If join() has been called returns std::nullopt. Otherwise + * returns a std::optional that wraps closure with a + * reference counter. + */ template std::optional> wrap(Closure&& closure) @@ -180,19 +184,22 @@ public: return ret; } - /** Current number of Closures outstanding. Only useful for testing. */ + /** + * Current number of Closures outstanding. Only useful for testing. + */ int count() const { return closureCount_; } - /** Returns true if this has been joined. - - Even if true is returned, counted closures may still be in flight. - However if (joined() && (count() == 0)) there should be no more - counted closures in flight. - */ + /** + * Returns true if this has been joined. + * + * Even if true is returned, counted closures may still be in flight. + * However if (joined() && (count() == 0)) there should be no more + * counted closures in flight. + */ bool joined() const { diff --git a/include/xrpl/core/Coro.ipp b/include/xrpl/core/Coro.ipp index 133caf37a9..9a45dac504 100644 --- a/include/xrpl/core/Coro.ipp +++ b/include/xrpl/core/Coro.ipp @@ -4,8 +4,10 @@ namespace xrpl { -/// Coroutine stack size (1.5 MB). Increased from 1 MB because -/// ASAN-instrumented deep call stacks exceeded the original limit. +/** + * Coroutine stack size (1.5 MB). Increased from 1 MB because + * ASAN-instrumented deep call stacks exceeded the original limit. + */ constexpr std::size_t kCoroStackSize = 1536 * 1024; template diff --git a/include/xrpl/core/HashRouter.h b/include/xrpl/core/HashRouter.h index dad1afb405..20aafecc5f 100644 --- a/include/xrpl/core/HashRouter.h +++ b/include/xrpl/core/HashRouter.h @@ -75,19 +75,21 @@ any(HashRouterFlags flags) class Config; -/** Routing table for objects identified by hash. - - This table keeps track of which hashes have been received by which peers. - It is used to manage the routing and broadcasting of messages in the peer - to peer overlay. -*/ +/** + * Routing table for objects identified by hash. + * + * This table keeps track of which hashes have been received by which peers. + * It is used to manage the routing and broadcasting of messages in the peer + * to peer overlay. + */ class HashRouter { public: // The type here *MUST* match the type of Peer::id_t using PeerShortID = std::uint32_t; - /** Structure used to customize @ref HashRouter behavior. + /** + * Structure used to customize @ref HashRouter behavior. * * Even though these items are configurable, they are undocumented. Don't * change them unless there is a good reason, and network-wide coordination @@ -97,22 +99,27 @@ public: */ struct Setup { - /// Default constructor + /** + * Default constructor + */ explicit Setup() = default; using seconds = std::chrono::seconds; - /** Expiration time for a hash entry + /** + * Expiration time for a hash entry */ seconds holdTime{300}; - /** Amount of time required before a relayed item will be relayed again. + /** + * Amount of time required before a relayed item will be relayed again. */ seconds relayTime{30}; }; private: - /** An entry in the routing table. + /** + * An entry in the routing table. */ class Entry : public CountedObject { @@ -138,26 +145,31 @@ private: flags_ |= flagsToSet; } - /** Return set of peers we've relayed to and reset tracking */ + /** + * Return set of peers we've relayed to and reset tracking + */ std::set releasePeerSet() { return std::move(peers_); } - /** Return seated relay time point if the message has been relayed */ + /** + * Return seated relay time point if the message has been relayed + */ [[nodiscard]] std::optional relayed() const { return relayed_; } - /** Determines if this item should be relayed. - - Checks whether the item has been recently relayed. - If it has, return false. If it has not, update the - last relay timestamp and return true. - */ + /** + * Determines if this item should be relayed. + * + * Checks whether the item has been recently relayed. + * If it has, return false. If it has not, update the + * last relay timestamp and return true. + */ bool shouldRelay(Stopwatch::time_point const& now, std::chrono::seconds relayTime) { @@ -203,11 +215,13 @@ public: bool addSuppressionPeer(uint256 const& key, PeerShortID peer); - /** Add a suppression peer and get message's relay status. + /** + * Add a suppression peer and get message's relay status. * Return pair: * element 1: true if the peer is added. * element 2: optional is seated to the relay time point or - * is unseated if has not relayed yet. */ + * is unseated if has not relayed yet. + */ std::pair> addSuppressionPeerWithStatus(uint256 const& key, PeerShortID peer); @@ -222,28 +236,30 @@ public: HashRouterFlags& flags, std::chrono::seconds txInterval); - /** Set the flags on a hash. - - @return `true` if the flags were changed. `false` if unchanged. - */ + /** + * Set the flags on a hash. + * + * @return `true` if the flags were changed. `false` if unchanged. + */ bool setFlags(uint256 const& key, HashRouterFlags flags); HashRouterFlags getFlags(uint256 const& key); - /** Determines whether the hashed item should be relayed. - - Effects: - - If the item should be relayed, this function will not - return a seated optional again until the relay time has expired. - The internal set of peers will also be reset. - - @return A `std::optional` set of peers which do not need to be - relayed to. If the result is unseated, the item should - _not_ be relayed. - */ + /** + * Determines whether the hashed item should be relayed. + * + * Effects: + * + * If the item should be relayed, this function will not + * return a seated optional again until the relay time has expired. + * The internal set of peers will also be reset. + * + * @return A `std::optional` set of peers which do not need to be + * relayed to. If the result is unseated, the item should + * _not_ be relayed. + */ std::optional> shouldRelay(uint256 const& key); diff --git a/include/xrpl/core/Job.h b/include/xrpl/core/Job.h index e16d7412bf..93b39701be 100644 --- a/include/xrpl/core/Job.h +++ b/include/xrpl/core/Job.h @@ -83,12 +83,13 @@ class Job : public CountedObject public: using clock_type = std::chrono::steady_clock; - /** Default constructor. - - Allows Job to be used as a container type. - - This is used to allow things like jobMap [key] = value. - */ + /** + * Default constructor. + * + * Allows Job to be used as a container type. + * + * This is used to allow things like jobMap [key] = value. + */ // VFALCO NOTE I'd prefer not to have a default constructed object. // What is the semantic meaning of a Job with no associated // function? Having the invariant "all Job objects refer to @@ -108,7 +109,9 @@ public: [[nodiscard]] JobType getType() const; - /** Returns the time when the job was queued. */ + /** + * Returns the time when the job was queued. + */ [[nodiscard]] clock_type::time_point const& queueTime() const; diff --git a/include/xrpl/core/JobQueue.h b/include/xrpl/core/JobQueue.h index e4b64546f3..0c9fc76357 100644 --- a/include/xrpl/core/JobQueue.h +++ b/include/xrpl/core/JobQueue.h @@ -45,20 +45,23 @@ struct CoroCreateT explicit CoroCreateT() = default; }; -/** A pool of threads to perform work. - - A job posted will always run to completion. - - Coroutines that are suspended must be resumed, - and run to completion. - - When the JobQueue stops, it waits for all jobs - and coroutines to finish. -*/ +/** + * A pool of threads to perform work. + * + * A job posted will always run to completion. + * + * Coroutines that are suspended must be resumed, + * and run to completion. + * + * When the JobQueue stops, it waits for all jobs + * and coroutines to finish. + */ class JobQueue : private Workers::Callback { public: - /** Coroutines must run to completion. */ + /** + * Coroutines must run to completion. + */ class Coro : public std::enable_shared_from_this { private: @@ -87,55 +90,64 @@ public: ~Coro(); - /** Suspend coroutine execution. - Effects: - The coroutine's stack is saved. - The associated Job thread is released. - Note: - The associated Job function returns. - Undefined behavior if called consecutively without a corresponding - post. - */ + /** + * Suspend coroutine execution. + * Effects: + * The coroutine's stack is saved. + * The associated Job thread is released. + * Note: + * The associated Job function returns. + * Undefined behavior if called consecutively without a corresponding + * post. + */ void yield() const; - /** Schedule coroutine execution. - Effects: - Returns immediately. - A new job is scheduled to resume the execution of the coroutine. - When the job runs, the coroutine's stack is restored and execution - continues at the beginning of coroutine function or the - statement after the previous call to yield. Undefined behavior if - called after the coroutine has completed with a return (as opposed to - a yield()). Undefined behavior if post() or resume() called - consecutively without a corresponding yield. - - @return true if the Coro's job is added to the JobQueue. - */ + /** + * Schedule coroutine execution. + * Effects: + * Returns immediately. + * A new job is scheduled to resume the execution of the coroutine. + * When the job runs, the coroutine's stack is restored and execution + * continues at the beginning of coroutine function or the + * statement after the previous call to yield. Undefined behavior if + * called after the coroutine has completed with a return (as opposed to + * a yield()). Undefined behavior if post() or resume() called + * consecutively without a corresponding yield. + * + * @return true if the Coro's job is added to the JobQueue. + */ bool post(); - /** Resume coroutine execution. - Effects: - The coroutine continues execution from where it last left off - using this same thread. - If the coroutine has already completed, returns immediately - (handles the documented post-before-yield race condition). - Undefined behavior if resume() or post() called consecutively - without a corresponding yield. - */ + /** + * Resume coroutine execution. + * Effects: + * The coroutine continues execution from where it last left off + * using this same thread. + * If the coroutine has already completed, returns immediately + * (handles the documented post-before-yield race condition). + * Undefined behavior if resume() or post() called consecutively + * without a corresponding yield. + */ void resume(); - /** Returns true if the Coro is still runnable (has not returned). */ + /** + * Returns true if the Coro is still runnable (has not returned). + */ [[nodiscard]] bool runnable() const; - /** Once called, the Coro allows early exit without an assert. */ + /** + * Once called, the Coro allows early exit without an assert. + */ void expectEarlyExit(); - /** Waits until coroutine returns from the user function. */ + /** + * Waits until coroutine returns from the user function. + */ void join(); }; @@ -150,14 +162,15 @@ public: perf::PerfLog& perfLog); ~JobQueue() override; - /** Adds a job to the JobQueue. - - @param type The type of job. - @param name Name of the job. - @param jobHandler Callable with signature void(). Called when the job is executed. - - @return true if jobHandler added to queue. - */ + /** + * Adds a job to the JobQueue. + * + * @param type The type of job. + * @param name Name of the job. + * @param jobHandler Callable with signature void(). Called when the job is executed. + * + * @return true if jobHandler added to queue. + */ template bool addJob(JobType type, std::string const& name, JobHandler&& jobHandler) @@ -170,40 +183,46 @@ public: return false; } - /** Creates a coroutine and adds a job to the queue which will run it. - - @param t The type of job. - @param name Name of the job. - @param f Has a signature of void(std::shared_ptr). Called when the - job executes. - - @return shared_ptr to posted Coro. nullptr if post was not successful. - */ + /** + * Creates a coroutine and adds a job to the queue which will run it. + * + * @param t The type of job. + * @param name Name of the job. + * @param f Has a signature of void(std::shared_ptr). Called when the + * job executes. + * + * @return shared_ptr to posted Coro. nullptr if post was not successful. + */ template std::shared_ptr postCoro(JobType t, std::string const& name, F&& f); - /** Jobs waiting at this priority. + /** + * Jobs waiting at this priority. */ int getJobCount(JobType t) const; - /** Jobs waiting plus running at this priority. + /** + * Jobs waiting plus running at this priority. */ int getJobCountTotal(JobType t) const; - /** All waiting jobs at or greater than this priority. + /** + * All waiting jobs at or greater than this priority. */ int getJobCountGE(JobType t) const; - /** Return a scoped LoadEvent. + /** + * Return a scoped LoadEvent. */ std::unique_ptr makeLoadEvent(JobType t, std::string const& name); - /** Add multiple load events. + /** + * Add multiple load events. */ void addLoadEvents(JobType t, int count, std::chrono::milliseconds elapsed); @@ -216,7 +235,9 @@ public: json::Value getJson(int c = 0); - /** Block until no jobs running. */ + /** + * Block until no jobs running. + */ void rendezvous(); diff --git a/include/xrpl/core/JobTypeInfo.h b/include/xrpl/core/JobTypeInfo.h index b5db0dbaab..302a462ac6 100644 --- a/include/xrpl/core/JobTypeInfo.h +++ b/include/xrpl/core/JobTypeInfo.h @@ -8,21 +8,26 @@ namespace xrpl { -/** Holds all the 'static' information about a job, which does not change */ +/** + * Holds all the 'static' information about a job, which does not change + */ class JobTypeInfo { private: JobType const type_; std::string const name_; - /** The limit on the number of running jobs for this job type. - - A limit of 0 marks this as a "special job" which is not - dispatched via the job queue. + /** + * The limit on the number of running jobs for this job type. + * + * A limit of 0 marks this as a "special job" which is not + * dispatched via the job queue. */ int const limit_; - /** Average and peak latencies for this job type. 0 is none specified */ + /** + * Average and peak latencies for this job type. 0 is none specified + */ std::chrono::milliseconds const avgLatency_; std::chrono::milliseconds const peakLatency_; diff --git a/include/xrpl/core/NetworkIDService.h b/include/xrpl/core/NetworkIDService.h index 009f9ba6f8..8e2b3fcfe2 100644 --- a/include/xrpl/core/NetworkIDService.h +++ b/include/xrpl/core/NetworkIDService.h @@ -4,25 +4,27 @@ namespace xrpl { -/** Service that provides access to the network ID. - - This service provides read-only access to the network ID configured - for this server. The network ID identifies which network (mainnet, - testnet, devnet, or custom network) this server is configured to - connect to. - - Well-known network IDs: - - 0: Mainnet - - 1: Testnet - - 2: Devnet - - 1025+: Custom networks (require NetworkID field in transactions) -*/ +/** + * Service that provides access to the network ID. + * + * This service provides read-only access to the network ID configured + * for this server. The network ID identifies which network (mainnet, + * testnet, devnet, or custom network) this server is configured to + * connect to. + * + * Well-known network IDs: + * - 0: Mainnet + * - 1: Testnet + * - 2: Devnet + * - 1025+: Custom networks (require NetworkID field in transactions) + */ class NetworkIDService { public: virtual ~NetworkIDService() = default; - /** Get the configured network ID + /** + * Get the configured network ID * * @return The network ID this server is configured for */ diff --git a/include/xrpl/core/PeerReservationTable.h b/include/xrpl/core/PeerReservationTable.h index e6f6dd622e..c95c88b967 100644 --- a/include/xrpl/core/PeerReservationTable.h +++ b/include/xrpl/core/PeerReservationTable.h @@ -81,7 +81,7 @@ public: /** * @return the replaced reservation if it existed - * @throw soci::soci_error + * @throws soci::soci_error */ std::optional insertOrAssign(PeerReservation const& reservation); diff --git a/include/xrpl/core/ServiceRegistry.h b/include/xrpl/core/ServiceRegistry.h index 50bf2d7c10..592964134b 100644 --- a/include/xrpl/core/ServiceRegistry.h +++ b/include/xrpl/core/ServiceRegistry.h @@ -83,17 +83,17 @@ using RCLValidations = Validations; using NodeCache = TaggedCache; -/** Service registry for dependency injection. - - This abstract interface provides access to various services and components - used throughout the application. It separates the service locator pattern - from the Application lifecycle management. - - Components that need access to services can hold a reference to - ServiceRegistry rather than Application when they only need service - access and not lifecycle management. - -*/ +/** + * Service registry for dependency injection. + * + * This abstract interface provides access to various services and components + * used throughout the application. It separates the service locator pattern + * from the Application lifecycle management. + * + * Components that need access to services can hold a reference to + * ServiceRegistry rather than Application when they only need service + * access and not lifecycle management. + */ class ServiceRegistry { public: @@ -240,7 +240,9 @@ public: [[nodiscard]] virtual std::optional const& getTrapTxID() const = 0; - /** Retrieve the "wallet database" */ + /** + * Retrieve the "wallet database" + */ virtual DatabaseCon& getWalletDB() = 0; diff --git a/include/xrpl/core/detail/Workers.h b/include/xrpl/core/detail/Workers.h index d20ebf7a64..6829d5a14b 100644 --- a/include/xrpl/core/detail/Workers.h +++ b/include/xrpl/core/detail/Workers.h @@ -60,7 +60,9 @@ class PerfLog; class Workers { public: - /** Called to perform tasks as needed. */ + /** + * Called to perform tasks as needed. + */ struct Callback { virtual ~Callback() = default; @@ -69,27 +71,29 @@ public: Callback& operator=(Callback const&) = delete; - /** Perform a task. - - The call is made on a thread owned by Workers. It is important - that you only process one task from inside your callback. Each - call to addTask will result in exactly one call to processTask. - - @param instance The worker thread instance. - - @see Workers::addTask - */ + /** + * Perform a task. + * + * The call is made on a thread owned by Workers. It is important + * that you only process one task from inside your callback. Each + * call to addTask will result in exactly one call to processTask. + * + * @param instance The worker thread instance. + * + * @see Workers::addTask + */ virtual void processTask(int instance) = 0; }; - /** Create the object. - - A number of initial threads may be optionally specified. The - default is to create one thread per CPU. - - @param threadNames The name given to each created worker thread. - */ + /** + * Create the object. + * + * A number of initial threads may be optionally specified. The + * default is to create one thread per CPU. + * + * @param threadNames The name given to each created worker thread. + */ explicit Workers( Callback& callback, perf::PerfLog* perfLog, @@ -98,49 +102,54 @@ public: ~Workers(); - /** Retrieve the desired number of threads. - - This just returns the number of active threads that were requested. If - there was a recent call to setNumberOfThreads, the actual number of - active threads may be temporarily different from what was last requested. - - @note This function is not thread-safe. - */ + /** + * Retrieve the desired number of threads. + * + * This just returns the number of active threads that were requested. If + * there was a recent call to setNumberOfThreads, the actual number of + * active threads may be temporarily different from what was last requested. + * + * @note This function is not thread-safe. + */ [[nodiscard]] int getNumberOfThreads() const noexcept; - /** Set the desired number of threads. - @note This function is not thread-safe. - */ + /** + * Set the desired number of threads. + * @note This function is not thread-safe. + */ void setNumberOfThreads(int numberOfThreads); - /** Pause all threads and wait until they are paused. - - If a thread is processing a task it will pause as soon as the task - completes. There may still be tasks signaled even after all threads - have paused. - - @note This function is not thread-safe. - */ + /** + * Pause all threads and wait until they are paused. + * + * If a thread is processing a task it will pause as soon as the task + * completes. There may still be tasks signaled even after all threads + * have paused. + * + * @note This function is not thread-safe. + */ void stop(); - /** Add a task to be performed. - - Every call to addTask will eventually result in a call to - Callback::processTask unless the Workers object is destroyed or - the number of threads is never set above zero. - - @note This function is thread-safe. - */ + /** + * Add a task to be performed. + * + * Every call to addTask will eventually result in a call to + * Callback::processTask unless the Workers object is destroyed or + * the number of threads is never set above zero. + * + * @note This function is thread-safe. + */ void addTask(); - /** Get the number of currently executing calls of Callback::processTask. - While this function is thread-safe, the value may not stay - accurate for very long. It's mainly for diagnostic purposes. - */ + /** + * Get the number of currently executing calls of Callback::processTask. + * While this function is thread-safe, the value may not stay + * accurate for very long. It's mainly for diagnostic purposes. + */ [[nodiscard]] int numberOfCurrentlyRunningTasks() const noexcept; diff --git a/include/xrpl/core/detail/semaphore.h b/include/xrpl/core/detail/semaphore.h index e40463e322..abf6705097 100644 --- a/include/xrpl/core/detail/semaphore.h +++ b/include/xrpl/core/detail/semaphore.h @@ -45,14 +45,17 @@ private: public: using size_type = std::size_t; - /** Create the semaphore, with an optional initial count. - If unspecified, the initial count is zero. - */ + /** + * Create the semaphore, with an optional initial count. + * If unspecified, the initial count is zero. + */ explicit BasicSemaphore(size_type count = 0) : count_(count) { } - /** Increment the count and unblock one waiting thread. */ + /** + * Increment the count and unblock one waiting thread. + */ void notify() { @@ -61,7 +64,9 @@ public: cond_.notify_one(); } - /** Block until notify is called. */ + /** + * Block until notify is called. + */ void wait() { @@ -71,9 +76,10 @@ public: --count_; } - /** Perform a non-blocking wait. - @return `true` If the wait would be satisfied. - */ + /** + * Perform a non-blocking wait. + * @return `true` If the wait would be satisfied. + */ bool tryWait() { diff --git a/include/xrpl/crypto/RFC1751.h b/include/xrpl/crypto/RFC1751.h index 278f3c207b..3de65c3028 100644 --- a/include/xrpl/crypto/RFC1751.h +++ b/include/xrpl/crypto/RFC1751.h @@ -16,13 +16,14 @@ public: static void getEnglishFromKey(std::string& strHuman, std::string const& strKey); - /** Chooses a single dictionary word from the data. - - This is not particularly secure but it can be useful to provide - a unique name for something given a GUID or fixed data. We use - it to turn the pubkey_node into an easily remembered and identified - 4 character string. - */ + /** + * Chooses a single dictionary word from the data. + * + * This is not particularly secure but it can be useful to provide + * a unique name for something given a GUID or fixed data. We use + * it to turn the pubkey_node into an easily remembered and identified + * 4 character string. + */ static std::string getWordFromBlob(void const* blob, size_t bytes); diff --git a/include/xrpl/crypto/csprng.h b/include/xrpl/crypto/csprng.h index cdc6a723c8..e19d33a464 100644 --- a/include/xrpl/crypto/csprng.h +++ b/include/xrpl/crypto/csprng.h @@ -7,14 +7,15 @@ namespace xrpl { -/** A cryptographically secure random number engine - - The engine is thread-safe (it uses a lock to serialize - access) and will, automatically, mix in some randomness - from std::random_device. - - Meets the requirements of UniformRandomNumberEngine -*/ +/** + * A cryptographically secure random number engine + * + * The engine is thread-safe (it uses a lock to serialize + * access) and will, automatically, mix in some randomness + * from std::random_device. + * + * Meets the requirements of UniformRandomNumberEngine + */ class CsprngEngine { private: @@ -34,15 +35,21 @@ public: CsprngEngine(); ~CsprngEngine(); - /** Mix entropy into the pool */ + /** + * Mix entropy into the pool + */ void mixEntropy(void* buffer = nullptr, std::size_t count = 0); - /** Generate a random integer */ + /** + * Generate a random integer + */ result_type operator()(); - /** Fill a buffer with the requested amount of random data */ + /** + * Fill a buffer with the requested amount of random data + */ void operator()(void* ptr, std::size_t count); @@ -61,14 +68,15 @@ public: } }; -/** The default cryptographically secure PRNG - - Use this when you need to generate random numbers or - data that will be used for encryption or passed into - cryptographic routines. - - This meets the requirements of UniformRandomNumberEngine -*/ +/** + * The default cryptographically secure PRNG + * + * Use this when you need to generate random numbers or + * data that will be used for encryption or passed into + * cryptographic routines. + * + * This meets the requirements of UniformRandomNumberEngine + */ CsprngEngine& cryptoPrng(); diff --git a/include/xrpl/crypto/secure_erase.h b/include/xrpl/crypto/secure_erase.h index 74284b03f7..38531afc1d 100644 --- a/include/xrpl/crypto/secure_erase.h +++ b/include/xrpl/crypto/secure_erase.h @@ -4,20 +4,21 @@ namespace xrpl { -/** Attempts to clear the given blob of memory. - - The underlying implementation of this function takes pains to - attempt to outsmart the compiler from optimizing the clearing - away. Please note that, despite that, remnants of content may - remain floating around in memory as well as registers, caches - and more. - - For a more in-depth discussion of the subject please see the - below posts by Colin Percival: - - http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-buffer.html - http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html -*/ +/** + * Attempts to clear the given blob of memory. + * + * The underlying implementation of this function takes pains to + * attempt to outsmart the compiler from optimizing the clearing + * away. Please note that, despite that, remnants of content may + * remain floating around in memory as well as registers, caches + * and more. + * + * For a more in-depth discussion of the subject please see the + * below posts by Colin Percival: + * + * http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-buffer.html + * http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html + */ void secureErase(void* dest, std::size_t bytes); diff --git a/include/xrpl/json/JsonPropertyStream.h b/include/xrpl/json/JsonPropertyStream.h index 405a61cd34..498283c16b 100644 --- a/include/xrpl/json/JsonPropertyStream.h +++ b/include/xrpl/json/JsonPropertyStream.h @@ -8,7 +8,9 @@ namespace xrpl { -/** A PropertyStream::Sink which produces a json::Value of type ValueType::Object. */ +/** + * A PropertyStream::Sink which produces a json::Value of type ValueType::Object. + */ class JsonPropertyStream : public beast::PropertyStream { public: diff --git a/include/xrpl/json/Output.h b/include/xrpl/json/Output.h index c01253f713..53d453c277 100644 --- a/include/xrpl/json/Output.h +++ b/include/xrpl/json/Output.h @@ -17,18 +17,20 @@ stringOutput(std::string& s) return [&](boost::beast::string_view const& b) { s.append(b.data(), b.size()); }; } -/** Writes a minimal representation of a Json value to an Output in O(n) time. - - Data is streamed right to the output, so only a marginal amount of memory is - used. This can be very important for a very large json::Value. +/** + * Writes a minimal representation of a Json value to an Output in O(n) time. + * + * Data is streamed right to the output, so only a marginal amount of memory is + * used. This can be very important for a very large json::Value. */ void outputJson(json::Value const&, Output const&); -/** Return the minimal string representation of a json::Value in O(n) time. - - This requires a memory allocation for the full size of the output. - If possible, use outputJson(). +/** + * Return the minimal string representation of a json::Value in O(n) time. + * + * This requires a memory allocation for the full size of the output. + * If possible, use outputJson(). */ std::string jsonAsString(json::Value const&); diff --git a/include/xrpl/json/Writer.h b/include/xrpl/json/Writer.h index 024876a43c..ec7fd6a0d2 100644 --- a/include/xrpl/json/Writer.h +++ b/include/xrpl/json/Writer.h @@ -12,98 +12,98 @@ namespace json { /** - * Writer implements an O(1)-space, O(1)-granular output JSON writer. + * Writer implements an O(1)-space, O(1)-granular output JSON writer. * - * O(1)-space means that it uses a fixed amount of memory, and that there are - * no heap allocations at each step of the way. + * O(1)-space means that it uses a fixed amount of memory, and that there are + * no heap allocations at each step of the way. * - * O(1)-granular output means the writer only outputs in small segments of a - * bounded size, using a bounded number of CPU cycles in doing so. This is - * very helpful in scheduling long jobs. + * O(1)-granular output means the writer only outputs in small segments of a + * bounded size, using a bounded number of CPU cycles in doing so. This is + * very helpful in scheduling long jobs. * - * The tradeoff is that you have to fill items in the JSON tree as you go, - * and you can never go backward. + * The tradeoff is that you have to fill items in the JSON tree as you go, + * and you can never go backward. * - * Writer can write single JSON tokens, but the typical use is to write out an - * entire JSON object. For example: + * Writer can write single JSON tokens, but the typical use is to write out an + * entire JSON object. For example: * - * { - * Writer w (out); + * { + * Writer w (out); * - * w.startObject (); // Start the root object. - * w.set ("hello", "world"); - * w.set ("goodbye", 23); - * w.finishObject (); // Finish the root object. - * } + * w.startObject (); // Start the root object. + * w.set ("hello", "world"); + * w.set ("goodbye", 23); + * w.finishObject (); // Finish the root object. + * } * - * which outputs the string + * which outputs the string * - * {"hello":"world","goodbye":23} + * {"hello":"world","goodbye":23} * - * There can be an object inside an object: + * There can be an object inside an object: * - * { - * Writer w (out); + * { + * Writer w (out); * - * w.startObject (); // Start the root object. - * w.set ("hello", "world"); + * w.startObject (); // Start the root object. + * w.set ("hello", "world"); * - * w.startObjectSet ("subobject"); // Start a sub-object. - * w.set ("goodbye", 23); // Add a key, value assignment. - * w.finishObject (); // Finish the sub-object. + * w.startObjectSet ("subobject"); // Start a sub-object. + * w.set ("goodbye", 23); // Add a key, value assignment. + * w.finishObject (); // Finish the sub-object. * - * w.finishObject (); // Finish the root-object. - * } + * w.finishObject (); // Finish the root-object. + * } * - * which outputs the string + * which outputs the string * - * {"hello":"world","subobject":{"goodbye":23}}. + * {"hello":"world","subobject":{"goodbye":23}}. * - * Arrays work similarly + * Arrays work similarly * - * { - * Writer w (out); - * w.startObject (); // Start the root object. + * { + * Writer w (out); + * w.startObject (); // Start the root object. * - * w.startArraySet ("hello"); // Start an array. - * w.append (23) // Append some items. - * w.append ("skidoo") - * w.finishArray (); // Finish the array. + * w.startArraySet ("hello"); // Start an array. + * w.append (23) // Append some items. + * w.append ("skidoo") + * w.finishArray (); // Finish the array. * - * w.finishObject (); // Finish the root object. - * } + * w.finishObject (); // Finish the root object. + * } * - * which outputs the string + * which outputs the string * - * {"hello":[23,"skidoo"]}. + * {"hello":[23,"skidoo"]}. * * - * If you've reached the end of a long object, you can just use finishAll() - * which finishes all arrays and objects that you have started. + * If you've reached the end of a long object, you can just use finishAll() + * which finishes all arrays and objects that you have started. * - * { - * Writer w (out); - * w.startObject (); // Start the root object. + * { + * Writer w (out); + * w.startObject (); // Start the root object. * - * w.startArraySet ("hello"); // Start an array. - * w.append (23) // Append an item. + * w.startArraySet ("hello"); // Start an array. + * w.append (23) // Append an item. * - * w.startArrayAppend () // Start a sub-array. - * w.append ("one"); - * w.append ("two"); + * w.startArrayAppend () // Start a sub-array. + * w.append ("one"); + * w.append ("two"); * - * w.startObjectAppend (); // Append a sub-object. - * w.finishAll (); // Finish everything. - * } + * w.startObjectAppend (); // Append a sub-object. + * w.finishAll (); // Finish everything. + * } * - * which outputs the string + * which outputs the string * - * {"hello":[23,["one","two",{}]]}. + * {"hello":[23,["one","two",{}]]}. * - * For convenience, the destructor of Writer calls w.finishAll() which makes - * sure that all arrays and objects are closed. This means that you can throw - * an exception, or have a coroutine simply clean up the stack, and be sure - * that you do in fact generate a complete JSON object. + * For convenience, the destructor of Writer calls w.finishAll() which makes + * sure that all arrays and objects are closed. This means that you can throw + * an exception, or have a coroutine simply clean up the stack, and be sure + * that you do in fact generate a complete JSON object. */ class Writer @@ -118,26 +118,37 @@ public: ~Writer(); - /** Start a new collection at the root level. */ + /** + * Start a new collection at the root level. + */ void startRoot(CollectionType); - /** Start a new collection inside an array. */ + /** + * Start a new collection inside an array. + */ void startAppend(CollectionType); - /** Start a new collection inside an object. */ + /** + * Start a new collection inside an object. + */ void startSet(CollectionType, std::string const& key); - /** Finish the collection most recently started. */ + /** + * Finish the collection most recently started. + */ void finish(); - /** Finish all objects and arrays. After finishArray() has been called, no - * more operations can be performed. */ + /** + * Finish all objects and arrays. After finishArray() has been called, no + * more operations can be performed. + */ void finishAll(); - /** Append a value to an array. + /** + * Append a value to an array. * * Scalar must be a scalar - that is, a number, boolean, string, string * literal, nullptr or json::Value @@ -150,12 +161,15 @@ public: output(t); } - /** Add a comma before this next item if not the first item in an array. - Useful if you are writing the actual array yourself. */ + /** + * Add a comma before this next item if not the first item in an array. + * Useful if you are writing the actual array yourself. + */ void rawAppend(); - /** Add a key, value assignment to an object. + /** + * Add a key, value assignment to an object. * * Scalar must be a scalar - that is, a number, boolean, string, string * literal, or nullptr. @@ -174,8 +188,10 @@ public: output(t); } - /** Emit just "tag": as part of an object. Useful if you are writing the - actual value data yourself. */ + /** + * Emit just "tag": as part of an object. Useful if you are writing the + * actual value data yourself. + */ void rawSet(std::string const& key); @@ -194,22 +210,32 @@ public: void output(json::Value const&); - /** Output a null. */ + /** + * Output a null. + */ void output(std::nullptr_t); - /** Output a float. */ + /** + * Output a float. + */ void output(float); - /** Output a double. */ + /** + * Output a double. + */ void output(double); - /** Output a bool. */ + /** + * Output a bool. + */ void output(bool); - /** Output numbers or booleans. */ + /** + * Output numbers or booleans. + */ template void output(Type t) diff --git a/include/xrpl/json/json_reader.h b/include/xrpl/json/json_reader.h index d1e4ada579..ed60f49ce4 100644 --- a/include/xrpl/json/json_reader.h +++ b/include/xrpl/json/json_reader.h @@ -12,9 +12,9 @@ namespace json { -/** \brief Unserialize a JSON document into a +/** + * @brief Unserialize a JSON document into a * Value. - * */ class Reader { @@ -22,48 +22,55 @@ public: using Char = char; using Location = Char const*; - /** \brief Constructs a Reader allowing all features + /** + * @brief Constructs a Reader allowing all features * for parsing. */ Reader() = default; - /** \brief Read a Value from a JSON - * document. \param document UTF-8 encoded string containing the document to - * read. \param root [out] Contains the root value of the document if it was + /** + * @brief Read a Value from a JSON + * document. @param document UTF-8 encoded string containing the document to + * read. @param root [out] Contains the root value of the document if it was * successfully parsed. - * \return \c true if the document was successfully parsed, \c false if an + * @return @c true if the document was successfully parsed, @c false if an * error occurred. */ bool parse(std::string const& document, Value& root); - /** \brief Read a Value from a JSON - * document. \param document UTF-8 encoded string containing the document to - * read. \param root [out] Contains the root value of the document if it was + /** + * @brief Read a Value from a JSON + * document. @param document UTF-8 encoded string containing the document to + * read. @param root [out] Contains the root value of the document if it was * successfully parsed. - * \return \c true if the document was successfully parsed, \c false if an + * @return @c true if the document was successfully parsed, @c false if an * error occurred. */ bool parse(char const* beginDoc, char const* endDoc, Value& root); - /// \brief Parse from input stream. - /// \see json::operator>>(std::istream&, json::Value&). + /** + * @brief Parse from input stream. + * @see json::operator>>(std::istream&, json::Value&). + */ bool parse(std::istream& is, Value& root); - /** \brief Read a Value from a JSON buffer - * sequence. \param root [out] Contains the root value of the document if it - * was successfully parsed. \param UTF-8 encoded buffer sequence. \return \c - * true if the buffer was successfully parsed, \c false if an error + /** + * @brief Read a Value from a JSON buffer + * sequence. @param root [out] Contains the root value of the document if it + * was successfully parsed. @param UTF-8 encoded buffer sequence. @return @c + * true if the buffer was successfully parsed, @c false if an error * occurred. */ template bool parse(Value& root, BufferSequence const& bs); - /** \brief Returns a user friendly string that list errors in the parsed - * document. \return Formatted error message with the list of errors with + /** + * @brief Returns a user friendly string that list errors in the parsed + * document. @return Formatted error message with the list of errors with * their location in the parsed document. An empty string is returned if no * error occurred during parsing. */ @@ -195,30 +202,31 @@ Reader::parse(Value& root, BufferSequence const& bs) return parse(s, root); } -/** \brief Read from 'sin' into 'root'. - - Always keep comments from the input JSON. - - This can be used to read a file into a particular sub-object. - For example: - \code - json::Value root; - cin >> root["dir"]["file"]; - cout << root; - \endcode - Result: - \verbatim - { -"dir": { - "file": { - // The input stream JSON would be nested here. - } -} - } - \endverbatim - \throw std::exception on parse error. - \see json::operator<<() -*/ +/** + * @brief Read from 'sin' into 'root'. + * + * Always keep comments from the input JSON. + * + * This can be used to read a file into a particular sub-object. + * For example: + * @code + * json::Value root; + * cin >> root["dir"]["file"]; + * cout << root; + * @endcode + * Result: + * @verbatim + * { + * "dir": { + * "file": { + * // The input stream JSON would be nested here. + * } + * } + * } + * @endverbatim + * @throws std::exception on parse error. + * @see json::operator<<() + */ std::istream& operator>>(std::istream&, Value&); diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index f786c6a9dc..47ad3ac1e0 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -9,11 +9,13 @@ #include #include -/** \brief JSON (JavaScript Object Notation). +/** + * @brief JSON (JavaScript Object Notation). */ namespace json { -/** \brief Type of the value held by a Value object. +/** + * @brief Type of the value held by a Value object. */ enum class ValueType { Null = 0, ///< 'null' value @@ -26,19 +28,20 @@ enum class ValueType { Object ///< object value (collection of name/value pairs). }; -/** \brief Lightweight wrapper to tag static string. +/** + * @brief Lightweight wrapper to tag static string. * * Value constructor and ValueType::Object member assignment takes advantage of the * StaticString and avoid the cost of string duplication when storing the * string or the member name. * * Example of usage: - * \code + * @code * json::Value aValue( StaticString("some text") ); * json::Value object; * static const StaticString code("code"); * object[code] = 1234; - * \endcode + * @endcode */ class StaticString { @@ -99,7 +102,8 @@ operator!=(StaticString x, std::string const& y) return !(y == x); } -/** \brief Represents a JSON value. +/** + * @brief Represents a JSON value. * * This class is a discriminated union wrapper that can represent a: * - signed integer [range: Value::kMinInt - Value::kMaxInt] @@ -175,37 +179,39 @@ public: using ObjectValues = std::map; public: - /** \brief Create a default Value of the given type. - - This is a very useful constructor. - To create an empty array, pass ValueType::Array. - To create an empty object, pass ValueType::Object. - Another Value can then be set to this one by assignment. - This is useful since clear() and resize() will not alter types. - - Examples: - \code - json::Value null_value; // null - json::Value arr_value(json::ValueType::Array); // [] - json::Value obj_value(json::ValueType::Object); // {} - \endcode - */ + /** + * @brief Create a default Value of the given type. + * + * This is a very useful constructor. + * To create an empty array, pass ValueType::Array. + * To create an empty object, pass ValueType::Object. + * Another Value can then be set to this one by assignment. + * This is useful since clear() and resize() will not alter types. + * + * Examples: + * @code + * json::Value null_value; // null + * json::Value arr_value(json::ValueType::Array); // [] + * json::Value obj_value(json::ValueType::Object); // {} + * @endcode + */ Value(ValueType type = ValueType::Null); Value(Int value); Value(UInt value); Value(double value); Value(char const* value); Value(xrpl::Number const& value); - /** \brief Constructs a value from a static string. - + /** + * @brief Constructs a value from a static string. + * * Like other value string constructor but do not duplicate the string for * internal storage. The given string must remain alive after the call to - this + * this * constructor. * Example of usage: - * \code + * @code * json::Value aValue( StaticString("some text") ); - * \endcode + * @endcode */ Value(StaticString const& value); Value(std::string const& value); @@ -220,7 +226,9 @@ public: Value(Value&& other) noexcept; - /// Swap values. + /** + * Swap values. + */ void swap(Value& other) noexcept; @@ -229,7 +237,9 @@ public: [[nodiscard]] char const* asCString() const; - /** Returns the unquoted string value. */ + /** + * Returns the unquoted string value. + */ [[nodiscard]] std::string asString() const; [[nodiscard]] Int @@ -241,13 +251,17 @@ public: [[nodiscard]] bool asBool() const; - /** Correct absolute value from int or unsigned int */ + /** + * Correct absolute value from int or unsigned int + */ [[nodiscard]] UInt asAbsUInt() const; // TODO: What is the "empty()" method this docstring mentions? - /** isNull() tests to see if this field is null. Don't use this method to - test for emptiness: use empty(). */ + /** + * isNull() tests to see if this field is null. Don't use this method to + * test for emptiness: use empty(). + */ [[nodiscard]] bool isNull() const; [[nodiscard]] bool @@ -276,116 +290,157 @@ public: [[nodiscard]] bool isConvertibleTo(ValueType other) const; - /// Number of values in array or object + /** + * Number of values in array or object + */ [[nodiscard]] UInt size() const; - /** Returns false if this is an empty array, empty object, empty string, - or null. */ + /** + * Returns false if this is an empty array, empty object, empty string, + * or null. + */ explicit operator bool() const; - /// Remove all object members and array elements. - /// \pre type() is ValueType::Array, ValueType::Object, or ValueType::Null - /// \post type() is unchanged + /** + * Remove all object members and array elements. + * @pre type() is ValueType::Array, ValueType::Object, or ValueType::Null + * @post type() is unchanged + */ void clear(); - /// Access an array element (zero based index ). - /// If the array contains less than index element, then null value are - /// inserted in the array so that its size is index+1. (You may need to say - /// 'value[0u]' to get your compiler to distinguish - /// this from the operator[] which takes a string.) + /** + * Access an array element (zero based index ). + * If the array contains less than index element, then null value are + * inserted in the array so that its size is index+1. (You may need to say + * 'value[0u]' to get your compiler to distinguish + * this from the operator[] which takes a string.) + */ Value& operator[](UInt index); - /// Access an array element (zero based index ) - /// (You may need to say 'value[0u]' to get your compiler to distinguish - /// this from the operator[] which takes a string.) + /** + * Access an array element (zero based index ) + * (You may need to say 'value[0u]' to get your compiler to distinguish + * this from the operator[] which takes a string.) + */ Value const& operator[](UInt index) const; - /// If the array contains at least index+1 elements, returns the element - /// value, otherwise returns defaultValue. + /** + * If the array contains at least index+1 elements, returns the element + * value, otherwise returns defaultValue. + */ [[nodiscard]] Value get(UInt index, Value const& defaultValue) const; - /// Return true if index < size(). + /** + * Return true if index < size(). + */ [[nodiscard]] bool isValidIndex(UInt index) const; - /// \brief Append value to array at the end. - /// - /// Equivalent to jsonvalue[jsonvalue.size()] = value; + /** + * @brief Append value to array at the end. + * + * Equivalent to jsonvalue[jsonvalue.size()] = value; + */ Value& append(Value const& value); Value& append(Value&& value); - /// Access an object value by name, create a null member if it does not - /// exist. + /** + * Access an object value by name, create a null member if it does not + * exist. + */ Value& operator[](char const* key); - /// Access an object value by name, returns null if there is no member with - /// that name. + /** + * Access an object value by name, returns null if there is no member with + * that name. + */ Value const& operator[](char const* key) const; - /// Access an object value by name, create a null member if it does not - /// exist. + /** + * Access an object value by name, create a null member if it does not + * exist. + */ Value& operator[](std::string const& key); - /// Access an object value by name, returns null if there is no member with - /// that name. + /** + * Access an object value by name, returns null if there is no member with + * that name. + */ Value const& operator[](std::string const& key) const; - /** \brief Access an object value by name, create a null member if it does - not exist. - + /** + * @brief Access an object value by name, create a null member if it does + * not exist. + * * If the object as no entry for that name, then the member name used to - store + * store * the new entry is not duplicated. * Example of use: - * \code + * @code * json::Value object; * static const StaticString code("code"); * object[code] = 1234; - * \endcode + * @endcode */ Value& operator[](StaticString const& key); Value const& operator[](StaticString const& key) const; - /// Return the member named key if it exist, defaultValue otherwise. + /** + * Return the member named key if it exist, defaultValue otherwise. + */ Value get(char const* key, Value const& defaultValue) const; - /// Return the member named key if it exist, defaultValue otherwise. + /** + * Return the member named key if it exist, defaultValue otherwise. + */ [[nodiscard]] Value get(std::string const& key, Value const& defaultValue) const; - /// \brief Remove and return the named member. - /// - /// Do nothing if it did not exist. - /// \return the removed Value, or null. - /// \pre type() is ValueType::Object or ValueType::Null - /// \post type() is unchanged + /** + * @brief Remove and return the named member. + * + * Do nothing if it did not exist. + * @return the removed Value, or null. + * @pre type() is ValueType::Object or ValueType::Null + * @post type() is unchanged + */ Value removeMember(char const* key); - /// Same as removeMember(const char*) + /** + * Same as removeMember(const char*) + */ Value removeMember(std::string const& key); - /// Return true if the object has a member named key. + /** + * Return true if the object has a member named key. + */ bool isMember(char const* key) const; - /// Return true if the object has a member named key. + /** + * Return true if the object has a member named key. + */ [[nodiscard]] bool isMember(std::string const& key) const; - /// Return true if the object has a member named key. + /** + * Return true if the object has a member named key. + */ [[nodiscard]] bool isMember(StaticString const& key) const; - /// \brief Return a list of the member names. - /// - /// If null, return an empty list. - /// \pre type() is ValueType::Object or ValueType::Null - /// \post if type() was ValueType::Null, it remains ValueType::Null + /** + * @brief Return a list of the member names. + * + * If null, return an empty list. + * @pre type() is ValueType::Object or ValueType::Null + * @post if type() was ValueType::Null, it remains ValueType::Null + */ [[nodiscard]] Members getMemberNames() const; @@ -461,7 +516,8 @@ operator>=(Value const& x, Value const& y) return !(x < y); } -/** \brief Experimental do not use: Allocator to customize member name and +/** + * @brief Experimental do not use: Allocator to customize member name and * string value memory management done by Value. * * - makeMemberName() and releaseMemberName() are called to respectively @@ -486,8 +542,8 @@ public: releaseStringValue(char* value) = 0; }; -/** \brief base class for Value iterators. - * +/** + * @brief base class for Value iterators. */ class ValueIteratorBase { @@ -512,17 +568,23 @@ public: return !isEqual(other); } - /// Return either the index or the member name of the referenced value as a - /// Value. + /** + * Return either the index or the member name of the referenced value as a + * Value. + */ [[nodiscard]] Value key() const; - /// Return the index of the referenced Value. -1 if it is not an ValueType::Array. + /** + * Return the index of the referenced Value. -1 if it is not an ValueType::Array. + */ [[nodiscard]] UInt index() const; - /// Return the member name of the referenced Value. "" if it is not an - /// ValueType::Object. + /** + * Return the member name of the referenced Value. "" if it is not an + * ValueType::Object. + */ [[nodiscard]] char const* memberName() const; @@ -551,8 +613,8 @@ private: bool isNull_; }; -/** \brief const iterator for object and array value. - * +/** + * @brief const iterator for object and array value. */ class ValueConstIterator : public ValueIteratorBase { @@ -569,7 +631,8 @@ public: ValueConstIterator(ValueConstIterator const& other) = default; private: - /*! \internal Use by Value to create an iterator. + /** + * @internal Use by Value to create an iterator. */ explicit ValueConstIterator(Value::ObjectValues::iterator const& current); @@ -614,7 +677,8 @@ public: } }; -/** \brief Iterator for object and array value. +/** + * @brief Iterator for object and array value. */ class ValueIterator : public ValueIteratorBase { @@ -632,7 +696,8 @@ public: ValueIterator(ValueIterator const& other); private: - /*! \internal Use by Value to create an iterator. + /** + * @internal Use by Value to create an iterator. */ explicit ValueIterator(Value::ObjectValues::iterator const& current); diff --git a/include/xrpl/json/json_writer.h b/include/xrpl/json/json_writer.h index 4bc15b71da..65c8b20931 100644 --- a/include/xrpl/json/json_writer.h +++ b/include/xrpl/json/json_writer.h @@ -13,7 +13,8 @@ namespace json { class Value; -/** \brief Abstract class for writers. +/** + * @brief Abstract class for writers. */ class WriterBase { @@ -23,12 +24,13 @@ public: write(Value const& root) = 0; }; -/** \brief Outputs a Value in JSON format +/** + * @brief Outputs a Value in JSON format * without formatting (not human friendly). * * The JSON document is written in a single line. It is not intended for 'human' * consumption, but may be useful to support feature such as RPC where bandwidth - * is limited. \sa Reader, Value + * is limited. @see Reader, Value */ class FastWriter : public WriterBase @@ -48,7 +50,8 @@ private: std::string document_; }; -/** \brief Writes a Value in JSON format in a +/** + * @brief Writes a Value in JSON format in a * human friendly way. * * The rules for line break and indent are as follow: @@ -64,7 +67,7 @@ private: * - otherwise, it the values do not fit on one line, or the array contains * object or non empty array, then print one value per line. * - * \sa Reader, Value + * @see Reader, Value */ class StyledWriter : public WriterBase { @@ -73,8 +76,9 @@ public: ~StyledWriter() override = default; public: // overridden from Writer - /** \brief Serialize a Value in JSON - * format. \param root Value to serialize. \return String containing the + /** + * @brief Serialize a Value in JSON + * format. @param root Value to serialize. @return String containing the * JSON document that represents the root value. */ std::string @@ -108,26 +112,27 @@ private: bool addChildValues_{}; }; -/** \brief Writes a Value in JSON format in a - human friendly way, to a stream rather than to a string. +/** + * @brief Writes a Value in JSON format in a + * human friendly way, to a stream rather than to a string. * * The rules for line break and indent are as follow: * - Object value: * - if empty then print {} without indent and line break * - if not empty the print '{', line break & indent, print one value per - line + * line * and then unindent and line break and print '}'. * - Array value: * - if empty then print [] without indent and line break * - if the array contains no object value, empty array or some other value - types, + * types, * and all the values fit on one lines, then print the array on a single - line. + * line. * - otherwise, it the values do not fit on one line, or the array contains * object or non empty array, then print one value per line. * - * \param indentation Each level will be indented by this amount extra. - * \sa Reader, Value + * @param indentation Each level will be indented by this amount extra. + * @see Reader, Value */ class StyledStreamWriter { @@ -136,10 +141,11 @@ public: ~StyledStreamWriter() = default; public: - /** \brief Serialize a Value in JSON - * format. \param out Stream to write to. (Can be ostringstream, e.g.) - * \param root Value to serialize. - * \note There is no point in deriving from Writer, since write() should not + /** + * @brief Serialize a Value in JSON + * format. @param out Stream to write to. (Can be ostringstream, e.g.) + * @param root Value to serialize. + * @note There is no point in deriving from Writer, since write() should not * return a value. */ void @@ -184,8 +190,10 @@ valueToString(bool value); std::string valueToQuotedString(char const* value); -/// \brief Output using the StyledStreamWriter. -/// \see json::operator>>() +/** + * @brief Output using the StyledStreamWriter. + * @see json::operator>>() + */ std::ostream& operator<<(std::ostream&, Value const& root); @@ -265,12 +273,13 @@ writeValue(Write const& write, Value const& value) } // namespace detail -/** Stream compact JSON to the specified function. - - @param jv The json::Value to write - @param write Invocable with signature void(void const*, std::size_t) that - is called when output should be written to the stream. -*/ +/** + * Stream compact JSON to the specified function. + * + * @param jv The json::Value to write + * @param write Invocable with signature void(void const*, std::size_t) that + * is called when output should be written to the stream. + */ template void stream(json::Value const& jv, Write const& write) @@ -279,29 +288,31 @@ stream(json::Value const& jv, Write const& write) write("\n", 1); } -/** Decorator for streaming out compact json - - Use - - json::Value jv; - out << json::Compact{jv} - - to write a single-line, compact version of `jv` to the stream, rather - than the styled format that comes from undecorated streaming. -*/ +/** + * Decorator for streaming out compact json + * + * Use + * + * json::Value jv; + * out << json::Compact{jv} + * + * to write a single-line, compact version of `jv` to the stream, rather + * than the styled format that comes from undecorated streaming. + */ class Compact { json::Value jv_; public: - /** Wrap a json::Value for compact streaming - - @param jv The json::Value to stream - - @note For now, we do not support wrapping lvalues to avoid - potentially costly copies. If we find a need, we can consider - adding support for compact lvalue streaming in the future. - */ + /** + * Wrap a json::Value for compact streaming + * + * @param jv The json::Value to stream + * + * @note For now, we do not support wrapping lvalues to avoid + * potentially costly copies. If we find a need, we can consider + * adding support for compact lvalue streaming in the future. + */ Compact(json::Value&& jv) : jv_{std::move(jv)} { } diff --git a/include/xrpl/json/to_string.h b/include/xrpl/json/to_string.h index 1d7b4c785a..bdd7a51e6a 100644 --- a/include/xrpl/json/to_string.h +++ b/include/xrpl/json/to_string.h @@ -6,11 +6,15 @@ namespace json { -/** Writes a json::Value to an std::string. */ +/** + * Writes a json::Value to an std::string. + */ std::string to_string(Value const&); -/** Writes a json::Value to an std::string. */ +/** + * Writes a json::Value to an std::string. + */ std::string pretty(Value const&); diff --git a/include/xrpl/ledger/AcceptedLedgerTx.h b/include/xrpl/ledger/AcceptedLedgerTx.h index f59b8a074d..283dcf6e24 100644 --- a/include/xrpl/ledger/AcceptedLedgerTx.h +++ b/include/xrpl/ledger/AcceptedLedgerTx.h @@ -21,17 +21,17 @@ namespace xrpl { /** - A transaction that is in a closed ledger. - - Description - - An accepted ledger transaction contains additional information that the - server needs to tell clients about the transaction. For example, - - The transaction in JSON form - - Which accounts are affected - * This is used by InfoSub to report to clients - - Cached stuff -*/ + * A transaction that is in a closed ledger. + * + * Description + * + * An accepted ledger transaction contains additional information that the + * server needs to tell clients about the transaction. For example, + * - The transaction in JSON form + * - Which accounts are affected + * * This is used by InfoSub to report to clients + * - Cached stuff + */ class AcceptedLedgerTx : public CountedObject { public: diff --git a/include/xrpl/ledger/AmendmentTable.h b/include/xrpl/ledger/AmendmentTable.h index 6598be5a5c..c3ef779eb1 100644 --- a/include/xrpl/ledger/AmendmentTable.h +++ b/include/xrpl/ledger/AmendmentTable.h @@ -37,10 +37,11 @@ namespace xrpl { class ServiceRegistry; -/** The amendment table stores the list of enabled and potential amendments. - Individuals amendments are voted on by validators during the consensus - process. -*/ +/** + * The amendment table stores the list of enabled and potential amendments. + * Individuals amendments are voted on by validators during the consensus + * process. + */ class AmendmentTable { public: @@ -90,11 +91,15 @@ public: [[nodiscard]] virtual json::Value getJson(bool isAdmin) const = 0; - /** Returns a json::ValueType::Object. */ + /** + * Returns a json::ValueType::Object. + */ [[nodiscard]] virtual json::Value getJson(uint256 const& amendment, bool isAdmin) const = 0; - /** Called when a new fully-validated ledger is accepted. */ + /** + * Called when a new fully-validated ledger is accepted. + */ void doValidatedLedger(std::shared_ptr const& lastValidatedLedger) { @@ -107,9 +112,10 @@ public: } } - /** Called to determine whether the amendment logic needs to process - a new validated ledger. (If it could have changed things.) - */ + /** + * Called to determine whether the amendment logic needs to process + * a new validated ledger. (If it could have changed things.) + */ [[nodiscard]] virtual bool needValidatedLedger(LedgerIndex seq) const = 0; diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index e519013d9a..724d89b7c6 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include // IWYU pragma: keep @@ -11,6 +12,7 @@ #include #include #include +#include #include #include @@ -90,47 +92,50 @@ operator&=(ApplyFlags& lhs, ApplyFlags const& rhs) //------------------------------------------------------------------------------ -/** Writeable view to a ledger, for applying a transaction. - - This refinement of ReadView provides an interface where - the SLE can be "checked out" for modifications and put - back in an updated or removed state. Also added is an - interface to provide contextual information necessary - to calculate the results of transaction processing, - including the metadata if the view is later applied to - the parent (using an interface in the derived class). - The context info also includes values from the base - ledger such as sequence number and the network time. - - This allows implementations to journal changes made to - the state items in a ledger, with the option to apply - those changes to the base or discard the changes without - affecting the base. - - Typical usage is to call read() for non-mutating - operations. - - For mutating operations the sequence is as follows: - - // Add a new value - v.insert(sle); - - // Check out a value for modification - sle = v.peek(k); - - // Indicate that changes were made - v.update(sle) - - // Or, erase the value - v.erase(sle) - - The invariant is that insert, update, and erase may not - be called with any SLE which belongs to different view. -*/ +/** + * Writeable view to a ledger, for applying a transaction. + * + * This refinement of ReadView provides an interface where + * the SLE can be "checked out" for modifications and put + * back in an updated or removed state. Also added is an + * interface to provide contextual information necessary + * to calculate the results of transaction processing, + * including the metadata if the view is later applied to + * the parent (using an interface in the derived class). + * The context info also includes values from the base + * ledger such as sequence number and the network time. + * + * This allows implementations to journal changes made to + * the state items in a ledger, with the option to apply + * those changes to the base or discard the changes without + * affecting the base. + * + * Typical usage is to call read() for non-mutating + * operations. + * + * For mutating operations the sequence is as follows: + * + * // Add a new value + * v.insert(sle); + * + * // Check out a value for modification + * sle = v.peek(k); + * + * // Indicate that changes were made + * v.update(sle) + * + * // Or, erase the value + * v.erase(sle) + * + * The invariant is that insert, update, and erase may not + * be called with any SLE which belongs to different view. + */ class ApplyView : public ReadView { private: - /** Add an entry to a directory using the specified insert strategy */ + /** + * Add an entry to a directory using the specified insert strategy + */ std::optional dirAdd( bool preserveOrder, @@ -141,84 +146,89 @@ private: public: ApplyView() = default; - /** Returns the tx apply flags. - - Flags can affect the outcome of transaction - processing. For example, transactions applied - to an open ledger generate "local" failures, - while transactions applied to the consensus - ledger produce hard failures (and claim a fee). - */ + /** + * Returns the tx apply flags. + * + * Flags can affect the outcome of transaction + * processing. For example, transactions applied + * to an open ledger generate "local" failures, + * while transactions applied to the consensus + * ledger produce hard failures (and claim a fee). + */ [[nodiscard]] virtual ApplyFlags flags() const = 0; - /** Prepare to modify the SLE associated with key. - - Effects: - - Gives the caller ownership of a modifiable - SLE associated with the specified key. - - The returned SLE may be used in a subsequent - call to erase or update. - - The SLE must not be passed to any other ApplyView. - - @return `nullptr` if the key is not present - */ + /** + * Prepare to modify the SLE associated with key. + * + * Effects: + * + * Gives the caller ownership of a modifiable + * SLE associated with the specified key. + * + * The returned SLE may be used in a subsequent + * call to erase or update. + * + * The SLE must not be passed to any other ApplyView. + * + * @return `nullptr` if the key is not present + */ virtual SLE::pointer peek(Keylet const& k) = 0; - /** Remove a peeked SLE. - - Requirements: - - `sle` was obtained from prior call to peek() - on this instance of the RawView. - - Effects: - - The key is no longer associated with the SLE. - */ + /** + * Remove a peeked SLE. + * + * Requirements: + * + * `sle` was obtained from prior call to peek() + * on this instance of the RawView. + * + * Effects: + * + * The key is no longer associated with the SLE. + */ virtual void erase(SLE::ref sle) = 0; - /** Insert a new state SLE - - Requirements: - - `sle` was not obtained from any calls to - peek() on any instances of RawView. - - The SLE's key must not already exist. - - Effects: - - The key in the state map is associated - with the SLE. - - The RawView acquires ownership of the shared_ptr. - - @note The key is taken from the SLE - */ + /** + * Insert a new state SLE + * + * Requirements: + * + * `sle` was not obtained from any calls to + * peek() on any instances of RawView. + * + * The SLE's key must not already exist. + * + * Effects: + * + * The key in the state map is associated + * with the SLE. + * + * The RawView acquires ownership of the shared_ptr. + * + * @note The key is taken from the SLE + */ virtual void insert(SLE::ref sle) = 0; - /** Indicate changes to a peeked SLE - - Requirements: - - The SLE's key must exist. - - `sle` was obtained from prior call to peek() - on this instance of the RawView. - - Effects: - - The SLE is updated - - @note The key is taken from the SLE - */ + /** + * Indicate changes to a peeked SLE + * + * Requirements: + * + * The SLE's key must exist. + * + * `sle` was obtained from prior call to peek() + * on this instance of the RawView. + * + * Effects: + * + * The SLE is updated + * + * @note The key is taken from the SLE + */ /** @{ */ virtual void update(SLE::ref sle) = 0; @@ -248,7 +258,8 @@ public: XRPL_ASSERT(amount.holds(), "creditHookMPT: amount is for MPTIssue"); } - /** Facilitate tracking of MPT sold by an issuer owning MPT sell offer. + /** + * Facilitate tracking of MPT sold by an issuer owning MPT sell offer. * Unlike IOU, MPT doesn't have bi-directional relationship with an issuer, * where a trustline limits an amount that can be issued to a holder. * Consequently, the credit step (last MPTEndpointStep or @@ -288,27 +299,28 @@ public: // Called when the owner count changes // This is required to support PaymentSandbox virtual void - adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) + adjustOwnerCountHook(AccountID const& account, OwnerCounts const& cur, OwnerCounts const& next) { } - /** Append an entry to a directory - - Entries in the directory will be stored in order of insertion, i.e. new - entries will always be added at the tail end of the last page. - - @param directory the base of the directory - @param key the entry to insert - @param describe callback to add required entries to a new page - - @return a \c std::optional which, if insertion was successful, - will contain the page number in which the item was stored. - - @note this function may create a page (including a root page), if no - page with space is available. This function will only fail if the - page counter exceeds the protocol-defined maximum number of - allowable pages. - */ + /** + * Append an entry to a directory + * + * Entries in the directory will be stored in order of insertion, i.e. new + * entries will always be added at the tail end of the last page. + * + * @param directory the base of the directory + * @param key the entry to insert + * @param describe callback to add required entries to a new page + * + * @return a @c std::optional which, if insertion was successful, + * will contain the page number in which the item was stored. + * + * @note this function may create a page (including a root page), if no + * page with space is available. This function will only fail if the + * page counter exceeds the protocol-defined maximum number of + * allowable pages. + */ /** @{ */ std::optional dirAppend( @@ -331,23 +343,24 @@ public: } /** @} */ - /** Insert an entry to a directory - - Entries in the directory will be stored in a semi-random order, but - each page will be maintained in sorted order. - - @param directory the base of the directory - @param key the entry to insert - @param describe callback to add required entries to a new page - - @return a \c std::optional which, if insertion was successful, - will contain the page number in which the item was stored. - - @note this function may create a page (including a root page), if no - page with space is available.this function will only fail if the - page counter exceeds the protocol-defined maximum number of - allowable pages. - */ + /** + * Insert an entry to a directory + * + * Entries in the directory will be stored in a semi-random order, but + * each page will be maintained in sorted order. + * + * @param directory the base of the directory + * @param key the entry to insert + * @param describe callback to add required entries to a new page + * + * @return a @c std::optional which, if insertion was successful, + * will contain the page number in which the item was stored. + * + * @note this function may create a page (including a root page), if no + * page with space is available.this function will only fail if the + * page counter exceeds the protocol-defined maximum number of + * allowable pages. + */ /** @{ */ std::optional dirInsert( @@ -368,21 +381,22 @@ public: } /** @} */ - /** Remove an entry from a directory - - @param directory the base of the directory - @param page the page number for this page - @param key the entry to remove - @param keepRoot if deleting the last entry, don't - delete the root page (i.e. the directory itself). - - @return \c true if the entry was found and deleted and - \c false otherwise. - - @note This function will remove zero or more pages from the directory; - the root page will not be deleted even if it is empty, unless - \p keepRoot is not set and the directory is empty. - */ + /** + * Remove an entry from a directory + * + * @param directory the base of the directory + * @param page the page number for this page + * @param key the entry to remove + * @param keepRoot if deleting the last entry, don't + * delete the root page (i.e. the directory itself). + * + * @return @c true if the entry was found and deleted and + * @c false otherwise. + * + * @note This function will remove zero or more pages from the directory; + * the root page will not be deleted even if it is empty, unless + * \p keepRoot is not set and the directory is empty. + */ /** @{ */ bool dirRemove(Keylet const& directory, std::uint64_t page, uint256 const& key, bool keepRoot); @@ -394,29 +408,51 @@ public: } /** @} */ - /** Remove the specified directory, invoking the callback for every node. */ + /** + * Remove the specified directory, invoking the callback for every node. + */ bool dirDelete(Keylet const& directory, std::function const&); - /** Remove the specified directory, if it is empty. - - @param directory the identifier of the directory node to be deleted - @return \c true if the directory was found and was successfully deleted - \c false otherwise. - - @note The function should only be called with the root entry (i.e. with - the first page) of a directory. - */ + /** + * Remove the specified directory, if it is empty. + * + * @param directory the identifier of the directory node to be deleted + * @return @c true if the directory was found and was successfully deleted + * @c false otherwise. + * + * @note The function should only be called with the root entry (i.e. with + * the first page) of a directory. + */ bool emptyDirDelete(Keylet const& directory); }; -namespace directory { -/** Helper functions for managing low-level directory operations. - These are not part of the ApplyView interface. +/** + * Bundles the mutable ledger view and the transaction being applied. + * + * Passed together to avoid threading two separate parameters through every + * helper that needs both the view (for state reads/writes) and the + * transaction (for field inspection and metadata). + * + * Both members are non-owning references; the caller is responsible for + * ensuring that the referenced objects outlive the ApplyViewContext. + * + * TODO: replace with ApplyContext after it's untangled with xrpl/tx + */ +struct ApplyViewContext +{ + ApplyView& view; + STTx const& tx; +}; - Don't use them unless you really, really know what you're doing. - Instead use dirAdd, dirInsert, etc. +namespace directory { +/** + * Helper functions for managing low-level directory operations. + * These are not part of the ApplyView interface. + * + * Don't use them unless you really, really know what you're doing. + * Instead use dirAdd, dirInsert, etc. */ std::uint64_t diff --git a/include/xrpl/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h index 9a3734a8ca..630153f90a 100644 --- a/include/xrpl/ledger/ApplyViewImpl.h +++ b/include/xrpl/ledger/ApplyViewImpl.h @@ -18,12 +18,13 @@ namespace xrpl { -/** Editable, discardable view that can build metadata for one tx. - - Iteration of the tx map is delegated to the base. - - @note Presented as ApplyView to clients. -*/ +/** + * Editable, discardable view that can build metadata for one tx. + * + * Iteration of the tx map is delegated to the base. + * + * @note Presented as ApplyView to clients. + */ class ApplyViewImpl final : public detail::ApplyViewBase { public: @@ -37,12 +38,13 @@ public: ApplyViewImpl(ApplyViewImpl&&) = default; ApplyViewImpl(ReadView const* base, ApplyFlags flags); - /** Apply the transaction. - - After a call to `apply`, the only valid - operation on this object is to call the - destructor. - */ + /** + * Apply the transaction. + * + * After a call to `apply`, the only valid + * operation on this object is to call the + * destructor. + */ std::optional apply( OpenView& to, @@ -52,25 +54,28 @@ public: bool isDryRun, beast::Journal j); - /** Set the amount of currency delivered. - - This value is used when generating metadata - for payments, to set the DeliveredAmount field. - If the amount is not specified, the field is - excluded from the resulting metadata. - */ + /** + * Set the amount of currency delivered. + * + * This value is used when generating metadata + * for payments, to set the DeliveredAmount field. + * If the amount is not specified, the field is + * excluded from the resulting metadata. + */ void deliver(STAmount const& amount) { deliver_ = amount; } - /** Get the number of modified entries + /** + * Get the number of modified entries */ std::size_t size(); - /** Visit modified entries + /** + * Visit modified entries */ void visit( diff --git a/include/xrpl/ledger/CachedView.h b/include/xrpl/ledger/CachedView.h index 1da3a67563..b9e2cf8d66 100644 --- a/include/xrpl/ledger/CachedView.h +++ b/include/xrpl/ledger/CachedView.h @@ -133,10 +133,11 @@ public: } // namespace detail -/** Wraps a DigestAwareReadView to provide caching. - - @tparam Base A subclass of DigestAwareReadView -*/ +/** + * Wraps a DigestAwareReadView to provide caching. + * + * @tparam Base A subclass of DigestAwareReadView + */ template class CachedView : public detail::CachedViewImpl { @@ -158,10 +159,11 @@ public: { } - /** Returns the base type. - - @note This breaks encapsulation and bypasses the cache. - */ + /** + * Returns the base type. + * + * @note This breaks encapsulation and bypasses the cache. + */ std::shared_ptr const& base() const { diff --git a/include/xrpl/ledger/CanonicalTXSet.h b/include/xrpl/ledger/CanonicalTXSet.h index f8349dfab6..11aadf4e92 100644 --- a/include/xrpl/ledger/CanonicalTXSet.h +++ b/include/xrpl/ledger/CanonicalTXSet.h @@ -13,13 +13,13 @@ namespace xrpl { -/** Holds transactions which were deferred to the next pass of consensus. - - "Canonical" refers to the order in which transactions are applied. - - - Puts transactions from the same account in SeqProxy order - -*/ +/** + * Holds transactions which were deferred to the next pass of consensus. + * + * "Canonical" refers to the order in which transactions are applied. + * + * - Puts transactions from the same account in SeqProxy order + */ // VFALCO TODO rename to SortedTxSet class CanonicalTXSet : public CountedObject { diff --git a/include/xrpl/ledger/Dir.h b/include/xrpl/ledger/Dir.h index 05df887d8b..233719cdeb 100644 --- a/include/xrpl/ledger/Dir.h +++ b/include/xrpl/ledger/Dir.h @@ -13,18 +13,19 @@ namespace xrpl { -/** A class that simplifies iterating ledger directory pages - - The Dir class provides a forward iterator for walking through - the uint256 values contained in ledger directories. - - The Dir class also allows accelerated directory walking by - stepping directly from one page to the next using the next_page() - member function. - - As of July 2024, the Dir class is only being used with NFTokenOffer - directories and for unit tests. -*/ +/** + * A class that simplifies iterating ledger directory pages + * + * The Dir class provides a forward iterator for walking through + * the uint256 values contained in ledger directories. + * + * The Dir class also allows accelerated directory walking by + * stepping directly from one page to the next using the next_page() + * member function. + * + * As of July 2024, the Dir class is only being used with NFTokenOffer + * directories and for unit tests. + */ class Dir { private: diff --git a/include/xrpl/ledger/Ledger.h b/include/xrpl/ledger/Ledger.h index 3453389a5e..e1dd2c422e 100644 --- a/include/xrpl/ledger/Ledger.h +++ b/include/xrpl/ledger/Ledger.h @@ -42,32 +42,33 @@ struct CreateGenesisT }; extern CreateGenesisT const kCreateGenesis; -/** Holds a ledger. - - The ledger is composed of two SHAMaps. The state map holds all of the - ledger entries such as account roots and order books. The tx map holds - all of the transactions and associated metadata that made it into that - particular ledger. Most of the operations on a ledger are concerned - with the state map. - - This can hold just the header, a partial set of data, or the entire set - of data. It all depends on what is in the corresponding SHAMap entry. - Various functions are provided to populate or depopulate the caches that - the object holds references to. - - Ledgers are constructed as either mutable or immutable. - - 1) If you are the sole owner of a mutable ledger, you can do whatever you - want with no need for locks. - - 2) If you have an immutable ledger, you cannot ever change it, so no need - for locks. - - 3) Mutable ledgers cannot be shared. - - @note Presented to clients as ReadView - @note Calls virtuals in the constructor, so marked as final -*/ +/** + * Holds a ledger. + * + * The ledger is composed of two SHAMaps. The state map holds all of the + * ledger entries such as account roots and order books. The tx map holds + * all of the transactions and associated metadata that made it into that + * particular ledger. Most of the operations on a ledger are concerned + * with the state map. + * + * This can hold just the header, a partial set of data, or the entire set + * of data. It all depends on what is in the corresponding SHAMap entry. + * Various functions are provided to populate or depopulate the caches that + * the object holds references to. + * + * Ledgers are constructed as either mutable or immutable. + * + * 1) If you are the sole owner of a mutable ledger, you can do whatever you + * want with no need for locks. + * + * 2) If you have an immutable ledger, you cannot ever change it, so no need + * for locks. + * + * 3) Mutable ledgers cannot be shared. + * + * @note Presented to clients as ReadView + * @note Calls virtuals in the constructor, so marked as final + */ class Ledger final : public std::enable_shared_from_this, public DigestAwareReadView, public TxsRawView, @@ -82,20 +83,21 @@ public: Ledger& operator=(Ledger&&) = delete; - /** Create the Genesis ledger. - - The Genesis ledger contains a single account whose - AccountID is generated with a Generator using the seed - computed from the string "masterpassphrase" and ordinal - zero. - - The account has an XRP balance equal to the total amount - of XRP in the system. No more XRP than the amount which - starts in this account can ever exist, with amounts - used to pay fees being destroyed. - - Amendments specified are enabled in the genesis ledger - */ + /** + * Create the Genesis ledger. + * + * The Genesis ledger contains a single account whose + * AccountID is generated with a Generator using the seed + * computed from the string "masterpassphrase" and ordinal + * zero. + * + * The account has an XRP balance equal to the total amount + * of XRP in the system. No more XRP than the amount which + * starts in this account can ever exist, with amounts + * used to pay fees being destroyed. + * + * Amendments specified are enabled in the genesis ledger + */ Ledger( CreateGenesisT, Rules rules, @@ -105,13 +107,14 @@ public: Ledger(LedgerHeader const& info, Rules rules, Family& family); - /** Used for ledgers loaded from JSON files - - @param acquire If true, acquires the ledger if not found locally - - @note The fees parameter provides default values, but setup() may - override them from the ledger state if fee-related SLEs exist. - */ + /** + * Used for ledgers loaded from JSON files + * + * @param acquire If true, acquires the ledger if not found locally + * + * @note The fees parameter provides default values, but setup() may + * override them from the ledger state if fee-related SLEs exist. + */ Ledger( LedgerHeader const& info, bool& loaded, @@ -121,12 +124,13 @@ public: Family& family, beast::Journal j); - /** Create a new ledger following a previous ledger - - The ledger will have the sequence number that - follows previous, and have - parentCloseTime == previous.closeTime. - */ + /** + * Create a new ledger following a previous ledger + * + * The ledger will have the sequence number that + * follows previous, and have + * parentCloseTime == previous.closeTime. + */ Ledger(Ledger const& previous, NetClock::time_point closeTime); // used for database ledgers @@ -369,11 +373,15 @@ public: void updateNegativeUNL(); - /** Returns true if the ledger is a flag ledger */ + /** + * Returns true if the ledger is a flag ledger + */ bool isFlagLedger() const; - /** Returns true if the ledger directly precedes a flag ledger */ + /** + * Returns true if the ledger directly precedes a flag ledger + */ bool isVotingLedger() const; @@ -387,23 +395,25 @@ private: bool setup(); - /** @brief Deserialize a SHAMapItem containing a single STTx. + /** + * @brief Deserialize a SHAMapItem containing a single STTx. * * @param item The SHAMapItem to deserialize. * @return A shared pointer to the deserialized transaction. - * @throw May throw on deserialization error. + * @throws May throw on deserialization error. */ static std::shared_ptr deserializeTx(SHAMapItem const& item); - /** @brief Deserialize a SHAMapItem containing STTx + STObject metadata. + /** + * @brief Deserialize a SHAMapItem containing STTx + STObject metadata. * * The SHAMapItem must contain two variable length serialization objects. * * @param item The SHAMapItem to deserialize. * @return A pair containing shared pointers to the deserialized transaction * and metadata. - * @throw May throw on deserialization error. + * @throws May throw on deserialization error. */ static std::pair, std::shared_ptr> deserializeTxPlusMeta(SHAMapItem const& item); @@ -425,7 +435,9 @@ private: beast::Journal j_; }; -/** A ledger wrapped in a CachedView. */ +/** + * A ledger wrapped in a CachedView. + */ using CachedLedger = CachedView; } // namespace xrpl diff --git a/include/xrpl/ledger/LedgerTiming.h b/include/xrpl/ledger/LedgerTiming.h index a97e229046..77254a434b 100644 --- a/include/xrpl/ledger/LedgerTiming.h +++ b/include/xrpl/ledger/LedgerTiming.h @@ -8,11 +8,12 @@ namespace xrpl { -/** Possible ledger close time resolutions. - - Values should not be duplicated. - @see getNextLedgerTimeResolution -*/ +/** + * Possible ledger close time resolutions. + * + * Values should not be duplicated. + * @see getNextLedgerTimeResolution + */ constexpr std::chrono::seconds kLedgerPossibleTimeResolutions[] = { std::chrono::seconds{10}, std::chrono::seconds{20}, @@ -21,41 +22,50 @@ constexpr std::chrono::seconds kLedgerPossibleTimeResolutions[] = { std::chrono::seconds{90}, std::chrono::seconds{120}}; -//! Initial resolution of ledger close time. +/** + * Initial resolution of ledger close time. + */ constexpr auto kLedgerDefaultTimeResolution = kLedgerPossibleTimeResolutions[2]; -//! Close time resolution in genesis ledger +/** + * Close time resolution in genesis ledger + */ constexpr auto kLedgerGenesisTimeResolution = kLedgerPossibleTimeResolutions[0]; -//! How often we increase the close time resolution (in numbers of ledgers) +/** + * How often we increase the close time resolution (in numbers of ledgers) + */ constexpr auto kIncreaseLedgerTimeResolutionEvery = 8; -//! How often we decrease the close time resolution (in numbers of ledgers) +/** + * How often we decrease the close time resolution (in numbers of ledgers) + */ constexpr auto kDecreaseLedgerTimeResolutionEvery = 1; -/** Calculates the close time resolution for the specified ledger. - - The XRPL protocol uses binning to represent time intervals using only one - timestamp. This allows servers to derive a common time for the next ledger, - without the need for perfectly synchronized clocks. - The time resolution (i.e. the size of the intervals) is adjusted dynamically - based on what happened in the last ledger, to try to avoid disagreements. - - @param previousResolution the resolution used for the prior ledger - @param previousAgree whether consensus agreed on the close time of the prior - ledger - @param ledgerSeq the sequence number of the new ledger - - @pre previousResolution must be a valid bin - from @ref kLedgerPossibleTimeResolutions - - @tparam Rep Type representing number of ticks in std::chrono::duration - @tparam Period An std::ratio representing tick period in - std::chrono::duration - @tparam Seq Unsigned integer-like type corresponding to the ledger sequence - number. It should be comparable to 0 and support modular - division. Built-in and tagged_integers are supported. -*/ +/** + * Calculates the close time resolution for the specified ledger. + * + * The XRPL protocol uses binning to represent time intervals using only one + * timestamp. This allows servers to derive a common time for the next ledger, + * without the need for perfectly synchronized clocks. + * The time resolution (i.e. the size of the intervals) is adjusted dynamically + * based on what happened in the last ledger, to try to avoid disagreements. + * + * @tparam Rep Type representing number of ticks in std::chrono::duration + * @tparam Period An std::ratio representing tick period in + * std::chrono::duration + * @tparam Seq Unsigned integer-like type corresponding to the ledger sequence + * number. It should be comparable to 0 and support modular + * division. Built-in and tagged_integers are supported. + * + * @param previousResolution the resolution used for the prior ledger + * @param previousAgree whether consensus agreed on the close time of the prior + * ledger + * @param ledgerSeq the sequence number of the new ledger + * + * @pre previousResolution must be a valid bin + * from @ref kLedgerPossibleTimeResolutions + */ template std::chrono::duration getNextLedgerTimeResolution( @@ -98,13 +108,14 @@ getNextLedgerTimeResolution( return previousResolution; } -/** Calculates the close time for a ledger, given a close time resolution. - - @param closeTime The time to be rounded - @param closeResolution The resolution - @return @b closeTime rounded to the nearest multiple of @b closeResolution. - Rounds up if @b closeTime is midway between multiples of @b closeResolution. -*/ +/** + * Calculates the close time for a ledger, given a close time resolution. + * + * @param closeTime The time to be rounded + * @param closeResolution The resolution + * @return @b closeTime rounded to the nearest multiple of @b closeResolution. + * Rounds up if @b closeTime is midway between multiples of @b closeResolution. + */ template std::chrono::time_point roundCloseTime( @@ -119,15 +130,16 @@ roundCloseTime( return closeTime - (closeTime.time_since_epoch() % closeResolution); } -/** Calculate the effective ledger close time - - After adjusting the ledger close time based on the current resolution, also - ensure it is sufficiently separated from the prior close time. - - @param closeTime The raw ledger close time - @param resolution The current close time resolution - @param priorCloseTime The close time of the prior ledger -*/ +/** + * Calculate the effective ledger close time + * + * After adjusting the ledger close time based on the current resolution, also + * ensure it is sufficiently separated from the prior close time. + * + * @param closeTime The raw ledger close time + * @param resolution The current close time resolution + * @param priorCloseTime The close time of the prior ledger + */ template std::chrono::time_point effCloseTime( diff --git a/include/xrpl/ledger/OpenView.h b/include/xrpl/ledger/OpenView.h index 875909715c..3f8e950b02 100644 --- a/include/xrpl/ledger/OpenView.h +++ b/include/xrpl/ledger/OpenView.h @@ -25,21 +25,23 @@ namespace xrpl { -/** Open ledger construction tag. - - Views constructed with this tag will have the - rules of open ledgers applied during transaction - processing. +/** + * Open ledger construction tag. + * + * Views constructed with this tag will have the + * rules of open ledgers applied during transaction + * processing. */ inline constexpr struct OpenLedgerT { explicit constexpr OpenLedgerT() = default; } kOpenLedger{}; -/** Batch view construction tag. - - Views constructed with this tag are part of a stack of views - used during batch transaction application. +/** + * Batch view construction tag. + * + * Views constructed with this tag are part of a stack of views + * used during batch transaction application. */ inline constexpr struct BatchViewT { @@ -48,10 +50,11 @@ inline constexpr struct BatchViewT //------------------------------------------------------------------------------ -/** Writable ledger view that accumulates state and tx changes. - - @note Presented as ReadView to clients. -*/ +/** + * Writable ledger view that accumulates state and tx changes. + * + * @note Presented as ReadView to clients. + */ class OpenView final : public ReadView, public TxsRawView { private: @@ -95,7 +98,9 @@ private: detail::RawStateTable items_; std::shared_ptr hold_; - /// In batch mode, the number of transactions already executed. + /** + * In batch mode, the number of transactions already executed. + */ std::size_t baseTxCount_ = 0; bool open_ = true; @@ -109,40 +114,42 @@ public: OpenView(OpenView&&) = default; - /** Construct a shallow copy. - - Effects: - - Creates a new object with a copy of - the modification state table. - - The objects managed by shared pointers are - not duplicated but shared between instances. - Since the SLEs are immutable, calls on the - RawView interface cannot break invariants. - */ + /** + * Construct a shallow copy. + * + * Effects: + * + * Creates a new object with a copy of + * the modification state table. + * + * The objects managed by shared pointers are + * not duplicated but shared between instances. + * Since the SLEs are immutable, calls on the + * RawView interface cannot break invariants. + */ OpenView(OpenView const&); - /** Construct an open ledger view. - - Effects: - - The sequence number is set to the - sequence number of parent plus one. - - The parentCloseTime is set to the - closeTime of parent. - - If `hold` is not nullptr, retains - ownership of a copy of `hold` until - the MetaView is destroyed. - - Calls to rules() will return the - rules provided on construction. - - The tx list starts empty and will contain - all newly inserted tx. - */ + /** + * Construct an open ledger view. + * + * Effects: + * + * The sequence number is set to the + * sequence number of parent plus one. + * + * The parentCloseTime is set to the + * closeTime of parent. + * + * If `hold` is not nullptr, retains + * ownership of a copy of `hold` until + * the MetaView is destroyed. + * + * Calls to rules() will return the + * rules provided on construction. + * + * The tx list starts empty and will contain + * all newly inserted tx. + */ OpenView( OpenLedgerT, ReadView const* base, @@ -159,35 +166,41 @@ public: baseTxCount_ = base.txCount(); } - /** Construct a new last closed ledger. - - Effects: - - The LedgerHeader is copied from the base. - - The rules are inherited from the base. - - The tx list starts empty and will contain - all newly inserted tx. - */ + /** + * Construct a new last closed ledger. + * + * Effects: + * + * The LedgerHeader is copied from the base. + * + * The rules are inherited from the base. + * + * The tx list starts empty and will contain + * all newly inserted tx. + */ OpenView(ReadView const* base, std::shared_ptr hold = nullptr); - /** Returns true if this reflects an open ledger. */ + /** + * Returns true if this reflects an open ledger. + */ bool open() const override { return open_; } - /** Return the number of tx inserted since creation. - - This is used to set the "apply ordinal" - when calculating transaction metadata. - */ + /** + * Return the number of tx inserted since creation. + * + * This is used to set the "apply ordinal" + * when calculating transaction metadata. + */ std::size_t txCount() const; - /** Apply changes. */ + /** + * Apply changes. + */ void apply(TxsRawView& to) const; diff --git a/include/xrpl/ledger/OrderBookDB.h b/include/xrpl/ledger/OrderBookDB.h index a44183900c..96dc94b1f4 100644 --- a/include/xrpl/ledger/OrderBookDB.h +++ b/include/xrpl/ledger/OrderBookDB.h @@ -14,85 +14,92 @@ namespace xrpl { -/** Tracks order books in the ledger. - - This interface provides access to order book information, including: - - Which order books exist in the ledger - - Querying order books by issue - - Managing order book subscriptions - - The order book database is updated as ledgers are accepted and provides - efficient lookup of order book information for pathfinding and client - subscriptions. -*/ +/** + * Tracks order books in the ledger. + * + * This interface provides access to order book information, including: + * - Which order books exist in the ledger + * - Querying order books by issue + * - Managing order book subscriptions + * + * The order book database is updated as ledgers are accepted and provides + * efficient lookup of order book information for pathfinding and client + * subscriptions. + */ class OrderBookDB { public: virtual ~OrderBookDB() = default; - /** Initialize or update the order book database with a new ledger. - - This method should be called when a new ledger is accepted to update - the order book database with the current state of all order books. - - @param ledger The ledger to scan for order books - */ + /** + * Initialize or update the order book database with a new ledger. + * + * This method should be called when a new ledger is accepted to update + * the order book database with the current state of all order books. + * + * @param ledger The ledger to scan for order books + */ virtual void setup(std::shared_ptr const& ledger) = 0; - /** Add an order book to track. - - @param book The order book to add - */ + /** + * Add an order book to track. + * + * @param book The order book to add + */ virtual void addOrderBook(Book const& book) = 0; - /** Get all order books that want a specific issue. - - Returns a list of all order books where the taker pays the specified - issue. This is useful for pathfinding to find all possible next hops - from a given currency. - - @param asset The asset to search for - @param domain Optional domain restriction for the order book - @return Vector of books that want this issue - */ + /** + * Get all order books that want a specific issue. + * + * Returns a list of all order books where the taker pays the specified + * issue. This is useful for pathfinding to find all possible next hops + * from a given currency. + * + * @param asset The asset to search for + * @param domain Optional domain restriction for the order book + * @return Vector of books that want this issue + */ virtual std::vector getBooksByTakerPays(Asset const& asset, std::optional const& domain = std::nullopt) = 0; - /** Get the count of order books that want a specific issue. - - @param asset The asset to search for - @param domain Optional domain restriction for the order book - @return Number of books that want this issue - */ + /** + * Get the count of order books that want a specific issue. + * + * @param asset The asset to search for + * @param domain Optional domain restriction for the order book + * @return Number of books that want this issue + */ virtual int getBookSize(Asset const& asset, std::optional const& domain = std::nullopt) = 0; - /** Check if an order book to XRP exists for the given issue. - - @param asset The asset to check - @param domain Optional domain restriction for the order book - @return true if a book from this issue to XRP exists - */ + /** + * Check if an order book to XRP exists for the given issue. + * + * @param asset The asset to check + * @param domain Optional domain restriction for the order book + * @return true if a book from this issue to XRP exists + */ virtual bool isBookToXRP(Asset const& asset, std::optional const& domain = std::nullopt) = 0; }; -/** Extract the set of books affected by a transaction. +/** + * Extract the set of books affected by a transaction. * - * Walks the transaction's metadata nodes and collects every order book - * whose offers were created, modified, or deleted. Used by NetworkOPs to - * fan transaction notifications out to book subscribers. + * Walks the transaction's metadata nodes and collects every order book + * whose offers were created, modified, or deleted. Used by NetworkOPs to + * fan transaction notifications out to book subscribers. * - * @param alTx The accepted ledger transaction to inspect. - * @param j Journal used to log per-node parsing failures. Inspecting an - * offer node can throw if a required field is missing; in that - * case the bad node is skipped and a warn-level message is - * emitted via @p j. Other affected books in the same transaction - * are still returned. - * @return The set of books whose offers were created, modified, or - * deleted. May be empty for non-offer transactions. + * @param alTx The accepted ledger transaction to inspect. + * @param j Journal used to log per-node parsing failures. Inspecting an + * offer node can throw if a required field is missing; in that + * case the bad node is skipped and a warn-level message is + * emitted via @p j. Other affected books in the same transaction + * are still returned. + * @return The set of books whose offers were created, modified, or + * deleted. May be empty for non-offer transactions. */ hash_set affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j); diff --git a/include/xrpl/ledger/OwnerCounts.h b/include/xrpl/ledger/OwnerCounts.h new file mode 100644 index 0000000000..74d60014b5 --- /dev/null +++ b/include/xrpl/ledger/OwnerCounts.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include // IWYU pragma: keep +#include + +#include +#include + +namespace xrpl { + +struct OwnerCounts +{ + std::uint32_t owner = 0; + std::uint32_t sponsored = 0; + std::uint32_t sponsoring = 0; + + OwnerCounts() = default; + OwnerCounts(SLE::const_ref sle) + : owner(sle->at(sfOwnerCount)) + , sponsored(sle->at(sfSponsoredOwnerCount)) + , sponsoring(sle->at(sfSponsoringOwnerCount)) + { + XRPL_ASSERT( + owner >= sponsored, + "xrpl::OwnerCounts : OwnerCount must be greater than or equal to " + "SponsoredOwnerCount"); + XRPL_ASSERT(sle->getType() == ltACCOUNT_ROOT, "xrpl::OwnerCounts : sle is AccountRoot"); + } + + [[nodiscard]] std::uint32_t + count() const + { + std::int64_t const x = static_cast(owner) - sponsored + sponsoring; + if (x < 0) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::OwnerCounts::count : count less than zero"); + return 0; + // LCOV_EXCL_STOP + } + + if (x > std::numeric_limits::max()) + return std::numeric_limits::max(); // LCOV_EXCL_LINE + return static_cast(x); + } + + auto + operator<=>(OwnerCounts const& o) const + { + if (auto cmp = count() <=> o.count(); cmp != 0) // NOLINT(modernize-use-nullptr) + return cmp; + if (auto cmp = owner <=> o.owner; cmp != 0) // NOLINT(modernize-use-nullptr) + return cmp; + if (auto cmp = sponsored <=> o.sponsored; cmp != 0) // NOLINT(modernize-use-nullptr) + return cmp; + return sponsoring <=> o.sponsoring; + } + + bool + operator==(OwnerCounts const& o) const + { + return this == &o || + (owner == o.owner && sponsored == o.sponsored && sponsoring == o.sponsoring); + } +}; + +} // namespace xrpl diff --git a/include/xrpl/ledger/PaymentSandbox.h b/include/xrpl/ledger/PaymentSandbox.h index 0117a962ff..e725bdd556 100644 --- a/include/xrpl/ledger/PaymentSandbox.h +++ b/include/xrpl/ledger/PaymentSandbox.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -107,12 +108,12 @@ public: issuerSelfDebitMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance); void - ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next); + ownerCount(AccountID const& id, OwnerCounts const& cur, OwnerCounts const& next); // Get the adjusted owner count. Since DeferredCredits is meant to be used // in payments, and payments only decrease owner counts, return the max // remembered owner count. - [[nodiscard]] std::optional + [[nodiscard]] std::optional ownerCount(AccountID const& id) const; void @@ -124,25 +125,26 @@ private: std::map creditsIOU_; std::map creditsMPT_; - std::map ownerCounts_; + std::map ownerCounts_; }; } // namespace detail //------------------------------------------------------------------------------ -/** A wrapper which makes credits unavailable to balances. - - This is used for payments and pathfinding, so that consuming - liquidity from a path never causes portions of that path or - other paths to gain liquidity. - - The behavior of certain free functions in the ApplyView API - will change via the balanceHook and creditHook overrides - of PaymentSandbox. - - @note Presented as ApplyView to clients -*/ +/** + * A wrapper which makes credits unavailable to balances. + * + * This is used for payments and pathfinding, so that consuming + * liquidity from a path never causes portions of that path or + * other paths to gain liquidity. + * + * The behavior of certain free functions in the ApplyView API + * will change via the balanceHook and creditHook overrides + * of PaymentSandbox. + * + * @note Presented as ApplyView to clients + */ class PaymentSandbox final : public detail::ApplyViewBase { public: @@ -163,16 +165,17 @@ public: { } - /** Construct on top of existing PaymentSandbox. - - The changes are pushed to the parent when - apply() is called. - - @param parent A non-null pointer to the parent. - - @note A pointer is used to prevent confusion - with copy construction. - */ + /** + * Construct on top of existing PaymentSandbox. + * + * The changes are pushed to the parent when + * apply() is called. + * + * @param parent A non-null pointer to the parent. + * + * @note A pointer is used to prevent confusion + * with copy construction. + */ // VFALCO If we are constructing on top of a PaymentSandbox, // or a PaymentSandbox-derived class, we MUST go through // one of these constructors or invariants will be broken. @@ -218,17 +221,19 @@ public: override; void - adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) override; + adjustOwnerCountHook(AccountID const& account, OwnerCounts const& cur, OwnerCounts const& next) + override; - [[nodiscard]] std::uint32_t - ownerCountHook(AccountID const& account, std::uint32_t count) const override; + [[nodiscard]] OwnerCounts + ownerCountHook(AccountID const& account, OwnerCounts const& count) const override; - /** Apply changes to base view. - - `to` must contain contents identical to the parent - view passed upon construction, else undefined - behavior will result. - */ + /** + * Apply changes to base view. + * + * `to` must contain contents identical to the parent + * view passed upon construction, else undefined + * behavior will result. + */ /** @{ */ void apply(RawView& to); diff --git a/include/xrpl/ledger/PendingSaves.h b/include/xrpl/ledger/PendingSaves.h index a18292df68..723ae1aef1 100644 --- a/include/xrpl/ledger/PendingSaves.h +++ b/include/xrpl/ledger/PendingSaves.h @@ -8,12 +8,13 @@ namespace xrpl { -/** Keeps track of which ledgers haven't been fully saved. - - During the ledger building process this collection will keep - track of those ledgers that are being built but have not yet - been completely written. -*/ +/** + * Keeps track of which ledgers haven't been fully saved. + * + * During the ledger building process this collection will keep + * track of those ledgers that are being built but have not yet + * been completely written. + */ class PendingSaves { private: @@ -22,12 +23,13 @@ private: std::condition_variable await_; public: - /** Start working on a ledger - - This is called prior to updating the SQLite indexes. - - @return 'true' if work should be done - */ + /** + * Start working on a ledger + * + * This is called prior to updating the SQLite indexes. + * + * @return 'true' if work should be done + */ bool startWork(LedgerIndex seq) { @@ -45,12 +47,13 @@ public: return true; } - /** Finish working on a ledger - - This is called after updating the SQLite indexes. - The tracking of the work in progress is removed and - threads awaiting completion are notified. - */ + /** + * Finish working on a ledger + * + * This is called after updating the SQLite indexes. + * The tracking of the work in progress is removed and + * threads awaiting completion are notified. + */ void finishWork(LedgerIndex seq) { @@ -60,7 +63,9 @@ public: await_.notify_all(); } - /** Return `true` if a ledger is in the progress of being saved. */ + /** + * Return `true` if a ledger is in the progress of being saved. + */ bool pending(LedgerIndex seq) { @@ -68,14 +73,15 @@ public: return map_.contains(seq); } - /** Check if a ledger should be dispatched - - Called to determine whether work should be done or - dispatched. If work is already in progress and the - call is synchronous, wait for work to be completed. - - @return 'true' if work should be done or dispatched - */ + /** + * Check if a ledger should be dispatched + * + * Called to determine whether work should be done or + * dispatched. If work is already in progress and the + * call is synchronous, wait for work to be completed. + * + * @return 'true' if work should be done or dispatched + */ bool shouldWork(LedgerIndex seq, bool isSynchronous) { @@ -108,12 +114,13 @@ public: } while (true); } - /** Get a snapshot of the pending saves - - Each entry in the returned map corresponds to a ledger - that is in progress or dispatched. The boolean indicates - whether work is currently in progress. - */ + /** + * Get a snapshot of the pending saves + * + * Each entry in the returned map corresponds to a ledger + * that is in progress or dispatched. The boolean indicates + * whether work is currently in progress. + */ std::map getSnapshot() const { diff --git a/include/xrpl/ledger/RawView.h b/include/xrpl/ledger/RawView.h index ac2674226f..b94a7aab27 100644 --- a/include/xrpl/ledger/RawView.h +++ b/include/xrpl/ledger/RawView.h @@ -9,10 +9,11 @@ namespace xrpl { -/** Interface for ledger entry changes. - - Subclasses allow raw modification of ledger entries. -*/ +/** + * Interface for ledger entry changes. + * + * Subclasses allow raw modification of ledger entries. + */ class RawView { public: @@ -22,66 +23,72 @@ public: RawView& operator=(RawView const&) = delete; - /** Delete an existing state item. - - The SLE is provided so the implementation - can calculate metadata. - */ + /** + * Delete an existing state item. + * + * The SLE is provided so the implementation + * can calculate metadata. + */ virtual void rawErase(SLE::ref sle) = 0; - /** Unconditionally insert a state item. - - Requirements: - The key must not already exist. - - Effects: - - The key is associated with the SLE. - - @note The key is taken from the SLE - */ + /** + * Unconditionally insert a state item. + * + * Requirements: + * The key must not already exist. + * + * Effects: + * + * The key is associated with the SLE. + * + * @note The key is taken from the SLE + */ virtual void rawInsert(SLE::ref sle) = 0; - /** Unconditionally replace a state item. - - Requirements: - - The key must exist. - - Effects: - - The key is associated with the SLE. - - @note The key is taken from the SLE - */ + /** + * Unconditionally replace a state item. + * + * Requirements: + * + * The key must exist. + * + * Effects: + * + * The key is associated with the SLE. + * + * @note The key is taken from the SLE + */ virtual void rawReplace(SLE::ref sle) = 0; - /** Destroy XRP. - - This is used to pay for transaction fees. - */ + /** + * Destroy XRP. + * + * This is used to pay for transaction fees. + */ virtual void rawDestroyXRP(XRPAmount const& fee) = 0; }; //------------------------------------------------------------------------------ -/** Interface for changing ledger entries with transactions. - - Allows raw modification of ledger entries and insertion - of transactions into the transaction map. -*/ +/** + * Interface for changing ledger entries with transactions. + * + * Allows raw modification of ledger entries and insertion + * of transactions into the transaction map. + */ class TxsRawView : public RawView { public: - /** Add a transaction to the tx map. - - Closed ledgers must have metadata, - while open ledgers omit metadata. - */ + /** + * Add a transaction to the tx map. + * + * Closed ledgers must have metadata, + * while open ledgers omit metadata. + */ virtual void rawTxInsert( ReadView::key_type const& key, diff --git a/include/xrpl/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h index 724533039b..d0010b6030 100644 --- a/include/xrpl/ledger/ReadView.h +++ b/include/xrpl/ledger/ReadView.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -28,12 +30,13 @@ namespace xrpl { //------------------------------------------------------------------------------ -/** A view into a ledger. - - This interface provides read access to state - and transaction items. There is no checkpointing - or calculation of metadata. -*/ +/** + * A view into a ledger. + * + * This interface provides read access to state + * and transaction items. There is no checkpointing + * or calculation of metadata. + */ class ReadView { public: @@ -84,72 +87,87 @@ public: { } - /** Returns information about the ledger. */ + /** + * Returns information about the ledger. + */ [[nodiscard]] virtual LedgerHeader const& header() const = 0; - /** Returns true if this reflects an open ledger. */ + /** + * Returns true if this reflects an open ledger. + */ [[nodiscard]] virtual bool open() const = 0; - /** Returns the close time of the previous ledger. */ + /** + * Returns the close time of the previous ledger. + */ [[nodiscard]] NetClock::time_point parentCloseTime() const { return header().parentCloseTime; } - /** Returns the sequence number of the base ledger. */ + /** + * Returns the sequence number of the base ledger. + */ [[nodiscard]] LedgerIndex seq() const { return header().seq; } - /** Returns the fees for the base ledger. */ + /** + * Returns the fees for the base ledger. + */ [[nodiscard]] virtual Fees const& fees() const = 0; - /** Returns the tx processing rules. */ + /** + * Returns the tx processing rules. + */ [[nodiscard]] virtual Rules const& rules() const = 0; - /** Determine if a state item exists. - - @note This can be more efficient than calling read. - - @return `true` if a SLE is associated with the - specified key. - */ + /** + * Determine if a state item exists. + * + * @note This can be more efficient than calling read. + * + * @return `true` if a SLE is associated with the + * specified key. + */ [[nodiscard]] virtual bool exists(Keylet const& k) const = 0; - /** Return the key of the next state item. - - This returns the key of the first state item - whose key is greater than the specified key. If - no such key is present, std::nullopt is returned. - - If `last` is engaged, returns std::nullopt when - the key returned would be outside the open - interval (key, last). - */ + /** + * Return the key of the next state item. + * + * This returns the key of the first state item + * whose key is greater than the specified key. If + * no such key is present, std::nullopt is returned. + * + * If `last` is engaged, returns std::nullopt when + * the key returned would be outside the open + * interval (key, last). + */ [[nodiscard]] virtual std::optional succ(key_type const& key, std::optional const& last = std::nullopt) const = 0; - /** Return the state item associated with a key. - - Effects: - If the key exists, gives the caller ownership - of the non-modifiable corresponding SLE. - - @note While the returned SLE is `const` from the - perspective of the caller, it can be changed - by other callers through raw operations. - - @return `nullptr` if the key is not present or - if the type does not match. - */ + /** + * Return the state item associated with a key. + * + * Effects: + * If the key exists, gives the caller ownership + * of the non-modifiable corresponding SLE. + * + * @note While the returned SLE is `const` from the + * perspective of the caller, it can be changed + * by other callers through raw operations. + * + * @return `nullptr` if the key is not present or + * if the type does not match. + */ [[nodiscard]] virtual SLE::const_pointer read(Keylet const& k) const = 0; @@ -189,8 +207,8 @@ public: // changes that accounts make during a payment. `ownerCountHook` adjusts the // ownerCount so it returns the max value of the ownerCount so far. // This is required to support PaymentSandbox. - [[nodiscard]] virtual std::uint32_t - ownerCountHook(AccountID const& account, std::uint32_t count) const + [[nodiscard]] virtual OwnerCounts + ownerCountHook(AccountID const& account, OwnerCounts const& count) const { return count; } @@ -215,22 +233,24 @@ public: [[nodiscard]] virtual std::unique_ptr txsEnd() const = 0; - /** Returns `true` if a tx exists in the tx map. - - A tx exists in the map if it is part of the - base ledger, or if it is a newly inserted tx. - */ + /** + * Returns `true` if a tx exists in the tx map. + * + * A tx exists in the map if it is part of the + * base ledger, or if it is a newly inserted tx. + */ [[nodiscard]] virtual bool txExists(key_type const& key) const = 0; - /** Read a transaction from the tx map. - - If the view represents an open ledger, - the metadata object will be empty. - - @return A pair of nullptr if the - key is not found in the tx map. - */ + /** + * Read a transaction from the tx map. + * + * If the view represents an open ledger, + * the metadata object will be empty. + * + * @return A pair of nullptr if the + * key is not found in the tx map. + */ [[nodiscard]] virtual tx_type txRead(key_type const& key) const = 0; @@ -238,11 +258,12 @@ public: // Memberspaces // - /** Iterable range of ledger state items. - - @note Visiting each state entry in the ledger can - become quite expensive as the ledger grows. - */ + /** + * Iterable range of ledger state items. + * + * @note Visiting each state entry in the ledger can + * become quite expensive as the ledger grows. + */ SlesType sles; // The range of transactions @@ -251,7 +272,9 @@ public: //------------------------------------------------------------------------------ -/** ReadView that associates keys with digests. */ +/** + * ReadView that associates keys with digests. + */ class DigestAwareReadView : public ReadView { public: @@ -260,10 +283,11 @@ public: DigestAwareReadView() = default; DigestAwareReadView(DigestAwareReadView const&) = default; - /** Return the digest associated with the key. - - @return std::nullopt if the item does not exist. - */ + /** + * Return the digest associated with the key. + * + * @return std::nullopt if the item does not exist. + */ [[nodiscard]] virtual std::optional digest(key_type const& key) const = 0; }; diff --git a/include/xrpl/ledger/Sandbox.h b/include/xrpl/ledger/Sandbox.h index ca8838631f..fd48e339eb 100644 --- a/include/xrpl/ledger/Sandbox.h +++ b/include/xrpl/ledger/Sandbox.h @@ -7,12 +7,13 @@ namespace xrpl { -/** Discardable, editable view to a ledger. - - The sandbox inherits the flags of the base. - - @note Presented as ApplyView to clients. -*/ +/** + * Discardable, editable view to a ledger. + * + * The sandbox inherits the flags of the base. + * + * @note Presented as ApplyView to clients. + */ class Sandbox : public detail::ApplyViewBase { public: diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index c89764df1d..768e518008 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -35,26 +35,27 @@ enum class SkipEntry : bool { No = false, Yes }; // //------------------------------------------------------------------------------ -/** Determines whether the given expiration time has passed. - - In the XRP Ledger, expiration times are defined as the number of whole - seconds after the "XRPL epoch" which, for historical reasons, is set - to January 1, 2000 (00:00 UTC). - - This is like the way the Unix epoch works, except the XRPL epoch is - precisely 946,684,800 seconds after the Unix Epoch. - - See https://xrpl.org/basic-data-types.html#specifying-time - - Expiration is defined in terms of the close time of the parent ledger, - because we definitively know the time that it closed (since consensus - agrees on time) but we do not know the closing time of the ledger that - is under construction. - - @param view The ledger whose parent time is used as the clock. - @param exp The optional expiration time we want to check. - - @returns `true` if `exp` is in the past; `false` otherwise. +/** + * Determines whether the given expiration time has passed. + * + * In the XRP Ledger, expiration times are defined as the number of whole + * seconds after the "XRPL epoch" which, for historical reasons, is set + * to January 1, 2000 (00:00 UTC). + * + * This is like the way the Unix epoch works, except the XRPL epoch is + * precisely 946,684,800 seconds after the Unix Epoch. + * + * See https://xrpl.org/basic-data-types.html#specifying-time + * + * Expiration is defined in terms of the close time of the parent ledger, + * because we definitively know the time that it closed (since consensus + * agrees on time) but we do not know the closing time of the ledger that + * is under construction. + * + * @param view The ledger whose parent time is used as the clock. + * @param exp The optional expiration time we want to check. + * + * @return `true` if `exp` is in the past; `false` otherwise. */ [[nodiscard]] bool hasExpired(ReadView const& view, std::optional const& exp); @@ -83,41 +84,44 @@ using majorityAmendments_t = std::map; [[nodiscard]] majorityAmendments_t getMajorityAmendments(ReadView const& view); -/** Return the hash of a ledger by sequence. - The hash is retrieved by looking up the "skip list" - in the passed ledger. As the skip list is limited - in size, if the requested ledger sequence number is - out of the range of ledgers represented in the skip - list, then std::nullopt is returned. - @return The hash of the ledger with the - given sequence number or std::nullopt. -*/ +/** + * Return the hash of a ledger by sequence. + * The hash is retrieved by looking up the "skip list" + * in the passed ledger. As the skip list is limited + * in size, if the requested ledger sequence number is + * out of the range of ledgers represented in the skip + * list, then std::nullopt is returned. + * @return The hash of the ledger with the + * given sequence number or std::nullopt. + */ [[nodiscard]] std::optional hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal); -/** Find a ledger index from which we could easily get the requested ledger - - The index that we return should meet two requirements: - 1) It must be the index of a ledger that has the hash of the ledger - we are looking for. This means that its sequence must be equal to - greater than the sequence that we want but not more than 256 greater - since each ledger contains the hashes of the 256 previous ledgers. - - 2) Its hash must be easy for us to find. This means it must be 0 mod 256 - because every such ledger is permanently enshrined in a LedgerHashes - page which we can easily retrieve via the skip list. -*/ +/** + * Find a ledger index from which we could easily get the requested ledger + * + * The index that we return should meet two requirements: + * 1) It must be the index of a ledger that has the hash of the ledger + * we are looking for. This means that its sequence must be equal to + * greater than the sequence that we want but not more than 256 greater + * since each ledger contains the hashes of the 256 previous ledgers. + * + * 2) Its hash must be easy for us to find. This means it must be 0 mod 256 + * because every such ledger is permanently enshrined in a LedgerHashes + * page which we can easily retrieve via the skip list. + */ inline LedgerIndex getCandidateLedger(LedgerIndex requested) { return (requested + 255) & (~255); } -/** Return false if the test ledger is provably incompatible - with the valid ledger, that is, they could not possibly - both be valid. Use the first form if you have both ledgers, - use the second form if you have not acquired the valid ledger yet -*/ +/** + * Return false if the test ledger is provably incompatible + * with the valid ledger, that is, they could not possibly + * both be valid. Use the first form if you have both ledgers, + * use the second form if you have not acquired the valid ledger yet + */ [[nodiscard]] bool areCompatible( ReadView const& validLedger, @@ -146,7 +150,8 @@ dirLink( SLE::pointer& object, SF_UINT64 const& node = sfOwnerNode); -/** Checks that can withdraw funds from an object to itself or a destination. +/** + * Checks that can withdraw funds from an object to itself or a destination. * * The receiver may be either the submitting account (sfAccount) or a different * destination account (sfDestination). @@ -169,7 +174,8 @@ canWithdraw( STAmount const& amount, bool hasDestinationTag); -/** Checks that can withdraw funds from an object to itself or a destination. +/** + * Checks that can withdraw funds from an object to itself or a destination. * * The receiver may be either the submitting account (sfAccount) or a different * destination account (sfDestination). @@ -191,7 +197,8 @@ canWithdraw( STAmount const& amount, bool hasDestinationTag); -/** Checks that can withdraw funds from an object to itself or a destination. +/** + * Checks that can withdraw funds from an object to itself or a destination. * * The receiver may be either the submitting account (sfAccount) or a different * destination account (sfDestination). @@ -210,8 +217,7 @@ canWithdraw(ReadView const& view, STTx const& tx); [[nodiscard]] TER doWithdraw( - ApplyView& view, - STTx const& tx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, @@ -219,13 +225,15 @@ doWithdraw( STAmount const& amount, beast::Journal j); -/** Deleter function prototype. Returns the status of the entry deletion +/** + * Deleter function prototype. Returns the status of the entry deletion * (if should not be skipped) and if the entry should be skipped. The status * is always tesSUCCESS if the entry should be skipped. */ using EntryDeleter = std::function(LedgerEntryType, uint256 const&, SLE::pointer&)>; -/** Cleanup owner directory entries on account delete. +/** + * Cleanup owner directory entries on account delete. * Used for a regular and AMM accounts deletion. The caller * has to provide the deleter function, which handles details of * specific account-owned object deletion. @@ -240,12 +248,13 @@ cleanupOnAccountDelete( beast::Journal j, std::optional maxNodesToDelete = std::nullopt); -/** Has the specified time passed? - - @param now the current time - @param mark the cutoff point - @return true if \a now refers to a time strictly after \a mark, else false. -*/ +/** + * Has the specified time passed? + * + * @param now the current time + * @param mark the cutoff point + * @return true if \a now refers to a time strictly after \a mark, else false. + */ bool after(NetClock::time_point now, std::uint32_t mark); diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index c6a2053010..7d41bfce81 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -53,7 +53,8 @@ enum class IsDeposit : bool { No = false, Yes = true }; inline Number const kAMMInvariantRelativeTolerance{1, -11}; -/** Calculate LP Tokens given AMM pool reserves. +/** + * Calculate LP Tokens given AMM pool reserves. * @param asset1 AMM one side of the pool reserve * @param asset2 AMM another side of the pool reserve * @return LP Tokens as IOU @@ -61,7 +62,8 @@ inline Number const kAMMInvariantRelativeTolerance{1, -11}; STAmount ammLPTokens(STAmount const& asset1, STAmount const& asset2, Asset const& lptIssue); -/** Calculate LP Tokens given asset's deposit amount. +/** + * Calculate LP Tokens given asset's deposit amount. * @param asset1Balance current AMM asset1 balance * @param asset1Deposit requested asset1 deposit amount * @param lptAMMBalance AMM LPT balance @@ -75,10 +77,11 @@ lpTokensOut( STAmount const& lptAMMBalance, std::uint16_t tfee); -/** Calculate asset deposit given LP Tokens. +/** + * Calculate asset deposit given LP Tokens. * @param asset1Balance current AMM asset1 balance - * @param lpTokens LP Tokens * @param lptAMMBalance AMM LPT balance + * @param lpTokens LP Tokens * @param tfee trading fee in basis points * @return */ @@ -89,7 +92,8 @@ ammAssetIn( STAmount const& lpTokens, std::uint16_t tfee); -/** Calculate LP Tokens given asset's withdraw amount. Return 0 +/** + * Calculate LP Tokens given asset's withdraw amount. Return 0 * if can't calculate. * @param asset1Balance current AMM asset1 balance * @param asset1Withdraw requested asset1 withdraw amount @@ -104,7 +108,8 @@ lpTokensIn( STAmount const& lptAMMBalance, std::uint16_t tfee); -/** Calculate asset withdrawal by tokens +/** + * Calculate asset withdrawal by tokens * @param assetBalance balance of the asset being withdrawn * @param lptAMMBalance total AMM Tokens balance * @param lpTokens LP Tokens balance @@ -118,7 +123,8 @@ ammAssetOut( STAmount const& lpTokens, std::uint16_t tfee); -/** Check if the relative distance between the qualities +/** + * Check if the relative distance between the qualities * is within the requested distance. * @param calcQuality calculated quality * @param reqQuality requested quality @@ -137,7 +143,8 @@ withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Nu return ((min.rate() - max.rate()) / min.rate()) < dist; } -/** Check if the relative distance between the amounts +/** + * Check if the relative distance between the amounts * is within the requested distance. * @param calc calculated amount * @param req requested amount @@ -158,13 +165,15 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) return ((max - min) / max) < dist; } -/** Solve quadratic equation to find takerGets or takerPays. Round +/** + * Solve quadratic equation to find takerGets or takerPays. Round * to minimize the amount in order to maximize the quality. */ std::optional solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c); -/** Generate AMM offer starting with takerGets when AMM pool +/** + * Generate AMM offer starting with takerGets when AMM pool * from the payment perspective is IOU(in)/XRP(out) * Equations: * Spot Price Quality after the offer is consumed: @@ -231,7 +240,8 @@ getAMMOfferStartWithTakerGets( return amounts; } -/** Generate AMM offer starting with takerPays when AMM pool +/** + * Generate AMM offer starting with takerPays when AMM pool * from the payment perspective is XRP(in)/IOU(out) or IOU(in)/IOU(out). * Equations: * Spot Price Quality after the offer is consumed: @@ -298,7 +308,8 @@ getAMMOfferStartWithTakerPays( return amounts; } -/** Generate AMM offer so that either updated Spot Price Quality (SPQ) +/** + * Generate AMM offer so that either updated Spot Price Quality (SPQ) * is equal to LOB quality (in this case AMM offer quality is * better than LOB quality) or AMM offer is equal to LOB quality * (in this case SPQ is better than LOB quality). @@ -415,7 +426,8 @@ changeSpotPriceQuality( return amounts; } -/** AMM pool invariant - the product (A * B) after swap in/out has to remain +/** + * AMM pool invariant - the product (A * B) after swap in/out has to remain * at least the same: (A + in) * (B - out) >= A * B * XRP round-off may result in a smaller product after swap in/out. * To address this: @@ -427,7 +439,8 @@ changeSpotPriceQuality( * value is increased. */ -/** Swap assetIn into the pool and swap out a proportional amount +/** + * Swap assetIn into the pool and swap out a proportional amount * of the other asset. Implements AMM Swap in. * @see [XLS30d:AMM * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) @@ -493,7 +506,8 @@ swapAssetIn(TAmounts const& pool, TIn const& assetIn, std::uint16_t t Number::RoundingMode::Downward); } -/** Swap assetOut out of the pool and swap in a proportional amount +/** + * Swap assetOut out of the pool and swap in a proportional amount * of the other asset. Implements AMM Swap out. * @see [XLS30d:AMM * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) @@ -559,12 +573,14 @@ swapAssetOut(TAmounts const& pool, TOut const& assetOut, std::uint16_ Number::RoundingMode::Upward); } -/** Return square of n. +/** + * Return square of n. */ Number square(Number const& n); -/** Adjust LP tokens to deposit/withdraw. +/** + * Adjust LP tokens to deposit/withdraw. * Amount type keeps 16 digits. Maintaining the LP balance by adding * deposited tokens or subtracting withdrawn LP tokens from LP balance * results in losing precision in LP balance. I.e. the resulting LP balance @@ -578,7 +594,8 @@ square(Number const& n); STAmount adjustLPTokens(STAmount const& lptAMMBalance, STAmount const& lpTokens, IsDeposit isDeposit); -/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if +/** + * Calls adjustLPTokens() and adjusts deposit or withdraw amounts if * the adjusted LP tokens are less than the provided LP tokens. * @param amountBalance asset1 pool balance * @param amount asset1 to deposit or withdraw @@ -599,7 +616,8 @@ adjustAmountsByLPTokens( std::uint16_t tfee, IsDeposit isDeposit); -/** Positive solution for quadratic equation: +/** + * Positive solution for quadratic equation: * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) */ Number @@ -630,7 +648,8 @@ getAssetRounding(IsDeposit isDeposit) } // namespace detail -/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas +/** + * Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas * calculate the amount as a fractional value of the pool balance. The rounding * takes place on the last step of multiplying the balance by the fraction if * AMMv1_3 is enabled. @@ -654,7 +673,8 @@ getRoundedAsset(Rules const& rules, STAmount const& balance, A const& frac, IsDe return multiply(balance, frac, rm); } -/** Round AMM single deposit/withdrawal amount. +/** + * Round AMM single deposit/withdrawal amount. * The lambda's are used to delay evaluation until the function * is executed so that the calculation is not done twice. noRoundCb() is * called if AMMv1_3 is disabled. Otherwise, the rounding is set and @@ -671,7 +691,8 @@ getRoundedAsset( std::function const& productCb, IsDeposit isDeposit); -/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas +/** + * Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas * calculate the lptokens as a fractional value of the AMM total lptokens. * The rounding takes place on the last step of multiplying the balance by * the fraction if AMMv1_3 is enabled. The tokens are then @@ -685,7 +706,8 @@ getRoundedLPTokens( Number const& frac, IsDeposit isDeposit); -/** Round AMM single deposit/withdrawal LPToken amount. +/** + * Round AMM single deposit/withdrawal LPToken amount. * The lambda's are used to delay evaluation until the function is executed * so that the calculations are not done twice. * noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set @@ -732,7 +754,8 @@ adjustAssetOutByTokens( STAmount const& tokens, std::uint16_t tfee); -/** Find a fraction of tokens after the tokens are adjusted. The fraction +/** + * Find a fraction of tokens after the tokens are adjusted. The fraction * is used to adjust equal deposit/withdraw amount. */ Number @@ -742,7 +765,8 @@ adjustFracByTokens( STAmount const& tokens, Number const& frac); -/** Get AMM pool balances. +/** + * Get AMM pool balances. */ std::pair ammPoolHolds( @@ -754,7 +778,8 @@ ammPoolHolds( AuthHandling authHandling, beast::Journal const j); -/** Check AMM pool product invariant after an AMM operation that changes LP tokens +/** + * Check AMM pool product invariant after an AMM operation that changes LP tokens * (deposit/withdraw/clawback) from an already calculated pool product mean. * Returns tecPRECISION_LOSS if poolProductMean < newLPTokenBalance beyond the * invariant tolerance, @@ -763,7 +788,8 @@ ammPoolHolds( TER checkAMMPrecisionLoss(Number const& poolProductMean, STAmount const& newLPTokenBalance); -/** Check AMM pool product invariant after an AMM operation that changes LP tokens +/** + * Check AMM pool product invariant after an AMM operation that changes LP tokens * (deposit/withdraw/clawback). * Returns tecPRECISION_LOSS if sqrt(asset1 * asset2) < newLPTokenBalance beyond * the invariant tolerance, @@ -778,7 +804,8 @@ checkAMMPrecisionLoss( STAmount const& newLPTokenBalance, beast::Journal const j); -/** Get AMM pool and LP token balances. If both optIssue are +/** + * Get AMM pool and LP token balances. If both optIssue are * provided then they are used as the AMM token pair issues. * Otherwise the missing issues are fetched from ammSle. */ @@ -792,7 +819,8 @@ ammHolds( AuthHandling authHandling, beast::Journal const j); -/** Get the balance of LP tokens. +/** + * Get the balance of LP tokens. */ STAmount ammLPHolds( @@ -810,25 +838,29 @@ ammLPHolds( AccountID const& lpAccount, beast::Journal const j); -/** Get AMM trading fee for the given account. The fee is discounted +/** + * Get AMM trading fee for the given account. The fee is discounted * if the account is the auction slot owner or one of the slot's authorized * accounts. */ std::uint16_t getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account); -/** Returns total amount held by AMM for the given token. +/** + * Returns total amount held by AMM for the given token. */ STAmount ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const& asset); -/** Delete trustlines to AMM. If all trustlines are deleted then +/** + * Delete trustlines to AMM. If all trustlines are deleted then * AMM object and account are deleted. Otherwise tecINCOMPLETE is returned. */ TER deleteAMMAccount(Sandbox& view, Asset const& asset, Asset const& asset2, beast::Journal j); -/** Initialize Auction and Voting slots and set the trading/discounted fee. +/** + * Initialize Auction and Voting slots and set the trading/discounted fee. */ void initializeFeeAuctionVote( @@ -838,14 +870,16 @@ initializeFeeAuctionVote( Asset const& lptAsset, std::uint16_t tfee); -/** Return true if the Liquidity Provider is the only AMM provider, false +/** + * Return true if the Liquidity Provider is the only AMM provider, false * otherwise. Return tecINTERNAL if encountered an unexpected condition, * for instance Liquidity Provider has more than one LPToken trustline. */ std::expected isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount); -/** Due to rounding, the LPTokenBalance of the last LP might +/** + * Due to rounding, the LPTokenBalance of the last LP might * not match the LP's trustline balance. If it's within the tolerance, * update LPTokenBalance to match the LP's trustline balance. */ diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index d0cfc175a3..350fc6ca85 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -14,35 +14,324 @@ #include #include +#include #include #include namespace xrpl { -/** Check if the issuer has the global freeze flag set. - @param issuer The account to check - @return true if the account has global freeze set -*/ +/** + * Check if the issuer has the global freeze flag set. + * @param issuer The account to check + * @return true if the account has global freeze set + */ [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); -// Calculate liquid XRP balance for an account. -// This function may be used to calculate the amount of XRP that -// the holder is able to freely spend. It subtracts reserve requirements. -// -// ownerCountAdj adjusts the owner count in case the caller calculates -// before ledger entries are added or removed. Positive to add, negative -// to subtract. -// -// @param ownerCountAdj positive to add to count, negative to reduce count. +/** + * Calculate liquid XRP balance for an account. + * + * This function may be used to calculate the amount of XRP that + * the holder is able to freely spend. It subtracts reserve requirements. + * + * ownerCountAdj adjusts the owner count in case the caller calculates + * before ledger entries are added or removed. Positive to add, negative + * to subtract. + * + * @param view The ledger view to read from + * @param id The account ID to check + * @param ownerCountAdj Positive to add to count, negative to reduce count + * @param j Journal for logging + * @return The liquid XRP amount available to the account + */ [[nodiscard]] XRPAmount xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j); -/** Adjust the owner count up or down. */ -void -adjustOwnerCount(ApplyView& view, SLE::ref sle, std::int32_t amount, beast::Journal j); +struct Adjustment +{ + std::int32_t ownerCountDelta = 0; + std::int32_t accountCountDelta = 0; +}; -/** Returns IOU issuer transfer fee as Rate. Rate specifies +/** + * Returns the account reserve, in drops. + * + * Actual owner count can be adjusted by delta in ownerCountAdj + * Actual reserve count can be adjusted by delta in accountCountAdj + * The reserve is calculated as: + * (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) * + * increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base" + * + * @param view The ledger view to read from + * @param sle The ledger entry for the account + * @param j Journal for logging + * @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to + * subtract. + * @return The account reserve amount in drops + */ +[[nodiscard]] XRPAmount +accountReserve(ReadView const& view, SLE::const_ref sle, beast::Journal j, Adjustment adj = {}); + +/** + * Convenience overload that accepts AccountID instead of SLE. + * + * @param view The ledger view to read from + * @param id The account ID + * @param j Journal for logging + * @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to + * subtract. + * @return The account reserve amount in drops + */ +[[nodiscard]] inline XRPAmount +accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adjustment adj = {}) +{ + return accountReserve(view, view.read(keylet::account(id)), j, adj); +} + +/** + * Check if an account has sufficient reserve. + * + * @param view The ledger view to read from + * @param tx The transaction being processed + * @param accSle The account's ledger entry + * @param accBalance The account's balance + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to + * subtract. + * @param j Journal for logging (default: null sink) + * @param insufReserveCode The transaction result code to return if the reserve is insufficient + * (default: tecINSUFFICIENT_RESERVE). + * @return Transaction result code + */ +[[nodiscard]] TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + SLE::const_ref sponsorSle, + Adjustment adj, + beast::Journal j, + TER insufReserveCode = tecINSUFFICIENT_RESERVE); + +/** + * Check if an account has sufficient reserve, deriving the sponsor internally. + * + * Equivalent to the overload above, but resolves the sponsor via + * getEffectiveTxReserveSponsor(ctx, accSle) instead of taking it explicitly. Use this + * in the common case where the sponsor is simply the transaction's reserve + * sponsor for accSle. Callers that must force the account's-own-reserve branch + * (passing a null sponsor) or supply a different sponsor should use the + * explicit overload above. + * + * @param ctx The apply-view context (view + tx) + * @param accSle The account's ledger entry + * @param accBalance The account's balance + * @param adj Reserve adjustments (owner/account count deltas) + * @param j Journal for logging (default: null sink) + * @return Transaction result code + */ +[[nodiscard]] TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + Adjustment adj, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); + +/** + * Return number of the objects which reserve is covered by the account(sle) (so called "owner + * count"). Actual owner count can be adjusted by delta in ownerCountAdj. + * + * @param sle The account's ledger entry + * @param j Journal for logging + * @param ownerCountAdj Adjustment to the owner count (default: 0) + * @return The adjusted owner count + */ +std::uint32_t +ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj = 0); + +/** + * Increase owner-count fields when the caller supplies the sponsor. + * + * This helper does not create a ledger object. It updates reserve accounting + * after the caller has created/updated an object. + * If sponsorSle is provided, this also adjusts the account's sponsored count + * and the sponsor's sponsoring count. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param count Amount to add to the owner count + * @param j Journal for logging + */ +void +increaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j); + +/** + * Increase owner-count fields, deriving the tx reserve sponsor internally. + * + * Equivalent to the overload above, but resolves the sponsor via + * getEffectiveTxReserveSponsor(ctx, accountSle) instead of taking it explicitly. Use + * this when the sponsor is the transaction's reserve sponsor for accountSle + * (the common create path). Deletion paths, which derive the sponsor from an + * object's sfSponsor field, should keep using the explicit overload. + * + * @param ctx The apply-view context (view + tx) + * @param accountSle The account's ledger entry + * @param count Amount to add to the owner count + * @param j Journal for logging + */ +void +increaseOwnerCount( + ApplyViewContext ctx, + SLE::ref accountSle, + std::uint32_t count, + beast::Journal j); + +/** + * Convenience overload that accepts AccountID instead of SLE references. + * + * @param view The apply view for making changes + * @param account The account ID + * @param sponsor The optional sponsor account ID + * @param count Amount to add to the owner count + * @param j Journal for logging + */ +inline void +increaseOwnerCount( + ApplyView& view, + AccountID const& account, + std::optional const& sponsor, + std::uint32_t count, + beast::Journal j) +{ + increaseOwnerCount( + view, + view.peek(keylet::account(account)), + sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(), + count, + j); +} + +/** + * Decrease owner-count fields when the caller supplies the sponsor. + * + * This helper does not delete a ledger object. It updates reserve accounting + * after the caller has removed an owner-counted reserve, or for special + * owner-count changes whose sponsor cannot be derived from an object's + * sfSponsor field. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +void +decreaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j); + +/** + * Convenience overload that accepts AccountID instead of SLE references. + * + * @param view The apply view for making changes + * @param account The account ID + * @param sponsor The optional sponsor account ID + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +inline void +decreaseOwnerCount( + ApplyView& view, + AccountID const& account, + std::optional const& sponsor, + std::uint32_t count, + beast::Journal j) +{ + decreaseOwnerCount( + view, + view.peek(keylet::account(account)), + sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(), + count, + j); +} + +/** + * Decrease owner-count fields for an existing ledger object. + * + * This helper derives the reserve sponsor from objectSle's sfSponsor field, + * then updates the same owner-count fields as decreaseOwnerCount. Use this + * when removing an existing object whose reserve sponsor is stored on that + * object. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param objectSle The object's ledger entry + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +void +decreaseOwnerCountForObject( + ApplyView& view, + SLE::ref accountSle, + SLE::ref objectSle, + std::uint32_t count, + beast::Journal j); + +/** + * Convenience overload that accepts AccountID instead of account SLE reference. + * + * @param view The apply view for making changes + * @param account The account ID + * @param objectSle The object's ledger entry + * @param count Amount to remove from the owner count + * @param j Journal for logging + */ +inline void +decreaseOwnerCountForObject( + ApplyView& view, + AccountID const& account, + SLE::ref objectSle, + std::uint32_t count, + beast::Journal j) +{ + SLE::ref accountSle = view.peek(keylet::account(account)); + decreaseOwnerCountForObject(view, accountSle, objectSle, count, j); +} + +/** + * Adjust a LoanBroker's owner count. + * + * A LoanBroker's sfOwnerCount tracks the number of outstanding loans on + * that broker; it is not a reserve-backed owner count and is distinct + * from the broker's pseudo-account's owner count. Loans can never carry a + * reserve sponsor (LoanSet rejects reserve sponsorship at preflight), so + * this never involves sponsor accounting and never invokes the + * ownerCountHook used for ACCOUNT_ROOT reserve tracking. + * + * @param view The apply view for making changes + * @param brokerSle The LoanBroker's ledger entry + * @param delta Amount to add (positive) or remove (negative) from the count + * @param j Journal for logging + */ +void +adjustLoanBrokerOwnerCount( + ApplyView& view, + SLE::ref brokerSle, + std::int32_t delta, + beast::Journal j); + +/** + * Returns IOU issuer transfer fee as Rate. Rate specifies * the fee as fractions of 1 billion. For example, 1% transfer rate * is represented as 1,010,000,000. * @param issuer The IOU issuer @@ -50,35 +339,40 @@ adjustOwnerCount(ApplyView& view, SLE::ref sle, std::int32_t amount, beast::Jour [[nodiscard]] Rate transferRate(ReadView const& view, AccountID const& issuer); -/** Generate a pseudo-account address from a pseudo owner key. - @param pseudoOwnerKey The key to generate the address from - @return The generated account ID -*/ +/** + * Generate a pseudo-account address from a pseudo owner key. + * @param pseudoOwnerKey The key to generate the address from + * @return The generated account ID + */ AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey); -/** Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account - if set. - - The list is constructed during initialization and is const after that. - Pseudo-account designator fields MUST be maintained by including the - SField::sMD_PseudoAccount flag in the SField definition. -*/ +/** + * Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account + * if set. + * + * The list is constructed during initialization and is const after that. + * Pseudo-account designator fields MUST be maintained by including the + * SField::sMD_PseudoAccount flag in the SField definition. + */ [[nodiscard]] std::vector const& getPseudoAccountFields(); -/** Returns true if and only if sleAcct is a pseudo-account or specific - pseudo-accounts in pseudoFieldFilter. - - Returns false if sleAcct is: - - NOT a pseudo-account OR - - NOT a ltACCOUNT_ROOT OR - - null pointer -*/ +/** + * Returns true if and only if sleAcct is a pseudo-account or specific + * pseudo-accounts in pseudoFieldFilter. + * + * Returns false if sleAcct is: + * - NOT a pseudo-account OR + * - NOT a ltACCOUNT_ROOT OR + * - null pointer + */ [[nodiscard]] bool isPseudoAccount(SLE::const_pointer sleAcct, std::set const& pseudoFieldFilter = {}); -/** Convenience overload that reads the account from the view. */ +/** + * Convenience overload that reads the account from the view. + */ [[nodiscard]] inline bool isPseudoAccount( ReadView const& view, @@ -99,11 +393,12 @@ isPseudoAccount( [[nodiscard]] std::expected createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const& ownerField); -/** Checks the destination and tag. - - - Checks that the SLE is not null. - - If the SLE requires a destination tag, checks that there is a tag. -*/ +/** + * Checks the destination and tag. + * + * - Checks that the SLE is not null. + * - If the SLE requires a destination tag, checks that there is a tag. + */ [[nodiscard]] TER checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h b/include/xrpl/ledger/helpers/DirectoryHelpers.h index a95b9bc95a..25085c4252 100644 --- a/include/xrpl/ledger/helpers/DirectoryHelpers.h +++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h @@ -95,19 +95,20 @@ internalDirFirst( } // namespace detail /** @{ */ -/** Returns the first entry in the directory, advancing the index - - @deprecated These are legacy function that are considered deprecated - and will soon be replaced with an iterator-based model - that is easier to use. You should not use them in new code. - - @param view The view against which to operate - @param root The root (i.e. first page) of the directory to iterate - @param page The current page - @param index The index inside the current page - @param entry The entry at the current index - - @return true if the directory isn't empty; false otherwise +/** + * Returns the first entry in the directory, advancing the index + * + * @deprecated These are legacy function that are considered deprecated + * and will soon be replaced with an iterator-based model + * that is easier to use. You should not use them in new code. + * + * @param view The view against which to operate + * @param root The root (i.e. first page) of the directory to iterate + * @param page The current page + * @param index The index inside the current page + * @param entry The entry at the current index + * + * @return true if the directory isn't empty; false otherwise */ bool cdirFirst( @@ -127,19 +128,20 @@ dirFirst( /** @} */ /** @{ */ -/** Returns the next entry in the directory, advancing the index - - @deprecated These are legacy function that are considered deprecated - and will soon be replaced with an iterator-based model - that is easier to use. You should not use them in new code. - - @param view The view against which to operate - @param root The root (i.e. first page) of the directory to iterate - @param page The current page - @param index The index inside the current page - @param entry The entry at the current index - - @return true if the directory isn't empty; false otherwise +/** + * Returns the next entry in the directory, advancing the index + * + * @deprecated These are legacy function that are considered deprecated + * and will soon be replaced with an iterator-based model + * that is easier to use. You should not use them in new code. + * + * @param view The view against which to operate + * @param root The root (i.e. first page) of the directory to iterate + * @param page The current page + * @param index The index inside the current page + * @param entry The entry at the current index + * + * @return true if the directory isn't empty; false otherwise */ bool cdirNext( @@ -158,16 +160,19 @@ dirNext( uint256& entry); /** @} */ -/** Iterate all items in the given directory. */ +/** + * Iterate all items in the given directory. + */ void forEachItem(ReadView const& view, Keylet const& root, std::function const& f); -/** Iterate all items after an item in the given directory. - @param after The key of the item to start after - @param hint The directory page containing `after` - @param limit The maximum number of items to return - @return `false` if the iteration failed -*/ +/** + * Iterate all items after an item in the given directory. + * @param after The key of the item to start after + * @param hint The directory page containing `after` + * @param limit The maximum number of items to return + * @return `false` if the iteration failed + */ bool forEachItemAfter( ReadView const& view, @@ -177,19 +182,22 @@ forEachItemAfter( unsigned int limit, std::function const& f); -/** Iterate all items in an account's owner directory. */ +/** + * Iterate all items in an account's owner directory. + */ inline void forEachItem(ReadView const& view, AccountID const& id, std::function const& f) { forEachItem(view, keylet::ownerDir(id), f); } -/** Iterate all items after an item in an owner directory. - @param after The key of the item to start after - @param hint The directory page containing `after` - @param limit The maximum number of items to return - @return `false` if the iteration failed -*/ +/** + * Iterate all items after an item in an owner directory. + * @param after The key of the item to start after + * @param hint The directory page containing `after` + * @param limit The maximum number of items to return + * @return `false` if the iteration failed + */ inline bool forEachItemAfter( ReadView const& view, @@ -202,13 +210,16 @@ forEachItemAfter( return forEachItemAfter(view, keylet::ownerDir(id), after, hint, limit, f); } -/** Returns `true` if the directory is empty - @param key The key of the directory -*/ +/** + * Returns `true` if the directory is empty + * @param key The key of the directory + */ [[nodiscard]] bool dirIsEmpty(ReadView const& view, Keylet const& k); -/** Returns a function that sets the owner on a directory SLE */ +/** + * Returns a function that sets the owner on a directory SLE + */ [[nodiscard]] std::function describeOwnerDir(AccountID const& account); diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 3f68cf4456..18c9d1cf0b 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -24,17 +25,15 @@ #include #include -#include - namespace xrpl { template TER escrowUnlockApplyHelper( - ApplyView& view, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, - STAmount const& xrpBalance, + XRPAmount xrpBalance, STAmount const& amount, AccountID const& issuer, AccountID const& sender, @@ -45,10 +44,10 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyView& view, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, - STAmount const& xrpBalance, + XRPAmount xrpBalance, STAmount const& amount, AccountID const& issuer, AccountID const& sender, @@ -68,16 +67,26 @@ escrowUnlockApplyHelper( if (receiverIssuer) return tesSUCCESS; - if (!view.exists(trustLineKey) && createAsset) + if (!ctx.view.exists(trustLineKey) && createAsset) { // Can the account cover the trust line's reserve? - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + if (auto const ret = checkReserve( + ctx, + sleDest, + xrpBalance, + *sponsorSle, + {.ownerCountDelta = 1}, + journal, + tecNO_LINE_INSUF_RESERVE); + !isTesSuccess(ret)) { JLOG(journal.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; - - return tecNO_LINE_INSUF_RESERVE; + return ret; } Currency const currency = issue.currency; @@ -85,7 +94,7 @@ escrowUnlockApplyHelper( initialBalance.get().account = noAccount(); if (TER const ter = trustCreate( - view, // payment sandbox + ctx.view, // payment sandbox recvLow, // is dest low? issuer, // source receiver, // destination @@ -99,19 +108,20 @@ escrowUnlockApplyHelper( Issue(currency, receiver), // limit of zero 0, // quality in 0, // quality out + *sponsorSle, // sponsor journal); // journal !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE } - view.update(sleDest); + ctx.view.update(sleDest); } - if (!view.exists(trustLineKey) && !receiverIssuer) + if (!ctx.view.exists(trustLineKey) && !receiverIssuer) return tecNO_LINE; - auto const xferRate = transferRate(view, amount); + auto const xferRate = transferRate(ctx.view, amount); // update if issuer rate is less than locked rate if (xferRate < lockedRate) lockedRate = xferRate; @@ -139,7 +149,7 @@ escrowUnlockApplyHelper( // of the funds if (!createAsset) { - auto const sleRippleState = view.peek(trustLineKey); + auto const sleRippleState = ctx.view.peek(trustLineKey); if (!sleRippleState) return tecINTERNAL; // LCOV_EXCL_LINE @@ -165,7 +175,7 @@ escrowUnlockApplyHelper( // if destination is not the issuer then transfer funds if (!receiverIssuer) { - auto const ter = directSendNoFee(view, issuer, receiver, finalAmt, true, journal); + auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal); if (!isTesSuccess(ter)) return ter; // LCOV_EXCL_LINE } @@ -175,10 +185,10 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyView& view, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, - STAmount const& xrpBalance, + XRPAmount xrpBalance, STAmount const& amount, AccountID const& issuer, AccountID const& sender, @@ -191,27 +201,32 @@ escrowUnlockApplyHelper( auto const mptID = amount.get().getMptID(); auto const issuanceKey = keylet::mptokenIssuance(mptID); - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer) + auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver); + if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer) { - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) - { - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ter = createMPToken(view, mptID, receiver, 0); !isTesSuccess(ter)) + if (auto const ret = checkReserve( + ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal); + !isTesSuccess(ret)) + return ret; + + if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0); + !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE } // update owner count. - adjustOwnerCount(view, sleDest, 1, journal); + increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal); } - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer) + if (!ctx.view.exists(mptKeylet) && !receiverIssuer) return tecNO_PERMISSION; - auto const xferRate = transferRate(view, amount); + auto const xferRate = transferRate(ctx.view, amount); // update if issuer rate is less than locked rate if (xferRate < lockedRate) lockedRate = xferRate; @@ -249,11 +264,11 @@ escrowUnlockApplyHelper( } } return unlockEscrowMPT( - view, + ctx.view, sender, receiver, finalAmt, - view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, + ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, journal); } diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index 873abae272..e2605e9ab7 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -63,7 +63,9 @@ static constexpr std::uint32_t kSecondsInYear = 365 * 24 * 60 * 60; Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); -/// Ensure the periodic payment is always rounded consistently +/** + * Ensure the periodic payment is always rounded consistently + */ inline Number roundPeriodicPayment(Asset const& asset, Number const& periodicPayment, std::int32_t scale) { @@ -127,7 +129,8 @@ struct LoanPaymentParts operator==(LoanPaymentParts const& other) const; }; -/** This structure captures the parts of a loan state. +/** + * This structure captures the parts of a loan state. * * Whether the values are theoretical (unrounded) or rounded will depend on how * it was computed. @@ -324,12 +327,14 @@ struct PaymentComponents // - extra: An additional payment beyond the regular schedule (overpayment) PaymentSpecialCase specialCase = PaymentSpecialCase::None; - // Calculates the tracked interest portion of this payment. - // This is derived from the other components as: - // trackedValueDelta - trackedPrincipalDelta - trackedManagementFeeDelta - // - // @return The amount of tracked interest included in this payment that - // will be paid to the vault. + /** + * Calculates the tracked interest portion of this payment. + * This is derived from the other components as: + * trackedValueDelta - trackedPrincipalDelta - trackedManagementFeeDelta + * + * @return The amount of tracked interest included in this payment that + * will be paid to the vault. + */ [[nodiscard]] Number trackedInterestPart() const; }; @@ -401,7 +406,8 @@ struct LoanStateDeltas // The difference in management fee outstanding between two loan states. Number managementFee; - /* Calculates the total change across all components. + /** + * Calculates the total change across all components. * @return The sum of principal, interest, and management fee deltas. */ [[nodiscard]] Number diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index a725871231..5418e5b26a 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -29,15 +29,17 @@ namespace xrpl { [[nodiscard]] bool isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); -/** Returns true if @p account's MPToken for @p mptIssue carries the - * individual-lock flag (lsfMPTLocked). +/** + * Returns true if @p account's MPToken for @p mptIssue carries the + * individual-lock flag (lsfMPTLocked). * - * @warning This checks only the raw per-holder lock bit. It does **not** - * perform the transitive vault pseudo-account check: if @p mptIssue is a - * vault share whose underlying asset is frozen, this function returns false. - * Call @ref isFrozen instead when determining whether an account may send or - * receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and - * isVaultPseudoAccountFrozen into a single complete check. */ + * @warning This checks only the raw per-holder lock bit. It does **not** + * perform the transitive vault pseudo-account check: if @p mptIssue is a + * vault share whose underlying asset is frozen, this function returns false. + * Call @ref isFrozen instead when determining whether an account may send or + * receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and + * isVaultPseudoAccountFrozen into a single complete check. + */ [[nodiscard]] bool isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); @@ -61,7 +63,8 @@ isAnyFrozen( // //------------------------------------------------------------------------------ -/** Returns MPT transfer fee as Rate. Rate specifies +/** + * Returns MPT transfer fee as Rate. Rate specifies * the fee as fractions of 1 billion. For example, 1% transfer rate * is represented as 1,010,000,000. * @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object @@ -86,7 +89,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] TER authorizeMPToken( - ApplyView& view, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -94,7 +97,8 @@ authorizeMPToken( std::uint32_t flags = 0, std::optional holderID = std::nullopt); -/** Check if the account lacks required authorization for MPT. +/** + * Check if the account lacks required authorization for MPT. * * requireAuth check is recursive for MPT shares in a vault, descending to * assets in the vault, up to maxAssetCheckDepth recursion depth. This is @@ -109,7 +113,8 @@ requireAuth( AuthType authType = AuthType::Legacy, std::uint8_t depth = 0); -/** Enforce account has MPToken to match its authorization. +/** + * Enforce account has MPToken to match its authorization. * * Called from doApply - it will check for expired (and delete if found any) * credentials matching DomainID set in MPTokenIssuance. Must be called if @@ -117,50 +122,52 @@ requireAuth( */ [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyView& view, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, beast::Journal j); -/** Resolve the underlying asset of a vault share. +/** + * Resolve the underlying asset of a vault share. * - * Reads sfReferenceHolding from @p sleShareIssuance to determine which - * asset the vault wraps. @p sleHolding must be the SLE that - * sfReferenceHolding points to — either an ltMPTOKEN (returns its - * MPTIssue) or an ltRIPPLE_STATE (returns its low/high Issue). + * Reads sfReferenceHolding from @p sleShareIssuance to determine which + * asset the vault wraps. @p sleHolding must be the SLE that + * sfReferenceHolding points to — either an ltMPTOKEN (returns its + * MPTIssue) or an ltRIPPLE_STATE (returns its low/high Issue). * - * @pre Both SLEs must exist and @p sleHolding must be of type ltMPTOKEN - * or ltRIPPLE_STATE. Passing any other type is undefined behaviour. - * @param sleShareIssuance MPTokenIssuance SLE for the vault share token. - * @param sleHolding SLE referenced by sfReferenceHolding. - * @return The underlying Asset (MPTIssue or Issue). + * @pre Both SLEs must exist and @p sleHolding must be of type ltMPTOKEN + * or ltRIPPLE_STATE. Passing any other type is undefined behaviour. + * @param sleShareIssuance MPTokenIssuance SLE for the vault share token. + * @param sleHolding SLE referenced by sfReferenceHolding. + * @return The underlying Asset (MPTIssue or Issue). */ [[nodiscard]] Asset assetOfHolding(SLE const& sleShareIssuance, SLE const& sleHolding); -/** Check whether @p to may receive the given MPT from @p from. +/** + * Check whether @p to may receive the given MPT from @p from. * - * The check passes when any of the following is true: - * - @p waive is WaiveMPTCanTransfer::Yes (recovery-path exemption), or - * - @p from or @p to is the issuer, or - * - lsfMPTCanTransfer is set on the MPTokenIssuance. + * The check passes when any of the following is true: + * - @p waive is WaiveMPTCanTransfer::Yes (recovery-path exemption), or + * - @p from or @p to is the issuer, or + * - lsfMPTCanTransfer is set on the MPTokenIssuance. * - * For vault shares (MPTokenIssuances that carry sfReferenceHolding) the - * check recurses into the underlying asset's transferability. This - * recursion is defensive; vault-of-vault-shares is rejected at vault - * creation, so in practice depth never exceeds 1. + * For vault shares (MPTokenIssuances that carry sfReferenceHolding) the + * check recurses into the underlying asset's transferability. This + * recursion is defensive; vault-of-vault-shares is rejected at vault + * creation, so in practice depth never exceeds 1. * - * @param view Ledger state to read from. - * @param mptIssue The MPT issuance being transferred. - * @param from Sending account. - * @param to Receiving account. - * @param waive WaiveMPTCanTransfer::Yes skips the lsfMPTCanTransfer - * check. Use for recovery paths (e.g. unwinding SAV or - * Lending Protocol positions after an issuer revokes - * transferability). - * @param depth Recursion depth; bounded at kMaxAssetCheckDepth. - * @return tesSUCCESS if the transfer is allowed, tecNO_AUTH otherwise. + * @param view Ledger state to read from. + * @param mptIssue The MPT issuance being transferred. + * @param from Sending account. + * @param to Receiving account. + * @param waive WaiveMPTCanTransfer::Yes skips the lsfMPTCanTransfer + * check. Use for recovery paths (e.g. unwinding SAV or + * Lending Protocol positions after an issuer revokes + * transferability). + * @param depth Recursion depth; bounded at kMaxAssetCheckDepth. + * @return tesSUCCESS if the transfer is allowed, tecNO_AUTH otherwise. */ [[nodiscard]] TER canTransfer( @@ -171,22 +178,24 @@ canTransfer( WaiveMPTCanTransfer waive = WaiveMPTCanTransfer::No, std::uint8_t depth = 0); -/** Check whether @p asset may be traded on the DEX. +/** + * Check whether @p asset may be traded on the DEX. * - * For IOU assets the check delegates to the existing offer/AMM freeze - * logic. For MPT assets it checks lsfMPTCanTrade on the MPTokenIssuance. - * Vault shares recurse into the underlying asset's tradability via - * sfReferenceHolding; depth is bounded at kMaxAssetCheckDepth. + * For IOU assets the check delegates to the existing offer/AMM freeze + * logic. For MPT assets it checks lsfMPTCanTrade on the MPTokenIssuance. + * Vault shares recurse into the underlying asset's tradability via + * sfReferenceHolding; depth is bounded at kMaxAssetCheckDepth. * - * @param view Ledger state to read from. - * @param asset The asset to check. - * @param depth Recursion depth; bounded at kMaxAssetCheckDepth. - * @return tesSUCCESS if trading is allowed, tecNO_PERMISSION otherwise. + * @param view Ledger state to read from. + * @param asset The asset to check. + * @param depth Recursion depth; bounded at kMaxAssetCheckDepth. + * @return tesSUCCESS if trading is allowed, tecNO_PERMISSION otherwise. */ [[nodiscard]] TER canTrade(ReadView const& view, Asset const& asset, std::uint8_t depth = 0); -/** Convenience to combine canTrade/Transfer. Returns tesSUCCESS if Asset is Issue. +/** + * Convenience to combine canTrade/Transfer. Returns tesSUCCESS if Asset is Issue. */ [[nodiscard]] TER canMPTTradeAndTransfer( @@ -203,7 +212,7 @@ canMPTTradeAndTransfer( [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -211,7 +220,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal); @@ -243,6 +252,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + SLE::ref sponsorSle, std::uint32_t const flags); TER @@ -250,6 +260,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + SLE::ref sponsorSle, beast::Journal j); //------------------------------------------------------------------------------ @@ -270,7 +281,8 @@ availableMPTAmount(SLE const& sleIssuance); std::int64_t availableMPTAmount(ReadView const& view, MPTID const& mptID); -/** Checks for two types of OutstandingAmount overflow during a send operation. +/** + * Checks for two types of OutstandingAmount overflow during a send operation. * 1. **Direct directSendNoFee (Overflow: No):** A true overflow check when * `OutstandingAmount > MaximumAmount`. This threshold is used for direct * directSendNoFee transactions that bypass the payment engine. @@ -295,7 +307,8 @@ isMPTOverflow( [[nodiscard]] STAmount issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue); -/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer. +/** + * Facilitate tracking of MPT sold by an issuer owning MPT sell offer. * See ApplyView::issuerSelfDebitHookMPT(). */ void diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 1c4d395fbe..d9d195c559 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -24,19 +24,25 @@ namespace xrpl::nft { -/** Delete up to a specified number of offers from the specified token offer - * directory. */ +/** + * Delete up to a specified number of offers from the specified token offer + * directory. + */ std::size_t removeTokenOffersWithLimit( ApplyView& view, Keylet const& directory, std::size_t maxDeletableOffers); -/** Finds the specified token in the owner's token directory. */ +/** + * Finds the specified token in the owner's token directory. + */ std::optional findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID); -/** Finds the token in the owner's token directory. Returns token and page. */ +/** + * Finds the token in the owner's token directory. Returns token and page. + */ struct TokenAndPage { STObject token; @@ -49,33 +55,39 @@ struct TokenAndPage std::optional findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID); -/** Insert the token in the owner's token directory. */ +/** + * Insert the token in the owner's token directory. + */ TER insertToken(ApplyView& view, AccountID owner, STObject&& nft); -/** Remove the token from the owner's token directory. */ +/** + * Remove the token from the owner's token directory. + */ TER removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID); TER removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, SLE::ref page); -/** Deletes the given token offer. - - An offer is tracked in two separate places: - - The token's 'buy' directory, if it's a buy offer; or - - The token's 'sell' directory, if it's a sell offer; and - - The owner directory of the account that placed the offer. - - The offer also consumes one incremental reserve. +/** + * Deletes the given token offer. + * + * An offer is tracked in two separate places: + * - The token's 'buy' directory, if it's a buy offer; or + * - The token's 'sell' directory, if it's a sell offer; and + * - The owner directory of the account that placed the offer. + * + * The offer also consumes one incremental reserve. */ bool deleteTokenOffer(ApplyView& view, SLE::ref offer); -/** Repairs the links in an NFTokenPage directory. - - Returns true if a repair took place, otherwise false. -*/ +/** + * Repairs the links in an NFTokenPage directory. + * + * Returns true if a repair took place, otherwise false. + */ bool repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner); @@ -89,7 +101,9 @@ changeTokenURI( uint256 const& nftokenID, std::optional const& uri); -/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */ +/** + * Preflight checks shared by NFTokenCreateOffer and NFTokenMint + */ NotTEC tokenOfferCreatePreflight( AccountID const& acctID, @@ -101,7 +115,9 @@ tokenOfferCreatePreflight( std::optional const& owner = std::nullopt, std::uint32_t txFlags = tfSellNFToken); -/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint */ +/** + * Preclaim checks shared by NFTokenCreateOffer and NFTokenMint + */ TER tokenOfferCreatePreclaim( ReadView const& view, @@ -115,7 +131,9 @@ tokenOfferCreatePreclaim( std::optional const& owner = std::nullopt, std::uint32_t txFlags = tfSellNFToken); -/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint */ +/** + * doApply implementation shared by NFTokenCreateOffer and NFTokenMint + */ TER tokenOfferCreateApply( ApplyView& view, diff --git a/include/xrpl/ledger/helpers/OfferHelpers.h b/include/xrpl/ledger/helpers/OfferHelpers.h index fc863dff0a..524288ea33 100644 --- a/include/xrpl/ledger/helpers/OfferHelpers.h +++ b/include/xrpl/ledger/helpers/OfferHelpers.h @@ -7,18 +7,19 @@ namespace xrpl { -/** Delete an offer. - - Requirements: - The offer must exist. - The caller must have already checked permissions. - - @param view The ApplyView to modify. - @param sle The offer to delete. - @param j Journal for logging. - - @return tesSUCCESS on success, otherwise an error code. -*/ +/** + * Delete an offer. + * + * Requirements: + * The offer must exist. + * The caller must have already checked permissions. + * + * @param view The ApplyView to modify. + * @param sle The offer to delete. + * @param j Journal for logging. + * + * @return tesSUCCESS on success, otherwise an error code. + */ // [[nodiscard]] // nodiscard commented out so Flow, BookTip and others compile. TER offerDelete(ApplyView& view, SLE::ref sle, beast::Journal j); diff --git a/include/xrpl/ledger/helpers/OracleHelpers.h b/include/xrpl/ledger/helpers/OracleHelpers.h new file mode 100644 index 0000000000..da04618e46 --- /dev/null +++ b/include/xrpl/ledger/helpers/OracleHelpers.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include // IWYU pragma: keep +#include + +#include +#include + +namespace xrpl { + +constexpr std::uint32_t kMinOracleReserveCount = 1; +constexpr std::uint32_t kMaxOracleReserveCount = 2; +constexpr std::size_t kOracleReserveCountThreshold = 5; + +template + requires requires(T const& t) { t.size(); } +inline std::uint32_t +calculateOracleReserve(T const& priceDataSeries) +{ + return priceDataSeries.size() > kOracleReserveCountThreshold ? kMaxOracleReserveCount + : kMinOracleReserveCount; +} + +inline std::uint32_t +calculateOracleReserve(SLE::const_ref oracleSle) +{ + return calculateOracleReserve(oracleSle->getFieldArray(sfPriceDataSeries)); +} + +} // namespace xrpl diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 6e8cd17f7f..5e1f590c58 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -12,37 +12,40 @@ namespace xrpl { -/** Close a payment channel and return its remaining funds to the channel owner. +/** + * Close a payment channel and return its remaining funds to the channel owner. * - * @param slep The SLE for the PayChannel object to close. - * @param view The apply view in which ledger state modifications are made. - * @param key The ledger key identifying the PayChannel entry. - * @param j Journal used for fatal-level diagnostic messages. - * @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal - * fails; tefINTERNAL if the source account SLE cannot be found. + * @param slep The SLE for the PayChannel object to close. + * @param view The apply view in which ledger state modifications are made. + * @param key The ledger key identifying the PayChannel entry. + * @param j Journal used for fatal-level diagnostic messages. + * @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal + * fails; tefINTERNAL if the source account SLE cannot be found. */ TER closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j); -/** Add two uint32_t values with saturation at UINT32_MAX. +/** + * Add two uint32_t values with saturation at UINT32_MAX. * - * @param rules The current ledger rules used to check amendment status. - * @param lhs Left-hand operand. - * @param rhs Right-hand operand. - * @return @p lhs + @p rhs, saturated at UINT32_MAX when the amendment - * is active. + * @param rules The current ledger rules used to check amendment status. + * @param lhs Left-hand operand. + * @param rhs Right-hand operand. + * @return @p lhs + @p rhs, saturated at UINT32_MAX when the amendment + * is active. */ uint32_t saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs); -/** Determine whether a payment channel time field represents an expired time. +/** + * Determine whether a payment channel time field represents an expired time. * - * @param view The apply view providing the parent close time and rules. - * @param timeField The optional expiry timestamp (seconds since the XRP - * Ledger epoch). If empty, the function returns false. - * @return @c true if @p timeField is set and the indicated time is - * in the past relative to the view's parent close time; - * @c false otherwise. + * @param view The apply view providing the parent close time and rules. + * @param timeField The optional expiry timestamp (seconds since the XRP + * Ledger epoch). If empty, the function returns false. + * @return @c true if @p timeField is set and the indicated time is + * in the past relative to the view's parent close time; + * @c false otherwise. */ bool isChannelExpired(ApplyView const& view, std::optional timeField); diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index ab09b931cc..a0508d074f 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -31,13 +31,14 @@ namespace xrpl { // //------------------------------------------------------------------------------ -/** Calculate the maximum amount of IOUs that an account can hold - @param view the ledger to check against. - @param account the account of interest. - @param issuer the issuer of the IOU. - @param currency the IOU to check. - @return The maximum amount that can be held. -*/ +/** + * Calculate the maximum amount of IOUs that an account can hold + * @param view the ledger to check against. + * @param account the account of interest. + * @param issuer the issuer of the IOU. + * @param currency the IOU to check. + * @return The maximum amount that can be held. + */ /** @{ */ STAmount creditLimit( @@ -50,12 +51,13 @@ IOUAmount creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Currency const& cur); /** @} */ -/** Returns the amount of IOUs issued by issuer that are held by an account - @param view the ledger to check against. - @param account the account of interest. - @param issuer the issuer of the IOU. - @param currency the IOU to check. -*/ +/** + * Returns the amount of IOUs issued by issuer that are held by an account + * @param view the ledger to check against. + * @param account the account of interest. + * @param issuer the issuer of the IOU. + * @param currency the IOU to check. + */ /** @{ */ STAmount creditBalance( @@ -134,10 +136,11 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Issue const& iss // //------------------------------------------------------------------------------ -/** Create a trust line - - This can set an initial balance. -*/ +/** + * Create a trust line + * + * This can set an initial balance. + */ [[nodiscard]] TER trustCreate( ApplyView& view, @@ -156,6 +159,7 @@ trustCreate( // Issuer should be the account being set. std::uint32_t uQualityIn, std::uint32_t uQualityOut, + SLE::ref sponsorSle, beast::Journal j); [[nodiscard]] TER @@ -178,6 +182,7 @@ issueIOU( AccountID const& account, STAmount const& amount, Issue const& issue, + SLE::ref sponsorSle, beast::Journal j); [[nodiscard]] TER @@ -194,7 +199,8 @@ redeemIOU( // //------------------------------------------------------------------------------ -/** Check if the account lacks required authorization. +/** + * Check if the account lacks required authorization. * * Return tecNO_AUTH or tecNO_LINE if it does * and tesSUCCESS otherwise. @@ -218,7 +224,8 @@ requireAuth( AccountID const& account, AuthType authType = AuthType::Legacy); -/** Check if the destination account is allowed +/** + * Check if the destination account is allowed * to receive IOU. Return terNO_RIPPLE if rippling is * disabled on both sides and tesSUCCESS otherwise. */ @@ -231,11 +238,13 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc // //------------------------------------------------------------------------------ -/// Any transactors that call addEmptyHolding() in doApply must call -/// canAddHolding() in preflight with the same View and Asset +/** + * Any transactors that call addEmptyHolding() in doApply must call + * canAddHolding() in preflight with the same View and Asset + */ [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -243,12 +252,13 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal); -/** Delete trustline to AMM. The passed `sle` must be obtained from a prior +/** + * Delete trustline to AMM. The passed `sle` must be obtained from a prior * call to view.peek(). Fail if neither side of the trustline is AMM or * if ammAccountID is seated and is not one of the trustline's side. */ @@ -259,7 +269,8 @@ deleteAMMTrustLine( std::optional const& ammAccountID, beast::Journal j); -/** Delete AMMs MPToken. The passed `sle` must be obtained from a prior +/** + * Delete AMMs MPToken. The passed `sle` must be obtained from a prior * call to view.peek(). */ [[nodiscard]] TER diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h new file mode 100644 index 0000000000..98bf419140 --- /dev/null +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -0,0 +1,204 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +/** + * Whether the given transaction type may use reserve sponsorship (v1). + * + * Reserve sponsorship is restricted to an explicit allow-list of transaction + * types; all others reject spfSponsorReserve at preflight. + */ +bool +isReserveSponsorAllowed(TxType txType); + +/** + * Whether the transaction's fee is sponsored (sfSponsor present + spfSponsorFee set). + */ +inline bool +isFeeSponsored(STTx const& tx) +{ + return tx.isFieldPresent(sfSponsor) && ((tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u); +} + +/** + * Whether the transaction's reserve is sponsored (sfSponsor present + spfSponsorReserve set). + */ +inline bool +isReserveSponsored(STTx const& tx) +{ + return tx.isFieldPresent(sfSponsor) && + ((tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u); +} + +/** + * Return the AccountID of the transaction's reserve sponsor, or nullopt if unsponsored. + */ +std::optional +getTxReserveSponsorID(STTx const& tx); + +/** + * Return a mutable SLE for the transaction's reserve sponsor account. + * + * @param ctx The apply-view context (view + tx) + * @return The sponsor account SLE, a null pointer if the tx is not + * reserve-sponsored, or tecINTERNAL if the sponsor account cannot + * be loaded (an already-checked invariant). + */ +std::expected +getTxReserveSponsor(ApplyViewContext ctx); + +/** + * Return a read-only SLE for the transaction's reserve sponsor account. + * + * @param view The ledger read view + * @param tx The transaction to inspect + * @return The sponsor account SLE, a null pointer if the tx is not + * reserve-sponsored, or tecINTERNAL if the sponsor account cannot + * be loaded (an already-checked invariant). + */ +std::expected +getTxReserveSponsor(ReadView const& view, STTx const& tx); + +/** + * The transaction's reserve sponsor for the given account, if applicable. + * + * A reserve sponsor only covers the transaction submitter's own objects, so + * this returns the tx reserve sponsor SLE only when accountSle is the tx's own + * (non-pseudo) account; otherwise it returns a null sponsor pointer. This is + * the single source of truth for the "sponsor applies to tx.Account only" rule + * that the sponsor-deriving helper overloads in AccountRootHelpers rely on. + * + * @param ctx The apply-view context (view + tx) + * @param accountSle The account whose sponsor is being resolved + * @return The sponsor SLE (nullptr if unsponsored), or tecINTERNAL if the + * sponsor account cannot be loaded (an already-checked invariant) + */ +[[nodiscard]] std::expected +getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::const_ref accountSle); + +/** + * Return the AccountID stored in the given sponsor field of a ledger entry, or nullopt if absent. + */ +std::optional +getLedgerEntryReserveSponsorID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor); + +/** + * Return a mutable SLE for the reserve sponsor recorded on a ledger entry. + * + * Reads the sponsor AccountID from @p field on @p sle and peeks the + * corresponding account root in @p view. + * + * @param view The mutable apply view + * @param sle The ledger entry whose sponsor field is inspected + * @param field The field that holds the sponsor AccountID (defaults to sfSponsor) + * @return The sponsor account SLE, or a null pointer if the entry is unsponsored. + */ +SLE::pointer +getLedgerEntryReserveSponsor( + ApplyView& view, + SLE::const_ref sle, + SF_ACCOUNT const& field = sfSponsor); + +/** + * Stamp a reserve sponsor onto a ledger entry using an explicit sponsor SLE. + * + * Sets @p field on @p sle to the AccountID from @p sponsorSle. A no-op when + * @p sponsorSle is null (unsponsored). For RippleState entries the field must + * be sfHighSponsor or sfLowSponsor; for all other entry types it must be + * sfSponsor. + * + * @param sle The ledger entry to stamp + * @param sponsorSle The sponsor's account root SLE (null → no-op) + * @param field The sponsor field to set (defaults to sfSponsor) + */ +void +addSponsorToLedgerEntry( + SLE::ref sle, + SLE::const_ref sponsorSle, + SF_ACCOUNT const& field = sfSponsor); + +/** + * Stamp the transaction's reserve sponsor onto a newly-created ledger entry. + * + * Equivalent to the overload above, but resolves the sponsor via + * getTxReserveSponsor(ctx) instead of taking it explicitly. A no-op when the + * transaction is not reserve-sponsored. The entry is assumed to be owned by + * the transaction submitter, which is the only account a tx reserve sponsor + * can cover. + */ +void +addSponsorToLedgerEntry(ApplyViewContext ctx, SLE::ref sle, SF_ACCOUNT const& field = sfSponsor); + +/** + * Remove the reserve sponsor field from a ledger entry. + * + * A no-op when @p field is not present on @p sle. For RippleState entries + * the field must be sfHighSponsor or sfLowSponsor; for all other entry types + * it must be sfSponsor. + * + * @param sle The ledger entry to modify + * @param field The sponsor field to clear (defaults to sfSponsor) + */ +void +removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor); + +/** + * Whether @p account is the owner of a ledger entry for sponsorship purposes. + * + * Ownership rules vary by entry type. For RippleState entries the owner is + * whichever side of the trust line holds the reserve. For credentials, the + * owner is the subject once accepted and the issuer before acceptance. + * + * @param view The ledger read view (used for SignerList lookup) + * @param sle The ledger entry whose owner is checked + * @param account The candidate account to match against + * @return true if @p account owns @p sle, false otherwise. + */ +bool +isLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& account); + +/** + * Whether this ledger entry type can have a reserve sponsor attached to it. + */ +bool +isLedgerEntrySupportedBySponsorship(SLE const& sle); + +/** + * Return the number of owner-count units the ledger entry consumes. + * + * Most entries cost 1. Exceptions: Oracles scale with their price-data series + * size, Vaults cost 2 (vault + pseudo-account), and legacy SignerList entries + * (pre-MultiSignReserve) cost 2 + signer count. + */ +std::uint32_t +getLedgerEntryOwnerCount(SLE const& sle); + +/** + * Return the SField used to store the reserve sponsor for @p owner on @p sle. + * + * For most entry types this is sfSponsor. RippleState entries use + * sfHighSponsor or sfLowSponsor depending on which side of the trust line + * @p owner holds. + * + * @param sle The ledger entry + * @param owner The account whose sponsor field is needed + * @return sfHighSponsor, sfLowSponsor, or sfSponsor as appropriate. + */ +SF_ACCOUNT const& +getLedgerEntrySponsorField(SLE const& sle, AccountID const& owner); + +} // namespace xrpl diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 32f785a0d6..501101136a 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -27,21 +28,30 @@ namespace xrpl { // //------------------------------------------------------------------------------ -/** Controls the treatment of frozen account balances */ +/** + * Controls the treatment of frozen account balances + */ enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen }; -/** Controls the treatment of unauthorized MPT balances */ +/** + * Controls the treatment of unauthorized MPT balances + */ enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized }; -/** Controls whether to include the account's full spendable balance */ +/** + * Controls whether to include the account's full spendable balance + */ enum class SpendableHandling { SimpleBalance, FullBalance }; enum class WaiveTransferFee : bool { No = false, Yes }; -/** Controls whether accountSend is allowed to overflow OutstandingAmount **/ +/** + * Controls whether accountSend is allowed to overflow OutstandingAmount * + */ enum class AllowMPTOverflow : bool { No = false, Yes }; -/** Controls whether canTransfer enforces lsfMPTCanTransfer on MPTs. +/** + * Controls whether canTransfer enforces lsfMPTCanTransfer on MPTs. * * Default is No (enforce). Use Yes at call sites that must remain available * even when an MPT issuer has cleared lsfMPTCanTransfer - for example, @@ -80,9 +90,9 @@ isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& checkIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset); /** - * isFrozen check is recursive for MPT shares in a vault, descending to - * assets in the vault, up to maxAssetCheckDepth recursion depth. This is - * purely defensive, as we currently do not allow such vaults to be created. + * isFrozen check is recursive for MPT shares in a vault, descending to + * assets in the vault, up to maxAssetCheckDepth recursion depth. This is + * purely defensive, as we currently do not allow such vaults to be created. */ [[nodiscard]] bool isFrozen( @@ -121,9 +131,9 @@ isDeepFrozen( std::uint8_t depth = 0); /** - * isFrozen check is recursive for MPT shares in a vault, descending to - * assets in the vault, up to maxAssetCheckDepth recursion depth. This is - * purely defensive, as we currently do not allow such vaults to be created. + * isFrozen check is recursive for MPT shares in a vault, descending to + * assets in the vault, up to maxAssetCheckDepth recursion depth. This is + * purely defensive, as we currently do not allow such vaults to be created. */ [[nodiscard]] bool isDeepFrozen( @@ -284,7 +294,8 @@ accountFunds( AuthHandling authHandling, beast::Journal j); -/** Returns the transfer fee as Rate based on the type of token +/** + * Returns the transfer fee as Rate based on the type of token * @param view The ledger view * @param amount The amount to transfer */ @@ -302,7 +313,7 @@ canAddHolding(ReadView const& view, Asset const& asset); [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -310,7 +321,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal); @@ -349,7 +360,8 @@ canTransfer( // --> bCheckIssuer : normally require issuer to be involved. // [[nodiscard]] // nodiscard commented out so DirectStep.cpp compiles. -/** Calls static directSendNoFeeIOU if saAmount represents Issue. +/** + * Calls static directSendNoFeeIOU if saAmount represents Issue. * Calls static directSendNoFeeMPT if saAmount represents MPTIssue. */ TER @@ -361,7 +373,8 @@ directSendNoFee( bool bCheckIssuer, beast::Journal j); -/** Calls static accountSendIOU if saAmount represents Issue. +/** + * Calls static accountSendIOU if saAmount represents Issue. * Calls static accountSendMPT if saAmount represents MPTIssue. */ [[nodiscard]] TER @@ -371,11 +384,13 @@ accountSend( AccountID const& to, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle = {}, WaiveTransferFee waiveFee = WaiveTransferFee::No, AllowMPTOverflow allowOverflow = AllowMPTOverflow::No); using MultiplePaymentDestinations = std::vector>; -/** Like accountSend, except one account is sending multiple payments (with the +/** + * Like accountSend, except one account is sending multiple payments (with the * same asset!) simultaneously * * Calls static accountSendMultiIOU if saAmount represents Issue. diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 2344b4de77..1bd1663314 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -9,57 +9,63 @@ namespace xrpl { -/** From the perspective of a vault, return the number of shares to give - depositor when they offer a fixed amount of assets. Note, since shares are - MPT, this number is integral and always truncated in this calculation. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param assets The amount of assets to convert. - - @return The number of shares, or nullopt on error. -*/ +/** + * From the perspective of a vault, return the number of shares to give + * depositor when they offer a fixed amount of assets. Note, since shares are + * MPT, this number is integral and always truncated in this calculation. + * + * @param vault The vault SLE. + * @param issuance The MPTokenIssuance SLE for the vault's shares. + * @param assets The amount of assets to convert. + * + * @return The number of shares, or nullopt on error. + */ [[nodiscard]] std::optional assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& assets); -/** From the perspective of a vault, return the number of assets to take from - depositor when they receive a fixed amount of shares. Note, since shares are - MPT, they are always an integral number. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param shares The amount of shares to convert. - - @return The number of assets, or nullopt on error. -*/ +/** + * From the perspective of a vault, return the number of assets to take from + * depositor when they receive a fixed amount of shares. Note, since shares are + * MPT, they are always an integral number. + * + * @param vault The vault SLE. + * @param issuance The MPTokenIssuance SLE for the vault's shares. + * @param shares The amount of shares to convert. + * + * @return The number of assets, or nullopt on error. + */ [[nodiscard]] std::optional sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares); -/** Controls whether to truncate shares instead of rounding. */ +/** + * Controls whether to truncate shares instead of rounding. + */ enum class TruncateShares : bool { No = false, Yes = true }; -/** Controls whether the withdraw conversion helpers - (assetsToSharesWithdraw and sharesToAssetsWithdraw) subtract - sfLossUnrealized from sfAssetsTotal before computing the exchange rate. - The default (No) applies the standard discounted rate; Yes is used when - the redeemer is the sole remaining shareholder. -*/ +/** + * Controls whether the withdraw conversion helpers + * (assetsToSharesWithdraw and sharesToAssetsWithdraw) subtract + * sfLossUnrealized from sfAssetsTotal before computing the exchange rate. + * The default (No) applies the standard discounted rate; Yes is used when + * the redeemer is the sole remaining shareholder. + */ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true }; -/** From the perspective of a vault, return the number of shares to demand from - the depositor when they ask to withdraw a fixed amount of assets. Since - shares are MPT this number is integral, and it will be rounded to nearest - unless explicitly requested to be truncated instead. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param assets The amount of assets to convert. - @param truncate Whether to truncate instead of rounding. - @param waive Whether to waive the unrealized-loss discount when computing - the exchange rate. - - @return The number of shares, or nullopt on error. -*/ +/** + * From the perspective of a vault, return the number of shares to demand from + * the depositor when they ask to withdraw a fixed amount of assets. Since + * shares are MPT this number is integral, and it will be rounded to nearest + * unless explicitly requested to be truncated instead. + * + * @param vault The vault SLE. + * @param issuance The MPTokenIssuance SLE for the vault's shares. + * @param assets The amount of assets to convert. + * @param truncate Whether to truncate instead of rounding. + * @param waive Whether to waive the unrealized-loss discount when computing + * the exchange rate. + * + * @return The number of shares, or nullopt on error. + */ [[nodiscard]] std::optional assetsToSharesWithdraw( SLE::const_ref vault, @@ -68,18 +74,19 @@ assetsToSharesWithdraw( TruncateShares truncate = TruncateShares::No, WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No); -/** From the perspective of a vault, return the number of assets to give the - depositor when they redeem a fixed amount of shares. Note, since shares are - MPT, they are always an integral number. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param shares The amount of shares to convert. - @param waive Whether to waive (i.e. not subtract) the vault's unrealized - loss when computing the exchange rate. - - @return The number of assets, or nullopt on error. -*/ +/** + * From the perspective of a vault, return the number of assets to give the + * depositor when they redeem a fixed amount of shares. Note, since shares are + * MPT, they are always an integral number. + * + * @param vault The vault SLE. + * @param issuance The MPTokenIssuance SLE for the vault's shares. + * @param shares The amount of shares to convert. + * @param waive Whether to waive (i.e. not subtract) the vault's unrealized + * loss when computing the exchange rate. + * + * @return The number of assets, or nullopt on error. + */ [[nodiscard]] std::optional sharesToAssetsWithdraw( SLE::const_ref vault, @@ -87,15 +94,16 @@ sharesToAssetsWithdraw( STAmount const& shares, WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No); -/** Returns true iff `account` holds all of the vault's outstanding shares — - i.e. is the sole remaining shareholder. Returns false if the account - holds no shares or fewer than the total outstanding. - - @param view The ledger view. - @param account The candidate sole shareholder. - @param issuance The MPTokenIssuance SLE for the vault's shares; provides - both the share MPTID and the outstanding-amount total. -*/ +/** + * Returns true iff `account` holds all of the vault's outstanding shares — + * i.e. is the sole remaining shareholder. Returns false if the account + * holds no shares or fewer than the total outstanding. + * + * @param view The ledger view. + * @param account The candidate sole shareholder. + * @param issuance The MPTokenIssuance SLE for the vault's shares; provides + * both the share MPTID and the outstanding-amount total. + */ [[nodiscard]] bool isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance); diff --git a/include/xrpl/net/HTTPClient.h b/include/xrpl/net/HTTPClient.h index 7ed9b35b9b..752afac9c4 100644 --- a/include/xrpl/net/HTTPClient.h +++ b/include/xrpl/net/HTTPClient.h @@ -14,7 +14,8 @@ namespace xrpl { -/** Provides an asynchronous HTTP client implementation with optional SSL. +/** + * Provides an asynchronous HTTP client implementation with optional SSL. */ class HTTPClient { @@ -30,14 +31,15 @@ public: bool sslVerify, beast::Journal j); - /** Destroys the global SSL context created by initializeSSLContext(). + /** + * Destroys the global SSL context created by initializeSSLContext(). * - * This releases the underlying boost::asio::ssl::context and any - * associated OpenSSL resources. Must not be called while any - * HTTPClient requests are in flight. + * This releases the underlying boost::asio::ssl::context and any + * associated OpenSSL resources. Must not be called while any + * HTTPClient requests are in flight. * - * @note Currently only called from tests during teardown. In production, - * the SSL context lives for the lifetime of the process. + * @note Currently only called from tests during teardown. In production, + * the SSL context lives for the lifetime of the process. */ static void cleanupSSLContext(); diff --git a/include/xrpl/net/RegisterSSLCerts.h b/include/xrpl/net/RegisterSSLCerts.h index 5cc9934638..004f893515 100644 --- a/include/xrpl/net/RegisterSSLCerts.h +++ b/include/xrpl/net/RegisterSSLCerts.h @@ -5,13 +5,14 @@ #include namespace xrpl { -/** Register default SSL certificates. - - Register the system default SSL root certificates. On linux/mac, - this just calls asio's `set_default_verify_paths` to look in standard - operating system locations. On windows, it uses the OS certificate - store accessible via CryptoAPI. -*/ +/** + * Register default SSL certificates. + * + * Register the system default SSL root certificates. On linux/mac, + * this just calls asio's `set_default_verify_paths` to look in standard + * operating system locations. On windows, it uses the OS certificate + * store accessible via CryptoAPI. + */ void registerSSLCerts(boost::asio::ssl::context&, boost::system::error_code&, beast::Journal j); diff --git a/include/xrpl/nodestore/Backend.h b/include/xrpl/nodestore/Backend.h index 29c4a8b526..564a874c5e 100644 --- a/include/xrpl/nodestore/Backend.h +++ b/include/xrpl/nodestore/Backend.h @@ -15,33 +15,37 @@ namespace xrpl::NodeStore { -/** A backend used for the NodeStore. - - The NodeStore uses a swappable backend so that other database systems - can be tried. Different databases may offer various features such - as improved performance, fault tolerant or distributed storage, or - all in-memory operation. - - A given instance of a backend is fixed to a particular key size. -*/ +/** + * A backend used for the NodeStore. + * + * The NodeStore uses a swappable backend so that other database systems + * can be tried. Different databases may offer various features such + * as improved performance, fault tolerant or distributed storage, or + * all in-memory operation. + * + * A given instance of a backend is fixed to a particular key size. + */ class Backend { public: - /** Destroy the backend. - - All open files are closed and flushed. If there are batched writes - or other tasks scheduled, they will be completed before this call - returns. - */ + /** + * Destroy the backend. + * + * All open files are closed and flushed. If there are batched writes + * or other tasks scheduled, they will be completed before this call + * returns. + */ virtual ~Backend() = default; - /** Get the human-readable name of this backend. - This is used for diagnostic output. - */ + /** + * Get the human-readable name of this backend. + * This is used for diagnostic output. + */ virtual std::string getName() = 0; - /** Get the block size for backends that support it + /** + * Get the block size for backends that support it */ [[nodiscard]] virtual std::optional getBlockSize() const @@ -49,25 +53,28 @@ public: return std::nullopt; } - /** Open the backend. - @param createIfMissing Create the database files if necessary. - This allows the caller to catch exceptions. - */ + /** + * Open the backend. + * @param createIfMissing Create the database files if necessary. + * This allows the caller to catch exceptions. + */ virtual void open(bool createIfMissing = true) = 0; - /** Returns true is the database is open. + /** + * Returns true is the database is open. */ virtual bool isOpen() = 0; - /** Open the backend. - @param createIfMissing Create the database files if necessary. - @param appType Deterministic appType used to create a backend. - @param uid Deterministic uid used to create a backend. - @param salt Deterministic salt used to create a backend. - @throws std::runtime_error is function is called not for NuDB backend. - */ + /** + * Open the backend. + * @param createIfMissing Create the database files if necessary. + * @param appType Deterministic appType used to create a backend. + * @param uid Deterministic uid used to create a backend. + * @param salt Deterministic salt used to create a backend. + * @throws std::runtime_error is function is called not for NuDB backend. + */ virtual void open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt) { @@ -75,60 +82,70 @@ public: "Deterministic appType/uid/salt not supported by backend " + getName()); } - /** Close the backend. - This allows the caller to catch exceptions. - */ + /** + * Close the backend. + * This allows the caller to catch exceptions. + */ virtual void close() = 0; - /** Fetch a single object. - If the object is not found or an error is encountered, the - result will indicate the condition. - @note This will be called concurrently. - @param hash The hash of the object. - @param pObject [out] The created object if successful. - @return The result of the operation. - */ + /** + * Fetch a single object. + * If the object is not found or an error is encountered, the + * result will indicate the condition. + * @note This will be called concurrently. + * @param hash The hash of the object. + * @param pObject [out] The created object if successful. + * @return The result of the operation. + */ virtual Status fetch(uint256 const& hash, std::shared_ptr* pObject) = 0; - /** Store a single object. - Depending on the implementation this may happen immediately - or deferred using a scheduled task. - @note This will be called concurrently. - @param object The object to store. - */ + /** + * Store a single object. + * Depending on the implementation this may happen immediately + * or deferred using a scheduled task. + * @note This will be called concurrently. + * @param object The object to store. + */ virtual void store(std::shared_ptr const& object) = 0; - /** Store a group of objects. - @note This function will not be called concurrently with - itself or @ref store. - */ + /** + * Store a group of objects. + * @note This function will not be called concurrently with + * itself or @ref store. + */ virtual void storeBatch(Batch const& batch) = 0; virtual void sync() = 0; - /** Visit every object in the database - This is usually called during import. - @note This routine will not be called concurrently with itself - or other methods. - @see import - */ + /** + * Visit every object in the database + * This is usually called during import. + * @note This routine will not be called concurrently with itself + * or other methods. + * @see import + */ virtual void forEach(std::function)> f) = 0; - /** Estimate the number of write operations pending. */ + /** + * Estimate the number of write operations pending. + */ virtual int getWriteLoad() = 0; - /** Remove contents on disk upon destruction. */ + /** + * Remove contents on disk upon destruction. + */ virtual void setDeletePath() = 0; - /** Perform consistency checks on database. + /** + * Perform consistency checks on database. * * This method is implemented only by NuDBBackend. It is not yet called * anywhere, but it might be a good idea to one day call it at startup to @@ -139,7 +156,9 @@ public: { } - /** Returns the number of file descriptors the backend expects to need. */ + /** + * Returns the number of file descriptors the backend expects to need. + */ [[nodiscard]] virtual int fdRequired() const = 0; }; diff --git a/include/xrpl/nodestore/Database.h b/include/xrpl/nodestore/Database.h index 49002ee301..96ba91bd76 100644 --- a/include/xrpl/nodestore/Database.h +++ b/include/xrpl/nodestore/Database.h @@ -27,100 +27,109 @@ class Section; namespace xrpl::NodeStore { -/** Persistency layer for NodeObject - - A Node is a ledger object which is uniquely identified by a key, which is - the 256-bit hash of the body of the node. The payload is a variable length - block of serialized data. - - All ledger data is stored as node objects and as such, needs to be persisted - between launches. Furthermore, since the set of node objects will in - general be larger than the amount of available memory, purged node objects - which are later accessed must be retrieved from the node store. - - @see NodeObject -*/ +/** + * Persistency layer for NodeObject + * + * A Node is a ledger object which is uniquely identified by a key, which is + * the 256-bit hash of the body of the node. The payload is a variable length + * block of serialized data. + * + * All ledger data is stored as node objects and as such, needs to be persisted + * between launches. Furthermore, since the set of node objects will in + * general be larger than the amount of available memory, purged node objects + * which are later accessed must be retrieved from the node store. + * + * @see NodeObject + */ class Database { public: Database() = delete; - /** Construct the node store. - - @param scheduler The scheduler to use for performing asynchronous tasks. - @param readThreads The number of asynchronous read threads to create. - @param config The configuration settings - @param journal Destination for logging output. - */ + /** + * Construct the node store. + * + * @param scheduler The scheduler to use for performing asynchronous tasks. + * @param readThreads The number of asynchronous read threads to create. + * @param config The configuration settings + * @param journal Destination for logging output. + */ Database(Scheduler& scheduler, int readThreads, Section const& config, beast::Journal j); - /** Destroy the node store. - All pending operations are completed, pending writes flushed, - and files closed before this returns. - */ + /** + * Destroy the node store. + * All pending operations are completed, pending writes flushed, + * and files closed before this returns. + */ virtual ~Database(); - /** Retrieve the name associated with this backend. - This is used for diagnostics and may not reflect the actual path - or paths used by the underlying backend. - */ + /** + * Retrieve the name associated with this backend. + * This is used for diagnostics and may not reflect the actual path + * or paths used by the underlying backend. + */ virtual std::string getName() const = 0; - /** Import objects from another database. */ + /** + * Import objects from another database. + */ virtual void importDatabase(Database& source) = 0; - /** Retrieve the estimated number of pending write operations. - This is used for diagnostics. - */ + /** + * Retrieve the estimated number of pending write operations. + * This is used for diagnostics. + */ virtual std::int32_t getWriteLoad() const = 0; - /** Store the object. - - The caller's Blob parameter is overwritten. - - @param type The type of object. - @param data The payload of the object. The caller's - variable is overwritten. - @param hash The 256-bit hash of the payload data. - @param ledgerSeq The sequence of the ledger the object belongs to. - - @return `true` if the object was stored? - */ + /** + * Store the object. + * + * The caller's Blob parameter is overwritten. + * + * @param type The type of object. + * @param data The payload of the object. The caller's + * variable is overwritten. + * @param hash The 256-bit hash of the payload data. + * @param ledgerSeq The sequence of the ledger the object belongs to. + * + * @return `true` if the object was stored? + */ virtual void store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t ledgerSeq) = 0; - /* Check if two ledgers are in the same database - - If these two sequence numbers map to the same database, - the result of a fetch with either sequence number would - be identical. - - @param s1 The first sequence number - @param s2 The second sequence number - - @return 'true' if both ledgers would be in the same DB - - */ + /** + * Check if two ledgers are in the same database + * + * If these two sequence numbers map to the same database, + * the result of a fetch with either sequence number would + * be identical. + * + * @param s1 The first sequence number + * @param s2 The second sequence number + * + * @return 'true' if both ledgers would be in the same DB + */ virtual bool isSameDB(std::uint32_t s1, std::uint32_t s2) = 0; virtual void sync() = 0; - /** Fetch a node object. - If the object is known to be not in the database, isn't found in the - database during the fetch, or failed to load correctly during the fetch, - `nullptr` is returned. - - @note This can be called concurrently. - @param hash The key of the object to retrieve. - @param ledgerSeq The sequence of the ledger where the object is stored. - @param fetchType the type of fetch, synchronous or asynchronous. - @return The object, or nullptr if it couldn't be retrieved. - */ + /** + * Fetch a node object. + * If the object is known to be not in the database, isn't found in the + * database during the fetch, or failed to load correctly during the fetch, + * `nullptr` is returned. + * + * @note This can be called concurrently. + * @param hash The key of the object to retrieve. + * @param ledgerSeq The sequence of the ledger where the object is stored. + * @param fetchType the type of fetch, synchronous or asynchronous. + * @return The object, or nullptr if it couldn't be retrieved. + */ std::shared_ptr fetchNodeObject( uint256 const& hash, @@ -128,29 +137,33 @@ public: FetchType fetchType = FetchType::Synchronous, bool duplicate = false); - /** Fetch an object without waiting. - If I/O is required to determine whether or not the object is present, - `false` is returned. Otherwise, `true` is returned and `object` is set - to refer to the object, or `nullptr` if the object is not present. - If I/O is required, the I/O is scheduled and `true` is returned - - @note This can be called concurrently. - @param hash The key of the object to retrieve - @param ledgerSeq The sequence of the ledger where the - object is stored. - @param callback Callback function when read completes - */ + /** + * Fetch an object without waiting. + * If I/O is required to determine whether or not the object is present, + * `false` is returned. Otherwise, `true` is returned and `object` is set + * to refer to the object, or `nullptr` if the object is not present. + * If I/O is required, the I/O is scheduled and `true` is returned + * + * @note This can be called concurrently. + * @param hash The key of the object to retrieve + * @param ledgerSeq The sequence of the ledger where the + * object is stored. + * @param callback Callback function when read completes + */ virtual void asyncFetch( uint256 const& hash, std::uint32_t ledgerSeq, std::function const&)>&& callback); - /** Remove expired entries from the positive and negative caches. */ + /** + * Remove expired entries from the positive and negative caches. + */ virtual void sweep() = 0; - /** Gather statistics pertaining to read and write activities. + /** + * Gather statistics pertaining to read and write activities. * * @param obj Json object reference into which to place counters. */ @@ -187,7 +200,9 @@ public: void getCountsJson(json::Value& obj); - /** Returns the number of file descriptors the database expects to need */ + /** + * Returns the number of file descriptors the database expects to need + */ int fdRequired() const { @@ -200,7 +215,8 @@ public: bool isStopping() const; - /** @return The earliest ledger sequence allowed + /** + * @return The earliest ledger sequence allowed */ [[nodiscard]] std::uint32_t earliestLedgerSeq() const noexcept @@ -277,13 +293,14 @@ private: FetchReport& fetchReport, bool duplicate) = 0; - /** Visit every object in the database - This is usually called during import. - - @note This routine will not be called concurrently with itself - or other methods. - @see import - */ + /** + * Visit every object in the database + * This is usually called during import. + * + * @note This routine will not be called concurrently with itself + * or other methods. + * @see import + */ virtual void forEach(std::function)> f) = 0; diff --git a/include/xrpl/nodestore/DatabaseRotating.h b/include/xrpl/nodestore/DatabaseRotating.h index 69eb31261d..5381b5c435 100644 --- a/include/xrpl/nodestore/DatabaseRotating.h +++ b/include/xrpl/nodestore/DatabaseRotating.h @@ -28,18 +28,32 @@ public: { } - /** Rotates the backends. - - @param newBackend New writable backend - @param f A function executed after the rotation outside of lock. The - values passed to f will be the new backend database names _after_ - rotation. - */ + /** + * Rotates the backends. + * + * @param newBackend New writable backend + * @param f A function executed after the rotation outside of lock. The + * values passed to f will be the new backend database names _after_ + * rotation. + */ virtual void rotate( std::unique_ptr&& newBackend, std::function const& f) = 0; + + /** + * Marks an online-delete rotation as in progress (or completed). + * + * While in flight, a read served by the archive backend is copied + * forward into the writable backend even for ordinary + * (duplicate == false) fetches: the archive is about to be deleted, + * and a node body canonicalized into caches during the rotation + * window would otherwise survive only in RAM once the archive is + * dropped. + */ + virtual void + setRotationInFlight(bool inFlight) = 0; }; } // namespace xrpl::NodeStore diff --git a/include/xrpl/nodestore/DummyScheduler.h b/include/xrpl/nodestore/DummyScheduler.h index f626115786..49b0d37462 100644 --- a/include/xrpl/nodestore/DummyScheduler.h +++ b/include/xrpl/nodestore/DummyScheduler.h @@ -5,7 +5,9 @@ namespace xrpl::NodeStore { -/** Simple NodeStore Scheduler that just performs the tasks synchronously. */ +/** + * Simple NodeStore Scheduler that just performs the tasks synchronously. + */ class DummyScheduler : public Scheduler { public: diff --git a/include/xrpl/nodestore/Factory.h b/include/xrpl/nodestore/Factory.h index e79ae3e05d..a18023a8a8 100644 --- a/include/xrpl/nodestore/Factory.h +++ b/include/xrpl/nodestore/Factory.h @@ -16,24 +16,29 @@ class Section; namespace xrpl::NodeStore { -/** Base class for backend factories. */ +/** + * Base class for backend factories. + */ class Factory { public: virtual ~Factory() = default; - /** Retrieve the name of this factory. */ + /** + * Retrieve the name of this factory. + */ [[nodiscard]] virtual std::string getName() const = 0; - /** Create an instance of this factory's backend. - - @param keyBytes The fixed number of bytes per key. - @param parameters A set of key/value configuration pairs. - @param burstSize Backend burst size in bytes. - @param scheduler The scheduler to use for running tasks. - @return A pointer to the Backend object. - */ + /** + * Create an instance of this factory's backend. + * + * @param keyBytes The fixed number of bytes per key. + * @param parameters A set of key/value configuration pairs. + * @param burstSize Backend burst size in bytes. + * @param scheduler The scheduler to use for running tasks. + * @return A pointer to the Backend object. + */ virtual std::unique_ptr createInstance( size_t keyBytes, @@ -42,15 +47,16 @@ public: Scheduler& scheduler, beast::Journal journal) = 0; - /** Create an instance of this factory's backend. - - @param keyBytes The fixed number of bytes per key. - @param parameters A set of key/value configuration pairs. - @param burstSize Backend burst size in bytes. - @param scheduler The scheduler to use for running tasks. - @param context The context used by database. - @return A pointer to the Backend object. - */ + /** + * Create an instance of this factory's backend. + * + * @param keyBytes The fixed number of bytes per key. + * @param parameters A set of key/value configuration pairs. + * @param burstSize Backend burst size in bytes. + * @param scheduler The scheduler to use for running tasks. + * @param context The context used by database. + * @return A pointer to the Backend object. + */ virtual std::unique_ptr createInstance( size_t keyBytes, diff --git a/include/xrpl/nodestore/Manager.h b/include/xrpl/nodestore/Manager.h index f813412846..54d99fe94b 100644 --- a/include/xrpl/nodestore/Manager.h +++ b/include/xrpl/nodestore/Manager.h @@ -12,7 +12,9 @@ namespace xrpl::NodeStore { -/** Singleton for managing NodeStore factories and back ends. */ +/** + * Singleton for managing NodeStore factories and back ends. + */ class Manager { public: @@ -22,26 +24,35 @@ public: Manager& operator=(Manager const&) = delete; - /** Returns the instance of the manager singleton. */ + /** + * Returns the instance of the manager singleton. + */ static Manager& instance(); - /** Add a factory. */ + /** + * Add a factory. + */ virtual void insert(Factory& factory) = 0; - /** Remove a factory. */ + /** + * Remove a factory. + */ virtual void erase(Factory& factory) = 0; - /** Return a pointer to the matching factory if it exists. - @param name The name to match, performed case-insensitive. - @return `nullptr` if a match was not found. - */ + /** + * Return a pointer to the matching factory if it exists. + * @param name The name to match, performed case-insensitive. + * @return `nullptr` if a match was not found. + */ virtual Factory* find(std::string const& name) = 0; - /** Create a backend. */ + /** + * Create a backend. + */ virtual std::unique_ptr makeBackend( Section const& parameters, @@ -49,34 +60,35 @@ public: Scheduler& scheduler, beast::Journal journal) = 0; - /** Construct a NodeStore database. - - The parameters are key value pairs passed to the backend. The - 'type' key must exist, it defines the choice of backend. Most - backends also require a 'path' field. - - Some choices for 'type' are: - HyperLevelDB, LevelDBFactory, SQLite, MDB - - If the fastBackendParameter is omitted or empty, no ephemeral database - is used. If the scheduler parameter is omitted or unspecified, a - synchronous scheduler is used which performs all tasks immediately on - the caller's thread. - - @note If the database cannot be opened or created, an exception is - thrown. - - @param name A diagnostic label for the database. - @param burstSize Backend burst size in bytes. - @param scheduler The scheduler to use for performing asynchronous tasks. - @param readThreads The number of async read threads to create - @param backendParameters The parameter string for the persistent - backend. - @param fastBackendParameters [optional] The parameter string for the - ephemeral backend. - - @return The opened database. - */ + /** + * Construct a NodeStore database. + * + * The parameters are key value pairs passed to the backend. The + * 'type' key must exist, it defines the choice of backend. Most + * backends also require a 'path' field. + * + * Some choices for 'type' are: + * HyperLevelDB, LevelDBFactory, SQLite, MDB + * + * If the fastBackendParameter is omitted or empty, no ephemeral database + * is used. If the scheduler parameter is omitted or unspecified, a + * synchronous scheduler is used which performs all tasks immediately on + * the caller's thread. + * + * @note If the database cannot be opened or created, an exception is + * thrown. + * + * @param name A diagnostic label for the database. + * @param burstSize Backend burst size in bytes. + * @param scheduler The scheduler to use for performing asynchronous tasks. + * @param readThreads The number of async read threads to create + * @param backendParameters The parameter string for the persistent + * backend. + * @param fastBackendParameters [optional] The parameter string for the + * ephemeral backend. + * + * @return The opened database. + */ virtual std::unique_ptr makeDatabase( std::size_t burstSize, diff --git a/include/xrpl/nodestore/NodeObject.h b/include/xrpl/nodestore/NodeObject.h index 3f3b75d5f8..b96d65fa12 100644 --- a/include/xrpl/nodestore/NodeObject.h +++ b/include/xrpl/nodestore/NodeObject.h @@ -12,7 +12,9 @@ namespace xrpl { -/** The types of node objects. */ +/** + * The types of node objects. + */ enum class NodeObjectType : std::uint32_t { Unknown = 0, Ledger = 1, @@ -21,15 +23,16 @@ enum class NodeObjectType : std::uint32_t { Dummy = 512 // an invalid or missing object }; -/** A simple object that the Ledger uses to store entries. - NodeObjects are comprised of a type, a hash, and a blob. - They can be uniquely identified by the hash, which is a half-SHA512 of - the blob. The blob is a variable length block of serialized data. The - type identifies what the blob contains. - - @note No checking is performed to make sure the hash matches the data. - @see SHAMap -*/ +/** + * A simple object that the Ledger uses to store entries. + * NodeObjects are comprised of a type, a hash, and a blob. + * They can be uniquely identified by the hash, which is a half-SHA512 of + * the blob. The blob is a variable length block of serialized data. The + * type identifies what the blob contains. + * + * @note No checking is performed to make sure the hash matches the data. + * @see SHAMap + */ class NodeObject : public CountedObject { public: @@ -48,29 +51,36 @@ public: // This constructor is private, use createObject instead. NodeObject(NodeObjectType type, Blob&& data, uint256 const& hash, PrivateAccess); - /** Create an object from fields. - - The caller's variable is modified during this call. The - underlying storage for the Blob is taken over by the NodeObject. - - @param type The type of object. - @param ledgerIndex The ledger in which this object appears. - @param data A buffer containing the payload. The caller's variable - is overwritten. - @param hash The 256-bit hash of the payload data. - */ + /** + * Create an object from fields. + * + * The caller's variable is modified during this call. The + * underlying storage for the Blob is taken over by the NodeObject. + * + * @param type The type of object. + * @param ledgerIndex The ledger in which this object appears. + * @param data A buffer containing the payload. The caller's variable + * is overwritten. + * @param hash The 256-bit hash of the payload data. + */ static std::shared_ptr createObject(NodeObjectType type, Blob&& data, uint256 const& hash); - /** Returns the type of this object. */ + /** + * Returns the type of this object. + */ [[nodiscard]] NodeObjectType getType() const; - /** Returns the hash of the data. */ + /** + * Returns the hash of the data. + */ [[nodiscard]] uint256 const& getHash() const; - /** Returns the underlying data. */ + /** + * Returns the underlying data. + */ [[nodiscard]] Blob const& getData() const; diff --git a/include/xrpl/nodestore/Scheduler.h b/include/xrpl/nodestore/Scheduler.h index 588ff19bdc..5d93a80eaa 100644 --- a/include/xrpl/nodestore/Scheduler.h +++ b/include/xrpl/nodestore/Scheduler.h @@ -8,7 +8,9 @@ namespace xrpl::NodeStore { enum class FetchType { Synchronous, Async }; -/** Contains information about a fetch operation. */ +/** + * Contains information about a fetch operation. + */ struct FetchReport { explicit FetchReport(FetchType fetchType) : fetchType(fetchType) @@ -20,7 +22,9 @@ struct FetchReport bool wasFound = false; }; -/** Contains information about a batch write operation. */ +/** + * Contains information about a batch write operation. + */ struct BatchWriteReport { explicit BatchWriteReport() = default; @@ -29,36 +33,40 @@ struct BatchWriteReport int writeCount; }; -/** Scheduling for asynchronous backend activity - - For improved performance, a backend has the option of performing writes - in batches. These writes can be scheduled using the provided scheduler - object. - - @see BatchWriter -*/ +/** + * Scheduling for asynchronous backend activity + * + * For improved performance, a backend has the option of performing writes + * in batches. These writes can be scheduled using the provided scheduler + * object. + * + * @see BatchWriter + */ class Scheduler { public: virtual ~Scheduler() = default; - /** Schedules a task. - Depending on the implementation, the task may be invoked either on - the current thread of execution, or an unspecified - implementation-defined foreign thread. - */ + /** + * Schedules a task. + * Depending on the implementation, the task may be invoked either on + * the current thread of execution, or an unspecified + * implementation-defined foreign thread. + */ virtual void scheduleTask(Task& task) = 0; - /** Reports completion of a fetch - Allows the scheduler to monitor the node store's performance - */ + /** + * Reports completion of a fetch + * Allows the scheduler to monitor the node store's performance + */ virtual void onFetch(FetchReport const& report) = 0; - /** Reports the completion of a batch write - Allows the scheduler to monitor the node store's performance - */ + /** + * Reports the completion of a batch write + * Allows the scheduler to monitor the node store's performance + */ virtual void onBatchWrite(BatchWriteReport const& report) = 0; }; diff --git a/include/xrpl/nodestore/Task.h b/include/xrpl/nodestore/Task.h index 0695970a68..59fe648476 100644 --- a/include/xrpl/nodestore/Task.h +++ b/include/xrpl/nodestore/Task.h @@ -2,14 +2,17 @@ namespace xrpl::NodeStore { -/** Derived classes perform scheduled tasks. */ +/** + * Derived classes perform scheduled tasks. + */ struct Task { virtual ~Task() = default; - /** Performs the task. - The call may take place on a foreign thread. - */ + /** + * Performs the task. + * The call may take place on a foreign thread. + */ virtual void performScheduledTask() = 0; }; diff --git a/include/xrpl/nodestore/Types.h b/include/xrpl/nodestore/Types.h index eaee82c99e..872d948a36 100644 --- a/include/xrpl/nodestore/Types.h +++ b/include/xrpl/nodestore/Types.h @@ -18,7 +18,9 @@ static constexpr auto kBatchWritePreallocationSize = 256; // static constexpr auto kBatchWriteLimitSize = 65536; -/** Return codes from Backend operations. */ +/** + * Return codes from Backend operations. + */ enum class Status { Ok = 0, NotFound = 1, @@ -29,7 +31,9 @@ enum class Status { CustomCode = 100 }; -/** A batch of NodeObjects to write at once. */ +/** + * A batch of NodeObjects to write at once. + */ using Batch = std::vector>; } // namespace xrpl::NodeStore diff --git a/include/xrpl/nodestore/detail/BatchWriter.h b/include/xrpl/nodestore/detail/BatchWriter.h index 7fa23bcb3e..b89df0da14 100644 --- a/include/xrpl/nodestore/detail/BatchWriter.h +++ b/include/xrpl/nodestore/detail/BatchWriter.h @@ -11,18 +11,21 @@ namespace xrpl::NodeStore { -/** Batch-writing assist logic. - - The batch writes are performed with a scheduled task. Use of the - class it not required. A backend can implement its own write batching, - or skip write batching if doing so yields a performance benefit. - - @see Scheduler -*/ +/** + * Batch-writing assist logic. + * + * The batch writes are performed with a scheduled task. Use of the + * class it not required. A backend can implement its own write batching, + * or skip write batching if doing so yields a performance benefit. + * + * @see Scheduler + */ class BatchWriter : private Task { public: - /** This callback does the actual writing. */ + /** + * This callback does the actual writing. + */ struct Callback { virtual ~Callback() = default; @@ -35,24 +38,30 @@ public: writeBatch(Batch const& batch) = 0; }; - /** Create a batch writer. */ + /** + * Create a batch writer. + */ BatchWriter(Callback& callback, Scheduler& scheduler); - /** Destroy a batch writer. - - Anything pending in the batch is written out before this returns. - */ + /** + * Destroy a batch writer. + * + * Anything pending in the batch is written out before this returns. + */ ~BatchWriter() override; - /** Store the object. - - This will add to the batch and initiate a scheduled task to - write the batch out. - */ + /** + * Store the object. + * + * This will add to the batch and initiate a scheduled task to + * write the batch out. + */ void store(std::shared_ptr const& object); - /** Get an estimate of the amount of writing I/O pending. */ + /** + * Get an estimate of the amount of writing I/O pending. + */ int getWriteLoad(); diff --git a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h index 6343275c76..ecbe9a513d 100644 --- a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h +++ b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -69,11 +70,22 @@ public: void sweep() override; + void + setRotationInFlight(bool inFlight) override; + private: std::shared_ptr writableBackend_; std::shared_ptr archiveBackend_; mutable std::mutex mutex_; + // True between SHAMapStore starting the cache-freshen phase and the + // completion of rotate(). While true, archive hits on ordinary + // (duplicate == false) fetches are copied forward into the writable + // backend; copyForwardCount_ tallies them per rotation for the + // summary line logged at swap. + std::atomic rotationInFlight_{false}; + std::atomic copyForwardCount_{0}; + std::shared_ptr fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate) override; diff --git a/include/xrpl/nodestore/detail/DecodedBlob.h b/include/xrpl/nodestore/detail/DecodedBlob.h index b2d5fc9c26..d0cc5e3404 100644 --- a/include/xrpl/nodestore/detail/DecodedBlob.h +++ b/include/xrpl/nodestore/detail/DecodedBlob.h @@ -6,39 +6,46 @@ namespace xrpl::NodeStore { -/** Parsed key/value blob into NodeObject components. - - This will extract the information required to construct a NodeObject. It - also does consistency checking and returns the result, so it is possible - to determine if the data is corrupted without throwing an exception. Not - all forms of corruption are detected so further analysis will be needed - to eliminate false negatives. - - @note This defines the database format of a NodeObject! -*/ +/** + * Parsed key/value blob into NodeObject components. + * + * This will extract the information required to construct a NodeObject. It + * also does consistency checking and returns the result, so it is possible + * to determine if the data is corrupted without throwing an exception. Not + * all forms of corruption are detected so further analysis will be needed + * to eliminate false negatives. + * + * @note This defines the database format of a NodeObject! + */ class DecodedBlob { public: - /** Construct the decoded blob from raw data. */ + /** + * Construct the decoded blob from raw data. + */ DecodedBlob(void const* key, void const* value, int valueBytes); - /** Determine if the decoding was successful. */ + /** + * Determine if the decoding was successful. + */ [[nodiscard]] bool wasOk() const noexcept { return success_; } - /** Create a NodeObject from this data. */ + /** + * Create a NodeObject from this data. + */ std::shared_ptr createObject(); private: - bool success_; + bool success_{false}; void const* key_; - NodeObjectType objectType_; - unsigned char const* objectData_; + NodeObjectType objectType_{NodeObjectType::Unknown}; + unsigned char const* objectData_{nullptr}; int dataBytes_; }; diff --git a/include/xrpl/nodestore/detail/EncodedBlob.h b/include/xrpl/nodestore/detail/EncodedBlob.h index 3982ab1b95..d668cdccd8 100644 --- a/include/xrpl/nodestore/detail/EncodedBlob.h +++ b/include/xrpl/nodestore/detail/EncodedBlob.h @@ -14,47 +14,54 @@ namespace xrpl::NodeStore { -/** Convert a NodeObject from in-memory to database format. - - The (suboptimal) database format consists of: - - - 8 prefix bytes which will typically be 0, but don't assume that's the - case; earlier versions of the code would use these bytes to store the - ledger index either once or twice. - - A single byte denoting the type of the object. - - The payload. - - @note This class is typically instantiated on the stack, so the size of - the object does not matter as much as it normally would since the - allocation is, effectively, free. - - We leverage that fact to preallocate enough memory to handle most - payloads as part of this object, eliminating the need for dynamic - allocation. As of this writing ~94% of objects require fewer than - 1024 payload bytes. +/** + * Convert a NodeObject from in-memory to database format. + * + * The (suboptimal) database format consists of: + * + * - 8 prefix bytes which will typically be 0, but don't assume that's the + * case; earlier versions of the code would use these bytes to store the + * ledger index either once or twice. + * - A single byte denoting the type of the object. + * - The payload. + * + * @note This class is typically instantiated on the stack, so the size of + * the object does not matter as much as it normally would since the + * allocation is, effectively, free. + * + * We leverage that fact to preallocate enough memory to handle most + * payloads as part of this object, eliminating the need for dynamic + * allocation. As of this writing ~94% of objects require fewer than + * 1024 payload bytes. */ class EncodedBlob { - /** The 32-byte key of the serialized object. */ + /** + * The 32-byte key of the serialized object. + */ std::array key_{}; - /** A pre-allocated buffer for the serialized object. - - The buffer is large enough for the 9 byte prefix and at least - 1024 more bytes. The precise size is calculated automatically - at compile time so as to avoid wasting space on padding bytes. + /** + * A pre-allocated buffer for the serialized object. + * + * The buffer is large enough for the 9 byte prefix and at least + * 1024 more bytes. The precise size is calculated automatically + * at compile time so as to avoid wasting space on padding bytes. */ std::array payload_{}; - /** The size of the serialized data. */ + /** + * The size of the serialized data. + */ std::uint32_t size_; - /** A pointer to the serialized data. - - This may point to the pre-allocated buffer (if it is sufficiently - large) or to a dynamically allocated buffer. + /** + * A pointer to the serialized data. + * + * This may point to the pre-allocated buffer (if it is sufficiently + * large) or to a dynamically allocated buffer. */ std::uint8_t* const ptr_; diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index c4fccd029a..a3666c7960 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -33,17 +33,20 @@ class STObject; class STAmount; class Rules; -/** Calculate Liquidity Provider Token (LPT) Currency. +/** + * Calculate Liquidity Provider Token (LPT) Currency. */ Currency ammLPTCurrency(Asset const& asset1, Asset const& asset2); -/** Calculate LPT Issue from AMM asset pair. +/** + * Calculate LPT Issue from AMM asset pair. */ Issue ammLPTIssue(Asset const& asset1, Asset const& asset2, AccountID const& ammAccountID); -/** Validate the amount. +/** + * Validate the amount. * If validZero is false and amount is beast::zero then invalid amount. * Return error code if invalid amount. * If pair then validate amount's issue matches one of the pair's issue. @@ -65,17 +68,20 @@ invalidAMMAssetPair( Asset const& asset2, std::optional> const& pair = std::nullopt); -/** Get time slot of the auction slot. +/** + * Get time slot of the auction slot. */ std::optional ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot); -/** Return true if required AMM amendment is enabled +/** + * Return true if required AMM amendment is enabled */ bool ammEnabled(Rules const&); -/** Convert to the fee from the basis points +/** + * Convert to the fee from the basis points * @param tfee trading fee in {0, 1000} * 1 = 1/10bps or 0.001%, 1000 = 1% */ @@ -85,7 +91,8 @@ getFee(std::uint16_t tfee) return Number{tfee} / kAuctionSlotFeeScaleFactor; } -/** Get fee multiplier (1 - tfee) +/** + * Get fee multiplier (1 - tfee) * @tfee trading fee in basis points */ inline Number @@ -94,7 +101,8 @@ feeMult(std::uint16_t tfee) return 1 - getFee(tfee); } -/** Get fee multiplier (1 - tfee / 2) +/** + * Get fee multiplier (1 - tfee / 2) * @tfee trading fee in basis points */ inline Number diff --git a/include/xrpl/protocol/AccountID.h b/include/xrpl/protocol/AccountID.h index a7d49246ca..ab3c5a996b 100644 --- a/include/xrpl/protocol/AccountID.h +++ b/include/xrpl/protocol/AccountID.h @@ -28,43 +28,53 @@ public: } // namespace detail -/** A 160-bit unsigned that uniquely identifies an account. */ +/** + * A 160-bit unsigned that uniquely identifies an account. + */ using AccountID = BaseUInt<160, detail::AccountIDTag>; -/** Convert AccountID to base58 checked string */ +/** + * Convert AccountID to base58 checked string + */ std::string toBase58(AccountID const& v); -/** Parse AccountID from checked, base58 string. - @return std::nullopt if a parse error occurs -*/ +/** + * Parse AccountID from checked, base58 string. + * @return std::nullopt if a parse error occurs + */ template <> std::optional parseBase58(std::string const& s); -/** Compute AccountID from public key. - - The account ID is computed as the 160-bit hash of the - public key data. This excludes the version byte and - guard bytes included in the base58 representation. - -*/ +/** + * Compute AccountID from public key. + * + * The account ID is computed as the 160-bit hash of the + * public key data. This excludes the version byte and + * guard bytes included in the base58 representation. + */ // VFALCO In PublicKey.h for now // AccountID // calcAccountID (PublicKey const& pk); -/** A special account that's used as the "issuer" for XRP. */ +/** + * A special account that's used as the "issuer" for XRP. + */ AccountID const& xrpAccount(); -/** A placeholder for empty accounts. */ +/** + * A placeholder for empty accounts. + */ AccountID const& noAccount(); -/** Convert hex or base58 string to AccountID. - - @return `true` if the parsing was successful. -*/ +/** + * Convert hex or base58 string to AccountID. + * + * @return `true` if the parsing was successful. + */ // DEPRECATED bool toIssuer(AccountID&, std::string const&); @@ -91,17 +101,18 @@ operator<<(std::ostream& os, AccountID const& x) return os; } -/** Initialize the global cache used to map AccountID to base58 conversions. - - The cache is optional and need not be initialized. But because conversion - is expensive (it requires a SHA-256 operation) in most cases the overhead - of the cache is worth the benefit. - - @param count The number of entries the cache should accommodate. Zero will - disable the cache, releasing any memory associated with it. - - @note The function will only initialize the cache the first time it is - invoked. Subsequent invocations do nothing. +/** + * Initialize the global cache used to map AccountID to base58 conversions. + * + * The cache is optional and need not be initialized. But because conversion + * is expensive (it requires a SHA-256 operation) in most cases the overhead + * of the cache is worth the benefit. + * + * @param count The number of entries the cache should accommodate. Zero will + * disable the cache, releasing any memory associated with it. + * + * @note The function will only initialize the cache the first time it is + * invoked. Subsequent invocations do nothing. */ void initAccountIdCache(std::size_t count); diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 2bf24b19fe..8e9c09eb89 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -62,7 +62,8 @@ private: public: Asset() = default; - /** Conversions to Asset are implicit and conversions to specific issue + /** + * Conversions to Asset are implicit and conversions to specific issue * type are explicit. This design facilitates the use of Asset. */ Asset(Issue const& issue) : issue_(issue) @@ -149,7 +150,8 @@ public: friend constexpr bool operator==(BadAsset const& lhs, Asset const& rhs); - /** Return true if both assets refer to the same currency (regardless of + /** + * Return true if both assets refer to the same currency (regardless of * issuer) or MPT issuance. Otherwise return false. */ friend constexpr bool diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index 476bdba35a..a83eb41b24 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -19,10 +19,11 @@ namespace xrpl { -/** Specifies an order book. - The order book is a pair of Issues called in and out. - @see Issue. -*/ +/** + * Specifies an order book. + * The order book is a pair of Issues called in and out. + * @see Issue. + */ class Book final : public CountedObject { public: @@ -60,7 +61,9 @@ hash_append(Hasher& h, Book const& b) Book reversed(Book const& book); -/** Equality comparison. */ +/** + * Equality comparison. + */ /** @{ */ [[nodiscard]] constexpr bool operator==(Book const& lhs, Book const& rhs) @@ -69,14 +72,16 @@ operator==(Book const& lhs, Book const& rhs) } /** @} */ -/** Strict weak ordering. */ +/** + * Strict weak ordering. + */ /** @{ */ [[nodiscard]] constexpr std::weak_ordering operator<=>(Book const& lhs, Book const& rhs) { - if (auto const c{lhs.in <=> rhs.in}; c != 0) + if (auto const c{lhs.in <=> rhs.in}; c != 0) // NOLINT(modernize-use-nullptr) return c; - if (auto const c{lhs.out <=> rhs.out}; c != 0) + if (auto const c{lhs.out <=> rhs.out}; c != 0) // NOLINT(modernize-use-nullptr) return c; // Manually compare optionals diff --git a/include/xrpl/protocol/BuildInfo.h b/include/xrpl/protocol/BuildInfo.h index a60c37e714..18ba20f23c 100644 --- a/include/xrpl/protocol/BuildInfo.h +++ b/include/xrpl/protocol/BuildInfo.h @@ -4,74 +4,83 @@ #include #include -/** Versioning information for this build. */ +/** + * Versioning information for this build. + */ // VFALCO The namespace is deprecated namespace xrpl::BuildInfo { -/** Server version. - Follows the Semantic Versioning Specification: - http://semver.org/ -*/ +/** + * Server version. + * Follows the Semantic Versioning Specification: + * http://semver.org/ + */ std::string const& getVersionString(); -/** Full server version string. - This includes the name of the server. It is used in the peer - protocol hello message and also the headers of some HTTP replies. -*/ +/** + * Full server version string. + * This includes the name of the server. It is used in the peer + * protocol hello message and also the headers of some HTTP replies. + */ std::string const& getFullVersionString(); -/** Encode an arbitrary server software version in a 64-bit integer. - - The general format is: - - ........-........-........-........-........-........-........-........ - XXXXXXXX-XXXXXXXX-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY - - X: 16 bits identifying the particular implementation - Y: 48 bits of data specific to the implementation - - The xrpld-specific format (implementation ID is: 0x18 0x3B) is: - - 00011000-00111011-MMMMMMMM-mmmmmmmm-pppppppp-TTNNNNNN-00000000-00000000 - - M: 8-bit major version (0-255) - m: 8-bit minor version (0-255) - p: 8-bit patch version (0-255) - T: 11 if neither an RC nor a beta - 10 if an RC - 01 if a beta - N: 6-bit rc/beta number (1-63) - - @param the version string - @return the encoded version in a 64-bit integer -*/ +/** + * Encode an arbitrary server software version in a 64-bit integer. + * + * The general format is: + * + * ........-........-........-........-........-........-........-........ + * XXXXXXXX-XXXXXXXX-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY + * + * X: 16 bits identifying the particular implementation + * Y: 48 bits of data specific to the implementation + * + * The xrpld-specific format (implementation ID is: 0x18 0x3B) is: + * + * 00011000-00111011-MMMMMMMM-mmmmmmmm-pppppppp-TTNNNNNN-00000000-00000000 + * + * M: 8-bit major version (0-255) + * m: 8-bit minor version (0-255) + * p: 8-bit patch version (0-255) + * T: 11 if neither an RC nor a beta + * 10 if an RC + * 01 if a beta + * N: 6-bit rc/beta number (1-63) + * + * @param the version string + * @return the encoded version in a 64-bit integer + */ std::uint64_t encodeSoftwareVersion(std::string_view versionStr); -/** Returns this server's version packed in a 64-bit integer. */ +/** + * Returns this server's version packed in a 64-bit integer. + */ std::uint64_t getEncodedVersion(); -/** Check if the encoded software version is an xrpld software version. - - @param version another node's encoded software version - @return true if the version is an xrpld software version, false otherwise -*/ +/** + * Check if the encoded software version is an xrpld software version. + * + * @param version another node's encoded software version + * @return true if the version is an xrpld software version, false otherwise + */ bool isXrpldVersion(std::uint64_t version); -/** Check if the version is newer than the local node's xrpld software - version. - - @param version another node's encoded software version - @return true if the version is newer than the local node's xrpld software - version, false otherwise. - - @note This function only understands version numbers that are generated by - xrpld. Please see the encodeSoftwareVersion() function for detail. -*/ +/** + * Check if the version is newer than the local node's xrpld software + * version. + * + * @param version another node's encoded software version + * @return true if the version is newer than the local node's xrpld software + * version, false otherwise. + * + * @note This function only understands version numbers that are generated by + * xrpld. Please see the encodeSoftwareVersion() function for detail. + */ bool isNewerVersion(std::uint64_t version); diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 325117eed4..ecf7970aba 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -28,7 +28,9 @@ namespace xrpl { */ struct ConfidentialRecipient { - /** @brief The recipient's ElGamal public key (size=xrpl::kEcPubKeyLength). */ + /** + * @brief The recipient's ElGamal public key (size=xrpl::kEcPubKeyLength). + */ Slice publicKey; /** @@ -44,10 +46,14 @@ struct ConfidentialRecipient */ struct EcPair { - /** @brief First ElGamal ciphertext component. */ + /** + * @brief First ElGamal ciphertext component. + */ secp256k1_pubkey c1; - /** @brief Second ElGamal ciphertext component. */ + /** + * @brief Second ElGamal ciphertext component. + */ secp256k1_pubkey c2; }; diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index 38b8bc6d76..8ac7c8c58f 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -147,10 +147,11 @@ enum ErrorCodeI { RpcLast = RpcUnexpectedLedgerType // rpcLAST should always equal the last code. }; -/** Codes returned in the `warnings` array of certain RPC commands. - - These values need to remain stable. -*/ +/** + * Codes returned in the `warnings` array of certain RPC commands. + * + * These values need to remain stable. + */ // Protocol-wide, 50+ files // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum WarningCodeI { @@ -168,7 +169,9 @@ enum WarningCodeI { namespace RPC { -/** Maps an rpc error code to its token, default message, and HTTP status. */ +/** + * Maps an rpc error code to its token, default message, and HTTP status. + */ struct ErrorInfo { // Default ctor needed to produce an empty std::array during constexpr eval. @@ -193,11 +196,15 @@ struct ErrorInfo int httpStatus; }; -/** Returns an ErrorInfo that reflects the error code. */ +/** + * Returns an ErrorInfo that reflects the error code. + */ ErrorInfo const& getErrorInfo(ErrorCodeI code); -/** Add or update the json update to reflect the error code. */ +/** + * Add or update the json update to reflect the error code. + */ /** @{ */ void injectError(ErrorCodeI code, json::Value& json); @@ -206,7 +213,9 @@ void injectError(ErrorCodeI code, std::string const& message, json::Value& json); /** @} */ -/** Returns a new json object that reflects the error code. */ +/** + * Returns a new json object that reflects the error code. + */ /** @{ */ json::Value makeError(ErrorCodeI code); @@ -214,7 +223,9 @@ json::Value makeError(ErrorCodeI code, std::string const& message); /** @} */ -/** Returns a new json object that indicates invalid parameters. */ +/** + * Returns a new json object that indicates invalid parameters. + */ /** @{ */ inline json::Value makeParamError(std::string const& message) @@ -314,17 +325,23 @@ notValidatorError() /** @} */ -/** Returns `true` if the json contains an rpc error specification. */ +/** + * Returns `true` if the json contains an rpc error specification. + */ bool containsError(json::Value const& json); -/** Returns http status that corresponds to the error code. */ +/** + * Returns http status that corresponds to the error code. + */ int errorCodeHttpStatus(ErrorCodeI code); } // namespace RPC -/** Returns a single string with the contents of an RPC error. */ +/** + * Returns a single string with the contents of an RPC error. + */ std::string rpcErrorString(json::Value const& jv); diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 927fde542a..f15b7e2d3f 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -112,7 +112,9 @@ validFeatureName(auto fn) -> bool enum class VoteBehavior : int { Obsolete = -1, DefaultNo = 0, DefaultYes = 1 }; enum class AmendmentSupport : int { Retired = -1, Supported = 0, Unsupported = 1 }; -/** All amendments libxrpl knows about. */ +/** + * All amendments libxrpl knows about. + */ std::map const& allAmendments(); @@ -152,23 +154,27 @@ static constexpr std::size_t kNumFeatures = #undef XRPL_FEATURE #pragma pop_macro("XRPL_FEATURE") -/** Amendments that this server supports and the default voting behavior. - Whether they are enabled depends on the Rules defined in the validated - ledger */ +/** + * Amendments that this server supports and the default voting behavior. + * Whether they are enabled depends on the Rules defined in the validated + * ledger + */ std::map const& supportedAmendments(); -/** Amendments that this server won't vote for by default. - - This function is only used in unit tests. -*/ +/** + * Amendments that this server won't vote for by default. + * + * This function is only used in unit tests. + */ std::size_t numDownVotedAmendments(); -/** Amendments that this server will vote for by default. - - This function is only used in unit tests. -*/ +/** + * Amendments that this server will vote for by default. + * + * This function is only used in unit tests. + */ std::size_t numUpVotedAmendments(); diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index 4e79204f4c..fdfadcd8fd 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -2,7 +2,6 @@ #include -#include #include namespace xrpl { @@ -11,20 +10,27 @@ namespace xrpl { // This was the reference fee units used in the old fee calculation. inline constexpr std::uint32_t kFeeUnitsDeprecated = 10; -/** Reflects the fee settings for a particular ledger. - - The fees are always the same for any transactions applied - to a ledger. Changes to fees occur in between ledgers. -*/ +/** + * Reflects the fee settings for a particular ledger. + * + * The fees are always the same for any transactions applied + * to a ledger. Changes to fees occur in between ledgers. + */ struct Fees { - /** @brief Cost of a reference transaction in drops. */ + /** + * @brief Cost of a reference transaction in drops. + */ XRPAmount base{0}; - /** @brief Minimum XRP an account must hold to exist on the ledger. */ + /** + * @brief Minimum XRP an account must hold to exist on the ledger. + */ XRPAmount reserve{0}; - /** @brief Additional XRP reserve required per owned ledger object. */ + /** + * @brief Additional XRP reserve required per owned ledger object. + */ XRPAmount increment{0}; explicit Fees() = default; @@ -37,15 +43,16 @@ struct Fees { } - /** Returns the account reserve given the owner count, in drops. - - The reserve is calculated as the reserve base plus - the reserve increment times the number of increments. - */ + /** + * Returns the account reserve given the owner count, in drops. + * + * The reserve is calculated as the reserve base times the number of accounts plus the reserve + * increment times the number of increments. + */ [[nodiscard]] XRPAmount - accountReserve(std::size_t ownerCount) const + accountReserve(std::uint32_t ownerCount, std::uint32_t accountCount) const { - return reserve + ownerCount * increment; + return (reserve * accountCount) + (increment * ownerCount); } }; diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index 1b05d450a1..9d4471d05c 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -17,55 +17,80 @@ makeHashPrefix(char a, char b, char c) } // namespace detail -/** Prefix for hashing functions. - - These prefixes are inserted before the source material used to generate - various hashes. This is done to put each hash in its own "space." This way, - two different types of objects with the same binary data will produce - different hashes. - - Each prefix is a 4-byte value with the last byte set to zero and the first - three bytes formed from the ASCII equivalent of some arbitrary string. For - example "TXN". - - @note Hash prefixes are part of the protocol; you cannot, arbitrarily, - change the type or the value of any of these without causing breakage. -*/ +/** + * Prefix for hashing functions. + * + * These prefixes are inserted before the source material used to generate + * various hashes. This is done to put each hash in its own "space." This way, + * two different types of objects with the same binary data will produce + * different hashes. + * + * Each prefix is a 4-byte value with the last byte set to zero and the first + * three bytes formed from the ASCII equivalent of some arbitrary string. For + * example "TXN". + * + * @note Hash prefixes are part of the protocol; you cannot, arbitrarily, + * change the type or the value of any of these without causing breakage. + */ enum class HashPrefix : std::uint32_t { - /** transaction plus signature to give transaction ID */ + /** + * transaction plus signature to give transaction ID + */ TransactionId = detail::makeHashPrefix('T', 'X', 'N'), - /** transaction plus metadata */ + /** + * transaction plus metadata + */ TxNode = detail::makeHashPrefix('S', 'N', 'D'), - /** account state */ + /** + * account state + */ LeafNode = detail::makeHashPrefix('M', 'L', 'N'), - /** inner node in V1 tree */ + /** + * inner node in V1 tree + */ InnerNode = detail::makeHashPrefix('M', 'I', 'N'), - /** ledger master data for signing */ + /** + * ledger master data for signing + */ LedgerMaster = detail::makeHashPrefix('L', 'W', 'R'), - /** inner transaction to sign */ + /** + * inner transaction to sign + */ TxSign = detail::makeHashPrefix('S', 'T', 'X'), - /** inner transaction to multi-sign */ + /** + * inner transaction to multi-sign + */ TxMultiSign = detail::makeHashPrefix('S', 'M', 'T'), - /** validation for signing */ + /** + * validation for signing + */ Validation = detail::makeHashPrefix('V', 'A', 'L'), - /** proposal for signing */ + /** + * proposal for signing + */ Proposal = detail::makeHashPrefix('P', 'R', 'P'), - /** Manifest */ + /** + * Manifest + */ Manifest = detail::makeHashPrefix('M', 'A', 'N'), - /** Payment Channel Claim */ + /** + * Payment Channel Claim + */ PaymentChannelClaim = detail::makeHashPrefix('C', 'L', 'M'), - /** Batch */ + /** + * Batch + */ Batch = detail::makeHashPrefix('B', 'C', 'H'), }; diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index 186ce054f1..060ad3d828 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -11,16 +11,17 @@ namespace xrpl { -/** Floating point representation of amounts with high dynamic range - - Amounts are stored as a normalized signed mantissa and an exponent. The - range of the normalized exponent is [-96,80] and the range of the absolute - value of the normalized mantissa is [1000000000000000, 9999999999999999]. - - Arithmetic operations can throw std::overflow_error during normalization - if the amount exceeds the largest representable amount, but underflows - will silently truncate to zero. -*/ +/** + * Floating point representation of amounts with high dynamic range + * + * Amounts are stored as a normalized signed mantissa and an exponent. The + * range of the normalized exponent is [-96,80] and the range of the absolute + * value of the normalized mantissa is [1000000000000000, 9999999999999999]. + * + * Arithmetic operations can throw std::overflow_error during normalization + * if the amount exceeds the largest representable amount, but underflows + * will silently truncate to zero. + */ class IOUAmount : private boost::totally_ordered, private boost::additive { private: @@ -29,12 +30,13 @@ private: mantissa_type mantissa_{}; exponent_type exponent_{}; - /** Adjusts the mantissa and exponent to the proper range. - - This can throw if the amount cannot be normalized, or is larger than - the largest value that can be represented as an IOU amount. Amounts - that are too small to be represented normalize to 0. - */ + /** + * Adjusts the mantissa and exponent to the proper range. + * + * This can throw if the amount cannot be normalized, or is larger than + * the largest value that can be represented as an IOU amount. Amounts + * that are too small to be represented normalize to 0. + */ void normalize(); @@ -66,11 +68,15 @@ public: bool operator<(IOUAmount const& other) const; - /** Returns true if the amount is not zero */ + /** + * Returns true if the amount is not zero + */ explicit operator bool() const noexcept; - /** Return the sign of the amount */ + /** + * Return the sign of the amount + */ [[nodiscard]] int signum() const noexcept; diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 053a66787f..07493da0bd 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -24,74 +23,90 @@ namespace xrpl { class SeqProxy; -/** Keylet computation functions. - - Entries in the ledger are located using 256-bit locators. The locators are - calculated using a wide range of parameters specific to the entry whose - locator we are calculating (e.g. an account's locator is derived from the - account's address, whereas the locator for an offer is derived from the - account and the offer sequence.) - - To enhance type safety during lookup and make the code more robust, we use - keylets, which contain not only the locator of the object but also the type - of the object being referenced. - - These functions each return a type-specific keylet. -*/ +/** + * Keylet computation functions. + * + * Entries in the ledger are located using 256-bit locators. The locators are + * calculated using a wide range of parameters specific to the entry whose + * locator we are calculating (e.g. an account's locator is derived from the + * account's address, whereas the locator for an offer is derived from the + * account and the offer sequence.) + * + * To enhance type safety during lookup and make the code more robust, we use + * keylets, which contain not only the locator of the object but also the type + * of the object being referenced. + * + * These functions each return a type-specific keylet. + */ namespace keylet { -/** AccountID root */ +/** + * AccountID root + */ Keylet account(AccountID const& id) noexcept; -/** The index of the amendment table */ +/** + * The index of the amendment table + */ Keylet const& amendments() noexcept; -/** Any item that can be in an owner dir. */ +/** + * Any item that can be in an owner dir. + */ Keylet child(uint256 const& key) noexcept; -/** The index of the "short" skip list - - The "short" skip list is a node (at a fixed index) that holds the hashes - of ledgers since the last flag ledger. It will contain, at most, 256 hashes. -*/ +/** + * The index of the "short" skip list + * + * The "short" skip list is a node (at a fixed index) that holds the hashes + * of ledgers since the last flag ledger. It will contain, at most, 256 hashes. + */ Keylet const& skip() noexcept; -/** The index of the long skip for a particular ledger range. - - The "long" skip list is a node that holds the hashes of (up to) 256 flag - ledgers. - - It can be used to efficiently skip back to any ledger using only two hops: - the first hop gets the "long" skip list for the ledger it wants to retrieve - and uses it to get the hash of the flag ledger whose short skip list will - contain the hash of the requested ledger. -*/ +/** + * The index of the long skip for a particular ledger range. + * + * The "long" skip list is a node that holds the hashes of (up to) 256 flag + * ledgers. + * + * It can be used to efficiently skip back to any ledger using only two hops: + * the first hop gets the "long" skip list for the ledger it wants to retrieve + * and uses it to get the hash of the flag ledger whose short skip list will + * contain the hash of the requested ledger. + */ Keylet skip(LedgerIndex ledger) noexcept; -/** The (fixed) index of the object containing the ledger fees. */ +/** + * The (fixed) index of the object containing the ledger fees. + */ Keylet const& feeSettings() noexcept; -/** The (fixed) index of the object containing the ledger negativeUNL. */ +/** + * The (fixed) index of the object containing the ledger negativeUNL. + */ Keylet const& negativeUNL() noexcept; -/** The beginning of an order book */ +/** + * The beginning of an order book + */ Keylet book(Book const& b); -/** The index of a trust line for a given currency - - Note that a trustline is *shared* between two accounts (commonly referred - to as the issuer and the holder); if Alice sets up a trust line to Bob for - BTC, and Bob trusts Alice for BTC, here is only a single BTC trust line - between them. -*/ +/** + * The index of a trust line for a given currency + * + * Note that a trustline is *shared* between two accounts (commonly referred + * to as the issuer and the holder); if Alice sets up a trust line to Bob for + * BTC, and Bob trusts Alice for BTC, here is only a single BTC trust line + * between them. + */ /** @{ */ Keylet trustLine(AccountID const& id0, AccountID const& id1, Currency const& currency) noexcept; @@ -103,7 +118,9 @@ trustLine(AccountID const& id, Issue const& issue) noexcept } /** @} */ -/** An offer from an account */ +/** + * An offer from an account + */ /** @{ */ Keylet offer(AccountID const& id, std::uint32_t seq) noexcept; @@ -115,15 +132,21 @@ offer(uint256 const& key) noexcept } /** @} */ -/** The initial directory page for a specific quality */ +/** + * The initial directory page for a specific quality + */ Keylet quality(Keylet const& k, std::uint64_t q) noexcept; -/** The directory for the next lower quality */ +/** + * The directory for the next lower quality + */ Keylet next(Keylet const& k); -/** A ticket belonging to an account */ +/** + * A ticket belonging to an account + */ /** @{ */ Keylet ticket(AccountID const& id, std::uint32_t ticketSeq); @@ -138,11 +161,21 @@ ticket(uint256 const& key) } /** @} */ -/** A SignerList */ +/** + * A SignerList + */ Keylet signerList(AccountID const& account) noexcept; -/** A Check */ +/** + * A Sponsorship + */ +Keylet +sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept; + +/** + * A Check + */ /** @{ */ Keylet check(AccountID const& id, std::uint32_t seq) noexcept; @@ -154,7 +187,9 @@ check(uint256 const& key) noexcept } /** @} */ -/** A DepositPreauth */ +/** + * A DepositPreauth + */ /** @{ */ Keylet depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept; @@ -173,15 +208,21 @@ depositPreauth(uint256 const& key) noexcept //------------------------------------------------------------------------------ -/** Any ledger entry */ +/** + * Any ledger entry + */ Keylet unchecked(uint256 const& key) noexcept; -/** The root page of an account's directory */ +/** + * The root page of an account's directory + */ Keylet ownerDir(AccountID const& id) noexcept; -/** A page in a directory */ +/** + * A page in a directory + */ /** @{ */ Keylet page(uint256 const& root, std::uint64_t index = 0) noexcept; @@ -194,27 +235,36 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept } /** @} */ -/** An escrow entry */ +/** + * An escrow entry + */ Keylet escrow(AccountID const& src, std::uint32_t seq) noexcept; -/** A PaymentChannel */ +/** + * A PaymentChannel + */ Keylet payChannel(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept; -/** NFT page keylets - - Unlike objects whose ledger identifiers are produced by hashing data, - NFT page identifiers are composite identifiers, consisting of the owner's - 160-bit AccountID, followed by a 96-bit value that determines which NFT - tokens are candidates for that page. +/** + * NFT page keylets + * + * Unlike objects whose ledger identifiers are produced by hashing data, + * NFT page identifiers are composite identifiers, consisting of the owner's + * 160-bit AccountID, followed by a 96-bit value that determines which NFT + * tokens are candidates for that page. */ /** @{ */ -/** A keylet for the owner's first possible NFT page. */ +/** + * A keylet for the owner's first possible NFT page. + */ Keylet nftokenPageMin(AccountID const& owner); -/** A keylet for the owner's last possible NFT page. */ +/** + * A keylet for the owner's last possible NFT page. + */ Keylet nftokenPageMax(AccountID const& owner); @@ -222,7 +272,9 @@ Keylet nftokenPage(Keylet const& k, uint256 const& token); /** @} */ -/** An offer from an account to buy or sell an NFT */ +/** + * An offer from an account to buy or sell an NFT + */ Keylet nftokenOffer(AccountID const& owner, std::uint32_t seq); @@ -232,22 +284,30 @@ nftokenOffer(uint256 const& offer) return {ltNFTOKEN_OFFER, offer}; } -/** The directory of buy offers for the specified NFT */ +/** + * The directory of buy offers for the specified NFT + */ Keylet nftBuys(uint256 const& id) noexcept; -/** The directory of sell offers for the specified NFT */ +/** + * The directory of sell offers for the specified NFT + */ Keylet nftSells(uint256 const& id) noexcept; -/** AMM entry */ +/** + * AMM entry + */ Keylet amm(Asset const& issue1, Asset const& issue2) noexcept; Keylet amm(uint256 const& amm) noexcept; -/** A keylet for Delegate object */ +/** + * A keylet for Delegate object + */ Keylet delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept; @@ -362,21 +422,8 @@ struct KeyletDesc bool includeInTests{}; }; -// This list should include all of the keylet functions that take a single -// AccountID parameter. -std::array, 6> const kDirectAccountKeylets{ - {{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false}, - {.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true}, - {.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true}, - // It's normally impossible to create an item at nftpage_min, but - // test it anyway, since the invariant checks for it. - {.function = &keylet::nftokenPageMin, - .expectedLEName = jss::NFTokenPage, - .includeInTests = true}, - {.function = &keylet::nftokenPageMax, - .expectedLEName = jss::NFTokenPage, - .includeInTests = true}, - {.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}}; +// This list should include all of the keylet functions that take a single AccountID parameter. +extern std::array, 6> const kDirectAccountKeylets; MPTID makeMptID(std::uint32_t sequence, AccountID const& account); diff --git a/include/xrpl/protocol/InnerObjectFormats.h b/include/xrpl/protocol/InnerObjectFormats.h index c8312c3701..7364e83cfd 100644 --- a/include/xrpl/protocol/InnerObjectFormats.h +++ b/include/xrpl/protocol/InnerObjectFormats.h @@ -6,14 +6,16 @@ namespace xrpl { -/** Manages the list of known inner object formats. +/** + * Manages the list of known inner object formats. */ class InnerObjectFormats : public KnownFormats { private: - /** Create the object. - This will load the object with all the known inner object formats. - */ + /** + * Create the object. + * This will load the object with all the known inner object formats. + */ InnerObjectFormats(); public: diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index 3d556e83eb..5cd8731609 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -10,9 +10,10 @@ namespace xrpl { -/** A currency issued by an account. - @see Currency, AccountID, Issue, Book -*/ +/** + * A currency issued by an account. + * @see Currency, AccountID, Issue, Book + */ class Issue { public: @@ -70,7 +71,9 @@ hash_append(Hasher& h, Issue const& r) hash_append(h, r.currency, r.account); } -/** Equality comparison. */ +/** + * Equality comparison. + */ /** @{ */ [[nodiscard]] constexpr bool operator==(Issue const& lhs, Issue const& rhs) @@ -79,12 +82,14 @@ operator==(Issue const& lhs, Issue const& rhs) } /** @} */ -/** Strict weak ordering. */ +/** + * Strict weak ordering. + */ /** @{ */ [[nodiscard]] constexpr std::weak_ordering operator<=>(Issue const& lhs, Issue const& rhs) { - if (auto const c{lhs.currency <=> rhs.currency}; c != 0) + if (auto const c{lhs.currency <=> rhs.currency}; c != 0) // NOLINT(modernize-use-nullptr) return c; if (isXRP(lhs.currency)) @@ -96,7 +101,9 @@ operator<=>(Issue const& lhs, Issue const& rhs) //------------------------------------------------------------------------------ -/** Returns an asset specifier that represents XRP. */ +/** + * Returns an asset specifier that represents XRP. + */ inline Issue const& xrpIssue() { @@ -104,7 +111,9 @@ xrpIssue() return kIssue; } -/** Returns an asset specifier that represents no account and currency. */ +/** + * Returns an asset specifier that represents no account and currency. + */ inline Issue const& noIssue() { diff --git a/include/xrpl/protocol/Keylet.h b/include/xrpl/protocol/Keylet.h index 19704e2a11..48d494f564 100644 --- a/include/xrpl/protocol/Keylet.h +++ b/include/xrpl/protocol/Keylet.h @@ -7,14 +7,15 @@ namespace xrpl { class STLedgerEntry; -/** A pair of SHAMap key and LedgerEntryType. - - A Keylet identifies both a key in the state map - and its ledger entry type. - - @note Keylet is a portmanteau of the words key - and LET, an acronym for LedgerEntryType. -*/ +/** + * A pair of SHAMap key and LedgerEntryType. + * + * A Keylet identifies both a key in the state map + * and its ledger entry type. + * + * @note Keylet is a portmanteau of the words key + * and LET, an acronym for LedgerEntryType. + */ struct Keylet { uint256 key; @@ -24,7 +25,9 @@ struct Keylet { } - /** Returns true if the SLE matches the type */ + /** + * Returns true if the SLE matches the type + */ [[nodiscard]] bool check(STLedgerEntry const&) const; }; diff --git a/include/xrpl/protocol/KnownFormats.h b/include/xrpl/protocol/KnownFormats.h index c31e28c37d..385feb2c27 100644 --- a/include/xrpl/protocol/KnownFormats.h +++ b/include/xrpl/protocol/KnownFormats.h @@ -15,18 +15,20 @@ namespace xrpl { -/** Manages a list of known formats. - - Each format has a name, an associated KeyType (typically an enumeration), - and a predefined @ref SOElement. - - @tparam KeyType The type of key identifying the format. -*/ +/** + * Manages a list of known formats. + * + * Each format has a name, an associated KeyType (typically an enumeration), + * and a predefined @ref SOElement. + * + * @tparam KeyType The type of key identifying the format. + */ template class KnownFormats { public: - /** A known format. + /** + * A known format. */ class Item { @@ -46,7 +48,8 @@ public: "KnownFormats KeyType must be integral or enum."); } - /** Retrieve the name of the format. + /** + * Retrieve the name of the format. */ [[nodiscard]] std::string const& getName() const @@ -54,7 +57,8 @@ public: return name_; } - /** Retrieve the transaction type this format represents. + /** + * Retrieve the transaction type this format represents. */ [[nodiscard]] KeyType getType() const @@ -74,32 +78,35 @@ public: KeyType const type_; }; - /** Create the known formats object. - - Derived classes will load the object with all the known formats. - */ + /** + * Create the known formats object. + * + * Derived classes will load the object with all the known formats. + */ private: KnownFormats() : name_(beast::typeName()) { } public: - /** Destroy the known formats object. - - The defined formats are deleted. - */ + /** + * Destroy the known formats object. + * + * The defined formats are deleted. + */ virtual ~KnownFormats() = default; KnownFormats(KnownFormats const&) = delete; KnownFormats& operator=(KnownFormats const&) = delete; - /** Retrieve the type for a format specified by name. - - If the format name is unknown, an exception is thrown. - - @param name The name of the type. - @return The type. - */ + /** + * Retrieve the type for a format specified by name. + * + * If the format name is unknown, an exception is thrown. + * + * @param name The name of the type. + * @return The type. + */ [[nodiscard]] KeyType findTypeByName(std::string const& name) const { @@ -110,7 +117,8 @@ public: name.substr(0, std::min(name.size(), std::size_t(32))) + "'"); } - /** Retrieve a format based on its type. + /** + * Retrieve a format based on its type. */ [[nodiscard]] Item const* findByType(KeyType type) const @@ -135,7 +143,8 @@ public: } protected: - /** Retrieve a format based on its name. + /** + * Retrieve a format based on its name. */ [[nodiscard]] Item const* findByName(std::string const& name) const @@ -146,15 +155,16 @@ protected: return itr->second; } - /** Add a new format. - - @param name The name of this format. - @param type The type of this format. - @param uniqueFields A std::vector of unique fields - @param commonFields A std::vector of common fields - - @return The created format. - */ + /** + * Add a new format. + * + * @param name The name of this format. + * @param type The type of this format. + * @param uniqueFields A std::vector of unique fields + * @param commonFields A std::vector of common fields + * + * @return The created format. + */ Item const& add(char const* name, KeyType type, diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 5b8a8cc2c5..7c504f6bdd 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -12,28 +12,29 @@ #include namespace xrpl { -/** Identifiers for on-ledger objects. - - Each ledger object requires a unique type identifier, which is stored within the object itself; - this makes it possible to iterate the entire ledger and determine each object's type and verify - that the object you retrieved from a given hash matches the expected type. - - @warning Since these values are stored inside objects stored on the ledger they are part of the - protocol. - **Changing them should be avoided because without special handling, this will result in a hard - fork.** - - @note Values outside this range may be used internally by the code for various purposes, but - attempting to use such values to identify on-ledger objects will result in an invariant failure. - - @note When retiring types, the specific values should not be removed but should be marked as - [[deprecated]]. This is to avoid accidental reuse of identifiers. - - @todo The C++ language does not enable checking for duplicate values here. - If it becomes possible then we should do this. - - @ingroup protocol -*/ +/** + * Identifiers for on-ledger objects. + * + * Each ledger object requires a unique type identifier, which is stored within the object itself; + * this makes it possible to iterate the entire ledger and determine each object's type and verify + * that the object you retrieved from a given hash matches the expected type. + * + * @warning Since these values are stored inside objects stored on the ledger they are part of the + * protocol. + * **Changing them should be avoided because without special handling, this will result in a hard + * fork.** + * + * @note Values outside this range may be used internally by the code for various purposes, but + * attempting to use such values to identify on-ledger objects will result in an invariant failure. + * + * @note When retiring types, the specific values should not be removed but should be marked as + * [[deprecated]]. This is to avoid accidental reuse of identifiers. + * + * @todo The C++ language does not enable checking for duplicate values here. + * If it becomes possible then we should do this. + * + * @ingroup protocol + */ // Protocol-critical, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum LedgerEntryType : std::uint16_t { @@ -49,66 +50,72 @@ enum LedgerEntryType : std::uint16_t { #pragma pop_macro("LEDGER_ENTRY") //--------------------------------------------------------------------------- - /** A special type, matching any ledger entry type. - - The value does not represent a concrete type, but rather is used in contexts where the - specific type of a ledger object is unimportant, unknown or unavailable. - - Objects with this special type cannot be created or stored on the ledger. - - \sa keylet::unchecked - */ + /** + * A special type, matching any ledger entry type. + * + * The value does not represent a concrete type, but rather is used in contexts where the + * specific type of a ledger object is unimportant, unknown or unavailable. + * + * Objects with this special type cannot be created or stored on the ledger. + * + * @see keylet::unchecked + */ ltANY = 0, - /** A special type, matching any ledger type except directory nodes. - - The value does not represent a concrete type, but rather is used in contexts where the - ledger object must not be a directory node but its specific type is otherwise unimportant, - unknown or unavailable. - - Objects with this special type cannot be created or stored on the ledger. - - \sa keylet::child + /** + * A special type, matching any ledger type except directory nodes. + * + * The value does not represent a concrete type, but rather is used in contexts where the + * ledger object must not be a directory node but its specific type is otherwise unimportant, + * unknown or unavailable. + * + * Objects with this special type cannot be created or stored on the ledger. + * + * @see keylet::child */ ltCHILD = 0x1CD2, //--------------------------------------------------------------------------- - /** A legacy, deprecated type. - - \deprecated **This object type is not supported and should not be used.** - Support for this type of object was never implemented. - No objects of this type were ever created. + /** + * A legacy, deprecated type. + * + * @deprecated **This object type is not supported and should not be used.** + * Support for this type of object was never implemented. + * No objects of this type were ever created. */ ltNICKNAME [[deprecated("This object type is not supported and should not be used.")]] = 0x006e, - /** A legacy, deprecated type. - - \deprecated **This object type is not supported and should not be used.** - Support for this type of object was never implemented. - No objects of this type were ever created. + /** + * A legacy, deprecated type. + * + * @deprecated **This object type is not supported and should not be used.** + * Support for this type of object was never implemented. + * No objects of this type were ever created. */ ltCONTRACT [[deprecated("This object type is not supported and should not be used.")]] = 0x0063, - /** A legacy, deprecated type. - - \deprecated **This object type is not supported and should not be used.** - Support for this type of object was never implemented. - No objects of this type were ever created. + /** + * A legacy, deprecated type. + * + * @deprecated **This object type is not supported and should not be used.** + * Support for this type of object was never implemented. + * No objects of this type were ever created. */ ltGENERATOR_MAP [[deprecated("This object type is not supported and should not be used.")]] = 0x0067, }; -/** Ledger object flags. - - These flags are specified in ledger objects and modify their behavior. - - @warning Ledger object flags form part of the protocol. - **Changing them should be avoided because without special handling, this will result in a hard - fork.** - - @ingroup protocol -*/ +/** + * Ledger object flags. + * + * These flags are specified in ledger objects and modify their behavior. + * + * @warning Ledger object flags form part of the protocol. + * **Changing them should be avoided because without special handling, this will result in a hard + * fork.** + * + * @ingroup protocol + */ #pragma push_macro("XMACRO") #pragma push_macro("TO_VALUE") #pragma push_macro("VALUE_TO_MAP") @@ -208,7 +215,11 @@ enum LedgerEntryType : std::uint16_t { LEDGER_OBJECT(Loan, \ LSF_FLAG(lsfLoanDefault, 0x00010000) \ LSF_FLAG(lsfLoanImpaired, 0x00020000) \ - LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */ + LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */ \ + \ + LEDGER_OBJECT(Sponsorship, \ + LSF_FLAG(lsfSponsorshipRequireSignForFee, 0x00010000) \ + LSF_FLAG(lsfSponsorshipRequireSignForReserve, 0x00020000)) // clang-format on @@ -285,14 +296,16 @@ getAllLedgerFlags() //------------------------------------------------------------------------------ -/** Holds the list of known ledger entry formats. +/** + * Holds the list of known ledger entry formats. */ class LedgerFormats : public KnownFormats { private: - /** Create the object. - This will load the object with all the known ledger formats. - */ + /** + * Create the object. + * This will load the object with all the known ledger formats. + */ LedgerFormats(); public: diff --git a/include/xrpl/protocol/LedgerHeader.h b/include/xrpl/protocol/LedgerHeader.h index df8f314c5f..d169e53e2c 100644 --- a/include/xrpl/protocol/LedgerHeader.h +++ b/include/xrpl/protocol/LedgerHeader.h @@ -12,7 +12,9 @@ namespace xrpl { -/** Information about the notional ledger backing the view. */ +/** + * Information about the notional ledger backing the view. + */ struct LedgerHeader { explicit LedgerHeader() = default; @@ -67,15 +69,21 @@ getCloseAgree(LedgerHeader const& info) void addRaw(LedgerHeader const&, Serializer&, bool includeHash = false); -/** Deserialize a ledger header from a byte array. */ +/** + * Deserialize a ledger header from a byte array. + */ LedgerHeader deserializeHeader(Slice data, bool hasHash = false); -/** Deserialize a ledger header (prefixed with 4 bytes) from a byte array. */ +/** + * Deserialize a ledger header (prefixed with 4 bytes) from a byte array. + */ LedgerHeader deserializePrefixedHeader(Slice data, bool hasHash = false); -/** Calculate the hash of a ledger header. */ +/** + * Calculate the hash of a ledger header. + */ uint256 calculateLedgerHash(LedgerHeader const& info); diff --git a/include/xrpl/protocol/LedgerShortcut.h b/include/xrpl/protocol/LedgerShortcut.h index 68c31c4c3c..037621121d 100644 --- a/include/xrpl/protocol/LedgerShortcut.h +++ b/include/xrpl/protocol/LedgerShortcut.h @@ -9,13 +9,19 @@ namespace xrpl { * without needing to specify their exact hash or sequence number. */ enum class LedgerShortcut { - /** The current working ledger (open, not yet closed) */ + /** + * The current working ledger (open, not yet closed) + */ Current, - /** The most recently closed ledger (may not be validated) */ + /** + * The most recently closed ledger (may not be validated) + */ Closed, - /** The most recently validated ledger */ + /** + * The most recently validated ledger + */ Validated }; diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 329d83610e..462092f7dd 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -60,7 +60,9 @@ public: bool operator<(MPTAmount const& other) const; - /** Returns true if the amount is not zero */ + /** + * Returns true if the amount is not zero + */ explicit constexpr operator bool() const noexcept; @@ -69,14 +71,17 @@ public: return value(); } - /** Return the sign of the amount */ + /** + * Return the sign of the amount + */ [[nodiscard]] constexpr int signum() const noexcept; - /** Returns the underlying value. Code SHOULD NOT call this - function unless the type has been abstracted away, - e.g. in a templated function. - */ + /** + * Returns the underlying value. Code SHOULD NOT call this + * function unless the type has been abstracted away, + * e.g. in a templated function. + */ [[nodiscard]] constexpr value_type value() const; @@ -100,14 +105,18 @@ MPTAmount::operator=(beast::Zero) return *this; } -/** Returns true if the amount is not zero */ +/** + * Returns true if the amount is not zero + */ constexpr MPTAmount:: operator bool() const noexcept { return value_ != 0; } -/** Return the sign of the amount */ +/** + * Return the sign of the amount + */ constexpr int MPTAmount::signum() const noexcept { @@ -116,10 +125,11 @@ MPTAmount::signum() const noexcept return (value_ != 0) ? 1 : 0; } -/** Returns the underlying value. Code SHOULD NOT call this - function unless the type has been abstracted away, - e.g. in a templated function. -*/ +/** + * Returns the underlying value. Code SHOULD NOT call this + * function unless the type has been abstracted away, + * e.g. in a templated function. + */ constexpr MPTAmount::value_type MPTAmount::value() const { diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 0c495aa57f..7f473da6a2 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -82,7 +82,8 @@ operator<=>(MPTIssue const& lhs, MPTIssue const& rhs) return lhs.mptID_ <=> rhs.mptID_; } -/** MPT is a non-native token. +/** + * MPT is a non-native token. */ inline bool isXRP(MPTID const&) diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h b/include/xrpl/protocol/NFTSyntheticSerializer.h index a1d8bce985..bef05b9a8f 100644 --- a/include/xrpl/protocol/NFTSyntheticSerializer.h +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h @@ -9,10 +9,9 @@ namespace xrpl::RPC { /** - Adds common synthetic fields to transaction-related JSON responses - - @{ + * Adds common synthetic fields to transaction-related JSON responses */ +/** @{ */ void insertNFTSyntheticInJson(json::Value&, std::shared_ptr const&, TxMeta const&); /** @} */ diff --git a/include/xrpl/protocol/NFTokenID.h b/include/xrpl/protocol/NFTokenID.h index f61c6bd5cb..b1b994eabd 100644 --- a/include/xrpl/protocol/NFTokenID.h +++ b/include/xrpl/protocol/NFTokenID.h @@ -12,13 +12,13 @@ namespace xrpl { /** - Add a `nftoken_ids` field to the `meta` output parameter. - The field is only added to successful NFTokenMint, NFTokenAcceptOffer, - and NFTokenCancelOffer transactions. - - Helper functions are not static because they can be used by Clio. - @{ + * Add a `nftoken_ids` field to the `meta` output parameter. + * The field is only added to successful NFTokenMint, NFTokenAcceptOffer, + * and NFTokenCancelOffer transactions. + * + * Helper functions are not static because they can be used by Clio. */ +/** @{ */ bool canHaveNFTokenID(std::shared_ptr const& serializedTx, TxMeta const& transactionMeta); diff --git a/include/xrpl/protocol/NFTokenOfferID.h b/include/xrpl/protocol/NFTokenOfferID.h index c4a80356bf..4810f7932a 100644 --- a/include/xrpl/protocol/NFTokenOfferID.h +++ b/include/xrpl/protocol/NFTokenOfferID.h @@ -11,12 +11,12 @@ namespace xrpl { /** - Add an `offer_id` field to the `meta` output parameter. - The field is only added to successful NFTokenCreateOffer transactions. - - Helper functions are not static because they can be used by Clio. - @{ + * Add an `offer_id` field to the `meta` output parameter. + * The field is only added to successful NFTokenCreateOffer transactions. + * + * Helper functions are not static because they can be used by Clio. */ +/** @{ */ bool canHaveNFTokenOfferID( std::shared_ptr const& serializedTx, diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index f802cfe058..e83e1c97b6 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -14,64 +14,88 @@ namespace xrpl { -/** Protocol specific constants. - - This information is, implicitly, part of the protocol. - - @note Changing these values without adding code to the - server to detect "pre-change" and "post-change" - will result in a hard fork. - - @ingroup protocol -*/ -/** Smallest legal byte size of a transaction. */ +/** + * Protocol specific constants. + * + * This information is, implicitly, part of the protocol. + * + * @note Changing these values without adding code to the + * server to detect "pre-change" and "post-change" + * will result in a hard fork. + * + * @ingroup protocol + */ +/** + * Smallest legal byte size of a transaction. + */ constexpr std::size_t kTxMinSizeBytes = 32; -/** Largest legal byte size of a transaction. */ +/** + * Largest legal byte size of a transaction. + */ constexpr std::size_t kTxMaxSizeBytes = megabytes(1); -/** The maximum number of unfunded offers to delete at once */ +/** + * The maximum number of unfunded offers to delete at once + */ constexpr std::size_t kUnfundedOfferRemoveLimit = 1000; -/** The maximum number of expired offers to delete at once */ +/** + * The maximum number of expired offers to delete at once + */ constexpr std::size_t kExpiredOfferRemoveLimit = 256; -/** The maximum number of metadata entries allowed in one transaction */ +/** + * The maximum number of metadata entries allowed in one transaction + */ constexpr std::size_t kOversizeMetaDataCap = 5200; -/** The maximum number of entries per directory page */ +/** + * The maximum number of entries per directory page + */ constexpr std::size_t kDirNodeMaxEntries = 32; -/** The maximum number of pages allowed in a directory - - Made obsolete by fixDirectoryLimit amendment. -*/ +/** + * The maximum number of pages allowed in a directory + * + * Made obsolete by fixDirectoryLimit amendment. + */ constexpr std::uint64_t kDirNodeMaxPages = 262144; -/** The maximum number of items in an NFT page */ +/** + * The maximum number of items in an NFT page + */ constexpr std::size_t kDirMaxTokensPerPage = 32; -/** The maximum number of owner directory entries for account to be deletable */ +/** + * The maximum number of owner directory entries for account to be deletable + */ constexpr std::size_t kMaxDeletableDirEntries = 1000; -/** The maximum number of token offers that can be canceled at once */ +/** + * The maximum number of token offers that can be canceled at once + */ constexpr std::size_t kMaxTokenOfferCancelCount = 500; -/** The maximum number of offers in an offer directory for NFT to be burnable */ +/** + * The maximum number of offers in an offer directory for NFT to be burnable + */ constexpr std::size_t kMaxDeletableTokenOfferEntries = 500; -/** The maximum token transfer fee allowed. - - Token transfer fees can range from 0% to 50% and are specified in tenths of - a basis point; that is a value of 1000 represents a transfer fee of 1% and - a value of 10000 represents a transfer fee of 10%. - - Note that for extremely low transfer fees values, it is possible that the - calculated fee will be 0. +/** + * The maximum token transfer fee allowed. + * + * Token transfer fees can range from 0% to 50% and are specified in tenths of + * a basis point; that is a value of 1000 represents a transfer fee of 1% and + * a value of 10000 represents a transfer fee of 10%. + * + * Note that for extremely low transfer fees values, it is possible that the + * calculated fee will be 0. */ constexpr std::uint16_t kMaxTransferFee = 50000; -/** There are 10,000 basis points (bips) in 100%. +/** + * There are 10,000 basis points (bips) in 100%. * * Basis points represent 0.01%. * @@ -116,36 +140,41 @@ tenthBipsOfValue(T value, TenthBips bips) } namespace Lending { -/** The maximum management fee rate allowed by a loan broker in 1/10 bips. - - Valid values are between 0 and 10% inclusive. -*/ +/** + * The maximum management fee rate allowed by a loan broker in 1/10 bips. + * + * Valid values are between 0 and 10% inclusive. + */ constexpr TenthBips16 kMaxManagementFeeRate( unsafeCast(percentageToTenthBips(10).value())); static_assert(kMaxManagementFeeRate == TenthBips16(std::uint16_t(10'000u))); -/** The maximum coverage rate required of a loan broker in 1/10 bips. - - Valid values are between 0 and 100% inclusive. -*/ +/** + * The maximum coverage rate required of a loan broker in 1/10 bips. + * + * Valid values are between 0 and 100% inclusive. + */ constexpr TenthBips32 kMaxCoverRate = percentageToTenthBips(100); static_assert(kMaxCoverRate == TenthBips32(100'000u)); -/** The maximum overpayment fee on a loan in 1/10 bips. -* - Valid values are between 0 and 100% inclusive. -*/ +/** + * The maximum overpayment fee on a loan in 1/10 bips. + * + * Valid values are between 0 and 100% inclusive. + */ constexpr TenthBips32 kMaxOverpaymentFee = percentageToTenthBips(100); static_assert(kMaxOverpaymentFee == TenthBips32(100'000u)); -/** Annualized interest rate of the Loan in 1/10 bips. +/** + * Annualized interest rate of the Loan in 1/10 bips. * * Valid values are between 0 and 100% inclusive. */ constexpr TenthBips32 kMaxInterestRate = percentageToTenthBips(100); static_assert(kMaxInterestRate == TenthBips32(100'000u)); -/** The maximum premium added to the interest rate for late payments on a loan +/** + * The maximum premium added to the interest rate for late payments on a loan * in 1/10 bips. * * Valid values are between 0 and 100% inclusive. @@ -153,7 +182,8 @@ static_assert(kMaxInterestRate == TenthBips32(100'000u)); constexpr TenthBips32 kMaxLateInterestRate = percentageToTenthBips(100); static_assert(kMaxLateInterestRate == TenthBips32(100'000u)); -/** The maximum close interest rate charged for repaying a loan early in 1/10 +/** + * The maximum close interest rate charged for repaying a loan early in 1/10 * bips. * * Valid values are between 0 and 100% inclusive. @@ -161,7 +191,8 @@ static_assert(kMaxLateInterestRate == TenthBips32(100'000u)); constexpr TenthBips32 kMaxCloseInterestRate = percentageToTenthBips(100); static_assert(kMaxCloseInterestRate == TenthBips32(100'000u)); -/** The maximum overpayment interest rate charged on loan overpayments in 1/10 +/** + * The maximum overpayment interest rate charged on loan overpayments in 1/10 * bips. * * Valid values are between 0 and 100% inclusive. @@ -169,7 +200,8 @@ static_assert(kMaxCloseInterestRate == TenthBips32(100'000u)); constexpr TenthBips32 kMaxOverpaymentInterestRate = percentageToTenthBips(100); static_assert(kMaxOverpaymentInterestRate == TenthBips32(100'000u)); -/** LoanPay transaction cost will be one base fee per X combined payments +/** + * LoanPay transaction cost will be one base fee per X combined payments * * The number of payments is estimated based on the Amount paid and the Loan's * Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag) @@ -180,7 +212,8 @@ static_assert(kMaxOverpaymentInterestRate == TenthBips32(100'000u)); */ static constexpr int kLoanPaymentsPerFeeIncrement = 5; -/** Maximum number of combined payments that a LoanPay transaction will process +/** + * Maximum number of combined payments that a LoanPay transaction will process * * This limit is enforced during the loan payment process, and thus is not * estimated. If the limit is hit, no further payments or overpayments will be @@ -205,173 +238,267 @@ static constexpr int kLoanPaymentsPerFeeIncrement = 5; static constexpr int kLoanMaximumPaymentsPerTransaction = 100; } // namespace Lending -/** The maximum length of a URI inside an NFT */ +/** + * The maximum length of a URI inside an NFT + */ constexpr std::size_t kMaxTokenUriLength = 256; -/** The maximum length of a Data element inside a DID */ +/** + * The maximum length of a Data element inside a DID + */ constexpr std::size_t kMaxDidDocumentLength = 256; -/** The maximum length of a URI inside a DID */ +/** + * The maximum length of a URI inside a DID + */ constexpr std::size_t kMaxDidUriLength = 256; -/** The maximum length of an Attestation inside a DID */ +/** + * The maximum length of an Attestation inside a DID + */ constexpr std::size_t kMaxDidDataLength = 256; -/** The maximum length of a domain */ +/** + * The maximum length of a domain + */ constexpr std::size_t kMaxDomainLength = 256; -/** The maximum length of a URI inside a Credential */ +/** + * The maximum length of a URI inside a Credential + */ constexpr std::size_t kMaxCredentialUriLength = 256; -/** The maximum length of a CredentialType inside a Credential */ +/** + * The maximum length of a CredentialType inside a Credential + */ constexpr std::size_t kMaxCredentialTypeLength = 64; -/** The maximum number of credentials can be passed in array */ +/** + * The maximum number of credentials can be passed in array + */ constexpr std::size_t kMaxCredentialsArraySize = 8; -/** The maximum number of credentials can be passed in array for permissioned - * domain */ +/** + * The maximum number of credentials can be passed in array for permissioned + * domain + */ constexpr std::size_t kMaxPermissionedDomainCredentialsArraySize = 10; -/** The maximum length of MPTokenMetadata */ +/** + * The maximum length of MPTokenMetadata + */ constexpr std::size_t kMaxMpTokenMetadataLength = 1024; -/** The maximum amount of MPTokenIssuance */ +/** + * The maximum amount of MPTokenIssuance + */ constexpr std::uint64_t kMaxMpTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull; static_assert(Number::kMaxRep >= kMaxMpTokenAmount); -/** The maximum length of Data payload */ +/** + * The maximum length of Data payload + */ constexpr std::size_t kMaxDataPayloadLength = 256; -/** Vault withdrawal policies */ +/** + * Vault withdrawal policies + */ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1; -/** Default IOU scale factor for a Vault */ +/** + * Default IOU scale factor for a Vault + */ constexpr std::uint8_t kVaultDefaultIouScale = 6; -/** Maximum scale factor for a Vault. The number is chosen to ensure that -1 IOU can be always converted to shares. -10^19 > maxMPTokenAmount (2^64-1) > 10^18 */ +/** + * Maximum scale factor for a Vault. The number is chosen to ensure that + * 1 IOU can be always converted to shares. + * 10^19 > maxMPTokenAmount (2^64-1) > 10^18 + */ constexpr std::uint8_t kVaultMaximumIouScale = 18; -/** Maximum recursion depth for vault shares being put as an asset inside - * another vault; counted from 0 */ +/** + * Maximum recursion depth for vault shares being put as an asset inside + * another vault; counted from 0 + */ constexpr std::uint8_t kMaxAssetCheckDepth = 5; -/** A ledger index. */ +/** + * A ledger index. + */ using LedgerIndex = std::uint32_t; constexpr std::uint32_t kFlagLedgerInterval = 256; -/** Returns true if the given ledgerIndex is a voting ledgerIndex */ +/** + * Returns true if the given ledgerIndex is a voting ledgerIndex + */ bool isVotingLedger(LedgerIndex seq); -/** Returns true if the given ledgerIndex is a flag ledgerIndex */ +/** + * Returns true if the given ledgerIndex is a flag ledgerIndex + */ bool isFlagLedger(LedgerIndex seq); -/** A transaction identifier. - The value is computed as the hash of the - canonicalized, serialized transaction object. -*/ +/** + * A transaction identifier. + * The value is computed as the hash of the + * canonicalized, serialized transaction object. + */ using TxID = uint256; -/** The maximum number of trustlines to delete as part of AMM account +/** + * The maximum number of trustlines to delete as part of AMM account * deletion cleanup. */ constexpr std::uint16_t kMaxDeletableAmmTrustLines = 512; -/** The maximum length of a URI inside an Oracle */ +/** + * The maximum length of a URI inside an Oracle + */ constexpr std::size_t kMaxOracleUri = 256; -/** The maximum length of a Provider inside an Oracle */ +/** + * The maximum length of a Provider inside an Oracle + */ constexpr std::size_t kMaxOracleProvider = 256; -/** The maximum size of a data series array inside an Oracle */ +/** + * The maximum size of a data series array inside an Oracle + */ constexpr std::size_t kMaxOracleDataSeries = 10; -/** The maximum length of a SymbolClass inside an Oracle */ +/** + * The maximum length of a SymbolClass inside an Oracle + */ constexpr std::size_t kMaxOracleSymbolClass = 16; -/** The maximum allowed time difference between lastUpdateTime and the time - of the last closed ledger -*/ +/** + * The maximum allowed time difference between lastUpdateTime and the time + * of the last closed ledger + */ constexpr std::size_t kMaxLastUpdateTimeDelta = 300; -/** The maximum price scaling factor +/** + * The maximum price scaling factor */ constexpr std::size_t kMaxPriceScale = 20; -/** The maximum percentage of outliers to trim +/** + * The maximum percentage of outliers to trim */ constexpr std::size_t kMaxTrim = 25; -/** The maximum number of delegate permissions an account can grant +/** + * The maximum number of delegate permissions an account can grant */ constexpr std::size_t kPermissionMaxSize = 10; -/** The maximum number of transactions that can be in a batch. */ +/** + * The maximum number of transactions that can be in a batch. + */ constexpr std::size_t kMaxBatchTxCount = 8; -/** The maximum number of batch signers. */ +/** + * The maximum number of batch signers. + */ constexpr std::size_t kMaxBatchSigners = kMaxBatchTxCount * 3; -/** Length of a secp256k1 scalar in bytes. */ +/** + * Length of a secp256k1 scalar in bytes. + */ constexpr std::size_t kEcScalarLength = kMPT_SCALAR_SIZE; -/** Length of EC point (compressed) */ +/** + * Length of EC point (compressed) + */ constexpr std::size_t kCompressedEcPointLength = 33; -/** Length of one compressed EC point component in an EC ElGamal ciphertext. */ +/** + * Length of one compressed EC point component in an EC ElGamal ciphertext. + */ constexpr std::size_t kEcCiphertextComponentLength = kMPT_ELGAMAL_CIPHER_SIZE; -/** EC ElGamal ciphertext length: two compressed EC points concatenated. */ +/** + * EC ElGamal ciphertext length: two compressed EC points concatenated. + */ constexpr std::size_t kEcGamalEncryptedTotalLength = kMPT_ELGAMAL_TOTAL_SIZE; -/** Length of EC public key (compressed) */ +/** + * Length of EC public key (compressed) + */ constexpr std::size_t kEcPubKeyLength = kMPT_PUBKEY_SIZE; -/** Length of EC private key in bytes */ +/** + * Length of EC private key in bytes + */ constexpr std::size_t kEcPrivKeyLength = kMPT_PRIVKEY_SIZE; -/** Length of the EC blinding factor in bytes */ +/** + * Length of the EC blinding factor in bytes + */ constexpr std::size_t kEcBlindingFactorLength = kMPT_BLINDING_FACTOR_SIZE; -/** Length of Schnorr ZKProof for public key registration (compact form) in bytes */ +/** + * Length of Schnorr ZKProof for public key registration (compact form) in bytes + */ constexpr std::size_t kEcSchnorrProofLength = kMPT_SCHNORR_PROOF_SIZE; -/** Length of Pedersen Commitment (compressed) */ +/** + * Length of Pedersen Commitment (compressed) + */ constexpr std::size_t kEcPedersenCommitmentLength = kMPT_PEDERSEN_COMMIT_SIZE; -/** Length of single bulletproof (range proof for 1 commitment) in bytes */ +/** + * Length of single bulletproof (range proof for 1 commitment) in bytes + */ constexpr std::size_t kEcSingleBulletproofLength = kMPT_SINGLE_BULLETPROOF_SIZE; -/** Length of double bulletproof (range proof for 2 commitments) in bytes */ +/** + * Length of double bulletproof (range proof for 2 commitments) in bytes + */ constexpr std::size_t kEcDoubleBulletproofLength = kMPT_DOUBLE_BULLETPROOF_SIZE; -/** Length of the compact sigma proof component for ConfidentialMPTSend. */ +/** + * Length of the compact sigma proof component for ConfidentialMPTSend. + */ constexpr std::size_t kEcSendSigmaProofLength = SECP256K1_COMPACT_STANDARD_PROOF_SIZE; -/** 192 bytes compact sigma proof + 754 bytes double bulletproof. */ +/** + * 192 bytes compact sigma proof + 754 bytes double bulletproof. + */ constexpr std::size_t kEcSendProofLength = kEcSendSigmaProofLength + kEcDoubleBulletproofLength; -/** Length of the compact sigma proof component for ConfidentialMPTConvertBack. */ +/** + * Length of the compact sigma proof component for ConfidentialMPTConvertBack. + */ constexpr std::size_t kEcConvertBackSigmaProofLength = SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE; -/** 128 bytes compact sigma proof + 688 bytes single bulletproof. */ +/** + * 128 bytes compact sigma proof + 688 bytes single bulletproof. + */ constexpr std::size_t kEcConvertBackProofLength = kEcConvertBackSigmaProofLength + kEcSingleBulletproofLength; -/** Length of the ZKProof for ConfidentialMPTClawback. */ +/** + * Length of the ZKProof for ConfidentialMPTClawback. + */ constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE; -/** Extra base fee multiplier charged to confidential MPT transactions. */ +/** + * Extra base fee multiplier charged to confidential MPT transactions. + */ constexpr std::uint32_t kConfidentialFeeMultiplier = 9; -/** Compressed EC point prefix for even y-coordinate */ +/** + * Compressed EC point prefix for even y-coordinate + */ constexpr std::uint8_t kEcCompressedPrefixEvenY = 0x02; -/** Compressed EC point prefix for odd y-coordinate */ +/** + * Compressed EC point prefix for odd y-coordinate + */ constexpr std::uint8_t kEcCompressedPrefixOddY = 0x03; } // namespace xrpl diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index 13db17fc6e..98301af487 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -26,28 +26,29 @@ namespace xrpl { -/** A public key. - - Public keys are used in the public-key cryptography - system used to verify signatures attached to messages. - - The format of the public key is XRPL specific, - information needed to determine the cryptosystem - parameters used is stored inside the key. - - As of this writing two systems are supported: - - secp256k1 - ed25519 - - secp256k1 public keys consist of a 33 byte - compressed public key, with the lead byte equal - to 0x02 or 0x03. - - The ed25519 public keys consist of a 1 byte - prefix constant 0xED, followed by 32 bytes of - public key data. -*/ +/** + * A public key. + * + * Public keys are used in the public-key cryptography + * system used to verify signatures attached to messages. + * + * The format of the public key is XRPL specific, + * information needed to determine the cryptosystem + * parameters used is stored inside the key. + * + * As of this writing two systems are supported: + * + * secp256k1 + * ed25519 + * + * secp256k1 public keys consist of a 33 byte + * compressed public key, with the lead byte equal + * to 0x02 or 0x03. + * + * The ed25519 public keys consist of a 1 byte + * prefix constant 0xED, followed by 32 bytes of + * public key data. + */ class PublicKey { protected: @@ -66,11 +67,12 @@ public: PublicKey& operator=(PublicKey const& other); - /** Create a public key. - - Preconditions: - publicKeyType(slice) != std::nullopt - */ + /** + * Create a public key. + * + * Preconditions: + * publicKeyType(slice) != std::nullopt + */ explicit PublicKey(Slice const& slice); [[nodiscard]] std::uint8_t const* @@ -121,7 +123,8 @@ public: } }; -/** Print the public key to a stream. +/** + * Print the public key to a stream. */ std::ostream& operator<<(std::ostream& os, PublicKey const& pk); @@ -180,39 +183,41 @@ parseBase58(TokenType type, std::string const& s); enum class ECDSACanonicality { Canonical, FullyCanonical }; -/** Determines the canonicality of a signature. - - A canonical signature is in its most reduced form. - For example the R and S components do not contain - additional leading zeroes. However, even in - canonical form, (R,S) and (R,G-S) are both - valid signatures for message M. - - Therefore, to prevent malleability attacks we - define a fully canonical signature as one where: - - R < G - S - - where G is the curve order. - - This routine returns std::nullopt if the format - of the signature is invalid (for example, the - points are encoded incorrectly). - - @return std::nullopt if the signature fails - validity checks. - - @note Only the format of the signature is checked, - no verification cryptography is performed. -*/ +/** + * Determines the canonicality of a signature. + * + * A canonical signature is in its most reduced form. + * For example the R and S components do not contain + * additional leading zeroes. However, even in + * canonical form, (R,S) and (R,G-S) are both + * valid signatures for message M. + * + * Therefore, to prevent malleability attacks we + * define a fully canonical signature as one where: + * + * R < G - S + * + * where G is the curve order. + * + * This routine returns std::nullopt if the format + * of the signature is invalid (for example, the + * points are encoded incorrectly). + * + * @return std::nullopt if the signature fails + * validity checks. + * + * @note Only the format of the signature is checked, + * no verification cryptography is performed. + */ std::optional ecdsaCanonicality(Slice const& sig); -/** Returns the type of public key. - - @return std::nullopt If the public key does not - represent a known type. -*/ +/** + * Returns the type of public key. + * + * @return std::nullopt If the public key does not + * represent a known type. + */ /** @{ */ [[nodiscard]] std::optional publicKeyType(Slice const& slice); @@ -224,7 +229,9 @@ publicKeyType(PublicKey const& publicKey) } /** @} */ -/** Verify a secp256k1 signature on the digest of a message. */ +/** + * Verify a secp256k1 signature on the digest of a message. + */ [[nodiscard]] bool verifyDigest( PublicKey const& publicKey, @@ -232,14 +239,17 @@ verifyDigest( Slice const& sig, bool mustBeFullyCanonical = true) noexcept; -/** Verify a signature on a message. - With secp256k1 signatures, the data is first hashed with - SHA512-Half, and the resulting digest is signed. -*/ +/** + * Verify a signature on a message. + * With secp256k1 signatures, the data is first hashed with + * SHA512-Half, and the resulting digest is signed. + */ [[nodiscard]] bool verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept; -/** Calculate the 160-bit node ID from a node public key. */ +/** + * Calculate the 160-bit node ID from a node public key. + */ NodeID calcNodeID(PublicKey const&); diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h index de61d79ca5..3475efa977 100644 --- a/include/xrpl/protocol/Quality.h +++ b/include/xrpl/protocol/Quality.h @@ -14,15 +14,16 @@ namespace xrpl { -/** Represents a pair of input and output currencies. - - The input currency can be converted to the output - currency by multiplying by the rate, represented by - Quality. - - For offers, "in" is always TakerPays and "out" is - always TakerGets. -*/ +/** + * Represents a pair of input and output currencies. + * + * The input currency can be converted to the output + * currency by multiplying by the rate, represented by + * Quality. + * + * For offers, "in" is always TakerPays and "out" is + * always TakerGets. + */ template struct TAmounts { @@ -36,7 +37,9 @@ struct TAmounts { } - /** Returns `true` if either quantity is not positive. */ + /** + * Returns `true` if either quantity is not positive. + */ [[nodiscard]] bool empty() const noexcept { @@ -84,11 +87,12 @@ operator!=(TAmounts const& lhs, TAmounts const& rhs) noexcept // XRPL specific constant used for parsing qualities and other things #define QUALITY_ONE 1'000'000'000 -/** Represents the logical ratio of output currency to input currency. - Internally this is stored using a custom floating point representation, - as the inverse of the ratio, so that quality will be descending in - a sequence of actual values that represent qualities. -*/ +/** + * Represents the logical ratio of output currency to input currency. + * Internally this is stored using a custom floating point representation, + * as the inverse of the ratio, so that quality will be descending in + * a sequence of actual values that represent qualities. + */ class Quality { public: @@ -109,26 +113,36 @@ private: public: Quality() = default; - /** Create a quality from the integer encoding of an STAmount */ + /** + * Create a quality from the integer encoding of an STAmount + */ explicit Quality(std::uint64_t value); - /** Create a quality from the ratio of two amounts. */ + /** + * Create a quality from the ratio of two amounts. + */ explicit Quality(Amounts const& amount); - /** Create a quality from the ratio of two amounts. */ + /** + * Create a quality from the ratio of two amounts. + */ template explicit Quality(TAmounts const& amount) : Quality(Amounts(toSTAmount(amount.in), toSTAmount(amount.out))) { } - /** Create a quality from the ratio of two amounts. */ + /** + * Create a quality from the ratio of two amounts. + */ template Quality(Out const& out, In const& in) : Quality(Amounts(toSTAmount(in), toSTAmount(out))) { } - /** Advances to the next higher quality level. */ + /** + * Advances to the next higher quality level. + */ /** @{ */ Quality& operator++(); @@ -137,7 +151,9 @@ public: operator++(int); /** @} */ - /** Advances to the next lower quality level. */ + /** + * Advances to the next lower quality level. + */ /** @{ */ Quality& operator--(); @@ -146,23 +162,27 @@ public: operator--(int); /** @} */ - /** Returns the quality as STAmount. */ + /** + * Returns the quality as STAmount. + */ [[nodiscard]] STAmount rate() const { return amountFromQuality(value_); } - /** Returns the quality rounded up to the specified number - of decimal digits. - */ + /** + * Returns the quality rounded up to the specified number + * of decimal digits. + */ [[nodiscard]] Quality round(int tickSize) const; - /** Returns the scaled amount with in capped. - Math is avoided if the result is exact. The output is clamped - to prevent money creation. - */ + /** + * Returns the scaled amount with in capped. + * Math is avoided if the result is exact. The output is clamped + * to prevent money creation. + */ [[nodiscard]] Amounts ceilIn(Amounts const& amount, STAmount const& limit) const; @@ -180,10 +200,11 @@ public: [[nodiscard]] TAmounts ceilInStrict(TAmounts const& amount, In const& limit, bool roundUp) const; - /** Returns the scaled amount with out capped. - Math is avoided if the result is exact. The input is clamped - to prevent money creation. - */ + /** + * Returns the scaled amount with out capped. + * Math is avoided if the result is exact. The input is clamped + * to prevent money creation. + */ [[nodiscard]] Amounts ceilOut(Amounts const& amount, STAmount const& limit) const; @@ -215,10 +236,11 @@ private: Round... round) const; public: - /** Returns `true` if lhs is lower quality than `rhs`. - Lower quality means the taker receives a worse deal. - Higher quality is better for the taker. - */ + /** + * Returns `true` if lhs is lower quality than `rhs`. + * Lower quality means the taker receives a worse deal. + * Higher quality is better for the taker. + */ friend bool operator<(Quality const& lhs, Quality const& rhs) noexcept { @@ -357,10 +379,11 @@ Quality::ceilOutStrict(TAmounts const& amount, Out const& limit, bool r return ceilTAmountsHelper(amount, limit, amount.out, kCeilOutFnPtr, roundUp); } -/** Calculate the quality of a two-hop path given the two hops. - @param lhs The first leg of the path: input to intermediate. - @param rhs The second leg of the path: intermediate to output. -*/ +/** + * Calculate the quality of a two-hop path given the two hops. + * @param lhs The first leg of the path: input to intermediate. + * @param rhs The second leg of the path: intermediate to output. + */ Quality composedQuality(Quality const& lhs, Quality const& rhs); diff --git a/include/xrpl/protocol/QualityFunction.h b/include/xrpl/protocol/QualityFunction.h index 7830519deb..128b37ce12 100644 --- a/include/xrpl/protocol/QualityFunction.h +++ b/include/xrpl/protocol/QualityFunction.h @@ -12,7 +12,8 @@ namespace xrpl { -/** Average quality of a path as a function of `out`: q(out) = m * out + b, +/** + * Average quality of a path as a function of `out`: q(out) = m * out + b, * where m = -1 / poolGets, b = poolPays / poolGets. If CLOB offer then * `m` is equal to 0 `b` is equal to the offer's quality. The function * is derived by substituting `in` in q = out / in with the swap out formula @@ -45,19 +46,22 @@ public: template QualityFunction(TAmounts const& amounts, std::uint32_t tfee, AMMTag); - /** Combines QF with the next step QF + /** + * Combines QF with the next step QF */ void combine(QualityFunction const& qf); - /** Find output to produce the requested + /** + * Find output to produce the requested * average quality. * @param quality requested average quality (quality limit) */ std::optional outFromAvgQ(Quality const& quality); - /** Return true if the quality function is constant + /** + * Return true if the quality function is constant */ [[nodiscard]] bool isConst() const diff --git a/include/xrpl/protocol/Rate.h b/include/xrpl/protocol/Rate.h index b8b04c8fb9..048787cab5 100644 --- a/include/xrpl/protocol/Rate.h +++ b/include/xrpl/protocol/Rate.h @@ -10,12 +10,13 @@ namespace xrpl { -/** Represents a transfer rate - - Transfer rates are specified as fractions of 1 billion. - For example, a transfer rate of 1% is represented as - 1,010,000,000. -*/ +/** + * Represents a transfer rate + * + * Transfer rates are specified as fractions of 1 billion. + * For example, a transfer rate of 1% is represented as + * 1,010,000,000. + */ struct Rate : private boost::totally_ordered { std::uint32_t value; @@ -65,13 +66,17 @@ STAmount divideRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp); namespace nft { -/** Given a transfer fee (in basis points) convert it to a transfer rate. */ +/** + * Given a transfer fee (in basis points) convert it to a transfer rate. + */ Rate transferFeeAsRate(std::uint16_t fee); } // namespace nft -/** A transfer rate signifying a 1:1 exchange */ +/** + * A transfer rate signifying a 1:1 exchange + */ extern Rate const kParityRate; } // namespace xrpl diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index da2031650f..2c2136b6e8 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -11,7 +11,8 @@ namespace xrpl { -/** Check whether a feature is enabled in the current ledger rules +/** + * Check whether a feature is enabled in the current ledger rules * * @param feature The feature to be tested. * @param resultIfNoRules What to return if called from outside a Transactor context. @@ -19,7 +20,8 @@ namespace xrpl { bool isFeatureEnabled(uint256 const& feature, bool resultIfNoRules); -/** Check whether a feature is enabled in the current ledger rules +/** + * Check whether a feature is enabled in the current ledger rules * * @param feature The feature to be tested. * @@ -31,7 +33,9 @@ isFeatureEnabled(uint256 const& feature); class DigestAwareReadView; -/** Rules controlling protocol behavior. */ +/** + * Rules controlling protocol behavior. + */ class Rules { private: @@ -54,11 +58,12 @@ public: Rules() = delete; - /** Construct an empty rule set. - - These are the rules reflected by - the genesis ledger. - */ + /** + * Construct an empty rule set. + * + * These are the rules reflected by + * the genesis ledger. + */ explicit Rules(std::unordered_set> const& presets); private: @@ -80,14 +85,17 @@ private: presets() const; public: - /** Returns `true` if a feature is enabled. */ + /** + * Returns `true` if a feature is enabled. + */ [[nodiscard]] bool enabled(uint256 const& feature) const; - /** Returns `true` if two rule sets are identical. - - @note This is for diagnostics. - */ + /** + * Returns `true` if two rule sets are identical. + * + * @note This is for diagnostics. + */ bool operator==(Rules const&) const; @@ -101,7 +109,8 @@ getCurrentTransactionRules(); void setCurrentTransactionRules(std::optional r); -/** RAII class to set and restore the current transaction rules +/** + * RAII class to set and restore the current transaction rules */ class CurrentTransactionRulesGuard { diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index d97bcb0a1d..21ab7813f9 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -117,16 +117,17 @@ fieldCode(int id, int index) return (id << 16) | index; } -/** Identifies fields. - - Fields are necessary to tag data in signed transactions so that - the binary format of the transaction can be canonicalized. All - SFields are created at compile time. - - Each SField, once constructed, lives until program termination, and there - is only one instance per fieldType/fieldValue pair which serves the - entire application. -*/ +/** + * Identifies fields. + * + * Fields are necessary to tag data in signed transactions so that + * the binary format of the transaction can be canonicalized. All + * SFields are created at compile time. + * + * Each SField, once constructed, lives until program termination, and there + * is only one instance per fieldType/fieldValue pair which serves the + * entire application. + */ class SField { public: @@ -299,7 +300,9 @@ private: static std::unordered_map knownNameToField; }; -/** A field with a type known at compile time. */ +/** + * A field with a type known at compile time. + */ template struct TypedField : SField { @@ -309,7 +312,9 @@ struct TypedField : SField explicit TypedField(PrivateAccessTagT pat, Args&&... args); }; -/** Indicate std::optional field semantics. */ +/** + * Indicate std::optional field semantics. + */ template struct OptionaledField { diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index 682a7c655e..cb24ee315a 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -12,7 +12,9 @@ namespace xrpl { -/** Kind of element in each entry of an SOTemplate. */ +/** + * Kind of element in each entry of an SOTemplate. + */ // 2026 usages, 129 files // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum SOEStyle { @@ -25,13 +27,17 @@ enum SOEStyle { }; // Part of a Python-parsed DSL (transactions.macro); bare enumerator names required by the parser -/** Amount fields that can support MPT */ +/** + * Amount fields that can support MPT + */ // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum SOETxMPTIssue { SoeMptNone, SoeMptSupported, SoeMptNotSupported }; //------------------------------------------------------------------------------ -/** An element in a SOTemplate. */ +/** + * An element in a SOTemplate. + */ class SOElement { // Use std::reference_wrapper so SOElement can be stored in a std::vector. @@ -90,10 +96,11 @@ public: //------------------------------------------------------------------------------ -/** Defines the fields and their attributes within a STObject. - Each subclass of SerializedObject will provide its own template - describing the available fields and their metadata attributes. -*/ +/** + * Defines the fields and their attributes within a STObject. + * Each subclass of SerializedObject will provide its own template + * describing the available fields and their metadata attributes. + */ class SOTemplate { public: @@ -103,14 +110,16 @@ public: SOTemplate& operator=(SOTemplate&& other) = default; - /** Create a template populated with all fields. - After creating the template fields cannot be added, modified, or removed. - */ + /** + * Create a template populated with all fields. + * After creating the template fields cannot be added, modified, or removed. + */ SOTemplate(std::vector uniqueFields, std::vector commonFields = {}); - /** Create a template populated with all fields. - Note: Defers to the vector constructor above. - */ + /** + * Create a template populated with all fields. + * Note: Defers to the vector constructor above. + */ SOTemplate( std::initializer_list uniqueFields, std::initializer_list commonFields = {}); @@ -140,14 +149,18 @@ public: return end(); } - /** The number of entries in this template */ + /** + * The number of entries in this template + */ [[nodiscard]] std::size_t size() const { return elements_.size(); } - /** Retrieve the position of a named field. */ + /** + * Retrieve the position of a named field. + */ [[nodiscard]] int getIndex(SField const&) const; diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 5e53a85129..cc80481582 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -190,7 +190,9 @@ public: [[nodiscard]] int signum() const noexcept; - /** Returns a zero value with the same issuer and currency. */ + /** + * Returns a zero value with the same issuer and currency. + */ [[nodiscard]] STAmount zeroed() const; @@ -255,7 +257,9 @@ public: void clear(Asset const& asset); - /** Set the Issue for this amount. */ + /** + * Set the Issue for this amount. + */ void setIssue(Asset const& asset); @@ -704,7 +708,8 @@ divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool std::uint64_t getRate(STAmount const& offerOut, STAmount const& offerIn); -/** Round an arbitrary precision Amount to the precision of an STAmount that has +/** + * Round an arbitrary precision Amount to the precision of an STAmount that has * a given exponent. * * This is used to ensure that calculations involving IOU amounts do not collect @@ -714,7 +719,6 @@ getRate(STAmount const& offerOut, STAmount const& offerIn); * @param scale An exponent value to establish the precision limit of * `value`. Should be larger than `value.exponent()`. * @param rounding Optional Number rounding mode - * */ [[nodiscard]] STAmount roundToScale( @@ -722,7 +726,8 @@ roundToScale( std::int32_t scale, Number::RoundingMode rounding = Number::getround()); -/** Round an arbitrary precision Number IN PLACE to the precision of a given +/** + * Round an arbitrary precision Number IN PLACE to the precision of a given * Asset. * * This is used to ensure that calculations do not collect dust for IOUs, or @@ -738,7 +743,8 @@ roundToAsset(A const& asset, Number& value) value = STAmount{asset, value}; } -/** Round an arbitrary precision Number to the precision of a given Asset. +/** + * Round an arbitrary precision Number to the precision of a given Asset. * * This is used to ensure that calculations do not collect dust beyond specified * scale for IOUs, or fractional amounts for the integral types XRP and MPT. @@ -780,7 +786,8 @@ canAdd(STAmount const& amt1, STAmount const& amt2); bool canSubtract(STAmount const& amt1, STAmount const& amt2); -/** Get the scale of a Number for a given asset. +/** + * Get the scale of a Number for a given asset. * * "scale" is similar to "exponent", but from the perspective of STAmount, which has different rules * and mantissa ranges for determining the exponent than Number. diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index 341c80edd7..acc5500a57 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -15,7 +15,9 @@ namespace xrpl { -/// Note, should be treated as flags that can be | and & +/** + * Note, should be treated as flags that can be | and & + */ struct JsonOptions { using underlying_t = unsigned int; @@ -53,22 +55,28 @@ struct JsonOptions [[nodiscard]] constexpr auto friend operator!=(JsonOptions lh, JsonOptions rh) noexcept -> bool = default; - /// Returns JsonOptions union of lh and rh + /** + * Returns JsonOptions union of lh and rh + */ [[nodiscard]] constexpr JsonOptions friend operator|(JsonOptions lh, JsonOptions rh) noexcept { return {lh.value | rh.value}; } - /// Returns JsonOptions intersection of lh and rh + /** + * Returns JsonOptions intersection of lh and rh + */ [[nodiscard]] constexpr JsonOptions friend operator&(JsonOptions lh, JsonOptions rh) noexcept { return {lh.value & rh.value}; } - /// Returns JsonOptions binary negation, can be used with & (above) for set - /// difference e.g. `(options & ~JsonOptions::kIncludeDate)` + /** + * Returns JsonOptions binary negation, can be used with & (above) for set + * difference e.g. `(options & ~JsonOptions::kIncludeDate)` + */ [[nodiscard]] constexpr JsonOptions friend operator~(JsonOptions v) noexcept { @@ -103,19 +111,20 @@ class STVar; //------------------------------------------------------------------------------ -/** A type which can be exported to a well known binary format. - - A STBase: - - Always a field - - Can always go inside an eligible enclosing STBase - (such as STArray) - - Has a field name - - Like JSON, a SerializedObject is a basket which has rules - on what it can hold. - - @note "ST" stands for "Serialized Type." -*/ +/** + * A type which can be exported to a well known binary format. + * + * A STBase: + * - Always a field + * - Can always go inside an eligible enclosing STBase + * (such as STArray) + * - Has a field name + * + * Like JSON, a SerializedObject is a basket which has rules + * on what it can hold. + * + * @note "ST" stands for "Serialized Type." + */ class STBase { SField const* fName_; @@ -162,9 +171,10 @@ public: [[nodiscard]] virtual bool isDefault() const; - /** A STBase is a field. - This sets the name. - */ + /** + * A STBase is a field. + * This sets the name. + */ void setFName(SField const& n); diff --git a/include/xrpl/protocol/STExchange.h b/include/xrpl/protocol/STExchange.h index a9c1f57bd8..ad5bd4c012 100644 --- a/include/xrpl/protocol/STExchange.h +++ b/include/xrpl/protocol/STExchange.h @@ -18,7 +18,9 @@ namespace xrpl { -/** Convert between serialized type U and C++ type T. */ +/** + * Convert between serialized type U and C++ type T. + */ template struct STExchange; @@ -90,7 +92,9 @@ struct STExchange //------------------------------------------------------------------------------ -/** Return the value of a field in an STObject as a given type. */ +/** + * Return the value of a field in an STObject as a given type. + */ /** @{ */ template std::optional @@ -119,7 +123,9 @@ get(STObject const& st, TypedField const& f) } /** @} */ -/** Set a field value in an STObject. */ +/** + * Set a field value in an STObject. + */ template void set(STObject& st, TypedField const& f, T&& t) @@ -127,7 +133,9 @@ set(STObject& st, TypedField const& f, T&& t) st.set(STExchange>::set(f, std::forward(t))); } -/** Set a blob field using an init function. */ +/** + * Set a blob field using an init function. + */ template void set(STObject& st, TypedField const& f, std::size_t size, Init&& init) @@ -135,7 +143,9 @@ set(STObject& st, TypedField const& f, std::size_t size, Init&& init) st.set(std::make_unique(f, size, init)); } -/** Set a blob field from data. */ +/** + * Set a blob field from data. + */ template void set(STObject& st, TypedField const& f, void const* data, std::size_t size) @@ -143,7 +153,9 @@ set(STObject& st, TypedField const& f, void const* data, std::size_t siz st.set(std::make_unique(f, data, size)); } -/** Remove a field in an STObject. */ +/** + * Remove a field in an STObject. + */ template void erase(STObject& st, TypedField const& f) diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h index a5f449f99c..8731488adb 100644 --- a/include/xrpl/protocol/STLedgerEntry.h +++ b/include/xrpl/protocol/STLedgerEntry.h @@ -33,7 +33,9 @@ public: using const_pointer = std::shared_ptr; using const_ref = std::shared_ptr const&; - /** Create an empty object with the given key and type. */ + /** + * Create an empty object with the given key and type. + */ explicit STLedgerEntry(Keylet const& k); STLedgerEntry(LedgerEntryType type, uint256 const& key); STLedgerEntry(SerialIter& sit, uint256 const& index); @@ -52,10 +54,11 @@ public: [[nodiscard]] json::Value getJson(JsonOptions options = JsonOptions::Values::None) const override; - /** Returns the 'key' (or 'index') of this item. - The key identifies this entry's position in - the SHAMap associative container. - */ + /** + * Returns the 'key' (or 'index') of this item. + * The key identifies this entry's position in + * the SHAMap associative container. + */ [[nodiscard]] uint256 const& key() const; @@ -105,10 +108,11 @@ inline STLedgerEntry::STLedgerEntry( { } -/** Returns the 'key' (or 'index') of this item. - The key identifies this entry's position in - the SHAMap associative container. -*/ +/** + * Returns the 'key' (or 'index') of this item. + * The key identifies this entry's position in + * the SHAMap associative container. + */ inline uint256 const& STLedgerEntry::key() const { diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index fe5c611648..ad87d106c4 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -229,11 +229,6 @@ public: [[nodiscard]] AccountID getAccountID(SField const& field) const; - /** The account responsible for the fee and authorization: the delegate when - sfDelegate is present, otherwise the account. */ - [[nodiscard]] AccountID - getFeePayer() const; - [[nodiscard]] Blob getFieldVL(SField const& field) const; [[nodiscard]] STAmount const& @@ -252,103 +247,112 @@ public: [[nodiscard]] STNumber const& getFieldNumber(SField const& field) const; - /** Get the value of a field. - @param A TypedField built from an SField value representing the desired - object field. In typical use, the TypedField will be implicitly - constructed. - @return The value of the specified field. - @throws STObject::FieldErr if the field is not present. - */ + /** + * Get the value of a field. + * @param A TypedField built from an SField value representing the desired + * object field. In typical use, the TypedField will be implicitly + * constructed. + * @return The value of the specified field. + * @throws STObject::FieldErr if the field is not present. + */ template T::value_type operator[](TypedField const& f) const; - /** Get the value of a field as a std::optional - - @param An OptionaledField built from an SField value representing the - desired object field. In typical use, the OptionaledField will be - constructed by using the ~ operator on an SField. - @return std::nullopt if the field is not present, else the value of - the specified field. - */ + /** + * Get the value of a field as a std::optional + * + * @param An OptionaledField built from an SField value representing the + * desired object field. In typical use, the OptionaledField will be + * constructed by using the ~ operator on an SField. + * @return std::nullopt if the field is not present, else the value of + * the specified field. + */ template std::optional> operator[](OptionaledField const& of) const; - /** Get a modifiable field value. - @param A TypedField built from an SField value representing the desired - object field. In typical use, the TypedField will be implicitly - constructed. - @return A modifiable reference to the value of the specified field. - @throws STObject::FieldErr if the field is not present. - */ + /** + * Get a modifiable field value. + * @param A TypedField built from an SField value representing the desired + * object field. In typical use, the TypedField will be implicitly + * constructed. + * @return A modifiable reference to the value of the specified field. + * @throws STObject::FieldErr if the field is not present. + */ template ValueProxy operator[](TypedField const& f); - /** Return a modifiable field value as std::optional - - @param An OptionaledField built from an SField value representing the - desired object field. In typical use, the OptionaledField will be - constructed by using the ~ operator on an SField. - @return Transparent proxy object to an `optional` holding a modifiable - reference to the value of the specified field. Returns - std::nullopt if the field is not present. - */ + /** + * Return a modifiable field value as std::optional + * + * @param An OptionaledField built from an SField value representing the + * desired object field. In typical use, the OptionaledField will be + * constructed by using the ~ operator on an SField. + * @return Transparent proxy object to an `optional` holding a modifiable + * reference to the value of the specified field. Returns + * std::nullopt if the field is not present. + */ template OptionalProxy operator[](OptionaledField const& of); - /** Get the value of a field. - @param A TypedField built from an SField value representing the desired - object field. In typical use, the TypedField will be implicitly - constructed. - @return The value of the specified field. - @throws STObject::FieldErr if the field is not present. - */ + /** + * Get the value of a field. + * @param A TypedField built from an SField value representing the desired + * object field. In typical use, the TypedField will be implicitly + * constructed. + * @return The value of the specified field. + * @throws STObject::FieldErr if the field is not present. + */ template [[nodiscard]] T::value_type at(TypedField const& f) const; - /** Get the value of a field as std::optional - - @param An OptionaledField built from an SField value representing the - desired object field. In typical use, the OptionaledField will be - constructed by using the ~ operator on an SField. - @return std::nullopt if the field is not present, else the value of - the specified field. - */ + /** + * Get the value of a field as std::optional + * + * @param An OptionaledField built from an SField value representing the + * desired object field. In typical use, the OptionaledField will be + * constructed by using the ~ operator on an SField. + * @return std::nullopt if the field is not present, else the value of + * the specified field. + */ template [[nodiscard]] std::optional> at(OptionaledField const& of) const; - /** Get a modifiable field value. - @param A TypedField built from an SField value representing the desired - object field. In typical use, the TypedField will be implicitly - constructed. - @return A modifiable reference to the value of the specified field. - @throws STObject::FieldErr if the field is not present. - */ + /** + * Get a modifiable field value. + * @param A TypedField built from an SField value representing the desired + * object field. In typical use, the TypedField will be implicitly + * constructed. + * @return A modifiable reference to the value of the specified field. + * @throws STObject::FieldErr if the field is not present. + */ template ValueProxy at(TypedField const& f); - /** Return a modifiable field value as std::optional - - @param An OptionaledField built from an SField value representing the - desired object field. In typical use, the OptionaledField will be - constructed by using the ~ operator on an SField. - @return Transparent proxy object to an `optional` holding a modifiable - reference to the value of the specified field. Returns - std::nullopt if the field is not present. - */ + /** + * Return a modifiable field value as std::optional + * + * @param An OptionaledField built from an SField value representing the + * desired object field. In typical use, the OptionaledField will be + * constructed by using the ~ operator on an SField. + * @return Transparent proxy object to an `optional` holding a modifiable + * reference to the value of the specified field. Returns + * std::nullopt if the field is not present. + */ template OptionalProxy at(OptionaledField const& of); - /** Set a field. - if the field already exists, it is replaced. - */ + /** + * Set a field. + * if the field already exists, it is replaced. + */ void set(std::unique_ptr v); @@ -503,8 +507,10 @@ public: value_type operator*() const; - /// Do not use operator->() unless the field is required, or you've checked - /// that it's set. + /** + * Do not use operator->() unless the field is required, or you've checked + * that it's set. + */ T const* operator->() const; @@ -604,17 +610,20 @@ public: OptionalProxy& operator=(OptionalProxy const&) = delete; - /** Returns `true` if the field is set. - - Fields with soeDEFAULT and set to the - default value will return `true` - */ + /** + * Returns `true` if the field is set. + * + * Fields with soeDEFAULT and set to the + * default value will return `true` + */ explicit operator bool() const noexcept; operator optional_type() const; - /** Explicit conversion to std::optional */ + /** + * Explicit conversion to std::optional + */ optional_type operator~() const; @@ -761,8 +770,10 @@ STObject::Proxy::operator*() const -> value_type return this->value(); } -/// Do not use operator->() unless the field is required, or you've checked that -/// it's set. +/** + * Do not use operator->() unless the field is required, or you've checked that + * it's set. + */ template T const* STObject::Proxy::operator->() const diff --git a/include/xrpl/protocol/STParsedJSON.h b/include/xrpl/protocol/STParsedJSON.h index 1eeecc8b9e..7189e0ec89 100644 --- a/include/xrpl/protocol/STParsedJSON.h +++ b/include/xrpl/protocol/STParsedJSON.h @@ -9,26 +9,33 @@ namespace xrpl { -/** Maximum JSON object nesting depth permitted during parsing. */ +/** + * Maximum JSON object nesting depth permitted during parsing. + */ inline constexpr std::size_t kMaxParsedJsonDepth = 64; -/** Maximum number of elements permitted in any JSON array field during parsing. - Requests exceeding this limit are rejected with an invalidParams error. */ +/** + * Maximum number of elements permitted in any JSON array field during parsing. + * Requests exceeding this limit are rejected with an invalidParams error. + */ inline constexpr std::size_t kMaxParsedJsonArraySize = 512; -/** Holds the serialized result of parsing an input JSON object. - This does validation and checking on the provided JSON. -*/ +/** + * Holds the serialized result of parsing an input JSON object. + * This does validation and checking on the provided JSON. + */ class STParsedJSONObject { public: - /** Parses and creates an STParsedJSON object. - The result of the parsing is stored in object and error. - Exceptions: - Does not throw. - @param name The name of the JSON field, used in diagnostics. - @param json The JSON-RPC to parse. - */ + /** + * Parses and creates an STParsedJSON object. + * The result of the parsing is stored in object and error. + * + * @note Does not throw. + * + * @param name The name of the JSON field, used in diagnostics. + * @param json The JSON-RPC to parse. + */ STParsedJSONObject(std::string const& name, json::Value const& json); STParsedJSONObject() = delete; @@ -37,10 +44,14 @@ public: operator=(STParsedJSONObject const&) = delete; ~STParsedJSONObject() = default; - /** The STObject if the parse was successful. */ + /** + * The STObject if the parse was successful. + */ std::optional object; - /** On failure, an appropriate set of error values. */ + /** + * On failure, an appropriate set of error values. + */ json::Value error; }; diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index f6b0fde7da..23f4e653c4 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -240,6 +240,9 @@ private: inline STPathElement::STPathElement() : type_(TypeNone), isOffer_(true) { + // hashValue_ is derived from the whole object, so it is computed in the body + // once every other member is initialized (as in the other constructors). + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) hashValue_ = getHash(*this); } @@ -315,6 +318,9 @@ inline STPathElement::STPathElement( assetID_.visit( [&](Currency const&) { type_ = type_ & (~Type::TypeMpt); }, [&](MPTID const&) { type_ = type_ & (~Type::TypeCurrency); }); + // hashValue_ must be computed after type_ is adjusted above, so this cannot + // be a member initializer. + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) hashValue_ = getHash(*this); } diff --git a/include/xrpl/protocol/STTakesAsset.h b/include/xrpl/protocol/STTakesAsset.h index 70bafd0e91..95667e4868 100644 --- a/include/xrpl/protocol/STTakesAsset.h +++ b/include/xrpl/protocol/STTakesAsset.h @@ -7,7 +7,8 @@ namespace xrpl { -/** Intermediate class for any STBase-derived class to store an Asset. +/** + * Intermediate class for any STBase-derived class to store an Asset. * * In the class definition, this class should be specified as a base class * _instead_ of STBase. @@ -41,7 +42,8 @@ STTakesAsset::associateAsset(Asset const& a) class STLedgerEntry; -/** Associate an Asset with all sMD_NeedsAsset fields in a ledger entry. +/** + * Associate an Asset with all sMD_NeedsAsset fields in a ledger entry. * * This function iterates over all fields in the given ledger entry. For each * field that is set and has the SField::sMD_NeedsAsset metadata flag, it calls @@ -54,7 +56,6 @@ class STLedgerEntry; * * @param sle The ledger entry whose fields will be updated. * @param asset The Asset to associate with the relevant fields. - * */ void associateAsset(STLedgerEntry& sle, Asset const& asset); diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b36207bf61..d329d42eee 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -55,12 +55,13 @@ public: explicit STTx(SerialIter&& sit); explicit STTx(STObject&& object); - /** Constructs a transaction. - - The returned transaction will have the specified type and - any fields that the callback function adds to the object - that's passed in. - */ + /** + * Constructs a transaction. + * + * The returned transaction will have the specified type and + * any fields that the callback function adds to the object + * that's passed in. + */ STTx(TxType type, std::function assembler); // STObject functions. @@ -92,7 +93,9 @@ public: [[nodiscard]] SeqProxy getSeqProxy() const; - /** Returns the first non-zero value of (Sequence, TicketSequence). */ + /** + * Returns the first non-zero value of (Sequence, TicketSequence). + */ [[nodiscard]] std::uint32_t getSeqValue() const; @@ -114,10 +117,11 @@ public: SecretKey const& secretKey, std::optional> signatureTarget = {}); - /** Check the signature. - @param rules The current ledger rules. - @return `true` if valid signature. If invalid, the error message string. - */ + /** + * Check the signature. + * @param rules The current ledger rules. + * @return `true` if valid signature. If invalid, the error message string. + */ [[nodiscard]] std::expected checkSign(Rules const& rules) const; @@ -138,16 +142,37 @@ public: TxnSql status, std::string const& escapedMetaData) const; - [[nodiscard]] std::vector const& + /** + * The IDs of the inner transactions of a Batch. + */ + [[nodiscard]] std::vector getBatchTransactionIDs() const; + /** + * The inner transactions of a Batch, built and validated at construction. + * Always seated for Batch STTx instances (construction throws if oversized). + */ + [[nodiscard]] std::vector> const& + getBatchTransactions() const; + + /** + * The account responsible for the authorization: the delegate when + * sfDelegate is present, otherwise the account. + */ + [[nodiscard]] AccountID + getInitiator() const; + + [[nodiscard]] AccountID + getFeePayerID() const; + private: - /** Check the signature. - @param rules The current ledger rules. - @param sigObject Reference to object that contains the signature fields. - Will be *this more often than not. - @return `true` if valid signature. If invalid, the error message string. - */ + /** + * Check the signature. + * @param rules The current ledger rules. + * @param sigObject Reference to object that contains the signature fields. + * Will be *this more often than not. + * @return `true` if valid signature. If invalid, the error message string. + */ [[nodiscard]] std::expected checkSign(Rules const& rules, STObject const& sigObject) const; @@ -158,13 +183,16 @@ private: checkMultiSign(Rules const& rules, STObject const& sigObject) const; [[nodiscard]] std::expected - checkBatchSingleSign(STObject const& batchSigner) const; + checkBatchSingleSign(STObject const& batchSigner, std::vector const& txIds) const; [[nodiscard]] std::expected - checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const; + checkBatchMultiSign( + STObject const& batchSigner, + Rules const& rules, + std::vector const& txIds) const; void - buildBatchTxnIds(); + buildBatchTxns(); STBase* copy(std::size_t n, void* buf) const override; @@ -172,23 +200,26 @@ private: move(std::size_t n, void* buf) override; friend class detail::STVar; - std::optional> batchTxnIds_; + std::optional>> batchTxns_; }; bool -passesLocalChecks(STObject const& st, std::string&); +passesLocalChecks(STTx const& tx, std::string&); -/** Sterilize a transaction. - - The transaction is serialized and then deserialized, - ensuring that all equivalent transactions are in canonical - form. This also ensures that program metadata such as - the transaction's digest, are all computed. -*/ +/** + * Sterilize a transaction. + * + * The transaction is serialized and then deserialized, + * ensuring that all equivalent transactions are in canonical + * form. This also ensures that program metadata such as + * the transaction's digest, are all computed. + */ std::shared_ptr sterilize(STTx const& stx); -/** Check whether a transaction is a pseudo-transaction */ +/** + * Check whether a transaction is a pseudo-transaction + */ bool isPseudoTx(STObject const& tx); diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h index 67a6594419..444fdfa600 100644 --- a/include/xrpl/protocol/STValidation.h +++ b/include/xrpl/protocol/STValidation.h @@ -54,30 +54,32 @@ class STValidation final : public STObject, public CountedObject NetClock::time_point seenTime_; public: - /** Construct a STValidation from a peer from serialized data. - - @param sit Iterator over serialized data - @param lookupNodeID Invocable with signature - NodeID(PublicKey const&) - used to find the Node ID based on the public key - that signed the validation. For manifest based - validators, this should be the NodeID of the master - public key. - @param checkSignature Whether to verify the data was signed properly - - @note Throws if the object is not valid - */ + /** + * Construct a STValidation from a peer from serialized data. + * + * @param sit Iterator over serialized data + * @param lookupNodeID Invocable with signature + * NodeID(PublicKey const&) + * used to find the Node ID based on the public key + * that signed the validation. For manifest based + * validators, this should be the NodeID of the master + * public key. + * @param checkSignature Whether to verify the data was signed properly + * + * @note Throws if the object is not valid + */ template STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature); - /** Construct, sign and trust a new STValidation issued by this node. - - @param signTime When the validation is signed - @param publicKey The current signing public key - @param secretKey The current signing secret key - @param nodeID ID corresponding to node's public master key - @param f callback function to "fill" the validation with necessary data - */ + /** + * Construct, sign and trust a new STValidation issued by this node. + * + * @param signTime When the validation is signed + * @param publicKey The current signing public key + * @param secretKey The current signing secret key + * @param nodeID ID corresponding to node's public master key + * @param f callback function to "fill" the validation with necessary data + */ template STValidation( NetClock::time_point signTime, @@ -183,14 +185,15 @@ STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool ch XRPL_ASSERT(nodeID_.isNonZero(), "xrpl::STValidation::STValidation(SerialIter) : nonzero node"); } -/** Construct, sign and trust a new STValidation issued by this node. - - @param signTime When the validation is signed - @param publicKey The current signing public key - @param secretKey The current signing secret key - @param nodeID ID corresponding to node's public master key - @param f callback function to "fill" the validation with necessary data -*/ +/** + * Construct, sign and trust a new STValidation issued by this node. + * + * @param signTime When the validation is signed + * @param publicKey The current signing public key + * @param secretKey The current signing secret key + * @param nodeID ID corresponding to node's public master key + * @param f callback function to "fill" the validation with necessary data + */ template STValidation::STValidation( NetClock::time_point signTime, diff --git a/include/xrpl/protocol/STVector256.h b/include/xrpl/protocol/STVector256.h index 46a1abc713..5a8418fef5 100644 --- a/include/xrpl/protocol/STVector256.h +++ b/include/xrpl/protocol/STVector256.h @@ -50,7 +50,9 @@ public: void setValue(STVector256 const& v); - /** Retrieve a copy of the vector we contain */ + /** + * Retrieve a copy of the vector we contain + */ explicit operator std::vector() const; @@ -138,7 +140,9 @@ STVector256::setValue(STVector256 const& v) value_ = v.value_; } -/** Retrieve a copy of the vector we contain */ +/** + * Retrieve a copy of the vector we contain + */ inline STVector256:: operator std::vector() const { diff --git a/include/xrpl/protocol/SecretKey.h b/include/xrpl/protocol/SecretKey.h index 8a0d917ab4..6d353acac0 100644 --- a/include/xrpl/protocol/SecretKey.h +++ b/include/xrpl/protocol/SecretKey.h @@ -17,7 +17,9 @@ namespace xrpl { -/** A secret key. */ +/** + * A secret key. + */ class SecretKey { public: @@ -56,11 +58,12 @@ public: return sizeof(buf_); } - /** Convert the secret key to a hexadecimal string. - - @note The operator<< function is deliberately omitted - to avoid accidental exposure of secret key material. - */ + /** + * Convert the secret key to a hexadecimal string. + * + * @note The operator<< function is deliberately omitted + * to avoid accidental exposure of secret key material. + */ [[nodiscard]] std::string toString() const; @@ -97,7 +100,9 @@ operator!=(SecretKey const& lhs, SecretKey const& rhs) = delete; //------------------------------------------------------------------------------ -/** Parse a secret key */ +/** + * Parse a secret key + */ template <> std::optional parseBase58(TokenType type, std::string const& s); @@ -108,38 +113,48 @@ toBase58(TokenType type, SecretKey const& sk) return encodeBase58Token(type, sk.data(), sk.size()); } -/** Create a secret key using secure random numbers. */ +/** + * Create a secret key using secure random numbers. + */ SecretKey randomSecretKey(); -/** Generate a new secret key deterministically. */ +/** + * Generate a new secret key deterministically. + */ SecretKey generateSecretKey(KeyType type, Seed const& seed); -/** Derive the public key from a secret key. */ +/** + * Derive the public key from a secret key. + */ PublicKey derivePublicKey(KeyType type, SecretKey const& sk); -/** Generate a key pair deterministically. - - This algorithm is specific to the XRPL: - - For secp256k1 key pairs, the seed is converted - to a Generator and used to compute the key pair - corresponding to ordinal 0 for the generator. -*/ +/** + * Generate a key pair deterministically. + * + * This algorithm is specific to the XRPL: + * + * For secp256k1 key pairs, the seed is converted + * to a Generator and used to compute the key pair + * corresponding to ordinal 0 for the generator. + */ std::pair generateKeyPair(KeyType type, Seed const& seed); -/** Create a key pair using secure random numbers. */ +/** + * Create a key pair using secure random numbers. + */ std::pair randomKeyPair(KeyType type); -/** Generate a signature for a message digest. - This can only be used with secp256k1 since Ed25519's - security properties come, in part, from how the message - is hashed. -*/ +/** + * Generate a signature for a message digest. + * This can only be used with secp256k1 since Ed25519's + * security properties come, in part, from how the message + * is hashed. + */ /** @{ */ Buffer signDigest(PublicKey const& pk, SecretKey const& sk, uint256 const& digest); @@ -151,10 +166,11 @@ signDigest(KeyType type, SecretKey const& sk, uint256 const& digest) } /** @} */ -/** Generate a signature for a message. - With secp256k1 signatures, the data is first hashed with - SHA512-Half, and the resulting digest is signed. -*/ +/** + * Generate a signature for a message. + * With secp256k1 signatures, the data is first hashed with + * SHA512-Half, and the resulting digest is signed. + */ /** @{ */ Buffer sign(PublicKey const& pk, SecretKey const& sk, Slice const& message); diff --git a/include/xrpl/protocol/Seed.h b/include/xrpl/protocol/Seed.h index a669f52079..4ccdd6707f 100644 --- a/include/xrpl/protocol/Seed.h +++ b/include/xrpl/protocol/Seed.h @@ -12,7 +12,9 @@ namespace xrpl { -/** Seeds are used to generate deterministic secret keys. */ +/** + * Seeds are used to generate deterministic secret keys. + */ class Seed { private: @@ -27,12 +29,15 @@ public: Seed& operator=(Seed const&) = default; - /** Destroy the seed. - The buffer will first be securely erased. - */ + /** + * Destroy the seed. + * The buffer will first be securely erased. + */ ~Seed(); - /** Construct a seed */ + /** + * Construct a seed + */ /** @{ */ explicit Seed(Slice const& slice); explicit Seed(uint128 const& seed); @@ -77,42 +82,52 @@ public: //------------------------------------------------------------------------------ -/** Create a seed using secure random numbers. */ +/** + * Create a seed using secure random numbers. + */ Seed randomSeed(); -/** Generate a seed deterministically. - - The algorithm is specific to the XRPL: - - The seed is calculated as the first 128 bits - of the SHA512-Half of the string text excluding - any terminating null. - - @note This will not attempt to determine the format of - the string (e.g. hex or base58). -*/ +/** + * Generate a seed deterministically. + * + * The algorithm is specific to the XRPL: + * + * The seed is calculated as the first 128 bits + * of the SHA512-Half of the string text excluding + * any terminating null. + * + * @note This will not attempt to determine the format of + * the string (e.g. hex or base58). + */ Seed generateSeed(std::string const& passPhrase); -/** Parse a Base58 encoded string into a seed */ +/** + * Parse a Base58 encoded string into a seed + */ template <> std::optional parseBase58(std::string const& s); -/** Attempt to parse a string as a seed. - - @param str the string to parse - @param rfc1751 true if we should attempt RFC1751 style parsing (deprecated) - * */ +/** + * Attempt to parse a string as a seed. + * + * @param str the string to parse + * @param rfc1751 true if we should attempt RFC1751 style parsing (deprecated) + */ std::optional parseGenericSeed(std::string const& str, bool rfc1751 = true); -/** Encode a Seed in RFC1751 format */ +/** + * Encode a Seed in RFC1751 format + */ std::string seedAs1751(Seed const& seed); -/** Format a seed as a Base58 string */ +/** + * Format a seed as a Base58 string + */ inline std::string toBase58(Seed const& seed) { diff --git a/include/xrpl/protocol/SeqProxy.h b/include/xrpl/protocol/SeqProxy.h index be040cceec..e6a97be0e7 100644 --- a/include/xrpl/protocol/SeqProxy.h +++ b/include/xrpl/protocol/SeqProxy.h @@ -5,33 +5,34 @@ namespace xrpl { -/** A type that represents either a sequence value or a ticket value. - - We use the value() of a SeqProxy in places where a sequence was used - before. An example of this is the sequence of an Offer stored in the - ledger. We do the same thing with the in-ledger identifier of a - Check, Payment Channel, and Escrow. - - Why is this safe? If we use the SeqProxy::value(), how do we know that - each ledger entry will be unique? - - There are two components that make this safe: - - 1. A "TicketCreate" transaction carefully avoids creating a ticket - that corresponds with an already used Sequence or Ticket value. - The transactor does this by referring to the account root's - sequence number. Creating the ticket advances the account root's - sequence number so the same ticket (or sequence) value cannot be - used again. - - 2. When a "TicketCreate" transaction creates a batch of tickets it advances - the account root sequence to one past the largest created ticket. - - Therefore all tickets in a batch other than the first may never have - the same value as a sequence on that same account. And since a ticket - may only be used once there will never be any duplicates within this - account. -*/ +/** + * A type that represents either a sequence value or a ticket value. + * + * We use the value() of a SeqProxy in places where a sequence was used + * before. An example of this is the sequence of an Offer stored in the + * ledger. We do the same thing with the in-ledger identifier of a + * Check, Payment Channel, and Escrow. + * + * Why is this safe? If we use the SeqProxy::value(), how do we know that + * each ledger entry will be unique? + * + * There are two components that make this safe: + * + * 1. A "TicketCreate" transaction carefully avoids creating a ticket + * that corresponds with an already used Sequence or Ticket value. + * The transactor does this by referring to the account root's + * sequence number. Creating the ticket advances the account root's + * sequence number so the same ticket (or sequence) value cannot be + * used again. + * + * 2. When a "TicketCreate" transaction creates a batch of tickets it advances + * the account root sequence to one past the largest created ticket. + * + * Therefore all tickets in a batch other than the first may never have + * the same value as a sequence on that same account. And since a ticket + * may only be used once there will never be any duplicates within this + * account. + */ class SeqProxy { public: @@ -51,7 +52,9 @@ public: SeqProxy& operator=(SeqProxy const& other) = default; - /** Factory function to return a sequence-based SeqProxy */ + /** + * Factory function to return a sequence-based SeqProxy + */ static constexpr SeqProxy sequence(std::uint32_t v) { diff --git a/include/xrpl/protocol/Sign.h b/include/xrpl/protocol/Sign.h index 18f085352d..fad2c35c9e 100644 --- a/include/xrpl/protocol/Sign.h +++ b/include/xrpl/protocol/Sign.h @@ -11,17 +11,18 @@ namespace xrpl { -/** Sign an STObject - - @param st Object to sign - @param prefix Prefix to insert before serialized object when hashing - @param type Signing key type used to derive public key - @param sk Signing secret key - @param sigField Field in which to store the signature on the object. - If not specified the value defaults to `sfSignature`. - - @note If a signature already exists, it is overwritten. -*/ +/** + * Sign an STObject + * + * @param st Object to sign + * @param prefix Prefix to insert before serialized object when hashing + * @param type Signing key type used to derive public key + * @param sk Signing secret key + * @param sigField Field in which to store the signature on the object. + * If not specified the value defaults to `sfSignature`. + * + * @note If a signature already exists, it is overwritten. + */ void sign( STObject& st, @@ -30,14 +31,15 @@ sign( SecretKey const& sk, SF_VL const& sigField = sfSignature); -/** Returns `true` if STObject contains valid signature - - @param st Signed object - @param prefix Prefix inserted before serialized object when hashing - @param pk Public key for verifying signature - @param sigField Object's field containing the signature. - If not specified the value defaults to `sfSignature`. -*/ +/** + * Returns `true` if STObject contains valid signature + * + * @param st Signed object + * @param prefix Prefix inserted before serialized object when hashing + * @param pk Public key for verifying signature + * @param sigField Object's field containing the signature. + * If not specified the value defaults to `sfSignature`. + */ bool verify( STObject const& st, @@ -45,22 +47,25 @@ verify( PublicKey const& pk, SF_VL const& sigField = sfSignature); -/** Return a Serializer suitable for computing a multisigning TxnSignature. */ +/** + * Return a Serializer suitable for computing a multisigning TxnSignature. + */ Serializer buildMultiSigningData(STObject const& obj, AccountID const& signingID); -/** Break the multi-signing hash computation into 2 parts for optimization. - - We can optimize verifying multiple multisignatures by splitting the - data building into two parts; - o A large part that is shared by all of the computations. - o A small part that is unique to each signer in the multisignature. - - The following methods support that optimization: - 1. startMultiSigningData provides the large part which can be shared. - 2. finishMultiSigningData caps the passed in serializer with each - signer's unique data. -*/ +/** + * Break the multi-signing hash computation into 2 parts for optimization. + * + * We can optimize verifying multiple multisignatures by splitting the + * data building into two parts; + * o A large part that is shared by all of the computations. + * o A small part that is unique to each signer in the multisignature. + * + * The following methods support that optimization: + * 1. startMultiSigningData provides the large part which can be shared. + * 2. finishMultiSigningData caps the passed in serializer with each + * signer's unique data. + */ Serializer startMultiSigningData(STObject const& obj); diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index b31dd0cd42..6ca36c8d9a 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -21,22 +21,30 @@ systemName() return kName; } -/** Configure the native currency. */ +/** + * Configure the native currency. + */ -/** Number of drops in the genesis account. */ +/** + * Number of drops in the genesis account. + */ constexpr XRPAmount kInitialXrp{100'000'000'000 * kDropsPerXrp}; static_assert(kInitialXrp.drops() == 100'000'000'000'000'000); static_assert(Number::kMaxRep >= kInitialXrp.drops()); -/** Returns true if the amount does not exceed the initial XRP in existence. */ +/** + * Returns true if the amount does not exceed the initial XRP in existence. + */ inline bool isLegalAmount(XRPAmount const& amount) { return amount <= kInitialXrp; } -/** Returns true if the absolute value of the amount does not exceed the initial - * XRP in existence. */ +/** + * Returns true if the absolute value of the amount does not exceed the initial + * XRP in existence. + */ inline bool isLegalAmountSigned(XRPAmount const& amount) { @@ -51,20 +59,30 @@ systemCurrencyCode() return kCode; } -/** The XRP ledger network's earliest allowed sequence */ +/** + * The XRP ledger network's earliest allowed sequence + */ static constexpr std::uint32_t kXrpLedgerEarliestSeq{32570u}; -/** The XRP Ledger mainnet's earliest ledger with a FeeSettings object. Only - * used in asserts and tests. */ +/** + * The XRP Ledger mainnet's earliest ledger with a FeeSettings object. Only + * used in asserts and tests. + */ static constexpr std::uint32_t kXrpLedgerEarliestFees{562177u}; -/** The minimum amount of support an amendment should have. */ +/** + * The minimum amount of support an amendment should have. + */ constexpr std::ratio<80, 100> kAmendmentMajorityCalcThreshold; -/** The minimum amount of time an amendment must hold a majority */ +/** + * The minimum amount of time an amendment must hold a majority + */ constexpr std::chrono::seconds const kDefaultAmendmentMajorityTime = weeks{2}; } // namespace xrpl -/** Default peer port (IANA registered) */ +/** + * Default peer port (IANA registered) + */ inline constexpr std::uint16_t kDefaultPeerPort{2459}; diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 54b081f358..730d021254 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -225,6 +225,7 @@ enum TERcodes : TERUnderlyingType { // create a pseudo-account terNO_DELEGATE_PERMISSION, // Delegate does not have permission terLOCKED, // MPT is locked + terNO_PERMISSION, // No permission but retry }; //------------------------------------------------------------------------------ @@ -368,6 +369,7 @@ enum TECcodes : TERUnderlyingType { // reclaimed after those networks reset. tecNO_DELEGATE_PERMISSION = 198, tecBAD_PROOF = 199, + tecNO_SPONSOR_PERMISSION = 200, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 461afd24e7..0afdebb898 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -12,29 +12,30 @@ namespace xrpl { -/** Transaction flags. - - These flags are specified in a transaction's 'Flags' field and modify - the behavior of that transaction. - - There are two types of flags: - - (1) Universal flags: these are flags which apply to, and are interpreted the same way by, - all transactions, except, perhaps, to special pseudo-transactions. - - (2) Tx-Specific flags: these are flags which are interpreted according to the type of the - transaction being executed. That is, the same numerical flag value may have different - effects, depending on the transaction being executed. - - @note The universal transaction flags occupy the high-order 8 bits. - The tx-specific flags occupy the remaining 24 bits. - - @warning Transaction flags form part of the protocol. - **Changing them should be avoided because without special handling, this will result in - a hard fork.** - - @ingroup protocol -*/ +/** + * Transaction flags. + * + * These flags are specified in a transaction's 'Flags' field and modify + * the behavior of that transaction. + * + * There are two types of flags: + * + * (1) Universal flags: these are flags which apply to, and are interpreted the same way by, + * all transactions, except, perhaps, to special pseudo-transactions. + * + * (2) Tx-Specific flags: these are flags which are interpreted according to the type of the + * transaction being executed. That is, the same numerical flag value may have different + * effects, depending on the transaction being executed. + * + * @note The universal transaction flags occupy the high-order 8 bits. + * The tx-specific flags occupy the remaining 24 bits. + * + * @warning Transaction flags form part of the protocol. + * **Changing them should be avoided because without special handling, this will result in + * a hard fork.** + * + * @ingroup protocol + */ using FlagValue = std::uint32_t; @@ -102,7 +103,8 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TRANSACTION(Payment, \ TF_FLAG(tfNoRippleDirect, 0x00010000) \ TF_FLAG(tfPartialPayment, 0x00020000) \ - TF_FLAG(tfLimitQuality, 0x00040000), \ + TF_FLAG(tfLimitQuality, 0x00040000) \ + TF_FLAG(tfSponsorCreatedAccount, 0x00080000), \ MASK_ADJ(0)) \ \ TRANSACTION(TrustSet, \ @@ -141,7 +143,7 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfMPTCanTrade, lsfMPTCanTrade) \ TF_FLAG(tfMPTCanTransfer, lsfMPTCanTransfer) \ TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback) \ - TF_FLAG(tfMPTCanHoldConfidentialBalance, lsfMPTCanHoldConfidentialBalance), \ + TF_FLAG(tfMPTCanHoldConfidentialBalance, lsfMPTCanHoldConfidentialBalance), \ MASK_ADJ(0)) \ \ TRANSACTION(MPTokenAuthorize, \ @@ -215,6 +217,20 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfLoanDefault, 0x00010000) \ TF_FLAG(tfLoanImpair, 0x00020000) \ TF_FLAG(tfLoanUnimpair, 0x00040000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(SponsorshipSet, \ + TF_FLAG(tfSponsorshipSetRequireSignForFee, 0x00010000) \ + TF_FLAG(tfSponsorshipClearRequireSignForFee, 0x00020000) \ + TF_FLAG(tfSponsorshipSetRequireSignForReserve, 0x00040000) \ + TF_FLAG(tfSponsorshipClearRequireSignForReserve, 0x00080000) \ + TF_FLAG(tfDeleteObject, 0x00100000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(SponsorshipTransfer, \ + TF_FLAG(tfSponsorshipEnd, 0x00010000) \ + TF_FLAG(tfSponsorshipCreate, 0x00020000) \ + TF_FLAG(tfSponsorshipReassign, 0x00040000), \ MASK_ADJ(0)) // clang-format on @@ -444,6 +460,12 @@ getAsfFlagMap() #pragma pop_macro("ACCOUNTSET_FLAG_TO_MAP") #pragma pop_macro("ACCOUNTSET_FLAGS") +// Sponsor flags (spf) + +inline constexpr FlagValue spfSponsorFee = 1; +inline constexpr FlagValue spfSponsorReserve = 2; +inline constexpr FlagValue spfSponsorFlagMask = ~(spfSponsorFee | spfSponsorReserve); + } // namespace xrpl // NOLINTEND(readability-identifier-naming) diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index 36eb6d0889..8fb32c93cb 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -8,34 +8,36 @@ namespace xrpl { -/** Transaction type identifiers. - - These are part of the binary message format. - - @ingroup protocol -*/ -/** Transaction type identifiers - - Each ledger object requires a unique type identifier, which is stored - within the object itself; this makes it possible to iterate the entire - ledger and determine each object's type and verify that the object you - retrieved from a given hash matches the expected type. - - @warning Since these values are included in transactions, which are signed - objects, and used by the code to determine the type of transaction - being invoked, they are part of the protocol. **Changing them - should be avoided because without special handling, this will - result in a hard fork.** - - @note When retiring types, the specific values should not be removed but - should be marked as [[deprecated]]. This is to avoid accidental - reuse of identifiers. - - @todo The C++ language does not enable checking for duplicate values - here. If it becomes possible then we should do this. - - @ingroup protocol -*/ +/** + * Transaction type identifiers. + * + * These are part of the binary message format. + * + * @ingroup protocol + */ +/** + * Transaction type identifiers + * + * Each ledger object requires a unique type identifier, which is stored + * within the object itself; this makes it possible to iterate the entire + * ledger and determine each object's type and verify that the object you + * retrieved from a given hash matches the expected type. + * + * @warning Since these values are included in transactions, which are signed + * objects, and used by the code to determine the type of transaction + * being invoked, they are part of the protocol. **Changing them + * should be avoided because without special handling, this will + * result in a hard fork.** + * + * @note When retiring types, the specific values should not be removed but + * should be marked as [[deprecated]]. This is to avoid accidental + * reuse of identifiers. + * + * @todo The C++ language does not enable checking for duplicate values + * here. If it becomes possible then we should do this. + * + * @ingroup protocol + */ // clang-format off // Protocol-critical, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) @@ -52,28 +54,38 @@ enum TxType : std::uint16_t #undef TRANSACTION #pragma pop_macro("TRANSACTION") - /** This transaction type is deprecated; it is retained for historical purposes. */ + /** + * This transaction type is deprecated; it is retained for historical purposes. + */ TtNicknameSet [[deprecated("This transaction type is not supported and should not be used.")]] = 6, - /** This transaction type is deprecated; it is retained for historical purposes. */ + /** + * This transaction type is deprecated; it is retained for historical purposes. + */ TtContract [[deprecated("This transaction type is not supported and should not be used.")]] = 9, - /** This identifier was never used, but the slot is reserved for historical purposes. */ + /** + * This identifier was never used, but the slot is reserved for historical purposes. + */ TtSpinalTap [[deprecated("This transaction type is not supported and should not be used.")]] = 11, - /** This transaction type installs a hook. */ + /** + * This transaction type installs a hook. + */ TtHookSet [[maybe_unused]] = 22, }; // clang-format on -/** Manages the list of known transaction formats. +/** + * Manages the list of known transaction formats. */ class TxFormats : public KnownFormats { private: - /** Create the object. - This will load the object with all the known transaction formats. - */ + /** + * Create the object. + * This will load the object with all the known transaction formats. + */ TxFormats(); public: diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 813f1b1615..88652afba9 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -60,7 +60,9 @@ public: STObject& getAffectedNode(uint256 const&); - /** Return a list of accounts affected by this transaction */ + /** + * Return a list of accounts affected by this transaction + */ [[nodiscard]] boost::container::flat_set getAffectedAccounts() const; diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index 1a3cb96691..25b0d1ffc9 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -30,34 +30,50 @@ public: } // namespace detail -/** Directory is an index into the directory of offer books. - The last 64 bits of this are the quality. */ +/** + * Directory is an index into the directory of offer books. + * The last 64 bits of this are the quality. + */ using Directory = BaseUInt<256, detail::DirectoryTag>; -/** Currency is a hash representing a specific currency. */ +/** + * Currency is a hash representing a specific currency. + */ using Currency = BaseUInt<160, detail::CurrencyTag>; -/** NodeID is a 160-bit hash representing one node. */ +/** + * NodeID is a 160-bit hash representing one node. + */ using NodeID = BaseUInt<160, detail::NodeIDTag>; -/** MPTID is a 192-bit value representing MPT Issuance ID, +/** + * MPTID is a 192-bit value representing MPT Issuance ID, * which is a concatenation of a 32-bit sequence (big endian) - * and a 160-bit account */ + * and a 160-bit account + */ using MPTID = BaseUInt<192>; -/** Domain is a 256-bit hash representing a specific domain. */ +/** + * Domain is a 256-bit hash representing a specific domain. + */ using Domain = BaseUInt<256>; -/** XRP currency. */ +/** + * XRP currency. + */ Currency const& xrpCurrency(); -/** A placeholder for empty currencies. */ +/** + * A placeholder for empty currencies. + */ Currency const& noCurrency(); -/** We deliberately disallow the currency that looks like "XRP" because too - many people were using it instead of the correct XRP currency. */ +/** + * We deliberately disallow the currency that looks like "XRP" because too + * many people were using it instead of the correct XRP currency. + */ Currency const& badCurrency(); @@ -67,26 +83,30 @@ isXRP(Currency const& c) return c == beast::kZero; } -/** Returns "", "XRP", or three letter ISO code. */ +/** + * Returns "", "XRP", or three letter ISO code. + */ std::string to_string(Currency const& c); -/** Tries to convert a string to a Currency, returns true on success. - - @note This function will return success if the resulting currency is - badCurrency(). This legacy behavior is unfortunate; changing this - will require very careful checking everywhere and may mean having - to rewrite some unit test code. -*/ +/** + * Tries to convert a string to a Currency, returns true on success. + * + * @note This function will return success if the resulting currency is + * badCurrency(). This legacy behavior is unfortunate; changing this + * will require very careful checking everywhere and may mean having + * to rewrite some unit test code. + */ bool toCurrency(Currency&, std::string const&); -/** Tries to convert a string to a Currency, returns noCurrency() on failure. - - @note This function can return badCurrency(). This legacy behavior is - unfortunate; changing this will require very careful checking - everywhere and may mean having to rewrite some unit test code. -*/ +/** + * Tries to convert a string to a Currency, returns noCurrency() on failure. + * + * @note This function can return badCurrency(). This legacy behavior is + * unfortunate; changing this will require very careful checking + * everywhere and may mean having to rewrite some unit test code. + */ Currency toCurrency(std::string const&); diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index 8b418704d6..169ee2c543 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -20,18 +20,26 @@ namespace xrpl { namespace unit { -/** "drops" are the smallest divisible amount of XRP. This is what most - of the code uses. */ +/** + * "drops" are the smallest divisible amount of XRP. This is what most + * of the code uses. + */ struct dropTag; -/** "fee levels" are used by the transaction queue to compare the relative - cost of transactions that require different levels of effort to process. - See also: src/xrpld/app/misc/FeeEscalation.md#fee-level */ +/** + * "fee levels" are used by the transaction queue to compare the relative + * cost of transactions that require different levels of effort to process. + * See also: src/xrpld/app/misc/FeeEscalation.md#fee-level + */ struct feelevelTag; -/** unitless values are plain scalars wrapped in a ValueUnit. They are - used for calculations in this header. */ +/** + * unitless values are plain scalars wrapped in a ValueUnit. They are + * used for calculations in this header. + */ struct unitlessTag; -/** Units to represent basis points (bips) and 1/10 basis points */ +/** + * Units to represent basis points (bips) and 1/10 basis points + */ class BipsTag; class TenthBipsTag; @@ -42,13 +50,14 @@ template concept Valid = std::is_class_v && std::is_object_v && std::is_object_v; -/** `Usable` is checked to ensure that only values with - known valid type tags can be used (sometimes transparently) in - non-unit contexts. At the time of implementation, this includes - all known tags, but more may be added in the future, and they - should not be added automatically unless determined to be - appropriate. -*/ +/** + * `Usable` is checked to ensure that only values with + * known valid type tags can be used (sometimes transparently) in + * non-unit contexts. At the time of implementation, this includes + * all known tags, but more may be added in the future, and they + * should not be added automatically unless determined to be + * appropriate. + */ template concept Usable = Valid && (std::is_same_v || @@ -115,9 +124,11 @@ public: return *this; } - /** Instances with the same unit, and a type that is - "safe" to convert to this one can be converted - implicitly */ + /** + * Instances with the same unit, and a type that is + * "safe" to convert to this one can be converted + * implicitly + */ template Other> constexpr ValueUnit(ValueUnit const& value) requires SafeToCast @@ -260,14 +271,18 @@ public: return value_ < other.value_; } - /** Returns true if the amount is not zero */ + /** + * Returns true if the amount is not zero + */ explicit constexpr operator bool() const noexcept { return value_ != 0; } - /** Return the sign of the amount */ + /** + * Return the sign of the amount + */ [[nodiscard]] constexpr int signum() const noexcept { @@ -276,7 +291,9 @@ public: return value_ ? 1 : 0; } - /** Returns the number of drops */ + /** + * Returns the number of drops + */ // TODO: Move this to a new class, maybe with the old "TaggedFee" name [[nodiscard]] constexpr value_type fee() const @@ -319,10 +336,11 @@ public: } } - /** Returns the underlying value. Code SHOULD NOT call this - function unless the type has been abstracted away, - e.g. in a templated function. - */ + /** + * Returns the underlying value. Code SHOULD NOT call this + * function unless the type has been abstracted away, + * e.g. in a templated function. + */ [[nodiscard]] constexpr value_type value() const { diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index 3190980ecb..26e10929df 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -138,7 +138,9 @@ public: return drops_ < other.drops_; } - /** Returns true if the amount is not zero */ + /** + * Returns true if the amount is not zero + */ explicit constexpr operator bool() const noexcept { @@ -150,7 +152,9 @@ public: return drops(); } - /** Return the sign of the amount */ + /** + * Return the sign of the amount + */ [[nodiscard]] constexpr int signum() const noexcept { @@ -159,7 +163,9 @@ public: return (drops_ != 0) ? 1 : 0; } - /** Returns the number of drops */ + /** + * Returns the number of drops + */ [[nodiscard]] constexpr value_type drops() const { @@ -217,10 +223,11 @@ public: return static_cast(drops_); } - /** Returns the underlying value. Code SHOULD NOT call this - function unless the type has been abstracted away, - e.g. in a templated function. - */ + /** + * Returns the underlying value. Code SHOULD NOT call this + * function unless the type has been abstracted away, + * e.g. in a templated function. + */ [[nodiscard]] constexpr value_type value() const { @@ -241,7 +248,9 @@ public: } }; -/** Number of drops per 1 XRP */ +/** + * Number of drops per 1 XRP + */ constexpr XRPAmount kDropsPerXrp{1'000'000}; constexpr double diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index 6853166174..12026f3d09 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -50,14 +50,13 @@ public: STVar& operator=(STVar&& rhs); - STVar(STBase&& t) // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) + // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) + STVar(STBase&& t) : p_(t.move(kMaxSize, &d_)) { - p_ = t.move(kMaxSize, &d_); } - STVar(STBase const& t) + STVar(STBase const& t) : p_(t.copy(kMaxSize, &d_)) { - p_ = t.copy(kMaxSize, &d_); } STVar(DefaultObjectT, SField const& name); @@ -121,7 +120,8 @@ private: } } - /** Construct requested Serializable Type according to id. + /** + * Construct requested Serializable Type according to id. * The variadic args are: (SField), or (SerialIter, SField). * depth is ignored in former case. */ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index fa1b1fba4a..e7ddc1844e 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialTransfer, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 2f403708c3..90810e06d2 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -127,29 +127,32 @@ LEDGER_ENTRY(ltTICKET, 0x0054, Ticket, ticket, ({ \sa keylet::account */ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ - {sfAccount, SoeRequired}, - {sfSequence, SoeRequired}, - {sfBalance, SoeRequired}, - {sfOwnerCount, SoeRequired}, - {sfPreviousTxnID, SoeRequired}, - {sfPreviousTxnLgrSeq, SoeRequired}, - {sfAccountTxnID, SoeOptional}, - {sfRegularKey, SoeOptional}, - {sfEmailHash, SoeOptional}, - {sfWalletLocator, SoeOptional}, - {sfWalletSize, SoeOptional}, - {sfMessageKey, SoeOptional}, - {sfTransferRate, SoeOptional}, - {sfDomain, SoeOptional}, - {sfTickSize, SoeOptional}, - {sfTicketCount, SoeOptional}, - {sfNFTokenMinter, SoeOptional}, - {sfMintedNFTokens, SoeDefault}, - {sfBurnedNFTokens, SoeDefault}, - {sfFirstNFTokenSequence, SoeOptional}, - {sfAMMID, SoeOptional}, // pseudo-account designator - {sfVaultID, SoeOptional}, // pseudo-account designator - {sfLoanBrokerID, SoeOptional}, // pseudo-account designator + {sfAccount, SoeRequired}, + {sfSequence, SoeRequired}, + {sfBalance, SoeRequired}, + {sfOwnerCount, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfAccountTxnID, SoeOptional}, + {sfRegularKey, SoeOptional}, + {sfEmailHash, SoeOptional}, + {sfWalletLocator, SoeOptional}, + {sfWalletSize, SoeOptional}, + {sfMessageKey, SoeOptional}, + {sfTransferRate, SoeOptional}, + {sfDomain, SoeOptional}, + {sfTickSize, SoeOptional}, + {sfTicketCount, SoeOptional}, + {sfNFTokenMinter, SoeOptional}, + {sfMintedNFTokens, SoeDefault}, + {sfBurnedNFTokens, SoeDefault}, + {sfFirstNFTokenSequence, SoeOptional}, + {sfSponsoredOwnerCount, SoeDefault}, + {sfSponsoringOwnerCount, SoeDefault}, + {sfSponsoringAccountCount, SoeDefault}, + {sfAMMID, SoeOptional}, // pseudo-account designator + {sfVaultID, SoeOptional}, // pseudo-account designator + {sfLoanBrokerID, SoeOptional}, // pseudo-account designator })) /** A ledger object which contains a list of object identifiers. @@ -286,6 +289,8 @@ LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({ {sfHighNode, SoeOptional}, {sfHighQualityIn, SoeOptional}, {sfHighQualityOut, SoeOptional}, + {sfHighSponsor, SoeOptional}, + {sfLowSponsor, SoeOptional}, })) /** The ledger object which lists the network's fee settings. @@ -616,5 +621,20 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanScale, SoeDefault}, })) +/** A ledger object representing a sponsorship. + \sa keylet::sponsorship + */ +LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({ + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfOwner, SoeRequired}, + {sfSponsee, SoeRequired}, + {sfFeeAmount, SoeOptional}, + {sfMaxFee, SoeOptional}, + {sfRemainingOwnerCount, SoeDefault}, + {sfOwnerNode, SoeRequired}, + {sfSponseeNode, SoeRequired}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 0d453eea11..4ef76c8b75 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -114,6 +114,11 @@ TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bi TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips) TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips) TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69) +TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 70) +TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) +TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) +TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73) +TYPED_SFIELD(sfSponsorFlags, UINT32, 74) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -148,6 +153,7 @@ TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::kSmdBaseTen|SFie TYPED_SFIELD(sfVaultNode, UINT64, 30) TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::kSmdBaseTen|SField::kSmdDefault) +TYPED_SFIELD(sfSponseeNode, UINT64, 33) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -209,6 +215,7 @@ TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfReferenceHolding, UINT256, 39) TYPED_SFIELD(sfBlindingFactor, UINT256, 40) +TYPED_SFIELD(sfObjectID, UINT256, 41) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -268,6 +275,8 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28) TYPED_SFIELD(sfSignatureReward, AMOUNT, 29) TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) +TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) +TYPED_SFIELD(sfMaxFee, AMOUNT, 33) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) @@ -343,6 +352,11 @@ TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23) TYPED_SFIELD(sfSubject, ACCOUNT, 24) TYPED_SFIELD(sfBorrower, ACCOUNT, 25) TYPED_SFIELD(sfCounterparty, ACCOUNT, 26) +TYPED_SFIELD(sfSponsor, ACCOUNT, 27) +TYPED_SFIELD(sfHighSponsor, ACCOUNT, 28) +TYPED_SFIELD(sfLowSponsor, ACCOUNT, 29) +TYPED_SFIELD(sfCounterpartySponsor, ACCOUNT, 30) +TYPED_SFIELD(sfSponsee, ACCOUNT, 31) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::kSmdNever) @@ -407,6 +421,7 @@ UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning) // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index b0c4f66bae..e805596c00 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1165,6 +1165,35 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, {sfZKProof, SoeRequired}, })) +/** This transaction transfers sponsorship on an object/account. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, + Delegation::NotDelegable, + featureSponsor, + NoPriv, + ({ + {sfObjectID, SoeOptional}, + {sfSponsee, SoeOptional}, +})) + +/** This transaction creates a Sponsorship object. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, + Delegation::Delegable, + featureSponsor, + NoPriv, + ({ + {sfCounterpartySponsor, SoeOptional}, + {sfSponsee, SoeOptional}, + {sfFeeAmount, SoeOptional}, + {sfMaxFee, SoeOptional}, + {sfRemainingOwnerCount, SoeOptional}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/digest.h b/include/xrpl/protocol/digest.h index c1e70cada2..44fca3d1ea 100644 --- a/include/xrpl/protocol/digest.h +++ b/include/xrpl/protocol/digest.h @@ -12,21 +12,23 @@ namespace xrpl { -/** Message digest functions used in the codebase - - @note These are modeled to meet the requirements of `Hasher` in the - `hash_append` interface, discussed in proposal: - - N3980 "Types Don't Know #" - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html -*/ +/** + * Message digest functions used in the codebase + * + * @note These are modeled to meet the requirements of `Hasher` in the + * `hash_append` interface, discussed in proposal: + * + * N3980 "Types Don't Know #" + * http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html + */ //------------------------------------------------------------------------------ -/** RIPEMD-160 digest - - @note This uses the OpenSSL implementation -*/ +/** + * RIPEMD-160 digest + * + * @note This uses the OpenSSL implementation + */ struct OpensslRipemd160Hasher { public: @@ -46,10 +48,11 @@ private: char ctx_[96]{}; }; -/** SHA-512 digest - - @note This uses the OpenSSL implementation -*/ +/** + * SHA-512 digest + * + * @note This uses the OpenSSL implementation + */ struct OpensslSha512Hasher { public: @@ -69,10 +72,11 @@ private: char ctx_[216]{}; }; -/** SHA-256 digest - - @note This uses the OpenSSL implementation -*/ +/** + * SHA-256 digest + * + * @note This uses the OpenSSL implementation + */ struct OpensslSha256Hasher { public: @@ -100,21 +104,22 @@ using sha512_hasher = OpensslSha512Hasher; //------------------------------------------------------------------------------ -/** Returns the RIPEMD-160 digest of the SHA256 hash of the message. - - This operation is used to compute the 160-bit identifier - representing an XRPL account, from a message. Typically the - message is the public key of the account - which is not - stored in the account root. - - The same computation is used regardless of the cryptographic - scheme implied by the public key. For example, the public key - may be an ed25519 public key or a secp256k1 public key. Support - for new cryptographic systems may be added, using the same - formula for calculating the account identifier. - - Meets the requirements of Hasher (in hash_append) -*/ +/** + * Returns the RIPEMD-160 digest of the SHA256 hash of the message. + * + * This operation is used to compute the 160-bit identifier + * representing an XRPL account, from a message. Typically the + * message is the public key of the account - which is not + * stored in the account root. + * + * The same computation is used regardless of the cryptographic + * scheme implied by the public key. For example, the public key + * may be an ed25519 public key or a secp256k1 public key. Support + * for new cryptographic systems may be added, using the same + * formula for calculating the account identifier. + * + * Meets the requirements of Hasher (in hash_append) + */ struct RipeshaHasher { private: @@ -145,11 +150,12 @@ public: namespace detail { -/** Returns the SHA512-Half digest of a message. - - The SHA512-Half is the first 256 bits of the - SHA-512 digest of the message. -*/ +/** + * Returns the SHA512-Half digest of a message. + * + * The SHA512-Half is the first 256 bits of the + * SHA-512 digest of the message. + */ template struct BasicSha512HalfHasher { @@ -201,7 +207,9 @@ using sha512_half_hasher_s = detail::BasicSha512HalfHasher; //------------------------------------------------------------------------------ -/** Returns the SHA512-Half of a series of objects. */ +/** + * Returns the SHA512-Half of a series of objects. + */ template sha512_half_hasher::result_type sha512Half(Args const&... args) @@ -212,12 +220,13 @@ sha512Half(Args const&... args) return static_cast(h); } -/** Returns the SHA512-Half of a series of objects. - - Postconditions: - Temporary memory storing copies of - input messages will be cleared. -*/ +/** + * Returns the SHA512-Half of a series of objects. + * + * Postconditions: + * Temporary memory storing copies of + * input messages will be cleared. + */ template sha512_half_hasher_s::result_type sha512HalfS(Args const&... args) diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 191ed385f3..63e877ca31 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -110,6 +110,7 @@ JSS(accounts); // in: LedgerEntry, Subscribe, handlers/Ledger JSS(accounts_proposed); // in: Subscribe, Unsubscribe JSS(action); // JSS(active); // out: OverlayImpl +JSS(actor); // in/out: AccountTx JSS(acquiring); // out: LedgerRequest JSS(address); // out: PeerImp JSS(affected); // out: AcceptedLedgerTx @@ -133,6 +134,7 @@ JSS(attestation_reward_account); // JSS(auction_slot); // out: amm_info JSS(authorized); // out: AccountLines JSS(authorize); // out: delegate +JSS(authorizer); // in/out: AccountTx JSS(authorized_credentials); // in: ledger_entry DepositPreauth JSS(auth_accounts); // out: amm_info JSS(auth_change); // out: AccountInfo @@ -191,6 +193,7 @@ JSS(converge_time); // out: NetworkOPs JSS(converge_time_s); // out: NetworkOPs JSS(cookie); // out: NetworkOPs JSS(count); // in: AccountTx*, ValidatorList +JSS(counter_party); // in/out: AccountTx JSS(counters); // in/out: retrieve counters JSS(credentials); // in: deposit_authorized JSS(credential_type); // in: LedgerEntry DepositPreauth @@ -270,6 +273,7 @@ JSS(freeze); // out: AccountLines JSS(freeze_peer); // out: AccountLines JSS(deep_freeze); // out: AccountLines JSS(deep_freeze_peer); // out: AccountLines +JSS(delegate_filter); // in/out: AccountTx JSS(frozen_balances); // out: GatewayBalances JSS(full); // in: LedgerClearer, handlers/Ledger JSS(full_reply); // out: PathFind @@ -558,6 +562,9 @@ JSS(source_account); // in: PathRequest, RipplePathFind JSS(source_amount); // in: PathRequest, RipplePathFind JSS(source_currencies); // in: PathRequest, RipplePathFind JSS(source_tag); // out: AccountChannels +JSS(sponsee); // in: LedgerEntry +JSS(sponsor); // in: LedgerEntry +JSS(sponsored); // in: AccountObjects JSS(stand_alone); // out: NetworkOPs JSS(standard_deviation); // out: get_aggregate_price JSS(start); // in: TxHistory diff --git a/include/xrpl/protocol/serialize.h b/include/xrpl/protocol/serialize.h index e758b57d49..130a2366cb 100644 --- a/include/xrpl/protocol/serialize.h +++ b/include/xrpl/protocol/serialize.h @@ -9,7 +9,9 @@ namespace xrpl { -/** Serialize an object to a blob. */ +/** + * Serialize an object to a blob. + */ template Blob serializeBlob(Object const& o) @@ -19,7 +21,9 @@ serializeBlob(Object const& o) return s.peekData(); } -/** Serialize an object to a hex string. */ +/** + * Serialize an object to a hex string. + */ inline std::string serializeHex(STObject const& o) { diff --git a/include/xrpl/protocol/tokens.h b/include/xrpl/protocol/tokens.h index bce7e7b5d6..0ce80030e6 100644 --- a/include/xrpl/protocol/tokens.h +++ b/include/xrpl/protocol/tokens.h @@ -35,17 +35,18 @@ template [[nodiscard]] std::optional parseBase58(TokenType type, std::string const& s); -/** Encode data in Base58Check format using XRPL alphabet - - For details on the format see - https://xrpl.org/base58-encodings.html#base58-encodings - - @param type The type of token to encode. - @param token Pointer to the data to encode. - @param size The size of the data to encode. - - @return the encoded token. -*/ +/** + * Encode data in Base58Check format using XRPL alphabet + * + * For details on the format see + * https://xrpl.org/base58-encodings.html#base58-encodings + * + * @param type The type of token to encode. + * @param token Pointer to the data to encode. + * @param size The size of the data to encode. + * + * @return the encoded token. + */ [[nodiscard]] std::string encodeBase58Token(TokenType type, void const* token, std::size_t size); diff --git a/include/xrpl/protocol_autogen/LedgerEntryBase.h b/include/xrpl/protocol_autogen/LedgerEntryBase.h index 5758adbb24..7902055b85 100644 --- a/include/xrpl/protocol_autogen/LedgerEntryBase.h +++ b/include/xrpl/protocol_autogen/LedgerEntryBase.h @@ -158,7 +158,9 @@ public: } protected: - /** @brief The underlying serialized ledger entry being wrapped. */ + /** + * @brief The underlying serialized ledger entry being wrapped. + */ SLE::const_pointer sle_; }; diff --git a/include/xrpl/protocol_autogen/TransactionBase.h b/include/xrpl/protocol_autogen/TransactionBase.h index 161d718c66..a135709576 100644 --- a/include/xrpl/protocol_autogen/TransactionBase.h +++ b/include/xrpl/protocol_autogen/TransactionBase.h @@ -450,7 +450,9 @@ public: } protected: - /** @brief The underlying transaction object being wrapped. */ + /** + * @brief The underlying transaction object being wrapped. + */ std::shared_ptr tx_; }; diff --git a/include/xrpl/protocol_autogen/ledger_entries/AMM.h b/include/xrpl/protocol_autogen/ledger_entries/AMM.h index 11fd3738c9..e6daaf0043 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/AMM.h +++ b/include/xrpl/protocol_autogen/ledger_entries/AMM.h @@ -265,7 +265,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h index f9a12a027f..04249b4e04 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h @@ -447,6 +447,78 @@ public: return this->sle_->isFieldPresent(sfFirstNFTokenSequence); } + /** + * @brief Get sfSponsoredOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoredOwnerCount() const + { + if (hasSponsoredOwnerCount()) + return this->sle_->at(sfSponsoredOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoredOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoredOwnerCount() const + { + return this->sle_->isFieldPresent(sfSponsoredOwnerCount); + } + + /** + * @brief Get sfSponsoringOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoringOwnerCount() const + { + if (hasSponsoringOwnerCount()) + return this->sle_->at(sfSponsoringOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoringOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoringOwnerCount() const + { + return this->sle_->isFieldPresent(sfSponsoringOwnerCount); + } + + /** + * @brief Get sfSponsoringAccountCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoringAccountCount() const + { + if (hasSponsoringAccountCount()) + return this->sle_->at(sfSponsoringAccountCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoringAccountCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoringAccountCount() const + { + return this->sle_->isFieldPresent(sfSponsoringAccountCount); + } + /** * @brief Get sfAMMID (SoeOptional) * @return The field value, or std::nullopt if not present. @@ -564,7 +636,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) @@ -786,6 +860,39 @@ public: return *this; } + /** + * @brief Set sfSponsoredOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoredOwnerCount(std::decay_t const& value) + { + object_[sfSponsoredOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfSponsoringOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoringOwnerCount(std::decay_t const& value) + { + object_[sfSponsoringOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfSponsoringAccountCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoringAccountCount(std::decay_t const& value) + { + object_[sfSponsoringAccountCount] = value; + return *this; + } + /** * @brief Set sfAMMID (SoeOptional) * @return Reference to this builder for method chaining. diff --git a/include/xrpl/protocol_autogen/ledger_entries/Amendments.h b/include/xrpl/protocol_autogen/ledger_entries/Amendments.h index 6a801308ca..c8b6c2d524 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Amendments.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Amendments.h @@ -175,7 +175,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAmendments (SoeOptional) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Bridge.h b/include/xrpl/protocol_autogen/ledger_entries/Bridge.h index 2c7479b243..31d0e50c02 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Bridge.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Bridge.h @@ -219,7 +219,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Check.h b/include/xrpl/protocol_autogen/ledger_entries/Check.h index 5b3fd10b92..d354425c39 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Check.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Check.h @@ -278,7 +278,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Credential.h b/include/xrpl/protocol_autogen/ledger_entries/Credential.h index dfce76e45c..f4d243aea8 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Credential.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Credential.h @@ -228,7 +228,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfSubject (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/DID.h b/include/xrpl/protocol_autogen/ledger_entries/DID.h index ad423377e7..71113862c5 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/DID.h +++ b/include/xrpl/protocol_autogen/ledger_entries/DID.h @@ -202,7 +202,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Delegate.h b/include/xrpl/protocol_autogen/ledger_entries/Delegate.h index bfe5f5587a..84a33ea2be 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Delegate.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Delegate.h @@ -181,7 +181,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h b/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h index 069bed6b77..c4c4434251 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h +++ b/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h @@ -179,7 +179,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h b/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h index 50659c33f6..7879f104af 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h +++ b/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h @@ -440,7 +440,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfOwner (SoeOptional) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Escrow.h b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h index f3c033d26d..106d69722f 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Escrow.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h @@ -372,7 +372,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h index 8f43d3b782..21478f749d 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h +++ b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h @@ -294,7 +294,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfBaseFee (SoeOptional) diff --git a/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h b/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h index f1d3684b55..2c6747b07b 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h +++ b/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h @@ -139,7 +139,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfFirstLedgerSequence (SoeOptional) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Loan.h b/include/xrpl/protocol_autogen/ledger_entries/Loan.h index 5d837736ec..a0abf9bd97 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Loan.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Loan.h @@ -616,7 +616,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfPreviousTxnID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h b/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h index 88f05e3433..281af2cfeb 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h +++ b/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h @@ -387,7 +387,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfPreviousTxnID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h index 379cfe53f5..874d779d09 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h @@ -335,7 +335,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h index b6c77093ac..8518a0fe14 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h @@ -420,7 +420,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfIssuer (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h b/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h index 072d3721f9..61aaccacdc 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h +++ b/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h @@ -217,7 +217,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfOwner (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h b/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h index 5e00cb1120..1aea6d6b01 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h +++ b/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h @@ -166,7 +166,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfPreviousPageMin (SoeOptional) diff --git a/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h b/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h index 7ca9729082..a35865202d 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h +++ b/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h @@ -199,7 +199,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfDisabledValidators (SoeOptional) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Offer.h b/include/xrpl/protocol_autogen/ledger_entries/Offer.h index f51b54cfd2..e3539fc1fc 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Offer.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Offer.h @@ -268,7 +268,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Oracle.h b/include/xrpl/protocol_autogen/ledger_entries/Oracle.h index 902032f94f..727e63ea84 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Oracle.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Oracle.h @@ -231,7 +231,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfOwner (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h index 61a4e2d044..4d00fbb7e5 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h +++ b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h @@ -339,7 +339,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h b/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h index 638dda2420..e793e2d658 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h +++ b/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h @@ -157,7 +157,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfOwner (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h index e8debfe792..dda1b78e66 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h @@ -243,6 +243,54 @@ public: { return this->sle_->isFieldPresent(sfHighQualityOut); } + + /** + * @brief Get sfHighSponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getHighSponsor() const + { + if (hasHighSponsor()) + return this->sle_->at(sfHighSponsor); + return std::nullopt; + } + + /** + * @brief Check if sfHighSponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasHighSponsor() const + { + return this->sle_->isFieldPresent(sfHighSponsor); + } + + /** + * @brief Get sfLowSponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getLowSponsor() const + { + if (hasLowSponsor()) + return this->sle_->at(sfLowSponsor); + return std::nullopt; + } + + /** + * @brief Check if sfLowSponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasLowSponsor() const + { + return this->sle_->isFieldPresent(sfLowSponsor); + } }; /** @@ -287,7 +335,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfBalance (SoeRequired) @@ -410,6 +460,28 @@ public: return *this; } + /** + * @brief Set sfHighSponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setHighSponsor(std::decay_t const& value) + { + object_[sfHighSponsor] = value; + return *this; + } + + /** + * @brief Set sfLowSponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setLowSponsor(std::decay_t const& value) + { + object_[sfLowSponsor] = value; + return *this; + } + /** * @brief Build and return the completed RippleState wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/SignerList.h b/include/xrpl/protocol_autogen/ledger_entries/SignerList.h index 443e5588f9..3aff8fa51b 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/SignerList.h +++ b/include/xrpl/protocol_autogen/ledger_entries/SignerList.h @@ -181,7 +181,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfOwner (SoeOptional) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h new file mode 100644 index 0000000000..065e655682 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h @@ -0,0 +1,346 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class SponsorshipBuilder; + +/** + * @brief Ledger Entry: Sponsorship + * + * Type: ltSPONSORSHIP (0x0090) + * RPC Name: sponsorship + * + * Immutable wrapper around SLE providing type-safe field access. + * Use SponsorshipBuilder to construct new ledger entries. + */ +class Sponsorship : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltSPONSORSHIP; + + /** + * @brief Construct a Sponsorship ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit Sponsorship(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for Sponsorship"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfPreviousTxnID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } + + /** + * @brief Get sfOwner (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfSponsee (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getSponsee() const + { + return this->sle_->at(sfSponsee); + } + + /** + * @brief Get sfFeeAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFeeAmount() const + { + if (hasFeeAmount()) + return this->sle_->at(sfFeeAmount); + return std::nullopt; + } + + /** + * @brief Check if sfFeeAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFeeAmount() const + { + return this->sle_->isFieldPresent(sfFeeAmount); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + return this->sle_->at(sfMaxFee); + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->sle_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfRemainingOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRemainingOwnerCount() const + { + if (hasRemainingOwnerCount()) + return this->sle_->at(sfRemainingOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfRemainingOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRemainingOwnerCount() const + { + return this->sle_->isFieldPresent(sfRemainingOwnerCount); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfSponseeNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getSponseeNode() const + { + return this->sle_->at(sfSponseeNode); + } +}; + +/** + * @brief Builder for Sponsorship ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class SponsorshipBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipBuilder with required fields. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + * @param owner The sfOwner field value. + * @param sponsee The sfSponsee field value. + * @param ownerNode The sfOwnerNode field value. + * @param sponseeNode The sfSponseeNode field value. + */ + SponsorshipBuilder(std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& owner,std::decay_t const& sponsee,std::decay_t const& ownerNode,std::decay_t const& sponseeNode) + : LedgerEntryBuilderBase(ltSPONSORSHIP) + { + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + setOwner(owner); + setSponsee(sponsee); + setOwnerNode(ownerNode); + setSponseeNode(sponseeNode); + } + + /** + * @brief Construct a SponsorshipBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + SponsorshipBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltSPONSORSHIP) + { + throw std::runtime_error("Invalid ledger entry type for Sponsorship"); + } + object_ = *sle; + } + + /** + * @brief Ledger entry-specific field setters + */ + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfOwner (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Set sfFeeAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setFeeAmount(std::decay_t const& value) + { + object_[sfFeeAmount] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfRemainingOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setRemainingOwnerCount(std::decay_t const& value) + { + object_[sfRemainingOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfSponseeNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setSponseeNode(std::decay_t const& value) + { + object_[sfSponseeNode] = value; + return *this; + } + + /** + * @brief Build and return the completed Sponsorship wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + Sponsorship + build(uint256 const& index) + { + return Sponsorship{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/ledger_entries/Ticket.h b/include/xrpl/protocol_autogen/ledger_entries/Ticket.h index 6fa5b57f6c..e1205bdb67 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Ticket.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Ticket.h @@ -143,7 +143,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Vault.h b/include/xrpl/protocol_autogen/ledger_entries/Vault.h index d1aaeb4ed8..2bf92b4f5d 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Vault.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Vault.h @@ -339,7 +339,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfPreviousTxnID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h index 3f8058a4a1..bfd7a0a8b8 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h +++ b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h @@ -196,7 +196,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h index e24009a4b7..4872d4063e 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h +++ b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h @@ -170,7 +170,9 @@ public: object_ = *sle; } - /** @brief Ledger entry-specific field setters */ + /** + * @brief Ledger entry-specific field setters + */ /** * @brief Set sfAccount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AMMBid.h b/include/xrpl/protocol_autogen/transactions/AMMBid.h index cd2792e810..30a2b6f2ab 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMBid.h +++ b/include/xrpl/protocol_autogen/transactions/AMMBid.h @@ -190,7 +190,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAsset (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AMMClawback.h b/include/xrpl/protocol_autogen/transactions/AMMClawback.h index ccbd7d99e6..38aba892c4 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMClawback.h +++ b/include/xrpl/protocol_autogen/transactions/AMMClawback.h @@ -154,7 +154,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfHolder (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AMMCreate.h b/include/xrpl/protocol_autogen/transactions/AMMCreate.h index cc88428e7a..c6ccd4e860 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMCreate.h +++ b/include/xrpl/protocol_autogen/transactions/AMMCreate.h @@ -127,7 +127,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAmount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AMMDelete.h b/include/xrpl/protocol_autogen/transactions/AMMDelete.h index 4cc0497c32..05899a46c8 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMDelete.h +++ b/include/xrpl/protocol_autogen/transactions/AMMDelete.h @@ -114,7 +114,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAsset (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AMMDeposit.h b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h index e01332c3e2..5416547dab 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMDeposit.h +++ b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h @@ -246,7 +246,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAsset (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AMMVote.h b/include/xrpl/protocol_autogen/transactions/AMMVote.h index b19d440c84..7dce3c252f 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMVote.h +++ b/include/xrpl/protocol_autogen/transactions/AMMVote.h @@ -127,7 +127,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAsset (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h index 196f0faba2..81258f22d6 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h @@ -220,7 +220,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAsset (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AccountDelete.h b/include/xrpl/protocol_autogen/transactions/AccountDelete.h index c0346d9499..cf6e97bb63 100644 --- a/include/xrpl/protocol_autogen/transactions/AccountDelete.h +++ b/include/xrpl/protocol_autogen/transactions/AccountDelete.h @@ -151,7 +151,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDestination (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/AccountSet.h b/include/xrpl/protocol_autogen/transactions/AccountSet.h index 7ddda45752..55c449e78e 100644 --- a/include/xrpl/protocol_autogen/transactions/AccountSet.h +++ b/include/xrpl/protocol_autogen/transactions/AccountSet.h @@ -346,7 +346,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfEmailHash (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/Batch.h b/include/xrpl/protocol_autogen/transactions/Batch.h index 00f553ada7..1a59d2b4c0 100644 --- a/include/xrpl/protocol_autogen/transactions/Batch.h +++ b/include/xrpl/protocol_autogen/transactions/Batch.h @@ -123,7 +123,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfRawTransactions (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/CheckCancel.h b/include/xrpl/protocol_autogen/transactions/CheckCancel.h index 4f7534278b..b75b717e3f 100644 --- a/include/xrpl/protocol_autogen/transactions/CheckCancel.h +++ b/include/xrpl/protocol_autogen/transactions/CheckCancel.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfCheckID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/CheckCash.h b/include/xrpl/protocol_autogen/transactions/CheckCash.h index a58a20c57e..c742a15154 100644 --- a/include/xrpl/protocol_autogen/transactions/CheckCash.h +++ b/include/xrpl/protocol_autogen/transactions/CheckCash.h @@ -153,7 +153,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfCheckID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/CheckCreate.h b/include/xrpl/protocol_autogen/transactions/CheckCreate.h index 17f985ac63..63e55f8604 100644 --- a/include/xrpl/protocol_autogen/transactions/CheckCreate.h +++ b/include/xrpl/protocol_autogen/transactions/CheckCreate.h @@ -191,7 +191,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDestination (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/Clawback.h b/include/xrpl/protocol_autogen/transactions/Clawback.h index ecd7ebe7a2..9a3a7f9feb 100644 --- a/include/xrpl/protocol_autogen/transactions/Clawback.h +++ b/include/xrpl/protocol_autogen/transactions/Clawback.h @@ -126,7 +126,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAmount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h index 2b16590649..c80fc81dc5 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h @@ -138,7 +138,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h index f7a4cb601a..dec7f733c9 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h @@ -229,7 +229,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h index 68bf326645..53a8e64125 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h @@ -203,7 +203,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h index bb932080d8..848da42a41 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h index 2d8a77d56f..806a2586e9 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h @@ -268,7 +268,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/CredentialAccept.h b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h index 152c18ea09..f2ab546320 100644 --- a/include/xrpl/protocol_autogen/transactions/CredentialAccept.h +++ b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h @@ -112,7 +112,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfIssuer (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/CredentialCreate.h b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h index d7e056f590..6cf09c852b 100644 --- a/include/xrpl/protocol_autogen/transactions/CredentialCreate.h +++ b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h @@ -164,7 +164,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfSubject (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/CredentialDelete.h b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h index 512f230a26..24a2bfa62a 100644 --- a/include/xrpl/protocol_autogen/transactions/CredentialDelete.h +++ b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h @@ -151,7 +151,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfSubject (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/DIDDelete.h b/include/xrpl/protocol_autogen/transactions/DIDDelete.h index 5f90821bfd..304287883d 100644 --- a/include/xrpl/protocol_autogen/transactions/DIDDelete.h +++ b/include/xrpl/protocol_autogen/transactions/DIDDelete.h @@ -86,7 +86,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Build and return the DIDDelete wrapper. diff --git a/include/xrpl/protocol_autogen/transactions/DIDSet.h b/include/xrpl/protocol_autogen/transactions/DIDSet.h index 27242cda71..67e5ba23c5 100644 --- a/include/xrpl/protocol_autogen/transactions/DIDSet.h +++ b/include/xrpl/protocol_autogen/transactions/DIDSet.h @@ -164,7 +164,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDIDDocument (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/DelegateSet.h b/include/xrpl/protocol_autogen/transactions/DelegateSet.h index 1f454ff4a1..592a778952 100644 --- a/include/xrpl/protocol_autogen/transactions/DelegateSet.h +++ b/include/xrpl/protocol_autogen/transactions/DelegateSet.h @@ -112,7 +112,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAuthorize (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/DepositPreauth.h b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h index d303ffbeff..b5d575aac5 100644 --- a/include/xrpl/protocol_autogen/transactions/DepositPreauth.h +++ b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h @@ -186,7 +186,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAuthorize (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/EnableAmendment.h b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h index b397c5df87..e811ca16df 100644 --- a/include/xrpl/protocol_autogen/transactions/EnableAmendment.h +++ b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h @@ -112,7 +112,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLedgerSequence (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCancel.h b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h index 4da943c351..e7e49eca0d 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowCancel.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h @@ -112,7 +112,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfOwner (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h index 35775c31ae..b994e4ec07 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h @@ -217,7 +217,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDestination (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h index f6ca73d209..2476def5c2 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h @@ -190,7 +190,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfOwner (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h index 52723ad5eb..af86dea0b0 100644 --- a/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h +++ b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h @@ -151,7 +151,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLedgerFixType (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h index 4842381362..875e0a4c5e 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h @@ -139,7 +139,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanBrokerID (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h index 98cebdccb2..38cc113844 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h @@ -113,7 +113,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanBrokerID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h index e734c6802a..56a93acbb4 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h @@ -165,7 +165,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanBrokerID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h index 55c0a12381..29b3a787fd 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanBrokerID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h index 854022242d..41c87c281d 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h @@ -255,7 +255,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfVaultID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanDelete.h b/include/xrpl/protocol_autogen/transactions/LoanDelete.h index 70c9e50097..8ed537b37a 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanDelete.h +++ b/include/xrpl/protocol_autogen/transactions/LoanDelete.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanManage.h b/include/xrpl/protocol_autogen/transactions/LoanManage.h index f11782c4e1..5eb95d21b1 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanManage.h +++ b/include/xrpl/protocol_autogen/transactions/LoanManage.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanPay.h b/include/xrpl/protocol_autogen/transactions/LoanPay.h index 4012225e17..8e1faeb981 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanPay.h +++ b/include/xrpl/protocol_autogen/transactions/LoanPay.h @@ -113,7 +113,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/LoanSet.h b/include/xrpl/protocol_autogen/transactions/LoanSet.h index 3fa3c905c2..2cadebd02e 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanSet.h +++ b/include/xrpl/protocol_autogen/transactions/LoanSet.h @@ -500,7 +500,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLoanBrokerID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h index 90440c41f9..2fb93eaf35 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h @@ -125,7 +125,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h index d723a3041c..e6fece8354 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h @@ -242,7 +242,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAssetScale (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h index 19c4530792..cbcd206097 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h index 8099af1148..803868c640 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h @@ -281,7 +281,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfMPTokenIssuanceID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h index 7627437a52..325d2d7fbd 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h @@ -164,7 +164,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfNFTokenBuyOffer (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h index 9f981b1ec4..ec423ea468 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h @@ -125,7 +125,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfNFTokenID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h index 2eca27fc8d..4c4fb1dc65 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfNFTokenOffers (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h index a5c6e226d9..a535a578e0 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h @@ -190,7 +190,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfNFTokenID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenMint.h b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h index ad68f1a18b..5af41eb3dd 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenMint.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h @@ -255,7 +255,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfNFTokenTaxon (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenModify.h b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h index 277146ce78..9b9701fed6 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenModify.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h @@ -151,7 +151,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfNFTokenID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/OfferCancel.h b/include/xrpl/protocol_autogen/transactions/OfferCancel.h index 833924f683..5e6010e0dd 100644 --- a/include/xrpl/protocol_autogen/transactions/OfferCancel.h +++ b/include/xrpl/protocol_autogen/transactions/OfferCancel.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfOfferSequence (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/OfferCreate.h b/include/xrpl/protocol_autogen/transactions/OfferCreate.h index 13e868643f..ffc1216297 100644 --- a/include/xrpl/protocol_autogen/transactions/OfferCreate.h +++ b/include/xrpl/protocol_autogen/transactions/OfferCreate.h @@ -192,7 +192,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfTakerPays (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/OracleDelete.h b/include/xrpl/protocol_autogen/transactions/OracleDelete.h index d6d46d1d7a..ebdc8fb7e9 100644 --- a/include/xrpl/protocol_autogen/transactions/OracleDelete.h +++ b/include/xrpl/protocol_autogen/transactions/OracleDelete.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfOracleDocumentID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/OracleSet.h b/include/xrpl/protocol_autogen/transactions/OracleSet.h index 1d295b4cc2..0ec6d5cad0 100644 --- a/include/xrpl/protocol_autogen/transactions/OracleSet.h +++ b/include/xrpl/protocol_autogen/transactions/OracleSet.h @@ -203,7 +203,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfOracleDocumentID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/Payment.h b/include/xrpl/protocol_autogen/transactions/Payment.h index b2e82cd6af..389900bf12 100644 --- a/include/xrpl/protocol_autogen/transactions/Payment.h +++ b/include/xrpl/protocol_autogen/transactions/Payment.h @@ -295,7 +295,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDestination (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h index 1db3057366..4c567b13f4 100644 --- a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h @@ -229,7 +229,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfChannel (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h index cf0cbae2aa..0a513d575a 100644 --- a/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h @@ -190,7 +190,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDestination (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h index 3612fcbf48..51210dd796 100644 --- a/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h @@ -138,7 +138,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfChannel (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h index e08ff1ed5a..3db921776c 100644 --- a/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDomainID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h index 68f1cfebb2..3e352cad76 100644 --- a/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h @@ -125,7 +125,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfDomainID (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/SetFee.h b/include/xrpl/protocol_autogen/transactions/SetFee.h index bc5fc0e603..177f39199b 100644 --- a/include/xrpl/protocol_autogen/transactions/SetFee.h +++ b/include/xrpl/protocol_autogen/transactions/SetFee.h @@ -294,7 +294,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLedgerSequence (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/SetRegularKey.h b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h index 1eca1bef25..a943bb0279 100644 --- a/include/xrpl/protocol_autogen/transactions/SetRegularKey.h +++ b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h @@ -112,7 +112,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfRegularKey (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/SignerListSet.h b/include/xrpl/protocol_autogen/transactions/SignerListSet.h index c711864e95..6e9d0e41ba 100644 --- a/include/xrpl/protocol_autogen/transactions/SignerListSet.h +++ b/include/xrpl/protocol_autogen/transactions/SignerListSet.h @@ -123,7 +123,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfSignerQuorum (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h new file mode 100644 index 0000000000..0124da5e58 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h @@ -0,0 +1,292 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class SponsorshipSetBuilder; + +/** + * @brief Transaction: SponsorshipSet + * + * Type: ttSPONSORSHIP_SET (91) + * Delegable: Delegation::Delegable + * Amendment: featureSponsor + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use SponsorshipSetBuilder to construct new transactions. + */ +class SponsorshipSet : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttSPONSORSHIP_SET; + + /** + * @brief Construct a SponsorshipSet transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit SponsorshipSet(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for SponsorshipSet"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfCounterpartySponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCounterpartySponsor() const + { + if (hasCounterpartySponsor()) + { + return this->tx_->at(sfCounterpartySponsor); + } + return std::nullopt; + } + + /** + * @brief Check if sfCounterpartySponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCounterpartySponsor() const + { + return this->tx_->isFieldPresent(sfCounterpartySponsor); + } + + /** + * @brief Get sfSponsee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsee() const + { + if (hasSponsee()) + { + return this->tx_->at(sfSponsee); + } + return std::nullopt; + } + + /** + * @brief Check if sfSponsee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsee() const + { + return this->tx_->isFieldPresent(sfSponsee); + } + + /** + * @brief Get sfFeeAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFeeAmount() const + { + if (hasFeeAmount()) + { + return this->tx_->at(sfFeeAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfFeeAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFeeAmount() const + { + return this->tx_->isFieldPresent(sfFeeAmount); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + { + return this->tx_->at(sfMaxFee); + } + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->tx_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfRemainingOwnerCount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRemainingOwnerCount() const + { + if (hasRemainingOwnerCount()) + { + return this->tx_->at(sfRemainingOwnerCount); + } + return std::nullopt; + } + + /** + * @brief Check if sfRemainingOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRemainingOwnerCount() const + { + return this->tx_->isFieldPresent(sfRemainingOwnerCount); + } +}; + +/** + * @brief Builder for SponsorshipSet transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class SponsorshipSetBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipSetBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + SponsorshipSetBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttSPONSORSHIP_SET, account, sequence, fee) + { + } + + /** + * @brief Construct a SponsorshipSetBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + SponsorshipSetBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttSPONSORSHIP_SET) + { + throw std::runtime_error("Invalid transaction type for SponsorshipSetBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfCounterpartySponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setCounterpartySponsor(std::decay_t const& value) + { + object_[sfCounterpartySponsor] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Set sfFeeAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setFeeAmount(std::decay_t const& value) + { + object_[sfFeeAmount] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfRemainingOwnerCount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setRemainingOwnerCount(std::decay_t const& value) + { + object_[sfRemainingOwnerCount] = value; + return *this; + } + + /** + * @brief Build and return the SponsorshipSet wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + SponsorshipSet + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return SponsorshipSet{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h new file mode 100644 index 0000000000..ab26e887e3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h @@ -0,0 +1,181 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class SponsorshipTransferBuilder; + +/** + * @brief Transaction: SponsorshipTransfer + * + * Type: ttSPONSORSHIP_TRANSFER (90) + * Delegable: Delegation::NotDelegable + * Amendment: featureSponsor + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use SponsorshipTransferBuilder to construct new transactions. + */ +class SponsorshipTransfer : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttSPONSORSHIP_TRANSFER; + + /** + * @brief Construct a SponsorshipTransfer transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit SponsorshipTransfer(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for SponsorshipTransfer"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfObjectID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getObjectID() const + { + if (hasObjectID()) + { + return this->tx_->at(sfObjectID); + } + return std::nullopt; + } + + /** + * @brief Check if sfObjectID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasObjectID() const + { + return this->tx_->isFieldPresent(sfObjectID); + } + + /** + * @brief Get sfSponsee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsee() const + { + if (hasSponsee()) + { + return this->tx_->at(sfSponsee); + } + return std::nullopt; + } + + /** + * @brief Check if sfSponsee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsee() const + { + return this->tx_->isFieldPresent(sfSponsee); + } +}; + +/** + * @brief Builder for SponsorshipTransfer transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class SponsorshipTransferBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipTransferBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + SponsorshipTransferBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttSPONSORSHIP_TRANSFER, account, sequence, fee) + { + } + + /** + * @brief Construct a SponsorshipTransferBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + SponsorshipTransferBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttSPONSORSHIP_TRANSFER) + { + throw std::runtime_error("Invalid transaction type for SponsorshipTransferBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfObjectID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipTransferBuilder& + setObjectID(std::decay_t const& value) + { + object_[sfObjectID] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipTransferBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Build and return the SponsorshipTransfer wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + SponsorshipTransfer + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return SponsorshipTransfer{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/TicketCreate.h b/include/xrpl/protocol_autogen/transactions/TicketCreate.h index 0b4206152e..0d8670a76a 100644 --- a/include/xrpl/protocol_autogen/transactions/TicketCreate.h +++ b/include/xrpl/protocol_autogen/transactions/TicketCreate.h @@ -99,7 +99,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfTicketCount (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/TrustSet.h b/include/xrpl/protocol_autogen/transactions/TrustSet.h index 30d537492d..22891b94ec 100644 --- a/include/xrpl/protocol_autogen/transactions/TrustSet.h +++ b/include/xrpl/protocol_autogen/transactions/TrustSet.h @@ -164,7 +164,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfLimitAmount (SoeOptional) diff --git a/include/xrpl/protocol_autogen/transactions/UNLModify.h b/include/xrpl/protocol_autogen/transactions/UNLModify.h index a8556ca699..6569e4bf7d 100644 --- a/include/xrpl/protocol_autogen/transactions/UNLModify.h +++ b/include/xrpl/protocol_autogen/transactions/UNLModify.h @@ -125,7 +125,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfUNLModifyDisabling (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/VaultClawback.h b/include/xrpl/protocol_autogen/transactions/VaultClawback.h index 8ec1d359dc..270ccc94bb 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultClawback.h +++ b/include/xrpl/protocol_autogen/transactions/VaultClawback.h @@ -139,7 +139,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfVaultID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/VaultCreate.h b/include/xrpl/protocol_autogen/transactions/VaultCreate.h index c8efa83cbf..b7e1527754 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultCreate.h +++ b/include/xrpl/protocol_autogen/transactions/VaultCreate.h @@ -256,7 +256,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfAsset (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h b/include/xrpl/protocol_autogen/transactions/VaultDelete.h index b4c08ae229..67cc32f543 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultDelete.h +++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h @@ -125,7 +125,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfVaultID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/VaultDeposit.h b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h index 01f707e07f..5bb5362114 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultDeposit.h +++ b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h @@ -113,7 +113,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfVaultID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/VaultSet.h b/include/xrpl/protocol_autogen/transactions/VaultSet.h index 5bdc6a7a95..14df70f13b 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultSet.h +++ b/include/xrpl/protocol_autogen/transactions/VaultSet.h @@ -177,7 +177,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfVaultID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h index 75b5b90035..3211524e1f 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h @@ -165,7 +165,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfVaultID (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h index 36d8170af1..b8d551c5e1 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h +++ b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h @@ -138,7 +138,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h index a04eec1505..22b57803dc 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h +++ b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h @@ -229,7 +229,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h index 1896f11681..5e80c05aae 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h +++ b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h @@ -229,7 +229,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainClaim.h b/include/xrpl/protocol_autogen/transactions/XChainClaim.h index f5b680049e..ec403b5eb8 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainClaim.h +++ b/include/xrpl/protocol_autogen/transactions/XChainClaim.h @@ -164,7 +164,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainCommit.h b/include/xrpl/protocol_autogen/transactions/XChainCommit.h index a9ce7d1d08..48b2263645 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainCommit.h +++ b/include/xrpl/protocol_autogen/transactions/XChainCommit.h @@ -151,7 +151,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h index ae5be4108f..9614b0bd88 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h @@ -138,7 +138,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h index 7c1beb20c7..d17759619f 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h @@ -125,7 +125,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h index 30558ab88c..e79c9139ce 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h +++ b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h @@ -151,7 +151,9 @@ public: object_ = *tx; } - /** @brief Transaction-specific field setters */ + /** + * @brief Transaction-specific field setters + */ /** * @brief Set sfXChainBridge (SoeRequired) diff --git a/include/xrpl/rdb/RelationalDatabase.h b/include/xrpl/rdb/RelationalDatabase.h index 91c282ff16..e5784c7418 100644 --- a/include/xrpl/rdb/RelationalDatabase.h +++ b/include/xrpl/rdb/RelationalDatabase.h @@ -46,6 +46,22 @@ struct LedgerRange uint32_t max; }; +/** + * @brief Enumeration of possible delegate types that can occur during filtering in account_tx + */ +enum class DelegateType { + Actor, ///< Another account signed and submitted transactions on behalf of this account (this + ///< account is the owner/delegator). + Authorizer ///< This account signed and submitted transactions on behalf of another account + ///< (this account is the signer/delegatee). +}; + +struct DelegateFilter +{ + DelegateType type = DelegateType::Actor; + std::optional counterparty; +}; + class RelationalDatabase { public: @@ -65,8 +81,10 @@ public: struct AccountTxOptions { AccountID const& account; - /// Ledger sequence range to search. A value of 0 for min or max - /// means unbounded in that direction (no constraint applied). + /** + * Ledger sequence range to search. A value of 0 for min or max + * means unbounded in that direction (no constraint applied). + */ LedgerRange ledgerRange{}; std::uint32_t offset = 0; std::uint32_t limit = 0; @@ -80,6 +98,7 @@ public: std::optional marker; std::uint32_t limit = 0; bool bAdmin = false; + std::optional delegate; }; using AccountTx = std::pair, std::shared_ptr>; @@ -99,6 +118,7 @@ public: bool forward = false; uint32_t limit = 0; std::optional marker; + std::optional delegate; }; struct AccountTxResult @@ -107,6 +127,7 @@ public: LedgerRange ledgerRange{}; uint32_t limit = 0; std::optional marker; + std::optional delegate; }; virtual ~RelationalDatabase() = default; diff --git a/include/xrpl/rdb/SociDB.h b/include/xrpl/rdb/SociDB.h index 0f427bd18b..80d83f0f75 100644 --- a/include/xrpl/rdb/SociDB.h +++ b/include/xrpl/rdb/SociDB.h @@ -1,18 +1,19 @@ #pragma once -/** An embedded database wrapper with an intuitive, type-safe interface. - - This collection of classes let's you access embedded SQLite databases - using C++ syntax that is very similar to regular SQL. - - This module requires the @ref beast_sqlite external module. -*/ +/** + * An embedded database wrapper with an intuitive, type-safe interface. + * + * This collection of classes let's you access embedded SQLite databases + * using C++ syntax that is very similar to regular SQL. + * + * This module requires the @ref beast_sqlite external module. + */ #include #include #include -#if defined(__clang__) +#ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" #endif @@ -35,9 +36,9 @@ namespace xrpl { class BasicConfig; /** - DBConfig is used when a client wants to delay opening a soci::session after - parsing the config parameters. If a client want to open a session - immediately, use the free function "open" below. + * DBConfig is used when a client wants to delay opening a soci::session after + * parsing the config parameters. If a client want to open a session + * immediately, use the free function "open" below. */ class DBConfig { @@ -53,27 +54,27 @@ public: }; /** - Open a soci session. - - @param s Session to open. - - @param config Parameters to pick the soci backend and how to connect to that - backend. - - @param dbName Name of the database. This has different meaning for different - backends. Sometimes it is part of a filename (sqlite3), - other times it is a database name (postgresql). -*/ + * Open a soci session. + * + * @param s Session to open. + * + * @param config Parameters to pick the soci backend and how to connect to that + * backend. + * + * @param dbName Name of the database. This has different meaning for different + * backends. Sometimes it is part of a filename (sqlite3), + * other times it is a database name (postgresql). + */ void open(soci::session& s, BasicConfig const& config, std::string const& dbName); /** - * Open a soci session. + * Open a soci session. * - * @param s Session to open. - * @param beName Backend name. - * @param connectionString Connection string to forward to soci::open. - * see the soci::open documentation for how to use this. + * @param s Session to open. + * @param beName Backend name. + * @param connectionString Connection string to forward to soci::open. + * see the soci::open documentation for how to use this. * */ void @@ -107,17 +108,18 @@ public: checkpoint() = 0; }; -/** Returns a new checkpointer which makes checkpoints of a - soci database every checkpointPageCount pages, using a job on the job queue. - - The checkpointer contains references to the session and job queue - and so must outlive them both. +/** + * Returns a new checkpointer which makes checkpoints of a + * soci database every checkpointPageCount pages, using a job on the job queue. + * + * The checkpointer contains references to the session and job queue + * and so must outlive them both. */ std::shared_ptr makeCheckpointer(std::uintptr_t id, std::weak_ptr, JobQueue&, ServiceRegistry&); } // namespace xrpl -#if defined(__clang__) +#ifdef __clang__ #pragma clang diagnostic pop #endif diff --git a/include/xrpl/resource/Charge.h b/include/xrpl/resource/Charge.h index 394d641e6c..12ea548fd2 100644 --- a/include/xrpl/resource/Charge.h +++ b/include/xrpl/resource/Charge.h @@ -6,28 +6,40 @@ namespace xrpl::Resource { -/** A consumption charge. */ +/** + * A consumption charge. + */ class Charge { public: - /** The type used to hold a consumption charge. */ + /** + * The type used to hold a consumption charge. + */ using value_type = int; // A default constructed Charge has no way to get a label. Delete Charge() = delete; - /** Create a charge with the specified cost and name. */ + /** + * Create a charge with the specified cost and name. + */ Charge(value_type cost, std::string label = std::string()); - /** Return the human readable label associated with the charge. */ + /** + * Return the human readable label associated with the charge. + */ [[nodiscard]] std::string const& label() const; - /** Return the cost of the charge in Resource::Manager units. */ + /** + * Return the cost of the charge in Resource::Manager units. + */ [[nodiscard]] value_type cost() const; - /** Converts this charge into a human readable string. */ + /** + * Converts this charge into a human readable string. + */ [[nodiscard]] std::string toString() const; diff --git a/include/xrpl/resource/Consumer.h b/include/xrpl/resource/Consumer.h index 18207832ac..9abcbffc82 100644 --- a/include/xrpl/resource/Consumer.h +++ b/include/xrpl/resource/Consumer.h @@ -13,7 +13,9 @@ namespace xrpl::Resource { struct Entry; class Logic; -/** An endpoint that consumes resources. */ +/** + * An endpoint that consumes resources. + */ class Consumer { private: @@ -27,42 +29,55 @@ public: Consumer& operator=(Consumer const& other); - /** Return a human readable string uniquely identifying this consumer. */ + /** + * Return a human readable string uniquely identifying this consumer. + */ [[nodiscard]] std::string toString() const; - /** Returns `true` if this is a privileged endpoint. */ + /** + * Returns `true` if this is a privileged endpoint. + */ [[nodiscard]] bool isUnlimited() const; - /** Raise the Consumer's privilege level to a Named endpoint. - The reference to the original endpoint descriptor is released. - */ + /** + * Raise the Consumer's privilege level to a Named endpoint. + * The reference to the original endpoint descriptor is released. + */ void elevate(std::string const& name); - /** Returns the current disposition of this consumer. - This should be checked upon creation to determine if the consumer - should be disconnected immediately. - */ + /** + * Returns the current disposition of this consumer. + * This should be checked upon creation to determine if the consumer + * should be disconnected immediately. + */ [[nodiscard]] Disposition disposition() const; - /** Apply a load charge to the consumer. */ + /** + * Apply a load charge to the consumer. + */ Disposition charge(Charge const& fee, std::string const& context = {}); - /** Returns `true` if the consumer should be warned. - This consumes the warning. - */ + /** + * Returns `true` if the consumer should be warned. + * This consumes the warning. + */ bool warn(); - /** Returns `true` if the consumer should be disconnected. */ + /** + * Returns `true` if the consumer should be disconnected. + */ bool disconnect(beast::Journal const& j); - /** Returns the credit balance representing consumption. */ + /** + * Returns the credit balance representing consumption. + */ int balance(); diff --git a/include/xrpl/resource/Disposition.h b/include/xrpl/resource/Disposition.h index 6dad8db19a..cd5bceafa5 100644 --- a/include/xrpl/resource/Disposition.h +++ b/include/xrpl/resource/Disposition.h @@ -2,16 +2,24 @@ namespace xrpl::Resource { -/** The disposition of a consumer after applying a load charge. */ +/** + * The disposition of a consumer after applying a load charge. + */ enum class Disposition { - /** No action required. */ + /** + * No action required. + */ Ok - /** Consumer should be warned that consumption is high. */ + /** + * Consumer should be warned that consumption is high. + */ , Warn - /** Consumer should be disconnected for excess consumption. */ + /** + * Consumer should be disconnected for excess consumption. + */ , Drop }; diff --git a/include/xrpl/resource/Fees.h b/include/xrpl/resource/Fees.h index 55d539ac6a..5001b504d6 100644 --- a/include/xrpl/resource/Fees.h +++ b/include/xrpl/resource/Fees.h @@ -4,7 +4,9 @@ namespace xrpl::Resource { -/** Schedule of fees charged for imposing load on the server. */ +/** + * Schedule of fees charged for imposing load on the server. + */ /** @{ */ extern Charge const kFeeMalformedRequest; // A request that we can immediately tell is invalid. extern Charge const kFeeRequestNoReply; // A request that we cannot satisfy. diff --git a/include/xrpl/resource/Gossip.h b/include/xrpl/resource/Gossip.h index e626af37c3..4ad5852de0 100644 --- a/include/xrpl/resource/Gossip.h +++ b/include/xrpl/resource/Gossip.h @@ -6,12 +6,16 @@ namespace xrpl::Resource { -/** Data format for exchanging consumption information across peers. */ +/** + * Data format for exchanging consumption information across peers. + */ struct Gossip { explicit Gossip() = default; - /** Describes a single consumer. */ + /** + * Describes a single consumer. + */ struct Item { explicit Item() = default; diff --git a/include/xrpl/resource/ResourceManager.h b/include/xrpl/resource/ResourceManager.h index 13e0d09343..03aab60c75 100644 --- a/include/xrpl/resource/ResourceManager.h +++ b/include/xrpl/resource/ResourceManager.h @@ -16,7 +16,9 @@ namespace xrpl::Resource { -/** Tracks load and resource consumption. */ +/** + * Tracks load and resource consumption. + */ class Manager : public beast::PropertyStream::Source { protected: @@ -25,8 +27,10 @@ protected: public: ~Manager() override = 0; - /** Create a new endpoint keyed by inbound IP address or the forwarded - * IP if proxied. */ + /** + * Create a new endpoint keyed by inbound IP address or the forwarded + * IP if proxied. + */ virtual Consumer newInboundEndpoint(beast::IP::Endpoint const& address) = 0; virtual Consumer @@ -35,27 +39,36 @@ public: bool const proxy, std::string_view forwardedFor) = 0; - /** Create a new endpoint keyed by outbound IP address and port. */ + /** + * Create a new endpoint keyed by outbound IP address and port. + */ virtual Consumer newOutboundEndpoint(beast::IP::Endpoint const& address) = 0; - /** Create a new unlimited endpoint keyed by forwarded IP. */ + /** + * Create a new unlimited endpoint keyed by forwarded IP. + */ virtual Consumer newUnlimitedEndpoint(beast::IP::Endpoint const& address) = 0; - /** Extract packaged consumer information for export. */ + /** + * Extract packaged consumer information for export. + */ virtual Gossip exportConsumers() = 0; - /** Extract consumer information for reporting. */ + /** + * Extract consumer information for reporting. + */ virtual json::Value getJson() = 0; virtual json::Value getJson(int threshold) = 0; - /** Import packaged consumer information. - @param origin An identifier that unique labels the origin. - */ + /** + * Import packaged consumer information. + * @param origin An identifier that unique labels the origin. + */ virtual void importConsumers(std::string const& origin, Gossip const& gossip) = 0; }; diff --git a/include/xrpl/resource/detail/Entry.h b/include/xrpl/resource/detail/Entry.h index 6f44ac2c29..1336bda6ab 100644 --- a/include/xrpl/resource/detail/Entry.h +++ b/include/xrpl/resource/detail/Entry.h @@ -23,8 +23,8 @@ struct Entry : public beast::List::Node Entry() = delete; /** - @param now Construction time of Entry. - */ + * @param now Construction time of Entry. + */ explicit Entry(clock_type::time_point const now) : refcount(0), localBalance(now), remoteBalance(0) { diff --git a/include/xrpl/resource/detail/Import.h b/include/xrpl/resource/detail/Import.h index a3df6fd73b..b19dbc4d1a 100644 --- a/include/xrpl/resource/detail/Import.h +++ b/include/xrpl/resource/detail/Import.h @@ -7,7 +7,9 @@ namespace xrpl::Resource { -/** A set of imported consumer data from a gossip origin. */ +/** + * A set of imported consumer data from a gossip origin. + */ struct Import { struct Item diff --git a/include/xrpl/resource/detail/Logic.h b/include/xrpl/resource/detail/Logic.h index 78cd0ac36c..3f36ad84a3 100644 --- a/include/xrpl/resource/detail/Logic.h +++ b/include/xrpl/resource/detail/Logic.h @@ -192,7 +192,9 @@ public: return getJson(kWarningThreshold); } - /** Returns a json::ValueType::Object. */ + /** + * Returns a json::ValueType::Object. + */ json::Value getJson(int threshold) { diff --git a/include/xrpl/resource/detail/Tuning.h b/include/xrpl/resource/detail/Tuning.h index 7b2046f45c..62f7fa3f9d 100644 --- a/include/xrpl/resource/detail/Tuning.h +++ b/include/xrpl/resource/detail/Tuning.h @@ -4,7 +4,9 @@ namespace xrpl::Resource { -/** Tunable constants. */ +/** + * Tunable constants. + */ // balance at which a warning is issued static constexpr auto kWarningThreshold = 5000; diff --git a/include/xrpl/server/Handoff.h b/include/xrpl/server/Handoff.h index b80a9c5745..6dc547467b 100644 --- a/include/xrpl/server/Handoff.h +++ b/include/xrpl/server/Handoff.h @@ -13,7 +13,9 @@ using http_request_type = boost::beast::http::request; -/** Used to indicate the result of a server connection handoff. */ +/** + * Used to indicate the result of a server connection handoff. + */ struct Handoff { // When `true`, the Session will close the socket. The diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index d708a456c4..2e9bd857c7 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -35,20 +35,21 @@ public: doStatus(json::Value const&) = 0; }; -/** Manages a client's subscription to data feeds. +/** + * Manages a client's subscription to data feeds. * - * An InfoSub holds a non-owning reference to its `Source` (typically the - * process-wide `NetworkOPsImp`). The destructor reaches back into the - * `Source` to remove this subscriber from every server-side subscription - * map. + * An InfoSub holds a non-owning reference to its `Source` (typically the + * process-wide `NetworkOPsImp`). The destructor reaches back into the + * `Source` to remove this subscriber from every server-side subscription + * map. * - * @note Lifetime contract: every `InfoSub` instance MUST be destroyed - * before the backing `Source`. NetworkOPsImp shutdown drops all - * subscriber strong refs before its own teardown to satisfy this. - * @note Thread-safety: per-instance state is guarded by `lock_`. The - * destructor reads tracking sets without taking `lock_` because - * the strong-pointer ref-count is zero at destruction time, so - * no other thread can be calling the public mutators. + * @note Lifetime contract: every `InfoSub` instance MUST be destroyed + * before the backing `Source`. NetworkOPsImp shutdown drops all + * subscriber strong refs before its own teardown to satisfy this. + * @note Thread-safety: per-instance state is guarded by `lock_`. The + * destructor reads tracking sets without taking `lock_` because + * the strong-pointer ref-count is zero at destruction time, so + * no other thread can be calling the public mutators. */ class InfoSub : public CountedObject { @@ -64,7 +65,8 @@ public: using Consumer = Resource::Consumer; public: - /** Abstracts the source of subscription data. + /** + * Abstracts the source of subscription data. */ class Source { @@ -217,7 +219,8 @@ public: virtual bool tryRemoveRpcSub(std::string const& strUrl) = 0; - /** Journal used by InfoSub for diagnostics that occur after the + /** + * Journal used by InfoSub for diagnostics that occur after the * owning subsystem (e.g. application-level Logs) is the only * surviving sink — primarily destructor-time cleanup failures. */ @@ -249,27 +252,29 @@ public: void deleteSubAccountInfo(AccountID const& account, bool rt); - /** Record that this subscriber is following @p book. + /** + * Record that this subscriber is following @p book. * - * Called by NetworkOPsImp::subBook so that ~InfoSub() can issue a - * matching unsubBook for every book this subscriber is tracking, - * keeping per-subscriber state symmetric with the server-side map. + * Called by NetworkOPsImp::subBook so that ~InfoSub() can issue a + * matching unsubBook for every book this subscriber is tracking, + * keeping per-subscriber state symmetric with the server-side map. * - * @param book The order book this subscriber has just subscribed to. - * @note Idempotent: re-inserting an already-tracked book is a no-op. - * @note Thread-safe: takes InfoSub::lock_. + * @param book The order book this subscriber has just subscribed to. + * @note Idempotent: re-inserting an already-tracked book is a no-op. + * @note Thread-safe: takes InfoSub::lock_. */ void insertBookSubscription(Book const& book); - /** Stop tracking @p book for this subscriber. + /** + * Stop tracking @p book for this subscriber. * - * Called by the unsubscribe RPC handler so that the book is not - * re-unsubscribed by ~InfoSub(). Pairs with insertBookSubscription. + * Called by the unsubscribe RPC handler so that the book is not + * re-unsubscribed by ~InfoSub(). Pairs with insertBookSubscription. * - * @param book The order book to forget. - * @note No-op if @p book was not previously inserted. - * @note Thread-safe: takes InfoSub::lock_. + * @param book The order book to forget. + * @note No-op if @p book was not previously inserted. + * @note Thread-safe: takes InfoSub::lock_. */ void deleteBookSubscription(Book const& book); diff --git a/include/xrpl/server/LoadFeeTrack.h b/include/xrpl/server/LoadFeeTrack.h index a19ca063d5..3afd602241 100644 --- a/include/xrpl/server/LoadFeeTrack.h +++ b/include/xrpl/server/LoadFeeTrack.h @@ -13,15 +13,16 @@ namespace xrpl { struct Fees; -/** Manages the current fee schedule. - - The "base" fee is the cost to send a reference transaction under no load, - expressed in millionths of one XRP. - - The "load" fee is how much the local server currently charges to send a - reference transaction. This fee fluctuates based on the load of the - server. -*/ +/** + * Manages the current fee schedule. + * + * The "base" fee is the cost to send a reference transaction under no load, + * expressed in millionths of one XRP. + * + * The "load" fee is how much the local server currently charges to send a + * reference transaction. This fee fluctuates based on the load of the + * server. + */ class LoadFeeTrack final { public: diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index c58c784de3..710545271a 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -68,22 +68,32 @@ namespace xrpl { struct Manifest { - /// The manifest in serialized form. + /** + * The manifest in serialized form. + */ std::string serialized; - /// The master key associated with this manifest. + /** + * The master key associated with this manifest. + */ PublicKey masterKey; - /// The ephemeral key associated with this manifest. + /** + * The ephemeral key associated with this manifest. + */ // A revoked manifest does not have a signingKey // This field is specified as "optional" in manifestFormat's // SOTemplate std::optional signingKey; - /// The sequence number of this manifest. + /** + * The sequence number of this manifest. + */ std::uint32_t sequence = 0; - /// The domain, if one was specified in the manifest; empty otherwise. + /** + * The domain, if one was specified in the manifest; empty otherwise. + */ std::string domain; Manifest() = delete; @@ -109,46 +119,61 @@ struct Manifest Manifest& operator=(Manifest&& other) = default; - /// Returns `true` if manifest signature is valid + /** + * Returns `true` if manifest signature is valid + */ [[nodiscard]] bool verify() const; - /// Returns hash of serialized manifest data + /** + * Returns hash of serialized manifest data + */ [[nodiscard]] uint256 hash() const; - /// Returns `true` if manifest revokes master key + /** + * Returns `true` if manifest revokes master key + */ // The maximum possible sequence number means that the master key has // been revoked static bool revoked(std::uint32_t sequence); - /// Returns `true` if manifest revokes master key + /** + * Returns `true` if manifest revokes master key + */ [[nodiscard]] bool revoked() const; - /// Returns manifest signature + /** + * Returns manifest signature + */ [[nodiscard]] std::optional getSignature() const; - /// Returns manifest master key signature + /** + * Returns manifest master key signature + */ [[nodiscard]] Blob getMasterSignature() const; }; -/** Format the specified manifest to a string for debugging purposes. */ +/** + * Format the specified manifest to a string for debugging purposes. + */ std::string to_string(Manifest const& m); -/** Constructs Manifest from serialized string - - @param s Serialized manifest string - - @return `std::nullopt` if string is invalid - - @note This does not verify manifest signatures. - `Manifest::verify` should be called after constructing manifest. -*/ +/** + * Constructs Manifest from serialized string + * + * @param s Serialized manifest string + * + * @return `std::nullopt` if string is invalid + * + * @note This does not verify manifest signatures. + * `Manifest::verify` should be called after constructing manifest. + */ /** @{ */ std::optional deserializeManifest(Slice s, beast::Journal journal); @@ -200,19 +225,29 @@ loadValidatorToken( beast::Journal journal = beast::Journal(beast::Journal::getNullSink())); enum class ManifestDisposition { - /// Manifest is valid + /** + * Manifest is valid + */ Accepted = 0, - /// Sequence is too old + /** + * Sequence is too old + */ Stale, - /// The master key is not acceptable to us + /** + * The master key is not acceptable to us + */ BadMasterKey, - /// The ephemeral key is not acceptable to us + /** + * The ephemeral key is not acceptable to us + */ BadEphemeralKey, - /// Timely, but invalid signature + /** + * Timely, but invalid signature + */ Invalid }; @@ -238,17 +273,23 @@ to_string(ManifestDisposition m) class DatabaseCon; -/** Remembers manifests with the highest sequence number. */ +/** + * Remembers manifests with the highest sequence number. + */ class ManifestCache { private: beast::Journal j_; std::shared_mutex mutable mutex_; - /** Active manifests stored by master public key. */ + /** + * Active manifests stored by master public key. + */ hash_map map_; - /** Master public keys stored by current ephemeral public key. */ + /** + * Master public keys stored by current ephemeral public key. + */ hash_map signingToMasterKeys_; std::atomic seq_{0}; @@ -258,104 +299,114 @@ public: { } - /** A monotonically increasing number used to detect new manifests. */ + /** + * A monotonically increasing number used to detect new manifests. + */ std::uint32_t sequence() const { return seq_.load(); } - /** Returns master key's current signing key. - - @param pk Master public key - - @return pk if no known signing key from a manifest - - @par Thread Safety - - May be called concurrently - */ + /** + * Returns master key's current signing key. + * + * @param pk Master public key + * + * @return pk if no known signing key from a manifest + * + * @par Thread Safety + * + * May be called concurrently + */ std::optional getSigningKey(PublicKey const& pk) const; - /** Returns ephemeral signing key's master public key. - - @param pk Ephemeral signing public key - - @return pk if signing key is not in a valid manifest - - @par Thread Safety - - May be called concurrently - */ + /** + * Returns ephemeral signing key's master public key. + * + * @param pk Ephemeral signing public key + * + * @return pk if signing key is not in a valid manifest + * + * @par Thread Safety + * + * May be called concurrently + */ PublicKey getMasterKey(PublicKey const& pk) const; - /** Returns master key's current manifest sequence. - - @return sequence corresponding to Master public key - if configured or std::nullopt otherwise - */ + /** + * Returns master key's current manifest sequence. + * + * @return sequence corresponding to Master public key + * if configured or std::nullopt otherwise + */ std::optional getSequence(PublicKey const& pk) const; - /** Returns domain claimed by a given public key - - @return domain corresponding to Master public key - if present, otherwise std::nullopt - */ + /** + * Returns domain claimed by a given public key + * + * @return domain corresponding to Master public key + * if present, otherwise std::nullopt + */ std::optional getDomain(PublicKey const& pk) const; - /** Returns manifest corresponding to a given public key - - @return manifest corresponding to Master public key - if present, otherwise std::nullopt - */ + /** + * Returns manifest corresponding to a given public key + * + * @return manifest corresponding to Master public key + * if present, otherwise std::nullopt + */ std::optional getManifest(PublicKey const& pk) const; - /** Returns `true` if master key has been revoked in a manifest. - - @param pk Master public key - - @par Thread Safety - - May be called concurrently - */ + /** + * Returns `true` if master key has been revoked in a manifest. + * + * @param pk Master public key + * + * @par Thread Safety + * + * May be called concurrently + */ bool revoked(PublicKey const& pk) const; - /** Add manifest to cache. - - @param m Manifest to add - - @return `ManifestDisposition::accepted` if successful, or - `stale` or `invalid` otherwise - - @par Thread Safety - - May be called concurrently - */ + /** + * Add manifest to cache. + * + * @param m Manifest to add + * + * @return `ManifestDisposition::accepted` if successful, or + * `stale` or `invalid` otherwise + * + * @par Thread Safety + * + * May be called concurrently + */ ManifestDisposition applyManifest(Manifest m); - /** Populate manifest cache with manifests in database and config. - - @param dbCon Database connection with dbTable - - @param dbTable Database table - - @param configManifest Base64 encoded manifest for local node's - validator keys - - @param configRevocation Base64 encoded validator key revocation - from the config - - @par Thread Safety - - May be called concurrently - */ + /** + * Populate manifest cache with manifests in database and config. + * + * @param dbCon Database connection with dbTable + * + * @param dbTable Database table + * + * @param configManifest Base64 encoded manifest for local node's + * validator keys + * + * @param configRevocation Base64 encoded validator key revocation + * from the config + * + * @par Thread Safety + * + * May be called concurrently + */ bool load( DatabaseCon& dbCon, @@ -363,48 +414,51 @@ public: std::string const& configManifest, std::vector const& configRevocation); - /** Populate manifest cache with manifests in database. - - @param dbCon Database connection with dbTable - - @param dbTable Database table - - @par Thread Safety - - May be called concurrently - */ + /** + * Populate manifest cache with manifests in database. + * + * @param dbCon Database connection with dbTable + * + * @param dbTable Database table + * + * @par Thread Safety + * + * May be called concurrently + */ void load(DatabaseCon& dbCon, std::string const& dbTable); - /** Save cached manifests to database. - - @param dbCon Database connection with `ValidatorManifests` table - - @param isTrusted Function that returns true if manifest is trusted - - @par Thread Safety - - May be called concurrently - */ + /** + * Save cached manifests to database. + * + * @param dbCon Database connection with `ValidatorManifests` table + * + * @param isTrusted Function that returns true if manifest is trusted + * + * @par Thread Safety + * + * May be called concurrently + */ void save( DatabaseCon& dbCon, std::string const& dbTable, std::function const& isTrusted); - /** Invokes the callback once for every populated manifest. - - @note Do not call ManifestCache member functions from within the - callback. This can re-lock the mutex from the same thread, which is UB. - @note Do not write ManifestCache member variables from within the - callback. This can lead to data races. - - @param f Function called for each manifest - - @par Thread Safety - - May be called concurrently - */ + /** + * Invokes the callback once for every populated manifest. + * + * @note Do not call ManifestCache member functions from within the + * callback. This can re-lock the mutex from the same thread, which is UB. + * @note Do not write ManifestCache member variables from within the + * callback. This can lead to data races. + * + * @param f Function called for each manifest + * + * @par Thread Safety + * + * May be called concurrently + */ template void forEachManifest(Function&& f) const @@ -417,22 +471,23 @@ public: } } - /** Invokes the callback once for every populated manifest. - - @note Do not call ManifestCache member functions from within the - callback. This can re-lock the mutex from the same thread, which is UB. - @note Do not write ManifestCache member variables from - within the callback. This can lead to data races. - - @param pf Pre-function called with the maximum number of times f will be - called (useful for memory allocations) - - @param f Function called for each manifest - - @par Thread Safety - - May be called concurrently - */ + /** + * Invokes the callback once for every populated manifest. + * + * @note Do not call ManifestCache member functions from within the + * callback. This can re-lock the mutex from the same thread, which is UB. + * @note Do not write ManifestCache member variables from + * within the callback. This can lead to data races. + * + * @param pf Pre-function called with the maximum number of times f will be + * called (useful for memory allocations) + * + * @param f Function called for each manifest + * + * @par Thread Safety + * + * May be called concurrently + */ template void forEachManifest(PreFun&& pf, EachFun&& f) const diff --git a/include/xrpl/server/NetworkOPs.h b/include/xrpl/server/NetworkOPs.h index ed2dbbb220..d3d3bb9f70 100644 --- a/include/xrpl/server/NetworkOPs.h +++ b/include/xrpl/server/NetworkOPs.h @@ -47,35 +47,37 @@ class SHAMap; // there's a functional network. // -/** Specifies the mode under which the server believes it's operating. - - This has implications about how the server processes transactions and - how it responds to requests (e.g. account balance request). - - @note Other code relies on the numerical values of these constants; do - not change them without verifying each use and ensuring that it is - not a breaking change. -*/ +/** + * Specifies the mode under which the server believes it's operating. + * + * This has implications about how the server processes transactions and + * how it responds to requests (e.g. account balance request). + * + * @note Other code relies on the numerical values of these constants; do + * not change them without verifying each use and ensuring that it is + * not a breaking change. + */ enum class OperatingMode { - DISCONNECTED = 0, //!< not ready to process requests - CONNECTED = 1, //!< convinced we are talking to the network - SYNCING = 2, //!< fallen slightly behind - TRACKING = 3, //!< convinced we agree with the network - FULL = 4 //!< we have the ledger and can even validate + DISCONNECTED = 0, ///< not ready to process requests + CONNECTED = 1, ///< convinced we are talking to the network + SYNCING = 2, ///< fallen slightly behind + TRACKING = 3, ///< convinced we agree with the network + FULL = 4 ///< we have the ledger and can even validate }; -/** Provides server functionality for clients. - - Clients include backend applications, local commands, and connected - clients. This class acts as a proxy, fulfilling the command with local - data if possible, or asking the network and returning the results if - needed. - - A backend application or local client can trust a local instance of - xrpld / NetworkOPs. However, client software connecting to non-local - instances of xrpld will need to be hardened to protect against hostile - or unreliable servers. -*/ +/** + * Provides server functionality for clients. + * + * Clients include backend applications, local commands, and connected + * clients. This class acts as a proxy, fulfilling the command with local + * data if possible, or asking the network and returning the results if + * needed. + * + * A backend application or local client can trust a local instance of + * xrpld / NetworkOPs. However, client software connecting to non-local + * instances of xrpld will need to be hardened to protect against hostile + * or unreliable servers. + */ class NetworkOPs : public InfoSub::Source { public: @@ -225,12 +227,13 @@ public: virtual json::Value getLedgerFetchInfo() = 0; - /** Accepts the current transaction tree, return the new ledger's sequence - - This API is only used via RPC with the server in STANDALONE mode and - performs a virtual consensus round, with all the transactions we are - proposing being accepted. - */ + /** + * Accepts the current transaction tree, return the new ledger's sequence + * + * This API is only used via RPC with the server in STANDALONE mode and + * performs a virtual consensus round, with all the transactions we are + * proposing being accepted. + */ virtual std::uint32_t acceptLedger(std::optional consensusDelay = std::nullopt) = 0; @@ -259,15 +262,16 @@ public: virtual void stateAccounting(json::Value& obj) = 0; - /** Total number of (book, subscriber) entries currently tracked. + /** + * Total number of (book, subscriber) entries currently tracked. * - * Counts every weak_ptr stored across every book in subBook_, NOT the - * number of distinct subscribers and NOT the number of distinct - * books: a single subscriber following N books contributes N entries. + * Counts every weak_ptr stored across every book in subBook_, NOT the + * number of distinct subscribers and NOT the number of distinct + * books: a single subscriber following N books contributes N entries. * - * @note Diagnostic accessor; intended for tests and operator visibility - * into per-book subscription state. The returned value is a - * snapshot under the subscription lock. + * @note Diagnostic accessor; intended for tests and operator visibility + * into per-book subscription state. The returned value is a + * snapshot under the subscription lock. */ virtual std::size_t getBookSubscribersCount() = 0; diff --git a/include/xrpl/server/Port.h b/include/xrpl/server/Port.h index c48a2546c1..b8bca6f95f 100644 --- a/include/xrpl/server/Port.h +++ b/include/xrpl/server/Port.h @@ -22,7 +22,9 @@ namespace xrpl { class Section; -/** Configuration information for a Server listening port. */ +/** + * Configuration information for a Server listening port. + */ struct Port { explicit Port() = default; diff --git a/include/xrpl/server/Server.h b/include/xrpl/server/Server.h index 956a414be8..f8d6005d0c 100644 --- a/include/xrpl/server/Server.h +++ b/include/xrpl/server/Server.h @@ -9,7 +9,9 @@ namespace xrpl { -/** Create the HTTP server using the specified handler. */ +/** + * Create the HTTP server using the specified handler. + */ template std::unique_ptr makeServer(Handler& handler, boost::asio::io_context& ioContext, beast::Journal journal) diff --git a/include/xrpl/server/Session.h b/include/xrpl/server/Session.h index 266570862a..be8d9a497c 100644 --- a/include/xrpl/server/Session.h +++ b/include/xrpl/server/Session.h @@ -15,11 +15,12 @@ namespace xrpl { -/** Persistent state information for a connection session. - These values are preserved between calls for efficiency. - Some fields are input parameters, some are output parameters, - and all only become defined during specific callbacks. -*/ +/** + * Persistent state information for a connection session. + * These values are preserved between calls for efficiency. + * Some fields are input parameters, some are output parameters, + * and all only become defined during specific callbacks. + */ class Session { public: @@ -29,29 +30,40 @@ public: operator=(Session const&) = delete; virtual ~Session() = default; - /** A user-definable pointer. - The initial value is always zero. - Changes to the value are persisted between calls. - */ + /** + * A user-definable pointer. + * The initial value is always zero. + * Changes to the value are persisted between calls. + */ void* tag = nullptr; - /** Returns the Journal to use for logging. */ + /** + * Returns the Journal to use for logging. + */ virtual beast::Journal journal() = 0; - /** Returns the Port settings for this connection. */ + /** + * Returns the Port settings for this connection. + */ virtual Port const& port() = 0; - /** Returns the remote address of the connection. */ + /** + * Returns the remote address of the connection. + */ virtual beast::IP::Endpoint remoteAddress() = 0; - /** Returns the current HTTP request. */ + /** + * Returns the current HTTP request. + */ virtual http_request_type& request() = 0; - /** Send a copy of data asynchronously. */ + /** + * Send a copy of data asynchronously. + */ /** @{ */ void write(std::string_view s) @@ -80,32 +92,37 @@ public: /** @} */ - /** Detach the session. - This holds the session open so that the response can be sent - asynchronously. Calls to io_context::run made by the server - will not return until all detached sessions are closed. - */ + /** + * Detach the session. + * This holds the session open so that the response can be sent + * asynchronously. Calls to io_context::run made by the server + * will not return until all detached sessions are closed. + */ virtual std::shared_ptr detach() = 0; - /** Indicate that the response is complete. - The handler should call this when it has completed writing - the response. If Keep-Alive is indicated on the connection, - this will trigger a read for the next request; else, the - connection will be closed when all remaining data has been sent. - */ + /** + * Indicate that the response is complete. + * The handler should call this when it has completed writing + * the response. If Keep-Alive is indicated on the connection, + * this will trigger a read for the next request; else, the + * connection will be closed when all remaining data has been sent. + */ virtual void complete() = 0; - /** Close the session. - This will be performed asynchronously. The session will be - closed gracefully after all pending writes have completed. - @param graceful `true` to wait until all data has finished sending. - */ + /** + * Close the session. + * This will be performed asynchronously. The session will be + * closed gracefully after all pending writes have completed. + * @param graceful `true` to wait until all data has finished sending. + */ virtual void close(bool graceful) = 0; - /** Convert the connection to WebSocket. */ + /** + * Convert the connection to WebSocket. + */ virtual std::shared_ptr websocketUpgrade() = 0; }; diff --git a/include/xrpl/server/SimpleWriter.h b/include/xrpl/server/SimpleWriter.h index 996403eafb..597e6e6d8f 100644 --- a/include/xrpl/server/SimpleWriter.h +++ b/include/xrpl/server/SimpleWriter.h @@ -14,7 +14,9 @@ namespace xrpl { -/// Deprecated: Writer that serializes a HTTP/1 message +/** + * Deprecated: Writer that serializes a HTTP/1 message + */ class SimpleWriter : public Writer { boost::beast::multi_buffer sb_; diff --git a/include/xrpl/server/WSSession.h b/include/xrpl/server/WSSession.h index 0087f9f50f..69f7decdb3 100644 --- a/include/xrpl/server/WSSession.h +++ b/include/xrpl/server/WSSession.h @@ -28,23 +28,24 @@ public: operator=(WSMsg const&) = delete; virtual ~WSMsg() = default; - /** Retrieve message data. - - Returns a tribool indicating whether or not - data is available, and a ConstBufferSequence - representing the data. - - tribool values: - maybe: Data is not ready yet - false: Data is available - true: Data is available, and - it is the last chunk of bytes. - - Derived classes that do not know when the data - ends (for example, when returning the output of a - paged database query) may return `true` and an - empty vector. - */ + /** + * Retrieve message data. + * + * Returns a tribool indicating whether or not + * data is available, and a ConstBufferSequence + * representing the data. + * + * tribool values: + * maybe: Data is not ready yet + * false: Data is available + * true: Data is available, and + * it is the last chunk of bytes. + * + * Derived classes that do not know when the data + * ends (for example, when returning the output of a + * paged database query) may return `true` and an + * empty vector. + */ virtual std::pair> prepare(std::size_t bytes, std::function resume) = 0; }; @@ -106,7 +107,9 @@ struct WSSession [[nodiscard]] virtual boost::asio::ip::tcp::endpoint const& remoteEndpoint() const = 0; - /** Send a WebSockets message. */ + /** + * Send a WebSockets message. + */ virtual void send(std::shared_ptr w) = 0; @@ -116,10 +119,11 @@ struct WSSession virtual void close(boost::beast::websocket::close_reason const& reason) = 0; - /** Indicate that the response is complete. - The handler should call this when it has completed writing - the response. - */ + /** + * Indicate that the response is complete. + * The handler should call this when it has completed writing + * the response. + */ virtual void complete() = 0; }; diff --git a/include/xrpl/server/Wallet.h b/include/xrpl/server/Wallet.h index eea44db200..ed8378989f 100644 --- a/include/xrpl/server/Wallet.h +++ b/include/xrpl/server/Wallet.h @@ -78,19 +78,22 @@ saveManifests( void addValidatorManifest(soci::session& session, std::string const& serialized); -/** Delete any saved public/private key associated with this node. */ +/** + * Delete any saved public/private key associated with this node. + */ void clearNodeIdentity(soci::session& session); -/** Returns a stable public and private key for this node. - - The node's public identity is defined by a secp256k1 keypair - that is (normally) randomly generated. This function will - return such a keypair, securely generating one if needed. - - @param session Session with the database. - - @return Pair of public and private secp256k1 keys. +/** + * Returns a stable public and private key for this node. + * + * The node's public identity is defined by a secp256k1 keypair + * that is (normally) randomly generated. This function will + * return such a keypair, securely generating one if needed. + * + * @param session Session with the database. + * + * @return Pair of public and private secp256k1 keys. */ std::pair getNodeIdentity(soci::session& session); diff --git a/include/xrpl/server/Writer.h b/include/xrpl/server/Writer.h index fe9d9519e1..a80563af43 100644 --- a/include/xrpl/server/Writer.h +++ b/include/xrpl/server/Writer.h @@ -13,26 +13,32 @@ class Writer public: virtual ~Writer() = default; - /** Returns `true` if there is no more data to pull. */ + /** + * Returns `true` if there is no more data to pull. + */ virtual bool complete() = 0; - /** Removes bytes from the input sequence. - - Can be called with 0. - */ + /** + * Removes bytes from the input sequence. + * + * Can be called with 0. + */ virtual void consume(std::size_t bytes) = 0; - /** Add data to the input sequence. - @param bytes A hint to the number of bytes desired. - @param resume A functor to later resume execution. - @return `true` if the writer is ready to provide more data. - */ + /** + * Add data to the input sequence. + * @param bytes A hint to the number of bytes desired. + * @param resume A functor to later resume execution. + * @return `true` if the writer is ready to provide more data. + */ virtual bool prepare(std::size_t bytes, std::function resume) = 0; - /** Returns a ConstBufferSequence representing the input sequence. */ + /** + * Returns a ConstBufferSequence representing the input sequence. + */ virtual std::vector data() = 0; }; diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h index 7b35dbd4be..c7553c1da3 100644 --- a/include/xrpl/server/detail/BaseHTTPPeer.h +++ b/include/xrpl/server/detail/BaseHTTPPeer.h @@ -35,7 +35,9 @@ namespace xrpl { -/** Represents an active connection. */ +/** + * Represents an active connection. + */ template class BaseHTTPPeer : public IOList::Work, public Session { diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h index d557140bd4..59a866ab8c 100644 --- a/include/xrpl/server/detail/BaseWSPeer.h +++ b/include/xrpl/server/detail/BaseWSPeer.h @@ -30,7 +30,9 @@ namespace xrpl { -/** Represents an active WebSocket connection. */ +/** + * Represents an active WebSocket connection. + */ template class BaseWSPeer : public BasePeer, public WSSession { @@ -48,9 +50,11 @@ private: boost::beast::multi_buffer rb_; boost::beast::multi_buffer wb_; std::list> wq_; - /// The socket has been closed, or will close after the next write - /// finishes. Do not do any more writes, and don't try to close - /// again. + /** + * The socket has been closed, or will close after the next write + * finishes. Do not do any more writes, and don't try to close + * again. + */ bool doClose_ = false; boost::beast::websocket::close_reason cr_; waitable_timer timer_; diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h index d2d7a7baf4..285bde14ac 100644 --- a/include/xrpl/server/detail/Door.h +++ b/include/xrpl/server/detail/Door.h @@ -40,7 +40,9 @@ namespace xrpl { -/** A listening socket. */ +/** + * A listening socket. + */ template class Door : public IOList::Work, public std::enable_shared_from_this> { @@ -129,12 +131,13 @@ public: void run(); - /** Close the Door listening socket and connections. - The listening socket is closed, and all open connections - belonging to the Door are closed. - Thread Safety: - May be called concurrently - */ + /** + * Close the Door listening socket and connections. + * The listening socket is closed, and all open connections + * belonging to the Door are closed. + * Thread Safety: + * May be called concurrently + */ void close() override; diff --git a/include/xrpl/server/detail/ServerImpl.h b/include/xrpl/server/detail/ServerImpl.h index df2bba0284..c2e411cf9b 100644 --- a/include/xrpl/server/detail/ServerImpl.h +++ b/include/xrpl/server/detail/ServerImpl.h @@ -26,39 +26,45 @@ namespace xrpl { using Endpoints = std::unordered_map; -/** A multi-protocol server. - - This server maintains multiple configured listening ports, - with each listening port allows for multiple protocols including - HTTP, HTTP/S, WebSocket, Secure WebSocket, and the Peer protocol. -*/ +/** + * A multi-protocol server. + * + * This server maintains multiple configured listening ports, + * with each listening port allows for multiple protocols including + * HTTP, HTTP/S, WebSocket, Secure WebSocket, and the Peer protocol. + */ class Server { public: - /** Destroy the server. - The server is closed if it is not already closed. This call - blocks until the server has stopped. - */ + /** + * Destroy the server. + * The server is closed if it is not already closed. This call + * blocks until the server has stopped. + */ virtual ~Server() = default; - /** Returns the Journal associated with the server. */ + /** + * Returns the Journal associated with the server. + */ virtual beast::Journal journal() = 0; - /** Set the listening port settings. - This may only be called once. - */ + /** + * Set the listening port settings. + * This may only be called once. + */ virtual Endpoints ports(std::vector const& v) = 0; - /** Close the server. - The close is performed asynchronously. The handler will be notified - when the server has stopped. The server is considered stopped when - there are no pending I/O completion handlers and all connections - have closed. - Thread safety: - Safe to call concurrently from any thread. - */ + /** + * Close the server. + * The close is performed asynchronously. The handler will be notified + * when the server has stopped. The server is considered stopped when + * there are no pending I/O completion handlers and all connections + * have closed. + * Thread safety: + * Safe to call concurrently from any thread. + */ virtual void close() = 0; }; diff --git a/include/xrpl/server/detail/io_list.h b/include/xrpl/server/detail/io_list.h index 0153bd3457..7ffa85b898 100644 --- a/include/xrpl/server/detail/io_list.h +++ b/include/xrpl/server/detail/io_list.h @@ -12,7 +12,9 @@ namespace xrpl { -/** Manages a set of objects performing asynchronous I/O. */ +/** + * Manages a set of objects performing asynchronous I/O. + */ class IOList final { public: @@ -31,12 +33,13 @@ public: destroy(); } - /** Return the IOList associated with the work. - - Requirements: - The call to IOList::emplace to - create the work has already returned. - */ + /** + * Return the IOList associated with the work. + * + * Requirements: + * The call to IOList::emplace to + * create the work has already returned. + */ IOList& ios() { @@ -62,71 +65,74 @@ private: public: IOList() = default; - /** Destroy the list. - - Effects: - Closes the IOList if it was not previously - closed. No finisher is invoked in this case. - - Blocks until all work is destroyed. - */ + /** + * Destroy the list. + * + * Effects: + * Closes the IOList if it was not previously + * closed. No finisher is invoked in this case. + * + * Blocks until all work is destroyed. + */ ~IOList() { destroy(); } - /** Return `true` if the list is closed. - - Thread Safety: - Undefined result if called concurrently - with close(). - */ + /** + * Return `true` if the list is closed. + * + * Thread Safety: + * Undefined result if called concurrently + * with close(). + */ [[nodiscard]] bool closed() const { return closed_; } - /** Create associated work if not closed. - - Requirements: - `std::is_base_of_v == true` - - Thread Safety: - May be called concurrently. - - Effects: - Atomically creates, inserts, and returns new - work T, or returns nullptr if the io_list is - closed, - - If the call succeeds and returns a new object, - it is guaranteed that a subsequent call to close - will invoke Work::close on the object. - - */ + /** + * Create associated work if not closed. + * + * Requirements: + * `std::is_base_of_v == true` + * + * Thread Safety: + * May be called concurrently. + * + * Effects: + * Atomically creates, inserts, and returns new + * work T, or returns nullptr if the io_list is + * closed, + * + * If the call succeeds and returns a new object, + * it is guaranteed that a subsequent call to close + * will invoke Work::close on the object. + */ template std::shared_ptr emplace(Args&&... args); - /** Cancel active I/O. - - Thread Safety: - May not be called concurrently. - - Effects: - Associated work is closed. - - Finisher if provided, will be called when - all associated work is destroyed. The finisher - may be called from a foreign thread, or within - the call to this function. - - Only the first call to close will set the - finisher. - - No effect after the first call. - */ + /** + * Cancel active I/O. + * + * Thread Safety: + * May not be called concurrently. + * + * Effects: + * Associated work is closed. + * + * Finisher if provided, will be called when + * all associated work is destroyed. The finisher + * may be called from a foreign thread, or within + * the call to this function. + * + * Only the first call to close will set the + * finisher. + * + * No effect after the first call. + */ template void close(Finisher&& f); @@ -137,20 +143,21 @@ public: close([] {}); } - /** Block until the io_list stops. - - Effects: - The caller is blocked until the io_list is - closed and all associated work is destroyed. - - Thread safety: - May be called concurrently. - - Preconditions: - No call to io_context::run on any io_context - used by work objects associated with this io_list - exists in the caller's call stack. - */ + /** + * Block until the io_list stops. + * + * Effects: + * The caller is blocked until the io_list is + * closed and all associated work is destroyed. + * + * Thread safety: + * May be called concurrently. + * + * Preconditions: + * No call to io_context::run on any io_context + * used by work objects associated with this io_list + * exists in the caller's call stack. + */ template void join(); diff --git a/include/xrpl/shamap/Family.h b/include/xrpl/shamap/Family.h index c5bf953bfd..7624b3e600 100644 --- a/include/xrpl/shamap/Family.h +++ b/include/xrpl/shamap/Family.h @@ -35,18 +35,23 @@ public: virtual beast::Journal const& journal() = 0; - /** Return a pointer to the Family Full Below Cache */ + /** + * Return a pointer to the Family Full Below Cache + */ virtual std::shared_ptr getFullBelowCache() = 0; - /** Return a pointer to the Family Tree Node Cache */ + /** + * Return a pointer to the Family Tree Node Cache + */ virtual std::shared_ptr getTreeNodeCache() = 0; virtual void sweep() = 0; - /** Acquire ledger that has a missing node by ledger sequence + /** + * Acquire ledger that has a missing node by ledger sequence * * @param refNum Sequence of ledger to acquire. * @param nodeHash Hash of missing node to report in throw. @@ -54,7 +59,8 @@ public: virtual void missingNodeAcquireBySeq(std::uint32_t refNum, uint256 const& nodeHash) = 0; - /** Acquire ledger that has a missing node by ledger hash + /** + * Acquire ledger that has a missing node by ledger hash * * @param refHash Hash of ledger to acquire. * @param refNum Ledger sequence with missing node. diff --git a/include/xrpl/shamap/FullBelowCache.h b/include/xrpl/shamap/FullBelowCache.h index b6d1142eb4..1bb67c7453 100644 --- a/include/xrpl/shamap/FullBelowCache.h +++ b/include/xrpl/shamap/FullBelowCache.h @@ -17,9 +17,10 @@ namespace xrpl { namespace detail { -/** Remembers which tree keys have all descendants resident. - This optimizes the process of acquiring a complete tree. -*/ +/** + * Remembers which tree keys have all descendants resident. + * This optimizes the process of acquiring a complete tree. + */ class BasicFullBelowCache { private: @@ -31,13 +32,14 @@ public: using key_type = uint256; using clock_type = CacheType::clock_type; - /** Construct the cache. - - @param name A label for diagnostics and stats reporting. - @param collector The collector to use for reporting stats. - @param targetSize The cache target size. - @param targetExpirationSeconds The expiration time for items. - */ + /** + * Construct the cache. + * + * @param name A label for diagnostics and stats reporting. + * @param collector The collector to use for reporting stats. + * @param targetSize The cache target size. + * @param targetExpirationSeconds The expiration time for items. + */ BasicFullBelowCache( std::string const& name, clock_type& clock, @@ -49,59 +51,67 @@ public: { } - /** Return the clock associated with the cache. */ + /** + * Return the clock associated with the cache. + */ clock_type& clock() { return cache_.clock(); } - /** Return the number of elements in the cache. - Thread safety: - Safe to call from any thread. - */ + /** + * Return the number of elements in the cache. + * Thread safety: + * Safe to call from any thread. + */ std::size_t size() const { return cache_.size(); } - /** Remove expired cache items. - Thread safety: - Safe to call from any thread. - */ + /** + * Remove expired cache items. + * Thread safety: + * Safe to call from any thread. + */ void sweep() { cache_.sweep(); } - /** Refresh the last access time of an item, if it exists. - Thread safety: - Safe to call from any thread. - @param key The key to refresh. - @return `true` If the key exists. - */ + /** + * Refresh the last access time of an item, if it exists. + * Thread safety: + * Safe to call from any thread. + * @param key The key to refresh. + * @return `true` If the key exists. + */ bool touchIfExists(key_type const& key) { return cache_.touchIfExists(key); } - /** Insert a key into the cache. - If the key already exists, the last access time will still - be refreshed. - Thread safety: - Safe to call from any thread. - @param key The key to insert. - */ + /** + * Insert a key into the cache. + * If the key already exists, the last access time will still + * be refreshed. + * Thread safety: + * Safe to call from any thread. + * @param key The key to insert. + */ void insert(key_type const& key) { cache_.insert(key); } - /** generation determines whether cached entry is valid */ + /** + * generation determines whether cached entry is valid + */ std::uint32_t getGeneration() const { diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h index d49e323b3f..a1194ccfd3 100644 --- a/include/xrpl/shamap/SHAMap.h +++ b/include/xrpl/shamap/SHAMap.h @@ -38,55 +38,62 @@ namespace xrpl { class SHAMapNodeID; class SHAMapSyncFilter; -/** Describes the current state of a given SHAMap */ +/** + * Describes the current state of a given SHAMap + */ enum class SHAMapState { - /** The map is in flux and objects can be added and removed. - - Example: map underlying the open ledger. + /** + * The map is in flux and objects can be added and removed. + * + * Example: map underlying the open ledger. */ Modifying = 0, - /** The map is set in stone and cannot be changed. - - Example: a map underlying a given closed ledger. + /** + * The map is set in stone and cannot be changed. + * + * Example: a map underlying a given closed ledger. */ Immutable = 1, - /** The map's hash is fixed but valid nodes may be missing and can be added. - - Example: a map that's syncing a given peer's closing ledger. + /** + * The map's hash is fixed but valid nodes may be missing and can be added. + * + * Example: a map that's syncing a given peer's closing ledger. */ Synching = 2, - /** The map is known to not be valid. - - Example: usually synching a corrupt ledger. + /** + * The map is known to not be valid. + * + * Example: usually synching a corrupt ledger. */ Invalid = 3, }; -/** A SHAMap is both a radix tree with a fan-out of 16 and a Merkle tree. - - A radix tree is a tree with two properties: - - 1. The key for a node is represented by the node's position in the tree - (the "prefix property"). - 2. A node with only one child is merged with that child - (the "merge property") - - These properties result in a significantly smaller memory footprint for - a radix tree. - - A fan-out of 16 means that each node in the tree has at most 16 - children. See https://en.wikipedia.org/wiki/Radix_tree - - A Merkle tree is a tree where each non-leaf node is labelled with the hash - of the combined labels of its children nodes. - - A key property of a Merkle tree is that testing for node inclusion is - O(log(N)) where N is the number of nodes in the tree. - - See https://en.wikipedia.org/wiki/Merkle_tree +/** + * A SHAMap is both a radix tree with a fan-out of 16 and a Merkle tree. + * + * A radix tree is a tree with two properties: + * + * 1. The key for a node is represented by the node's position in the tree + * (the "prefix property"). + * 2. A node with only one child is merged with that child + * (the "merge property") + * + * These properties result in a significantly smaller memory footprint for + * a radix tree. + * + * A fan-out of 16 means that each node in the tree has at most 16 + * children. See https://en.wikipedia.org/wiki/Radix_tree + * + * A Merkle tree is a tree where each non-leaf node is labelled with the hash + * of the combined labels of its children nodes. + * + * A key property of a Merkle tree is that testing for node inclusion is + * O(log(N)) where N is the number of nodes in the tree. + * + * See https://en.wikipedia.org/wiki/Merkle_tree */ class SHAMap { @@ -94,10 +101,14 @@ private: Family& f_; beast::Journal journal_; - /** ID to distinguish this map for all others we're sharing nodes with. */ + /** + * ID to distinguish this map for all others we're sharing nodes with. + */ std::uint32_t cowid_ = 1; - /** The sequence of the ledger that this map references, if any. */ + /** + * The sequence of the ledger that this map references, if any. + */ std::uint32_t ledgerSeq_ = 0; SHAMapTreeNodePtr root_; @@ -107,11 +118,15 @@ private: mutable bool full_ = false; // Map is believed complete in database public: - /** Number of children each non-leaf node has (the 'radix tree' part of the - * map) */ + /** + * Number of children each non-leaf node has (the 'radix tree' part of the + * map) + */ static constexpr unsigned int kBranchFactor = SHAMapInnerNode::kBranchFactor; - /** The depth of the hash map: data is only present in the leaves */ + /** + * The depth of the hash map: data is only present in the leaves + */ static constexpr unsigned int kLeafDepth = 64; using DeltaItem = @@ -147,10 +162,11 @@ public: //-------------------------------------------------------------------------- - /** Iterator to a SHAMap's leaves - This is always a const iterator. - Meets the requirements of ForwardRange. - */ + /** + * Iterator to a SHAMap's leaves + * This is always a const iterator. + * Meets the requirements of ForwardRange. + */ class ConstIterator; ConstIterator @@ -180,7 +196,9 @@ public: // normal hash access functions - /** Does the tree have an item with the given ID? */ + /** + * Does the tree have an item with the given ID? + */ bool hasItem(uint256 const& id) const; @@ -208,60 +226,66 @@ public: peekItem(uint256 const& id, SHAMapHash& hash) const; // traverse functions - /** Find the first item after the given item. - - @param id the identifier of the item. - - @note The item does not need to exist. + /** + * Find the first item after the given item. + * + * @param id the identifier of the item. + * + * @note The item does not need to exist. */ ConstIterator upperBound(uint256 const& id) const; - /** Find the object with the greatest object id smaller than the input id. - - @param id the identifier of the item. - - @note The item does not need to exist. + /** + * Find the object with the greatest object id smaller than the input id. + * + * @param id the identifier of the item. + * + * @note The item does not need to exist. */ ConstIterator lowerBound(uint256 const& id) const; - /** Visit every node in this SHAMap - - @param function called with every node visited. - If function returns false, visitNodes exits. - */ + /** + * Visit every node in this SHAMap + * + * @param function called with every node visited. + * If function returns false, visitNodes exits. + */ void visitNodes(std::function const& function) const; - /** Visit every node in this SHAMap that - is not present in the specified SHAMap - - @param function called with every node visited. - If function returns false, visitDifferences exits. - */ + /** + * Visit every node in this SHAMap that + * is not present in the specified SHAMap + * + * @param function called with every node visited. + * If function returns false, visitDifferences exits. + */ void visitDifferences(SHAMap const* have, std::function const&) const; - /** Visit every leaf node in this SHAMap - - @param function called with every non inner node visited. - */ + /** + * Visit every leaf node in this SHAMap + * + * @param function called with every non inner node visited. + */ void visitLeaves(std::function const&)> const&) const; // comparison/sync functions - /** Check for nodes in the SHAMap not available - - Traverse the SHAMap efficiently, maximizing I/O - concurrency, to discover nodes referenced in the - SHAMap but not available locally. - - @param maxNodes The maximum number of found nodes to return - @param filter The filter to use when retrieving nodes - @param return The nodes known to be missing - */ + /** + * Check for nodes in the SHAMap not available + * + * Traverse the SHAMap efficiently, maximizing I/O + * concurrency, to discover nodes referenced in the + * SHAMap but not available locally. + * + * @param maxNodes The maximum number of found nodes to return + * @param filter The filter to use when retrieving nodes + * @param return The nodes known to be missing + */ std::vector> getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter); @@ -291,7 +315,9 @@ public: static bool verifyProofPath(uint256 const& rootHash, uint256 const& key, std::vector const& path); - /** Serializes the root in a format appropriate for sending over the wire */ + /** + * Serializes the root in a format appropriate for sending over the wire + */ void serializeRoot(Serializer& s) const; @@ -317,11 +343,15 @@ public: bool compare(SHAMap const& otherMap, Delta& differences, int maxCount) const; - /** Convert any modified nodes to shared. */ + /** + * Convert any modified nodes to shared. + */ int unshare(); - /** Flush modified nodes to the nodestore and convert them to shared. */ + /** + * Flush modified nodes to the nodestore and convert them to shared. + */ int flushDirty(NodeObjectType t); @@ -364,30 +394,42 @@ private: SHAMapTreeNodePtr checkFilter(SHAMapHash const& hash, SHAMapSyncFilter const* filter) const; - /** Update hashes up to the root */ + /** + * Update hashes up to the root + */ void dirtyUp(SharedPtrNodeStack& stack, uint256 const& target, SHAMapTreeNodePtr terminal); - /** Walk towards the specified id, returning the node. Caller must check - if the return is nullptr, and if not, if the node->peekItem()->key() == - id */ + /** + * Walk towards the specified id, returning the node. Caller must check + * if the return is nullptr, and if not, if the node->peekItem()->key() == + * id + */ SHAMapLeafNode* walkTowardsKey(uint256 const& id, SharedPtrNodeStack* stack = nullptr) const; - /** Return nullptr if key not found */ + /** + * Return nullptr if key not found + */ SHAMapLeafNode* findKey(uint256 const& id) const; - /** Unshare the node, allowing it to be modified */ + /** + * Unshare the node, allowing it to be modified + */ template intr_ptr::SharedPtr unshareNode(intr_ptr::SharedPtr, SHAMapNodeID const& nodeID); - /** prepare a node to be modified before flushing */ + /** + * prepare a node to be modified before flushing + */ template intr_ptr::SharedPtr preFlushNode(intr_ptr::SharedPtr node) const; - /** write and canonicalize modified node */ + /** + * write and canonicalize modified node + */ SHAMapTreeNodePtr writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const; @@ -442,7 +484,9 @@ private: SHAMapTreeNodePtr descendNoStore(SHAMapInnerNode&, int branch) const; - /** If there is only one leaf below this node, get its contents */ + /** + * If there is only one leaf below this node, get its contents + */ boost::intrusive_ptr const& onlyBelow(SHAMapTreeNode*) const; diff --git a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h index ee81107f61..96c853eb28 100644 --- a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h +++ b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h @@ -15,7 +15,9 @@ namespace xrpl { -/** A leaf node for a state object. */ +/** + * A leaf node for a state object. + */ class SHAMapAccountStateLeafNode final : public SHAMapLeafNode, public CountedObject { diff --git a/include/xrpl/shamap/SHAMapInnerNode.h b/include/xrpl/shamap/SHAMapInnerNode.h index 0fb4e24077..44d3bd6279 100644 --- a/include/xrpl/shamap/SHAMapInnerNode.h +++ b/include/xrpl/shamap/SHAMapInnerNode.h @@ -18,63 +18,72 @@ namespace xrpl { class SHAMapInnerNode final : public SHAMapTreeNode, public CountedObject { public: - /** Each inner node has 16 children (the 'radix tree' part of the map) */ + /** + * Each inner node has 16 children (the 'radix tree' part of the map) + */ static constexpr unsigned int kBranchFactor = 16; private: - /** Opaque type that contains the `hashes` array (array of type - `SHAMapHash`) and the `children` array (array of type - `intr_ptr::SharedPtr`). + /** + * Opaque type that contains the `hashes` array (array of type + * `SHAMapHash`) and the `children` array (array of type + * `intr_ptr::SharedPtr`). */ TaggedPointer hashesAndChildren_; std::uint32_t fullBelowGen_ = 0; std::uint16_t isBranch_ = 0; - /** A bitlock for the children of this node, with one bit per child */ + /** + * A bitlock for the children of this node, with one bit per child + */ mutable std::atomic lock_ = 0; - /** Convert arrays stored in `hashesAndChildren_` so they can store the - requested number of children. - - @param toAllocate allocate space for at least this number of children - (must be <= branchFactor) - - @note the arrays may allocate more than the requested value in - `toAllocate`. This is due to the implementation of TagPointer, which - only supports allocating arrays of 4 different sizes. + /** + * Convert arrays stored in `hashesAndChildren_` so they can store the + * requested number of children. + * + * @param toAllocate allocate space for at least this number of children + * (must be <= branchFactor) + * + * @note the arrays may allocate more than the requested value in + * `toAllocate`. This is due to the implementation of TagPointer, which + * only supports allocating arrays of 4 different sizes. */ void resizeChildArrays(std::uint8_t toAllocate); - /** Get the child's index inside the `hashes` or `children` array (stored in - `hashesAndChildren_`. - - These arrays may or may not be sparse). The optional will be empty is an - empty branch is requested and the arrays are sparse. - - @param i index of the requested child + /** + * Get the child's index inside the `hashes` or `children` array (stored in + * `hashesAndChildren_`. + * + * These arrays may or may not be sparse). The optional will be empty is an + * empty branch is requested and the arrays are sparse. + * + * @param i index of the requested child */ std::optional getChildIndex(int i) const; - /** Call the `f` callback for all 16 (branchFactor) branches - even if - the branch is empty. - - @param f a one parameter callback function. The parameter is the - child's hash. - */ + /** + * Call the `f` callback for all 16 (branchFactor) branches - even if + * the branch is empty. + * + * @param f a one parameter callback function. The parameter is the + * child's hash. + */ template void iterChildren(F&& f) const; - /** Call the `f` callback for all non-empty branches. - - @param f a two parameter callback function. The first parameter is - the branch number, the second parameter is the index into the array. - For dense formats these are the same, for sparse they may be - different. - */ + /** + * Call the `f` callback for all non-empty branches. + * + * @param f a two parameter callback function. The first parameter is + * the branch number, the second parameter is the index into the array. + * For dense formats these are the same, for sparse they may be + * different. + */ template void iterNonEmptyChildIndexes(F&& f) const; @@ -149,7 +158,9 @@ public: void updateHash() override; - /** Recalculate the hash of all children and this node. */ + /** + * Recalculate the hash of all children and this node. + */ void updateHashDeep(); diff --git a/include/xrpl/shamap/SHAMapItem.h b/include/xrpl/shamap/SHAMapItem.h index 846f0bab4e..d45d75a942 100644 --- a/include/xrpl/shamap/SHAMapItem.h +++ b/include/xrpl/shamap/SHAMapItem.h @@ -138,6 +138,7 @@ intrusive_ptr_release(SHAMapItem const* x) // If the slabber doesn't claim this pointer, it was allocated // manually, so we free it manually. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) if (!detail::gSlabber.deallocate(const_cast(p))) delete[] p; } diff --git a/include/xrpl/shamap/SHAMapLeafNode.h b/include/xrpl/shamap/SHAMapLeafNode.h index 112c243131..26cfde9fe8 100644 --- a/include/xrpl/shamap/SHAMapLeafNode.h +++ b/include/xrpl/shamap/SHAMapLeafNode.h @@ -46,11 +46,12 @@ public: boost::intrusive_ptr const& peekItem() const; - /** Set the item that this node points to and update the node's hash. - - @param i the new item - @return false if the change was, effectively, a noop (that is, if the - hash was unchanged); true otherwise. + /** + * Set the item that this node points to and update the node's hash. + * + * @param i the new item + * @return false if the change was, effectively, a noop (that is, if the + * hash was unchanged); true otherwise. */ bool setItem(boost::intrusive_ptr i); diff --git a/include/xrpl/shamap/SHAMapNodeID.h b/include/xrpl/shamap/SHAMapNodeID.h index b812c10ca9..6094892091 100644 --- a/include/xrpl/shamap/SHAMapNodeID.h +++ b/include/xrpl/shamap/SHAMapNodeID.h @@ -12,7 +12,9 @@ namespace xrpl { -/** Identifies a node inside a SHAMap */ +/** + * Identifies a node inside a SHAMap + */ class SHAMapNodeID : public CountedObject { private: @@ -64,7 +66,9 @@ public: createID(int depth, uint256 const& key); // FIXME-C++20: use spaceship and operator synthesis - /** Comparison operators */ + /** + * Comparison operators + */ bool operator<(SHAMapNodeID const& n) const { @@ -117,7 +121,8 @@ operator<<(std::ostream& out, SHAMapNodeID const& node) return out << to_string(node); } -/** Return an object representing a serialized SHAMap Node ID +/** + * Return an object representing a serialized SHAMap Node ID * * @param s A string of bytes * @param data a non-null pointer to a buffer of @param size bytes. @@ -136,7 +141,9 @@ deserializeSHAMapNodeID(std::string_view s) } /** @} */ -/** Returns the branch that would contain the given hash */ +/** + * Returns the branch that would contain the given hash + */ [[nodiscard]] unsigned int selectBranch(SHAMapNodeID const& id, uint256 const& hash); diff --git a/include/xrpl/shamap/SHAMapSyncFilter.h b/include/xrpl/shamap/SHAMapSyncFilter.h index b6ce175915..60f340db3c 100644 --- a/include/xrpl/shamap/SHAMapSyncFilter.h +++ b/include/xrpl/shamap/SHAMapSyncFilter.h @@ -7,7 +7,9 @@ #include #include -/** Callback for filtering SHAMap during sync. */ +/** + * Callback for filtering SHAMap during sync. + */ namespace xrpl { class SHAMapSyncFilter diff --git a/include/xrpl/shamap/SHAMapTreeNode.h b/include/xrpl/shamap/SHAMapTreeNode.h index 1eebbaa17f..c8b242238a 100644 --- a/include/xrpl/shamap/SHAMapTreeNode.h +++ b/include/xrpl/shamap/SHAMapTreeNode.h @@ -39,18 +39,20 @@ class SHAMapTreeNode : public IntrusiveRefCounts protected: SHAMapHash hash_; - /** Determines the owning SHAMap, if any. Used for copy-on-write semantics. - - If this value is 0, the node is not dirty and does not need to be - flushed. It is eligible for sharing and may be included multiple - SHAMap instances. + /** + * Determines the owning SHAMap, if any. Used for copy-on-write semantics. + * + * If this value is 0, the node is not dirty and does not need to be + * flushed. It is eligible for sharing and may be included multiple + * SHAMap instances. */ std::uint32_t cowid_; - /** Construct a node - - @param cowid The identifier of a SHAMap. For more, see #cowid_ - @param hash The hash associated with this node, if any. + /** + * Construct a node + * + * @param cowid The identifier of a SHAMap. For more, see #cowid_ + * @param hash The hash associated with this node, if any. */ /** @{ */ explicit SHAMapTreeNode(std::uint32_t cowid) noexcept : cowid_(cowid) @@ -74,28 +76,30 @@ public: virtual void partialDestructor() {}; - /** \defgroup SHAMap Copy-on-Write Support - - By nature, a node may appear in multiple SHAMap instances. Rather - than actually duplicating these nodes, SHAMap opts to be memory - efficient and uses copy-on-write semantics for nodes. - - Only nodes that are not modified and don't need to be flushed back - can be shared. Once a node needs to be changed, it must first be - copied and the copy must marked as not shareable. - - Note that just because a node may not be *owned* by a given SHAMap - instance does not mean that the node is NOT a part of any SHAMap. It - only means that the node is not owned exclusively by any one SHAMap. - - For more on copy-on-write, check out: - https://en.wikipedia.org/wiki/Copy-on-write + /** + * @defgroup SHAMap Copy-on-Write Support + * + * By nature, a node may appear in multiple SHAMap instances. Rather + * than actually duplicating these nodes, SHAMap opts to be memory + * efficient and uses copy-on-write semantics for nodes. + * + * Only nodes that are not modified and don't need to be flushed back + * can be shared. Once a node needs to be changed, it must first be + * copied and the copy must marked as not shareable. + * + * Note that just because a node may not be *owned* by a given SHAMap + * instance does not mean that the node is NOT a part of any SHAMap. It + * only means that the node is not owned exclusively by any one SHAMap. + * + * For more on copy-on-write, check out: + * https://en.wikipedia.org/wiki/Copy-on-write */ /** @{ */ - /** Returns the SHAMap that owns this node. - - @return the ID of the SHAMap that owns this node, or 0 if the - node is not owned by any SHAMap and is a candidate for sharing. + /** + * Returns the SHAMap that owns this node. + * + * @return the ID of the SHAMap that owns this node, or 0 if the + * node is not owned by any SHAMap and is a candidate for sharing. */ std::uint32_t cowid() const @@ -103,10 +107,11 @@ public: return cowid_; } - /** If this node is shared with another map, mark it as no longer shared. - - Only nodes that are not modified and do not need to be flushed back - should be marked as unshared. + /** + * If this node is shared with another map, mark it as no longer shared. + * + * Only nodes that are not modified and do not need to be flushed back + * should be marked as unshared. */ void unshare() @@ -114,39 +119,55 @@ public: cowid_ = 0; } - /** Make a copy of this node, setting the owner. */ + /** + * Make a copy of this node, setting the owner. + */ virtual SHAMapTreeNodePtr clone(std::uint32_t cowid) const = 0; /** @} */ - /** Recalculate the hash of this node. */ + /** + * Recalculate the hash of this node. + */ virtual void updateHash() = 0; - /** Return the hash of this node. */ + /** + * Return the hash of this node. + */ SHAMapHash const& getHash() const { return hash_; } - /** Determines the type of node. */ + /** + * Determines the type of node. + */ virtual SHAMapNodeType getType() const = 0; - /** Determines if this is a leaf node. */ + /** + * Determines if this is a leaf node. + */ virtual bool isLeaf() const = 0; - /** Determines if this is an inner node. */ + /** + * Determines if this is an inner node. + */ virtual bool isInner() const = 0; - /** Serialize the node in a format appropriate for sending over the wire */ + /** + * Serialize the node in a format appropriate for sending over the wire + */ virtual void serializeForWire(Serializer&) const = 0; - /** Serialize the node in a format appropriate for hashing */ + /** + * Serialize the node in a format appropriate for hashing + */ virtual void serializeWithPrefix(Serializer&) const = 0; diff --git a/include/xrpl/shamap/SHAMapTxLeafNode.h b/include/xrpl/shamap/SHAMapTxLeafNode.h index 86186434f8..9b9ac2f996 100644 --- a/include/xrpl/shamap/SHAMapTxLeafNode.h +++ b/include/xrpl/shamap/SHAMapTxLeafNode.h @@ -15,7 +15,9 @@ namespace xrpl { -/** A leaf node for a transaction. No metadata is included. */ +/** + * A leaf node for a transaction. No metadata is included. + */ class SHAMapTxLeafNode final : public SHAMapLeafNode, public CountedObject { public: diff --git a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h index 9e4573d45b..6f8a7ebfda 100644 --- a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h +++ b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h @@ -15,7 +15,9 @@ namespace xrpl { -/** A leaf node for a transaction and its associated metadata. */ +/** + * A leaf node for a transaction and its associated metadata. + */ class SHAMapTxPlusMetaLeafNode final : public SHAMapLeafNode, public CountedObject { diff --git a/include/xrpl/shamap/detail/TaggedPointer.h b/include/xrpl/shamap/detail/TaggedPointer.h index 79f3464d5b..509e6cc58d 100644 --- a/include/xrpl/shamap/detail/TaggedPointer.h +++ b/include/xrpl/shamap/detail/TaggedPointer.h @@ -12,111 +12,122 @@ namespace xrpl { -/** TaggedPointer is a combination of a pointer and a mask stored in the - lowest two bits. - - Since pointers do not have arbitrary alignment, the lowest bits in the - pointer are guaranteed to be zero. TaggedPointer stores information in these - low bits. When dereferencing the pointer, these low "tag" bits are set to - zero. When accessing the tag bits, the high "pointer" bits are set to zero. - - The "pointer" part points to the equivalent to an array of - `SHAMapHash` followed immediately by an array of - `shared_ptr`. The sizes of these arrays are - determined by the tag. The tag is an index into an array (`boundaries`, - defined in the cpp file) that specifies the size. Both arrays are the - same size. Note that the sizes may be smaller than the full 16 elements - needed to explicitly store all the children. In this case, the arrays - only store the non-empty children. The non-empty children are stored in - index order. For example, if only children `2` and `14` are non-empty, a - two-element array would store child `2` in array index 0 and child `14` - in array index 1. There are functions to convert between a child's tree - index and the child's index in a sparse array. - - The motivation for this class is saving RAM. A large percentage of inner - nodes only store a small number of children. Memory can be saved by - storing the inner node's children in sparse arrays. Measurements show - that on average a typical SHAMap's inner nodes can be stored using only - 25% of the original space. -*/ +/** + * TaggedPointer is a combination of a pointer and a mask stored in the + * lowest two bits. + * + * Since pointers do not have arbitrary alignment, the lowest bits in the + * pointer are guaranteed to be zero. TaggedPointer stores information in these + * low bits. When dereferencing the pointer, these low "tag" bits are set to + * zero. When accessing the tag bits, the high "pointer" bits are set to zero. + * + * The "pointer" part points to the equivalent to an array of + * `SHAMapHash` followed immediately by an array of + * `shared_ptr`. The sizes of these arrays are + * determined by the tag. The tag is an index into an array (`boundaries`, + * defined in the cpp file) that specifies the size. Both arrays are the + * same size. Note that the sizes may be smaller than the full 16 elements + * needed to explicitly store all the children. In this case, the arrays + * only store the non-empty children. The non-empty children are stored in + * index order. For example, if only children `2` and `14` are non-empty, a + * two-element array would store child `2` in array index 0 and child `14` + * in array index 1. There are functions to convert between a child's tree + * index and the child's index in a sparse array. + * + * The motivation for this class is saving RAM. A large percentage of inner + * nodes only store a small number of children. Memory can be saved by + * storing the inner node's children in sparse arrays. Measurements show + * that on average a typical SHAMap's inner nodes can be stored using only + * 25% of the original space. + */ class TaggedPointer { private: static_assert( alignof(SHAMapHash) >= 4, "Bad alignment: Tag pointer requires low two bits to be zero."); - /** Upper bits are the pointer, lowest two bits are the tag - A moved-from object will have a tp_ of zero. - */ + /** + * Upper bits are the pointer, lowest two bits are the tag + * A moved-from object will have a tp_ of zero. + */ std::uintptr_t tp_ = 0; - /** bit-and with this mask to get the tag bits (lowest two bits) */ + /** + * bit-and with this mask to get the tag bits (lowest two bits) + */ static constexpr std::uintptr_t kTagMask = 3; - /** bit-and with this mask to get the pointer bits (mask out the tag) */ + /** + * bit-and with this mask to get the pointer bits (mask out the tag) + */ static constexpr std::uintptr_t kPtrMask = ~kTagMask; - /** Deallocate memory and run destructors */ + /** + * Deallocate memory and run destructors + */ void destroyHashesAndChildren(); struct RawAllocateTag { }; - /** This constructor allocates space for the hashes and children, but - does not run constructors. - - @param RawAllocateTag used to select overload only - - @param numChildren allocate space for at least this number of children - (must be <= branchFactor) - - @note Since the hashes/children destructors are always run in the - TaggedPointer destructor, this means those constructors _must_ be run - after this constructor is run. This constructor is private and only used - in places where the hashes/children constructor are subsequently run. - */ + /** + * This constructor allocates space for the hashes and children, but + * does not run constructors. + * + * @param RawAllocateTag used to select overload only + * + * @param numChildren allocate space for at least this number of children + * (must be <= branchFactor) + * + * @note Since the hashes/children destructors are always run in the + * TaggedPointer destructor, this means those constructors _must_ be run + * after this constructor is run. This constructor is private and only used + * in places where the hashes/children constructor are subsequently run. + */ explicit TaggedPointer(RawAllocateTag, std::uint8_t numChildren); public: TaggedPointer() = delete; explicit TaggedPointer(std::uint8_t numChildren); - /** Constructor is used change the number of allocated children. - - Existing children from `other` are copied (toAllocate must be >= the - number of children). The motivation for making this a constructor is it - saves unneeded copying and zeroing out of hashes if this were - implemented directly in the SHAMapInnerNode class. - - @param other children and hashes are moved from this param - - @param isBranch bitset of non-empty children in `other` - - @param toAllocate allocate space for at least this number of children - (must be <= branchFactor) - */ + /** + * Constructor is used change the number of allocated children. + * + * Existing children from `other` are copied (toAllocate must be >= the + * number of children). The motivation for making this a constructor is it + * saves unneeded copying and zeroing out of hashes if this were + * implemented directly in the SHAMapInnerNode class. + * + * @param other children and hashes are moved from this param + * + * @param isBranch bitset of non-empty children in `other` + * + * @param toAllocate allocate space for at least this number of children + * (must be <= branchFactor) + */ explicit TaggedPointer(TaggedPointer&& other, std::uint16_t isBranch, std::uint8_t toAllocate); - /** Given `other` with the specified children in `srcBranches`, create a - new TaggedPointer with the allocated number of children and the - children specified in `dstBranches`. - - @param other children and hashes are moved from this param - - @param srcBranches bitset of non-empty children in `other` - - @param dstBranches bitset of children to copy from `other` (or space to - leave in a sparse array - see note below) - - @param toAllocate allocate space for at least this number of children - (must be <= branchFactor) - - @note a child may be absent in srcBranches but present in dstBranches - (if dst has a sparse representation, space for the new child will be - left in the sparse array). Typically, srcBranches and dstBranches will - differ by at most one bit. The function works correctly if they differ - by more, but there are likely more efficient algorithms to consider if - this becomes a common use-case. - */ + /** + * Given `other` with the specified children in `srcBranches`, create a + * new TaggedPointer with the allocated number of children and the + * children specified in `dstBranches`. + * + * @param other children and hashes are moved from this param + * + * @param srcBranches bitset of non-empty children in `other` + * + * @param dstBranches bitset of children to copy from `other` (or space to + * leave in a sparse array - see note below) + * + * @param toAllocate allocate space for at least this number of children + * (must be <= branchFactor) + * + * @note a child may be absent in srcBranches but present in dstBranches + * (if dst has a sparse representation, space for the new child will be + * left in the sparse array). Typically, srcBranches and dstBranches will + * differ by at most one bit. The function works correctly if they differ + * by more, but there are likely more efficient algorithms to consider if + * this becomes a common use-case. + */ explicit TaggedPointer( TaggedPointer&& other, std::uint16_t srcBranches, @@ -132,68 +143,81 @@ public: ~TaggedPointer(); - /** Decode the tagged pointer into its tag and pointer */ + /** + * Decode the tagged pointer into its tag and pointer + */ [[nodiscard]] std::pair decode() const; - /** Get the number of elements allocated for each array */ + /** + * Get the number of elements allocated for each array + */ [[nodiscard]] std::uint8_t capacity() const; - /** Check if the arrays have a dense format. - - @note The dense format is when there is an array element for all 16 - (branchFactor) possible children. - */ + /** + * Check if the arrays have a dense format. + * + * @note The dense format is when there is an array element for all 16 + * (branchFactor) possible children. + */ [[nodiscard]] bool isDense() const; - /** Get the number of elements in each array and a pointer to the start - of each array. - */ + /** + * Get the number of elements in each array and a pointer to the start + * of each array. + */ [[nodiscard]] std::tuple getHashesAndChildren() const; - /** Get the `hashes` array */ + /** + * Get the `hashes` array + */ [[nodiscard]] SHAMapHash* getHashes() const; - /** Get the `children` array */ + /** + * Get the `children` array + */ [[nodiscard]] SHAMapTreeNodePtr* getChildren() const; - /** Call the `f` callback for all 16 (branchFactor) branches - even if - the branch is empty. - - @param isBranch bitset of non-empty children - - @param f a one parameter callback function. The parameter is the - child's hash. + /** + * Call the `f` callback for all 16 (branchFactor) branches - even if + * the branch is empty. + * + * @param isBranch bitset of non-empty children + * + * @param f a one parameter callback function. The parameter is the + * child's hash. */ template void iterChildren(std::uint16_t isBranch, F&& f) const; - /** Call the `f` callback for all non-empty branches. - - @param isBranch bitset of non-empty children - - @param f a two parameter callback function. The first parameter is - the branch number, the second parameter is the index into the array. - For dense formats these are the same, for sparse they may be - different. + /** + * Call the `f` callback for all non-empty branches. + * + * @param isBranch bitset of non-empty children + * + * @param f a two parameter callback function. The first parameter is + * the branch number, the second parameter is the index into the array. + * For dense formats these are the same, for sparse they may be + * different. */ template void iterNonEmptyChildIndexes(std::uint16_t isBranch, F&& f) const; - /** Get the child's index inside the `hashes` or `children` array (which - may or may not be sparse). The optional will be empty if an empty - branch is requested and the children are sparse. - - @param isBranch bitset of non-empty children - - @param i index of the requested child + /** + * Get the child's index inside the `hashes` or `children` array (which + * may or may not be sparse). The optional will be empty if an empty + * branch is requested and the children are sparse. + * + * @param isBranch bitset of non-empty children + * + * @param i index of the requested child */ [[nodiscard]] std::optional getChildIndex(std::uint16_t isBranch, int i) const; diff --git a/include/xrpl/shamap/detail/TaggedPointer.ipp b/include/xrpl/shamap/detail/TaggedPointer.ipp index 6606c49a6b..9275f3d15a 100644 --- a/include/xrpl/shamap/detail/TaggedPointer.ipp +++ b/include/xrpl/shamap/detail/TaggedPointer.ipp @@ -507,6 +507,9 @@ TaggedPointer::operator=(TaggedPointer&& other) [[nodiscard]] inline std::pair TaggedPointer::decode() const { + // tp_ packs a raw pointer together with the tag bits; recovering the + // pointer inherently requires an integer-to-pointer cast. + // NOLINTNEXTLINE(performance-no-int-to-ptr) return {tp_ & kTagMask, reinterpret_cast(tp_ & kPtrMask)}; } @@ -535,6 +538,9 @@ TaggedPointer::getHashesAndChildren() const [[nodiscard]] inline SHAMapHash* TaggedPointer::getHashes() const { + // tp_ packs a raw pointer together with the tag bits; recovering the + // pointer inherently requires an integer-to-pointer cast. + // NOLINTNEXTLINE(performance-no-int-to-ptr) return reinterpret_cast(tp_ & kPtrMask); }; diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h index ca778387cd..472afdf624 100644 --- a/include/xrpl/tx/ApplyContext.h +++ b/include/xrpl/tx/ApplyContext.h @@ -21,7 +21,9 @@ namespace xrpl { -/** State information when applying a tx. */ +/** + * State information when applying a tx. + */ class ApplyContext { public: @@ -83,7 +85,9 @@ public: return flags_; } - /** Sets the DeliveredAmount field in the metadata */ + /** + * Sets the DeliveredAmount field in the metadata + */ void deliver(STAmount const& amount) { @@ -91,18 +95,26 @@ public: view_->deliver(amount); } - /** Discard changes and start fresh. */ + /** + * Discard changes and start fresh. + */ void discard(); - /** Apply the transaction result to the base. */ + /** + * Apply the transaction result to the base. + */ std::optional apply(TER); - /** Get the number of unapplied changes. */ + /** + * Get the number of unapplied changes. + */ std::size_t size(); - /** Visit unapplied changes. */ + /** + * Visit unapplied changes. + */ void visit( std::functionrawDestroyXRP(fee); } - /** Applies all invariant checkers one by one. - - @param result the result generated by processing this transaction. - @param fee the fee charged for this transaction - @return the result code that should be returned for this transaction. + /** + * Applies all invariant checkers one by one. + * + * @param result the result generated by processing this transaction. + * @param fee the fee charged for this transaction + * @return the result code that should be returned for this transaction. */ TER checkInvariants(TER const result, XRPAmount const fee); + ApplyViewContext + getApplyViewContext() + { + XRPL_ASSERT( + view_.has_value(), + "xrpl::ApplyContext::getApplyViewContext : view_ emplaced in constructor"); + return {.view = *view_, .tx = tx}; + } + private: static TER failInvariantCheck(TER const result); diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index bc5e8c80e7..a71285f70e 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -29,7 +31,9 @@ namespace xrpl { -/** State information when preflighting a tx. */ +/** + * State information when preflighting a tx. + */ struct PreflightContext { public: @@ -72,7 +76,9 @@ public: operator=(PreflightContext const&) = delete; }; -/** State information when determining if a tx is likely to claim a fee. */ +/** + * State information when determining if a tx is likely to claim a fee. + */ struct PreclaimContext { public: @@ -126,6 +132,21 @@ struct PreflightResult; // Needed for preflight specialization class Change; +enum class FeePayerType { + Account, + Delegate, + SponsorCoSigned, + SponsorPreFunded, +}; + +struct FeePayer +{ + AccountID id; + Keylet keylet; + SF_AMOUNT const& balanceField; + FeePayerType type{FeePayerType::Account}; +}; + class Transactor { protected: @@ -144,7 +165,9 @@ public: enum class ConsequencesFactoryType { Normal, Blocker, Custom }; - /** Process the transaction. */ + /** + * Process the transaction. + */ ApplyResult operator()(); @@ -160,16 +183,17 @@ public: return ctx_.view(); } - /** Check all invariants for the current transaction. + /** + * Check all invariants for the current transaction. * - * Runs transaction-specific invariants first (visitInvariantEntry + - * finalizeInvariants), then protocol-level invariants. Both layers - * always run; the worst failure code is returned. + * Runs transaction-specific invariants first (visitInvariantEntry + + * finalizeInvariants), then protocol-level invariants. Both layers + * always run; the worst failure code is returned. * - * @param result the tentative TER from transaction processing. - * @param fee the fee consumed by the transaction. + * @param result the tentative TER from transaction processing. + * @param fee the fee consumed by the transaction. * - * @return the final TER after all invariant checks. + * @return the final TER after all invariant checks. */ [[nodiscard]] TER checkInvariants(TER result, XRPAmount fee); @@ -298,6 +322,10 @@ public: return T::checkGranularSemantics(view, tx, heldGranularPermissions); } + + static NotTEC + checkSponsor(ReadView const& view, STTx const& tx); + ///////////////////////////////////////////////////// // Interface used by AccountDelete @@ -320,40 +348,42 @@ protected: virtual TER doApply() = 0; - /** Inspect a single ledger entry modified by this transaction. + /** + * Inspect a single ledger entry modified by this transaction. * - * Called once for every SLE created, modified, or deleted by the - * transaction, before finalizeInvariants. Implementations should - * accumulate whatever state they need to verify transaction-specific - * post-conditions. + * Called once for every SLE created, modified, or deleted by the + * transaction, before finalizeInvariants. Implementations should + * accumulate whatever state they need to verify transaction-specific + * post-conditions. * - * @param isDelete true if the entry was erased from the ledger. - * @param before the entry's state before the transaction (nullptr - * for newly created entries). - * @param after the entry's state as supplied by the apply logic - * for this transaction. For deletions, this is the - * SLE being erased and is not guaranteed to be null; - * callers must use isDelete rather than after == nullptr - * to detect deletions. + * @param isDelete true if the entry was erased from the ledger. + * @param before the entry's state before the transaction (nullptr + * for newly created entries). + * @param after the entry's state as supplied by the apply logic + * for this transaction. For deletions, this is the + * SLE being erased and is not guaranteed to be null; + * callers must use isDelete rather than after == nullptr + * to detect deletions. */ virtual void visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) = 0; - /** Check transaction-specific post-conditions after all entries have - * been visited. + /** + * Check transaction-specific post-conditions after all entries have + * been visited. * - * Called once after every modified ledger entry has been passed to - * visitInvariantEntry. Returns true if all transaction-specific - * invariants hold, or false to fail the transaction with - * tecINVARIANT_FAILED. + * Called once after every modified ledger entry has been passed to + * visitInvariantEntry. Returns true if all transaction-specific + * invariants hold, or false to fail the transaction with + * tecINVARIANT_FAILED. * - * @param tx the transaction being applied. - * @param result the tentative TER result so far. - * @param fee the fee consumed by the transaction. - * @param view read-only view of the ledger after the transaction. - * @param j journal for logging invariant failures. + * @param tx the transaction being applied. + * @param result the tentative TER result so far. + * @param fee the fee consumed by the transaction. + * @param view read-only view of the ledger after the transaction. + * @param j journal for logging invariant failures. * - * @return true if all invariants pass; false otherwise. + * @return true if all invariants pass; false otherwise. */ [[nodiscard]] virtual bool finalizeInvariants( @@ -363,14 +393,15 @@ protected: ReadView const& view, beast::Journal const& j) = 0; - /** Compute the minimum fee required to process a transaction - with a given baseFee based on the current server load. - - @param registry The service registry. - @param baseFee The base fee of a candidate transaction - @see xrpl::calculateBaseFee - @param fees Fee settings from the current ledger - @param flags Transaction processing fees + /** + * Compute the minimum fee required to process a transaction + * with a given baseFee based on the current server load. + * + * @param registry The service registry. + * @param baseFee The base fee of a candidate transaction + * @see xrpl::calculateBaseFee + * @param fees Fee settings from the current ledger + * @param flags Transaction processing fees */ static XRPAmount minimumFee(ServiceRegistry& registry, XRPAmount baseFee, Fees const& fees, ApplyFlags flags); @@ -419,12 +450,16 @@ protected: unit::ValueUnit max, unit::ValueUnit min = unit::ValueUnit{}); - /// Minimum will usually be zero. + /** + * Minimum will usually be zero. + */ template static bool validNumericMinimum(std::optional value, T min = T{}); - /// Minimum will usually be zero. + /** + * Minimum will usually be zero. + */ template static bool validNumericMinimum( @@ -459,6 +494,9 @@ private: std::pair reset(XRPAmount fee); + static FeePayer + getFeePayer(ReadView const& view, STTx const& tx); + TER consumeSeqProxy(SLE::pointer const& sleAccount); TER @@ -469,44 +507,48 @@ private: void trapTransaction(uint256) const; - /** Performs early sanity checks on the account and fee fields. - - (And passes flagMask to preflight0) - - Do not try to call preflight1 from preflight() in derived classes. See - the description of invokePreflight for details. - */ + /** + * Performs early sanity checks on the account and fee fields. + * + * (And passes flagMask to preflight0) + * + * Do not try to call preflight1 from preflight() in derived classes. See + * the description of invokePreflight for details. + */ static NotTEC preflight1(PreflightContext const& ctx, std::uint32_t flagMask); - /** Checks whether the signature appears valid - - Do not try to call preflight2 from preflight() in derived classes. See - the description of invokePreflight for details. - */ + /** + * Checks whether the signature appears valid + * + * Do not try to call preflight2 from preflight() in derived classes. See + * the description of invokePreflight for details. + */ static NotTEC preflight2(PreflightContext const& ctx); - /** Universal validations - - Valid MPTAmount and XRPAmount - - Do not try to call preflightUniversal from preflight() in derived classes. See - the description of invokePreflight for details. - */ + /** + * Universal validations + * - Valid MPTAmount and XRPAmount + * + * Do not try to call preflightUniversal from preflight() in derived classes. See + * the description of invokePreflight for details. + */ static NotTEC preflightUniversal(PreflightContext const& ctx); - /** Check transaction-specific invariants only. + /** + * Check transaction-specific invariants only. * - * Walks every modified ledger entry via visitInvariantEntry, then - * calls finalizeInvariants on the derived transactor. Returns - * tecINVARIANT_FAILED if any transaction invariant is violated. + * Walks every modified ledger entry via visitInvariantEntry, then + * calls finalizeInvariants on the derived transactor. Returns + * tecINVARIANT_FAILED if any transaction invariant is violated. * - * @param result the tentative TER from transaction processing. - * @param fee the fee consumed by the transaction. + * @param result the tentative TER from transaction processing. + * @param fee the fee consumed by the transaction. * - * @return the original result if all invariants pass, or - * tecINVARIANT_FAILED otherwise. + * @return the original result if all invariants pass, or + * tecINVARIANT_FAILED otherwise. */ [[nodiscard]] TER checkTransactionInvariants(TER result, XRPAmount fee); @@ -518,20 +560,24 @@ Transactor::checkExtraFeatures(PreflightContext const& ctx) return true; } -/** Performs early sanity checks on the txid and flags */ +/** + * Performs early sanity checks on the txid and flags + */ NotTEC preflight0(PreflightContext const& ctx, std::uint32_t flagMask); namespace detail { -/** Checks the validity of the transactor signing key. +/** + * Checks the validity of the transactor signing key. * * Normally called from preflight1 with ctx.tx. */ NotTEC preflightCheckSigningKey(STObject const& sigObject, beast::Journal j); -/** Checks the special signing key state needed for simulation +/** + * Checks the special signing key state needed for simulation * * Normally called from preflight2 with ctx.tx. */ diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h index 55d365c31b..8c11d870f1 100644 --- a/include/xrpl/tx/apply.h +++ b/include/xrpl/tx/apply.h @@ -16,88 +16,98 @@ namespace xrpl { class HashRouter; class ServiceRegistry; -/** Describes the pre-processing validity of a transaction. - - @see checkValidity, forceValidity -*/ +/** + * Describes the pre-processing validity of a transaction. + * + * @see checkValidity, forceValidity + */ enum class Validity { - /// Signature is bad. Didn't do local checks. + /** + * Signature is bad. Didn't do local checks. + */ SigBad, - /// Signature is good, but local checks fail. + /** + * Signature is good, but local checks fail. + */ SigGoodOnly, - /// Signature and local checks are good / passed. + /** + * Signature and local checks are good / passed. + */ Valid }; -/** Checks transaction signature and local checks. - - @return A `Validity` enum representing how valid the - `STTx` is and, if not `Valid`, a reason string. - - @note Results are cached internally, so tests will not be - repeated over repeated calls, unless cache expires. - - @return `std::pair`, where `.first` is the status, and - `.second` is the reason if appropriate. - - @see Validity -*/ +/** + * Checks transaction signature and local checks. + * + * @return A `Validity` enum representing how valid the + * `STTx` is and, if not `Valid`, a reason string. + * + * @note Results are cached internally, so tests will not be + * repeated over repeated calls, unless cache expires. + * + * @return `std::pair`, where `.first` is the status, and + * `.second` is the reason if appropriate. + * + * @see Validity + */ std::pair checkValidity(HashRouter& router, STTx const& tx, Rules const& rules); -/** Sets the validity of a given transaction in the cache. - - @warning Use with extreme care. - - @note Can only raise the validity to a more valid state, - and can not override anything cached bad. - - @see checkValidity, Validity -*/ +/** + * Sets the validity of a given transaction in the cache. + * + * @warning Use with extreme care. + * + * @note Can only raise the validity to a more valid state, + * and can not override anything cached bad. + * + * @see checkValidity, Validity + */ void forceValidity(HashRouter& router, uint256 const& txid, Validity validity); -/** Apply a transaction to an `OpenView`. - - This function is the canonical way to apply a transaction - to a ledger. It rolls the validation and application - steps into one function. To do the steps manually, the - correct calling order is: - @code{.cpp} - preflight -> preclaim -> doApply - @endcode - The result of one function must be passed to the next. - The `preflight` result can be safely cached and reused - asynchronously, but `preclaim` and `doApply` must be called - in the same thread and with the same view. - - @note Does not throw. - - For open ledgers, the `Transactor` will catch exceptions - and return `tefEXCEPTION`. For closed ledgers, the - `Transactor` will attempt to only charge a fee, - and return `tecFAILED_PROCESSING`. - - If the `Transactor` gets an exception while trying - to charge the fee, it will be caught and - turned into `tefEXCEPTION`. - - For network health, a `Transactor` makes its - best effort to at least charge a fee if the - ledger is closed. - - @param app The current running `Application`. - @param view The open ledger that the transaction - will attempt to be applied to. - @param tx The transaction to be checked. - @param flags `ApplyFlags` describing processing options. - @param journal A journal. - - @see preflight, preclaim, doApply - - @return A pair with the `TER` and a `bool` indicating - whether or not the transaction was applied. -*/ +/** + * Apply a transaction to an `OpenView`. + * + * This function is the canonical way to apply a transaction + * to a ledger. It rolls the validation and application + * steps into one function. To do the steps manually, the + * correct calling order is: + * @code + * preflight -> preclaim -> doApply + * @endcode + * The result of one function must be passed to the next. + * The `preflight` result can be safely cached and reused + * asynchronously, but `preclaim` and `doApply` must be called + * in the same thread and with the same view. + * + * @note Does not throw. + * + * For open ledgers, the `Transactor` will catch exceptions + * and return `tefEXCEPTION`. For closed ledgers, the + * `Transactor` will attempt to only charge a fee, + * and return `tecFAILED_PROCESSING`. + * + * If the `Transactor` gets an exception while trying + * to charge the fee, it will be caught and + * turned into `tefEXCEPTION`. + * + * For network health, a `Transactor` makes its + * best effort to at least charge a fee if the + * ledger is closed. + * + * @param app The current running `Application`. + * @param view The open ledger that the transaction + * will attempt to be applied to. + * @param tx The transaction to be checked. + * @param flags `ApplyFlags` describing processing options. + * @param journal A journal. + * + * @see preflight, preclaim, doApply + * + * @return A pair with the `TER` and a `bool` indicating + * whether or not the transaction was applied. + */ ApplyResult apply( ServiceRegistry& registry, @@ -106,26 +116,34 @@ apply( ApplyFlags flags, beast::Journal journal); -/** Enum class for return value from `applyTransaction` - - @see applyTransaction -*/ +/** + * Enum class for return value from `applyTransaction` + * + * @see applyTransaction + */ enum class ApplyTransactionResult { - /// Applied to this ledger + /** + * Applied to this ledger + */ Success, - /// Should not be retried in this ledger + /** + * Should not be retried in this ledger + */ Fail, - /// Should be retried in this ledger + /** + * Should be retried in this ledger + */ Retry }; -/** Transaction application helper - - Provides more detailed logging and decodes the - correct behavior based on the `TER` type - - @see ApplyTransactionResult -*/ +/** + * Transaction application helper + * + * Provides more detailed logging and decodes the + * correct behavior based on the `TER` type + * + * @see ApplyTransactionResult + */ ApplyTransactionResult applyTransaction( ServiceRegistry& registry, diff --git a/include/xrpl/tx/applySteps.h b/include/xrpl/tx/applySteps.h index 3298e49192..bd495481f2 100644 --- a/include/xrpl/tx/applySteps.h +++ b/include/xrpl/tx/applySteps.h @@ -33,8 +33,9 @@ struct ApplyResult } }; -/** Return true if the transaction can claim a fee (tec), - and the `ApplyFlags` do not allow soft failures. +/** + * Return true if the transaction can claim a fee (tec), + * and the `ApplyFlags` do not allow soft failures. */ inline bool isTecClaimHardFail(TER ter, ApplyFlags flags) @@ -42,34 +43,51 @@ isTecClaimHardFail(TER ter, ApplyFlags flags) return isTecClaim(ter) && ((flags & TapRetry) == 0u); } -/** Class describing the consequences to the account - of applying a transaction if the transaction consumes - the maximum XRP allowed. -*/ +/** + * Class describing the consequences to the account + * of applying a transaction if the transaction consumes + * the maximum XRP allowed. + */ class TxConsequences { public: - /// Describes how the transaction affects subsequent - /// transactions + /** + * Describes how the transaction affects subsequent + * transactions + */ enum class Category { - /// Moves currency around, creates offers, etc. + /** + * Moves currency around, creates offers, etc. + */ Normal = 0, - /// Affects the ability of subsequent transactions - /// to claim a fee. Eg. `SetRegularKey` + /** + * Affects the ability of subsequent transactions + * to claim a fee. Eg. `SetRegularKey` + */ Blocker }; private: - /// Describes how the transaction affects subsequent - /// transactions + /** + * Describes how the transaction affects subsequent + * transactions + */ bool isBlocker_; - /// Transaction fee + /** + * Transaction fee + */ XRPAmount fee_; - /// Does NOT include the fee. + /** + * Does NOT include the fee. + */ XRPAmount potentialSpend_; - /// SeqProxy of transaction. + /** + * SeqProxy of transaction. + */ SeqProxy seqProx_; - /// Number of sequences consumed. + /** + * Number of sequences consumed. + */ std::uint32_t sequencesConsumed_; public: @@ -77,58 +95,84 @@ public: // Asserts if tesSUCCESS is passed. explicit TxConsequences(NotTEC pfResult); - /// Constructor if the STTx has no notable consequences for the TxQ. + /** + * Constructor if the STTx has no notable consequences for the TxQ. + */ explicit TxConsequences(STTx const& tx); - /// Constructor for a blocker. + /** + * Constructor for a blocker. + */ TxConsequences(STTx const& tx, Category category); - /// Constructor for an STTx that may consume more XRP than the fee. + /** + * Constructor for an STTx that may consume more XRP than the fee. + */ TxConsequences(STTx const& tx, XRPAmount potentialSpend); - /// Constructor for an STTx that consumes more than the usual sequences. + /** + * Constructor for an STTx that consumes more than the usual sequences. + */ TxConsequences(STTx const& tx, std::uint32_t sequencesConsumed); - /// Copy constructor + /** + * Copy constructor + */ TxConsequences(TxConsequences const&) = default; - /// Copy assignment operator + /** + * Copy assignment operator + */ TxConsequences& operator=(TxConsequences const&) = default; - /// Move constructor + /** + * Move constructor + */ TxConsequences(TxConsequences&&) = default; - /// Move assignment operator + /** + * Move assignment operator + */ TxConsequences& operator=(TxConsequences&&) = default; - /// Fee + /** + * Fee + */ [[nodiscard]] XRPAmount fee() const { return fee_; } - /// Potential Spend + /** + * Potential Spend + */ [[nodiscard]] XRPAmount const& potentialSpend() const { return potentialSpend_; } - /// SeqProxy + /** + * SeqProxy + */ [[nodiscard]] SeqProxy seqProxy() const { return seqProx_; } - /// Sequences consumed + /** + * Sequences consumed + */ [[nodiscard]] std::uint32_t sequencesConsumed() const { return sequencesConsumed_; } - /// Returns true if the transaction is a blocker. + /** + * Returns true if the transaction is a blocker. + */ [[nodiscard]] bool isBlocker() const { @@ -145,32 +189,49 @@ public: } }; -/** Describes the results of the `preflight` check - - @note All members are const to make it more difficult - to "fake" a result without calling `preflight`. - @see preflight, preclaim, doApply, apply -*/ +/** + * Describes the results of the `preflight` check + * + * @note All members are const to make it more difficult + * to "fake" a result without calling `preflight`. + * @see preflight, preclaim, doApply, apply + */ struct PreflightResult { public: - /// From the input - the transaction + /** + * From the input - the transaction + */ STTx const& tx; - /// From the input - the batch identifier, if part of a batch + /** + * From the input - the batch identifier, if part of a batch + */ std::optional const parentBatchId; - /// From the input - the rules + /** + * From the input - the rules + */ Rules const rules; - /// Consequences of the transaction + /** + * Consequences of the transaction + */ TxConsequences const consequences; - /// From the input - the flags + /** + * From the input - the flags + */ ApplyFlags const flags; - /// From the input - the journal + /** + * From the input - the journal + */ beast::Journal const j; - /// Intermediate transaction result + /** + * Intermediate transaction result + */ NotTEC const ter; - /// Constructor + /** + * Constructor + */ template PreflightResult(Context const& ctx, std::pair const& result) : tx(ctx.tx) @@ -184,39 +245,58 @@ public: } PreflightResult(PreflightResult const&) = default; - /// Deleted copy assignment operator + /** + * Deleted copy assignment operator + */ PreflightResult& operator=(PreflightResult const&) = delete; }; -/** Describes the results of the `preclaim` check - - @note All members are const to make it more difficult - to "fake" a result without calling `preclaim`. - @see preflight, preclaim, doApply, apply -*/ +/** + * Describes the results of the `preclaim` check + * + * @note All members are const to make it more difficult + * to "fake" a result without calling `preclaim`. + * @see preflight, preclaim, doApply, apply + */ struct PreclaimResult { public: - /// From the input - the ledger view + /** + * From the input - the ledger view + */ ReadView const& view; - /// From the input - the transaction + /** + * From the input - the transaction + */ STTx const& tx; - /// From the input - the batch identifier, if part of a batch + /** + * From the input - the batch identifier, if part of a batch + */ std::optional const parentBatchId; - /// From the input - the flags + /** + * From the input - the flags + */ ApplyFlags const flags; - /// From the input - the journal + /** + * From the input - the journal + */ beast::Journal const j; - /// Intermediate transaction result + /** + * Intermediate transaction result + */ TER const ter; - /// Success flag - whether the transaction is likely to - /// claim a fee + /** + * Success flag - whether the transaction is likely to + * claim a fee + */ bool const likelyToClaimFee{}; - /// Constructor + /** + * Constructor + */ template PreclaimResult(Context const& ctx, TER ter) : view(ctx.view) @@ -230,27 +310,30 @@ public: } PreclaimResult(PreclaimResult const&) = default; - /// Deleted copy assignment operator + /** + * Deleted copy assignment operator + */ PreclaimResult& operator=(PreclaimResult const&) = delete; }; -/** Gate a transaction based on static information. - - The transaction is checked against all possible - validity constraints that do not require a ledger. - - @param app The current running `Application`. - @param rules The `Rules` in effect at the time of the check. - @param tx The transaction to be checked. - @param flags `ApplyFlags` describing processing options. - @param j A journal. - - @see PreflightResult, preclaim, doApply, apply - - @return A `PreflightResult` object containing, among - other things, the `TER` code. -*/ +/** + * Gate a transaction based on static information. + * + * The transaction is checked against all possible + * validity constraints that do not require a ledger. + * + * @param app The current running `Application`. + * @param rules The `Rules` in effect at the time of the check. + * @param tx The transaction to be checked. + * @param flags `ApplyFlags` describing processing options. + * @param j A journal. + * + * @see PreflightResult, preclaim, doApply, apply + * + * @return A `PreflightResult` object containing, among + * other things, the `TER` code. + */ /** @{ */ PreflightResult preflight( @@ -270,86 +353,90 @@ preflight( beast::Journal j); /** @} */ -/** Gate a transaction based on static ledger information. - - The transaction is checked against all possible - validity constraints that DO require a ledger. - - If preclaim succeeds, then the transaction is very - likely to claim a fee. This will determine if the - transaction is safe to relay without being applied - to the open ledger. - - "Succeeds" in this case is defined as returning a - `tes` or `tec`, since both lead to claiming a fee. - - @pre The transaction has been checked - and validated using `preflight` - - @param preflightResult The result of a previous - call to `preflight` for the transaction. - @param app The current running `Application`. - @param view The open ledger that the transaction - will attempt to be applied to. - - @see PreclaimResult, preflight, doApply, apply - - @return A `PreclaimResult` object containing, among - other things the `TER` code and the base fee value for - this transaction. -*/ +/** + * Gate a transaction based on static ledger information. + * + * The transaction is checked against all possible + * validity constraints that DO require a ledger. + * + * If preclaim succeeds, then the transaction is very + * likely to claim a fee. This will determine if the + * transaction is safe to relay without being applied + * to the open ledger. + * + * "Succeeds" in this case is defined as returning a + * `tes` or `tec`, since both lead to claiming a fee. + * + * @pre The transaction has been checked + * and validated using `preflight` + * + * @param preflightResult The result of a previous + * call to `preflight` for the transaction. + * @param app The current running `Application`. + * @param view The open ledger that the transaction + * will attempt to be applied to. + * + * @see PreclaimResult, preflight, doApply, apply + * + * @return A `PreclaimResult` object containing, among + * other things the `TER` code and the base fee value for + * this transaction. + */ PreclaimResult preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, OpenView const& view); -/** Compute only the expected base fee for a transaction. - - Base fees are transaction specific, so any calculation - needing them must get the base fee for each transaction. - - No validation is done or implied by this function. - - Caller is responsible for handling any exceptions. - Since none should be thrown, that will usually - mean terminating. - - @param view The current open ledger. - @param tx The transaction to be checked. - - @return The base fee. -*/ +/** + * Compute only the expected base fee for a transaction. + * + * Base fees are transaction specific, so any calculation + * needing them must get the base fee for each transaction. + * + * No validation is done or implied by this function. + * + * Caller is responsible for handling any exceptions. + * Since none should be thrown, that will usually + * mean terminating. + * + * @param view The current open ledger. + * @param tx The transaction to be checked. + * + * @return The base fee. + */ XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); -/** Return the minimum fee that an "ordinary" transaction would pay. - - When computing the FeeLevel for a transaction the TxQ sometimes needs - the know what an "ordinary" or reference transaction would be required - to pay. - - @param view The current open ledger. - @param tx The transaction so the correct multisigner count is used. - - @return The base fee in XRPAmount. -*/ +/** + * Return the minimum fee that an "ordinary" transaction would pay. + * + * When computing the FeeLevel for a transaction the TxQ sometimes needs + * the know what an "ordinary" or reference transaction would be required + * to pay. + * + * @param view The current open ledger. + * @param tx The transaction so the correct multisigner count is used. + * + * @return The base fee in XRPAmount. + */ XRPAmount calculateDefaultBaseFee(ReadView const& view, STTx const& tx); -/** Apply a prechecked transaction to an OpenView. - - @pre The transaction has been checked - and validated using `preflight` and `preclaim` - - @param preclaimResult The result of a previous - call to `preclaim` for the transaction. - @param registry The service registry. - @param view The open ledger that the transaction - will attempt to be applied to. - - @see preflight, preclaim, apply - - @return A pair with the `TER` and a `bool` indicating - whether or not the transaction was applied. -*/ +/** + * Apply a prechecked transaction to an OpenView. + * + * @pre The transaction has been checked + * and validated using `preflight` and `preclaim` + * + * @param preclaimResult The result of a previous + * call to `preclaim` for the transaction. + * @param registry The service registry. + * @param view The open ledger that the transaction + * will attempt to be applied to. + * + * @see preflight, preclaim, apply + * + * @return A pair with the `TER` and a `bool` indicating + * whether or not the transaction was applied. + */ ApplyResult doApply(PreclaimResult const& preclaimResult, ServiceRegistry& registry, OpenView& view); diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index d19f34635d..1239305e79 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -70,9 +71,18 @@ public: /** * @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 + * @param isDelete true if the SLE is being deleted. + * @param before ledger entry before modification by the transaction. `before` will be null if + * the entry is new. + * @param after ledger entry after modification by the transaction. Always non-null. When + * deleting, `after` may differ from `before`. Whether that is important is up to the + * individual invariant check. + * + * @note `after` IS NEVER NULL. `isDelete` is the only correct way to check for deletions. + * Do not make logic or branching decisions on whether on `after` is set, because it will + * always be set. Treat a null `after` as a programming error (with XRPL_ASSERT). An + * invariant MAY check for null defensively, if it makes more sense, but an assertion is + * preferred for new invariants. */ void visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after); @@ -315,17 +325,26 @@ public: }; /** - * @brief Invariant: Token holder's trustline balance cannot be negative after - * Clawback. + * @brief Invariant: Token holder's trustline/MPT balance cannot be invalid + * 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. + * non-negative. When featureMPTokensV2 is enabled, also verify the holder's + * raw trustline/MPToken balance decreased by the clawed amount. */ class ValidClawback { + struct EntryChange + { + SLE::const_pointer before; + SLE::const_pointer after; + }; + std::uint32_t trustlinesChanged_ = 0; std::uint32_t mptokensChanged_ = 0; + EntryChange iou_; + EntryChange mpt_; public: void @@ -375,7 +394,8 @@ public: finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); }; -/** Verify that MPT/XRP STAmounts are canonical in any ledger entries left after the +/** + * Verify that MPT/XRP STAmounts are canonical in any ledger entries left after the * transaction applies. */ class ValidAmounts @@ -438,10 +458,12 @@ using InvariantChecks = std::tuple< ValidLoan, ValidVault, ValidConfidentialMPToken, - ValidMPTPayment, + ValidMPTBalanceChanges, ValidAmounts, ValidMPTTransfer, - ObjectHasPseudoAccount>; + ObjectHasPseudoAccount, + SponsorshipOwnerCountsMatch, + SponsorshipAccountCountMatchesField>; /** * @brief get a tuple of all invariant checks diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h index 1d39fd13d8..5740cd5be2 100644 --- a/include/xrpl/tx/invariants/MPTInvariant.h +++ b/include/xrpl/tx/invariants/MPTInvariant.h @@ -31,16 +31,22 @@ class ValidMPTIssuance // MPToken by an issuer bool mptCreatedByIssuer_ = false; - /// sfReferenceHolding is intended to be set exactly once at vault - /// creation and immutable thereafter; true when that rule was violated. + /** + * sfReferenceHolding is intended to be set exactly once at vault + * creation and immutable thereafter; true when that rule was violated. + */ bool referenceHoldingSetOnCreate_ = false; - /// True when sfReferenceHolding was mutated on an existing MPTokenIssuance. + /** + * True when sfReferenceHolding was mutated on an existing MPTokenIssuance. + */ bool referenceHoldingMutated_ = false; - /// MPTokens and RippleStates deleted during apply. finalize() checks each - /// holder's AccountRoot to detect vault pseudo-account holdings deleted - /// outside VaultDelete. All these checks are gated on fixCleanup3_2_0. + /** + * MPTokens and RippleStates deleted during apply. finalize() checks each + * holder's AccountRoot to detect vault pseudo-account holdings deleted + * outside VaultDelete. All these checks are gated on fixCleanup3_2_0. + */ std::vector> deletedHoldings_; public: @@ -81,7 +87,7 @@ public: * OutstandingAmount after application equals OutstandingAmount before * application plus the net holder balance delta. */ -class ValidMPTPayment +class ValidMPTBalanceChanges { enum class Order { Before = 0, After = 1 }; struct MPTData diff --git a/include/xrpl/tx/invariants/SponsorshipInvariant.h b/include/xrpl/tx/invariants/SponsorshipInvariant.h new file mode 100644 index 0000000000..e664685c16 --- /dev/null +++ b/include/xrpl/tx/invariants/SponsorshipInvariant.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Invariant: Sponsored owner counts are balanced. + * + * The following checks are made for every transaction: + * - The sum of all per-account deltas of `sfSponsoredOwnerCount` equals + * the sum of all per-account deltas of `sfSponsoringOwnerCount`. + * - Account OwnerCount must be greater than or equal to SponsoredOwnerCount. + * - The net delta of sponsored object owner counts (the owner-count + * magnitude of sponsored ledger entries) equals the net delta of + * `sfSponsoredOwnerCount`. + */ +class SponsorshipOwnerCountsMatch +{ + std::int64_t deltaSponsoredOwnerCount_ = 0; + std::int64_t deltaSponsoringOwnerCount_ = 0; + std::int64_t deltaSponsoredObjectOwnerCount_ = 0; + std::uint64_t ownerCountBelowSponsored_ = 0; + +public: + void + visitEntry(bool, SLE::const_ref, SLE::const_ref); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; +}; + +/** + * @brief Invariant: Sponsoring account relationships tracked consistently. + * + * The following check is made for every transaction: + * - The net delta of `sfSponsoringAccountCount` across all accounts equals + * the net delta of the count of ltACCOUNT_ROOT entries having + * `sfSponsor` present (presence transitions only: add/remove). + */ +class SponsorshipAccountCountMatchesField +{ + std::int64_t deltaSponsoringAccountCount_ = 0; + std::int64_t deltaSponsorFieldPresence_ = 0; + +public: + void + visitEntry(bool, SLE::const_ref, SLE::const_ref); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/VaultInvariant.h b/include/xrpl/tx/invariants/VaultInvariant.h index bc8246b234..136c6c4a25 100644 --- a/include/xrpl/tx/invariants/VaultInvariant.h +++ b/include/xrpl/tx/invariants/VaultInvariant.h @@ -21,7 +21,7 @@ namespace xrpl { -/* +/** * @brief Invariants: Vault object and MPTokenIssuance for vault shares * * - vault deleted and vault created is empty @@ -96,7 +96,7 @@ private: * * @param vaultDelta Delta of the vault's asset balance for this transaction. * @param rules Active ledger rules (used to check the amendment). - * @returns The minimum scale to apply when rounding vault-related amounts. + * @return The minimum scale to apply when rounding vault-related amounts. */ [[nodiscard]] std::int32_t computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const; @@ -109,7 +109,7 @@ private: * to the vault asset held by @p id. * * @param id Account whose asset delta is requested. - * @returns The delta, or @c std::nullopt if the entry was not touched. + * @return The delta, or @c std::nullopt if the entry was not touched. */ [[nodiscard]] std::optional deltaAssets(AccountID const& id) const; @@ -124,8 +124,8 @@ private: * * @param tx The transaction being applied. * @param fee Fee charged by this transaction. - * @returns The fee-adjusted delta, or @c std::nullopt if the net delta is - * zero or the account entry was not touched. + * @return The fee-adjusted delta, or @c std::nullopt if the net delta is + * zero or the account entry was not touched. */ [[nodiscard]] std::optional deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const; @@ -138,7 +138,7 @@ private: * returned. * * @param id Account whose share delta is requested. - * @returns The delta, or @c std::nullopt if the entry was not touched. + * @return The delta, or @c std::nullopt if the entry was not touched. */ [[nodiscard]] std::optional deltaShares(AccountID const& id) const; @@ -147,8 +147,8 @@ private: * @brief Check whether a vault holds no assets. * * @param vault Snapshot of the vault to test. - * @returns @c true when both @c assetsAvailable and @c assetsTotal are - * zero. + * @return @c true when both @c assetsAvailable and @c assetsTotal are + * zero. */ [[nodiscard]] static bool isVaultEmpty(Vault const& vault); diff --git a/include/xrpl/tx/paths/AMMLiquidity.h b/include/xrpl/tx/paths/AMMLiquidity.h index 24a850e2f4..530b13756f 100644 --- a/include/xrpl/tx/paths/AMMLiquidity.h +++ b/include/xrpl/tx/paths/AMMLiquidity.h @@ -17,7 +17,8 @@ namespace xrpl { template class AMMOffer; -/** AMMLiquidity class provides AMM offers to BookStep class. +/** + * AMMLiquidity class provides AMM offers to BookStep class. * The offers are generated in two ways. If there are multiple * paths specified to the payment transaction then the offers * are generated based on the Fibonacci sequence with @@ -59,7 +60,8 @@ public: AMMLiquidity& operator=(AMMLiquidity const&) = delete; - /** Generate AMM offer. Returns nullopt if clobQuality is provided + /** + * Generate AMM offer. Returns nullopt if clobQuality is provided * and it is better than AMM offer quality. Otherwise returns AMM offer. * If clobQuality is provided then AMM offer size is set based on the * quality. @@ -104,12 +106,14 @@ public: } private: - /** Fetches current AMM balances. + /** + * Fetches current AMM balances. */ [[nodiscard]] TAmounts fetchBalances(ReadView const& view) const; - /** Generate AMM offers with the offer size based on Fibonacci sequence. + /** + * Generate AMM offers with the offer size based on Fibonacci sequence. * The sequence corresponds to the payment engine iterations with AMM * liquidity. Iterations that don't consume AMM offers don't count. * The number of iterations with AMM offers is limited. @@ -119,7 +123,8 @@ private: [[nodiscard]] TAmounts generateFibSeqOffer(TAmounts const& balances) const; - /** Generate max offer. The offer is generated as: + /** + * Generate max offer. The offer is generated as: * takerGets = 99% * balances.out takerPays = swapOut(takerGets). * Return nullopt if takerGets is 0 or takerGets == balances.out. */ diff --git a/include/xrpl/tx/paths/AMMOffer.h b/include/xrpl/tx/paths/AMMOffer.h index 40a9cf40b3..8e7ffedc10 100644 --- a/include/xrpl/tx/paths/AMMOffer.h +++ b/include/xrpl/tx/paths/AMMOffer.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -19,7 +20,8 @@ template class AMMLiquidity; class QualityFunction; -/** Represents synthetic AMM offer in BookStep. AMMOffer mirrors TOffer +/** + * Represents synthetic AMM offer in BookStep. AMMOffer mirrors TOffer * methods for use in generic BookStep methods. AMMOffer amounts * are changed indirectly in BookStep limiting steps. */ @@ -86,14 +88,16 @@ public: return consumed_; } - /** Limit out of the provided offer. If one-path then swapOut + /** + * Limit out of the provided offer. If one-path then swapOut * using current balances. If multi-path then ceil_out using * current quality. */ [[nodiscard]] TAmounts limitOut(TAmounts const& offerAmount, TOut const& limit, bool roundUp) const; - /** Limit in of the provided offer. If one-path then swapIn + /** + * Limit in of the provided offer. If one-path then swapIn * using current balances. If multi-path then ceil_in using * current quality. */ @@ -103,14 +107,18 @@ public: [[nodiscard]] QualityFunction getQualityFunc() const; - /** Send funds without incurring the transfer fee + /** + * Send funds without incurring the transfer fee */ template static TER send(Args&&... args) { return accountSend( - std::forward(args)..., WaiveTransferFee::Yes, AllowMPTOverflow::Yes); + std::forward(args)..., + SLE::pointer(), + WaiveTransferFee::Yes, + AllowMPTOverflow::Yes); } [[nodiscard]] bool @@ -127,7 +135,8 @@ public: return {ofrInRate, QUALITY_ONE}; } - /** Check the new pool product is greater or equal to the old pool + /** + * Check the new pool product is greater or equal to the old pool * product or if decreases then within some threshold. */ [[nodiscard]] bool diff --git a/include/xrpl/tx/paths/BookTip.h b/include/xrpl/tx/paths/BookTip.h index bad007ca5b..0a7e3c343e 100644 --- a/include/xrpl/tx/paths/BookTip.h +++ b/include/xrpl/tx/paths/BookTip.h @@ -11,10 +11,11 @@ namespace xrpl { class Logs; -/** Iterates and consumes raw offers in an order book. - Offers are presented from highest quality to lowest quality. This will - return all offers present including missing, invalid, unfunded, etc. -*/ +/** + * Iterates and consumes raw offers in an order book. + * Offers are presented from highest quality to lowest quality. This will + * return all offers present including missing, invalid, unfunded, etc. + */ class BookTip { private: @@ -28,7 +29,9 @@ private: Quality quality_{}; public: - /** Create the iterator. */ + /** + * Create the iterator. + */ BookTip(ApplyView& view, Book const& book); [[nodiscard]] uint256 const& @@ -55,10 +58,11 @@ public: return entry_; } - /** Erases the current offer and advance to the next offer. - Complexity: Constant - @return `true` if there is a next offer - */ + /** + * Erases the current offer and advance to the next offer. + * Complexity: Constant + * @return `true` if there is a next offer + */ bool step(beast::Journal j); }; diff --git a/include/xrpl/tx/paths/Flow.h b/include/xrpl/tx/paths/Flow.h index f73b9a3440..af056ce2fe 100644 --- a/include/xrpl/tx/paths/Flow.h +++ b/include/xrpl/tx/paths/Flow.h @@ -18,25 +18,25 @@ struct FlowDebugInfo; } // namespace path::detail /** - Make a payment from the src account to the dst account - - @param view Trust lines and balances - @param deliver Amount to deliver to the dst account - @param src Account providing input funds for the payment - @param dst Account receiving the payment - @param paths Set of paths to explore for liquidity - @param defaultPaths Include defaultPaths in the path set - @param partialPayment If the payment cannot deliver the entire - requested amount, deliver as much as possible, given the constraints - @param ownerPaysTransferFee If true then owner, not sender, pays fee - @param offerCrossing If Yes or Sell then flow is executing offer crossing, not - payments - @param limitQuality Do not use liquidity below this quality threshold - @param sendMax Do not spend more than this amount - @param j Journal to write journal messages to - @param flowDebugInfo If non-null a pointer to FlowDebugInfo for debugging - @return Actual amount in and out, and the result code -*/ + * Make a payment from the src account to the dst account + * + * @param view Trust lines and balances + * @param deliver Amount to deliver to the dst account + * @param src Account providing input funds for the payment + * @param dst Account receiving the payment + * @param paths Set of paths to explore for liquidity + * @param defaultPaths Include defaultPaths in the path set + * @param partialPayment If the payment cannot deliver the entire + * requested amount, deliver as much as possible, given the constraints + * @param ownerPaysTransferFee If true then owner, not sender, pays fee + * @param offerCrossing If Yes or Sell then flow is executing offer crossing, not + * payments + * @param limitQuality Do not use liquidity below this quality threshold + * @param sendMax Do not spend more than this amount + * @param j Journal to write journal messages to + * @param flowDebugInfo If non-null a pointer to FlowDebugInfo for debugging + * @return Actual amount in and out, and the result code + */ path::RippleCalc::Output flow( PaymentSandbox& view, diff --git a/include/xrpl/tx/paths/Offer.h b/include/xrpl/tx/paths/Offer.h index 164a933ba1..7f368fc2dd 100644 --- a/include/xrpl/tx/paths/Offer.h +++ b/include/xrpl/tx/paths/Offer.h @@ -44,38 +44,44 @@ public: TOffer(SLE::pointer entry, Quality quality); - /** Returns the quality of the offer. - Conceptually, the quality is the ratio of output to input currency. - The implementation calculates it as the ratio of input to output - currency (so it sorts ascending). The quality is computed at the time - the offer is placed, and never changes for the lifetime of the offer. - This is an important business rule that maintains accuracy when an - offer is partially filled; Subsequent partial fills will use the - original quality. - */ + /** + * Returns the quality of the offer. + * Conceptually, the quality is the ratio of output to input currency. + * The implementation calculates it as the ratio of input to output + * currency (so it sorts ascending). The quality is computed at the time + * the offer is placed, and never changes for the lifetime of the offer. + * This is an important business rule that maintains accuracy when an + * offer is partially filled; Subsequent partial fills will use the + * original quality. + */ [[nodiscard]] Quality quality() const noexcept { return quality_; } - /** Returns the account id of the offer's owner. */ + /** + * Returns the account id of the offer's owner. + */ [[nodiscard]] AccountID const& owner() const { return accountID_; } - /** Returns the in and out amounts. - Some or all of the out amount may be unfunded. - */ + /** + * Returns the in and out amounts. + * Some or all of the out amount may be unfunded. + */ [[nodiscard]] TAmounts const& amount() const { return amounts_; } - /** Returns `true` if no more funds can flow through this offer. */ + /** + * Returns `true` if no more funds can flow through this offer. + */ [[nodiscard]] bool fullyConsumed() const { @@ -86,7 +92,9 @@ public: return false; } - /** Adjusts the offer to indicate that we consumed some (or all) of it. */ + /** + * Adjusts the offer to indicate that we consumed some (or all) of it. + */ void consume(ApplyView& view, TAmounts const& consumed) { @@ -142,7 +150,8 @@ public: return {ofrInRate, ofrOutRate}; } - /** Check any required invariant. Limit order book offer + /** + * Check any required invariant. Limit order book offer * always returns true. */ [[nodiscard]] bool @@ -234,7 +243,8 @@ template TER TOffer::send(Args&&... args) { - return accountSend(std::forward(args)..., WaiveTransferFee::No, AllowMPTOverflow::Yes); + return accountSend( + std::forward(args)..., SLE::pointer(), WaiveTransferFee::No, AllowMPTOverflow::Yes); } template diff --git a/include/xrpl/tx/paths/OfferStream.h b/include/xrpl/tx/paths/OfferStream.h index 98c58876c9..28eefb5d66 100644 --- a/include/xrpl/tx/paths/OfferStream.h +++ b/include/xrpl/tx/paths/OfferStream.h @@ -85,23 +85,26 @@ public: virtual ~TOfferStreamBase() = default; - /** Returns the offer at the tip of the order book. - Offers are always presented in decreasing quality. - Only valid if step() returned `true`. - */ + /** + * Returns the offer at the tip of the order book. + * Offers are always presented in decreasing quality. + * Only valid if step() returned `true`. + */ [[nodiscard]] TOffer& tip() const { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) return const_cast(this)->offer_; } - /** Advance to the next valid offer. - This automatically removes: - - Offers with missing ledger entries - - Offers found unfunded - - expired offers - @return `true` if there is a valid offer. - */ + /** + * Advance to the next valid offer. + * This automatically removes: + * - Offers with missing ledger entries + * - Offers found unfunded + * - expired offers + * @return `true` if there is a valid offer. + */ bool step(); @@ -113,23 +116,24 @@ public: } }; -/** Presents and consumes the offers in an order book. - - The `view_' ` `ApplyView` accumulates changes to the ledger. - The `cancelView_` is used to determine if an offer is found - unfunded or became unfunded. - The `permToRemove` collection identifies offers that should be - removed even if the strand associated with this OfferStream - is not applied. - - Certain invalid offers are added to the `permToRemove` collection: - - Offers with missing ledger entries - - Offers that expired - - Offers found unfunded: - An offer is found unfunded when the corresponding balance is zero - and the caller has not modified the balance. This is accomplished - by also looking up the balance in the cancel view. -*/ +/** + * Presents and consumes the offers in an order book. + * + * The `view_' ` `ApplyView` accumulates changes to the ledger. + * The `cancelView_` is used to determine if an offer is found + * unfunded or became unfunded. + * The `permToRemove` collection identifies offers that should be + * removed even if the strand associated with this OfferStream + * is not applied. + * + * Certain invalid offers are added to the `permToRemove` collection: + * - Offers with missing ledger entries + * - Offers that expired + * - Offers found unfunded: + * An offer is found unfunded when the corresponding balance is zero + * and the caller has not modified the balance. This is accomplished + * by also looking up the balance in the cancel view. + */ template class FlowOfferStream : public TOfferStreamBase { diff --git a/include/xrpl/tx/paths/RippleCalc.h b/include/xrpl/tx/paths/RippleCalc.h index 62c966d384..c3ee9165a6 100644 --- a/include/xrpl/tx/paths/RippleCalc.h +++ b/include/xrpl/tx/paths/RippleCalc.h @@ -20,11 +20,12 @@ namespace detail { struct FlowDebugInfo; } // namespace detail -/** RippleCalc calculates the quality of a payment path. - - Quality is the amount of input required to produce a given output along a - specified path - another name for this is exchange rate. -*/ +/** + * RippleCalc calculates the quality of a payment path. + * + * Quality is the amount of input required to produce a given output along a + * specified path - another name for this is exchange rate. + */ class RippleCalc { public: diff --git a/include/xrpl/tx/paths/detail/FlatSets.h b/include/xrpl/tx/paths/detail/FlatSets.h index c0fc8fa417..267c0499a2 100644 --- a/include/xrpl/tx/paths/detail/FlatSets.h +++ b/include/xrpl/tx/paths/detail/FlatSets.h @@ -4,11 +4,12 @@ namespace xrpl { -/** Given two flat sets dst and src, compute dst = dst union src - - @param dst set to store the resulting union, and also a source of elements - for the union - @param src second source of elements for the union +/** + * Given two flat sets dst and src, compute dst = dst union src + * + * @param dst set to store the resulting union, and also a source of elements + * for the union + * @param src second source of elements for the union */ template void diff --git a/include/xrpl/tx/paths/detail/Steps.h b/include/xrpl/tx/paths/detail/Steps.h index 7eb6b938a5..8ee37c026c 100644 --- a/include/xrpl/tx/paths/detail/Steps.h +++ b/include/xrpl/tx/paths/detail/Steps.h @@ -50,31 +50,31 @@ issues(DebtDirection dir) } /** - A step in a payment path - - There are five concrete step classes: - DirectStepI is an IOU step between accounts - BookStepII is an IOU/IOU offer book - BookStepIX is an IOU/XRP offer book - BookStepXI is an XRP/IOU offer book - XRPEndpointStep is the source or destination account for XRP - MPTEndpointStep is the source or destination account for MPT - - Amounts may be transformed through a step in either the forward or the - reverse direction. In the forward direction, the function `fwd` is used to - find the amount the step would output given an input amount. In the reverse - direction, the function `rev` is used to find the amount of input needed to - produce the desired output. - - Amounts are always transformed using liquidity with the same quality (quality - is the amount out/amount in). For example, a BookStep may use multiple offers - when executing `fwd` or `rev`, but all those offers will be from the same - quality directory. - - A step may not have enough liquidity to transform the entire requested - amount. Both `fwd` and `rev` return a pair of amounts (one for input amount, - one for output amount) that show how much of the requested amount the step - was actually able to use. + * A step in a payment path + * + * There are five concrete step classes: + * DirectStepI is an IOU step between accounts + * BookStepII is an IOU/IOU offer book + * BookStepIX is an IOU/XRP offer book + * BookStepXI is an XRP/IOU offer book + * XRPEndpointStep is the source or destination account for XRP + * MPTEndpointStep is the source or destination account for MPT + * + * Amounts may be transformed through a step in either the forward or the + * reverse direction. In the forward direction, the function `fwd` is used to + * find the amount the step would output given an input amount. In the reverse + * direction, the function `rev` is used to find the amount of input needed to + * produce the desired output. + * + * Amounts are always transformed using liquidity with the same quality (quality + * is the amount out/amount in). For example, a BookStep may use multiple offers + * when executing `fwd` or `rev`, but all those offers will be from the same + * quality directory. + * + * A step may not have enough liquidity to transform the entire requested + * amount. Both `fwd` and `rev` return a pair of amounts (one for input amount, + * one for output amount) that show how much of the requested amount the step + * was actually able to use. */ class Step { @@ -82,17 +82,17 @@ public: virtual ~Step() = default; /** - Find the amount we need to put into the step to get the requested out - subject to liquidity limits - - @param sb view with the strand's state of balances and offers - @param afView view the state of balances before the strand runs - this determines if an offer becomes unfunded or is found unfunded - @param ofrsToRm offers found unfunded or in an error state are added to - this collection - @param out requested step output - @return actual step input and output - */ + * Find the amount we need to put into the step to get the requested out + * subject to liquidity limits + * + * @param sb view with the strand's state of balances and offers + * @param afView view the state of balances before the strand runs + * this determines if an offer becomes unfunded or is found unfunded + * @param ofrsToRm offers found unfunded or in an error state are added to + * this collection + * @param out requested step output + * @return actual step input and output + */ virtual std::pair rev(PaymentSandbox& sb, ApplyView& afView, @@ -100,17 +100,17 @@ public: EitherAmount const& out) = 0; /** - Find the amount we get out of the step given the input - subject to liquidity limits - - @param sb view with the strand's state of balances and offers - @param afView view the state of balances before the strand runs - this determines if an offer becomes unfunded or is found unfunded - @param ofrsToRm offers found unfunded or in an error state are added to - this collection - @param in requested step input - @return actual step input and output - */ + * Find the amount we get out of the step given the input + * subject to liquidity limits + * + * @param sb view with the strand's state of balances and offers + * @param afView view the state of balances before the strand runs + * this determines if an offer becomes unfunded or is found unfunded + * @param ofrsToRm offers found unfunded or in an error state are added to + * this collection + * @param in requested step input + * @return actual step input and output + */ virtual std::pair fwd(PaymentSandbox& sb, ApplyView& afView, @@ -118,23 +118,23 @@ public: EitherAmount const& in) = 0; /** - Amount of currency computed coming into the Step the last time the - step ran in reverse. - */ + * Amount of currency computed coming into the Step the last time the + * step ran in reverse. + */ [[nodiscard]] virtual std::optional cachedIn() const = 0; /** - Amount of currency computed coming out of the Step the last time the - step ran in reverse. - */ + * Amount of currency computed coming out of the Step the last time the + * step ran in reverse. + */ [[nodiscard]] virtual std::optional cachedOut() const = 0; /** - If this step is DirectStepI (IOU->IOU direct step), return the src - account. This is needed for checkNoRipple. - */ + * If this step is DirectStepI (IOU->IOU direct step), return the src + * account. This is needed for checkNoRipple. + */ [[nodiscard]] virtual std::optional directStepSrcAcct() const { @@ -150,19 +150,19 @@ public: } /** - If this step is a DirectStepI and the src redeems to the dst, return - true, otherwise return false. If this step is a BookStep, return false if - the owner pays the transfer fee, otherwise return true. - - @param sb view with the strand's state of balances and offers - @param dir reverse -> called from rev(); forward -> called from fwd(). - */ + * If this step is a DirectStepI and the src redeems to the dst, return + * true, otherwise return false. If this step is a BookStep, return false if + * the owner pays the transfer fee, otherwise return true. + * + * @param sb view with the strand's state of balances and offers + * @param dir reverse -> called from rev(); forward -> called from fwd(). + */ [[nodiscard]] virtual DebtDirection debtDirection(ReadView const& sb, StrandDirection dir) const = 0; /** - If this step is a DirectStepI, return the quality in of the dst account. - */ + * If this step is a DirectStepI, return the quality in of the dst account. + */ [[nodiscard]] virtual std::uint32_t lineQualityIn(ReadView const&) const { @@ -170,22 +170,23 @@ public: } /** - Find an upper bound of quality for the step - - @param v view to query the ledger state from - @param prevStepDir Set to DebtDirection::redeems if the previous step redeems. - @return A pair. The first element is the upper bound of quality for the step, or std::nullopt - if the step is dry. The second element will be set to DebtDirection::redeems if this - steps redeems, DebtDirection:issues if this step issues. - @note It is an upper bound because offers on the books may be unfunded. If there is always a - funded offer at the tip of the book, then we could rename this `theoreticalQuality` - rather than `qualityUpperBound`. It could still differ from the actual quality, but - except for "dust" amounts, it should be a good estimate for the actual quality. - */ + * Find an upper bound of quality for the step + * + * @param v view to query the ledger state from + * @param prevStepDir Set to DebtDirection::redeems if the previous step redeems. + * @return A pair. The first element is the upper bound of quality for the step, or std::nullopt + * if the step is dry. The second element will be set to DebtDirection::redeems if this + * steps redeems, DebtDirection:issues if this step issues. + * @note It is an upper bound because offers on the books may be unfunded. If there is always a + * funded offer at the tip of the book, then we could rename this `theoreticalQuality` + * rather than `qualityUpperBound`. It could still differ from the actual quality, but + * except for "dust" amounts, it should be a good estimate for the actual quality. + */ [[nodiscard]] virtual std::pair, DebtDirection> qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const = 0; - /** Get QualityFunction. Used in one path optimization where + /** + * Get QualityFunction. Used in one path optimization where * the quality function is non-constant (has AMM) and there is * limitQuality. QualityFunction allows calculation of * required path output given requested limitQuality. @@ -195,12 +196,13 @@ public: [[nodiscard]] virtual std::pair, DebtDirection> getQualityFunc(ReadView const& v, DebtDirection prevStepDir) const; - /** Return the number of offers consumed or partially consumed the last time - the step ran, including expired and unfunded offers. - - N.B. This this not the total number offers consumed by this step for the - entire payment, it is only the number the last time it ran. Offers may - be partially consumed multiple times during a payment. + /** + * Return the number of offers consumed or partially consumed the last time + * the step ran, including expired and unfunded offers. + * + * N.B. This this not the total number offers consumed by this step for the + * entire payment, it is only the number the last time it ran. Offers may + * be partially consumed multiple times during a payment. */ [[nodiscard]] virtual std::uint32_t offersUsed() const @@ -209,8 +211,8 @@ public: } /** - If this step is a BookStep, return the book. - */ + * If this step is a BookStep, return the book. + */ [[nodiscard]] virtual std::optional bookStepBook() const { @@ -218,15 +220,15 @@ public: } /** - Check if amount is zero - */ + * Check if amount is zero + */ [[nodiscard]] virtual bool isZero(EitherAmount const& out) const = 0; /** - Return true if the step should be considered inactive. - A strand that has additional liquidity may be marked inactive if a step - has consumed too many offers. + * Return true if the step should be considered inactive. + * A strand that has additional liquidity may be marked inactive if a step + * has consumed too many offers. */ [[nodiscard]] virtual bool inactive() const @@ -235,55 +237,59 @@ public: } /** - Return true if Out of lhs == Out of rhs. - */ + * Return true if Out of lhs == Out of rhs. + */ [[nodiscard]] virtual bool equalOut(EitherAmount const& lhs, EitherAmount const& rhs) const = 0; /** - Return true if In of lhs == In of rhs. - */ + * Return true if In of lhs == In of rhs. + */ [[nodiscard]] virtual bool equalIn(EitherAmount const& lhs, EitherAmount const& rhs) const = 0; /** - Check that the step can correctly execute in the forward direction - - @param sb view with the strands state of balances and offers - @param afView view the state of balances before the strand runs - this determines if an offer becomes unfunded or is found unfunded - @param in requested step input - @return first element is true if step is valid, second element is out - amount - */ + * Check that the step can correctly execute in the forward direction + * + * @param sb view with the strands state of balances and offers + * @param afView view the state of balances before the strand runs + * this determines if an offer becomes unfunded or is found unfunded + * @param in requested step input + * @return first element is true if step is valid, second element is out + * amount + */ virtual std::pair validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmount const& in) = 0; - /** Return true if lhs == rhs. - - @param lhs Step to compare. - @param rhs Step to compare. - @return true if lhs == rhs. - */ + /** + * Return true if lhs == rhs. + * + * @param lhs Step to compare. + * @param rhs Step to compare. + * @return true if lhs == rhs. + */ friend bool operator==(Step const& lhs, Step const& rhs) { return lhs.equal(rhs); } - /** Return true if lhs != rhs. - - @param lhs Step to compare. - @param rhs Step to compare. - @return true if lhs != rhs. - */ + /** + * Return true if lhs != rhs. + * + * @param lhs Step to compare. + * @param rhs Step to compare. + * @return true if lhs != rhs. + */ friend bool operator!=(Step const& lhs, Step const& rhs) { return !(lhs == rhs); } - /** Streaming operator for a Step. */ + /** + * Streaming operator for a Step. + */ friend std::ostream& operator<<(std::ostream& stream, Step const& step) { @@ -308,7 +314,7 @@ Step::getQualityFunc(ReadView const& v, DebtDirection prevStepDir) const return {std::nullopt, res.second}; } -/// @cond INTERNAL +/** @cond INTERNAL */ using Strand = std::vector>; inline std::uint32_t @@ -322,9 +328,9 @@ offersUsed(Strand const& strand) } return r; } -/// @endcond +/** @endcond */ -/// @cond INTERNAL +/** @cond INTERNAL */ inline bool operator==(Strand const& lhs, Strand const& rhs) { @@ -337,21 +343,21 @@ operator==(Strand const& lhs, Strand const& rhs) } return true; } -/// @endcond +/** @endcond */ -/* - Normalize a path by inserting implied accounts and offers - - @param src Account that is sending assets - @param dst Account that is receiving assets - @param deliver Asset the dst account will receive - (if issuer of deliver == dst, then accept any issuer) - @param sendMax Optional asset to send. - @param path Liquidity sources to use for this strand of the payment. The path - contains an ordered collection of the offer books to use and - accounts to ripple through. - @return error code and normalized path -*/ +/** + * Normalize a path by inserting implied accounts and offers + * + * @param src Account that is sending assets + * @param dst Account that is receiving assets + * @param deliver Asset the dst account will receive + * (if issuer of deliver == dst, then accept any issuer) + * @param sendMax Optional asset to send. + * @param path Liquidity sources to use for this strand of the payment. The path + * contains an ordered collection of the offer books to use and + * accounts to ripple through. + * @return error code and normalized path + */ std::pair normalizePath( AccountID const& src, @@ -361,29 +367,29 @@ normalizePath( STPath const& path); /** - Create a Strand for the specified path - - @param sb view for trust lines, balances, and attributes like auth and freeze - @param src Account that is sending assets - @param dst Account that is receiving assets - @param deliver Asset the dst account will receive - (if issuer of deliver == dst, then accept any issuer) - @param limitQuality Offer crossing BookSteps use this value in an - optimization. If, during direct offer crossing, the - quality of the tip of the book drops below this value, - then evaluating the strand can stop. - @param sendMaxAsset Optional asset to send. - @param path Liquidity sources to use for this strand of the payment. The path - contains an ordered collection of the offer books to use and - accounts to ripple through. - @param ownerPaysTransferFee false -> charge sender; true -> charge offer - owner - @param offerCrossing false -> payment; true -> offer crossing - @param ammContext counts iterations with AMM offers - @param domainID the domain that order books will use - @param j Journal for logging messages - @return Error code and constructed Strand -*/ + * Create a Strand for the specified path + * + * @param sb view for trust lines, balances, and attributes like auth and freeze + * @param src Account that is sending assets + * @param dst Account that is receiving assets + * @param deliver Asset the dst account will receive + * (if issuer of deliver == dst, then accept any issuer) + * @param limitQuality Offer crossing BookSteps use this value in an + * optimization. If, during direct offer crossing, the + * quality of the tip of the book drops below this value, + * then evaluating the strand can stop. + * @param sendMaxAsset Optional asset to send. + * @param path Liquidity sources to use for this strand of the payment. The path + * contains an ordered collection of the offer books to use and + * accounts to ripple through. + * @param ownerPaysTransferFee false -> charge sender; true -> charge offer + * owner + * @param offerCrossing false -> payment; true -> offer crossing + * @param ammContext counts iterations with AMM offers + * @param domainID the domain that order books will use + * @param j Journal for logging messages + * @return Error code and constructed Strand + */ std::pair toStrand( ReadView const& sb, @@ -400,31 +406,31 @@ toStrand( beast::Journal j); /** - Create a Strand for each specified path (including the default path, if - indicated) - - @param sb View for trust lines, balances, and attributes like auth and freeze - @param src Account that is sending assets - @param dst Account that is receiving assets - @param deliver Asset the dst account will receive - (if issuer of deliver == dst, then accept any issuer) - @param limitQuality Offer crossing BookSteps use this value in an - optimization. If, during direct offer crossing, the - quality of the tip of the book drops below this value, - then evaluating the strand can stop. - @param sendMax Optional asset to send. - @param paths Paths to use to fulfill the payment. Each path in the pathset - contains an ordered collection of the offer books to use and - accounts to ripple through. - @param addDefaultPath Determines if the default path should be included - @param ownerPaysTransferFee false -> charge sender; true -> charge offer - owner - @param offerCrossing false -> payment; true -> offer crossing - @param ammContext counts iterations with AMM offers - @param domainID the domain that order books will use - @param j Journal for logging messages - @return error code and collection of strands -*/ + * Create a Strand for each specified path (including the default path, if + * indicated) + * + * @param sb View for trust lines, balances, and attributes like auth and freeze + * @param src Account that is sending assets + * @param dst Account that is receiving assets + * @param deliver Asset the dst account will receive + * (if issuer of deliver == dst, then accept any issuer) + * @param limitQuality Offer crossing BookSteps use this value in an + * optimization. If, during direct offer crossing, the + * quality of the tip of the book drops below this value, + * then evaluating the strand can stop. + * @param sendMax Optional asset to send. + * @param paths Paths to use to fulfill the payment. Each path in the pathset + * contains an ordered collection of the offer books to use and + * accounts to ripple through. + * @param addDefaultPath Determines if the default path should be included + * @param ownerPaysTransferFee false -> charge sender; true -> charge offer + * owner + * @param offerCrossing false -> payment; true -> offer crossing + * @param ammContext counts iterations with AMM offers + * @param domainID the domain that order books will use + * @param j Journal for logging messages + * @return error code and collection of strands + */ std::pair> toStrands( ReadView const& sb, @@ -441,7 +447,7 @@ toStrands( std::optional const& domainID, beast::Journal j); -/// @cond INTERNAL +/** @cond INTERNAL */ template struct StepImp : public Step { @@ -490,9 +496,9 @@ public: } friend TDerived; }; -/// @endcond +/** @endcond */ -/// @cond INTERNAL +/** @cond INTERNAL */ // Thrown when unexpected errors occur class FlowException : public std::runtime_error { @@ -507,9 +513,9 @@ public: { } }; -/// @endcond +/** @endcond */ -/// @cond INTERNAL +/** @cond INTERNAL */ // Check equal with tolerance bool checkNear(IOUAmount const& expected, IOUAmount const& actual); @@ -523,10 +529,10 @@ checkNear(XRPAmount const& expected, XRPAmount const& actual) { return expected == actual; } -/// @endcond +/** @endcond */ /** - Context needed to build Strand Steps and for error checking + * Context needed to build Strand Steps and for error checking */ struct StrandContext { @@ -541,25 +547,30 @@ struct StrandContext OfferCrossing const offerCrossing; ///< Yes/Sell if offer crossing, not payment bool const isDefaultPath; ///< true if Strand is default path size_t const strandSize; ///< Length of Strand - /** The previous step in the strand. Needed to check the no ripple - constraint + /** + * The previous step in the strand. Needed to check the no ripple + * constraint */ Step const* const prevStep = nullptr; - /** A strand may not include the same account node more than once - in the same currency. In a direct step, an account will show up - at most twice: once as a src and once as a dst (hence the two element - array). The strandSrc and strandDst will only show up once each. - */ + /** + * A strand may not include the same account node more than once + * in the same currency. In a direct step, an account will show up + * at most twice: once as a src and once as a dst (hence the two element + * array). The strandSrc and strandDst will only show up once each. + */ std::array, 2>& seenDirectAssets; - /** A strand may not include an offer that output the same issue more - than once - */ + /** + * A strand may not include an offer that output the same issue more + * than once + */ boost::container::flat_set& seenBookOuts; AMMContext& ammContext; std::optional domainID; // the domain the order book will use beast::Journal const j; - /** StrandContext constructor. */ + /** + * StrandContext constructor. + */ StrandContext( ReadView const& view, std::vector> const& strand, @@ -581,7 +592,7 @@ struct StrandContext beast::Journal j); ///< Journal for logging }; -/// @cond INTERNAL +/** @cond INTERNAL */ namespace test { // Needed for testing bool @@ -659,6 +670,6 @@ isDirectXrpToXrp(Strand const& strand) return false; } } -/// @endcond +/** @endcond */ } // namespace xrpl diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h index fe657b2100..c932c49cca 100644 --- a/include/xrpl/tx/paths/detail/StrandFlow.h +++ b/include/xrpl/tx/paths/detail/StrandFlow.h @@ -39,7 +39,9 @@ namespace xrpl { -/** Result of flow() execution of a single Strand. */ +/** + * Result of flow() execution of a single Strand. + */ template struct StrandResult { @@ -56,7 +58,9 @@ struct StrandResult bool inactive = false; ///< Strand should not considered as a further ///< source of liquidity (dry) - /** Strand result constructor */ + /** + * Strand result constructor + */ StrandResult() = default; StrandResult( @@ -83,15 +87,15 @@ struct StrandResult }; /** - Request `out` amount from a strand - - @param baseView Trust lines and balances - @param strand Steps of Accounts to ripple through and offer books to use - @param maxIn Max amount of input allowed - @param out Amount of output requested from the strand - @param j Journal to write log messages to - @return Actual amount in and out from the strand, errors, offers to remove, - and payment sandbox + * Request `out` amount from a strand + * + * @param baseView Trust lines and balances + * @param strand Steps of Accounts to ripple through and offer books to use + * @param maxIn Max amount of input allowed + * @param out Amount of output requested from the strand + * @param j Journal to write log messages to + * @return Actual amount in and out from the strand, errors, offers to remove, + * and payment sandbox */ template StrandResult @@ -296,7 +300,7 @@ flow( } } -/// @cond INTERNAL +/** @cond INTERNAL */ template struct FlowResult { @@ -335,9 +339,9 @@ struct FlowResult { } }; -/// @endcond +/** @endcond */ -/// @cond INTERNAL +/** @cond INTERNAL */ inline std::optional qualityUpperBound(ReadView const& v, Strand const& strand) { @@ -357,10 +361,11 @@ qualityUpperBound(ReadView const& v, Strand const& strand) } return q; }; -/// @endcond +/** @endcond */ -/// @cond INTERNAL -/** Limit remaining out only if one strand and limitQuality is included. +/** @cond INTERNAL */ +/** + * Limit remaining out only if one strand and limitQuality is included. * Targets one path payment with AMM where the average quality is linear * and instant quality is quadratic function of output. Calculating quality * function for the whole strand enables figuring out required output @@ -428,9 +433,9 @@ limitOut( return remainingOut; return std::min(out, remainingOut); }; -/// @endcond +/** @endcond */ -/// @cond INTERNAL +/** @cond INTERNAL */ /* Track the non-dry strands flow will search the non-dry strands (stored in `cur_`) for the best @@ -545,28 +550,28 @@ public: return cur_.size(); } }; -/// @endcond +/** @endcond */ /** - Request `out` amount from a collection of strands - - Attempt to fulfill the payment by using liquidity from the strands in order - from least expensive to most expensive - - @param baseView Trust lines and balances - @param strands Each strand contains the steps of accounts to ripple through - and offer books to use - @param outReq Amount of output requested from the strand - @param partialPayment If true allow less than the full payment - @param offerCrossing If true offer crossing, not handling a standard payment - @param limitQuality If present, the minimum quality for any strand taken - @param sendMaxST If present, the maximum STAmount to send - @param j Journal to write journal messages to - @param ammContext counts iterations with AMM offers - @param flowDebugInfo If pointer is non-null, write flow debug info here - @return Actual amount in and out from the strands, errors, and payment - sandbox -*/ + * Request `out` amount from a collection of strands + * + * Attempt to fulfill the payment by using liquidity from the strands in order + * from least expensive to most expensive + * + * @param baseView Trust lines and balances + * @param strands Each strand contains the steps of accounts to ripple through + * and offer books to use + * @param outReq Amount of output requested from the strand + * @param partialPayment If true allow less than the full payment + * @param offerCrossing If true offer crossing, not handling a standard payment + * @param limitQuality If present, the minimum quality for any strand taken + * @param sendMaxST If present, the maximum STAmount to send + * @param j Journal to write journal messages to + * @param ammContext counts iterations with AMM offers + * @param flowDebugInfo If pointer is non-null, write flow debug info here + * @return Actual amount in and out from the strands, errors, and payment + * sandbox + */ template FlowResult flow( diff --git a/include/xrpl/tx/transactors/account/SignerListSet.h b/include/xrpl/tx/transactors/account/SignerListSet.h index da2274a1de..9f7872a18e 100644 --- a/include/xrpl/tx/transactors/account/SignerListSet.h +++ b/include/xrpl/tx/transactors/account/SignerListSet.h @@ -20,9 +20,9 @@ namespace xrpl { /** -See the README.md for an overview of the SignerListSet transaction that -this class implements. -*/ + * See the README.md for an overview of the SignerListSet transaction that + * this class implements. + */ class SignerListSet : public Transactor { private: diff --git a/include/xrpl/tx/transactors/dex/AMMBid.h b/include/xrpl/tx/transactors/dex/AMMBid.h index fa257696e8..9d8eb8578b 100644 --- a/include/xrpl/tx/transactors/dex/AMMBid.h +++ b/include/xrpl/tx/transactors/dex/AMMBid.h @@ -11,7 +11,8 @@ namespace xrpl { -/** AMMBid implements AMM bid Transactor. +/** + * AMMBid implements AMM bid Transactor. * This is a mechanism for an AMM instance to auction-off * the trading advantages to users (arbitrageurs) at a discounted * TradingFee for a 24 hour slot. Any account that owns corresponding diff --git a/include/xrpl/tx/transactors/dex/AMMClawback.h b/include/xrpl/tx/transactors/dex/AMMClawback.h index 54eb3c9f27..3f3f2c421a 100644 --- a/include/xrpl/tx/transactors/dex/AMMClawback.h +++ b/include/xrpl/tx/transactors/dex/AMMClawback.h @@ -56,7 +56,8 @@ private: TER applyGuts(Sandbox& view); - /** Withdraw both assets by providing maximum amount of asset1, + /** + * Withdraw both assets by providing maximum amount of asset1, * asset2's amount will be calculated according to the current proportion. * Since it is two-asset withdrawal, tfee is omitted. * @param view diff --git a/include/xrpl/tx/transactors/dex/AMMContext.h b/include/xrpl/tx/transactors/dex/AMMContext.h index 65954044be..b878e4f10b 100644 --- a/include/xrpl/tx/transactors/dex/AMMContext.h +++ b/include/xrpl/tx/transactors/dex/AMMContext.h @@ -6,7 +6,8 @@ namespace xrpl { -/** Maintains AMM info per overall payment engine execution and +/** + * Maintains AMM info per overall payment engine execution and * individual iteration. * Only one instance of this class is created in Flow.cpp::flow(). * The reference is percolated through calls to AMMLiquidity class, @@ -84,7 +85,8 @@ public: return accountID_; } - /** Strand execution may fail. Reset the flag at the start + /** + * Strand execution may fail. Reset the flag at the start * of each payment engine iteration. */ void diff --git a/include/xrpl/tx/transactors/dex/AMMCreate.h b/include/xrpl/tx/transactors/dex/AMMCreate.h index 188af8d4ef..5260cf31cd 100644 --- a/include/xrpl/tx/transactors/dex/AMMCreate.h +++ b/include/xrpl/tx/transactors/dex/AMMCreate.h @@ -11,34 +11,35 @@ namespace xrpl { -/** AMMCreate implements Automatic Market Maker(AMM) creation Transactor. - * It creates a new AMM instance with two tokens. Any trader, or Liquidity - * Provider (LP), can create the AMM instance and receive in return shares - * of the AMM pool in the form of LPTokens. The number of tokens that LP gets - * are determined by LPTokens = sqrt(A * B), where A and B is the current - * composition of the AMM pool. LP can add (AMMDeposit) or withdraw - * (AMMWithdraw) tokens from AMM and - * AMM can be used transparently in the payment or offer crossing transactions. - * Trading fee is charged to the traders for the trades executed against - * AMM instance. The fee is added to the AMM pool and distributed to the LPs - * in proportion to the LPTokens upon liquidity removal. The fee can be voted - * on by LP's (AMMVote). LP's can continuously bid (AMMBid) for the 24 hour - * auction slot, which enables LP's to trade at zero trading fee. - * AMM instance creates AccountRoot object with disabled master key - * for book-keeping of XRP balance if one of the tokens - * is XRP, a trustline for each IOU token, a trustline to keep track - * of LPTokens, and ltAMM ledger object. AccountRoot ID is generated - * internally from the parent's hash. ltAMM's object ID is +/** + * AMMCreate implements Automatic Market Maker(AMM) creation Transactor. + * It creates a new AMM instance with two tokens. Any trader, or Liquidity + * Provider (LP), can create the AMM instance and receive in return shares + * of the AMM pool in the form of LPTokens. The number of tokens that LP gets + * are determined by LPTokens = sqrt(A * B), where A and B is the current + * composition of the AMM pool. LP can add (AMMDeposit) or withdraw + * (AMMWithdraw) tokens from AMM and + * AMM can be used transparently in the payment or offer crossing transactions. + * Trading fee is charged to the traders for the trades executed against + * AMM instance. The fee is added to the AMM pool and distributed to the LPs + * in proportion to the LPTokens upon liquidity removal. The fee can be voted + * on by LP's (AMMVote). LP's can continuously bid (AMMBid) for the 24 hour + * auction slot, which enables LP's to trade at zero trading fee. + * AMM instance creates AccountRoot object with disabled master key + * for book-keeping of XRP balance if one of the tokens + * is XRP, a trustline for each IOU token, a trustline to keep track + * of LPTokens, and ltAMM ledger object. AccountRoot ID is generated + * internally from the parent's hash. ltAMM's object ID is * hash{token1.currency, token1.issuer, token2.currency, token2.issuer}, where * issue1 < issue2. ltAMM object provides mapping from the hash to AccountRoot * ID and contains: AMMAccount - AMM AccountRoot ID. TradingFee - AMM voted * TradingFee. VoteSlots - Array of VoteEntry, contains fee vote information. - * AuctionSlot - Auction slot, contains discounted fee bid information. - * LPTokenBalance - LPTokens outstanding balance. - * AMMToken - currency/issuer information for AMM tokens. - * AMMDeposit, AMMWithdraw, AMMVote, and AMMBid transactions use the hash - * to access AMM instance. - * @see [XLS30d:Creating AMM instance on + * AuctionSlot - Auction slot, contains discounted fee bid information. + * LPTokenBalance - LPTokens outstanding balance. + * AMMToken - currency/issuer information for AMM tokens. + * AMMDeposit, AMMWithdraw, AMMVote, and AMMBid transactions use the hash + * to access AMM instance. + * @see [XLS30d:Creating AMM instance on * XRPL](https://github.com/XRPLF/XRPL-Standards/discussions/78) */ class AMMCreate : public Transactor @@ -62,7 +63,9 @@ public: static TER preclaim(PreclaimContext const& ctx); - /** Attempt to create the AMM instance. */ + /** + * Attempt to create the AMM instance. + */ TER doApply() override; diff --git a/include/xrpl/tx/transactors/dex/AMMDelete.h b/include/xrpl/tx/transactors/dex/AMMDelete.h index 4a0905fe10..abf800c4d1 100644 --- a/include/xrpl/tx/transactors/dex/AMMDelete.h +++ b/include/xrpl/tx/transactors/dex/AMMDelete.h @@ -11,7 +11,8 @@ namespace xrpl { -/** AMMDelete implements AMM delete transactor. This is a mechanism to +/** + * AMMDelete implements AMM delete transactor. This is a mechanism to * delete AMM in an empty state when the number of LP tokens is 0. * AMMDelete deletes the trustlines up to configured maximum. If all * trustlines are deleted then AMM ltAMM and root account are deleted. diff --git a/include/xrpl/tx/transactors/dex/AMMDeposit.h b/include/xrpl/tx/transactors/dex/AMMDeposit.h index b87db19b2d..2959f1b10a 100644 --- a/include/xrpl/tx/transactors/dex/AMMDeposit.h +++ b/include/xrpl/tx/transactors/dex/AMMDeposit.h @@ -20,7 +20,8 @@ namespace xrpl { class Sandbox; -/** AMMDeposit implements AMM deposit Transactor. +/** + * AMMDeposit implements AMM deposit Transactor. * The deposit transaction is used to add liquidity to the AMM instance pool, * thus obtaining some share of the instance's pools in the form of LPTokens. * If the trader deposits proportional values of both assets without changing @@ -92,7 +93,8 @@ private: std::pair applyGuts(Sandbox& view); - /** Deposit requested assets and token amount into LP account. + /** + * Deposit requested assets and token amount into LP account. * Return new total LPToken balance. * @param view * @param ammAccount @@ -121,7 +123,8 @@ private: std::optional const& lpTokensDepositMin, std::uint16_t tfee); - /** Equal asset deposit (LPTokens) for the specified share of + /** + * Equal asset deposit (LPTokens) for the specified share of * the AMM instance pools. The trading fee is not charged. * @param view * @param ammAccount @@ -146,7 +149,8 @@ private: std::optional const& deposit2Min, std::uint16_t tfee); - /** Equal asset deposit (Asset1In, Asset2In) with the constraint on + /** + * Equal asset deposit (Asset1In, Asset2In) with the constraint on * the maximum amount of both assets that the trader is willing to deposit. * The trading fee is not charged. * @param view @@ -172,7 +176,8 @@ private: std::optional const& lpTokensDepositMin, std::uint16_t tfee); - /** Single asset deposit (Asset1In) by the amount. + /** + * Single asset deposit (Asset1In) by the amount. * The trading fee is charged. * @param view * @param ammAccount @@ -193,7 +198,8 @@ private: std::optional const& lpTokensDepositMin, std::uint16_t tfee); - /** Single asset deposit (Asset1In, LPTokens) by the tokens. + /** + * Single asset deposit (Asset1In, LPTokens) by the tokens. * The trading fee is charged. * @param view * @param ammAccount @@ -214,7 +220,8 @@ private: STAmount const& lpTokensDeposit, std::uint16_t tfee); - /** Single asset deposit (Asset1In, EPrice) with two constraints. + /** + * Single asset deposit (Asset1In, EPrice) with two constraints. * The trading fee is charged. * @param view * @param ammAccount @@ -235,7 +242,8 @@ private: STAmount const& ePrice, std::uint16_t tfee); - /** Equal deposit in empty AMM state (LP tokens balance is 0) + /** + * Equal deposit in empty AMM state (LP tokens balance is 0) * @param view * @param ammAccount * @param amount requested asset1 deposit amount diff --git a/include/xrpl/tx/transactors/dex/AMMVote.h b/include/xrpl/tx/transactors/dex/AMMVote.h index 10ad284bb3..4d63a98534 100644 --- a/include/xrpl/tx/transactors/dex/AMMVote.h +++ b/include/xrpl/tx/transactors/dex/AMMVote.h @@ -11,7 +11,8 @@ namespace xrpl { -/** AMMVote implements AMM vote Transactor. +/** + * AMMVote implements AMM vote Transactor. * This transactor allows for the TradingFee of the AMM instance be a votable * parameter. Any account (LP) that holds the corresponding LPTokens can cast * a vote using the new AMMVote transaction. VoteSlots array in ltAMM object diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 8f6700037d..7004dd57c1 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -22,7 +22,8 @@ namespace xrpl { class Sandbox; -/** AMMWithdraw implements AMM withdraw Transactor. +/** + * AMMWithdraw implements AMM withdraw Transactor. * The withdraw transaction is used to remove liquidity from the AMM instance * pool, thus redeeming some share of the pools that one owns in the form * of LPTokens. If the trader withdraws proportional values of both assets @@ -96,7 +97,8 @@ public: ReadView const& view, beast::Journal const& j) override; - /** Equal-asset withdrawal (LPTokens) of some AMM instance pools + /** + * Equal-asset withdrawal (LPTokens) of some AMM instance pools * shares represented by the number of LPTokens . * The trading fee is not charged. * @param view @@ -129,7 +131,8 @@ public: XRPAmount const& priorBalance, beast::Journal const& journal); - /** Withdraw requested assets and token from AMM into LP account. + /** + * Withdraw requested assets and token from AMM into LP account. * Return new total LPToken balance and the withdrawn amounts for both * assets. * @param view @@ -173,15 +176,18 @@ public: beast::Journal const& journal); private: - /** Returns IgnoreFreeze when the withdrawer is the issuer of a pool - * asset (post-fixCleanup3_3_0), ZeroIfFrozen otherwise. */ + /** + * Returns IgnoreFreeze when the withdrawer is the issuer of a pool + * asset (post-fixCleanup3_3_0), ZeroIfFrozen otherwise. + */ [[nodiscard]] FreezeHandling issuerFreezeHandling() const; std::pair applyGuts(Sandbox& view); - /** Withdraw requested assets and token from AMM into LP account. + /** + * Withdraw requested assets and token from AMM into LP account. * Return new total LPToken balance. * @param view * @param ammSle AMM ledger entry @@ -205,7 +211,8 @@ private: STAmount const& lpTokensWithdraw, std::uint16_t tfee); - /** Equal-asset withdrawal (LPTokens) of some AMM instance pools + /** + * Equal-asset withdrawal (LPTokens) of some AMM instance pools * shares represented by the number of LPTokens . * The trading fee is not charged. * @param view @@ -230,7 +237,8 @@ private: STAmount const& lpTokensWithdraw, std::uint16_t tfee); - /** Withdraw both assets (Asset1Out, Asset2Out) with the constraints + /** + * Withdraw both assets (Asset1Out, Asset2Out) with the constraints * on the maximum amount of each asset that the trader is willing * to withdraw. The trading fee is not charged. * @param view @@ -255,7 +263,8 @@ private: STAmount const& amount2, std::uint16_t tfee); - /** Single asset withdrawal (Asset1Out) equivalent to the amount specified + /** + * Single asset withdrawal (Asset1Out) equivalent to the amount specified * in Asset1Out. The trading fee is charged. * @param view * @param ammAccount @@ -275,7 +284,8 @@ private: STAmount const& amount, std::uint16_t tfee); - /** Single asset withdrawal (Asset1Out, LPTokens) proportional + /** + * Single asset withdrawal (Asset1Out, LPTokens) proportional * to the share specified by tokens. The trading fee is charged. * @param view * @param ammAccount @@ -297,7 +307,8 @@ private: STAmount const& lpTokensWithdraw, std::uint16_t tfee); - /** Withdraw single asset (Asset1Out, EPrice) with two constraints. + /** + * Withdraw single asset (Asset1Out, EPrice) with two constraints. * The trading fee is charged. * @param view * @param ammAccount @@ -319,7 +330,9 @@ private: STAmount const& ePrice, std::uint16_t tfee); - /** Check from the flags if it's withdraw all */ + /** + * Check from the flags if it's withdraw all + */ static WithdrawAll isWithdrawAll(STTx const& tx); }; diff --git a/include/xrpl/tx/transactors/dex/OfferCreate.h b/include/xrpl/tx/transactors/dex/OfferCreate.h index a3bf7626f9..0ce34646dd 100644 --- a/include/xrpl/tx/transactors/dex/OfferCreate.h +++ b/include/xrpl/tx/transactors/dex/OfferCreate.h @@ -28,13 +28,17 @@ namespace xrpl { class PaymentSandbox; class Sandbox; -/** Transactor specialized for creating offers in the ledger. */ +/** + * Transactor specialized for creating offers in the ledger. + */ class OfferCreate : public Transactor { public: static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Custom; - /** Construct a Transactor subclass that creates an offer in the ledger. */ + /** + * Construct a Transactor subclass that creates an offer in the ledger. + */ explicit OfferCreate(ApplyContext& ctx) : Transactor(ctx) { } @@ -48,15 +52,21 @@ public: static std::uint32_t getFlagsMask(PreflightContext const& ctx); - /** Enforce constraints beyond those of the Transactor base class. */ + /** + * Enforce constraints beyond those of the Transactor base class. + */ static NotTEC preflight(PreflightContext const& ctx); - /** Enforce constraints beyond those of the Transactor base class. */ + /** + * Enforce constraints beyond those of the Transactor base class. + */ static TER preclaim(PreclaimContext const& ctx); - /** Precondition: fee collection is likely. Attempt to create the offer. */ + /** + * Precondition: fee collection is likely. Attempt to create the offer. + */ TER doApply() override; diff --git a/include/xrpl/tx/transactors/lending/LoanManage.h b/include/xrpl/tx/transactors/lending/LoanManage.h index c8a5584131..e641e03dd1 100644 --- a/include/xrpl/tx/transactors/lending/LoanManage.h +++ b/include/xrpl/tx/transactors/lending/LoanManage.h @@ -36,7 +36,8 @@ public: static TER preclaim(PreclaimContext const& ctx); - /** Helper function that might be needed by other transactors + /** + * Helper function that might be needed by other transactors */ static TER defaultLoan( @@ -47,7 +48,8 @@ public: Asset const& vaultAsset, beast::Journal j); - /** Helper function that might be needed by other transactors + /** + * Helper function that might be needed by other transactors */ static TER impairLoan( @@ -57,7 +59,8 @@ public: Asset const& vaultAsset, beast::Journal j); - /** Helper function that might be needed by other transactors + /** + * Helper function that might be needed by other transactors */ [[nodiscard]] static TER unimpairLoan( diff --git a/include/xrpl/tx/transactors/oracle/OracleDelete.h b/include/xrpl/tx/transactors/oracle/OracleDelete.h index f9e9230527..d44065d7bb 100644 --- a/include/xrpl/tx/transactors/oracle/OracleDelete.h +++ b/include/xrpl/tx/transactors/oracle/OracleDelete.h @@ -14,13 +14,13 @@ namespace xrpl { /** - Price Oracle is a system that acts as a bridge between - a blockchain network and the external world, providing off-chain price data - to decentralized applications (dApps) on the blockchain. This implementation - conforms to the requirements specified in the XLS-47d. - - The OracleDelete transactor implements the deletion of Oracle objects. -*/ + * Price Oracle is a system that acts as a bridge between + * a blockchain network and the external world, providing off-chain price data + * to decentralized applications (dApps) on the blockchain. This implementation + * conforms to the requirements specified in the XLS-47d. + * + * The OracleDelete transactor implements the deletion of Oracle objects. + */ class OracleDelete : public Transactor { diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h b/include/xrpl/tx/transactors/oracle/OracleSet.h index e95970923b..b6aed3f4fc 100644 --- a/include/xrpl/tx/transactors/oracle/OracleSet.h +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h @@ -12,13 +12,13 @@ namespace xrpl { /** - Price Oracle is a system that acts as a bridge between - a blockchain network and the external world, providing off-chain price data - to decentralized applications (dApps) on the blockchain. This implementation - conforms to the requirements specified in the XLS-47d. - - The OracleSet transactor implements creating or updating Oracle objects. -*/ + * Price Oracle is a system that acts as a bridge between + * a blockchain network and the external world, providing off-chain price data + * to decentralized applications (dApps) on the blockchain. This implementation + * conforms to the requirements specified in the XLS-47d. + * + * The OracleSet transactor implements creating or updating Oracle objects. + */ class OracleSet : public Transactor { diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h index 5a07262a3b..6bd68a0142 100644 --- a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h @@ -26,7 +26,9 @@ public: static TER preclaim(PreclaimContext const& ctx); - /** Attempt to delete the Permissioned Domain. */ + /** + * Attempt to delete the Permissioned Domain. + */ TER doApply() override; diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h index 38de800284..cb5d341c50 100644 --- a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h @@ -29,7 +29,9 @@ public: static TER preclaim(PreclaimContext const& ctx); - /** Attempt to create the Permissioned Domain. */ + /** + * Attempt to create the Permissioned Domain. + */ TER doApply() override; diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h new file mode 100644 index 0000000000..3310c995ae --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class SponsorshipSet : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit SponsorshipSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h b/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h new file mode 100644 index 0000000000..cf0022ab72 --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class SponsorshipTransfer : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit SponsorshipTransfer(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/system/Batch.h b/include/xrpl/tx/transactors/system/Batch.h index 6540005877..ceaa42e8e0 100644 --- a/include/xrpl/tx/transactors/system/Batch.h +++ b/include/xrpl/tx/transactors/system/Batch.h @@ -12,6 +12,7 @@ #include #include +#include namespace xrpl { @@ -39,6 +40,9 @@ public: static NotTEC checkSign(PreclaimContext const& ctx); + static TER + preclaim(PreclaimContext const& ctx); + TER doApply() override; @@ -76,6 +80,10 @@ private: // only be reached through Batch::checkSign. static NotTEC checkBatchSign(PreclaimContext const& ctx); + + // nullopt on overflow or oversized signer arrays. + static std::optional + calculateBaseFeeImpl(ReadView const& view, STTx const& tx); }; } // namespace xrpl diff --git a/include/xrpl/tx/transactors/system/TicketCreate.h b/include/xrpl/tx/transactors/system/TicketCreate.h index 2a1036c732..f249fd7db2 100644 --- a/include/xrpl/tx/transactors/system/TicketCreate.h +++ b/include/xrpl/tx/transactors/system/TicketCreate.h @@ -57,15 +57,21 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); - /** Enforce constraints beyond those of the Transactor base class. */ + /** + * Enforce constraints beyond those of the Transactor base class. + */ static NotTEC preflight(PreflightContext const& ctx); - /** Enforce constraints beyond those of the Transactor base class. */ + /** + * Enforce constraints beyond those of the Transactor base class. + */ static TER preclaim(PreclaimContext const& ctx); - /** Precondition: fee collection is likely. Attempt to create ticket(s). */ + /** + * Precondition: fee collection is likely. Attempt to create ticket(s). + */ TER doApply() override; diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index 045911c7ae..5d35f65f44 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -76,7 +76,7 @@ public: beast::Journal const& j) override; static std::expected - create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args); + create(ApplyViewContext ctx, beast::Journal journal, MPTCreateArgs const& args); }; } // namespace xrpl diff --git a/nix/ci-env.nix b/nix/ci-env.nix index f823f71de0..9b754af97d 100644 --- a/nix/ci-env.nix +++ b/nix/ci-env.nix @@ -4,14 +4,16 @@ ... }: let - inherit (import ./packages.nix { inherit pkgs; }) commonPackages; - inherit (pkgs) lib; + inherit (import ./packages.nix { inherit pkgs; }) + commonPackages + gccPackage + llvmPackages + llvmVersion + ; - # Underlying compiler toolchains to wrap. Bump these in one place to - # roll the whole environment forward. - customGccPackage = pkgs.gcc15; - customLlvmPackages = pkgs.llvmPackages_22; - customClangMajor = lib.versions.major (lib.getVersion customLlvmPackages.clang-unwrapped); + # Underlying compiler toolchains to wrap (versions pinned in packages.nix). + customGccPackage = gccPackage; + customLlvmPackages = llvmPackages; # binutils wrapped to emit binaries that reference the custom glibc # (dynamic linker path, library search path, RPATH). @@ -90,7 +92,7 @@ let extraBuildCommands = '' rsrc="$out/resource-root" mkdir "$rsrc" - ln -s "${customLlvmPackages.clang-unwrapped.lib}/lib/clang/${customClangMajor}/include" "$rsrc/include" + ln -s "${customLlvmPackages.clang-unwrapped.lib}/lib/clang/${toString llvmVersion}/include" "$rsrc/include" ln -s "${customCompilerRt.out}/lib" "$rsrc/lib" ln -s "${customCompilerRt.out}/share" "$rsrc/share" || true echo "-resource-dir=$rsrc" >> $out/nix-support/cc-cflags diff --git a/nix/devshell.nix b/nix/devshell.nix index 1bd7ea4c0c..34f173ef08 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -1,127 +1,57 @@ { pkgs, ... }: let - inherit (import ./packages.nix { inherit pkgs; }) commonPackages; + inherit (import ./packages.nix { inherit pkgs; }) + commonPackages + gccVersion + llvmPackages + ; - # Supported compiler versions - gccVersion = pkgs.lib.range 13 15; - clangVersions = pkgs.lib.range 18 21; + # Plain nixpkgs stdenvs — no custom glibc, unlike ci-env.nix. + gccStdenv = pkgs."gcc${toString gccVersion}Stdenv"; + clangStdenv = llvmPackages.stdenv; - 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 + # compilerName is the command used to print the version, or null for none. makeShell = { - compiler ? defaultCompiler, - version ? ( - if compiler == "gcc" then - defaultGccVersion - else if compiler == "clang" then - defaultClangVersion - else - null - ), + stdenv, + compilerName, }: 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 + if compilerName == null then + ''echo "No compiler specified - using system compiler"'' + else '' 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 - ); - + (pkgs.mkShell.override { inherit stdenv; }) { + packages = commonPackages; + shellHook = '' + echo "Welcome to xrpld development shell"; + ${compilerVersion} + ''; + }; in -gccShells -// clangShells -// { - # Default shells - default = makeShell { }; - gcc = makeShell { compiler = "gcc"; }; - clang = makeShell { compiler = "clang"; }; +rec { + # macOS: Nix Clang. Linux: Nix GCC. + default = if pkgs.stdenv.isDarwin then clang else gcc; - # No compiler - no-compiler = makeShell { compiler = "none"; }; - apple-clang = makeShell { compiler = "apple-clang"; }; + gcc = makeShell { + stdenv = gccStdenv; + compilerName = "gcc"; + }; + + clang = makeShell { + stdenv = clangStdenv; + compilerName = "clang"; + }; + + # Nix provides no compiler; use the one from your system (e.g. Apple Clang). + no-compiler = makeShell { + stdenv = pkgs.stdenvNoCC; + compilerName = null; + }; + apple-clang = no-compiler; } diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index 6d8980f897..7222cc8fa8 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -75,9 +75,13 @@ COPY bin/check-tools.sh /tmp/check-tools.sh RUN /tmp/check-tools.sh # Sanity-check that the g++/clang++ are able to build binaries, including sanitizer-instrumented ones. -COPY nix/docker/test_files/cpp_sources/ /tmp/cpp_sources/ -COPY nix/docker/test_files/compile-cpp-sources.sh /tmp/compile-cpp-sources.sh -RUN /tmp/compile-cpp-sources.sh /tmp/cpp_sources /tmp/bins +COPY nix/docker/test_files/cpp/ /tmp/test_files/cpp/ +RUN /tmp/test_files/cpp/compile-sources.sh /tmp/test_files/cpp/sources /tmp/cpp-bins + +# Sanity-check that rustc is able to build binaries, including ones that rely on +# the runtime overflow check. +COPY nix/docker/test_files/rust/ /tmp/test_files/rust/ +RUN /tmp/test_files/rust/compile-sources.sh /tmp/test_files/rust/sources /tmp/rust-bins # Tester: start from a clean BASE_IMAGE, install sanitizer runtime libraries, # and run the compiled test binaries to verify they execute correctly. @@ -94,15 +98,18 @@ SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] # Sanity-check that the built binaries run correctly in the vanilla base image, with the necessary sanitizer runtime libraries installed. COPY bin/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh -COPY nix/docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh -COPY --from=final /tmp/bins /tmp/bins +COPY nix/docker/test_files/cpp/run-binaries.sh /tmp/test_files/cpp/run-binaries.sh +COPY nix/docker/test_files/rust/run-binaries.sh /tmp/test_files/rust/run-binaries.sh +COPY --from=final /tmp/cpp-bins /tmp/cpp-bins +COPY --from=final /tmp/rust-bins /tmp/rust-bins RUN < as for name in +# {hello,panic,overflow}. + +set -eo pipefail + +bins_dir="${1:?usage: $0 }" + +failed_binaries=() + +# Run a binary and verify its exit code and output. +# Usage: run +function run() { + local binary="${1}" + local expected_output="${2}" + local expected_rc="${3}" + + local out_file + out_file="$(mktemp)" + + echo "=== Run ${binary} ===" + set +e + "${binary}" >"${out_file}" 2>&1 + local rc=$? + set -e + + cat "${out_file}" + + local failed=0 + if [ "${expected_rc}" = "nonzero" ]; then + if [ "${rc}" -eq 0 ]; then + echo "ERROR: expected non-zero exit code from ${binary}, got ${rc}" >&2 + failed=1 + fi + elif [ "${rc}" -ne "${expected_rc}" ]; then + echo "ERROR: expected exit code ${expected_rc} from ${binary}, got ${rc}" >&2 + failed=1 + fi + + if ! grep -q "${expected_output}" "${out_file}"; then + echo "ERROR: expected '${expected_output}' from ${binary}" >&2 + failed=1 + fi + + if [ "${failed}" -eq 0 ]; then + echo "OK: '${expected_output}' detected" + else + failed_binaries+=("${binary}") + fi +} + +declare -A expect=( + [hello]="Hello from main thread" + [panic]="explicit panic from test" + [overflow]="attempt to add with overflow" +) + +for name in hello panic overflow; do + binary="${bins_dir}/${name}" + + if [ "${name}" = "hello" ]; then + expected_rc=0 + else + expected_rc=nonzero + fi + run "${binary}" "${expect[$name]}" "${expected_rc}" +done + +if [ "${#failed_binaries[@]}" -gt 0 ]; then + echo "ERROR: the following binaries failed:" >&2 + printf ' %s\n' "${failed_binaries[@]}" >&2 + exit 1 +fi diff --git a/nix/docker/test_files/rust/sources/hello.rs b/nix/docker/test_files/rust/sources/hello.rs new file mode 100644 index 0000000000..78e32c17f3 --- /dev/null +++ b/nix/docker/test_files/rust/sources/hello.rs @@ -0,0 +1,16 @@ +use std::thread; + +fn main() { + const NUM_THREADS: usize = 10; + let mut handles = Vec::with_capacity(NUM_THREADS); + for id in 0..NUM_THREADS { + handles.push(thread::spawn(move || { + println!("Hello from thread {id}"); + })); + } + for handle in handles { + handle.join().expect("worker thread panicked"); + } + + println!("Hello from main thread"); +} diff --git a/nix/docker/test_files/rust/sources/overflow.rs b/nix/docker/test_files/rust/sources/overflow.rs new file mode 100644 index 0000000000..2a4a54472c --- /dev/null +++ b/nix/docker/test_files/rust/sources/overflow.rs @@ -0,0 +1,13 @@ +use std::hint::black_box; + +// Rust analogue of the C++ UBSan check: with overflow checks enabled the +// compiler inserts a runtime check that panics on signed integer overflow. +// `black_box` keeps the operands opaque so the addition is evaluated at +// runtime rather than being rejected by the compile-time overflow lint. +fn main() { + let max = black_box(i32::MAX); + let one = black_box(1); + println!("Current max: {max}"); + let overflowed = max + one; + println!("Overflowed result: {overflowed}"); +} diff --git a/nix/docker/test_files/rust/sources/panic.rs b/nix/docker/test_files/rust/sources/panic.rs new file mode 100644 index 0000000000..38779ff515 --- /dev/null +++ b/nix/docker/test_files/rust/sources/panic.rs @@ -0,0 +1,5 @@ +fn main() { + // Verify the panic runtime works: a panic must print its message to stderr + // and exit with a non-zero status (Rust's default panic exit code is 101). + panic!("explicit panic from test"); +} diff --git a/nix/packages.nix b/nix/packages.nix index bcadfe7456..41d7e97328 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -1,15 +1,33 @@ { pkgs }: let + # Compiler versions used across the dev shell and the CI environment. + gccVersion = 15; + llvmVersion = 22; + + gccPackage = pkgs."gcc${toString gccVersion}"; + llvmPackages = pkgs."llvmPackages_${toString llvmVersion}"; + + # Bound explicitly so it tracks llvmPackages above, not the `with pkgs` default. + clangTools = llvmPackages.clang-tools; + # In LLVM 22, run-clang-tidy.py moved from share/clang/ to bin/, so nixpkgs # clang-tools no longer links it. Wrap it manually. runClangTidy = pkgs.writeShellScriptBin "run-clang-tidy" '' - exec ${pkgs.python3}/bin/python3 ${pkgs.llvmPackages_22.clang-unwrapped}/bin/run-clang-tidy "$@" + exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@" ''; in { + inherit + gccVersion + llvmVersion + gccPackage + llvmPackages + ; + commonPackages = with pkgs; [ ccache clangbuildanalyzer + clangTools cmake conan curlMinimal # needed for codecov/codecov-action @@ -23,7 +41,6 @@ in gnumake gnupg # needed for signing commits & codecov/codecov-action graphviz - llvmPackages_22.clang-tools less # needed for git diff mold nettools # provides netstat, used to debug failures in CI @@ -45,5 +62,15 @@ in runClangTidy vim zip + # Rust packages + cargo + cargo-audit + cargo-llvm-cov + cargo-nextest + clippy + corrosion + rust-analyzer + rustc + rustfmt ]; } diff --git a/sanitizers/suppressions/sanitizer-ignorelist.txt b/sanitizers/suppressions/sanitizer-ignorelist.txt index dc9a31f8e4..ffd9b4103a 100644 --- a/sanitizers/suppressions/sanitizer-ignorelist.txt +++ b/sanitizers/suppressions/sanitizer-ignorelist.txt @@ -29,7 +29,7 @@ src:test/beast/beast_PropertyStream_test.cpp src:src/test/app/Invariants_test.cpp # ASan false positive: stack-use-after-scope in ErrorCodes.h inline functions. -# When Clang inlines the StaticString overloads (e.g. invalid_field_error(StaticString)), +# When Clang inlines the StaticString overloads (e.g. invalidFieldError(StaticString)), # ASan scope-poisons the temporary std::string before the inlined callee finishes reading # through the const ref. This corrupts the coroutine stack and crashes the Simulate test. # See asan.supp comments for full explanation and planned fix. diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 4d7a821040..1f2c41809a 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #ifdef _MSC_VER @@ -31,74 +30,103 @@ namespace xrpl { thread_local Number::RoundingMode Number::mode = Number::RoundingMode::ToNearest; thread_local std::reference_wrapper Number::kRange = - MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large); + MantissaRange::Access::mantissaRange(MantissaRange::MantissaScale::Large330); -std::set const& -MantissaRange::getAllScales() +std::string +to_string(MantissaRange::MantissaScale const& scale) { - static std::set const kScales = { - MantissaRange::MantissaScale::Small, - MantissaRange::MantissaScale::LargeLegacy, - MantissaRange::MantissaScale::Large, - }; - return kScales; + switch (scale) + { + case MantissaRange::MantissaScale::Small: + return "Small"; + case MantissaRange::MantissaScale::LargeLegacy: + return "LargeLegacy"; + case MantissaRange::MantissaScale::Large320: + return "Large320"; + case MantissaRange::MantissaScale::Large330: + return "Large330"; + default: + throw std::runtime_error("Bad scale"); // LCOV_EXCL_LINE + } } -std::unordered_map const& -MantissaRange::getRanges() +std::string +to_string(Number::RoundingMode const& round) { - static auto const kMap = []() { - std::unordered_map map; - for (auto const scale : getAllScales()) - { - map.emplace(scale, scale); - } - - // Use these constexpr declarations to do static_asserts to verify the MantissaRanges are - // created correctly, but nothing else. - { - [[maybe_unused]] - constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Small}; - static_assert(isPowerOfTen(kRange.min)); - static_assert(kRange.min == 1'000'000'000'000'000LL); - static_assert(kRange.max == 9'999'999'999'999'999LL); - static_assert(kRange.log == 15); - static_assert(kRange.min < Number::kMaxRep); - static_assert(kRange.max < Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); - } - { - [[maybe_unused]] - constexpr static MantissaRange kRange{MantissaRange::MantissaScale::LargeLegacy}; - static_assert(isPowerOfTen(kRange.min)); - static_assert(kRange.min == 1'000'000'000'000'000'000ULL); - static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL)); - static_assert(kRange.log == 18); - static_assert(kRange.min < Number::kMaxRep); - static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); - } - { - [[maybe_unused]] - constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large}; - static_assert(isPowerOfTen(kRange.min)); - static_assert(kRange.min == 1'000'000'000'000'000'000ULL); - static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL)); - static_assert(kRange.log == 18); - static_assert(kRange.min < Number::kMaxRep); - static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); - } - return map; - }(); - - return kMap; + switch (round) + { + case Number::RoundingMode::ToNearest: + return "ToNearest"; + case Number::RoundingMode::TowardsZero: + return "TowardsZero"; + case Number::RoundingMode::Downward: + return "Downward"; + case Number::RoundingMode::Upward: + return "Upward"; + default: + throw std::runtime_error("Bad rounding mode"); // LCOV_EXCL_LINE + } } -MantissaRange const& -MantissaRange::getMantissaRange(MantissaScale scale) +constexpr MantissaRange const& +MantissaRange::Access::mantissaRange(MantissaScale scale) { - return getRanges().at(scale); + static constexpr MantissaRange kSmall{MantissaScale::Small}; + static constexpr MantissaRange kLegacy{MantissaScale::LargeLegacy}; + static constexpr MantissaRange kLarge320{MantissaScale::Large320}; + static constexpr MantissaRange kLarge330{MantissaScale::Large330}; + + switch (scale) + { + case MantissaScale::Small: + return kSmall; + case MantissaScale::LargeLegacy: + return kLegacy; + case MantissaScale::Large320: + return kLarge320; + case MantissaScale::Large330: + return kLarge330; + } + throw std::logic_error("Unknown mantissa scale"); + + // static_asserts are checked at compile time, so it doesn't matter where in the function they + // are located. For readability of the main body, put them after it. + + // Small + static_assert(isPowerOfTen(kSmall.min)); + static_assert(kSmall.min == 1'000'000'000'000'000LL); + static_assert(kSmall.max == 9'999'999'999'999'999LL); + static_assert(kSmall.log == 15); + static_assert(kSmall.min < Number::kMaxRep); + static_assert(kSmall.max < Number::kMaxRep); + static_assert(kSmall.cuspRoundingFix == CuspRoundingFix::Disabled); + + // LargeLegacy + static_assert(isPowerOfTen(kLegacy.min)); + static_assert(kLegacy.min == 1'000'000'000'000'000'000ULL); + static_assert(kLegacy.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(kLegacy.log == 18); + static_assert(kLegacy.min < Number::kMaxRep); + static_assert(kLegacy.max > Number::kMaxRep); + static_assert(kLegacy.cuspRoundingFix == CuspRoundingFix::Disabled); + + // Large320 + static_assert(isPowerOfTen(kLarge320.min)); + static_assert(kLarge320.min == 1'000'000'000'000'000'000ULL); + static_assert(kLarge320.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(kLarge320.log == 18); + static_assert(kLarge320.min < Number::kMaxRep); + static_assert(kLarge320.max > Number::kMaxRep); + static_assert(kLarge320.cuspRoundingFix == CuspRoundingFix::Enabled320); + + // Large330 + static_assert(isPowerOfTen(kLarge330.min)); + static_assert(kLarge330.min == 1'000'000'000'000'000'000ULL); + static_assert(kLarge330.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(kLarge330.log == 18); + static_assert(kLarge330.min < Number::kMaxRep); + static_assert(kLarge330.max > Number::kMaxRep); + static_assert(kLarge330.cuspRoundingFix == CuspRoundingFix::Enabled330); } Number::RoundingMode @@ -124,7 +152,7 @@ Number::setMantissaScale(MantissaRange::MantissaScale scale) { if (!MantissaRange::getAllScales().contains(scale)) logicError("Unknown mantissa scale"); - kRange = MantissaRange::getMantissaRange(scale); + kRange = MantissaRange::Access::mantissaRange(scale); } // Optimization equivalent to: @@ -155,15 +183,39 @@ divu10(uint128_t& u) return r; } -// Guard - -// The Guard class is used to temporarily add extra digits of -// precision to an operation. This enables the final result -// to be correctly rounded to the internal precision of Number. - template concept UnsignedMantissa = std::is_unsigned_v || std::is_same_v; +/** + * Guard + * + * The Guard class is used to temporarily add extra digits of + * precision to an operation. This enables the final result + * to be correctly rounded to the internal precision of Number. + * + * At its core, the Guard really only needs three pieces of information to determine how to round: + * 1. The rounding mode + * 2. The last digit dropped from the mantissa (i.e. the first digit after the decimal point). + * (first byte of digits_) + * 3. Whether any other non-zero digits were dropped from the mantissa. (remaining bytes of digits_ + * and xbit_) + * + * Upward and Downward rounding modes round the unsigned mantissa toward or away from zero + * depending on whether the sign is negative (sbit_). For positive values, Upward is away, and + * Downward is toward. For negative values, that's reversed. For simplicity, I'm going to describe + * the logic using "TowardZero" and "AwayFromZero". + * + * TowardZero is the easiest rounding mode. It always rounds down. digits_ and xbit_ are + * irrelevant. + * AwayFromZero is almost as simple. If both "digits_" and "xbit_" are zero (0), it rounds down. + * Else it rounds up. + * ToNearest is only a little more complicated. If the last dropped digit is < 5, then round + * down. If it is > 5, round up. If it is exactly 5, and there are _any_ other digits (the + * remainder of "digits_" or "xbit_"), round up, else round to even. + * + * The current implementation stores 16 digits in "digits_" so that digits can be "pop"ped back + * out if needed during subtraction (negative addition) operations. + */ class Number::Guard { std::uint64_t digits_{0}; // 16 decimal guard digits @@ -171,7 +223,21 @@ class Number::Guard std::uint8_t sbit_ : 1 {0}; // the sign of the guard digits public: - explicit Guard() = default; + internalrep const minMantissa; + internalrep const maxMantissa; + MantissaRange::CuspRoundingFix const cuspRoundingFix; + + explicit Guard( + internalrep const& minMantissa, + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFix) + : minMantissa(minMantissa), maxMantissa(maxMantissa), cuspRoundingFix(cuspRoundingFix) + { + } + + explicit Guard(MantissaRange const& range) : Guard(range.min, range.max, range.cuspRoundingFix) + { + } // set & test the sign bit void @@ -194,52 +260,70 @@ public: unsigned pop() noexcept; - /** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in + // if true, there are no digits in the guard, including dropped digits (xbit_) + [[nodiscard]] bool + empty() const noexcept; + + /** + * Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in * this Guard. * * Substitute for: - push(mantissa % 10); - mantissa /= 10; - ++exponent; + * push(mantissa % 10); + * mantissa /= 10; + * ++exponent; */ template void doDropDigit(T& mantissa, int& exponent) noexcept; - // Indicate round direction: 1 is up, -1 is down, 0 is even - // This enables the client to round towards nearest, and on - // tie, round towards even. - [[nodiscard]] int - round() const noexcept; + // Modify the result to the correctly rounded value + template + void + doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); // Modify the result to the correctly rounded value template void - doRoundUp( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa, - internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, - std::string location); - - // Modify the result to the correctly rounded value - template - void - doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + doRoundDown(bool& negative, T& mantissa, int& exponent) const; // Modify the result to the correctly rounded value void doRound(rep& drops, std::string location) const; private: + template + void + pushOverflow(T mantissa); + + enum class Round { + // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or + // higher. + Exact = -2, + // Round down. Since we use integer math, that usually means no change is needed. + // Exceptions are for when the result is between kMaxRep and kMaxRepUp (round to kMaxRep), + // or after subtraction where _any_ remainder will modify the result. The latter is what + // distinguishes Exact from Down. + Down = -1, + // The result was exactly half-way between two integers. This will round to even. + Even = 0, + // Round up. Always adds 1 (or subtracts 1 in some cases if cuspRoundingFix is not + // Enabled330) + Up = 1, + }; + + // Indicate round direction. See Round enum above. + // This enables the client to round towards nearest, and on + // tie, round towards even. + [[nodiscard]] Round + round() const noexcept; + void doPush(unsigned d) noexcept; template void - bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + bringIntoRange(bool& negative, T& mantissa, int& exponent) const; }; inline void @@ -269,6 +353,7 @@ Number::Guard::isNegative() const noexcept inline void Number::Guard::doPush(unsigned d) noexcept { + XRPL_ASSERT(d < 10, "xrpl::Number::Guard::doPush : valid digit"); xbit_ = xbit_ || ((digits_ & 0x0000'0000'0000'000F) != 0); digits_ >>= 4; digits_ |= (d & 0x0000'0000'0000'000FULL) << 60; @@ -289,6 +374,12 @@ Number::Guard::pop() noexcept return d; } +inline bool +Number::Guard::empty() const noexcept +{ + return digits_ == 0 && !xbit_; +} + template void Number::Guard::doDropDigit(T& mantissa, int& exponent) noexcept @@ -310,64 +401,131 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce ++exponent; } +template +void +Number::Guard::pushOverflow(T mantissa) +{ + XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::pushOverflow : valid mantissa"); + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa >= kMaxRep && + mantissa < kMaxRepUp) + { + // Special case rounding rules for the values in the range [kMaxRep, kMaxRepUp). + + auto constexpr spread = kMaxRepUp - kMaxRep; + static_assert(spread == 3); + + // Round in two steps. + + // The first step uses the digits _already_ in the Guard to possibly round the mantissa up. + // Ultimately, the purpose of this step is to capture rounding where the stored digits would + // change the decision without those digits. (e.g. From just _below_ the midpoint to just + // _above_ the midpoint for ToNearest, or from kMaxRep into the in-between for Upward. Make + // an exception if the final digit is 9, because it can only get larger, and we don't want + // to bump up to kMaxRepUp. + if (mantissa % 10 < 9) + { + // Intentionally use integer math to get the largest value under the midpoint. + auto constexpr kMidpoint = kMaxRep + (spread / 2); + static_assert(kMidpoint == kMaxRep + 1); + auto const r = round(); + if (r == Round::Up || (r == Round::Even && mantissa == kMidpoint)) + { + ++mantissa; + } + } + + // The second step scales the final digit of the updated mantissa proportionally, converting + // from (kMaxRep, kMaxRepUp) to (0 to 9]. It then pushes that scaled digit onto the guard as + // if it was a digit that got removed, but doesn't actually remove it. This method should be + // future-proof in case the number of mantissa bits ever changes. (Though for integer values + // of the form 2^(2^x-1), the spread will always be the same.) Effects: + // * For round to nearest + // * if the updated mantissa is below the midpoint, it'll round "down" to kMaxRep + // * if above the midpoint, it'll round "up" to kMaxRepUp + // * it can never be exactly at the midpoint, because kMaxRepUp is always even, and + // kMaxRep is always odd, so don't worry about that case. + // * For round upward, will round up to kMaxRepUp for positive values, down to kMaxRep for + // negative. + // * For round downward, does the opposite of upward. + // * For round toward zero, always rounds down to kMaxRep. + + auto const diff = mantissa - kMaxRep; + auto const digit = static_cast((diff * 10) / spread); + XRPL_ASSERT( + digit < 10u && digit != 5, "xrpl::Number::Guard::pushOverflow : valid overflow digit"); + + // Don't remove the digit from the mantissa, but add it to the guard as if it was. + push(digit); + } +} + // Returns: -// -1 if Guard is less than half -// 0 if Guard is exactly half -// 1 if Guard is greater than half -int +// Exact if Guard is _zero_, and appropriate amendments are enabled +// Down if Guard is less than half +// Even if Guard is exactly half +// Up if Guard is greater than half +Number::Guard::Round Number::Guard::round() const noexcept { - auto mode = Number::getround(); + // Local "mode" shadows and has the same value as the static thread_local "Number::mode". + // This ensures the overhead of loading the thread_local is only incurred once. + auto const mode = Number::getround(); + + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && empty()) + { + // No remainder + return Round::Exact; + } if (mode == RoundingMode::TowardsZero) - return -1; + return Round::Down; - if (mode == RoundingMode::Downward) + // Also Towards Zero + if ((mode == RoundingMode::Downward && !sbit_) || (mode == RoundingMode::Upward && sbit_)) { - if (sbit_) - { - if (digits_ > 0 || xbit_) - return 1; - } - return -1; + return Round::Down; } - if (mode == RoundingMode::Upward) + // Away from Zero. Since we checked sbit_ in the previous block, we don't need to check it + // again. + if (mode == RoundingMode::Downward || mode == RoundingMode::Upward) { - if (sbit_) - return -1; - if (digits_ > 0 || xbit_) - return 1; - return -1; + if (empty()) + return Round::Down; + return Round::Up; } + XRPL_ASSERT( + mode == RoundingMode::ToNearest, "xrpl::Number::Guard::Round : fallthrough to ToNearest"); // assume round to nearest if mode is not one of the predefined values if (digits_ > 0x5000'0000'0000'0000) - return 1; + return Round::Up; if (digits_ < 0x5000'0000'0000'0000) - return -1; + return Round::Down; if (xbit_) - return 1; - return 0; + return Round::Up; + return Round::Even; } template void -Number::Guard::bringIntoRange( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa) +Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) const { // Bring mantissa back into the minMantissa / maxMantissa range AFTER - // rounding - if (mantissa < minMantissa) + // rounding. + if (mantissa < minMantissa && + (cuspRoundingFix < MantissaRange::CuspRoundingFix::Enabled330 || mantissa != 0)) { mantissa *= 10; --exponent; } - if (exponent < kMinExponent) + // mantissa should never be 0, but if it _is_ assert, but fall back to making the result kZero. + if (exponent < kMinExponent || + (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0)) { + // Engineers: If you hit this assert, you probably did something wrong in the operation + // leading up to the rounding work. + XRPL_ASSERT(mantissa != 0, "xrpl::Number::Guard::bringIntoRange : valid mantissa"); static constexpr Number kZero = Number{}; negative = kZero.negative_; @@ -378,22 +536,17 @@ Number::Guard::bringIntoRange( template void -Number::Guard::doRoundUp( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa, - internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, - std::string location) +Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) { - auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + pushOverflow(mantissa); + + auto const r = round(); + if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { - auto const safeToIncrement = [&maxMantissa](auto const& mantissa) { + auto const safeToIncrement = [this](auto const& mantissa) { return mantissa < maxMantissa && mantissa < kMaxRep; }; - if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". @@ -404,25 +557,29 @@ Number::Guard::doRoundUp( } else { - // Incrementing the mantissa will require dividing, which will require rounding. So - // _don't_ increment the mantissa. Instead, divide and round recursively. It should - // be impossible to recurse more than once, because once the mantissa is divided by - // 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 will have no - // chance of bringing it back over. - doDropDigit(mantissa, exponent); - XRPL_ASSERT_PARTS( - safeToIncrement(mantissa), - "xrpl::Number::Guard::doRoundUp", - "can't recurse more than once"); - doRoundUp( - negative, - mantissa, - exponent, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - location); - return; + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && + mantissa > kMaxRep && mantissa < kMaxRepUp) + { + // When rounding up a value in between kMaxRep, and kMaxRepUp, round to + // kMaxRepUp. Note that the decision for this rounding is dominated by the + // results of pushOverflow. + mantissa = kMaxRepUp; + } + else + { + // Incrementing the mantissa will require dividing, which will require rounding. + // So _don't_ increment the mantissa. Instead, divide and round recursively. It + // should be impossible to recurse more than once, because once the mantissa is + // divided by 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 + // will have no chance of bringing it back over. + doDropDigit(mantissa, exponent); + XRPL_ASSERT_PARTS( + safeToIncrement(mantissa), + "xrpl::Number::Guard::doRoundUp", + "can't recurse more than once"); + doRoundUp(negative, mantissa, exponent, location); + return; + } } } else @@ -440,38 +597,63 @@ Number::Guard::doRoundUp( } } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + else if ( + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && + mantissa < kMaxRepUp) + { + // When rounding down a value in between kMaxRep, and kMaxRepUp, round to kMaxRep. + // Note that the decision for this rounding is dominated by the results of pushOverflow. + mantissa = kMaxRep; + } + bringIntoRange(negative, mantissa, exponent); if (exponent > kMaxExponent) Throw(std::string(location)); } template void -Number::Guard::doRoundDown( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa) +Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) const { + // Do not pushOverflow here. + auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { - --mantissa; - if (mantissa < minMantissa) + // If there was any remainder, subtract 1 from the result. This is sufficient to get the + // best rounding. + XRPL_ASSERT( + r == Round::Exact || mantissa > maxMantissa, + "xrpl::Number::Guard::doRoundDown : mantissa is expected size"); + if (r != Round::Exact) { - mantissa *= 10; - --exponent; + --mantissa; } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + else + { + // Need to preserve the incorrect behavior until the fix amendment can be retired, + // because otherwise would risk an unplanned ledger fork. + if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) + { + --mantissa; + if (mantissa < minMantissa) + { + mantissa *= 10; + --exponent; + } + } + } + bringIntoRange(negative, mantissa, exponent); } // Modify the result to the correctly rounded value void Number::Guard::doRound(rep& drops, std::string location) const { + // Do not pushOverflow here. + auto r = round(); - if (r == 1 || (r == 0 && (drops & 1) == 1)) + if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) { if (drops >= kMaxRep) { @@ -486,6 +668,8 @@ Number::Guard::doRound(rep& drops, std::string location) const } ++drops; } + XRPL_ASSERT(drops >= 0, "xrpl::Number::Guard::doRound : positive magnitude"); + if (isNegative()) drops = -drops; } @@ -530,12 +714,14 @@ doNormalize( int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped) { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; - static constexpr auto kMaxRep = Number::kMaxRep; + auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 + ? Number::kMaxRepUp + : Number::kMaxRep; using Guard = Number::Guard; @@ -553,7 +739,7 @@ doNormalize( m *= 10; --exponent; } - Guard g; + Guard g(minMantissa, maxMantissa, cuspRoundingFix); if (negative) g.setNegative(); if (dropped) @@ -585,27 +771,20 @@ doNormalize( // 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 > kMaxRep) + if (m > repLimit) { if (exponent >= kMaxExponent) throw std::overflow_error("Number::normalize 1.5"); g.doDropDigit(m, exponent); } // Before modification, m should be within the min/max range. After - // modification, it must be less than kMaxRep. In other words, the original - // value should have been no more than kMaxRep * 10. - // (kMaxRep * 10 > maxMantissa) - XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64"); + // modification, it must be less than repLimit. In other words, the original + // value should have been no more than repLimit * 10. + // (repLimit * 10 > maxMantissa) + XRPL_ASSERT_PARTS(m <= repLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit"); mantissa = m; - g.doRoundUp( - negative, - mantissa, - exponent, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::normalize 2"); + g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2"); XRPL_ASSERT_PARTS( mantissa >= minMantissa && mantissa <= maxMantissa, "xrpl::doNormalize", @@ -620,13 +799,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -638,13 +816,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -656,16 +833,27 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); } void Number::normalize(MantissaRange const& range) { - normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled); + normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFix); +} + +void +Number::normalize(Guard const& guard) +{ + normalize( + negative_, + mantissa_, + exponent_, + guard.minMantissa, + guard.maxMantissa, + guard.cuspRoundingFix); } // Copy the number, but set a new exponent. Because the mantissa doesn't change, @@ -719,46 +907,105 @@ Number::operator+=(Number const& y) bool const yn = y.negative_; uint128_t ym = y.mantissa_; auto ye = y.exponent_; - Guard g; + Guard g(kRange); + + auto const& minMantissa = g.minMantissa; + auto const& maxMantissa = g.maxMantissa; + auto const cuspRoundingFix = g.cuspRoundingFix; + + auto const repLimit = + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep; + + // Bring the exponents of both values into agreement, so the mantissas are on the same scale + // and can be added directly together. + + auto const upperLimit = static_cast(g.minMantissa) * 1000; + // For the "adjust" lambda + // expandM / expandE: The values for which the mantissa will be expanded, and the exponent + // decreased to match. Mantissa won't be expanded beyond upperLimit. + // (37e8 == 37000e5 == 37000000e2) + // shrinkM / shrinkE: The values for which the mantissa will be shrunk, and exponent increased + // to match, if necessary. + auto const adjust = [&g, &upperLimit]( + uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) { + // Adjust up and down until the exponents match + if (g.cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330) + { + // For Enabled330, there are three steps. + // 1. First, shrink the mantissa of shrinkM/shrinkE while shrinkM ends in 0. + while (shrinkE < expandE && shrinkM % 10 == 0) + { + g.doDropDigit(shrinkM, shrinkE); + } + + // 2. Then expand the mantissa of expandM/expandE, with a limit for expandM a few orders + // of magnitude above the MantissaRange. This will leave a few extra digits for rounding + // later, but nothing excessive. + while (shrinkE < expandE && expandE > kMinExponent && expandM < upperLimit) + { + expandM *= 10; + --expandE; + } + } + + // 3. Finally, shrink the mantissa of shrinkM/shrinkE until the exponents match. Any removed + // digits will be put into the Guard. This is the only step for non-Enabled330 modes. + while (shrinkE < expandE) + { + g.doDropDigit(shrinkM, shrinkE); + } + }; + + // Shrink the mantissa and raise the exponent of the value with the lower exponent. Store any + // dropped digits in the Guard. if (xe < ye) { if (xn) g.setNegative(); - do - { - g.doDropDigit(xm, xe); - } while (xe < ye); + + adjust(ym, ye, xm, xe); } else if (xe > ye) { if (yn) g.setNegative(); - do - { - g.doDropDigit(ym, ye); - } while (xe > ye); - } - auto const& range = kRange.get(); - auto const& minMantissa = range.min; - auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + adjust(xm, xe, ym, ye); + } + else if (g.cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330) + { + // Both values have the same exponent. + // Set the sign of the Guard based on the sign of the Number with the smallest + // unsigned _mantissa_ + if ((xm < ym && xn) || (ym < xm && yn)) + g.setNegative(); + } if (xn == yn) { xm += ym; - if (xm > maxMantissa || xm > kMaxRep) + + if (g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { - g.doDropDigit(xm, xe); + // Don't do any adjustments for Enabled330. Normalize will take care of it + // Because of "adjust", the only way there can be data in the Guard is if we first grew + // the mantissa past the maxMantissa. Since we added here, it can only get bigger. + // If xm > maxMantissa, then doNormalize has all the data it needs from the last 3-4 + // digits, plus the "dropped" flag that will be passed in. + // If not, then the mantissa will only need to be padded out with 0s and won't need to + // round. + XRPL_ASSERT( + xm > maxMantissa || g.empty(), + "xrpl::Number::operator+ : rounding state expected after add"); + } + else + { + if (xm > maxMantissa || xm > repLimit) + { + g.doDropDigit(xm, xe); + } + g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } - g.doRoundUp( - xn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::addition overflow"); } else { @@ -772,19 +1019,67 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - while (xm < minMantissa && xm * 10 <= kMaxRep) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { - xm *= 10; - xm -= g.pop(); - --xe; + // Because we subtracted, xm can have any number of digits from 1 up to + // upperLimit * 10, and g can be in any state. (Note that xm can't be zero, because that + // special case was tested earlier.) + + // Grow xm/xe and pull digits out of the Guard until xm reaches upperLimit, but stop if + // the Guard empties out, because no rounding will be necessary. This will ensure that + // normalize will have enough information to make an accurate rounding decision. + // (Normalize will pad a small mantissa back into range.) Note that if any digits were + // lost (xbit_), the Guard will never be empty, so xm will grow larger than upperLimit. + while (xm < upperLimit && !g.empty()) + { + xm *= 10; + xm -= g.pop(); + --xe; + } + XRPL_ASSERT( + xm > maxMantissa || g.empty(), + "xrpl::Number::operator+ : rounding state expected after subtract"); } - g.doRoundDown(xn, xm, xe, minMantissa); + else + { + // Grow xm/xe and pull digits out of the Guard until it's back in the + // minMantissa/maxMantissa range. + while (xm < minMantissa && xm * 10 <= repLimit) + { + xm *= 10; + xm -= g.pop(); + --xe; + } + } + // Rounding down can result in decrementing xm, based on whether there is any data left in + // the Guard (depending on cuspRoundingFix). Note that if that happens, then the Guard is + // not empty. For Enabled330, that will also result in the "dropped" flag being passed to + // doNormalize, which may result in the mantissa being incremented again. It doesn't matter + // what the dropped digits are, only that they exist. This is because subtracting one + // "overcorrects", so we know there are still trailing digits to be accounted for in the + // rounding. + // + // This works because + // 1. The rounding up will be done _after_ the mantissa is brought into range. It may not + // be in range right now, and + // 2. The "dropped" flag is only ever used as a tie-breaker, specifically when rounding + // away from zero, and the dropped digits are 0, or when rounding to nearest, and + // the dropped digits represent exactly 0.5. + g.doRoundDown(xn, xm, xe); } + doNormalize( + xn, + xm, + xe, + minMantissa, + maxMantissa, + cuspRoundingFix, + cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330 && !g.empty()); negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; - normalize(range); + XRPL_ASSERT(isnormal(), "xrpl::Number::operator+= : result is normal"); return *this; } @@ -818,35 +1113,27 @@ Number::operator*=(Number const& y) auto ze = xe + ye; auto zs = xs * ys; bool zn = (zs == -1); - Guard g; + Guard g(kRange); if (zn) g.setNegative(); - auto const& range = kRange.get(); - auto const& minMantissa = range.min; - auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const& maxMantissa = g.maxMantissa; + auto const repLimit = + g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep; - while (zm > maxMantissa || zm > kMaxRep) + while (zm > maxMantissa || zm > repLimit) { g.doDropDigit(zm, ze); } xm = static_cast(zm); xe = ze; - g.doRoundUp( - zn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::multiplication overflow : exponent is " + std::to_string(xe)); + g.doRoundUp(zn, xm, xe, "Number::multiplication overflow : exponent is " + std::to_string(xe)); negative_ = zn; mantissa_ = xm; exponent_ = xe; - normalize(range); + normalize(g); return *this; } @@ -882,7 +1169,7 @@ Number::operator/=(Number const& y) auto const& range = kRange.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const cuspRoundingFix = range.cuspRoundingFix; // Division operates on two large integers (16-digit for small // mantissas, 19-digit for large) using integer math. If the values @@ -930,7 +1217,7 @@ Number::operator/=(Number const& y) // This is equivalent to if we had used an initial factor of 10^22, // a couple digits more than we actually need. // - // Stage 3: If there is still a remainder, and the CuspRoundingFix + // Stage 3: If there is still a remainder, and the cuspRoundingFix // is enabled, pass a flag indicating such to doNormalize. The Guard // in doNormalize will treat that flag as if non-zero digits had // been dropped from the mantissa when shrinking it into range. @@ -1014,14 +1301,14 @@ Number::operator/=(Number const& y) // rounding fix is enabled, flag if there is still // a remainder from stage 2. bool const useTrailingRemainder = - cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; + cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled; if (useTrailingRemainder) { dropped = partialNumerator % dm != 0; } } } - doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); + doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFix, dropped); negative_ = zp; mantissa_ = static_cast(zm); exponent_ = ze; @@ -1035,7 +1322,7 @@ operator rep() const { rep drops = mantissa(); int offset = exponent(); - Guard g; + Guard g(kRange); if (drops != 0) { if (negative_) @@ -1099,8 +1386,11 @@ to_string(Number const& amount) } std::string ret = negative ? "-" : ""; ret.append(std::to_string(mantissa)); - ret.append(1, 'e'); - ret.append(std::to_string(exponent)); + if (exponent != 0) + { + ret.append(1, 'e'); + ret.append(std::to_string(exponent)); + } return ret; } diff --git a/src/libxrpl/basics/ResolverAsio.cpp b/src/libxrpl/basics/ResolverAsio.cpp index 7e1b56f87a..25e95b7fc5 100644 --- a/src/libxrpl/basics/ResolverAsio.cpp +++ b/src/libxrpl/basics/ResolverAsio.cpp @@ -33,10 +33,11 @@ namespace xrpl { -/** Mix-in to track when all pending I/O is complete. - Derived classes must be callable with this signature: - void asyncHandlersComplete() -*/ +/** + * Mix-in to track when all pending I/O is complete. + * Derived classes must be callable with this signature: + * void asyncHandlersComplete() + */ template class AsyncObject { @@ -51,10 +52,11 @@ public: XRPL_ASSERT(pending_.load() == 0, "xrpl::AsyncObject::~AsyncObject : nothing pending"); } - /** RAII container that maintains the count of pending I/O. - Bind this into the argument list of every handler passed - to an initiating function. - */ + /** + * RAII container that maintains the count of pending I/O. + * Bind this into the argument list of every handler passed + * to an initiating function. + */ class CompletionCounter { public: diff --git a/src/libxrpl/basics/base64.cpp b/src/libxrpl/basics/base64.cpp index 541ddd0839..c980a08669 100644 --- a/src/libxrpl/basics/base64.cpp +++ b/src/libxrpl/basics/base64.cpp @@ -76,32 +76,37 @@ getInverse() return &kTab[0]; } -/// Returns max chars needed to encode a base64 string +/** + * Returns max chars needed to encode a base64 string + */ constexpr std::size_t encodedSize(std::size_t n) { return 4 * ((n + 2) / 3); } -/// Returns max bytes needed to decode a base64 string +/** + * Returns max bytes needed to decode a base64 string + */ constexpr std::size_t decodedSize(std::size_t n) { return ((n / 4) * 3) + 2; } -/** Encode a series of octets as a padded, base64 string. - - The resulting string will not be null terminated. - - @par Requires - - The memory pointed to by `out` points to valid memory - of at least `encoded_size(len)` bytes. - - @return The number of characters written to `out`. This - will exclude any null termination. -*/ +/** + * Encode a series of octets as a padded, base64 string. + * + * The resulting string will not be null terminated. + * + * @par Requires + * + * The memory pointed to by `out` points to valid memory + * of at least `encoded_size(len)` bytes. + * + * @return The number of characters written to `out`. This + * will exclude any null termination. + */ std::size_t encode(void* dest, void const* src, std::size_t len) { @@ -142,17 +147,18 @@ encode(void* dest, void const* src, std::size_t len) return out - static_cast(dest); } -/** Decode a padded base64 string into a series of octets. - - @par Requires - - The memory pointed to by `out` points to valid memory - of at least `decoded_size(len)` bytes. - - @return The number of octets written to `out`, and - the number of characters read from the input string, - expressed as a pair. -*/ +/** + * Decode a padded base64 string into a series of octets. + * + * @par Requires + * + * The memory pointed to by `out` points to valid memory + * of at least `decoded_size(len)` bytes. + * + * @return The number of octets written to `out`, and + * the number of characters read from the input string, + * expressed as a pair. + */ std::pair decode(void* dest, char const* src, std::size_t len) { diff --git a/src/libxrpl/basics/make_SSLContext.cpp b/src/libxrpl/basics/make_SSLContext.cpp index 165d36076b..3fca0c0d77 100644 --- a/src/libxrpl/basics/make_SSLContext.cpp +++ b/src/libxrpl/basics/make_SSLContext.cpp @@ -30,35 +30,37 @@ namespace xrpl { namespace openssl::detail { -/** The default strength of self-signed RSA certificates. - - Per NIST Special Publication 800-57 Part 3, 2048-bit RSA is still - considered acceptably secure. Generally, we would want to go above - and beyond such recommendations (e.g. by using 3072 or 4096 bits) - but there is a computational cost associated with that may not - be worth paying, considering that: - - - We regenerate a new ephemeral certificate and a securely generated - random private key every time the server is started; and - - There should not be any truly secure information (e.g. seeds or private - keys) that gets relayed to the server anyways over these RPCs. - - @note If you increase the number of bits you need to generate new - default DH parameters and update defaultDH accordingly. - * */ +/** + * The default strength of self-signed RSA certificates. + * + * Per NIST Special Publication 800-57 Part 3, 2048-bit RSA is still + * considered acceptably secure. Generally, we would want to go above + * and beyond such recommendations (e.g. by using 3072 or 4096 bits) + * but there is a computational cost associated with that may not + * be worth paying, considering that: + * + * - We regenerate a new ephemeral certificate and a securely generated + * random private key every time the server is started; and + * - There should not be any truly secure information (e.g. seeds or private + * keys) that gets relayed to the server anyways over these RPCs. + * + * @note If you increase the number of bits you need to generate new + * default DH parameters and update defaultDH accordingly. + */ int gDefaultRsaKeyBits = 2048; -/** The default DH parameters. - - These were generated using the OpenSSL command: `openssl dhparam 2048` - by Nik Bougalis on May, 29, 2022. - - It is safe to use this, but if you want you can generate different - parameters and put them here. There's no easy way to change this - via the config file at this time. - - @note If you increase the number of bits you need to update - defaultRSAKeyBits accordingly. +/** + * The default DH parameters. + * + * These were generated using the OpenSSL command: `openssl dhparam 2048` + * by Nik Bougalis on May, 29, 2022. + * + * It is safe to use this, but if you want you can generate different + * parameters and put them here. There's no easy way to change this + * via the config file at this time. + * + * @note If you increase the number of bits you need to update + * defaultRSAKeyBits accordingly. */ static constexpr char kDefaultDh[] = "-----BEGIN DH PARAMETERS-----\n" @@ -70,19 +72,20 @@ static constexpr char kDefaultDh[] = "9yqY3xXZID240RRcaJ25+U4lszFPqP+CEwIBAg==\n" "-----END DH PARAMETERS-----"; -/** The default list of ciphers we accept over TLS. - - Generally we include cipher suites that are part of TLS v1.2, but - we specifically exclude: - - - the DSS cipher suites (!DSS); - - cipher suites using pre-shared keys (!PSK); - - cipher suites that don't offer encryption (!eNULL); and - - cipher suites that don't offer authentication (!aNULL). - - @note Server administrators can override this default list, on either a - global or per-port basis, using the `ssl_ciphers` directive in the - config file. +/** + * The default list of ciphers we accept over TLS. + * + * Generally we include cipher suites that are part of TLS v1.2, but + * we specifically exclude: + * + * - the DSS cipher suites (!DSS); + * - cipher suites using pre-shared keys (!PSK); + * - cipher suites that don't offer encryption (!eNULL); and + * - cipher suites that don't offer authentication (!aNULL). + * + * @note Server administrators can override this default list, on either a + * global or per-port basis, using the `ssl_ciphers` directive in the + * config file. */ std::string const kDefaultCipherList = "TLSv1.2:!CBC:!DSS:!PSK:!eNULL:!aNULL"; diff --git a/src/libxrpl/core/detail/LoadMonitor.cpp b/src/libxrpl/core/detail/LoadMonitor.cpp index 95e0e7d3b9..92eff61285 100644 --- a/src/libxrpl/core/detail/LoadMonitor.cpp +++ b/src/libxrpl/core/detail/LoadMonitor.cpp @@ -101,10 +101,11 @@ LoadMonitor::addLoadSample(LoadEvent const& s) addSamples(1, latency); } -/* Add multiple samples - @param count The number of samples to add - @param latencyMS The total number of milliseconds -*/ +/** + * Add multiple samples + * @param count The number of samples to add + * @param latencyMS The total number of milliseconds + */ void LoadMonitor::addSamples(int count, std::chrono::milliseconds latency) { diff --git a/src/libxrpl/core/detail/Workers.cpp b/src/libxrpl/core/detail/Workers.cpp index 0d9c1afd26..abbbb2f25c 100644 --- a/src/libxrpl/core/detail/Workers.cpp +++ b/src/libxrpl/core/detail/Workers.cpp @@ -127,15 +127,11 @@ Workers::deleteWorkers(beast::LockFreeStack& stack) { Worker const* const worker = stack.popFront(); - if (worker != nullptr) - { - // This call blocks until the thread orderly exits - delete worker; - } - else - { + if (worker == nullptr) break; - } + + // This call blocks until the thread orderly exits + delete worker; } } diff --git a/src/libxrpl/crypto/RFC1751.cpp b/src/libxrpl/crypto/RFC1751.cpp index 41e29ee00c..4b17e1443c 100644 --- a/src/libxrpl/crypto/RFC1751.cpp +++ b/src/libxrpl/crypto/RFC1751.cpp @@ -379,14 +379,15 @@ RFC1751::etob(std::string& strData, std::vector vsHuman) return 1; } -/** Convert words separated by spaces into a 128 bit key in big-endian format. - - @return - 1 if succeeded - 0 if word not in dictionary - -1 if badly formed string - -2 if words are okay but parity is wrong. -*/ +/** + * Convert words separated by spaces into a 128 bit key in big-endian format. + * + * @return + * 1 if succeeded + * 0 if word not in dictionary + * -1 if badly formed string + * -2 if words are okay but parity is wrong. + */ int RFC1751::getKeyFromEnglish(std::string& strKey, std::string const& strHuman) { @@ -415,7 +416,8 @@ RFC1751::getKeyFromEnglish(std::string& strKey, std::string const& strHuman) return rc; } -/** Convert to human from a 128 bit key in big-endian format +/** + * Convert to human from a 128 bit key in big-endian format */ void RFC1751::getEnglishFromKey(std::string& strHuman, std::string const& strKey) diff --git a/src/libxrpl/json/Writer.cpp b/src/libxrpl/json/Writer.cpp index c5c4b3cd31..4c922a0e33 100644 --- a/src/libxrpl/json/Writer.cpp +++ b/src/libxrpl/json/Writer.cpp @@ -199,15 +199,21 @@ private: // JSON collections are either arrays, or objects. struct Collection { - /** What type of collection are we in? */ + /** + * What type of collection are we in? + */ Writer::CollectionType type = Writer::CollectionType::Array; - /** Is this the first entry in a collection? - * If false, we have to emit a , before we write the next entry. */ + /** + * Is this the first entry in a collection? + * If false, we have to emit a , before we write the next entry. + */ bool isFirst = true; #ifndef NDEBUG - /** What tags have we already seen in this collection? */ + /** + * What tags have we already seen in this collection? + */ std::set tags{}; // NOLINT(readability-redundant-member-init) #endif }; @@ -230,9 +236,8 @@ Writer::~Writer() impl_->finishAll(); } -Writer::Writer(Writer&& w) noexcept +Writer::Writer(Writer&& w) noexcept : impl_(std::move(w.impl_)) { - impl_ = std::move(w.impl_); } Writer& diff --git a/src/libxrpl/json/json_reader.cpp b/src/libxrpl/json/json_reader.cpp index a12ec7b565..f9134e6629 100644 --- a/src/libxrpl/json/json_reader.cpp +++ b/src/libxrpl/json/json_reader.cpp @@ -5,12 +5,13 @@ #include #include +#include #include #include -#include #include #include #include +#include namespace json { // Implementation of class Reader @@ -605,34 +606,17 @@ bool Reader::decodeDouble(Token& token) { double value = 0; - int const bufferSize = 32; - int count = 0; - int const length = int(token.end - token.start); - // Sanity check to avoid buffer overflow exploits. - if (length < 0) - { - return addError("Unable to parse token length", token); - } - // Avoid using a string constant for the format control string given to - // sscanf, as this can cause hard to debug crashes on OS X. See here for - // more info: - // - // http://developer.apple.com/library/mac/#DOCUMENTATION/DeveloperTools/gcc-4.0.1/gcc/Incompatibilities.html - char format[] = "%lf"; - if (length <= bufferSize) - { - Char buffer[bufferSize + 1]; - memcpy(buffer, token.start, length); - buffer[length] = 0; - count = sscanf(buffer, format, &value); - } - else - { - std::string const buffer(token.start, token.end); - count = sscanf(buffer.c_str(), format, &value); - } - if (count != 1) + auto const [ptr, ec] = std::from_chars(token.start, token.end, value); + + // Reject anything from_chars could not turn into a finite double: + // - ec != std::errc{}: no valid conversion, or an out-of-range magnitude + // (e.g. 1e400). + // - ptr != token.end: readNumber() is permissive about which characters + // it collects into a token (it will, for example, keep a '+' mid-token), + // but from_chars() will stop at the first character it cannot parse. + if (ec != std::errc{} || ptr != token.end) return addError("'" + std::string(token.start, token.end) + "' is not a number.", token); + currentValue() = value; return true; } diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index c679e30e16..e7ebb04495 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -48,6 +48,7 @@ public: if (length == kUnknown) length = (value != nullptr) ? (unsigned int)strlen(value) : 0; + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) char* newString = static_cast(malloc(length + 1)); if (value != nullptr) memcpy(newString, value, length); @@ -59,7 +60,10 @@ public: releaseStringValue(char* value) override { if (value != nullptr) + { + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) free(value); + } } }; @@ -120,7 +124,10 @@ Value::CZString::CZString(CZString const& other) Value::CZString::~CZString() { if ((cstr_ != nullptr) && index_ == static_cast(DuplicationPolicy::Duplicate)) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) valueAllocator()->releaseMemberName(const_cast(cstr_)); + } } bool @@ -167,7 +174,8 @@ Value::CZString::isStaticString() const // ////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////// -/*! \internal Default constructor initialization must be equivalent to: +/** + * @internal Default constructor initialization must be equivalent to: * memset( this, 0, sizeof(Value) ) * This optimization is used in ValueInternalMap fast allocator. */ @@ -241,6 +249,7 @@ Value::Value(std::string const& value) : type_(ValueType::String), allocated_(tr Value::Value(StaticString const& value) : type_(ValueType::String) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) value_.stringVal = const_cast(value.cStr()); } @@ -305,8 +314,7 @@ Value::~Value() case ValueType::Array: case ValueType::Object: - if (value_.mapVal != nullptr) - delete value_.mapVal; + delete value_.mapVal; break; // LCOV_EXCL_START @@ -786,7 +794,9 @@ Value::isConvertibleTo(ValueType other) const return false; // unreachable; } -/// Number of values in array or object +/** + * Number of values in array or object + */ Value::UInt Value::size() const { diff --git a/src/libxrpl/json/json_writer.cpp b/src/libxrpl/json/json_writer.cpp index c9f8cb688b..fdfa5d3cf0 100644 --- a/src/libxrpl/json/json_writer.cpp +++ b/src/libxrpl/json/json_writer.cpp @@ -4,13 +4,14 @@ #include #include -#include +#include #include #include #include #include #include #include +#include #include namespace json { @@ -76,19 +77,18 @@ valueToString(UInt value) std::string valueToString(double value) { - // Allocate a buffer that is more than large enough to store the 16 digits - // of precision requested below. + // Format with 16 significant digits. + // We need not request the alternative representation that always has a + // decimal point because JSON doesn't distinguish the concepts of reals and integers. + // A double never needs more than 32 characters in this form, + // so to_chars cannot actually run out of room here. char buffer[32]; - // Print into the buffer. We need not request the alternative representation - // that always has a decimal point because JSON doesn't distinguish the - // concepts of reals and integers. -#if defined(_MSC_VER) && defined(__STDC_SECURE_LIB__) // Use secure version with visual studio 2005 - // to avoid warning. - sprintf_s(buffer, sizeof(buffer), "%.16g", value); -#else - snprintf(buffer, sizeof(buffer), "%.16g", value); -#endif - return buffer; + auto const [ptr, ec] = + std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general, 16); + XRPL_ASSERT(ec == std::errc{}, "json::valueToString(double) : conversion fits buffer"); + if (ec != std::errc{}) + return {}; + return std::string(buffer, ptr); } std::string diff --git a/src/libxrpl/ledger/PaymentSandbox.cpp b/src/libxrpl/ledger/PaymentSandbox.cpp index 97e3e53cbf..a7a8962095 100644 --- a/src/libxrpl/ledger/PaymentSandbox.cpp +++ b/src/libxrpl/ledger/PaymentSandbox.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -170,7 +171,7 @@ DeferredCredits::issuerSelfDebitMPT( } void -DeferredCredits::ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next) +DeferredCredits::ownerCount(AccountID const& id, OwnerCounts const& cur, OwnerCounts const& next) { auto const v = std::max(cur, next); auto r = ownerCounts_.emplace(id, v); @@ -181,7 +182,7 @@ DeferredCredits::ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_ } } -std::optional +std::optional DeferredCredits::ownerCount(AccountID const& id) const { auto i = ownerCounts_.find(id); @@ -391,10 +392,10 @@ PaymentSandbox::balanceHookSelfIssueMPT(xrpl::MPTIssue const& issue, std::int64_ return STAmount{issue}; } -std::uint32_t -PaymentSandbox::ownerCountHook(AccountID const& account, std::uint32_t count) const +OwnerCounts +PaymentSandbox::ownerCountHook(AccountID const& account, OwnerCounts const& count) const { - std::uint32_t result = count; + OwnerCounts result = count; for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_) { if (auto adj = curSB->tab_.ownerCount(account)) @@ -442,8 +443,8 @@ PaymentSandbox::issuerSelfDebitHookMPT( void PaymentSandbox::adjustOwnerCountHook( AccountID const& account, - std::uint32_t cur, - std::uint32_t next) + OwnerCounts const& cur, + OwnerCounts const& next) { tab_.ownerCount(account, cur, next); } diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index ebac76a754..8116f4f641 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -431,8 +432,7 @@ canWithdraw(ReadView const& view, STTx const& tx) TER doWithdraw( - ApplyView& view, - STTx const& tx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, @@ -440,23 +440,24 @@ doWithdraw( STAmount const& amount, beast::Journal j) { + auto const dstSle = ctx.view.read(keylet::account(dstAcct)); + // Create trust line or MPToken for the receiving account if (dstAcct == senderAcct) { - if (auto const ter = addEmptyHolding(view, senderAcct, priorBalance, amount.asset(), j); + if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j); !isTesSuccess(ter) && ter != tecDUPLICATE) return ter; } else { - auto dstSle = view.read(keylet::account(dstAcct)); - if (auto err = verifyDepositPreauth(tx, view, senderAcct, dstAcct, dstSle, j)) + if (auto err = verifyDepositPreauth(ctx.tx, ctx.view, senderAcct, dstAcct, dstSle, j)) return err; } // Sanity check if (accountHolds( - view, + ctx.view, sourceAcct, amount.asset(), FreezeHandling::IgnoreFreeze, @@ -469,9 +470,18 @@ doWithdraw( // LCOV_EXCL_STOP } + // A reserve sponsor only covers tx.Account's own objects, so resolve the + // sponsor against the destination. accountSend can auto-create a holding + // for dstAcct; keying on the destination ensures a third-party destination's + // holding is never stamped with the tx's reserve sponsor. + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, dstSle); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Move the funds directly from the broker's pseudo-account to the // dstAcct - return accountSend(view, sourceAcct, dstAcct, amount, j, WaiveTransferFee::Yes); + return accountSend( + ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes); } TER diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 1c4acc7bc4..faca4ebfb6 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -41,46 +44,226 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer) return false; } +namespace { + // An owner count cannot be negative. If adjustment would cause a negative // owner count, clamp the owner count at 0. Similarly for overflow. This // adjustment allows the ownerCount to be adjusted up or down in multiple steps. // If id != std::nullopt, then do error reporting. // // Returns adjusted owner count. -static std::uint32_t +std::uint32_t confineOwnerCount( - std::uint32_t current, - std::int32_t adjustment, + std::uint32_t currentOwnerCount, + std::int32_t ownerCountAdj, std::optional const& id = std::nullopt, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) { - std::uint32_t adjusted{current + adjustment}; - if (adjustment > 0) + std::uint32_t totalOwnerCount{currentOwnerCount + ownerCountAdj}; + if (ownerCountAdj > 0) { // Overflow is well defined on unsigned - if (adjusted < current) + if (totalOwnerCount < currentOwnerCount) { + // LCOV_EXCL_START if (id) { JLOG(j.fatal()) << "Account " << *id << " owner count exceeds max!"; } - adjusted = std::numeric_limits::max(); + totalOwnerCount = std::numeric_limits::max(); + // LCOV_EXCL_STOP } } else { // Underflow is well defined on unsigned - if (adjusted > current) + if (totalOwnerCount > currentOwnerCount) { + // LCOV_EXCL_START if (id) { JLOG(j.fatal()) << "Account " << *id << " owner count set below 0!"; } - adjusted = 0; + totalOwnerCount = 0; XRPL_ASSERT(!id, "xrpl::confineOwnerCount : id is not set"); + // LCOV_EXCL_STOP } } - return adjusted; + return totalOwnerCount; +} + +// Returns the number of account reserves funded by this account: 1 for itself (0 if sponsored by +// another account) plus the count of accounts it sponsors. +std::uint32_t +accountCountImpl(SLE::const_ref sle, std::int32_t accountCountAdj, beast::Journal j) +{ + bool const isSponsored = sle->isFieldPresent(sfSponsor); + std::int64_t const sponsoringAccountCount = sle->getFieldU32(sfSponsoringAccountCount); + std::int64_t const currentAccountCount = (isSponsored ? 0 : 1) + sponsoringAccountCount; + + std::int64_t totalAccountCount{currentAccountCount + accountCountAdj}; + if (totalAccountCount > std::numeric_limits::max()) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Reserve count exceeds max!"; + totalAccountCount = std::numeric_limits::max(); + // LCOV_EXCL_STOP + } + else if (totalAccountCount < 0) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::accountCountImpl : Reserve count set below 0"); + JLOG(j.fatal()) << "Reserve count set below 0"; + totalAccountCount = 0; + // LCOV_EXCL_STOP + } + + return totalAccountCount; +} + +std::uint32_t +adjustOwnerCountImpl( + ApplyView& view, + SLE::ref sle, + SF_UINT32 const& sfield, + AccountID const& accID, + std::int32_t ownerCountAdj, + beast::Journal j) +{ + std::uint32_t const currentOwnerCount = sle->at(sfield); + std::uint32_t const totalOwnerCount = + confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j); + sle->at(sfield) = totalOwnerCount; + view.update(sle); + return totalOwnerCount; +} + +void +adjustOwnerCountSigned( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::int32_t adjustment, + beast::Journal j) +{ + if (view.rules().enabled(featureSponsor)) + { + XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle"); + if (!accountSle) + return; // LCOV_EXCL_LINE + + auto const accountID = accountSle->getAccountID(sfAccount); + bool const validType = accountSle->getType() == ltACCOUNT_ROOT; + XRPL_ASSERT(validType, "xrpl::adjustOwnerCountSigned : valid account sle type"); + if (!validType) + return; // LCOV_EXCL_LINE + + XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCountSigned : nonzero adjustment input"); + + OwnerCounts const currentOwnerCount(accountSle); + OwnerCounts totalOwnerCount(currentOwnerCount); + + if (sponsorSle) + { + bool const validSponsorType = sponsorSle->getType() == ltACCOUNT_ROOT; + XRPL_ASSERT(validSponsorType, "xrpl::adjustOwnerCountSigned : valid sponsor sle type"); + if (!validSponsorType) + return; // LCOV_EXCL_LINE + auto const sponsorID = sponsorSle->getAccountID(sfAccount); + + totalOwnerCount.sponsored = adjustOwnerCountImpl( + view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j); + + { + OwnerCounts const sponsorCurrent(sponsorSle); + OwnerCounts sponsorAdjustment(sponsorCurrent); + sponsorAdjustment.sponsoring = adjustOwnerCountImpl( + view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j); + view.adjustOwnerCountHook(sponsorID, sponsorCurrent, sponsorAdjustment); + } + + auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID)); + if (sponsorshipSle && adjustment > 0) + { + // Only decrease the pre-funded ReserveCount on Sponsorship if we assign new + // objects. Removing/reassigning ownership of the object doesn't increase + // RemainingOwnerCount back. Don't call hook because this counter is not something + // that requires reserve (like other sf...OwnerCounts do). + adjustOwnerCountImpl( + view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -adjustment, j); + } + } + + totalOwnerCount.owner = + adjustOwnerCountImpl(view, accountSle, sfOwnerCount, accountID, adjustment, j); + view.adjustOwnerCountHook(accountID, currentOwnerCount, totalOwnerCount); + } + else + { + XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle"); + if (!accountSle) + return; + // the remaining are only asserts to preserve existing behavior + XRPL_ASSERT(sponsorSle == nullptr, "xrpl::adjustOwnerCountSigned : sponsor not enabled"); + XRPL_ASSERT( + accountSle->getType() == ltACCOUNT_ROOT, + "xrpl::adjustOwnerCountSigned : valid account sle type"); + XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input"); + std::uint32_t const current{accountSle->getFieldU32(sfOwnerCount)}; + AccountID const id = (*accountSle)[sfAccount]; + std::uint32_t const adjusted = confineOwnerCount(current, adjustment, id, j); + + OwnerCounts const currentOwnerCount(accountSle); + OwnerCounts finalOwnerCount(currentOwnerCount); + finalOwnerCount.owner = adjusted; + + view.adjustOwnerCountHook(id, currentOwnerCount, finalOwnerCount); + accountSle->at(sfOwnerCount) = adjusted; + view.update(accountSle); + } +} + +} // namespace + +std::uint32_t +ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj) +{ + XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::ownerCount : sle is account root"); + + AccountID const id = sle->getAccountID(sfAccount); + std::uint32_t const currentOwnerCount = sle->at(sfOwnerCount); + std::uint32_t const sponsoredOwnerCount = sle->at(sfSponsoredOwnerCount); + std::uint32_t const sponsoringOwnerCount = sle->at(sfSponsoringOwnerCount); + + XRPL_ASSERT( + currentOwnerCount >= sponsoredOwnerCount, + "xrpl::ownerCount : OwnerCount must be greater than or equal to SponsoredOwnerCount"); + + std::int64_t deltaCount = + static_cast(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount; + + if (deltaCount > std::numeric_limits::max()) + { + // LCOV_EXCL_START + deltaCount = std::numeric_limits::max(); + JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, " + << "adjustment: " << ownerCountAdj + << ", sponsoredCount: " << sponsoredOwnerCount + << ", sponsoringOwnerCount: " << sponsoringOwnerCount; + // LCOV_EXCL_STOP + } + else if (deltaCount < std::numeric_limits::min()) + { + // LCOV_EXCL_START + deltaCount = std::numeric_limits::min(); + JLOG(j.fatal()) << "Account " << id << " delta count is below min, " + << "adjustment: " << ownerCountAdj + << ", sponsoredCount: " << sponsoredOwnerCount + << ", sponsoringCount: " << sponsoringOwnerCount; + // LCOV_EXCL_STOP + } + + return confineOwnerCount(currentOwnerCount, deltaCount); } XRPAmount @@ -91,12 +274,14 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, return beast::kZero; // Return balance minus reserve - std::uint32_t const ownerCount = - confineOwnerCount(view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); + std::uint32_t const currentOwnerCount = + confineOwnerCount(view.ownerCountHook(id, OwnerCounts(sle)).count(), ownerCountAdj); + std::uint32_t const currentAccountCount = accountCountImpl(sle, 0, j); // Pseudo-accounts have no reserve requirement - auto const reserve = - isPseudoAccount(sle) ? XRPAmount{0} : view.fees().accountReserve(ownerCount); + auto const reserve = isPseudoAccount(sle) + ? XRPAmount{0} + : view.fees().accountReserve(currentOwnerCount, currentAccountCount); auto const fullBalance = sle->getFieldAmount(sfBalance); @@ -108,7 +293,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, << " amount=" << amount.getFullText() << " fullBalance=" << fullBalance.getFullText() << " balance=" << balance.getFullText() << " reserve=" << reserve - << " ownerCount=" << ownerCount << " ownerCountAdj=" << ownerCountAdj; + << " ownerCount=" << currentOwnerCount << " ownerCountAdj=" << ownerCountAdj; return amount.xrp(); } @@ -125,19 +310,193 @@ transferRate(ReadView const& view, AccountID const& issuer) } void -adjustOwnerCount(ApplyView& view, SLE::ref sle, std::int32_t amount, beast::Journal j) +increaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j) { - if (!sle) - return; - XRPL_ASSERT(amount, "xrpl::adjustOwnerCount : nonzero amount input"); - std::uint32_t const current{sle->getFieldU32(sfOwnerCount)}; - AccountID const id = (*sle)[sfAccount]; - std::uint32_t const adjusted = confineOwnerCount(current, amount, id, j); - view.adjustOwnerCountHook(id, current, adjusted); - sle->at(sfOwnerCount) = adjusted; - view.update(sle); + XRPL_ASSERT( + count != 0 && count <= std::numeric_limits::max(), + "xrpl::increaseOwnerCount : count in signed delta range"); + if (count == 0 || count > std::numeric_limits::max()) + return; // LCOV_EXCL_LINE + + adjustOwnerCountSigned(view, accountSle, sponsorSle, static_cast(count), j); } +void +increaseOwnerCount(ApplyViewContext ctx, SLE::ref accountSle, std::uint32_t count, beast::Journal j) +{ + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accountSle); + + // The sponsor's existence is validated by checkReserve/checkSponsor before + // any owner-count mutation, so loading it here cannot fail. + XRPL_ASSERT( + sponsorExp.has_value(), "xrpl::increaseOwnerCount : sponsor validated before mutation"); + + increaseOwnerCount(ctx.view, accountSle, sponsorExp ? *sponsorExp : SLE::pointer(), count, j); +} + +void +decreaseOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::uint32_t count, + beast::Journal j) +{ + XRPL_ASSERT( + count != 0 && count <= std::numeric_limits::max(), + "xrpl::decreaseOwnerCount : count in signed delta range"); + if (count == 0 || count > std::numeric_limits::max()) + return; // LCOV_EXCL_LINE + + adjustOwnerCountSigned(view, accountSle, sponsorSle, -static_cast(count), j); +} + +void +decreaseOwnerCountForObject( + ApplyView& view, + SLE::ref accountSle, + SLE::ref objectSle, + std::uint32_t count, + beast::Journal j) +{ + XRPL_ASSERT(objectSle, "xrpl::decreaseOwnerCountForObject : valid object sle"); + if (!objectSle) + return; // LCOV_EXCL_LINE + + bool const validObjectType = objectSle->getType() != ltACCOUNT_ROOT; + XRPL_ASSERT(validObjectType, "xrpl::decreaseOwnerCountForObject : valid object sle type"); + if (!validObjectType) + return; // LCOV_EXCL_LINE + + SLE::ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle); + decreaseOwnerCount(view, accountSle, sponsorSle, count, j); +} + +void +adjustLoanBrokerOwnerCount( + ApplyView& view, + SLE::ref brokerSle, + std::int32_t delta, + beast::Journal j) +{ + XRPL_ASSERT( + brokerSle && brokerSle->getType() == ltLOAN_BROKER, + "xrpl::adjustLoanBrokerOwnerCount : valid loan broker sle"); + if (!brokerSle || brokerSle->getType() != ltLOAN_BROKER) + return; // LCOV_EXCL_LINE + + XRPL_ASSERT(delta != 0, "xrpl::adjustLoanBrokerOwnerCount : nonzero delta input"); + if (delta == 0) + return; // LCOV_EXCL_LINE + + adjustOwnerCountImpl( + view, brokerSle, sfOwnerCount, brokerSle->getAccountID(sfAccount), delta, j); +} + +XRPAmount +accountReserve(ReadView const& view, SLE::const_ref sle, beast::Journal j, Adjustment adj) +{ + XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::accountReserve : valid sle"); + + if (!view.rules().enabled(featureSponsor)) + { + XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::accountReserve : no account count delta"); + return view.fees().accountReserve(sle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1); + } + std::uint32_t const currentOwnerCount = ownerCount(sle, j, adj.ownerCountDelta); + std::uint32_t const currentAccountCount = accountCountImpl(sle, adj.accountCountDelta, j); + + return view.fees().accountReserve(currentOwnerCount, currentAccountCount); +} + +TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + SLE::const_ref sponsorSle, + Adjustment adj, + beast::Journal j, + TER insufReserveCode) +{ + // TODO: swap to assert after fixCleanup3_2_0 is retired + if (!accSle || accSle->getType() != ltACCOUNT_ROOT) + return tefINTERNAL; // LCOV_EXCL_LINE + XRPL_ASSERT( + !isTesSuccess(insufReserveCode), "xrpl::checkReserve : insufReserveCode is not tesSUCCESS"); + if (ctx.view.rules().enabled(featureSponsor)) + { + if (sponsorSle) + { + if (sponsorSle->getType() != ltACCOUNT_ROOT) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sle = ctx.view.read( + keylet::sponsorship( + sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); + + // A reserve-sponsored tx must carry a sponsor signature + // (cosigning path) and/or have a pre-existing sponsorship SLE + // (prefunded path). Absence of both is an internal invariant break. + if (isReserveSponsored(ctx.tx) && !sle && !ctx.tx.isFieldPresent(sfSponsorSignature)) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (sle) + { + auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount); + if (adj.ownerCountDelta > 0 && + ownerCountAllowed < static_cast(adj.ownerCountDelta)) + return insufReserveCode; + } + + auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance).xrp(); + XRPAmount const sponsorReserve = accountReserve(ctx.view, sponsorSle, j, adj); + + if (sponsorBalance < sponsorReserve) + return insufReserveCode; + } + else + { + XRPAmount const reserve = accountReserve(ctx.view, accSle, j, adj); + if (accBalance < reserve) + return insufReserveCode; + } + } + else + { + XRPL_ASSERT( + !sponsorSle, + "xrpl::checkReserve : featureSponsor disabled and sponsorSle not provided"); + XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::checkReserve : accountCountDelta is 0"); + auto const reserve = ctx.view.fees().accountReserve( + accSle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1); + if (accBalance < reserve) + return insufReserveCode; + } + return tesSUCCESS; +} + +TER +checkReserve( + ApplyViewContext ctx, + SLE::const_ref accSle, + XRPAmount accBalance, + Adjustment adj, + beast::Journal j) +{ + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accSle); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + return checkReserve(ctx, accSle, accBalance, *sponsorExp, adj, j); +} + +// ---------------------------------------------------- + AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) { diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index 6fc2faf03e..226ea100e9 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -97,7 +97,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j) } if (isOwner) - adjustOwnerCount(view, sleAccount, -1, j); + decreaseOwnerCountForObject(view, sleAccount, sleCredential, 1, j); return tesSUCCESS; }; diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 3c4e55a16e..6fe7328fa7 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -11,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -80,13 +81,9 @@ isAnyFrozen( return true; } - for (auto const& account : accounts) - { - if (isVaultPseudoAccountFrozen(view, account, mptIssue, depth)) - return true; - } - - return false; + return std::ranges::any_of(accounts, [&](auto const& account) { + return isVaultPseudoAccountFrozen(view, account, mptIssue, depth); + }); } Rate @@ -125,29 +122,29 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue) [[nodiscard]] TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, beast::Journal journal) { auto const& mptID = mptIssue.getMptID(); - auto const mpt = view.peek(keylet::mptokenIssuance(mptID)); + auto const mpt = ctx.view.peek(keylet::mptokenIssuance(mptID)); if (!mpt) return tefINTERNAL; // LCOV_EXCL_LINE if (mpt->isFlag(lsfMPTLocked)) return tefINTERNAL; // LCOV_EXCL_LINE - if (view.peek(keylet::mptoken(mptID, accountID))) + if (ctx.view.peek(keylet::mptoken(mptID, accountID))) return tecDUPLICATE; if (accountID == mptIssue.getIssuer()) return tesSUCCESS; - return authorizeMPToken(view, priorBalance, mptID, accountID, journal); + return authorizeMPToken(ctx, priorBalance, mptID, accountID, journal); } [[nodiscard]] TER authorizeMPToken( - ApplyView& view, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -155,7 +152,7 @@ authorizeMPToken( std::uint32_t flags, std::optional holderID) { - auto const sleAcct = view.peek(keylet::account(account)); + auto const sleAcct = ctx.view.peek(keylet::account(account)); if (!sleAcct) return tecINTERNAL; // LCOV_EXCL_LINE @@ -170,19 +167,19 @@ authorizeMPToken( if ((flags & tfMPTUnauthorize) != 0u) { auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); - auto const sleMpt = view.peek(mptokenKey); + auto const sleMpt = ctx.view.peek(mptokenKey); if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0 || - (view.rules().enabled(fixCleanup3_1_3) && + (ctx.view.rules().enabled(fixCleanup3_1_3) && (*sleMpt)[~sfLockedAmount].valueOr(0) != 0)) return tecINTERNAL; // LCOV_EXCL_LINE - if (!view.dirRemove( + if (!ctx.view.dirRemove( keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleAcct, -1, journal); + decreaseOwnerCountForObject(ctx.view, sleAcct, sleMpt, 1, journal); - view.erase(sleMpt); + ctx.view.erase(sleMpt); return tesSUCCESS; } @@ -190,47 +187,57 @@ authorizeMPToken( // - add the new mptokenKey to the owner directory // - create the MPToken object for the holder + // A reserve sponsor only covers tx.Account's own objects. + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleAcct); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + // The reserve that is required to create the MPToken. Note // that although the reserve increases with every item // an account owns, in the case of MPTokens we only // *enforce* a reserve if the user owns more than two // items. This is similar to the reserve requirements of trust lines. - std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); - XRPAmount const reserveCreate( - (uOwnerCount < 2) ? XRPAmount(beast::kZero) - : view.fees().accountReserve(uOwnerCount + 1)); - - if (priorBalance < reserveCreate) - return tecINSUFFICIENT_RESERVE; + // The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on + // the tx — the sponsor must always cover the reserve (via balance or prefunded + // budget), so this check always runs for sponsored transactions. + if (sponsorSle || ownerCount(sleAcct, journal) >= 2) + { + if (auto const ret = checkReserve( + ctx, sleAcct, priorBalance, sponsorSle, {.ownerCountDelta = 1}, journal); + !isTesSuccess(ret)) + return ret; + } // Defensive check before we attempt to create MPToken for the issuer - auto const mpt = view.read(keylet::mptokenIssuance(mptIssuanceID)); + auto const mpt = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); if (!mpt || mpt->getAccountID(sfIssuer) == account) { // LCOV_EXCL_START UNREACHABLE("xrpl::authorizeMPToken : invalid issuance or issuers token"); - if (view.rules().enabled(featureLendingProtocol)) + if (ctx.view.rules().enabled(featureLendingProtocol)) return tecINTERNAL; // LCOV_EXCL_STOP } auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); auto mptoken = std::make_shared(mptokenKey); - if (auto ter = dirLink(view, account, mptoken)) + if (auto ter = dirLink(ctx.view, account, mptoken)) return ter; // LCOV_EXCL_LINE (*mptoken)[sfAccount] = account; (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; (*mptoken)[sfFlags] = 0; - view.insert(mptoken); + ctx.view.insert(mptoken); // Update owner count. - adjustOwnerCount(view, sleAcct, 1, journal); + increaseOwnerCount(ctx.view, sleAcct, sponsorSle, 1, journal); + addSponsorToLedgerEntry(mptoken, sponsorSle); return tesSUCCESS; } - auto const sleMptIssuance = view.read(keylet::mptokenIssuance(mptIssuanceID)); + auto const sleMptIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); if (!sleMptIssuance) return tecINTERNAL; // LCOV_EXCL_LINE @@ -240,7 +247,7 @@ authorizeMPToken( if (account != (*sleMptIssuance)[sfIssuer]) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sleMpt = view.peek(keylet::mptoken(mptIssuanceID, *holderID)); + auto const sleMpt = ctx.view.peek(keylet::mptoken(mptIssuanceID, *holderID)); if (!sleMpt) return tecINTERNAL; // LCOV_EXCL_LINE @@ -263,13 +270,13 @@ authorizeMPToken( if (flagsIn != flagsOut) sleMpt->setFieldU32(sfFlags, flagsOut); - view.update(sleMpt); + ctx.view.update(sleMpt); return tesSUCCESS; } [[nodiscard]] TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal) @@ -279,7 +286,7 @@ removeEmptyHolding( // a token does exist, it will get deleted. If not, return success. bool const accountIsIssuer = accountID == mptIssue.getIssuer(); auto const& mptID = mptIssue.getMptID(); - auto const mptoken = view.peek(keylet::mptoken(mptID, accountID)); + auto const mptoken = ctx.view.peek(keylet::mptoken(mptID, accountID)); if (!mptoken) return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; // Unlike a trust line, if the account is the issuer, and the token has a @@ -287,7 +294,7 @@ removeEmptyHolding( // accounting out of balance, so fail. Since this should be impossible // anyway, I'm not going to put any effort into it. if (mptoken->at(sfMPTAmount) != 0 || - (view.rules().enabled(fixCleanup3_1_3) && (*mptoken)[~sfLockedAmount].valueOr(0) != 0)) + (ctx.view.rules().enabled(fixCleanup3_1_3) && (*mptoken)[~sfLockedAmount].valueOr(0) != 0)) return tecHAS_OBLIGATIONS; // Don't delete if the token still has confidential balances @@ -300,7 +307,7 @@ removeEmptyHolding( } return authorizeMPToken( - view, + ctx, {}, // priorBalance mptID, accountID, @@ -419,13 +426,13 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyView& view, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, // for MPToken authorization beast::Journal j) { - auto const sleIssuance = view.read(keylet::mptokenIssuance(mptIssuanceID)); + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); if (!sleIssuance) return tefINTERNAL; // LCOV_EXCL_LINE @@ -437,7 +444,7 @@ enforceMPTokenAuthorization( return tefINTERNAL; // LCOV_EXCL_LINE auto const keylet = keylet::mptoken(mptIssuanceID, account); - auto const sleToken = view.read(keylet); // NOTE: might be null + auto const sleToken = ctx.view.read(keylet); // NOTE: might be null auto const maybeDomainID = sleIssuance->at(~sfDomainID); bool expired = false; bool const authorizedByDomain = [&]() -> bool { @@ -445,7 +452,7 @@ enforceMPTokenAuthorization( if (!maybeDomainID.has_value()) return false; // LCOV_EXCL_LINE - auto const ter = verifyValidDomain(view, account, *maybeDomainID, j); + auto const ter = verifyValidDomain(ctx.view, account, *maybeDomainID, j); if (isTesSuccess(ter)) return true; if (ter == tecEXPIRED) @@ -500,7 +507,7 @@ enforceMPTokenAuthorization( maybeDomainID.has_value() && sleToken == nullptr, "xrpl::enforceMPTokenAuthorization : new MPToken for domain"); if (auto const err = authorizeMPToken( - view, + ctx, priorBalance, // priorBalance mptIssuanceID, // mptIssuanceID account, // account @@ -915,6 +922,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + SLE::ref sponsorSle, std::uint32_t const flags) { auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); @@ -931,6 +939,8 @@ createMPToken( (*mptoken)[sfFlags] = flags; (*mptoken)[sfOwnerNode] = *ownerNode; + addSponsorToLedgerEntry(mptoken, sponsorSle); + view.insert(mptoken); return tesSUCCESS; @@ -941,6 +951,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + SLE::ref sponsorSle, beast::Journal j) { if (mptIssue.getIssuer() == holder) @@ -950,7 +961,7 @@ checkCreateMPT( auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder); if (!view.exists(mptokenID)) { - if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, 0); + if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, 0); !isTesSuccess(err)) { return err; @@ -960,7 +971,8 @@ checkCreateMPT( { return tecINTERNAL; } - adjustOwnerCount(view, sleAcct, 1, j); + + increaseOwnerCount(view, sleAcct, sponsorSle, 1, j); } return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 6dca715a8c..589e49d335 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -258,7 +258,9 @@ changeTokenURI( return tesSUCCESS; } -/** Insert the token in the owner's token directory. */ +/** + * Insert the token in the owner's token directory. + */ TER insertToken(ApplyView& view, AccountID owner, STObject&& nft) { @@ -269,11 +271,7 @@ insertToken(ApplyView& view, AccountID owner, STObject&& nft) // the NFT. SLE::pointer const page = getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) { - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - 1, - beast::Journal{beast::Journal::getNullSink()}); + increaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()}); }); if (!page) @@ -351,7 +349,9 @@ mergePages(ApplyView& view, SLE::ref p1, SLE::ref p2) return true; } -/** Remove the token from the owner's token directory. */ +/** + * Remove the token from the owner's token directory. + */ TER removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID) { @@ -364,7 +364,9 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID) return removeToken(view, owner, nftokenID, page); } -/** Remove the token from the owner's token directory. */ +/** + * Remove the token from the owner's token directory. + */ TER removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, SLE::ref curr) { @@ -411,21 +413,17 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->setFieldArray(sfNFTokens, arr); view.update(curr); - int cnt = 0; + std::uint32_t cnt = 0; if (prev && mergePages(view, prev, curr)) - cnt--; + ++cnt; if (next && mergePages(view, curr, next)) - cnt--; + ++cnt; if (cnt != 0) { - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - cnt, - beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()}); } return tesSUCCESS; @@ -460,11 +458,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->makeFieldAbsent(sfPreviousPageMin); } - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1, - beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()}); view.update(curr); view.erase(prev); @@ -502,7 +496,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view.erase(curr); - int cnt = 1; + uint32_t cnt = 1; // Since we're here, try to consolidate the previous and current pages // of the page we removed (if any) into one. mergePages() _should_ @@ -519,11 +513,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view.peek(Keylet(ltNFTOKEN_PAGE, next->key())))) cnt++; - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1 * cnt, - beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()}); return tesSUCCESS; } @@ -639,8 +629,7 @@ deleteTokenOffer(ApplyView& view, SLE::ref offer) false)) return false; - adjustOwnerCount( - view, view.peek(keylet::account(owner)), -1, beast::Journal{beast::Journal::getNullSink()}); + decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()}); view.erase(offer); return true; @@ -743,9 +732,11 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner) auto const newPrev = view.peek(Keylet(ltNFTOKEN_PAGE, *prevLink)); if (!newPrev) { + // LCOV_EXCL_START Throw( "NFTokenPage directory for " + to_string(owner) + - " cannot be repaired. Unexpected link problem."); + " cannot be repaired. Unexpected link problem."); + // LCOV_EXCL_STOP } newPrev->at(sfNextPageMin) = nextPage->key(); view.update(newPrev); @@ -931,7 +922,7 @@ tokenOfferCreateApply( { Keylet const acctKeylet = keylet::account(acctID); if (auto const acct = view.read(acctKeylet); - priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) + priorBalance < accountReserve(view, acct, j, {.ownerCountDelta = 1})) return tecINSUFFICIENT_RESERVE; auto const offerID = keylet::nftokenOffer(acctID, seqProxy.value()); @@ -983,7 +974,7 @@ tokenOfferCreateApply( } // Update owner count. - adjustOwnerCount(view, view.peek(acctKeylet), 1, j); + increaseOwnerCount(view, acctID, {}, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp b/src/libxrpl/ledger/helpers/OfferHelpers.cpp index 6e72b71564..caa6ffb8ae 100644 --- a/src/libxrpl/ledger/helpers/OfferHelpers.cpp +++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp @@ -55,7 +55,7 @@ offerDelete(ApplyView& view, SLE::ref sle, beast::Journal j) } } - adjustOwnerCount(view, view.peek(keylet::account(owner)), -1, j); + decreaseOwnerCountForObject(view, owner, sle, 1, j); view.erase(sle); diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index 03b68e8860..d5a0b25681 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -58,7 +58,7 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal XRPL_ASSERT( (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; - adjustOwnerCount(view, sle, -1, j); + decreaseOwnerCountForObject(view, sle, slep, 1, j); view.update(sle); // Remove PayChan from ledger diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index 5a2995c030..868c9fb26d 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -195,6 +196,7 @@ trustCreate( // Issuer should be the account being set. std::uint32_t uQualityIn, std::uint32_t uQualityOut, + SLE::ref sponsorSle, beast::Journal j) { JLOG(j.trace()) << "trustCreate: " << to_string(uSrcAccountID) << ", " @@ -281,7 +283,9 @@ trustCreate( } sleRippleState->setFieldU32(sfFlags, uFlags); - adjustOwnerCount(view, sleAccount, 1, j); + increaseOwnerCount(view, sleAccount, sponsorSle, 1, j); + + addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor); // ONLY: Create ripple balance. sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance); @@ -317,6 +321,9 @@ trustDelete( return tefBAD_LEDGER; // LCOV_EXCL_LINE } + removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor); + removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor); + JLOG(j.trace()) << "trustDelete: Deleting ripple line: state"; view.erase(sleRippleState); @@ -369,11 +376,15 @@ updateTrustLine( { // VFALCO Where is the line being deleted? // Clear the reserve of the sender, possibly delete the line! - adjustOwnerCount(view, sle, -1, j); + auto const currentSponsor = + getLedgerEntryReserveSponsor(view, state, bSenderHigh ? sfHighSponsor : sfLowSponsor); + decreaseOwnerCount(view, sle, currentSponsor, 1, j); // Clear reserve flag. state->clearFlag(senderReserveFlag); + removeSponsorFromLedgerEntry(state, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + // Balance is zero, receiver reserve is clear. if (!after && !state->isFlag(receiverReserveFlag)) return true; @@ -381,12 +392,14 @@ updateTrustLine( return false; } +// Only used in tests TER issueIOU( ApplyView& view, AccountID const& account, STAmount const& amount, Issue const& issue, + SLE::ref sponsorSle, beast::Journal j) { XRPL_ASSERT( @@ -472,6 +485,7 @@ issueIOU( limit, 0, 0, + sponsorSle, j); } @@ -620,7 +634,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -632,30 +646,45 @@ addEmptyHolding( auto const& issuerId = issue.getIssuer(); auto const& currency = issue.currency; - if (isGlobalFrozen(view, issuerId)) + if (isGlobalFrozen(ctx.view, issuerId)) return tecFROZEN; // LCOV_EXCL_LINE auto const& srcId = issuerId; auto const& dstId = accountID; auto const high = srcId > dstId; auto const index = keylet::trustLine(srcId, dstId, currency); - auto const sleSrc = view.peek(keylet::account(srcId)); - auto const sleDst = view.peek(keylet::account(dstId)); + auto const sleSrc = ctx.view.peek(keylet::account(srcId)); + auto const sleDst = ctx.view.peek(keylet::account(dstId)); if (!sleDst || !sleSrc) return tefINTERNAL; // LCOV_EXCL_LINE if (!sleSrc->isFlag(lsfDefaultRipple)) return tecINTERNAL; // LCOV_EXCL_LINE // If the line already exists, don't create it again. - if (view.read(index)) + if (ctx.view.read(index)) return tecDUPLICATE; + // A reserve sponsor only covers tx.Account's own objects. + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleDst); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + // Can the account cover the trust line reserve ? - std::uint32_t const ownerCount = sleDst->at(sfOwnerCount); - if (priorBalance < view.fees().accountReserve(ownerCount + 1)) - return tecNO_LINE_INSUF_RESERVE; + if (auto const ret = checkReserve( + ctx, + sleDst, + priorBalance, + sponsorSle, + {.ownerCountDelta = 1}, + journal, + tecNO_LINE_INSUF_RESERVE); + !isTesSuccess(ret)) + { + return ret; + } return trustCreate( - view, + ctx.view, high, srcId, dstId, @@ -669,19 +698,20 @@ addEmptyHolding( /*saLimit=*/STAmount{Issue{currency, dstId}}, /*uQualityIn=*/0, /*uQualityOut=*/0, + sponsorSle, journal); } TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal) { if (issue.native()) { - auto const sle = view.read(keylet::account(accountID)); + auto const sle = ctx.view.read(keylet::account(accountID)); if (!sle) return tecINTERNAL; // LCOV_EXCL_LINE @@ -696,7 +726,7 @@ removeEmptyHolding( // If the account is the issuer, then no line should exist. Check anyway. // If a line does exist, it will get deleted. If not, return success. bool const accountIsIssuer = accountID == issue.account; - auto const line = view.peek(keylet::trustLine(accountID, issue)); + auto const line = ctx.view.peek(keylet::trustLine(accountID, issue)); if (!line) return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::kZero) @@ -706,33 +736,43 @@ removeEmptyHolding( if (line->isFlag(lsfLowReserve)) { // Clear reserve for low account. - auto sleLowAccount = view.peek(keylet::account(line->at(sfLowLimit)->getIssuer())); + auto sleLowAccount = ctx.view.peek(keylet::account(line->at(sfLowLimit)->getIssuer())); if (!sleLowAccount) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleLowAccount, -1, journal); + auto const currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor); + + decreaseOwnerCount(ctx.view, sleLowAccount, currentLowSponsor, 1, journal); // It's not really necessary to clear the reserve flag, since the line // is about to be deleted, but this will make the metadata reflect an // accurate state at the time of deletion. line->clearFlag(lsfLowReserve); + removeSponsorFromLedgerEntry(line, sfLowSponsor); } if (line->isFlag(lsfHighReserve)) { // Clear reserve for high account. - auto sleHighAccount = view.peek(keylet::account(line->at(sfHighLimit)->getIssuer())); + auto sleHighAccount = ctx.view.peek(keylet::account(line->at(sfHighLimit)->getIssuer())); if (!sleHighAccount) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleHighAccount, -1, journal); + auto const currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor); + + decreaseOwnerCount(ctx.view, sleHighAccount, currentHighSponsor, 1, journal); // It's not really necessary to clear the reserve flag, since the line // is about to be deleted, but this will make the metadata reflect an // accurate state at the time of deletion. line->clearFlag(lsfHighReserve); + removeSponsorFromLedgerEntry(line, sfHighSponsor); } return trustDelete( - view, line, line->at(sfLowLimit)->getIssuer(), line->at(sfHighLimit)->getIssuer(), journal); + ctx.view, + line, + line->at(sfLowLimit)->getIssuer(), + line->at(sfHighLimit)->getIssuer(), + journal); } TER @@ -768,6 +808,9 @@ deleteAMMTrustLine( if (ammAccountID && (low != *ammAccountID && high != *ammAccountID)) return terNO_AMM; + auto const sponsorSle = + getLedgerEntryReserveSponsor(view, sleState, !ammLow ? sfLowSponsor : sfHighSponsor); + if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter)) { JLOG(j.error()) << "deleteAMMTrustLine: failed to delete the trustline."; @@ -778,7 +821,7 @@ deleteAMMTrustLine( if (!sleState->isFlag(uFlags)) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, !ammLow ? sleLow : sleHigh, -1, j); + decreaseOwnerCount(view, !ammLow ? sleLow : sleHigh, sponsorSle, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/SponsorHelpers.cpp b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp new file mode 100644 index 0000000000..7e0c041854 --- /dev/null +++ b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp @@ -0,0 +1,345 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +bool +isReserveSponsorAllowed(TxType txType) +{ + // Transaction types explicitly allow-listed for reserve sponsorship, for + // v1. Lazily-initialized function-local static: constructed once on first + // use, with no startup cost paid by clients that never call this. + static std::unordered_set const kReserveSponsorAllowed = { + ttDELEGATE_SET, + ttDEPOSIT_PREAUTH, + ttPAYMENT, + ttSIGNER_LIST_SET, + ttCHECK_CANCEL, + ttCHECK_CASH, + ttCHECK_CREATE, + ttESCROW_CANCEL, + ttESCROW_CREATE, + ttESCROW_FINISH, + ttPAYCHAN_CLAIM, + ttPAYCHAN_CREATE, + ttPAYCHAN_FUND, + ttCLAWBACK, + ttMPTOKEN_AUTHORIZE, + ttMPTOKEN_ISSUANCE_CREATE, + ttMPTOKEN_ISSUANCE_DESTROY, + ttMPTOKEN_ISSUANCE_SET, + ttTRUST_SET, + ttCREDENTIAL_ACCEPT, + ttCREDENTIAL_CREATE, + ttCREDENTIAL_DELETE, + ttACCOUNT_SET, + ttREGULAR_KEY_SET, + ttSPONSORSHIP_TRANSFER, + }; + return kReserveSponsorAllowed.contains(txType); +} + +std::optional +getTxReserveSponsorID(STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx)) + { + XRPL_ASSERT( + getCurrentTransactionRules()->enabled( // NOLINT(bugprone-unchecked-optional-access) + featureSponsor), + "xrpl::getTxReserveSponsorID : sponsor exists + Sponsor enabled"); + return tx.getAccountID(sfSponsor); + } + return {}; +} + +std::expected +getTxReserveSponsor(ApplyViewContext ctx) +{ + auto const sponsorID = getTxReserveSponsorID(ctx.tx); + if (sponsorID) + { + XRPL_ASSERT( + ctx.view.rules().enabled(featureSponsor), + "xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled"); + auto sle = ctx.view.peek(keylet::account(*sponsorID)); + + // already checked in Transactor::checkSponsor + if (!sle) + return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE + return sle; + } + return SLE::pointer(); +} + +std::expected +getTxReserveSponsor(ReadView const& view, STTx const& tx) +{ + auto const sponsorID = getTxReserveSponsorID(tx); + if (sponsorID) + { + XRPL_ASSERT( + view.rules().enabled(featureSponsor), + "xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled"); + auto sle = view.read(keylet::account(*sponsorID)); + + // already checked in Transactor::checkSponsor + if (!sle) + return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE + return sle; + } + return SLE::pointer(); +} + +std::expected +getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::const_ref accountSle) +{ + // A reserve sponsor only covers tx.Account's own objects. + if (ctx.view.rules().enabled(fixCleanup3_2_0)) + { + XRPL_ASSERT( + accountSle && accountSle->getType() == ltACCOUNT_ROOT, + "xrpl::getEffectiveTxReserveSponsor : accountSle exists and is account type"); + } + else + { + XRPL_ASSERT( + accountSle && + ((accountSle->getType() == ltACCOUNT_ROOT) || (accountSle->getType() == ltESCROW)), + "xrpl::getEffectiveTxReserveSponsor : accountSle exists and is account type"); + } + + if (isPseudoAccount(accountSle) || accountSle->getAccountID(sfAccount) != ctx.tx[sfAccount]) + return SLE::pointer(); + return getTxReserveSponsor(ctx); +} + +std::optional +getLedgerEntryReserveSponsorID(SLE::const_ref sle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle && + ((sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor))), + "xrpl::getLedgerEntryReserveSponsorID : correct sfield"); + + if (sle->isFieldPresent(field)) + return sle->getAccountID(field); + return {}; +} + +SLE::pointer +getLedgerEntryReserveSponsor(ApplyView& view, SLE::const_ref sle, SF_ACCOUNT const& field) +{ + auto const sponsorID = getLedgerEntryReserveSponsorID(sle, field); + if (sponsorID) + { + XRPL_ASSERT( + view.rules().enabled(featureSponsor), + "xrpl::getLedgerEntryReserveSponsor : sponsor exists + Sponsor enabled"); + return view.peek(keylet::account(*sponsorID)); + } + return {}; +} + +void +addSponsorToLedgerEntry(SLE::ref sle, SLE::const_ref sponsorSle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "addSponsorToLedgerEntry : Invalid field to the LedgerEntry"); + if (sponsorSle) + { + XRPL_ASSERT( + getCurrentTransactionRules()->enabled( // NOLINT(bugprone-unchecked-optional-access) + featureSponsor), + "xrpl::addSponsorToLedgerEntry : sponsor exists + Sponsor enabled"); + sle->setAccountID(field, sponsorSle->getAccountID(sfAccount)); + } +} + +void +addSponsorToLedgerEntry(ApplyViewContext ctx, SLE::ref sle, SF_ACCOUNT const& field) +{ + // getTxReserveSponsor yields a null pointer when the tx is not + // reserve-sponsored, so addSponsorToLedgerEntry becomes a no-op then. The + // error case (tecINTERNAL) is an already-checked invariant; skip stamping. + auto const sponsorSle = getTxReserveSponsor(ctx); + if (sponsorSle && *sponsorSle) + { + XRPL_ASSERT( + ctx.view.rules().enabled(featureSponsor), + "xrpl::addSponsorToLedgerEntry : sponsor exists + Sponsor enabled"); + addSponsorToLedgerEntry(sle, *sponsorSle, field); + } +} + +void +removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry"); + if (sle->isFieldPresent(field)) + { + XRPL_ASSERT( + getCurrentTransactionRules()->enabled( // NOLINT(bugprone-unchecked-optional-access) + featureSponsor), + "xrpl::removeSponsorFromLedgerEntry : sponsor exists + Sponsor enabled"); + sle->makeFieldAbsent(field); + } +} + +bool +isLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& account) +{ + switch (sle.getType()) + { + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + return sle.getAccountID(sfAccount) == account; + case ltMPTOKEN_ISSUANCE: + return sle.getAccountID(sfIssuer) == account; + case ltSIGNER_LIST: { + auto const signerList = view.read(keylet::signerList(account)); + if (!signerList) + return false; + return signerList->key() == sle.key(); + } + case ltCREDENTIAL: { + auto const& ownerField = sle.isFlag(lsfAccepted) ? sfSubject : sfIssuer; + return sle.getAccountID(ownerField) == account; + } + case ltRIPPLE_STATE: { + if (sle.isFlag(lsfHighReserve)) + { + auto const highAccount = sle.getFieldAmount(sfHighLimit).getIssuer(); + if (highAccount == account) + return true; + } + if (sle.isFlag(lsfLowReserve)) + { + auto const lowAccount = sle.getFieldAmount(sfLowLimit).getIssuer(); + if (lowAccount == account) + return true; + } + // Reachable: the sponsee may be a third party or the side of the + // line that holds no reserve (e.g. the issuer). Callers map this + // to tecNO_PERMISSION. + return false; + } + default: + // LCOV_EXCL_START + UNREACHABLE("xrpl::isLedgerEntryOwner : object is not supported by sponsorship."); + return false; + // LCOV_EXCL_STOP + }; +} + +bool +isLedgerEntrySupportedBySponsorship(SLE const& sle) +{ + switch (sle.getType()) + { + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + case ltMPTOKEN_ISSUANCE: + case ltSIGNER_LIST: + case ltCREDENTIAL: + case ltRIPPLE_STATE: + return true; + default: + return false; + }; +} + +std::uint32_t +getLedgerEntryOwnerCount(SLE const& sle) +{ + switch (sle.getType()) + { + case ltORACLE: { + return calculateOracleReserve(sle.getFieldArray(sfPriceDataSeries)); + } + // Vaults require 2 owner counts (the vault and a pseudo-account) + case ltVAULT: + return 2; + case ltSIGNER_LIST: { + // Mirror SignerListSet's owner-count accounting so that create and + // delete agree. Modern lists (post-MultiSignReserve) carry the + // lsfOneOwnerCount flag and cost a single owner count. Legacy + // pre-MultiSignReserve lists cost 2 + signer_count owner counts + if (sle.isFlag(lsfOneOwnerCount)) + return 1; + return 2 + static_cast(sle.getFieldArray(sfSignerEntries).size()); + } + case ltACCOUNT_ROOT: + // LCOV_EXCL_START + UNREACHABLE("AccountRoots are not supported by object sponsorship."); + return 0; + // LCOV_EXCL_STOP + default: + return 1; + } +} + +SF_ACCOUNT const& +getLedgerEntrySponsorField(SLE const& sle, AccountID const& owner) +{ + switch (sle.getType()) + { + case ltRIPPLE_STATE: { + if (sle.isFlag(lsfHighReserve)) + { + auto const highAccount = sle.getFieldAmount(sfHighLimit).getIssuer(); + if (highAccount == owner) + return sfHighSponsor; + } + if (sle.isFlag(lsfLowReserve)) + { + auto const lowAccount = sle.getFieldAmount(sfLowLimit).getIssuer(); + if (lowAccount == owner) + return sfLowSponsor; + } + // LCOV_EXCL_START + UNREACHABLE("xrpl::getLedgerEntrySponsorField : unknown owner for RippleState"); + return sfSponsor; + // LCOV_EXCL_STOP + } + default: + return sfSponsor; + } +} + +} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 0ab97a4b60..79e10cdf79 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include +#include #include #include #include @@ -104,12 +106,9 @@ isAnyFrozen( std::initializer_list const& accounts, Issue const& issue) { - for (auto const& account : accounts) - { - if (isFrozen(view, account, issue.currency, issue.account)) - return true; - } - return false; + return std::ranges::any_of(accounts, [&](auto const& account) { + return isFrozen(view, account, issue.currency, issue.account); + }); } bool @@ -571,7 +570,7 @@ canAddHolding(ReadView const& view, Asset const& asset) TER addEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -579,21 +578,21 @@ addEmptyHolding( { return std::visit( [&](TIss const& issue) -> TER { - return addEmptyHolding(view, accountID, priorBalance, issue, journal); + return addEmptyHolding(ctx, accountID, priorBalance, issue, journal); }, asset.value()); } TER removeEmptyHolding( - ApplyView& view, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal) { return std::visit( [&](TIss const& issue) -> TER { - return removeEmptyHolding(view, accountID, issue, journal); + return removeEmptyHolding(ctx, accountID, issue, journal); }, asset.value()); } @@ -647,6 +646,7 @@ directSendNoFeeIOU( AccountID const& uReceiverID, STAmount const& saAmount, bool bCheckIssuer, + SLE::ref sponsorSle, beast::Journal j) { AccountID const& issuer = saAmount.getIssuer(); @@ -717,7 +717,12 @@ directSendNoFeeIOU( // Sender quality out is 0. { // Clear the reserve of the sender, possibly delete the line! - adjustOwnerCount(view, view.peek(keylet::account(uSenderID)), -1, j); + auto const currentSponsor = getLedgerEntryReserveSponsor( + view, sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + decreaseOwnerCount(view, view.peek(keylet::account(uSenderID)), currentSponsor, 1, j); + + removeSponsorFromLedgerEntry( + sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor); // Clear reserve flag. sleRippleState->clearFlag(senderReserveFlag); @@ -780,6 +785,7 @@ directSendNoFeeIOU( saReceiverLimit, 0, 0, + sponsorSle, j); } @@ -794,6 +800,7 @@ directSendNoLimitIOU( STAmount const& saAmount, STAmount& saActual, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { auto const& issuer = saAmount.getIssuer(); @@ -806,7 +813,8 @@ directSendNoLimitIOU( if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount()) { // Direct send: redeeming IOUs and/or sending own IOUs. - auto const ter = directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, false, j); + auto const ter = + directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, false, sponsorSle, j); if (!isTesSuccess(ter)) return ter; saActual = saAmount; @@ -824,10 +832,12 @@ directSendNoLimitIOU( << to_string(uReceiverID) << " : deliver=" << saAmount.getFullText() << " cost=" << saActual.getFullText(); - TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, j); + TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, sponsorSle, j); if (tesSUCCESS == terResult) - terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, j); + { + terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, sponsorSle, j); + } return terResult; } @@ -870,7 +880,8 @@ directSendNoLimitMultiIOU( if (senderID == issuer || receiverID == issuer || issuer == noAccount()) { // Direct send: redeeming IOUs and/or sending own IOUs. - if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j); + if (auto const ter = + directSendNoFeeIOU(view, senderID, receiverID, amount, false, {}, j); !isTesSuccess(ter)) return ter; actual += amount; @@ -894,14 +905,14 @@ directSendNoLimitMultiIOU( << to_string(receiverID) << " : deliver=" << amount.getFullText() << " cost=" << actual.getFullText(); - if (TER const terResult = directSendNoFeeIOU(view, issuer, receiverID, amount, true, j)) + if (TER const terResult = directSendNoFeeIOU(view, issuer, receiverID, amount, true, {}, j)) return terResult; } if (senderID != issuer && takeFromSender) { if (TER const terResult = - directSendNoFeeIOU(view, senderID, issuer, takeFromSender, true, j)) + directSendNoFeeIOU(view, senderID, issuer, takeFromSender, true, {}, j)) return terResult; } @@ -915,6 +926,7 @@ accountSendIOU( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { if (view.rules().enabled(fixAMMv1_1)) @@ -946,7 +958,8 @@ accountSendIOU( JLOG(j.trace()) << "accountSendIOU: " << to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : " << saAmount.getFullText(); - return directSendNoLimitIOU(view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); + return directSendNoLimitIOU( + view, uSenderID, uReceiverID, saAmount, saActual, j, sponsorSle, waiveFee); } /* XRP send which does not check reserve and can do pure adjustment. @@ -1475,7 +1488,7 @@ directSendNoFee( { return saAmount.asset().visit( [&](Issue const&) { - return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j); + return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, {}, j); }, [&](MPTIssue const&) { XRPL_ASSERT(!bCheckIssuer, "xrpl::directSendNoFee : not checking issuer"); @@ -1490,12 +1503,13 @@ accountSend( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee, AllowMPTOverflow allowOverflow) { return saAmount.asset().visit( [&](Issue const&) { - return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, waiveFee); + return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, sponsorSle, waiveFee); }, [&](MPTIssue const&) { return accountSendMPT( diff --git a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp index 7f4dca3ed1..23a48a3bf3 100644 --- a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp +++ b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -52,6 +53,7 @@ DatabaseRotatingImp::rotate( // callback finishes. Only then will the archive directory be // deleted. std::shared_ptr oldArchiveBackend; + std::uint64_t copyForwards = 0; { std::scoped_lock const lock(mutex_); @@ -62,11 +64,28 @@ DatabaseRotatingImp::rotate( newArchiveBackendName = archiveBackend_->getName(); writableBackend_ = std::move(newBackend); + + copyForwards = copyForwardCount_.exchange(0, std::memory_order_relaxed); + } + + if (copyForwards > 0) + { + JLOG(j_.warn()) << "Rotating: copied forward " << copyForwards + << " archive-served reads into the writable backend " + "during the rotation window"; } f(newWritableBackendName, newArchiveBackendName); } +void +DatabaseRotatingImp::setRotationInFlight(bool inFlight) +{ + rotationInFlight_.store(inFlight, std::memory_order_release); + JLOG(j_.debug()) << "Rotating: copy-forward on archive reads " + << (inFlight ? "enabled" : "disabled"); +} + std::string DatabaseRotatingImp::getName() const { @@ -177,9 +196,18 @@ DatabaseRotatingImp::fetchNodeObject( writable = writableBackend_; } - // Update writable backend with data from the archive backend - if (duplicate) + // Update writable backend with data from the archive backend. + // While a rotation is in flight, ordinary (duplicate == false) + // reads served by the archive are copied forward too: the + // archive is about to be deleted, and a body canonicalized + // into the cache after the freshen getKeys() snapshot would + // otherwise survive only in RAM once the archive is dropped. + if (duplicate || rotationInFlight_.load(std::memory_order_acquire)) + { + if (!duplicate) + copyForwardCount_.fetch_add(1, std::memory_order_relaxed); writable->store(nodeObject); + } } } diff --git a/src/libxrpl/nodestore/DecodedBlob.cpp b/src/libxrpl/nodestore/DecodedBlob.cpp index fe07252f23..9740462ae8 100644 --- a/src/libxrpl/nodestore/DecodedBlob.cpp +++ b/src/libxrpl/nodestore/DecodedBlob.cpp @@ -12,7 +12,7 @@ namespace xrpl::NodeStore { -DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) +DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) : key_(key) { /* Data format: @@ -23,10 +23,6 @@ DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) 9...end The body of the object data */ - success_ = false; - key_ = key; - objectType_ = NodeObjectType::Unknown; - objectData_ = nullptr; dataBytes_ = std::max(0, valueBytes - 9); // VFALCO NOTE What about bytes 4 through 7 inclusive? diff --git a/src/libxrpl/nodestore/backend/NullFactory.cpp b/src/libxrpl/nodestore/backend/NullFactory.cpp index e36b13a2e1..0c76cb9938 100644 --- a/src/libxrpl/nodestore/backend/NullFactory.cpp +++ b/src/libxrpl/nodestore/backend/NullFactory.cpp @@ -81,7 +81,9 @@ public: { } - /** Returns the number of file descriptors the backend expects to need */ + /** + * Returns the number of file descriptors the backend expects to need + */ [[nodiscard]] int fdRequired() const override { diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index bcf4ba4a49..673b0daae0 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -419,7 +419,9 @@ public: storeBatch(batch); } - /** Returns the number of file descriptors the backend expects to need */ + /** + * Returns the number of file descriptors the backend expects to need + */ [[nodiscard]] int fdRequired() const override { diff --git a/src/libxrpl/protocol/AccountID.cpp b/src/libxrpl/protocol/AccountID.cpp index 6050144a8e..c6a5226566 100644 --- a/src/libxrpl/protocol/AccountID.cpp +++ b/src/libxrpl/protocol/AccountID.cpp @@ -21,7 +21,9 @@ namespace xrpl { namespace detail { -/** Caches the base58 representations of AccountIDs */ +/** + * Caches the base58 representations of AccountIDs + */ class AccountIdCache { private: diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 820936a22d..6ac352f3e1 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-b0" +char const* const versionString = "3.3.0-rc1" // clang-format on ; diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 059abe2996..2a8476dea8 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -154,7 +154,9 @@ public: uint256 registerFeature(std::string const& name, Supported support, VoteBehavior vote); - /** Tell FeatureCollections when registration is complete. */ + /** + * Tell FeatureCollections when registration is complete. + */ bool registrationIsDone(); @@ -167,30 +169,38 @@ public: std::string featureToName(uint256 const& f) const; - /** All amendments that are registered within the table. */ + /** + * All amendments that are registered within the table. + */ std::map const& allAmendments() const { return all_; } - /** Amendments that this server supports. - Whether they are enabled depends on the Rules defined in the validated - ledger */ + /** + * Amendments that this server supports. + * Whether they are enabled depends on the Rules defined in the validated + * ledger + */ std::map const& supportedAmendments() const { return supported_; } - /** Amendments that this server WON'T vote for by default. */ + /** + * Amendments that this server WON'T vote for by default. + */ std::size_t numDownVotedAmendments() const { return downVotes_; } - /** Amendments that this server WILL vote for by default. */ + /** + * Amendments that this server WILL vote for by default. + */ std::size_t numUpVotedAmendments() const { @@ -271,7 +281,9 @@ FeatureCollections::registerFeature(std::string const& name, Supported support, logicError("Duplicate feature registration"); } -/** Tell FeatureCollections when registration is complete. */ +/** + * Tell FeatureCollections when registration is complete. + */ bool FeatureCollections::registrationIsDone() { @@ -313,30 +325,38 @@ FeatureCollections gFeatureCollections; } // namespace -/** All amendments libxrpl knows of. */ +/** + * All amendments libxrpl knows of. + */ std::map const& allAmendments() { return gFeatureCollections.allAmendments(); } -/** Amendments that this server supports. - Whether they are enabled depends on the Rules defined in the validated - ledger */ +/** + * Amendments that this server supports. + * Whether they are enabled depends on the Rules defined in the validated + * ledger + */ std::map const& detail::supportedAmendments() { return gFeatureCollections.supportedAmendments(); } -/** Amendments that this server won't vote for by default. */ +/** + * Amendments that this server won't vote for by default. + */ std::size_t detail::numDownVotedAmendments() { return gFeatureCollections.numDownVotedAmendments(); } -/** Amendments that this server will vote for by default. */ +/** + * Amendments that this server will vote for by default. + */ std::size_t detail::numUpVotedAmendments() { @@ -365,7 +385,9 @@ retireFeature(std::string const& name) return registerFeature(name, Supported::Yes, VoteBehavior::Obsolete); } -/** Tell FeatureCollections when registration is complete. */ +/** + * Tell FeatureCollections when registration is complete. + */ bool registrationIsDone() { diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index a49f7b85ee..95416d0f2a 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -32,23 +33,41 @@ namespace xrpl { -/** Type-specific prefix for calculating ledger indices. +// This list should include all of the keylet functions that take a single +// AccountID parameter. Declared in Indexes.h; defined here so the header need +// not include jss.h. +std::array, 6> const kDirectAccountKeylets{ + {{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false}, + {.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true}, + {.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true}, + // It's normally impossible to create an item at nftpage_min, but + // test it anyway, since the invariant checks for it. + {.function = &keylet::nftokenPageMin, + .expectedLEName = jss::NFTokenPage, + .includeInTests = true}, + {.function = &keylet::nftokenPageMax, + .expectedLEName = jss::NFTokenPage, + .includeInTests = true}, + {.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}}; - The identifier for a given object within the ledger is calculated based - on some object-specific parameters. To ensure that different types of - objects have different indices, even if they happen to use the same set - of parameters, we use "tagged hashing" by adding a type-specific prefix. - - @note These values are part of the protocol and *CANNOT* be arbitrarily - changed. If they were, on-ledger objects may no longer be able to - be located or addressed. - - Additions to this list are OK, but changing existing entries to - assign them a different values should never be needed. - - Entries that are removed should be moved to the bottom of the enum - and marked as [[deprecated]] to prevent accidental reuse. -*/ +/** + * Type-specific prefix for calculating ledger indices. + * + * The identifier for a given object within the ledger is calculated based + * on some object-specific parameters. To ensure that different types of + * objects have different indices, even if they happen to use the same set + * of parameters, we use "tagged hashing" by adding a type-specific prefix. + * + * @note These values are part of the protocol and *CANNOT* be arbitrarily + * changed. If they were, on-ledger objects may no longer be able to + * be located or addressed. + * + * Additions to this list are OK, but changing existing entries to + * assign them a different values should never be needed. + * + * Entries that are removed should be moved to the bottom of the enum + * and marked as [[deprecated]] to prevent accidental reuse. + */ enum class LedgerNameSpace : std::uint16_t { Account = 'a', DirNode = 'd', @@ -84,6 +103,7 @@ enum class LedgerNameSpace : std::uint16_t { Vault = 'V', LoanBroker = 'l', // lower-case L Loan = 'L', + Sponsorship = '>', // No longer used or supported. Left here to reserve the space to avoid accidental reuse. Contract [[deprecated]] = 'c', @@ -152,7 +172,12 @@ std::uint64_t getQuality(uint256 const& uBase) { // VFALCO [base_uint] This assumes a certain storage format - return boost::endian::big_to_native(((std::uint64_t*)uBase.end())[-1]); + // + // Load the final 8 bytes as a big-endian integer. load_big_u64 reads + // through unaligned byte storage (via memcpy) and applies the endian + // conversion, avoiding the alignment/strict-aliasing UB of casting the + // unsigned char* returned by end() to a std::uint64_t*. + return boost::endian::load_big_u64(uBase.end() - 8); } uint256 @@ -278,8 +303,11 @@ quality(Keylet const& k, std::uint64_t q) noexcept // for indexes. uint256 x = k.key; - // FIXME This is ugly and we can and should do better... - ((std::uint64_t*)x.end())[-1] = boost::endian::native_to_big(q); + // Store the quality as a big-endian integer in the final 8 bytes. + // store_big_u64 writes through unaligned byte storage (via memcpy) and + // applies the endian conversion, avoiding the alignment/strict-aliasing UB + // of casting the unsigned char* returned by end() to a std::uint64_t*. + boost::endian::store_big_u64(x.end() - 8, q); return {ltDIR_NODE, x}; } @@ -318,6 +346,12 @@ signerList(AccountID const& account) noexcept return signerList(account, 0); } +Keylet +sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept +{ + return {ltSPONSORSHIP, indexHash(LedgerNameSpace::Sponsorship, sponsor, sponsee)}; +} + Keylet check(AccountID const& id, std::uint32_t seq) noexcept { diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 66b2822a42..0bdb217771 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -160,6 +160,14 @@ InnerObjectFormats::InnerObjectFormats() {sfTxnSignature, SoeOptional}, {sfSigners, SoeOptional}, }); + + add(sfSponsorSignature.jsonName.cStr(), + sfSponsorSignature.getCode(), + { + {sfSigningPubKey, SoeOptional}, + {sfTxnSignature, SoeOptional}, + {sfSigners, SoeOptional}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 8b91bb7930..a29ea49266 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -15,6 +15,7 @@ LedgerFormats::getCommonFields() {sfLedgerIndex, SoeOptional}, {sfLedgerEntryType, SoeRequired}, {sfFlags, SoeRequired}, + {sfSponsor, SoeOptional}, }; return kCommonFields; } diff --git a/src/libxrpl/protocol/PublicKey.cpp b/src/libxrpl/protocol/PublicKey.cpp index 97948fcae3..cb6ea9e851 100644 --- a/src/libxrpl/protocol/PublicKey.cpp +++ b/src/libxrpl/protocol/PublicKey.cpp @@ -96,18 +96,19 @@ sliceToHex(Slice const& slice) return s; } -/** Determine whether a signature is canonical. - Canonical signatures are important to protect against signature morphing - attacks. - @param vSig the signature data - @param sigLen the length of the signature - @param strict_param whether to enforce strictly canonical semantics - - @note For more details please see: - https://xrpl.org/transaction-malleability.html - https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623 - https://github.com/sipa/bitcoin/commit/58bc86e37fda1aec270bccb3df6c20fbd2a6591c -*/ +/** + * Determine whether a signature is canonical. + * Canonical signatures are important to protect against signature morphing + * attacks. + * @param vSig the signature data + * @param sigLen the length of the signature + * @param strict_param whether to enforce strictly canonical semantics + * + * @note For more details please see: + * https://xrpl.org/transaction-malleability.html + * https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623 + * https://github.com/sipa/bitcoin/commit/58bc86e37fda1aec270bccb3df6c20fbd2a6591c + */ std::optional ecdsaCanonicality(Slice const& sig) { diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index e0968ea868..197139027a 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -39,22 +39,30 @@ setCurrentTransactionRules(std::optional r) // Push the appropriate setting, instead of having the class pull every time // the value is needed. That could get expensive fast. - // If any new conditions with new amendments are added, those amendments must also be added to - // useRulesGuards. - bool const enableVaultNumbers = - !r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); - bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); - XRPL_ASSERT( - !r || useRulesGuards(*r) == (enableCuspRoundingFix || enableVaultNumbers), - "setCurrentTransactionRules : rule decisions match"); - // Declare the range this way to keep clang-tidy from complaining - auto const range = [enableCuspRoundingFix, enableVaultNumbers]() { - if (enableVaultNumbers) + auto const range = [&r]() { + // If any new conditions with new amendments are added to "enableLargeNumbers", those + // amendments must also be added to useRulesGuards. + bool const enableLargeNumbers = + !r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); + // If enableLargeNumbers is true, then useRulesGuards must also return true. + // However, the reverse is not true. Other amendments can cause the rules guard to be used, + // even though large numbers are _not_ used. + XRPL_ASSERT( + !r || !enableLargeNumbers || useRulesGuards(*r), + "setCurrentTransactionRules : rule decisions match"); + + if (enableLargeNumbers) { - if (enableCuspRoundingFix) + static_assert( + MantissaRange::MantissaScale::Large == MantissaRange::MantissaScale::Large330); + if (!r || r->enabled(fixCleanup3_3_0)) { - return MantissaRange::MantissaScale::Large; + return MantissaRange::MantissaScale::Large330; + } + if (r->enabled(fixCleanup3_2_0)) + { + return MantissaRange::MantissaScale::Large320; } return MantissaRange::MantissaScale::LargeLegacy; } @@ -69,14 +77,14 @@ bool useRulesGuards(Rules const& rules) { // The list of amendments used here - to decide whether to create a RulesGuard - must be a - // superset of the list used to figure out which mantissa scale to use in - // setCurrentTransactionRules. Additional amendments can be added if desired. + // superset of the list used to determine "enableLargeNumbers" in setCurrentTransactionRules. + // Additional amendments can be added if desired. // // As soon as any one of these amendments is retired, this whole function can be removed, along // with createGuards, and any other callers, and the first set of guards can be created directly // at the call site, without using optional. - return rules.enabled(fixCleanup3_2_0) || rules.enabled(featureSingleAssetVault) || - rules.enabled(featureLendingProtocol); + return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) || + rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0); } void @@ -87,7 +95,8 @@ createGuards( { if (useRulesGuards(rules)) { - // raii classes for the current ledger rules. + // raii classes for the current ledger rules. If the rules are set, the MantissaRange will + // be updated, too. rulesGuard.emplace(rules); } else diff --git a/src/libxrpl/protocol/SOTemplate.cpp b/src/libxrpl/protocol/SOTemplate.cpp index 708fd465e2..171f96e1ed 100644 --- a/src/libxrpl/protocol/SOTemplate.cpp +++ b/src/libxrpl/protocol/SOTemplate.cpp @@ -21,11 +21,12 @@ SOTemplate::SOTemplate( } SOTemplate::SOTemplate(std::vector uniqueFields, std::vector commonFields) - : indices_(SField::getNumFields() + 1, -1) // Unmapped indices == -1 + : elements_(std::move(uniqueFields)) + , indices_(SField::getNumFields() + 1, -1) // Unmapped indices == -1 { // Add all SOElements. // - elements_ = std::move(uniqueFields); + std::ranges::move(commonFields, std::back_inserter(elements_)); // Validate and index elements_. diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index 79f7655869..7bf98f270c 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -88,7 +88,6 @@ STNumber::add(Serializer& s) const } else { -#if !NDEBUG // There are circumstances where an already-rounded Number is // serialized without being touched by a transactor, and thus // without an asset. We can't know if it's rounded, because it could @@ -96,11 +95,9 @@ STNumber::add(Serializer& s) const // Json. Regardless, the only time we should be serializing an // STNumber is when the scale is large. XRPL_ASSERT_PARTS( - Number::getMantissaScale() == MantissaRange::MantissaScale::LargeLegacy || - Number::getMantissaScale() == MantissaRange::MantissaScale::Large, + Number::getMantissaScale() != MantissaRange::MantissaScale::Small, "xrpl::STNumber::add", "STNumber only used with large mantissa scale"); -#endif } } @@ -258,8 +255,47 @@ numberFromJson(SField const& field, json::Value const& value) Throw("not a number"); } - return STNumber{ - field, Number{parts.negative, parts.mantissa, parts.exponent, Number::Normalized{}}}; + Number const num{parts.negative, parts.mantissa, parts.exponent, Number::Normalized{}}; + + // Canonicalize "parts" and "num" with each other by getting rid of trailing 0s until either the + // exponents match, or there are no more 0s. If the two results don't match exactly, then the + // value has been rounded one way or another, and should not be used, because it may lead to an + // unexpected result. canonicalizeParts is not to be confused with Number::canonicalize, because + // they have completely different goals. + auto canonicalizeParts = [](NumberParts p, int otherExponent) { + if (p.mantissa == 0) + return NumberParts{}; + + while (p.exponent < otherExponent && p.mantissa % 10 == 0) + { + p.mantissa /= 10; + ++p.exponent; + } + + return p; + }; + + auto const numberMantissa = num.mantissa(); + auto const numberExponent = num.exponent(); + + auto const canonicalParts = canonicalizeParts(parts, numberExponent); + + auto const canonicalNum = canonicalizeParts( + NumberParts{ + .mantissa = Number::externalToInternal(numberMantissa), + .exponent = numberExponent, + .negative = numberMantissa < 0, + }, + canonicalParts.exponent); + + if (canonicalParts.mantissa != canonicalNum.mantissa || + canonicalParts.exponent != canonicalNum.exponent || + canonicalParts.negative != canonicalNum.negative) + { + Throw("number cannot be represented"); + } + + return STNumber{field, num}; } } // namespace xrpl diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 869700baed..4b3ace2be3 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -633,20 +633,6 @@ STObject::getAccountID(SField const& field) const return getFieldByValue(field); } -AccountID -STObject::getFeePayer() const -{ - // If sfDelegate is present, the delegate account is the payer - // note: if a delegate is specified, its authorization to act on behalf of the account is - // enforced in `Transactor::invokeCheckPermission` - // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) - if (isFieldPresent(sfDelegate)) - return getAccountID(sfDelegate); - - // Default payer - return getAccountID(sfAccount); -} - Blob STObject::getFieldVL(SField const& field) const { @@ -710,7 +696,7 @@ STObject::getFieldNumber(SField const& field) const void STObject::set(std::unique_ptr v) { - set(std::move(*v.get())); + set(std::move(*v)); } void diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index d3342ef728..33ca5424d1 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -69,6 +69,17 @@ toUnsigned(U2 value) return static_cast(value); } +static std::string +joinName(std::string const& jsonName, std::string const& fieldName) +{ + std::string result; + result.reserve(jsonName.size() + 1 + fieldName.size()); + result += jsonName; + result += '.'; + result += fieldName; + return result; +} + // LCOV_EXCL_START static inline std::string makeName(std::string const& object, std::string const& field) @@ -76,7 +87,7 @@ makeName(std::string const& object, std::string const& field) if (field.empty()) return object; - return object + "." + field; + return joinName(object, field); } static inline json::Value @@ -1036,8 +1047,8 @@ parseObject( try { - auto ret = - parseObject(jsonName + "." + fieldName, value, field, depth + 1, error); + auto ret = parseObject( + joinName(jsonName, fieldName), value, field, depth + 1, error); if (!ret) return std::nullopt; data.emplaceBack(std::move(*ret)); @@ -1054,8 +1065,8 @@ parseObject( case STI_ARRAY: try { - auto array = - parseArray(jsonName + "." + fieldName, value, field, depth + 1, error); + auto array = parseArray( + joinName(jsonName, fieldName), value, field, depth + 1, error); if (!array.has_value()) return std::nullopt; data.emplaceBack(std::move(*array)); diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index d61f17ecc6..8987d05f1e 100644 --- a/src/libxrpl/protocol/STPathSet.cpp +++ b/src/libxrpl/protocol/STPathSet.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -159,13 +160,10 @@ STPathSet::isDefault() const bool STPath::hasSeen(AccountID const& account, PathAsset const& asset, AccountID const& issuer) const { - for (auto& p : path_) - { - if (p.getAccountID() == account && p.getPathAsset() == asset && p.getIssuerID() == issuer) - return true; - } - - return false; + return std::ranges::any_of(path_, [&](auto& p) { + return p.getAccountID() == account && p.getPathAsset() == asset && + p.getIssuerID() == issuer; + }); } json::Value diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 9f3c6738ae..17d7617590 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -66,12 +67,12 @@ getTxFormat(TxType type) return format; } -STTx::STTx(STObject&& object) : STObject(std::move(object)) +STTx::STTx(STObject&& object) + : STObject(std::move(object)), txType_(safeCast(getFieldU16(sfTransactionType))) { - txType_ = safeCast(getFieldU16(sfTransactionType)); applyTemplate(getTxFormat(txType_)->getSOTemplate()); // may throw tid_ = getHash(HashPrefix::TransactionId); - buildBatchTxnIds(); + buildBatchTxns(); } STTx::STTx(SerialIter& sit) : STObject(sfTransaction) @@ -88,7 +89,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction) applyTemplate(getTxFormat(txType_)->getSOTemplate()); // May throw tid_ = getHash(HashPrefix::TransactionId); - buildBatchTxnIds(); + buildBatchTxns(); } STTx::STTx(TxType type, std::function assembler) : STObject(sfTransaction) @@ -100,13 +101,16 @@ STTx::STTx(TxType type, std::function assembler) : STObject(sfT assembler(*this); + // txType_ must be read after the object is assembled, so this cannot be a + // member initializer. + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) txType_ = safeCast(getFieldU16(sfTransactionType)); if (txType_ != type) logicError("Transaction type was mutated during assembly"); tid_ = getHash(HashPrefix::TransactionId); - buildBatchTxnIds(); + buildBatchTxns(); } STBase* @@ -268,12 +272,16 @@ STTx::checkSign(Rules const& rules) const return std::unexpected("Counterparty: " + ret.error()); } - // Verify the batch signer signatures here too, so they are cached with the - // rest of signature checking (checkValidity / SF_SIGGOOD) and stay out of - // the transaction engine. Gated on a batch (batchTxnIds_ seated) that - // actually carries signers; a batch whose inners are all from the outer - // account has no sfBatchSigners and needs no signer crypto. - if (batchTxnIds_ && isFieldPresent(sfBatchSigners)) + if (isFieldPresent(sfSponsorSignature)) + { + auto const sponsorSignatureObj = getFieldObject(sfSponsorSignature); + if (auto const ret = checkSign(rules, sponsorSignatureObj); !ret) + return std::unexpected("Sponsor: " + ret.error()); + } + + // Verify batch signer signatures here so they are cached with the rest + // of signature checking. + if (isFieldPresent(sfBatchSigners)) { if (auto const ret = checkBatchSign(rules); !ret) return ret; @@ -296,11 +304,28 @@ STTx::checkBatchSign(Rules const& rules) const if (!isFieldPresent(sfBatchSigners)) return std::unexpected("Missing BatchSigners field."); // LCOV_EXCL_LINE STArray const& signers{getFieldArray(sfBatchSigners)}; + // Bound signature verification to the protocol cap. This runs in + // checkValidity (via checkSign) at relay / submit time, BEFORE preflight + // and passesLocalChecks enforce the cap. Without this guard a malicious + // peer could put an oversized sfBatchSigners array in a 1 MB blob and + // force one signature verification per entry before any of those checks + // (or the fee charge) runs. + if (signers.size() > kMaxBatchSigners) + return std::unexpected("BatchSigners array exceeds max entries."); + // Defensive. + if (!batchTxns_) + { + // LCOV_EXCL_START + UNREACHABLE("STTx::checkBatchSign : batch transactions not built"); + return std::unexpected("Missing inner transactions."); + // LCOV_EXCL_STOP + } + auto const txIds = getBatchTransactionIDs(); for (auto const& signer : signers) { Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey); - auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules) - : checkBatchSingleSign(signer); + auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules, txIds) + : checkBatchSingleSign(signer, txIds); if (!result) return result; @@ -430,12 +455,11 @@ STTx::checkSingleSign(STObject const& sigObject) const } std::expected -STTx::checkBatchSingleSign(STObject const& batchSigner) const +STTx::checkBatchSingleSign(STObject const& batchSigner, std::vector const& txIds) const { XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction"); Serializer msg; - serializeBatch( - msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs()); + serializeBatch(msg, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds); finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg); return singleSignHelper(batchSigner, msg.slice()); } @@ -518,7 +542,10 @@ multiSignHelper( } std::expected -STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const +STTx::checkBatchMultiSign( + STObject const& batchSigner, + Rules const& rules, + std::vector const& txIds) const { XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction"); // We can ease the computational load inside the loop a bit by @@ -526,8 +553,7 @@ STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const // with the stuff that stays constant from signature to signature. auto const batchSignerAccount = batchSigner.getAccountID(sfAccount); Serializer dataStart; - serializeBatch( - dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs()); + serializeBatch(dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds); dataStart.addBitString(batchSignerAccount); return multiSignHelper( batchSigner, @@ -548,7 +574,7 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const // For delegated transactions sfDelegate is the account whose signer list is checked, // the delegate account itself can not be among the signers. auto const txnAccountID = - &sigObject != this ? std::nullopt : std::optional(getFeePayer()); + &sigObject != this ? std::nullopt : std::optional(getInitiator()); // We can ease the computational load inside the loop a bit by // pre-constructing part of the data that we hash. Fill a Serializer @@ -566,38 +592,88 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const } void -STTx::buildBatchTxnIds() +STTx::buildBatchTxns() { // Precondition: the template must have been applied first, so the fields // (including sfRawTransactions) are canonical before the inner txns are // hashed. The constructors call this immediately after applying the // template; isFree() being false confirms a template is set. - XRPL_ASSERT(!isFree(), "STTx::buildBatchTxnIds : template applied"); - if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions)) + XRPL_ASSERT(!isFree(), "STTx::buildBatchTxns : template applied"); + if (getTxnType() != ttBATCH) return; + // A Batch always seats its inner transactions here, so every downstream + // consumer can rely on them. sfRawTransactions is required by the format + // (applyTemplate rejects a Batch without it); this guards a future change + // that made it optional. + if (!isFieldPresent(sfRawTransactions)) + { + // LCOV_EXCL_START + UNREACHABLE("STTx::buildBatchTxns : missing RawTransactions"); + Throw("Batch has no RawTransactions."); + // LCOV_EXCL_STOP + } auto const& raw = getFieldArray(sfRawTransactions); + if (raw.size() > kMaxBatchTxCount) + Throw("Batch has too many inner transactions."); - // Seated for any batch with raw transactions. The count is validated in - // preflight and at the relay boundary, so build every id here; this keeps - // the invariant batchTxnIds_->size() == rawTransactions.size(). - auto& ids = batchTxnIds_.emplace(); - ids.reserve(raw.size()); + // Build and validate each inner as an STTx once. A malformed inner throws; + // a nested batch is rejected before building it (a batch cannot contain a + // batch, and building one would recurse). + auto& txns = batchTxns_.emplace(); + txns.reserve(raw.size()); for (STObject const& rb : raw) - ids.push_back(rb.getHash(HashPrefix::TransactionId)); + { + if (rb.getFieldU16(sfTransactionType) == ttBATCH) + Throw("Batch inner transaction cannot be a Batch."); + + txns.push_back(std::make_shared(STObject{rb})); + } } -std::vector const& +std::vector STTx::getBatchTransactionIDs() const { - XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : batch transaction"); + auto const& txns = getBatchTransactions(); + std::vector ids; + ids.reserve(txns.size()); + for (auto const& stx : txns) + ids.push_back(stx->getTransactionID()); + return ids; +} + +std::vector> const& +STTx::getBatchTransactions() const +{ + XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactions : batch transaction"); + XRPL_ASSERT(batchTxns_.has_value(), "STTx::getBatchTransactions : batch transactions built"); XRPL_ASSERT( - batchTxnIds_.has_value(), "STTx::getBatchTransactionIDs : batch transaction IDs built"); - XRPL_ASSERT( - batchTxnIds_->size() == getFieldArray(sfRawTransactions).size(), - "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access): guarded by assert above - return *batchTxnIds_; + batchTxns_->size() == getFieldArray(sfRawTransactions).size(), + "STTx::getBatchTransactions : batch transactions size mismatch"); + return *batchTxns_; +} + +AccountID +STTx::getInitiator() const +{ + // If sfDelegate is present, the delegate account is the initiator + // note: if a delegate is specified, its authorization to act on behalf of the account is + // enforced in `Transactor::invokeCheckPermission` + // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) + if (isFieldPresent(sfDelegate)) + return getAccountID(sfDelegate); + + // Default initiator + return getAccountID(sfAccount); +} + +AccountID +STTx::getFeePayerID() const +{ + if (isFieldPresent(sfSponsor) && ((getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u)) + return at(sfSponsor); + + return getInitiator(); } //------------------------------------------------------------------------------ @@ -734,86 +810,62 @@ invalidMPTAmountInTx(STObject const& tx) } static bool -isBatchRawTransactionOkay(STObject const& st, std::string& reason) +isBatchRawTransactionOkay(STTx const& tx, std::string& reason) { - if (!st.isFieldPresent(sfRawTransactions)) + if (!tx.isFieldPresent(sfRawTransactions)) return true; // sfRawTransactions only appears on a Batch. passesLocalChecks runs on // unverified user and peer input, so reject (rather than assert) a non-batch // transaction that carries it. - if (st.getFieldU16(sfTransactionType) != ttBATCH) + if (tx.getTxnType() != ttBATCH) { reason = "Only Batch transactions may contain raw transactions."; return false; } - if (st.isFieldPresent(sfBatchSigners) && - st.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners) + if (tx.isFieldPresent(sfBatchSigners) && + tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners) { - reason = "Batch Signers array exceeds max entries."; + reason = "BatchSigners array exceeds max entries."; return false; } - auto const& rawTxns = st.getFieldArray(sfRawTransactions); - if (rawTxns.size() > kMaxBatchTxCount) + // Inner structure (type, template, no nesting, count) is validated when the + // batch STTx is constructed; here we only run each inner's local checks. + for (auto const& inner : tx.getBatchTransactions()) { - reason = "Raw Transactions array exceeds max entries."; - return false; - } - for (STObject raw : rawTxns) - { - try - { - auto const tt = safeCast(raw.getFieldU16(sfTransactionType)); - if (tt == ttBATCH) - { - reason = "Raw Transactions may not contain batch transactions."; - return false; - } - - raw.applyTemplate(getTxFormat(tt)->getSOTemplate()); - - // passesLocalChecks recurses back into isBatchRawTransactionOkay, - // but an inner can never be a batch (rejected above), so the - // recursion terminates at depth 1. - if (!passesLocalChecks(raw, reason)) - return false; - } - catch (std::exception const& e) - { - reason = e.what(); + if (!passesLocalChecks(*inner, reason)) return false; - } } return true; } bool -passesLocalChecks(STObject const& st, std::string& reason) +passesLocalChecks(STTx const& tx, std::string& reason) { - if (!isMemoOkay(st, reason)) + if (!isMemoOkay(tx, reason)) return false; - if (!isAccountFieldOkay(st)) + if (!isAccountFieldOkay(tx)) { reason = "An account field is invalid."; return false; } - if (isPseudoTx(st)) + if (isPseudoTx(tx)) { reason = "Cannot submit pseudo transactions."; return false; } - if (invalidMPTAmountInTx(st)) + if (invalidMPTAmountInTx(tx)) { reason = "Amount can not be MPT."; return false; } - if (!isBatchRawTransactionOkay(st, reason)) + if (!isBatchRawTransactionOkay(tx, reason)) return false; return true; diff --git a/src/libxrpl/protocol/SecretKey.cpp b/src/libxrpl/protocol/SecretKey.cpp index f33b1871e1..7713911ab4 100644 --- a/src/libxrpl/protocol/SecretKey.cpp +++ b/src/libxrpl/protocol/SecretKey.cpp @@ -99,23 +99,24 @@ deriveDeterministicRootKey(Seed const& seed) } //------------------------------------------------------------------------------ -/** Produces a sequence of secp256k1 key pairs. - - The reference implementation of the XRP Ledger uses a custom derivation - algorithm which enables the derivation of an entire family of secp256k1 - keypairs from a single 128-bit seed. The algorithm predates widely-used - standards like BIP-32 and BIP-44. - - Important note to implementers: - - Using this algorithm is not required: all valid secp256k1 keypairs will - work correctly. Third party implementations can use whatever mechanisms - they prefer. However, implementers of wallets or other tools that allow - users to use existing accounts should consider at least supporting this - derivation technique to make it easier for users to 'import' accounts. - - For more details, please check out: - https://xrpl.org/cryptographic-keys.html#secp256k1-key-derivation +/** + * Produces a sequence of secp256k1 key pairs. + * + * The reference implementation of the XRP Ledger uses a custom derivation + * algorithm which enables the derivation of an entire family of secp256k1 + * keypairs from a single 128-bit seed. The algorithm predates widely-used + * standards like BIP-32 and BIP-44. + * + * Important note to implementers: + * + * Using this algorithm is not required: all valid secp256k1 keypairs will + * work correctly. Third party implementations can use whatever mechanisms + * they prefer. However, implementers of wallets or other tools that allow + * users to use existing accounts should consider at least supporting this + * derivation technique to make it easier for users to 'import' accounts. + * + * For more details, please check out: + * https://xrpl.org/cryptographic-keys.html#secp256k1-key-derivation */ class Generator { @@ -177,7 +178,9 @@ public: secureErase(generator_.data(), generator_.size()); } - /** Generate the nth key pair. */ + /** + * Generate the nth key pair. + */ std::pair operator()(std::size_t ordinal) const { diff --git a/src/libxrpl/protocol/Serializer.cpp b/src/libxrpl/protocol/Serializer.cpp index 1eda04705f..80ecdee6c8 100644 --- a/src/libxrpl/protocol/Serializer.cpp +++ b/src/libxrpl/protocol/Serializer.cpp @@ -99,7 +99,7 @@ int Serializer::addRaw(void const* ptr, int len) { int const ret = data_.size(); - data_.insert(data_.end(), (char const*)ptr, ((char const*)ptr) + len); + data_.insert(data_.end(), static_cast(ptr), static_cast(ptr) + len); return ret; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index a6f8192a2f..c2167d58ce 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -107,6 +107,7 @@ transResults() MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"), + MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -220,6 +221,7 @@ transResults() MAKE_ERROR(terADDRESS_COLLISION, "Failed to allocate an unique account address."), MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), MAKE_ERROR(terLOCKED, "Fund is locked."), + MAKE_ERROR(terNO_PERMISSION, "No permission to perform requested operation."), MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."), }; diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index b926bdf0e5..e4d4c4b03c 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -30,6 +30,9 @@ TxFormats::getCommonFields() {sfSigners, SoeOptional}, // submit_multisigned {sfNetworkID, SoeOptional}, {sfDelegate, SoeOptional}, + {sfSponsor, SoeOptional}, + {sfSponsorFlags, SoeOptional}, + {sfSponsorSignature, SoeOptional}, }; return kCommonFields; } diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index 0373706e84..b743598a76 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -23,16 +23,16 @@ namespace xrpl { TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) - : transactionID_(txid), ledgerSeq_(ledger), nodes_(obj.getFieldArray(sfAffectedNodes)) + : transactionID_(txid) + , ledgerSeq_(ledger) + , index_(obj.getFieldU32(sfTransactionIndex)) + , result_(obj.getFieldU8(sfTransactionResult)) + , nodes_([&obj] { + auto const affectedNodes = dynamic_cast(obj.peekAtPField(sfAffectedNodes)); + XRPL_ASSERT(affectedNodes, "xrpl::TxMeta::TxMeta(STObject) : type cast succeeded"); + return affectedNodes != nullptr ? *affectedNodes : obj.getFieldArray(sfAffectedNodes); + }()) { - result_ = obj.getFieldU8(sfTransactionResult); - index_ = obj.getFieldU32(sfTransactionIndex); - - auto affectedNodes = dynamic_cast(obj.peekAtPField(sfAffectedNodes)); - XRPL_ASSERT(affectedNodes, "xrpl::TxMeta::TxMeta(STObject) : type cast succeeded"); - if (affectedNodes != nullptr) - nodes_ = *affectedNodes; - setAdditionalFields(obj); } diff --git a/src/libxrpl/protocol/UintTypes.cpp b/src/libxrpl/protocol/UintTypes.cpp index 486c11ba45..e1b8895f44 100644 --- a/src/libxrpl/protocol/UintTypes.cpp +++ b/src/libxrpl/protocol/UintTypes.cpp @@ -64,7 +64,7 @@ to_string(Currency const& currency) bool toCurrency(Currency& currency, std::string const& code) { - if (code.empty() || (code.compare(systemCurrencyCode()) == 0)) + if (code.empty() || code == systemCurrencyCode()) { currency = beast::kZero; return true; diff --git a/src/libxrpl/protocol/tokens.cpp b/src/libxrpl/protocol/tokens.cpp index 21984c67e7..bd0f54c3ae 100644 --- a/src/libxrpl/protocol/tokens.cpp +++ b/src/libxrpl/protocol/tokens.cpp @@ -160,15 +160,16 @@ digest2(Args const&... args) return digest(digest(args...)); } -/** Calculate a 4-byte checksum of the data - - The checksum is calculated as the first 4 bytes - of the SHA256 digest of the message. This is added - to the base58 encoding of identifiers to detect - user error in data entry. - - @note This checksum algorithm is part of the client API -*/ +/** + * Calculate a 4-byte checksum of the data + * + * The checksum is calculated as the first 4 bytes + * of the SHA256 digest of the message. This is added + * to the base58 encoding of identifiers to detect + * user error in data entry. + * + * @note This checksum algorithm is part of the client API + */ static void checksum(void* out, void const* message, std::size_t size) { diff --git a/src/libxrpl/rdb/SociDB.cpp b/src/libxrpl/rdb/SociDB.cpp index 06e58d373f..2c3fb1bde1 100644 --- a/src/libxrpl/rdb/SociDB.cpp +++ b/src/libxrpl/rdb/SociDB.cpp @@ -17,7 +17,7 @@ #include #include #include -#if defined(__clang__) +#ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" #endif @@ -188,14 +188,15 @@ convert(std::string const& from, soci::blob& to) namespace { -/** Run a thread to checkpoint the write ahead log (wal) for - the given soci::session every 1000 pages. This is only implemented - for sqlite databases. - - Note: According to: https://www.sqlite.org/wal.html#ckpt this - is the default behavior of sqlite. We may be able to remove this - class. -*/ +/** + * Run a thread to checkpoint the write ahead log (wal) for + * the given soci::session every 1000 pages. This is only implemented + * for sqlite databases. + * + * Note: According to: https://www.sqlite.org/wal.html#ckpt this + * is the default behavior of sqlite. We may be able to remove this + * class. + */ class WALCheckpointer : public Checkpointer { @@ -213,6 +214,12 @@ public: if (auto [conn, keepAlive] = getConnection(); conn) { (void)keepAlive; + // The checkpointer is identified to the C callback by an integer id + // (resolved via checkpointerFromId) rather than a raw `this`, so it + // cannot dangle if the checkpointer is destroyed. Passing the id + // through sqlite's void* user-data requires an integer-to-pointer + // cast. + // NOLINTNEXTLINE(performance-no-int-to-ptr) sqlite_api::sqlite3_wal_hook(conn, &sqliteWALHook, reinterpret_cast(id_)); } } @@ -335,6 +342,6 @@ makeCheckpointer( } // namespace xrpl -#if defined(__clang__) +#ifdef __clang__ #pragma clang diagnostic pop #endif diff --git a/src/libxrpl/shamap/SHAMap.cpp b/src/libxrpl/shamap/SHAMap.cpp index 8a521f6a47..2483e6f6e1 100644 --- a/src/libxrpl/shamap/SHAMap.cpp +++ b/src/libxrpl/shamap/SHAMap.cpp @@ -838,6 +838,7 @@ SHAMap::getHash() const auto hash = root_->getHash(); if (hash.isZero()) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) const_cast(*this).unshare(); hash = root_->getHash(); } @@ -918,17 +919,18 @@ SHAMap::fetchRoot(SHAMapHash const& hash, SHAMapSyncFilter const* filter) return false; } -/** Replace a node with a shareable node. - - This code handles two cases: - - 1) An unshared, unshareable node needs to be made shareable - so immutable SHAMap's can have references to it. - 2) An unshareable node is shared. This happens when you make - a mutable snapshot of a mutable SHAMap. - - @note The node must have already been unshared by having the caller - first call SHAMapTreeNode::unshare(). +/** + * Replace a node with a shareable node. + * + * This code handles two cases: + * + * 1) An unshared, unshareable node needs to be made shareable + * so immutable SHAMap's can have references to it. + * 2) An unshareable node is shared. This happens when you make + * a mutable snapshot of a mutable SHAMap. + * + * @note The node must have already been unshared by having the caller + * first call SHAMapTreeNode::unshare(). */ SHAMapTreeNodePtr SHAMap::writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const diff --git a/src/libxrpl/shamap/SHAMapSync.cpp b/src/libxrpl/shamap/SHAMapSync.cpp index 1f38049abe..cc30426f9d 100644 --- a/src/libxrpl/shamap/SHAMapSync.cpp +++ b/src/libxrpl/shamap/SHAMapSync.cpp @@ -300,10 +300,11 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn) mn.deferred = 0; } -/** Get a list of node IDs and hashes for nodes that are part of this SHAMap - but not available locally. The filter can hold alternate sources of - nodes that are not permanently stored locally -*/ +/** + * Get a list of node IDs and hashes for nodes that are part of this SHAMap + * but not available locally. The filter can hold alternate sources of + * nodes that are not permanently stored locally + */ std::vector> SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter) { @@ -720,7 +721,8 @@ SHAMap::deepCompare(SHAMap& other) const return true; } -/** Does this map have this inner node? +/** + * Does this map have this inner node? */ bool SHAMap::hasInnerNode(SHAMapNodeID const& targetNodeID, SHAMapHash const& targetNodeHash) const @@ -742,7 +744,8 @@ SHAMap::hasInnerNode(SHAMapNodeID const& targetNodeID, SHAMapHash const& targetN return (node->isInner()) && (node->getHash() == targetNodeHash); } -/** Does this map have this leaf node? +/** + * Does this map have this leaf node? */ bool SHAMap::hasLeafNode(uint256 const& tag, SHAMapHash const& targetNodeHash) const diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 0f6988543e..4b562692d7 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include #include +#include #include #include #include @@ -54,7 +56,9 @@ namespace xrpl { -/** Performs early sanity checks on the txid */ +/** + * Performs early sanity checks on the txid + */ NotTEC preflight0(PreflightContext const& ctx, std::uint32_t flagMask) { @@ -109,7 +113,8 @@ preflight0(PreflightContext const& ctx, std::uint32_t flagMask) namespace detail { -/** Checks the validity of the transactor signing key. +/** + * Checks the validity of the transactor signing key. * * Normally called from preflight1. */ @@ -167,7 +172,61 @@ preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::J } // namespace detail -/** Performs early sanity checks on the account and fee fields */ +static NotTEC +preflight1Sponsor(PreflightContext const& ctx) +{ + bool const hasSponsor = ctx.tx.isFieldPresent(sfSponsor); + bool const hasSponsorFlags = ctx.tx.isFieldPresent(sfSponsorFlags); + bool const hasSponsorSig = ctx.tx.isFieldPresent(sfSponsorSignature); + + if ((hasSponsor || hasSponsorFlags || hasSponsorSig) && !ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if (hasSponsor != hasSponsorFlags) + { + JLOG(ctx.j.debug()) << "preflight1: sponsor and sponsor flags mismatch"; + return temINVALID_FLAG; + } + if (hasSponsorSig && (!hasSponsor || !hasSponsorFlags)) + { + JLOG(ctx.j.debug()) << "preflight1: sponsor signature without sponsor definition"; + return temMALFORMED; + } + + if (hasSponsorFlags) + { + auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags); + if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0) + { + JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags"; + return temINVALID_FLAG; + } + + // Reserve sponsorship is only permitted for an explicit allow-list of + // transaction types, for v1. All other tx types reject spfSponsorReserve here. + if (isReserveSponsored(ctx.tx)) + { + if (!isReserveSponsorAllowed(ctx.tx.getTxnType())) + { + JLOG(ctx.j.debug()) + << "preflight1: spfSponsorReserve not allowed for this transaction type"; + return temINVALID_FLAG; + } + } + } + + if (hasSponsor && ctx.tx.getAccountID(sfSponsor) == ctx.tx.getAccountID(sfAccount)) + { + JLOG(ctx.j.debug()) << "preflight1: Sponsor account cannot be the same as the account"; + return temMALFORMED; + } + + return tesSUCCESS; +} + +/** + * Performs early sanity checks on the account and fee fields + */ NotTEC Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) { @@ -230,10 +289,15 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value()) return temINVALID_INNER_BATCH; + if (auto const ter = preflight1Sponsor(ctx); !isTesSuccess(ter)) + return ter; + return tesSUCCESS; } -/** Checks whether the signature appears valid */ +/** + * Checks whether the signature appears valid + */ NotTEC Transactor::preflight2(PreflightContext const& ctx) { @@ -343,6 +407,44 @@ Transactor::checkPermission( return tesSUCCESS; } +NotTEC +Transactor::checkSponsor(ReadView const& view, STTx const& tx) +{ + if (!tx.isFieldPresent(sfSponsor)) + return tesSUCCESS; + + // Reserve sponsorship with permissioned delegation is disallowed. + if (tx.isFieldPresent(sfDelegate) && isReserveSponsored(tx)) + return temINVALID; + + if (!view.exists(keylet::account(tx.getAccountID(sfSponsor)))) + return terNO_ACCOUNT; + + // Skip Sponsorship existence checks if the sponsor has signed the transaction - this + // transaction is valid regardless of the Sponsorship object. + // The use of the Sponsorship object is properly handled in + // getFeePayer/checkReserve/increaseOwnerCount/decreaseOwnerCount. + if (tx.isFieldPresent(sfSponsorSignature)) + return tesSUCCESS; + + // If the transaction contains sfDelegate, the Sponsorship object should be + // between the sponsor and the delegate. + auto const sponsorshipSle = + view.read(keylet::sponsorship(tx.getAccountID(sfSponsor), tx.getInitiator())); + + // sponsorship object missing for pre-funded (no co-signing) tx + if (!sponsorshipSle) + return terNO_PERMISSION; + + if (isFeeSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee)) + return terNO_PERMISSION; + + if (isReserveSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve)) + return terNO_PERMISSION; + + return tesSUCCESS; +} + XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -351,6 +453,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) // The computation has two parts: // * The base fee, which is the same for most transactions. // * The additional cost of each multisignature on the transaction. + // * The additional cost of each multisignature on the sponsor. XRPAmount const baseFee = view.fees().base; // Each signer adds one more baseFee to the minimum required fee @@ -358,7 +461,15 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) std::size_t const signerCount = tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0; - return baseFee + (signerCount * baseFee); + std::size_t sponsorSignerCount = 0; + if (tx.isFieldPresent(sfSponsorSignature)) + { + auto const sponsorObj = tx.getFieldObject(sfSponsorSignature); + if (sponsorObj.isFieldPresent(sfSigners)) + sponsorSignerCount += sponsorObj.getFieldArray(sfSigners).size(); + } + + return baseFee + ((signerCount + sponsorSignerCount) * baseFee); } XRPAmount @@ -437,12 +548,51 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (feePaid == beast::kZero) return tesSUCCESS; - auto const id = ctx.tx.getFeePayer(); - auto const sle = ctx.view.read(keylet::account(id)); - if (!sle) - return terNO_ACCOUNT; + auto const feePayer = getFeePayer(ctx.view, ctx.tx); + auto const payerSle = ctx.view.read(feePayer.keylet); - auto const balance = (*sle)[sfBalance].xrp(); + if (!payerSle) + { + if (feePayer.type == FeePayerType::SponsorPreFunded) + { + // Sanity check: already checked in checkSponsor + return tefINTERNAL; // LCOV_EXCL_LINE + } + + return terNO_ACCOUNT; + } + + XRPAmount maxSpendable = beast::kZero; + + if (feePayer.type == FeePayerType::SponsorPreFunded) + { + if (payerSle->getType() != ltSPONSORSHIP) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (payerSle->isFieldPresent(feePayer.balanceField)) + maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + + if (payerSle->isFieldPresent(sfMaxFee)) + { + auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp(); + maxSpendable = std::min(maxSpendable, cap); + } + } + else + { + if (payerSle->getType() != ltACCOUNT_ROOT) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (feePayer.type == FeePayerType::SponsorCoSigned) + { + auto const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j); + maxSpendable = payerSle->getFieldAmount(sfBalance).xrp() - sponsorReserve; + } + else + { + maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + } + } // NOTE: Because preclaim evaluates against a static readview, it // does not reflect fee deductions from other transactions paid by @@ -451,12 +601,12 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) // transactions, this check may pass optimistically. // The fee shortfall will be handled by the Transactor::reset mechanism, // which caps the fee to the remaining actual balance. - if (balance < feePaid) + if (maxSpendable < feePaid) { - JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(balance) + JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(maxSpendable) << " paid=" << to_string(feePaid); - if ((balance > beast::kZero) && !ctx.view.open()) + if ((maxSpendable > beast::kZero) && !ctx.view.open()) { // Closed ledger, non-zero balance, less than fee return tecINSUFF_FEE; @@ -473,16 +623,72 @@ Transactor::payFee() { auto const feePaid = ctx_.tx[sfFee].xrp(); - auto const feePayer = ctx_.tx.getFeePayer(); - auto const sle = view().peek(keylet::account(feePayer)); + auto const feePayer = getFeePayer(view(), ctx_.tx); + auto const sle = view().peek(feePayer.keylet); + + JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id); + if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - // Deduct the fee, so it's not available during the transaction. - // Will only write the account back if the transaction succeeds. - sle->setFieldAmount(sfBalance, sle->getFieldAmount(sfBalance) - feePaid); - if (feePayer != accountID_) - view().update(sle); // done in `apply()` for the account + if (feePaid == beast::kZero) + return tesSUCCESS; + + XRPAmount balance = beast::kZero; + if (sle->isFieldPresent(feePayer.balanceField)) + { + balance = sle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return tefINTERNAL; // LCOV_EXCL_LINE + } + + // A co-signed sponsor pays the fee out of its own account balance, but must + // never be charged into its account reserve, and a pre-funded sponsorship's + // fee is capped by sfMaxFee. Mirror the spendable amount computed in + // checkFee() so both limits are enforced on the apply path too. + XRPAmount spendable = balance; + if (feePayer.type == FeePayerType::SponsorCoSigned) + { + auto const sponsorReserve = accountReserve(view(), sle, j_); + // max(balance - reserve, 0) with overflow handling + spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero; + } + else if (feePayer.type == FeePayerType::SponsorPreFunded && sle->isFieldPresent(sfMaxFee)) + { + auto const cap = sle->getFieldAmount(sfMaxFee).xrp(); + spendable = std::min(spendable, cap); + } + + // Only sponsor fee-payers reject here on insufficient funds. For an + // ordinary account, the fee falls through and is capped by reset(), which + // caps to the account's balance. That capping is wrong for sponsors: a + // co-signed sponsor would be charged into its own reserve, and a prefunded + // sponsorship's fee amount should be rejected rather than partially spent. + if (feePaid > spendable && + (feePayer.type == FeePayerType::SponsorPreFunded || + feePayer.type == FeePayerType::SponsorCoSigned)) + { + if ((spendable > beast::kZero) && !view().open()) + return tecINSUFF_FEE; + + return terINSUF_FEE_B; + } + + auto const feeAmountAfter = balance - feePaid; + + if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount) + { + // Because ltSponsorship.sfFeeAmount is soeOptional + sle->makeFieldAbsent(feePayer.balanceField); + } + else + { + sle->setFieldAmount(feePayer.balanceField, feeAmountAfter); + } + + view().update(sle); // VFALCO Should we call view().rawDestroyXRP() here as well? return tesSUCCESS; @@ -656,7 +862,7 @@ Transactor::ticketDelete( } // Update the Ticket owner's reserve. - adjustOwnerCount(view, sleAccount, -1, j); + decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j); // Remove Ticket from ledger. view.erase(sleTicket); @@ -750,6 +956,21 @@ Transactor::checkSign( return tesSUCCESS; } + if (sigObject.isFieldPresent(sfSponsorSignature)) + { + // Co-signed sponsorship + + // Sanity check: already checked in preflight1 + if (!sigObject.isFieldPresent(sfSponsor)) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorID = sigObject.getAccountID(sfSponsor); + auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature); + if (auto const ret = checkSign(view, flags, std::nullopt, sponsorID, sponsorSignature, j); + !isTesSuccess(ret)) + return ret; + } + // If the pk is empty and not simulate or simulate and signers, // then we must be multi-signing. if (sigObject.isFieldPresent(sfSigners)) @@ -1062,10 +1283,11 @@ removeDeletedTrustLines( } } -/** Reset the context, discarding any changes made and adjust the fee. - - @param fee The transaction fee to be charged. - @return A pair containing the transaction result and the actual fee charged. +/** + * Reset the context, discarding any changes made and adjust the fee. + * + * @param fee The transaction fee to be charged. + * @return A pair containing the transaction result and the actual fee charged. */ std::pair Transactor::reset(XRPAmount fee) @@ -1079,11 +1301,38 @@ Transactor::reset(XRPAmount fee) if (!txnAcct) return {tefINTERNAL, beast::kZero}; - auto const payerSle = view().peek(keylet::account(ctx_.tx.getFeePayer())); + auto const feePayer = getFeePayer(view(), ctx_.tx); + auto const payerSle = view().peek(feePayer.keylet); + if (!payerSle) return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE - auto const balance = payerSle->getFieldAmount(sfBalance).xrp(); + XRPAmount balance = beast::kZero; + if (payerSle->isFieldPresent(feePayer.balanceField)) + { + balance = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE + } + + if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee)) + { + auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp(); + fee = std::min(fee, cap); + } + + // A co-signed sponsor must never be charged into its own account reserve, + // so the fee is capped to the balance above the reserve rather than to the + // full balance. + XRPAmount spendable = balance; + if (feePayer.type == FeePayerType::SponsorCoSigned) + { + auto const sponsorReserve = accountReserve(view(), payerSle, j_); + // max(balance - reserve, 0) with overflow handling + spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero; + } // balance should have already been checked in checkFee / preFlight. XRPL_ASSERT( @@ -1093,8 +1342,8 @@ Transactor::reset(XRPAmount fee) // We retry/reject the transaction if the account balance is zero or // we're applying against an open ledger and the balance is less than // the fee - if (fee > balance) - fee = balance; + if (fee > spendable) + fee = spendable; // Since we reset the context, we need to charge the fee and update // the account's sequence number (or consume the Ticket) again. @@ -1102,7 +1351,17 @@ Transactor::reset(XRPAmount fee) // If for some reason we are unable to consume the ticket or sequence // then the ledger is corrupted. Rather than make things worse we // reject the transaction. - payerSle->setFieldAmount(sfBalance, balance - fee); + auto const feeAmountAfter = balance - fee; + if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount) + { + // Because ltSponsorship.sfFeeAmount is soeOptional + payerSle->makeFieldAbsent(feePayer.balanceField); + } + else + { + payerSle->setFieldAmount(feePayer.balanceField, feeAmountAfter); + } + TER const ter{consumeSeqProxy(txnAcct)}; XRPL_ASSERT(isTesSuccess(ter), "xrpl::Transactor::reset : result is tesSUCCESS"); @@ -1116,6 +1375,48 @@ Transactor::reset(XRPAmount fee) return {ter, fee}; } +FeePayer +Transactor::getFeePayer(ReadView const& view, STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx)) + { + auto const sponsorID = tx.getAccountID(sfSponsor); + auto const sponseeID = tx.getInitiator(); + auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID); + + // if pre-funded sponsorship exists, prefer it + if (view.exists(sponsorshipKeylet)) + { + // pre funded + return FeePayer{ + .id = sponsorID, + .keylet = sponsorshipKeylet, + .balanceField = sfFeeAmount, + .type = FeePayerType::SponsorPreFunded}; + } + + // Checked in Transactor::checkSponsor + XRPL_ASSERT( + tx.isFieldPresent(sfSponsorSignature), + "xrpl::getFeePayer has sponsor signature without a sponsorship object"); + + // co-signed + return FeePayer{ + .id = sponsorID, + .keylet = keylet::account(sponsorID), + .balanceField = sfBalance, + .type = FeePayerType::SponsorCoSigned}; + } + + AccountID const payerID = tx.getInitiator(); + auto const payerAccountKeylet = keylet::account(payerID); + auto const payerType = + tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account; + + return FeePayer{ + .id = payerID, .keylet = payerAccountKeylet, .balanceField = sfBalance, .type = payerType}; +} + // The sole purpose of this function is to provide a convenient, named // location to set a breakpoint, to be used when replaying transactions. void diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp index d85c4cfc40..f93b19a158 100644 --- a/src/libxrpl/tx/apply.cpp +++ b/src/libxrpl/tx/apply.cpp @@ -177,9 +177,9 @@ applyBatchTransactions( int applied = 0; - for (STObject rb : batchTxn.getFieldArray(sfRawTransactions)) + for (auto const& stx : batchTxn.getBatchTransactions()) { - auto const result = applyOneTransaction(STTx{std::move(rb)}); + auto const result = applyOneTransaction(*stx); XRPL_ASSERT( result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)), "Outer Batch failure, inner transaction should not be applied"); diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index caaacfd010..5af4f621a7 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -181,6 +181,9 @@ invokePreclaim(PreclaimContext const& ctx) if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx)) return result; + if (NotTEC const result = T::checkSponsor(ctx.view, ctx.tx)) + return result; + if (NotTEC const result = Transactor::invokeCheckPermission(ctx.view, ctx.tx)) return result; diff --git a/src/libxrpl/tx/invariants/DirectoryInvariant.cpp b/src/libxrpl/tx/invariants/DirectoryInvariant.cpp index 1624a19830..b850f02ab5 100644 --- a/src/libxrpl/tx/invariants/DirectoryInvariant.cpp +++ b/src/libxrpl/tx/invariants/DirectoryInvariant.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include +#include #include namespace xrpl { @@ -88,17 +90,15 @@ ValidBookDirectory::finalize( return false; } - for (auto const& rootIndex : rootIndexes_) - { + return std::ranges::all_of(rootIndexes_, [&](auto const& rootIndex) { auto const root = view.read(Keylet(ltDIR_NODE, rootIndex)); if (!root) { JLOG(j.fatal()) << "Invariant failed: book directory root missing"; return false; } - } - - return true; + return true; + }); } } // namespace xrpl diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp b/src/libxrpl/tx/invariants/FreezeInvariant.cpp index 0293d42e97..0a604d4c39 100644 --- a/src/libxrpl/tx/invariants/FreezeInvariant.cpp +++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp @@ -16,6 +16,7 @@ #include #include +#include #include namespace xrpl { @@ -73,8 +74,8 @@ TransfersNotFrozen::finalize( */ [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze); - for (auto const& [issue, changes] : balanceChanges_) - { + return std::ranges::all_of(balanceChanges_, [&](auto const& entry) { + auto const& [issue, changes] = entry; auto const issuerSle = findIssuer(issue.account, view); // It should be impossible for the issuer to not be found, but check // just in case so xrpld doesn't crash in release. @@ -86,20 +87,11 @@ TransfersNotFrozen::finalize( enforce, "xrpl::TransfersNotFrozen::finalize : enforce " "invariant."); - if (enforce) - { - return false; - } - continue; + return !enforce; } - if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce)) - { - return false; - } - } - - return true; + return validateIssuerChanges(issuerSle, changes, tx, j, enforce); + }); } bool diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 32df44a96b..9b997e06dd 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -148,29 +148,51 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a if (isXRP((*before)[sfAmount])) drops_ -= (*before)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (before->isFieldPresent(sfFeeAmount)) + { + XRPL_ASSERT( + isXRP((*before)[sfFeeAmount]), + "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP"); + drops_ -= (*before)[sfFeeAmount].xrp().drops(); + } + break; default: break; } } - if (after) + if (!after) { - switch (after->getType()) - { - case ltACCOUNT_ROOT: - drops_ += (*after)[sfBalance].xrp().drops(); - break; - case ltPAYCHAN: - if (!isDelete) - drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); - break; - case ltESCROW: - if (!isDelete && isXRP((*after)[sfAmount])) - drops_ += (*after)[sfAmount].xrp().drops(); - break; - default: - break; - } + // LCOV_EXCL_START + UNREACHABLE("xrpl::XRPNotCreated::visitEntry : after can't be null"); + return; + // LCOV_EXCL_STOP + } + switch (after->getType()) + { + case ltACCOUNT_ROOT: + drops_ += (*after)[sfBalance].xrp().drops(); + break; + case ltPAYCHAN: + if (!isDelete) + drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); + break; + case ltESCROW: + if (!isDelete && isXRP((*after)[sfAmount])) + drops_ += (*after)[sfAmount].xrp().drops(); + break; + case ltSPONSORSHIP: + if (!isDelete && after->isFieldPresent(sfFeeAmount)) + { + XRPL_ASSERT( + isXRP((*after)[sfFeeAmount]), + "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP"); + drops_ += (*after)[sfFeeAmount].xrp().drops(); + } + break; + default: + break; } } @@ -467,7 +489,7 @@ AccountRootsDeletedClean::finalize( // feature is enabled. Enabled, or not, though, a fatal-level message will // be logged [[maybe_unused]] bool const enforce = view.rules().enabled(fixCleanup3_2_0) || - view.rules().enabled(featureSingleAssetVault) || + view.rules().enabled(featureSponsor) || view.rules().enabled(featureSingleAssetVault) || view.rules().enabled(featureLendingProtocol); auto const objectExists = [&view, enforce, &j](auto const& keylet) { @@ -515,6 +537,20 @@ AccountRootsDeletedClean::finalize( if (enforce) return false; } + // An account should not be deleted with sponsorship fields + if (after->isFieldPresent(sfSponsoredOwnerCount) || + after->isFieldPresent(sfSponsoringOwnerCount) || + after->isFieldPresent(sfSponsoringAccountCount) || after->isFieldPresent(sfSponsor)) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a sponsorship field"; + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize : " + "deleted account has no sponsorship fields"); + if (enforce) + return false; + } // Simple types for (auto const& [keyletfunc, _1, _2] : kDirectAccountKeylets) { @@ -767,14 +803,49 @@ ValidNewAccountRoot::finalize( //------------------------------------------------------------------------------ +static std::optional +clawbackTrustLineBalanceInHolderTerms( + SLE::const_pointer const& sle, + AccountID const& holder, + AccountID const& issuer, + Currency const& currency) +{ + if (!sle) + return STAmount{Issue{currency, issuer}}; + + if (sle->getType() != ltRIPPLE_STATE || + sle->key() != keylet::trustLine(holder, issuer, currency).key) + { + return std::nullopt; + } + + STAmount balance = sle->getFieldAmount(sfBalance); + if (holder > issuer) + balance.negate(); + balance.get().account = issuer; + return balance; +} + void -ValidClawback::visitEntry(bool, SLE::const_ref before, SLE::const_ref) +ValidClawback::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { if (before && before->getType() == ltRIPPLE_STATE) + { trustlinesChanged_++; + iou_.before = before; + } + + if (!isDelete && after && after->getType() == ltRIPPLE_STATE) + iou_.after = after; if (before && before->getType() == ltMPTOKEN) + { mptokensChanged_++; + mpt_.before = before; + } + + if (!isDelete && after && after->getType() == ltMPTOKEN) + mpt_.after = after; } bool @@ -803,31 +874,109 @@ ValidClawback::finalize( } bool const mptV2Enabled = view.rules().enabled(featureMPTokensV2); + if (trustlinesChanged_ != 0 && mptokensChanged_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: trustline and MPToken both changed."; + if (mptV2Enabled) + return false; + } + if (trustlinesChanged_ == 1 || (mptV2Enabled && mptokensChanged_ == 1)) { - AccountID const issuer = tx.getAccountID(sfAccount); STAmount const& amount = tx.getFieldAmount(sfAmount); - AccountID const& holder = amount.getIssuer(); - STAmount const holderBalance = amount.asset().visit( + + return amount.asset().visit( [&](Issue const& issue) { - return accountHolds( + AccountID const issuer = tx.getAccountID(sfAccount); + AccountID const& holder = amount.getIssuer(); + STAmount const holderBalance = accountHolds( view, holder, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j); + + if (holderBalance.signum() < 0) + { + JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative"; + return false; + } + + if (!iou_.before) + { + JLOG(j.fatal()) + << "Invariant failed: trustline clawback changed the wrong line"; + return !mptV2Enabled; + } + + auto const beforeBalance = clawbackTrustLineBalanceInHolderTerms( + iou_.before, holder, issuer, issue.currency); + auto const afterBalance = clawbackTrustLineBalanceInHolderTerms( + iou_.after, holder, issuer, issue.currency); + if (!beforeBalance || !afterBalance) + { + JLOG(j.fatal()) + << "Invariant failed: trustline clawback changed the wrong line"; + return !mptV2Enabled; + } + + STAmount clawAmount = amount; + clawAmount.get().account = issuer; + if (clawAmount <= beast::kZero) + { + JLOG(j.fatal()) << "Invariant failed: trustline clawback amount is invalid"; + return !mptV2Enabled; + } + + if (*afterBalance > *beforeBalance || + (*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount)) + { + JLOG(j.fatal()) + << "Invariant failed: trustline clawback balance change is invalid"; + return !mptV2Enabled; + } + + return true; }, [&](MPTIssue const& issue) { - return accountHolds( - view, - holder, - issue, - FreezeHandling::IgnoreFreeze, - AuthHandling::IgnoreAuth, - j); - }); + auto const holder = tx[~sfHolder]; + if (!holder) + { + JLOG(j.fatal()) << "Invariant failed: MPT clawback missing holder"; + return !mptV2Enabled; + } - if (holderBalance.signum() < 0) - { - JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative"; - return false; - } + if (!mpt_.before || !mpt_.after) + { + JLOG(j.fatal()) << "Invariant failed: MPT clawback token is missing"; + return !mptV2Enabled; + } + + if (mpt_.before->getAccountID(sfAccount) != *holder || + mpt_.after->getAccountID(sfAccount) != *holder || + (*mpt_.before)[sfMPTokenIssuanceID] != issue.getMptID() || + (*mpt_.after)[sfMPTokenIssuanceID] != issue.getMptID()) + { + JLOG(j.fatal()) << "Invariant failed: MPT clawback changed the wrong token"; + return !mptV2Enabled; + } + + auto const before = mpt_.before->getFieldU64(sfMPTAmount); + auto const after = mpt_.after->getFieldU64(sfMPTAmount); + if (amount.negative() || amount.mantissa() == 0) + { + JLOG(j.fatal()) << "Invariant failed: MPT clawback amount is invalid"; + return !mptV2Enabled; + } + auto const clawAmount = amount.mantissa(); + + // MPT balances are unsigned, so validate the raw holder + // debit instead of routing through accountHolds(). + if (after > before || (before - after) != std::min(before, clawAmount)) + { + JLOG(j.fatal()) + << "Invariant failed: MPT clawback balance change is invalid"; + return !mptV2Enabled; + } + + return true; + }); } } else @@ -881,8 +1030,10 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const // 1. Exactly one of the pseudo-account fields is set. // 2. The sequence number is not changed. // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth - // flags are set. + // flags are set. // 4. The RegularKey is not set. + // 5. The SponsoredOwnerCount, SponsoringOwnerCount, SponsoringAccountCount, Sponsor + // fields are not set. { std::vector const& fields = getPseudoAccountFields(); @@ -908,6 +1059,12 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const { errors_.emplace_back("pseudo-account has a regular key"); } + if (after->isFieldPresent(sfSponsoredOwnerCount) || + after->isFieldPresent(sfSponsoringOwnerCount) || after->isFieldPresent(sfSponsor) || + after->isFieldPresent(sfSponsoringAccountCount)) + { + errors_.emplace_back("pseudo-account has a sponsorship field"); + } } } } diff --git a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp index d239acd417..b70c02947f 100644 --- a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp +++ b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp @@ -15,6 +15,8 @@ #include #include +#include + namespace xrpl { void @@ -127,8 +129,8 @@ ValidLoanBroker::finalize( } } - for (auto const& [brokerID, broker] : brokers_) - { + return std::ranges::all_of(brokers_, [&](auto const& entry) { + auto const& [brokerID, broker] = entry; auto const& after = broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanBroker(brokerID)); @@ -204,8 +206,8 @@ ValidLoanBroker::finalize( return false; } } - } - return true; + return true; + }); } } // namespace xrpl diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index d8f7fcc27d..77c5ad781e 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -403,7 +403,7 @@ ValidMPTIssuance::finalize( } void -ValidMPTPayment::visitEntry(bool, SLE::const_ref before, SLE::const_ref after) +ValidMPTBalanceChanges::visitEntry(bool, SLE::const_ref before, SLE::const_ref after) { if (overflow_) return; @@ -465,7 +465,7 @@ ValidMPTPayment::visitEntry(bool, SLE::const_ref before, SLE::const_ref after) } bool -ValidMPTPayment::finalize( +ValidMPTBalanceChanges::finalize( STTx const& tx, TER const result, XRPAmount const, diff --git a/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp new file mode 100644 index 0000000000..bdf37492f3 --- /dev/null +++ b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp @@ -0,0 +1,158 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +// Add new sponsorship-related invariants implementations +void +SponsorshipOwnerCountsMatch::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) +{ + auto getSponsored = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoredOwnerCount); + return 0; + }; + auto getSponsoring = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringOwnerCount); + return 0; + }; + + auto getOwnerCount = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfOwnerCount); + return 0; + }; + + auto getSponsoredObjectOwnerCount = [&](SLE::const_ref sle) -> std::uint32_t { + if (!sle) + return 0; + switch (sle->getType()) + { + case ltACCOUNT_ROOT: + return 0; + case ltRIPPLE_STATE: { + // A trust line can be reserve-sponsored independently on each + // side, so it may contribute up to two sponsored owner counts. + uint32_t ownerCount = 0; + if (sle->isFieldPresent(sfHighSponsor)) + ownerCount++; + if (sle->isFieldPresent(sfLowSponsor)) + ownerCount++; + return ownerCount; + } + default: + // Every other supported type carries a single sfSponsor field + // and contributes its full owner-count magnitude only when it is + // sponsored. + if (!sle->isFieldPresent(sfSponsor)) + return 0; + return getLedgerEntryOwnerCount(*sle); + } + }; + + // The values are implicitly casted to std::int64_t to calculate deltas. + std::int64_t const beforeSponsored = getSponsored(before); + std::int64_t const afterSponsored = getSponsored(after); + std::int64_t const beforeSponsoring = getSponsoring(before); + std::int64_t const afterSponsoring = getSponsoring(after); + + std::int64_t const beforeSponsoredObjectOwnerCount = getSponsoredObjectOwnerCount(before); + std::int64_t const afterSponsoredObjectOwnerCount = + isDelete ? 0 : getSponsoredObjectOwnerCount(after); + + deltaSponsoredOwnerCount_ += (afterSponsored - beforeSponsored); + deltaSponsoringOwnerCount_ += (afterSponsoring - beforeSponsoring); + + deltaSponsoredObjectOwnerCount_ += + (afterSponsoredObjectOwnerCount - beforeSponsoredObjectOwnerCount); + + if (getOwnerCount(after) < getSponsored(after)) + ownerCountBelowSponsored_ += 1; +} + +bool +SponsorshipOwnerCountsMatch::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) const +{ + if (deltaSponsoredOwnerCount_ != deltaSponsoringOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredOwnerCount does not " + "equal SponsoringOwnerCount delta."; + return false; + } + + if (ownerCountBelowSponsored_ > 0) + { + JLOG(j.fatal()) + << "Invariant failed: OwnerCount must be greater than or equal to SponsoredOwnerCount."; + return false; + } + + if (deltaSponsoredObjectOwnerCount_ != deltaSponsoredOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredObjectOwnerCount does not " + "equal SponsoredOwnerCount delta."; + return false; + } + + return true; +} + +void +SponsorshipAccountCountMatchesField::visitEntry(bool, SLE::const_ref before, SLE::const_ref after) +{ + auto getSponsoringAccountCount = [](SLE::const_ref sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringAccountCount); + return 0; + }; + + auto hasSponsorField = [](SLE::const_ref sle) -> bool { + return sle && sle->getType() == ltACCOUNT_ROOT && sle->isFieldPresent(sfSponsor); + }; + + std::int64_t const beforeCount = getSponsoringAccountCount(before); + std::int64_t const afterCount = getSponsoringAccountCount(after); + deltaSponsoringAccountCount_ += (afterCount - beforeCount); + + int const beforePresent = hasSponsorField(before) ? 1 : 0; + int const afterPresent = hasSponsorField(after) ? 1 : 0; + deltaSponsorFieldPresence_ += (afterPresent - beforePresent); +} + +bool +SponsorshipAccountCountMatchesField::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) const +{ + if (deltaSponsoringAccountCount_ != deltaSponsorFieldPresence_) + { + JLOG(j.fatal()) << "Invariant failed: Net delta of SponsoringAccountCount does not " + "match net delta of sfSponsor presence."; + return false; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index 84814977db..a9ba0ec874 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -222,7 +222,10 @@ ValidVault::deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const if (!ret.has_value() || !vaultAsset.native()) return ret; - if (auto const delegate = tx[~sfDelegate]; delegate.has_value() && *delegate != tx[sfAccount]) + // Only add the fee back if tx[sfAccount] actually paid it. When the fee is + // paid by someone else (a delegate or a fee sponsor), the + // account's XRP balance moved only by the vault amount. + if (tx.getFeePayerID() != tx[sfAccount]) return ret; ret->delta += fee.drops(); @@ -1044,10 +1047,8 @@ ValidVault::finalize( case ttLOAN_SET: case ttLOAN_MANAGE: - case ttLOAN_PAY: { - // TBD + case ttLOAN_PAY: return true; - } default: // LCOV_EXCL_START diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index 5f46b30339..0e94d21aa6 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -66,13 +66,14 @@ protected: bool const ownerPaysTransferFee_; // Mark as inactive (dry) if too many offers are consumed bool inactive_ = false; - /** Number of offers consumed or partially consumed the last time - the step ran, including expired and unfunded offers. - - N.B. This is not the total number offers consumed by this step for the - entire payment, it is only the number the last time it ran. Offers may - be partially consumed multiple times during a payment. - */ + /** + * Number of offers consumed or partially consumed the last time + * the step ran, including expired and unfunded offers. + * + * N.B. This is not the total number offers consumed by this step for the + * entire payment, it is only the number the last time it ran. Offers may + * be partially consumed multiple times during a payment. + */ std::uint32_t offersUsed_ = 0; // If set, AMM liquidity might be available // if AMM offer quality is better than CLOB offer @@ -731,7 +732,7 @@ BookStep::forEachOffer( // Create MPToken for the offer's owner. No need to check // for the reserve since the offer is removed if it is consumed. // Therefore, the owner count remains the same. - if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, j_); + if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, {}, j_); !isTesSuccess(err)) { return true; diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp index a47cfa15a5..0a0f6a9f27 100644 --- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp @@ -410,7 +410,8 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio // for the reserve since the offer doesn't go on the books // if crossed. Insufficient reserve is allowed if the offer // crossed. See CreateOffer::applyGuts() for reserve check. - if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err)) + if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, {}, j_); + !isTesSuccess(err)) { JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT"; resetCache(srcDebtDir); diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 254733a618..0055fce403 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -35,7 +35,6 @@ #include namespace xrpl { - bool AccountDelete::checkExtraFeatures(PreflightContext const& ctx) { @@ -268,6 +267,15 @@ AccountDelete::preclaim(PreclaimContext const& ctx) if (cp) return tecHAS_OBLIGATIONS; + if (sleAccount->isFieldPresent(sfSponsor)) + { + if (dst != sleAccount->getAccountID(sfSponsor)) + return tecNO_SPONSOR_PERMISSION; + } + if (sleAccount->isFieldPresent(sfSponsoringOwnerCount) || + sleAccount->isFieldPresent(sfSponsoringAccountCount)) + return tecHAS_OBLIGATIONS; + // We don't allow an account to be deleted if its sequence number // is within 256 of the current ledger. This prevents replay of old // transactions if this account is resurrected after it is deleted. @@ -394,6 +402,35 @@ AccountDelete::doApply() (*src)[sfBalance] = (*src)[sfBalance] - remainingBalance; ctx_.deliver(remainingBalance); + if (src->isFieldPresent(sfSponsor)) + { + auto const sponsorID = src->getAccountID(sfSponsor); + auto sponsorSle = view().peek(keylet::account(sponsorID)); + + if (!sponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount); + + XRPL_ASSERT( + sponsoringAccountCount != 0, + "xrpl::AccountDelete::doApply : sponsoring account count is present"); + if (sponsoringAccountCount == 0) + { + // sanity check + // Since sfSponsoringAccountCount is set to soeDEFAULT, the field will not be + // present with a value of 0. + return tefINTERNAL; // LCOV_EXCL_LINE + } + sponsorSle->at(sfSponsoringAccountCount) = sponsoringAccountCount - 1; + view().update(sponsorSle); + + // Following line might look redundant, but without it, sfSponsor + // would end up remaining in after-ltAccountRoot during the + // InvariantCheck. + src->makeFieldAbsent(sfSponsor); + } + XRPL_ASSERT( (*src)[sfBalance] == XRPAmount(0), "xrpl::AccountDelete::doApply : source balance is zero"); diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp b/src/libxrpl/tx/transactors/account/SignerListSet.cpp index 2f95d32664..799c292bfc 100644 --- a/src/libxrpl/tx/transactors/account/SignerListSet.cpp +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -147,9 +148,7 @@ SignerListSet::preCompute() Transactor::preCompute(); } -// The return type is signed so it is compatible with the 3rd argument -// of adjustOwnerCount() (which must be signed). -static int +static std::uint32_t signerCountBasedOwnerCountDelta(std::size_t entryCount, Rules const& rules) { // We always compute the full change in OwnerCount, taking into account: @@ -165,7 +164,7 @@ signerCountBasedOwnerCountDelta(std::size_t entryCount, Rules const& rules) // units. A SignerList with 8 entries would cost 10 OwnerCount units. // // The static_cast should always be safe since entryCount should always - // be in the range from 1 to 32. + // be in the range from 1 to 32, so the result is always positive. // We've got a lot of room to grow. XRPL_ASSERT( entryCount >= STTx::kMinMultiSigners, @@ -196,12 +195,11 @@ removeSignersFromLedger( // There are two different ways that the OwnerCount could be managed. // If the lsfOneOwnerCount bit is set then remove just one owner count. // Otherwise use the pre-MultiSignReserve amendment calculation. - int removeFromOwnerCount = -1; + std::uint32_t removeFromOwnerCount = 1; if (!signers->isFlag(lsfOneOwnerCount)) { STArray const& actualList = signers->getFieldArray(sfSignerEntries); - removeFromOwnerCount = - signerCountBasedOwnerCountDelta(actualList.size(), view.rules()) * -1; + removeFromOwnerCount = signerCountBasedOwnerCountDelta(actualList.size(), view.rules()); } // Remove the node from the account directory. @@ -215,8 +213,8 @@ removeSignersFromLedger( // LCOV_EXCL_STOP } - adjustOwnerCount( - view, view.peek(accountKeylet), removeFromOwnerCount, registry.getJournal("View")); + decreaseOwnerCountForObject( + view, view.peek(accountKeylet), signers, removeFromOwnerCount, registry.getJournal("View")); view.erase(signers); @@ -315,19 +313,20 @@ SignerListSet::replaceSignerList() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - // Compute new reserve. Verify the account has funds to meet the reserve. - std::uint32_t const oldOwnerCount{(*sle)[sfOwnerCount]}; - static constexpr int kAddedOwnerCount = 1; std::uint32_t const flags{lsfOneOwnerCount}; - XRPAmount const newReserve{view().fees().accountReserve(oldOwnerCount + kAddedOwnerCount)}; - // We check the reserve against the starting balance because we want to // allow dipping into the reserve to pay fees. This behavior is consistent // with TicketCreate. - if (preFeeBalance_ < newReserve) - return tecINSUFFICIENT_RESERVE; + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sle, + preFeeBalance_, + {.ownerCountDelta = kAddedOwnerCount}, + ctx_.journal); + !isTesSuccess(ret)) + return ret; // Everything's ducky. Add the ltSIGNER_LIST to the ledger. auto signerList = std::make_shared(signerListKeylet); @@ -349,7 +348,8 @@ SignerListSet::replaceSignerList() // If we succeeded, the new entry counts against the // creator's reserve. - adjustOwnerCount(view(), sle, kAddedOwnerCount, viewJ); + increaseOwnerCount(ctx_.getApplyViewContext(), sle, kAddedOwnerCount, viewJ); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), signerList); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index 9092ae4140..e03cb56fd5 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -238,35 +238,35 @@ claimHelper( } /** - Handle a new attestation event. - - Attempt to add the given attestation and reconcile with the current - signer's list. Attestations that are not part of the current signer's - list will be removed. - - @param claimAtt New attestation to add. It will be added if it is not - already part of the collection, or attests to a larger value. - - @param quorum Min weight required for a quorum - - @param signersList Map from signer's account id (derived from public keys) - to the weight of that key. - - @return optional reward accounts. If after handling the new attestation - there is a quorum for the amount specified on the new attestation, then - return the reward accounts for that amount, otherwise return a nullopt. - Note that if the signer's list changes and there have been `commit` - transactions of different amounts then there may be a different subset that - has reached quorum. However, to "trigger" that subset would require adding - (or re-adding) an attestation that supports that subset. - - The reason for using a nullopt instead of an empty vector when a quorum is - not reached is to allow for an interface where a quorum is reached but no - rewards are distributed. - - @note This function is not called `add` because it does more than just - add the new attestation (in fact, it may not add the attestation at - all). Instead, it handles the event of a new attestation. + * Handle a new attestation event. + * + * Attempt to add the given attestation and reconcile with the current + * signer's list. Attestations that are not part of the current signer's + * list will be removed. + * + * @param claimAtt New attestation to add. It will be added if it is not + * already part of the collection, or attests to a larger value. + * + * @param quorum Min weight required for a quorum + * + * @param signersList Map from signer's account id (derived from public keys) + * to the weight of that key. + * + * @return optional reward accounts. If after handling the new attestation + * there is a quorum for the amount specified on the new attestation, then + * return the reward accounts for that amount, otherwise return a nullopt. + * Note that if the signer's list changes and there have been `commit` + * transactions of different amounts then there may be a different subset that + * has reached quorum. However, to "trigger" that subset would require adding + * (or re-adding) an attestation that supports that subset. + * + * The reason for using a nullopt instead of an empty vector when a quorum is + * not reached is to allow for an interface where a quorum is reached but no + * rewards are distributed. + * + * @note This function is not called `add` because it does more than just + * add the new attestation (in fact, it may not add the attestation at + * all). Instead, it handles the event of a new attestation. */ struct OnNewAttestationResult { @@ -364,26 +364,27 @@ struct TransferHelperSubmittingAccountInfo STAmount postFeeBalance; }; -/** Transfer funds from the src account to the dst account - - @param psb The payment sandbox. - @param src The source of funds. - @param dst The destination for funds. - @param dstTag Integer destination tag. Used to check if funds should be - transferred to an account with a `RequireDstTag` flag set. - @param claimOwner Owner of the claim ledger object. - @param amt Amount to transfer from the src account to the dst account. - @param canCreate Flag to determine if accounts may be created using this - transfer. - @param depositAuthPolicy Flag to determine if dst can bypass deposit auth if - it is also the claim owner. - @param submittingAccountInfo If the transaction is allowed to dip into the - reserve to pay fees, then this optional will be seated ("commit" - transactions support this, other transactions should not). - @param j Log - - @return tesSUCCESS if payment succeeds, otherwise the error code for the - failure reason. +/** + * Transfer funds from the src account to the dst account + * + * @param psb The payment sandbox. + * @param src The source of funds. + * @param dst The destination for funds. + * @param dstTag Integer destination tag. Used to check if funds should be + * transferred to an account with a `RequireDstTag` flag set. + * @param claimOwner Owner of the claim ledger object. + * @param amt Amount to transfer from the src account to the dst account. + * @param canCreate Flag to determine if accounts may be created using this + * transfer. + * @param depositAuthPolicy Flag to determine if dst can bypass deposit auth if + * it is also the claim owner. + * @param submittingAccountInfo If the transaction is allowed to dip into the + * reserve to pay fees, then this optional will be seated ("commit" + * transactions support this, other transactions should not). + * @param j Log + * + * @return tesSUCCESS if payment succeeds, otherwise the error code for the + * failure reason. */ TER @@ -435,8 +436,7 @@ transferHelper( return tecINTERNAL; // LCOV_EXCL_LINE { - auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); - auto const reserve = psb.fees().accountReserve(ownerCount); + auto const reserve = accountReserve(psb, sleSrc, j); auto const availableBalance = [&]() -> STAmount { STAmount curBal = (*sleSrc)[sfBalance]; @@ -505,21 +505,28 @@ transferHelper( return tecXCHAIN_PAYMENT_FAILED; } -/** Action to take when the transfer from the door account to the dst fails - - @note This is useful to prevent a failed "create account" transaction from - blocking subsequent "create account" transactions. -*/ +/** + * Action to take when the transfer from the door account to the dst fails + * + * @note This is useful to prevent a failed "create account" transaction from + * blocking subsequent "create account" transactions. + */ enum class OnTransferFail { - /** Remove the claim even if the transfer fails */ + /** + * Remove the claim even if the transfer fails + */ RemoveClaim, - /** Keep the claim if the transfer fails */ + /** + * Keep the claim if the transfer fails + */ KeepClaim }; struct FinalizeClaimHelperResult { - /// TER for transfering the payment funds + /** + * TER for transfering the payment funds + */ std::optional mainFundsTer; // TER for transfering the reward funds std::optional rewardTer; @@ -563,33 +570,34 @@ struct FinalizeClaimHelperResult } }; -/** Transfer funds from the door account to the dst and distribute rewards - - @param psb The payment sandbox. - @param bridgeSpc Bridge - @param dst The destination for funds. - @param dstTag Integer destination tag. Used to check if funds should be - transferred to an account with a `RequireDstTag` flag set. - @param claimOwner Owner of the claim ledger object. - @param sendingAmount Amount that was committed on the source chain. - @param rewardPoolSrc Source of the funds for the reward pool (claim owner). - @param rewardPool Amount to split among the rewardAccounts. - @param rewardAccounts Account to receive the reward pool. - @param srcChain Chain where the commit event occurred. - @param sleClaimID sle for the claim id (may be NULL or XChainClaimID or - XChainCreateAccountClaimID). Don't read fields that aren't in common - with those two types and always check for NULL. Remove on success (if - not null). Remove on fail if the onTransferFail flag is removeClaim. - @param onTransferFail Flag to determine if the claim is removed on transfer - failure. This is used for create account transactions where claims - are removed so they don't block future txns. - @param j Log - - @return FinalizeClaimHelperResult. See the comments in this struct for what - the fields mean. The individual ters need to be returned instead of - an overall ter because the caller needs this information if the - attestation list changed or not. -*/ +/** + * Transfer funds from the door account to the dst and distribute rewards + * + * @param psb The payment sandbox. + * @param bridgeSpc Bridge + * @param dst The destination for funds. + * @param dstTag Integer destination tag. Used to check if funds should be + * transferred to an account with a `RequireDstTag` flag set. + * @param claimOwner Owner of the claim ledger object. + * @param sendingAmount Amount that was committed on the source chain. + * @param rewardPoolSrc Source of the funds for the reward pool (claim owner). + * @param rewardPool Amount to split among the rewardAccounts. + * @param rewardAccounts Account to receive the reward pool. + * @param srcChain Chain where the commit event occurred. + * @param sleClaimID sle for the claim id (may be NULL or XChainClaimID or + * XChainCreateAccountClaimID). Don't read fields that aren't in common + * with those two types and always check for NULL. Remove on success (if + * not null). Remove on fail if the onTransferFail flag is removeClaim. + * @param onTransferFail Flag to determine if the claim is removed on transfer + * failure. This is used for create account transactions where claims + * are removed so they don't block future txns. + * @param j Log + * + * @return FinalizeClaimHelperResult. See the comments in this struct for what + * the fields mean. The individual ters need to be returned instead of + * an overall ter because the caller needs this information if the + * attestation list changed or not. + */ FinalizeClaimHelperResult finalizeClaimHelper( @@ -726,24 +734,24 @@ finalizeClaimHelper( } // Remove the claim id from the ledger + decreaseOwnerCountForObject(outerSb, sleOwner, sleClaimID, 1, j); outerSb.erase(sleClaimID); - - adjustOwnerCount(outerSb, sleOwner, -1, j); } } return result; } -/** Get signers list corresponding to the account that owns the bridge - - @param view View to read the signer's list from. - @param sleBridge Sle of the bridge. - @param j Log - - @return map of the signer's list (AccountIDs and weights), the quorum, and - error code -*/ +/** + * Get signers list corresponding to the account that owns the bridge + * + * @param view View to read the signer's list from. + * @param sleBridge Sle of the bridge. + * @param j Log + * + * @return map of the signer's list (AccountIDs and weights), the quorum, and + * error code + */ std::tuple, std::uint32_t, TER> getSignersListAndQuorum(ReadView const& view, SLE const& sleBridge, beast::Journal j) { @@ -1028,7 +1036,7 @@ applyCreateAccountAttestations( // Check reserve auto const balance = (*sleDoor)[sfBalance]; - auto const reserve = psb.fees().accountReserve((*sleDoor)[sfOwnerCount] + 1); + auto const reserve = accountReserve(psb, sleDoor, j, {.ownerCountDelta = 1}); if (balance < reserve) return std::unexpected(tecINSUFFICIENT_RESERVE); @@ -1137,7 +1145,7 @@ applyCreateAccountAttestations( return tecINTERNAL; // LCOV_EXCL_LINE // Reserve was already checked - adjustOwnerCount(psb, sleDoor, 1, j); + increaseOwnerCount(psb, sleDoor, {}, 1, j); psb.insert(createdSleClaimID); psb.update(sleDoor); } @@ -1157,7 +1165,11 @@ toClaim(STTx const& tx) try { - STObject o{tx}; + // Copy just the field bag out of the transaction (explicitly, via the + // STObject base) so it can be reinterpreted as a cross-chain attestation + // below, with sfAccount replaced by sfOtherChainSource. STTx-specific + // state (txType_, tid_) is intentionally not needed here. + STObject o{static_cast(tx)}; o.setAccountID(sfAccount, o[sfOtherChainSource]); return TAttestation(o); } @@ -1435,7 +1447,7 @@ XChainCreateBridge::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); + auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -1480,7 +1492,7 @@ XChainCreateBridge::doApply() (*sleBridge)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + increaseOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal); ctx_.view().insert(sleBridge); ctx_.view().update(sleAcct); @@ -1983,8 +1995,7 @@ XChainCreateClaimID::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); - + auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; } @@ -2042,7 +2053,7 @@ XChainCreateClaimID::doApply() (*sleClaimID)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + increaseOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal); ctx_.view().insert(sleClaimID); ctx_.view().update(sleBridge); diff --git a/src/libxrpl/tx/transactors/check/CheckCancel.cpp b/src/libxrpl/tx/transactors/check/CheckCancel.cpp index d966772191..f4602f98c6 100644 --- a/src/libxrpl/tx/transactors/check/CheckCancel.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCancel.cpp @@ -1,10 +1,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -19,6 +21,9 @@ namespace xrpl { NotTEC CheckCancel::preflight(PreflightContext const& ctx) { + if (ctx.rules.enabled(fixCleanup3_3_0) && ctx.tx[sfCheckID] == beast::kZero) + return temMALFORMED; + return tesSUCCESS; } @@ -91,8 +96,7 @@ CheckCancel::doApply() } // If we succeeded, update the check owner's reserve. - auto const sleSrc = view().peek(keylet::account(srcId)); - adjustOwnerCount(view(), sleSrc, -1, viewJ); + decreaseOwnerCountForObject(view(), srcId, sleCheck, 1, viewJ); // Remove check from ledger. view().erase(sleCheck); diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index 36d721c2c0..e4d8f192c0 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -2,12 +2,15 @@ #include #include +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -31,7 +34,7 @@ #include #include -#include +#include #include namespace xrpl { @@ -50,6 +53,9 @@ CheckCash::checkExtraFeatures(xrpl::PreflightContext const& ctx) NotTEC CheckCash::preflight(PreflightContext const& ctx) { + if (ctx.rules.enabled(fixCleanup3_3_0) && ctx.tx[sfCheckID] == beast::kZero) + return temMALFORMED; + // Exactly one of Amount or DeliverMin must be present. auto const optAmount = ctx.tx[~sfAmount]; auto const optDeliverMin = ctx.tx[~sfDeliverMin]; @@ -176,7 +182,7 @@ CheckCash::preclaim(PreclaimContext const& ctx) // once the check is cashed, since the check's reserve will no // longer be required. So, if we're dealing in XRP, we add one // reserve's worth to the available funds. - if (value.native()) + if (value.native() && !sleCheck->isFieldPresent(sfSponsor)) availableFunds += XRPAmount{ctx.view.fees().increment}; if (value > availableFunds) @@ -308,6 +314,8 @@ CheckCash::doApply() // LCOV_EXCL_STOP } + auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck); + // Preclaim already checked that source has at least the requested // funds. // @@ -336,7 +344,7 @@ CheckCash::doApply() // from src's directory, we allow them to send that additional // incremental reserve amount in the transfer. Hence the -1 // argument. - STAmount const srcLiquid{xrpLiquid(psb, srcId, -1, viewJ)}; + STAmount const srcLiquid{xrpLiquid(psb, srcId, sponsorCheckSle ? 0 : -1, viewJ)}; // Now, how much do they need in order to be successful? STAmount const xrpDeliver{ @@ -397,14 +405,25 @@ CheckCash::doApply() STAmount const flowDeliver{ optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; + auto applyViewContext = ApplyViewContext({.view = psb, .tx = ctx_.tx}); + auto const sponsorSle = getTxReserveSponsor(applyViewContext); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Check reserve. Return destination account SLE if enough reserve, // otherwise return nullptr. - auto checkReserve = [&]() -> SLE::pointer { + auto checkDstReserve = [&]() -> SLE::pointer { auto sleDst = psb.peek(keylet::account(accountID_)); // Can the account cover the trust line's or MPT reserve? - if (std::uint32_t const ownerCount = {sleDst->at(sfOwnerCount)}; - preFeeBalance_ < psb.fees().accountReserve(ownerCount + 1)) + if (auto const ret = checkReserve( + applyViewContext, + sleDst, + preFeeBalance_, + *sponsorSle, + {.ownerCountDelta = 1}, + j_); + !isTesSuccess(ret)) { JLOG(j_.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; @@ -438,7 +457,7 @@ CheckCash::doApply() // a. this (destination) account and // b. issuing account (not sending account). - auto const sleDst = checkReserve(); + auto const sleDst = checkDstReserve(); if (sleDst == nullptr) return tecNO_LINE_INSUF_RESERVE; @@ -461,6 +480,7 @@ CheckCash::doApply() Issue(currency, accountID_), // limit of zero 0, // quality in 0, // quality out + *sponsorSle, // sponsor viewJ); // journal !isTesSuccess(ter)) { @@ -503,11 +523,12 @@ CheckCash::doApply() auto const mptokenKey = keylet::mptoken(mptID, accountID_); if (!psb.exists(mptokenKey)) { - auto sleDst = checkReserve(); + auto sleDst = checkDstReserve(); if (sleDst == nullptr) return tecINSUFFICIENT_RESERVE; - if (auto const err = checkCreateMPT(psb, mptID, accountID_, j_); + if (auto const err = + checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_); !isTesSuccess(err)) { return err; @@ -593,7 +614,7 @@ CheckCash::doApply() } // If we succeeded, update the check owner's reserve. - adjustOwnerCount(psb, psb.peek(keylet::account(srcId)), -1, viewJ); + decreaseOwnerCountForObject(psb, srcId, sleCheck, 1, viewJ); // Remove check from ledger. psb.erase(sleCheck); diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp b/src/libxrpl/tx/transactors/check/CheckCreate.cpp index 595bcc4ab1..cb1d81ba4a 100644 --- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -194,13 +195,10 @@ CheckCreate::doApply() // A check counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{view().fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } - + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 1}, ctx_.journal); + !isTesSuccess(ret)) + return ret; // Note that we use the value from the sequence or ticket as the // Check sequence. For more explanation see comments in SeqProxy.h. std::uint32_t const seq = ctx_.tx.getSeqValue(); @@ -253,7 +251,9 @@ CheckCreate::doApply() sleCheck->setFieldU64(sfOwnerNode, *page); } // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sle, 1, viewJ); + + increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, viewJ); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCheck); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp index e0ebdd893a..c0ef7ea9e8 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -93,12 +93,19 @@ CredentialAccept::doApply() if (!sleSubject || !sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - { - STAmount const reserve{ - view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)}; - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto txSponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!txSponsorSle) + return txSponsorSle.error(); // LCOV_EXCL_LINE + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleSubject, + preFeeBalance_, + *txSponsorSle, + {.ownerCountDelta = 1}, + j_); + !isTesSuccess(ret)) + return ret; auto const credType(ctx_.tx[sfCredentialType]); Keylet const credentialKey = keylet::credential(accountID_, issuer, credType); @@ -115,10 +122,16 @@ CredentialAccept::doApply() } sleCred->setFieldU32(sfFlags, lsfAccepted); - view().update(sleCred); - adjustOwnerCount(view(), sleIssuer, -1, j_); - adjustOwnerCount(view(), sleSubject, 1, j_); + // Release the original creation sponsor from the credential (it covered + // the issuer's reserve), then assign the accept tx's sponsor (if any) so + // the credential reflects whoever is now covering the subject's reserve. + decreaseOwnerCountForObject(view(), sleIssuer, sleCred, 1, j_); + removeSponsorFromLedgerEntry(sleCred); + + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCred); + increaseOwnerCount(ctx_.getApplyViewContext(), sleSubject, 1, j_); + view().update(sleCred); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index acca408a95..e902ee73a6 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -7,13 +7,13 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -130,12 +130,10 @@ CredentialCreate::doApply() if (!sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - { - STAmount const reserve{ - view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)}; - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sleIssuer, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; sleCred->setAccountID(sfSubject, subject); sleCred->setAccountID(sfIssuer, accountID_); @@ -153,7 +151,8 @@ CredentialCreate::doApply() return tecDIR_FULL; sleCred->setFieldU64(sfIssuerNode, *page); - adjustOwnerCount(view(), sleIssuer, 1, j_); + increaseOwnerCount(ctx_.getApplyViewContext(), sleIssuer, 1, j_); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCred); } if (subject == accountID_) diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index 32a51555b1..96e6c9e443 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -5,10 +5,10 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -98,11 +98,14 @@ DelegateSet::doApply() if (permissions.empty()) return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const reserve{ - ctx_.view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleOwner, + preFeeBalance_, + {.ownerCountDelta = 1}, + ctx_.journal); + !isTesSuccess(ret)) + return ret; sle = std::make_shared(delegateKey); sle->setAccountID(sfAccount, accountID_); @@ -130,7 +133,8 @@ DelegateSet::doApply() (*sle)[sfDestinationNode] = *destPage; ctx_.view().insert(sle); - adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal); + increaseOwnerCount(ctx_.getApplyViewContext(), sleOwner, 1, ctx_.journal); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sle); return tesSUCCESS; } @@ -170,7 +174,7 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::ref sle, beast::Journal j) if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); + decreaseOwnerCountForObject(view, sleOwner, sle, 1, j); view.erase(sle); diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index 66d059c54d..7c7d35497a 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -332,17 +332,30 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou return err; } - if (auto const err = createMPToken(sb, mptID, accountId, flags); !isTesSuccess(err)) + if (auto const err = createMPToken(sb, mptID, accountId, {}, flags); + !isTesSuccess(err)) return err; // Don't adjust AMM owner count. // It's irrelevant for pseudo-account like AMM. return accountSend( - sb, account, accountId, amount, ctx.journal, WaiveTransferFee::Yes); + sb, + account, + accountId, + amount, + ctx.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); }, // Set AMM flag on AMM trustline [&](Issue const& issue) -> TER { if (auto const res = accountSend( - sb, account, accountId, amount, ctx.journal, WaiveTransferFee::Yes)) + sb, + account, + accountId, + amount, + ctx.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes)) return res; // Set AMM flag on AMM trustline if (!isXRP(amount)) diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 956a1603f3..b3b799421a 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -619,7 +619,13 @@ AMMDeposit::deposit( } auto res = accountSend( - view, accountID_, ammAccount, amountDepositActual, ctx_.journal, WaiveTransferFee::Yes); + view, + accountID_, + ammAccount, + amountDepositActual, + ctx_.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit " << amountDepositActual; @@ -643,6 +649,7 @@ AMMDeposit::deposit( ammAccount, *amount2DepositActual, ctx_.journal, + {}, // don't sponsor for AMM Trustline WaiveTransferFee::Yes); if (!isTesSuccess(res)) { @@ -674,7 +681,8 @@ adjustLPTokensOut( return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit::Yes); } -/** Proportional deposit of pools assets in exchange for the specified +/** + * Proportional deposit of pools assets in exchange for the specified * amount of LPTokens. */ std::pair @@ -722,7 +730,8 @@ AMMDeposit::equalDepositTokens( } } -/** Proportional deposit of pool assets with the constraints on the maximum +/** + * Proportional deposit of pool assets with the constraints on the maximum * amount of each asset that the trader is willing to deposit. * a = (t/T) * A (1) * b = (t/T) * B (2) @@ -844,7 +853,8 @@ AMMDeposit::equalDepositLimit( } } -/** Single asset deposit of the amount of asset specified by Asset1In. +/** + * Single asset deposit of the amount of asset specified by Asset1In. * t = T * (b / B - x) / (1 + x) (3) * where * f1 = (1 - 0.5 * tfee) / (1 - tfee) @@ -892,7 +902,8 @@ AMMDeposit::singleDeposit( tfee); } -/** Single asset asset1 is deposited to obtain some share of +/** + * Single asset asset1 is deposited to obtain some share of * the AMM instance's pools represented by amount of LPTokens. * Use equation 4 to compute the amount of asset1 to be deposited, * given t represented by amount of LPTokens. Equation 4 solves @@ -930,7 +941,8 @@ AMMDeposit::singleDepositTokens( tfee); } -/** Single asset deposit with two constraints. +/** + * Single asset deposit with two constraints. * a. Amount of asset1 if specified (not 0) in Asset1In specifies the maximum * amount of asset1 that the trader is willing to deposit. * b. The effective-price of the LPToken traded out does not exceed diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 14ed5a4646..2baa7edfb4 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -658,15 +659,15 @@ AMMWithdraw::withdraw( auto sleAccount = view.peek(keylet::account(account)); if (!sleAccount) return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const balance = (*sleAccount)[sfBalance]; - std::uint32_t const ownerCount = sleAccount->at(sfOwnerCount); + auto const balance = (*sleAccount)[sfBalance]->xrp(); // See also TrustSet::doApply() and MPTokenAuthorize::authorize() XRPAmount const reserve( - (ownerCount < 2) ? XRPAmount(beast::kZero) - : view.fees().accountReserve(ownerCount + 1)); + (ownerCount(sleAccount, journal) < 2) + ? XRPAmount(beast::kZero) + : accountReserve(view, sleAccount, journal, {.ownerCountDelta = 1})); - auto const balanceAdj = isIssue ? std::max(priorBalance, balance.xrp()) : priorBalance; + auto const balanceAdj = isIssue ? std::max(priorBalance, balance) : priorBalance; if (balanceAdj < reserve) return tecINSUFFICIENT_RESERVE; } @@ -683,7 +684,7 @@ AMMWithdraw::withdraw( !isTesSuccess(err)) return err; - if (auto const err = checkCreateMPT(view, mptIssue, account, journal); + if (auto const err = checkCreateMPT(view, mptIssue, account, {}, journal); !isTesSuccess(err)) { return err; @@ -700,7 +701,7 @@ AMMWithdraw::withdraw( // Withdraw amountWithdraw auto res = accountSend( - view, ammAccount, account, amountWithdrawActual, journal, WaiveTransferFee::Yes); + view, ammAccount, account, amountWithdrawActual, journal, {}, WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -719,7 +720,7 @@ AMMWithdraw::withdraw( return {res, STAmount{}, STAmount{}, STAmount{}}; res = accountSend( - view, ammAccount, account, *amount2WithdrawActual, journal, WaiveTransferFee::Yes); + view, ammAccount, account, *amount2WithdrawActual, journal, {}, WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -759,7 +760,8 @@ adjustLPTokensIn( return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No); } -/** Proportional withdrawal of pool assets for the amount of LPTokens. +/** + * Proportional withdrawal of pool assets for the amount of LPTokens. */ std::pair AMMWithdraw::equalWithdrawTokens( @@ -823,7 +825,8 @@ AMMWithdraw::deleteAMMAccountIfEmpty( return {ter, true}; } -/** Proportional withdrawal of pool assets for the amount of LPTokens. +/** + * Proportional withdrawal of pool assets for the amount of LPTokens. */ std::tuple> AMMWithdraw::equalWithdrawTokens( @@ -909,7 +912,8 @@ AMMWithdraw::equalWithdrawTokens( // LCOV_EXCL_STOP } -/** All assets withdrawal with the constraints on the maximum amount +/** + * All assets withdrawal with the constraints on the maximum amount * of each asset that the trader is willing to withdraw. * a = (t/T) * A (5) * b = (t/T) * B (6) @@ -999,7 +1003,8 @@ AMMWithdraw::equalWithdrawLimit( tfee); } -/** Withdraw single asset equivalent to the amount specified in Asset1Out. +/** + * Withdraw single asset equivalent to the amount specified in Asset1Out. * t = T * (c - sqrt(c**2 - 4*R))/2 * where R = b/B, c = R*fee + 2 - fee * Use equation 7 to compute the t, given the amount in Asset1Out. @@ -1045,7 +1050,8 @@ AMMWithdraw::singleWithdraw( tfee); } -/** withdrawal of single asset specified in Asset1Out proportional +/** + * withdrawal of single asset specified in Asset1Out proportional * to the share represented by the amount of LPTokens. * Use equation 8 to compute the amount of asset1, given the redeemed t * represented by LPTokens. Let this be Y. @@ -1089,7 +1095,8 @@ AMMWithdraw::singleWithdrawTokens( return {tecAMM_FAILED, STAmount{}}; } -/** Withdraw single asset with two constraints. +/** + * Withdraw single asset with two constraints. * a. amount of asset1 if specified (not 0) in Asset1Out specifies the minimum * amount of asset1 that the trader is willing to withdraw. * b. The effective price of asset traded out does not exceed the amount diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index a915bb60d1..fb47cf0f97 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -833,9 +833,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) return {tefINTERNAL, false}; { - XRPAmount const reserve = - sb.fees().accountReserve(sleCreator->getFieldU32(sfOwnerCount) + 1); - + XRPAmount const reserve = accountReserve(sb, sleCreator, viewJ, {.ownerCountDelta = 1}); if (preFeeBalance_ < reserve) { // If we are here, the signing account had an insufficient reserve @@ -869,7 +867,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) } // Update owner count. - adjustOwnerCount(sb, sleCreator, 1, viewJ); + increaseOwnerCount(sb, sleCreator, {}, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : " << to_string(saTakerGets.asset()) diff --git a/src/libxrpl/tx/transactors/did/DIDDelete.cpp b/src/libxrpl/tx/transactors/did/DIDDelete.cpp index 90aa21d8a1..a2af4c1100 100644 --- a/src/libxrpl/tx/transactors/did/DIDDelete.cpp +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp @@ -50,8 +50,7 @@ DIDDelete::deleteSLE(ApplyView& view, SLE::pointer sle, AccountID const owner, b if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); - view.update(sleOwner); + decreaseOwnerCountForObject(view, sleOwner, sle, 1, j); // Remove object from ledger view.erase(sle); diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp index 1392581bb0..2659460b8d 100644 --- a/src/libxrpl/tx/transactors/did/DIDSet.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -72,7 +72,8 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) // Check reserve availability for new object creation { auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); - auto const reserve = ctx.view().fees().accountReserve((*sleAccount)[sfOwnerCount] + 1); + auto const reserve = + accountReserve(ctx.view(), sleAccount, ctx.journal, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -89,7 +90,7 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) return tecDIR_FULL; // LCOV_EXCL_LINE (*sle)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx.view(), sleAccount, 1, ctx.journal); + increaseOwnerCount(ctx.view(), sleAccount, {}, 1, ctx.journal); ctx.view().update(sleAccount); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index 55a9133e1e..feed43d410 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -181,7 +181,7 @@ EscrowCancel::doApply() if (auto const ret = std::visit( [&](T const&) { return escrowUnlockApplyHelper( - ctx_.view(), + ctx_.getApplyViewContext(), kParityRate, ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : slep, preFeeBalance_, @@ -209,8 +209,7 @@ EscrowCancel::doApply() } } - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); + decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal); // Remove escrow from ledger ctx_.view().erase(slep); diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 531098bd1c..50f2e8b859 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -434,16 +435,33 @@ EscrowCreate::doApply() // Check reserve and funds availability STAmount const amount{ctx_.tx[sfAmount]}; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); - auto const balance = sle->getFieldAmount(sfBalance).xrp(); - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + // First check: whoever is on the hook for the new owner increment + // can cover it. When sponsored this hits the sponsor branch and + // validates the sponsor's reserve + remaining credit. When + // unsponsored this hits the source branch and validates the + // source's pre-lock balance against base + (currentOC+1)*increment. + if (auto const ret = + checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; - // Check reserve and funds availability if (isXRP(amount)) { - if (balance < reserve + STAmount(amount).xrp()) + // Second check (XRP escrow only): after locking the escrowed + // amount, the source must still meet its own reserve floor. This is + // always the source's own balance against the source's own reserve — + // the sponsor's reserve was already validated above, and a sponsor + // never covers the locked funds. We compare directly (rather than via + // checkReserve) because that helper diverts to the sponsor's balance + // when a sponsor is present and would ignore the source's post-lock + // balance entirely. ownerCountDelta differs by case: + // - sponsored: 0 — sponsor covers the new owner increment, so the + // source only owes reserve for its current owners. + // - unsponsored: 1 — source owes reserve including the new increment. + auto const sourceReserve = accountReserve( + ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1}); + if (balance - STAmount(amount).xrp() < sourceReserve) return tecUNFUNDED; } @@ -535,7 +553,8 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep); ctx_.view().update(sle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 7f1f9ec078..8bc98c7aa8 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -340,6 +340,16 @@ EscrowFinish::doApply() } } + // With the Sponsor amendment, release the escrow reserve before delivery. + // Token delivery can auto-create a destination holding, and the same + // sponsor (or the same account, for a self-escrow) may cover both the + // escrow being removed and the holding being created. Without the + // amendment, keep the legacy order: releasing early changes the reserve + // arithmetic for self-escrows and would break consensus if not gated. + bool const sponsorEnabled = ctx_.view().rules().enabled(featureSponsor); + if (sponsorEnabled) + decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal); + STAmount const amount = slep->getFieldAmount(sfAmount); // Transfer amount to destination if (isXRP(amount)) @@ -359,7 +369,7 @@ EscrowFinish::doApply() if (auto const ret = std::visit( [&](T const&) { return escrowUnlockApplyHelper( - ctx_.view(), + ctx_.getApplyViewContext(), lockedRate, sled, preFeeBalance_, @@ -389,10 +399,9 @@ EscrowFinish::doApply() ctx_.view().update(sled); - // Adjust source owner count - auto const sle = ctx_.view().peek(keylet::account(account)); - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); + // Adjust source owner count (legacy position, pre-Sponsor) + if (!sponsorEnabled) + decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal); // Remove escrow from ledger ctx_.view().erase(slep); diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp index 9bb84e878e..b914f3cf24 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp @@ -372,7 +372,7 @@ LoanBrokerCoverClawback::doApply() associateAsset(*sleBroker, vaultAsset); // Transfer assets from pseudo-account to depositor. - return accountSend(view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes); + return accountSend(view(), brokerPseudoID, account, clawAmount, j_, {}, WaiveTransferFee::Yes); } void diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index 69b28b57af..09ab03347a 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -176,7 +176,7 @@ LoanBrokerCoverDeposit::doApply() // Transfer assets from depositor to pseudo-account. if (auto ter = - accountSend(view(), accountID_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes)) + accountSend(view(), accountID_, brokerPseudoID, amount, j_, {}, WaiveTransferFee::Yes)) return ter; // Increase the LoanBroker's CoverAvailable by Amount diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp index 008857a4ad..498f3c99eb 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp @@ -211,7 +211,14 @@ LoanBrokerCoverWithdraw::doApply() associateAsset(*broker, vaultAsset); - return doWithdraw(view(), tx, accountID_, dstAcct, brokerPseudoID, preFeeBalance_, amount, j_); + return doWithdraw( + ctx_.getApplyViewContext(), + accountID_, + dstAcct, + brokerPseudoID, + preFeeBalance_, + amount, + j_); } void diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp index 141cc2cf56..b36977d225 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -155,11 +155,11 @@ LoanBrokerDelete::doApply() { auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)}; if (auto const ter = accountSend( - view(), brokerPseudoID, accountID_, coverAvailable, j_, WaiveTransferFee::Yes)) + view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes)) return ter; } - if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_)) + if (auto ter = removeEmptyHolding(ctx_.getApplyViewContext(), brokerPseudoID, vaultAsset, j_)) return ter; auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID)); @@ -186,8 +186,6 @@ LoanBrokerDelete::doApply() view().erase(brokerPseudoSLE); - view().erase(broker); - { auto owner = view().peek(keylet::account(accountID_)); if (!owner) @@ -195,9 +193,11 @@ LoanBrokerDelete::doApply() // Decreases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. - adjustOwnerCount(view(), owner, -2, j_); + decreaseOwnerCountForObject(view(), owner, broker, 2, j_); } + view().erase(broker); + associateAsset(*broker, vaultAsset); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index ab1c8f22dc..e9c153404c 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -238,9 +238,8 @@ LoanBrokerSet::doApply() // Increases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. - adjustOwnerCount(view, owner, 2, j_); - auto const ownerCount = owner->at(sfOwnerCount); - if (preFeeBalance_ < view.fees().accountReserve(ownerCount)) + increaseOwnerCount(view, owner, {}, 2, j_); + if (preFeeBalance_ < accountReserve(view, owner, j_)) return tecINSUFFICIENT_RESERVE; auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID); @@ -249,7 +248,8 @@ LoanBrokerSet::doApply() auto& pseudo = *maybePseudo; auto pseudoId = pseudo->at(sfAccount); - if (auto ter = addEmptyHolding(view, pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) + if (auto ter = addEmptyHolding( + ctx_.getApplyViewContext(), pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) return ter; // Initialize data fields: diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp index 3459119379..1a77489b4b 100644 --- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp @@ -107,9 +107,8 @@ LoanDelete::doApply() view.erase(loanSle); // Decrement the LoanBroker's owner count. - // The broker's owner count is solely for the number of outstanding loans, - // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, -1, j_); + adjustLoanBrokerOwnerCount(view, brokerSle, -1, j_); + // If there are no loans left, then any remaining debt must be forgiven, // because there is no other way to pay it back. if (brokerSle->at(sfOwnerCount) == 0) @@ -129,7 +128,7 @@ LoanDelete::doApply() } } // Decrement the borrower's owner count - adjustOwnerCount(view, borrowerSle, -1, j_); + decreaseOwnerCountForObject(view, borrowerSle, loanSle, 1, j_); // These associations shouldn't do anything, but do them just to be safe associateAsset(*loanSle, vaultAsset); diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index 830eb30272..a0aa948876 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -292,6 +292,7 @@ LoanManage::defaultLoan( vaultSle->at(sfAccount), STAmount{vaultAsset, defaultCovered}, j, + {}, WaiveTransferFee::Yes); } diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index fcda2a9fff..54ee85b186 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -625,7 +625,11 @@ LoanPay::doApply() { // The broker may have deleted their holding. Recreate it if needed if (auto const ter = addEmptyHolding( - view, brokerPayee, brokerPayeeSle->at(sfBalance).value().xrp(), asset, j_); + ctx_.getApplyViewContext(), + brokerPayee, + brokerPayeeSle->at(sfBalance).value().xrp(), + asset, + j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 1ac387d1b1..694d01c69f 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,12 @@ LoanSet::preflight(PreflightContext const& ctx) auto const& tx = ctx.tx; + if (tx.isFieldPresent(sfSponsorFlags) && isReserveSponsored(tx)) + { + JLOG(ctx.j.debug()) << "LoanSet: reserve sponsorship is not allowed."; + return temINVALID_FLAG; + } + // Special case for Batch inner transactions if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) && !tx.isFieldPresent(sfCounterparty)) @@ -512,12 +519,12 @@ LoanSet::doApply() } } - adjustOwnerCount(view, borrowerSle, 1, j_); + increaseOwnerCount(view, borrowerSle, {}, 1, j_); + { - auto const ownerCount = borrowerSle->at(sfOwnerCount); auto const balance = accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp(); - if (balance < view.fees().accountReserve(ownerCount)) + if (balance < accountReserve(view, borrowerSle, j_)) return tecINSUFFICIENT_RESERVE; } @@ -531,8 +538,9 @@ LoanSet::doApply() borrower == accountID_ || borrower == counterparty, "xrpl::LoanSet::doApply", "borrower signed transaction"); + auto applyViewContext = ctx_.getApplyViewContext(); if (auto const ter = addEmptyHolding( - view, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_); + applyViewContext, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, and @@ -555,7 +563,11 @@ LoanSet::doApply() "broker owner signed transaction"); if (auto const ter = addEmptyHolding( - view, brokerOwner, brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j_); + applyViewContext, + brokerOwner, + brokerOwnerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, @@ -631,9 +643,7 @@ LoanSet::doApply() // Update the balances in the loan broker adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale); - // The broker's owner count is solely for the number of outstanding loans, - // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, 1, j_); + adjustLoanBrokerOwnerCount(view, brokerSle, 1, j_); loanSequenceProxy += 1; // The sequence should be extremely unlikely to roll over, but fail if it // does diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index 14bf3646c0..41bb051768 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -387,8 +388,7 @@ NFTokenAcceptOffer::transferNFToken( auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount); if (buyerOwnerCountAfter > buyerOwnerCountBefore) { - if (auto const reserve = view().fees().accountReserve(buyerOwnerCountAfter); - buyerBalance < reserve) + if (buyerBalance < accountReserve(view(), sleBuyer, j_)) return tecINSUFFICIENT_RESERVE; } diff --git a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp index d8e5a7b235..5490c2de3a 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -331,12 +332,14 @@ NFTokenMint::doApply() // allows NFTs to be added to the page (and burn fees) without // requiring the reserve to be met each time. The reserve is // only managed when a new NFT page or sell offer is added. - if (auto const ownerCountAfter = - view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount); + auto const sleAccount = view().read(keylet::account(accountID_)); + if (!sleAccount) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (auto const ownerCountAfter = sleAccount->getFieldU32(sfOwnerCount); ownerCountAfter > ownerCountBefore) { - if (auto const reserve = view().fees().accountReserve(ownerCountAfter); - preFeeBalance_ < reserve) + if (preFeeBalance_ < accountReserve(view(), sleAccount, j_)) return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp index 9a26816155..b4c12b9514 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,8 @@ #include #include +#include + namespace xrpl { NotTEC @@ -68,10 +71,8 @@ OracleDelete::deleteOracle( if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - auto const count = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1; - - adjustOwnerCount(view, sleOwner, count, j); - + std::uint32_t const count = calculateOracleReserve(sle); + decreaseOwnerCountForObject(view, sleOwner, sle, count, j); view.erase(sle); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp index 12d826e54d..ead26824e5 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp @@ -1,9 +1,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -22,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -150,8 +153,9 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (!pairsDel.empty()) return tecTOKEN_PAIR_NOT_FOUND; - auto const oldCount = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? 2 : 1; - auto const newCount = pairs.size() > 5 ? 2 : 1; + auto const oldCount = calculateOracleReserve(sle); + auto const newCount = calculateOracleReserve(pairs); + adjustReserve = newCount - oldCount; } else @@ -160,7 +164,7 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (!ctx.tx.isFieldPresent(sfProvider) || !ctx.tx.isFieldPresent(sfAssetClass)) return temMALFORMED; - adjustReserve = pairs.size() > 5 ? 2 : 1; + adjustReserve = calculateOracleReserve(pairs); } if (pairs.empty()) @@ -169,7 +173,7 @@ OracleSet::preclaim(PreclaimContext const& ctx) return tecARRAY_TOO_LARGE; auto const reserve = - ctx.view.fees().accountReserve(sleSetter->getFieldU32(sfOwnerCount) + adjustReserve); + accountReserve(ctx.view, sleSetter, ctx.j, {.ownerCountDelta = adjustReserve}); auto const& balance = sleSetter->getFieldAmount(sfBalance); if (balance < reserve) @@ -179,11 +183,22 @@ OracleSet::preclaim(PreclaimContext const& ctx) } static bool -adjustOwnerCount(ApplyContext& ctx, int count) +adjustOracleOwnerCount(ApplyContext& ctx, int count) { + XRPL_ASSERT(std::abs(count) <= 2, "xrpl::adjustOracleOwnerCount abs(counter) <= 2"); + if (auto const sleAccount = ctx.view().peek(keylet::account(ctx.tx[sfAccount]))) { - adjustOwnerCount(ctx.view(), sleAccount, count, ctx.journal); + if (count > 0) + { + increaseOwnerCount( + ctx.view(), sleAccount, {}, static_cast(count), ctx.journal); + } + else if (count < 0) + { + decreaseOwnerCount( + ctx.view(), sleAccount, {}, static_cast(-count), ctx.journal); + } return true; } @@ -228,7 +243,7 @@ OracleSet::doApply() priceData.setFieldCurrency(sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset)); pairs.emplace(tokenPairKey(entry), std::move(priceData)); } - auto const oldCount = pairs.size() > 5 ? 2 : 1; + auto const oldCount = calculateOracleReserve(pairs); // update/add/delete pairs for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries)) { @@ -266,9 +281,10 @@ OracleSet::doApply() (*sle)[sfOracleDocumentID] = ctx_.tx[sfOracleDocumentID]; } - auto const newCount = pairs.size() > 5 ? 2 : 1; - auto const adjust = newCount - oldCount; - if (adjust != 0 && !adjustOwnerCount(ctx_, adjust)) + auto const newCount = calculateOracleReserve(pairs); + int32_t const adjust = newCount - oldCount; + + if (adjust != 0 && !adjustOracleOwnerCount(ctx_, adjust)) return tefINTERNAL; // LCOV_EXCL_LINE ctx_.view().update(sle); @@ -317,8 +333,8 @@ OracleSet::doApply() (*sle)[sfOwnerNode] = *page; - auto const count = series.size() > 5 ? 2 : 1; - if (!adjustOwnerCount(ctx_, count)) + auto const count = calculateOracleReserve(series); + if (!adjustOracleOwnerCount(ctx_, count)) return tefINTERNAL; // LCOV_EXCL_LINE ctx_.view().insert(sle); diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index 7e950dc743..d3e2af86ef 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -7,13 +7,13 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -151,6 +151,7 @@ DepositPreauth::preclaim(PreclaimContext const& ctx) TER DepositPreauth::doApply() { + auto applyViewContext = ctx_.getApplyViewContext(); if (ctx_.tx.isFieldPresent(sfAuthorize)) { auto const sleOwner = view().peek(keylet::account(accountID_)); @@ -160,13 +161,10 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{ - view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (auto const ret = checkReserve( + applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -190,7 +188,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, 1, j_); + increaseOwnerCount(applyViewContext, sleOwner, 1, j_); + addSponsorToLedgerEntry(applyViewContext, slePreauth); } else if (ctx_.tx.isFieldPresent(sfUnauthorize)) { @@ -207,13 +206,10 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{ - view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (auto const ret = checkReserve( + applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -251,7 +247,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, 1, j_); + increaseOwnerCount(applyViewContext, sleOwner, 1, j_); + addSponsorToLedgerEntry(applyViewContext, slePreauth); } else if (ctx_.tx.isFieldPresent(sfUnauthorizeCredentials)) { @@ -289,8 +286,7 @@ DepositPreauth::removeFromLedger(ApplyView& view, uint256 const& preauthIndex, b if (!sleOwner) return tefINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); - + decreaseOwnerCountForObject(view, sleOwner, slePreauth, 1, j); // Remove DepositPreauth from ledger. view.erase(slePreauth); diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 6e3883572a..17c96a1919 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include +#include #include #include #include @@ -120,6 +122,21 @@ Payment::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1) && isDstMPT) return temDISABLED; + if (tx.isFlag(tfSponsorCreatedAccount)) + { + if (!ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if (tx.isFlag(tfNoRippleDirect) || tx.isFlag(tfPartialPayment) || tx.isFlag(tfLimitQuality)) + return temINVALID_FLAG; + + if (tx.isFieldPresent(sfSendMax) || tx.isFieldPresent(sfPaths)) + return temINVALID; + + if (!dstAmount.native()) + return temBAD_AMOUNT; + } + if (!mpTokensV2 && isDstMPT && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; @@ -383,15 +400,28 @@ Payment::preclaim(PreclaimContext const& ctx) { // accountReserve is the minimum amount that an account can have. // Reserve is not scaled by load. - JLOG(ctx.j.trace()) << "Delay transaction: Destination account does not exist. " - << "Insufficent payment to create account."; + if (!ctx.tx.isFlag(tfSponsorCreatedAccount)) + { + // The minimum amount when creating a Sponsored Account is 1 drop. + // Since the reserve is covered by the sponsor, you don't need to hold the + // 1-increment reserve yourself. + JLOG(ctx.j.trace()) << "Delay transaction: Destination account does not exist. " + << "Insufficient payment to create account."; - // TODO: de-dupe - // Another transaction could create the account and then this - // transaction would succeed. - return tecNO_DST_INSUF_XRP; + // TODO: de-dupe + // Another transaction could create the account and then this + // transaction would succeed. + return tecNO_DST_INSUF_XRP; + } } } + else if (ctx.tx.isFlag(tfSponsorCreatedAccount)) + { + // The tfSponsorCreatedAccount flag is specific to account creation via + // sponsorship. If the destination account already exists, applying this + // flag is invalid. + return tecNO_SPONSOR_PERMISSION; + } else if (sleDst->isFlag(lsfRequireDestTag) && !ctx.tx.isFieldPresent(sfDestinationTag)) { // The tag is basically account-specific information we don't @@ -470,6 +500,27 @@ Payment::doApply() sleDst->setFieldU32(sfSequence, view().seq()); sleDst->setFieldAmount(sfBalance, XRPAmount(beast::kZero)); + if (ctx_.tx.isFlag(tfSponsorCreatedAccount)) + { + auto const sponsor = view().peek(keylet::account(accountID_)); + if (!sponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const currentSponsoringAccountCount = + sponsor->getFieldU32(sfSponsoringAccountCount); + if (currentSponsoringAccountCount == std::numeric_limits::max()) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Sponsoring account count overflow for account " + << to_string(accountID_); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + sponsor->setFieldU32(sfSponsoringAccountCount, currentSponsoringAccountCount + 1); + + addSponsorToLedgerEntry(sleDst, sponsor); + view().update(sponsor); + } + view().insert(sleDst); } else @@ -636,16 +687,12 @@ Payment::doApply() if (!sleSrc) return tefINTERNAL; // LCOV_EXCL_LINE - // ownerCount is the number of entries in this ledger for this - // account that require a reserve. - auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); + // the number of reserves in this ledger for this account that require a + // reserve. + auto const reserve = accountReserve(view(), sleSrc, j_); - // This is the total reserve in drops. - auto const reserve = view().fees().accountReserve(ownerCount); - - // In a delegated payment, the fee payer is the delegated account, - // not the source account (accountID_). - bool const accountIsPayer = (ctx_.tx.getFeePayer() == accountID_); + // In a delegated / fee sponsored payment, the fee payer is not the source account (accountID_). + bool const accountIsPayer = ctx_.tx.getFeePayerID() == accountID_; // preFeeBalance_ is the balance on the source account (accountID_) BEFORE the fees // were charged. If source account is the fee payer, it must also cover the fee. diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index 977da56861..b17430948a 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -77,9 +78,10 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; // Check reserve and funds availability + if (!ctx.view.rules().enabled(featureSponsor)) { auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1); + auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1, 1); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -131,6 +133,36 @@ PaymentChannelCreate::doApply() return tecEXPIRED; } + if (ctx_.view().rules().enabled(featureSponsor)) + { + // First check: whoever is on the hook for the new owner increment + // can cover it. When sponsored this hits the sponsor branch and + // validates the sponsor's reserve + remaining credit. When + // unsponsored this hits the source branch and validates the + // source's pre-lock balance against base + (currentOC+1)*increment. + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; + + // Second check: after locking sfAmount in the channel, the source + // must still meet its own reserve floor. This is always the + // source's own balance against the source's own reserve — the + // sponsor's reserve was already validated above, and a sponsor + // never covers the locked funds. We compare directly (rather than + // via checkReserve) because that helper diverts to the + // sponsor's balance when a sponsor is present and would ignore the + // source's post-lock balance entirely. ownerCountDelta differs by + // case: + // - sponsored: 0 — sponsor covers the new owner increment, so + // the source only owes reserve for its current owners. + // - unsponsored: 1 — source owes reserve including the new increment. + auto const sourceReserve = accountReserve( + ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1}); + if (preFeeBalance_ - ctx_.tx[sfAmount].xrp() < sourceReserve) + return tecUNFUNDED; + } + auto const dst = ctx_.tx[sfDestination]; // Create PayChan in ledger. @@ -178,7 +210,8 @@ PaymentChannelCreate::doApply() // Deduct owner's balance, increment owner count (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep); ctx_.view().update(sle); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 4e3c5dd638..1d1cf07e25 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -87,13 +88,18 @@ PaymentChannelFund::doApply() { // Check reserve and funds availability - auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); + STAmount const balance = (*sle)[sfBalance]; + if (auto const ret = checkReserve(ctx_.getApplyViewContext(), sle, balance.xrp(), {}, j_); + !isTesSuccess(ret)) + return ret; - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx_.tx[sfAmount]) + // After locking sfAmount in the channel, the source must still meet + // its own reserve floor. We compare directly (rather than via + // checkReserve) because that helper diverts to the sponsor's balance + // when a sponsor is present and would ignore the source's post-lock + // balance entirely. Funding an existing channel adds no owned object, + // so there is no owner-count delta. + if (balance < accountReserve(ctx_.view(), sle, j_) + ctx_.tx[sfAmount]) return tecUNFUNDED; } diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp index 7eb3f282b9..dc1b323482 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp @@ -42,7 +42,9 @@ PermissionedDomainDelete::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -/** Attempt to delete the Permissioned Domain. */ +/** + * Attempt to delete the Permissioned Domain. + */ TER PermissionedDomainDelete::doApply() { @@ -65,7 +67,7 @@ PermissionedDomainDelete::doApply() XRPL_ASSERT( ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0, "xrpl::PermissionedDomainDelete::doApply : nonzero owner count"); - adjustOwnerCount(view(), ownerSle, -1, ctx_.journal); + decreaseOwnerCountForObject(view(), ownerSle, slePd, 1, ctx_.journal); view().erase(slePd); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp index 0e71ceada1..61ebdcf9c7 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp @@ -73,7 +73,9 @@ PermissionedDomainSet::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -/** Attempt to create the Permissioned Domain. */ +/** + * Attempt to create the Permissioned Domain. + */ TER PermissionedDomainSet::doApply() { @@ -106,7 +108,8 @@ PermissionedDomainSet::doApply() // Create new permissioned domain. // Check reserve availability for new object creation auto const balance = STAmount((*ownerSle)[sfBalance]).xrp(); - auto const reserve = ctx_.view().fees().accountReserve((*ownerSle)[sfOwnerCount] + 1); + auto const reserve = + accountReserve(ctx_.view(), ownerSle, ctx_.journal, {.ownerCountDelta = 1}); if (balance < reserve) return tecINSUFFICIENT_RESERVE; @@ -125,7 +128,7 @@ PermissionedDomainSet::doApply() slePd->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), ownerSle, 1, ctx_.journal); + increaseOwnerCount(view(), ownerSle, {}, 1, ctx_.journal); view().insert(slePd); } diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp new file mode 100644 index 0000000000..2b6ab8cf15 --- /dev/null +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp @@ -0,0 +1,420 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +static bool +hasSponsorshipBudget( + SLE::const_ref sponsorshipSle, + std::optional const& feeAmount, + std::optional const& remainingOwnerCount) +{ + // A field the transaction omits keeps whatever the existing object holds, + // so fall back to the current SLE value when the tx does not set it. + bool const hasFeeAmount = feeAmount + ? *feeAmount > beast::kZero + : sponsorshipSle && (*sponsorshipSle)[~sfFeeAmount].value_or(STAmount{0}) > beast::kZero; + + bool const hasRemainingOwnerCount = remainingOwnerCount + ? *remainingOwnerCount > 0 + : sponsorshipSle && (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0) > 0; + + return hasFeeAmount || hasRemainingOwnerCount; +} + +TxConsequences +SponsorshipSet::makeTxConsequences(PreflightContext const& ctx) +{ + auto const feeAmount = ctx.tx[~sfFeeAmount]; + return TxConsequences{ctx.tx, feeAmount.has_value() ? feeAmount->xrp() : beast::kZero}; +} + +std::uint32_t +SponsorshipSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfSponsorshipSetMask; +} + +NotTEC +SponsorshipSet::preflight(PreflightContext const& ctx) +{ + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForFee) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + return temINVALID_FLAG; + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForReserve) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + return temINVALID_FLAG; + + auto const account = ctx.tx.getAccountID(sfAccount); + bool const hasSponsor = ctx.tx.isFieldPresent(sfCounterpartySponsor); + bool const hasSponsee = ctx.tx.isFieldPresent(sfSponsee); + + // The transaction must specify either Sponsor or Sponsee, but not both. + if (hasSponsor == hasSponsee) + return temMALFORMED; + + auto const sponsorID = ctx.tx[~sfCounterpartySponsor].value_or(account); + auto const sponseeID = ctx.tx[~sfSponsee].value_or(account); + + if (sponsorID == sponseeID) + return temMALFORMED; + + if (ctx.tx.isFlag(tfDeleteObject)) + { + // Transactions deleting `Sponsorship` cannot set modification flags. + constexpr std::uint32_t kModifyFlags = tfSponsorshipSetRequireSignForFee | + tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForFee | + tfSponsorshipClearRequireSignForReserve; + + if ((ctx.tx.getFlags() & kModifyFlags) != 0u) + return temINVALID_FLAG; + + // Transactions deleting `Sponsorship` cannot include modification fields. + if (ctx.tx.isFieldPresent(sfFeeAmount) || ctx.tx.isFieldPresent(sfRemainingOwnerCount) || + ctx.tx.isFieldPresent(sfMaxFee)) + return temMALFORMED; + } + else + { + // Both sponsor and sponsee can delete a Sponsorship object, but only + // the sponsor can create or update one. + if (account != sponsorID) + return temMALFORMED; + + // FeeAmount and MaxFee must be non-negative XRP amounts when present. + auto const checkOptionalAmountField = [&](SField const& field) -> NotTEC { + if (!ctx.tx.isFieldPresent(field)) + return tesSUCCESS; + + auto const amount = ctx.tx.getFieldAmount(field); + + if (!isXRP(amount)) + return temBAD_AMOUNT; + + if (amount.xrp() < beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; + }; + + if (auto const ret = checkOptionalAmountField(sfFeeAmount); !isTesSuccess(ret)) + return ret; + + if (auto const ret = checkOptionalAmountField(sfMaxFee); !isTesSuccess(ret)) + return ret; + } + + return tesSUCCESS; +} + +TER +SponsorshipSet::preclaim(PreclaimContext const& ctx) +{ + auto const sponsorID = ctx.tx[~sfCounterpartySponsor].value_or(ctx.tx[sfAccount]); + auto const sponseeID = ctx.tx[~sfSponsee].value_or(ctx.tx[sfAccount]); + + if (sponseeID == sponsorID) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorAccSle = ctx.view.read(keylet::account(sponsorID)); + if (!sponsorAccSle) + return tecNO_DST; + + auto const sponseeSle = ctx.view.read(keylet::account(sponseeID)); + if (!sponseeSle) + return tecNO_DST; + + // Pseudo-accounts cannot participate in sponsorship. + if (isPseudoAccount(sponsorAccSle) || isPseudoAccount(sponseeSle)) + return tecNO_PERMISSION; + + auto const sponsorshipSle = ctx.view.read(keylet::sponsorship(sponsorID, sponseeID)); + + // Deleting a Sponsorship object requires the object to already exist. + if (ctx.tx.isFlag(tfDeleteObject) && !sponsorshipSle) + return tecNO_ENTRY; + + // Reject creating or updating a Sponsorship that would be left with no + // budget (neither a positive FeeAmount nor a positive RemainingOwnerCount). + // Such an object is unusable yet still consumes the sponsor's reserve. + if (!ctx.tx.isFlag(tfDeleteObject) && + !hasSponsorshipBudget(sponsorshipSle, ctx.tx[~sfFeeAmount], ctx.tx[~sfRemainingOwnerCount])) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +static TER +deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j) +{ + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorID = (*sle)[sfOwner]; + auto const sponseeID = (*sle)[sfSponsee]; + + // The sponsor owns the Sponsorship object, so deletion releases the + // sponsor's owner reserve. + auto sponsorAccSle = view.peek(keylet::account(sponsorID)); + if (!sponsorAccSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove(keylet::ownerDir(sponsorID), (*sle)[sfOwnerNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsor."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + if (!view.dirRemove(keylet::ownerDir(sponseeID), (*sle)[sfSponseeNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsee."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + decreaseOwnerCountForObject(view, sponsorAccSle, sle, 1, j); + + // Return any prefunded fee amount to the sponsor before erasing the object. + if (sle->isFieldPresent(sfFeeAmount)) + { + (*sponsorAccSle)[sfBalance] += sle->getFieldAmount(sfFeeAmount); + view.update(sponsorAccSle); + } + + view.erase(sle); + + return tesSUCCESS; +} + +TER +SponsorshipSet::doApply() +{ + auto const sponsorID = ctx_.tx[~sfCounterpartySponsor].value_or(accountID_); + auto const sponseeID = ctx_.tx[~sfSponsee].value_or(accountID_); + + if (sponseeID == sponsorID) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorAccSle = ctx_.view().peek(keylet::account(sponsorID)); + if (!sponsorAccSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!ctx_.view().exists(keylet::account(sponseeID))) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorKeylet = keylet::sponsorship(sponsorID, sponseeID); + auto const sponsorshipSle = ctx_.view().peek(sponsorKeylet); + + if (ctx_.tx.isFlag(tfDeleteObject)) + { + if (!sponsorshipSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + return deleteSponsorship(ctx_.view(), sponsorshipSle, ctx_.journal); + } + + auto const feeAmount = ctx_.tx[~sfFeeAmount]; + auto const maxFee = ctx_.tx[~sfMaxFee]; + auto const remainingOwnerCount = ctx_.tx[~sfRemainingOwnerCount]; + + bool const hasPositiveFeeAmount = feeAmount.has_value() && *feeAmount > beast::kZero; + + auto reserveSponsorAccSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!reserveSponsorAccSle) + return reserveSponsorAccSle.error(); // LCOV_EXCL_LINE + + if (!sponsorshipSle) + { + // Create a new Sponsorship object between the sponsor and sponsee. + auto newSle = std::make_shared(sponsorKeylet); + + (*newSle)[sfOwner] = sponsorID; + (*newSle)[sfSponsee] = sponseeID; + if (feeAmount && (*feeAmount).xrp() > (*sponsorAccSle)[sfBalance]) + return tecUNFUNDED; + + STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; + if (hasPositiveFeeAmount) + sponsorBalanceAfterFee -= *feeAmount; + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sponsorAccSle, + sponsorBalanceAfterFee.xrp(), + *reserveSponsorAccSle, + {.ownerCountDelta = 1}, + ctx_.journal, + tecUNFUNDED); + !isTesSuccess(ret)) + { + return ret; + } + + if (hasPositiveFeeAmount) + { + // New object: FeeAmount starts absent, so deduct and record the full amount + (*newSle)[sfFeeAmount] = *feeAmount; + (*sponsorAccSle)[sfBalance] -= *feeAmount; + } + + if (maxFee && *maxFee > beast::kZero) + (*newSle)[sfMaxFee] = *maxFee; + if (remainingOwnerCount && *remainingOwnerCount > 0) + (*newSle)[sfRemainingOwnerCount] = *remainingOwnerCount; + + std::uint32_t flags = 0; + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) + flags |= lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) + flags |= lsfSponsorshipRequireSignForReserve; + + (*newSle)[sfFlags] = flags; + + auto const sponsorPage = view().dirInsert( + keylet::ownerDir(sponsorID), sponsorKeylet, describeOwnerDir(sponsorID)); + if (!sponsorPage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfOwnerNode] = *sponsorPage; + + auto const sponseePage = view().dirInsert( + keylet::ownerDir(sponseeID), sponsorKeylet, describeOwnerDir(sponseeID)); + if (!sponseePage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfSponseeNode] = *sponseePage; + + // NOLINTNEXTLINE(readability-suspicious-call-argument) + increaseOwnerCount(view(), sponsorAccSle, *reserveSponsorAccSle, 1, ctx_.journal); + addSponsorToLedgerEntry(newSle, *reserveSponsorAccSle); + + ctx_.view().insert(newSle); + return tesSUCCESS; + } + + // Update the existing Sponsorship object. + if (feeAmount) + { + auto const currentFeeAmount = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount{0}); + auto const feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount); + + if (feeAmountDelta > beast::kZero && feeAmountDelta > (*sponsorAccSle)[sfBalance]) + return tecUNFUNDED; + + // Move the FeeAmount delta between the sponsor balance and Sponsorship + // object. + if (feeAmountDelta != beast::kZero) + { + STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; + sponsorBalanceAfterFee -= feeAmountDelta; + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sponsorAccSle, + sponsorBalanceAfterFee.xrp(), + *reserveSponsorAccSle, + {}, + ctx_.journal, + tecUNFUNDED); + !isTesSuccess(ret)) + { + return ret; + } + + (*sponsorAccSle)[sfBalance] -= feeAmountDelta; + if (*feeAmount == beast::kZero) + { + (*sponsorshipSle).makeFieldAbsent(sfFeeAmount); + } + else + { + (*sponsorshipSle).setFieldAmount(sfFeeAmount, *feeAmount); + } + ctx_.view().update(sponsorAccSle); + } + } + + if (maxFee) + { + if (*maxFee == beast::kZero) + { + (*sponsorshipSle).makeFieldAbsent(sfMaxFee); + } + else + { + (*sponsorshipSle)[sfMaxFee] = *maxFee; + } + } + + if (remainingOwnerCount) + { + if (*remainingOwnerCount == 0) + { + sponsorshipSle->makeFieldAbsent(sfRemainingOwnerCount); + } + else + { + sponsorshipSle->at(sfRemainingOwnerCount) = *remainingOwnerCount; + } + } + + // Apply requested flag changes. + auto flags = sponsorshipSle->getFieldU32(sfFlags); + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) + flags |= lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + flags &= ~lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) + flags |= lsfSponsorshipRequireSignForReserve; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + flags &= ~lsfSponsorshipRequireSignForReserve; + + if (flags != (*sponsorshipSle)[sfFlags]) + (*sponsorshipSle)[sfFlags] = flags; + + view().update(sponsorshipSle); + + return tesSUCCESS; +} + +void +SponsorshipSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +SponsorshipSet::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp new file mode 100644 index 0000000000..0e036649fd --- /dev/null +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp @@ -0,0 +1,542 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +// Increment an uint32 sponsor count field and update the SLE. +static TER +incrementSponsorCount( + ApplyView& view, + SLE::ref sle, + SF_UINT32 const& field, + std::uint32_t const delta) +{ + auto const currentValue = sle->getFieldU32(field); + if (std::numeric_limits::max() - currentValue < delta) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::incrementSponsorCount : sponsor field overflow"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + sle->at(field) = currentValue + delta; + view.update(sle); + return tesSUCCESS; +} + +// Decrement an uint32 sponsor count field and update the SLE. +static TER +decrementSponsorCount( + ApplyView& view, + SLE::ref sle, + SF_UINT32 const& field, + std::uint32_t const delta) +{ + auto const currentValue = sle->getFieldU32(field); + if (currentValue < delta) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::decrementSponsorCount : sponsor field underflow"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + sle->at(field) = currentValue - delta; + view.update(sle); + return tesSUCCESS; +} + +// Consume the sponsor's pre-funded reserve budget and lowers the Sponsorship +// object's RemainingOwnerCount. +static TER +decrementPrefundedReserveCount(ApplyView& view, SLE::ref sponsorshipSle, std::uint32_t const delta) +{ + if (delta == 0) + return tesSUCCESS; // LCOV_EXCL_LINE + + auto const currentReserveCount = sponsorshipSle->getFieldU32(sfRemainingOwnerCount); + if (currentReserveCount < delta) + { + // LCOV_EXCL_START + // Already verified by checkReserve (sufficient RemainingOwnerCount) + UNREACHABLE("xrpl::decrementPrefundedReserveCount : invalid reserve count"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + sponsorshipSle->at(sfRemainingOwnerCount) = currentReserveCount - delta; + view.update(sponsorshipSle); + return tesSUCCESS; +} + +std::uint32_t +SponsorshipTransfer::getFlagsMask(PreflightContext const& ctx) +{ + return tfSponsorshipTransferMask; +} + +NotTEC +SponsorshipTransfer::preflight(PreflightContext const& ctx) +{ + static constexpr auto transferFlags = + tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd; + if (std::popcount(ctx.tx.getFlags() & transferFlags) != 1) + { + JLOG(ctx.j.debug()) << "preflight: Only one SponsorshipTransfer flag can be set per tx."; + return temINVALID_FLAG; + } + + if (ctx.tx.isFlag(tfSponsorshipCreate)) + { + // Creating sponsorship transfers an unsponsored target from the sponsee + // to a reserve sponsor identified by sfSponsor + spfSponsorReserve. + if (!ctx.tx.isFieldPresent(sfSponsor)) + { + JLOG(ctx.j.debug()) << "preflight: sfSponsor must be present when creating sponsorship"; + return temMALFORMED; + } + + if (!isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) + << "preflight: spfSponsorReserve must be set when creating sponsorship"; + return temINVALID_FLAG; + } + + if (ctx.tx.isFieldPresent(sfSponsee)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsee must not be present when creating sponsorship"; + return temMALFORMED; + } + } + + if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + // Reassigning sponsorship transfers an already sponsored target from its + // current reserve sponsor to the new sponsor identified by sfSponsor + + // spfSponsorReserve. + if (!ctx.tx.isFieldPresent(sfSponsor)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsor must be present when reassigning sponsorship"; + return temMALFORMED; + } + + if (!isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) + << "preflight: spfSponsorReserve must be set when reassigning sponsorship"; + return temINVALID_FLAG; + } + if (ctx.tx.isFieldPresent(sfSponsee)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsee must not be present when reassigning sponsorship"; + return temMALFORMED; + } + } + + if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + // Ending sponsorship removes reserve sponsorship from a sponsored target. + // The target is sfSponsee when provided; otherwise it is sfAccount. + if (ctx.tx.isFieldPresent(sfSponsor)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsor must not be present when ending sponsorship"; + return temMALFORMED; + } + + // sfSponsorFlags should not be present if it is ending sponsorship. + if (ctx.tx.isFieldPresent(sfSponsorFlags)) + { + // Unreachable: reaching here means sfSponsor is absent, which is already checked above, + // and preflight1Sponsor already rejects sfSponsorFlags present without sfSponsor with + // temINVALID_FLAG. Keep this as a defensive check. + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::SponsorshipTransfer::preflight : sfSponsorFlags present without sfSponsor " + "when ending sponsorship"); + return temINVALID_FLAG; + // LCOV_EXCL_STOP + } + + if (ctx.tx.isFieldPresent(sfSponsee) && + ctx.tx.getAccountID(sfSponsee) == ctx.tx.getAccountID(sfAccount)) + { + JLOG(ctx.j.debug()) << "preflight: sfSponsee should not be the same as the account"; + return temMALFORMED; + } + } + + // Account-level reserve sponsorship changes the reserve responsibility for + // the account itself, so the new sponsor must explicitly co-sign. Object-level + // sponsorship may use pre-funded reserve sponsorship instead. + bool const isCreateOrReassign = + ctx.tx.isFlag(tfSponsorshipCreate) || ctx.tx.isFlag(tfSponsorshipReassign); + auto const reserveSponsor = getTxReserveSponsorID(ctx.tx); + bool const isAccountReserveSponsorship = + isCreateOrReassign && reserveSponsor && !ctx.tx.isFieldPresent(sfObjectID); + + if (isAccountReserveSponsorship && !ctx.tx.isFieldPresent(sfSponsorSignature)) + { + JLOG(ctx.j.debug()) << "preflight: account sponsorship requires sfSponsorSignature"; + return temMALFORMED; + } + + return tesSUCCESS; +} + +TER +SponsorshipTransfer::preclaim(PreclaimContext const& ctx) +{ + auto const objectID = ctx.tx[~sfObjectID]; + auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx); + if (!newSponsorSleExpected) + return newSponsorSleExpected.error(); // LCOV_EXCL_LINE + auto const newSponsorSle = *newSponsorSleExpected; + + auto const account = ctx.tx[sfAccount]; + auto const sponseeID = ctx.tx[~sfSponsee].value_or(account); + auto const sponseeSle = ctx.view.read(keylet::account(sponseeID)); + if (!sponseeSle) + { + // If it is ending sponsorship, sfSponsee is user input, return terNO_ACCOUNT if it does not + // exist. + if (ctx.tx.isFieldPresent(sfSponsee)) + return terNO_ACCOUNT; + + // If it is creating or reassigning sponsorship, sfSponsee is the account itself, which is + // always present by the time preclaim runs. Return tecINTERNAL if it does not exist. + return tecINTERNAL; // LCOV_EXCL_LINE + } + + // Default setup with an account sponsorship transfer. If it is an object transfer, they will be + // overridden to the object SLE and its type-specific sponsor field: + // sfHighSponsor/sfLowSponsor for a RippleState, sfSponsor for other object types. + SLE::const_pointer targetSle = sponseeSle; + auto const* sponsorField = &sfSponsor; + + if (objectID.has_value()) + { + auto const objectSle = ctx.view.read(keylet::unchecked(*objectID)); + if (!objectSle) + return tecNO_ENTRY; + + if (!isLedgerEntrySupportedBySponsorship(*objectSle)) + return tecNO_PERMISSION; + + if (!isLedgerEntryOwner(ctx.view, *objectSle, sponseeID)) + return tecNO_PERMISSION; + + // Object transfer: the target is the object, and its sponsor field + // depends on the object type, a RippleState stores the sponsor in + // sfHighSponsor/sfLowSponsor, while other object type uses sfSponsor. + targetSle = objectSle; + sponsorField = &getLedgerEntrySponsorField(*objectSle, sponseeID); + } + + bool const isSponsored = targetSle->isFieldPresent(*sponsorField); + + if (ctx.tx.isFlag(tfSponsorshipCreate)) + { + // Creating a new sponsorship: needs a new reserve sponsor, and the + // target must not already be sponsored + if (!newSponsorSle || isSponsored) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + // Reassigning sponsorship: needs a new reserve sponsor, and the target must already + // be sponsored + if (!newSponsorSle || !isSponsored) + return tecNO_PERMISSION; + + // Reassigning to the current sponsor would change no state, but would + // still draw down the sponsor's pre-funded reserve budget (and its + // reserve headroom would be double-counted in checkReserve). + if (targetSle->getAccountID(*sponsorField) == ctx.tx.getAccountID(sfSponsor)) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + // Ending sponsorship: no new reserve sponsor, the target must be sponsored. + if (newSponsorSle || !isSponsored) + return tecNO_PERMISSION; + + // Only the sponsor or sponsee can end sponsorship. + auto const sponsor = targetSle->getAccountID(*sponsorField); + if (account != sponsor && account != sponseeID) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +SponsorshipTransfer::doApply() +{ + auto const objectID = ctx_.tx[~sfObjectID]; + + auto const sponseeID = ctx_.tx[~sfSponsee].value_or(accountID_); + auto const sponseeSle = view().peek(keylet::account(sponseeID)); + if (!sponseeSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const balanceBeforeFee = [&](SLE::const_ref sle) -> XRPAmount { + if (sle->getAccountID(sfAccount) == accountID_) + return preFeeBalance_; + return sle->getFieldAmount(sfBalance).xrp(); + }; + + bool const isCreate = ctx_.tx.isFlag(tfSponsorshipCreate); + bool const isReassign = ctx_.tx.isFlag(tfSponsorshipReassign); + + if (objectID.has_value()) + { + // Transfer object sponsor + auto const objectSle = view().peek(keylet::unchecked(*objectID)); + if (!objectSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // preclaim established that the sponsee owns the object. + if (!isLedgerEntryOwner(view(), *objectSle, sponseeID)) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ownerSle = view().peek(keylet::account(sponseeID)); + if (!ownerSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ownerCountDelta = + static_cast(getLedgerEntryOwnerCount(*objectSle)); + auto const& sponsorField = getLedgerEntrySponsorField(*objectSle, sponseeID); + + if (isCreate || isReassign) + { + auto const newSponsor = ctx_.tx[~sfSponsor]; + XRPL_ASSERT( + newSponsor.has_value(), + "xrpl::SponsorshipTransfer::doApply : sfSponsor present for object sponsor " + "create/reassign"); + if (!newSponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const newSponsorID = *newSponsor; + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Check if new sponsor has sufficient balance + // NOLINTNEXTLINE(readability-suspicious-call-argument) + if (auto const ter = checkReserve( + ctx_.getApplyViewContext(), + sponseeSle, + sponseeSle->getFieldAmount(sfBalance).xrp(), + newSponsorSle, + {.ownerCountDelta = ownerCountDelta}, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + + if (isCreate) + { + // Update owner's sponsored count + if (auto const ter = incrementSponsorCount( + view(), ownerSle, sfSponsoredOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + else if (isReassign) + { + auto const oldSponsorID = objectSle->getAccountID(sponsorField); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Decrement old sponsor's sponsoring count + if (auto const ter = decrementSponsorCount( + view(), oldSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + + // Increment new sponsor's sponsoring count + if (auto const ter = incrementSponsorCount( + view(), newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Object is now sponsored by new sponsor + objectSle->setAccountID(sponsorField, newSponsorID); + view().update(objectSle); + + auto const sponsorshipSle = view().peek(keylet::sponsorship(newSponsorID, sponseeID)); + if (sponsorshipSle) + { + // Update ReserveCount for sponsorship object if it exists + if (auto const ter = + decrementPrefundedReserveCount(view(), sponsorshipSle, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + } + else if (ctx_.tx.isFlag(tfSponsorshipEnd)) + { + // End object sponsor + auto const oldSponsorID = objectSle->getAccountID(sponsorField); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // The owner reclaims the reserve burden when the object is no longer sponsored. + // We do not check the sponsee's reserve here (via `checkReserve`) so that a sponsor can + // always end a sponsorship, even if the sponsee lacks sufficient reserve. + + // Decrement sponsored count + if (auto const ter = decrementSponsorCount( + view(), sponseeSle, sfSponsoredOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Decrement old sponsoring count + if (auto const ter = decrementSponsorCount( + view(), oldSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Remove sponsor from object + objectSle->makeFieldAbsent(sponsorField); + view().update(objectSle); + } + } + else + { + // Account-level sponsorship is always co-signed (preflight requires + // sfSponsorSignature), so there is no pre-funded budget to draw down here. + if (isCreate || isReassign) + { + auto const newSponsor = ctx_.tx[~sfSponsor]; + XRPL_ASSERT( + newSponsor.has_value(), + "xrpl::SponsorshipTransfer::doApply : sfSponsor present for account sponsor " + "create/reassign"); + if (!newSponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const newSponsorID = *newSponsor; + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (auto const ter = checkReserve( + ctx_.getApplyViewContext(), + sponseeSle, + sponseeSle->getFieldAmount(sfBalance).xrp(), + newSponsorSle, + {.accountCountDelta = 1}, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + + if (isReassign) + { + auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Decrement old sponsoring count + if (auto const ter = + decrementSponsorCount(view(), oldSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + + // Increment new sponsoring count + if (auto const ter = + incrementSponsorCount(view(), newSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + + // Account is now sponsored by new sponsor + sponseeSle->setAccountID(sfSponsor, newSponsorID); + view().update(sponseeSle); + } + else if (ctx_.tx.isFlag(tfSponsorshipEnd)) + { + // End account sponsor + auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor); + if (!oldSponsorID) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // The sponsee must be able to hold its own account reserve after + // the sponsorship is removed. + if (auto const ter = checkReserve( + ctx_.getApplyViewContext(), + sponseeSle, + balanceBeforeFee(sponseeSle), + SLE::pointer(), + {.accountCountDelta = 1}, + ctx_.journal); + !isTesSuccess(ter)) + return ter; + + sponseeSle->makeFieldAbsent(sfSponsor); + view().update(sponseeSle); + + // Decrement account sponsoring count + if (auto const ter = + decrementSponsorCount(view(), oldSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + } + } + + return tesSUCCESS; +} + +void +SponsorshipTransfer::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +SponsorshipTransfer::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index ba54c14b1e..ccb113e07b 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -14,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -45,17 +46,11 @@ namespace xrpl { * * @param view The ledger view providing fee and state information. * @param tx The batch transaction to calculate the fee for. - * @return XRPAmount The total base fee required for the batch transaction. - * - * @throws std::overflow_error If any fee calculation would overflow the - * XRPAmount type. - * @throws std::length_error If the number of inner transactions or signers - * exceeds the allowed maximum. - * @throws std::invalid_argument If an inner transaction is itself a batch - * transaction. + * @return XRPAmount The total base fee required for the batch transaction, + * or std::nullopt on failure (overflow, oversized arrays). */ -XRPAmount -Batch::calculateBaseFee(ReadView const& view, STTx const& tx) +std::optional +Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx) { XRPAmount const maxAmount{std::numeric_limits::max()}; @@ -66,49 +61,26 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) if (baseFee > maxAmount - view.fees().base) { JLOG(debugLog().error()) << "BatchTrace: Base fee overflow detected."; - return XRPAmount{kInitialXrp}; + return std::nullopt; } // LCOV_EXCL_STOP XRPAmount const batchBase = view.fees().base + baseFee; - // Calculate the Inner Txn Fees + // Calculate the Inner Txn Fees. Inners are built and validated (count, + // no nesting) at construction, so they are reused here directly. XRPAmount txnFees{0}; - if (tx.isFieldPresent(sfRawTransactions)) + for (auto const& stx : tx.getBatchTransactions()) { - auto const& txns = tx.getFieldArray(sfRawTransactions); - + auto const fee = xrpl::calculateBaseFee(view, *stx); // LCOV_EXCL_START - if (txns.size() > kMaxBatchTxCount) + if (txnFees > maxAmount - fee) { - JLOG(debugLog().error()) << "BatchTrace: Raw Transactions array exceeds max entries."; - return XRPAmount{kInitialXrp}; + JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in txnFees calculation."; + return std::nullopt; } // LCOV_EXCL_STOP - - for (STObject txn : txns) - { - STTx const stx = STTx{std::move(txn)}; - - // LCOV_EXCL_START - if (stx.getTxnType() == ttBATCH) - { - JLOG(debugLog().error()) << "BatchTrace: Inner Batch transaction found."; - return XRPAmount{kInitialXrp}; - } - // LCOV_EXCL_STOP - - auto const fee = xrpl::calculateBaseFee(view, stx); - // LCOV_EXCL_START - if (txnFees > maxAmount - fee) - { - JLOG(debugLog().error()) - << "BatchTrace: XRPAmount overflow in txnFees calculation."; - return XRPAmount{kInitialXrp}; - } - // LCOV_EXCL_STOP - txnFees += fee; - } + txnFees += fee; } // Calculate the Signers/BatchSigners Fees @@ -121,7 +93,7 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) if (signers.size() > kMaxBatchSigners) { JLOG(debugLog().error()) << "BatchTrace: Batch Signers array exceeds max entries."; - return XRPAmount{kInitialXrp}; + return std::nullopt; } // LCOV_EXCL_STOP @@ -139,7 +111,7 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) { JLOG(debugLog().error()) << "BatchTrace: Nested Signers array exceeds max entries."; - return kInitialXrp; + return std::nullopt; } // LCOV_EXCL_STOP signerCount += nestedSigners.size(); @@ -151,7 +123,7 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) if (signerCount > 0 && view.fees().base > maxAmount / signerCount) { JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerCount calculation."; - return XRPAmount{kInitialXrp}; + return std::nullopt; } // LCOV_EXCL_STOP @@ -161,13 +133,13 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) if (signerFees > maxAmount - txnFees) { JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerFees calculation."; - return XRPAmount{kInitialXrp}; + return std::nullopt; } XRPAmount const innerFees = txnFees + signerFees; if (innerFees > maxAmount - batchBase) { JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in total fee calculation."; - return XRPAmount{kInitialXrp}; + return std::nullopt; } // LCOV_EXCL_STOP @@ -175,6 +147,24 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) return innerFees + batchBase; } +XRPAmount +Batch::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + if (auto const fee = calculateBaseFeeImpl(view, tx)) + return *fee; + // The fee could not be computed, so return a placeholder the account can + // pay; preclaim rejects the transaction with tecINSUFF_FEE. + return view.fees().base; // LCOV_EXCL_LINE +} + +TER +Batch::preclaim(PreclaimContext const& ctx) +{ + if (!calculateBaseFeeImpl(ctx.view, ctx.tx)) + return tecINSUFF_FEE; // LCOV_EXCL_LINE + return tesSUCCESS; +} + std::uint32_t Batch::getFlagsMask(PreflightContext const& ctx) { @@ -227,6 +217,16 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (ctx.tx.isFieldPresent(sfSponsorFlags)) + { + if (isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "spfSponsorReserve is not allowed on outer Batch."; + return temINVALID_FLAG; + } + } + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); if (rawTxns.size() <= 1) { @@ -235,13 +235,6 @@ Batch::preflight(PreflightContext const& ctx) return temARRAY_EMPTY; } - if (rawTxns.size() > kMaxBatchTxCount) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" - << "txns array exceeds 8 entries."; - return temARRAY_TOO_LARGE; - } - if (ctx.tx.isFieldPresent(sfBatchSigners) && ctx.tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners) { @@ -282,9 +275,9 @@ Batch::preflight(PreflightContext const& ctx) return tesSUCCESS; }; - for (STObject rb : rawTxns) + for (auto const& stxPtr : ctx.tx.getBatchTransactions()) { - STTx const stx = STTx{std::move(rb)}; + STTx const& stx = *stxPtr; auto const hash = stx.getTransactionID(); if (!uniqueHashes.emplace(hash).second) { @@ -295,14 +288,6 @@ Batch::preflight(PreflightContext const& ctx) } auto const txType = stx.getFieldU16(sfTransactionType); - if (txType == ttBATCH) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "batch cannot have an inner batch txn. " - << "txID: " << hash; - return temINVALID; - } - if (std::ranges::any_of( kDisabledTxTypes, [txType](auto const& disabled) { return txType == disabled; })) { @@ -331,6 +316,14 @@ Batch::preflight(PreflightContext const& ctx) return ret; } } + if (stx.isFieldPresent(sfSponsorSignature)) + { + auto const sponsorSignature = stx.getFieldObject(sfSponsorSignature); + if (auto const ret = checkSignatureFields(sponsorSignature, hash, "sponsor signature ")) + { + return ret; + } + } // Check that the Fee is native asset (XRP) and zero if (auto const fee = stx.getFieldAmount(sfFee); !fee.native() || fee.xrp() != beast::kZero) @@ -341,6 +334,10 @@ Batch::preflight(PreflightContext const& ctx) return temBAD_FEE; } + // Disallow fee sponsorship on Batch inner txs + if (stx.isFieldPresent(sfSponsor) && isFeeSponsored(stx)) + return temINVALID_FLAG; + auto const innerAccount = stx.getAccountID(sfAccount); if (auto const preflightResult = xrpl::preflight(ctx.registry, ctx.rules, parentBatchId, stx, TapBatch, ctx.j); @@ -411,18 +408,17 @@ Batch::preflightSigValidated(PreflightContext const& ctx) ctx.tx.getTxnType() == ttBATCH, "xrpl::Batch::preflightSigValidated : batch transaction"); auto const parentBatchId = ctx.tx.getTransactionID(); auto const outerAccount = ctx.tx.getAccountID(sfAccount); - auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); - // Accounts that must sign the batch: each inner authorizer and counterparty // (excluding the outer account), sorted and de-duplicated to match against // the ascending, unique batch signers. std::vector requiredSigners; requiredSigners.reserve(kMaxBatchSigners); - for (STObject const& rb : rawTxns) + for (auto const& stxPtr : ctx.tx.getBatchTransactions()) { + STTx const& rb = *stxPtr; // A delegated inner is signed by the delegate, not the account holder, // so the delegate is the required signer when present. - AccountID const authorizer = rb.getFeePayer(); + AccountID const authorizer = rb.getInitiator(); // The outer account signs the batch itself, so it is never added to the // required signers. @@ -433,6 +429,10 @@ Batch::preflightSigValidated(PreflightContext const& ctx) if (auto const counterparty = rb[~sfCounterparty]; counterparty && counterparty != outerAccount) requiredSigners.push_back(*counterparty); + + if (auto const sponsor = rb.at(~sfSponsor); + sponsor && rb.isFieldPresent(sfSponsorSignature) && sponsor != outerAccount) + requiredSigners.push_back(*sponsor); } std::ranges::sort(requiredSigners); auto const dupes = std::ranges::unique(requiredSigners); diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index 73a72c217d..e19dc9fe96 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -75,13 +75,12 @@ TicketCreate::doApply() // check the starting balance because we want to allow dipping into the // reserve to pay fees. std::uint32_t const ticketCount = ctx_.tx[sfTicketCount]; - { - XRPAmount const reserve = - view().fees().accountReserve(sleAccountRoot->getFieldU32(sfOwnerCount) + ticketCount); - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + if (preFeeBalance_ < accountReserve( + view(), + sleAccountRoot, + j_, + {.ownerCountDelta = static_cast(ticketCount)})) + return tecINSUFFICIENT_RESERVE; beast::Journal const viewJ{ctx_.registry.get().getJournal("View")}; @@ -105,6 +104,7 @@ TicketCreate::doApply() sleTicket->setAccountID(sfAccount, accountID_); sleTicket->setFieldU32(sfTicketSequence, curTicketSeq); + view().insert(sleTicket); auto const page = view().dirInsert( @@ -125,7 +125,7 @@ TicketCreate::doApply() sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount); // Every added Ticket counts against the creator's reserve. - adjustOwnerCount(view(), sleAccountRoot, ticketCount, viewJ); + increaseOwnerCount(view(), sleAccountRoot, {}, ticketCount, viewJ); // TicketCreate is the only transaction that can cause an account root's // Sequence field to increase by more than one. October 2018. diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp index e0d1d28a8f..6366e99105 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -22,9 +21,6 @@ namespace xrpl { NotTEC ConfidentialMPTClawback::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureConfidentialTransfer)) - return temDISABLED; - auto const account = ctx.tx[sfAccount]; // Only issuer can clawback diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp index 44e2596325..454eb39ead 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -26,9 +25,6 @@ namespace xrpl { NotTEC ConfidentialMPTConvert::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureConfidentialTransfer)) - return temDISABLED; - // issuer cannot convert if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) return temMALFORMED; diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index d6fed78833..87f9e476d6 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -25,9 +24,6 @@ namespace xrpl { NotTEC ConfidentialMPTConvertBack::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureConfidentialTransfer)) - return temDISABLED; - // issuer cannot convert back if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) return temMALFORMED; diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp index 02c759c521..0b98382a61 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -24,9 +23,6 @@ namespace xrpl { NotTEC ConfidentialMPTMergeInbox::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureConfidentialTransfer)) - return temDISABLED; - // issuer cannot merge if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) return temMALFORMED; diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index 302b7d239b..d121ec2634 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -31,9 +31,6 @@ ConfidentialMPTSend::checkExtraFeatures(PreflightContext const& ctx) NotTEC ConfidentialMPTSend::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureConfidentialTransfer)) - return temDISABLED; - auto const account = ctx.tx[sfAccount]; auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer(); diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp index 6db37515cb..0aeb6f33d1 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -164,7 +164,7 @@ MPTokenAuthorize::doApply() { auto const& tx = ctx_.tx; return authorizeMPToken( - ctx_.view(), + ctx_.getApplyViewContext(), preFeeBalance_, tx[sfMPTokenIssuanceID], accountID_, diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index e30127b688..aad1642f68 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -113,22 +114,35 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) } std::expected -MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args) +MPTokenIssuanceCreate::create( + ApplyViewContext ctx, + beast::Journal journal, + MPTCreateArgs const& args) { - auto const acct = view.peek(keylet::account(args.account)); + auto const acct = ctx.view.peek(keylet::account(args.account)); if (!acct) return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE - if (args.priorBalance && - *(args.priorBalance) < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) - return std::unexpected(tecINSUFFICIENT_RESERVE); + // A reserve sponsor only covers tx.Account's own objects. + auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, acct); + if (!sponsorExp) + return std::unexpected(sponsorExp.error()); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + + if (args.priorBalance) + { + if (auto const ret = checkReserve( + ctx, acct, *(args.priorBalance), sponsorSle, {.ownerCountDelta = 1}, journal); + !isTesSuccess(ret)) + return std::unexpected(ret); + } auto const mptId = makeMptID(args.sequence, args.account); auto const mptIssuanceKeylet = keylet::mptokenIssuance(mptId); // create the MPTokenIssuance { - auto const ownerNode = view.dirInsert( + auto const ownerNode = ctx.view.dirInsert( keylet::ownerDir(args.account), mptIssuanceKeylet, describeOwnerDir(args.account)); if (!ownerNode) @@ -166,7 +180,7 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate // populate this after the pseudo-account's MPToken / // RippleState has been installed. A missing holding here // would dangle the pointer and is a programmer error. - auto const sleHolding = view.read(keylet::unchecked(*args.referenceHolding)); + auto const sleHolding = ctx.view.read(keylet::unchecked(*args.referenceHolding)); if (!sleHolding) return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE auto const type = sleHolding->getType(); @@ -175,11 +189,13 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate (*mptIssuance)[sfReferenceHolding] = *args.referenceHolding; } - view.insert(mptIssuance); + addSponsorToLedgerEntry(mptIssuance, sponsorSle); + + ctx.view.insert(mptIssuance); } // Update owner count. - adjustOwnerCount(view, acct, 1, journal); + increaseOwnerCount(ctx.view, acct, sponsorSle, 1, journal); return mptId; } @@ -189,7 +205,7 @@ MPTokenIssuanceCreate::doApply() { auto const& tx = ctx_.tx; auto const result = create( - view(), + ctx_.getApplyViewContext(), j_, { .priorBalance = preFeeBalance_, diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp index c56697767b..05d39c3596 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp @@ -49,10 +49,9 @@ MPTokenIssuanceDestroy::doApply() if (!view().dirRemove(keylet::ownerDir(accountID_), (*mpt)[sfOwnerNode], mpt->key(), false)) return tefBAD_LEDGER; // LCOV_EXCL_LINE + decreaseOwnerCountForObject(view(), accountID_, mpt, 1, j_); view().erase(mpt); - adjustOwnerCount(view(), view().peek(keylet::account(accountID_)), -1, j_); - return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index e79e3cad5a..3d078ce825 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -297,8 +298,6 @@ TrustSet::doApply() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - std::uint32_t const uOwnerCount = sle->getFieldU32(sfOwnerCount); - // The reserve that is required to create the line. Note // that although the reserve increases with every item // an account owns, in the case of trust lines we only @@ -317,9 +316,23 @@ TrustSet::doApply() // well. A person with no intention of using the gateway // could use the extra XRP for their own purposes. + auto const sponsorExp = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!sponsorExp) + return sponsorExp.error(); // LCOV_EXCL_LINE + auto const sponsorSle = *sponsorExp; + + auto getSponsor = [&sponsorSle, this](AccountID const& account) { + return (sponsorSle && account == accountID_) ? sponsorSle : SLE::pointer(); + }; + + // The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor. + // With any sponsor on the tx, the sponsor must cover the reserve (via balance or + // prefunded budget), so the reserve check always runs. + bool const freeTrustLine = !sponsorSle && (ownerCount(sle, j_) < 2); + std::uint32_t const uOwnerCount = ownerCount(sle, j_); XRPAmount const reserveCreate( (uOwnerCount < 2) ? XRPAmount(beast::kZero) - : view().fees().accountReserve(uOwnerCount + 1)); + : accountReserve(view(), sle, j_, {.ownerCountDelta = 1})); std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0); @@ -505,6 +518,11 @@ TrustSet::doApply() bool bReserveIncrease = false; + auto const currentHighSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfHighSponsor); + auto const currentLowSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfLowSponsor); + if (bSetAuth) { uFlagsOut |= (bHigh ? lsfHighAuth : lsfLowAuth); @@ -512,10 +530,30 @@ TrustSet::doApply() if (bLowReserveSet && !bLowReserved) { + SLE::pointer const lowSponsor = getSponsor(uLowAccountID); + + if (view().rules().enabled(featureSponsor)) + { + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleLowAccount, + preFeeBalance_, + lowSponsor, + {.ownerCountDelta = 1}, + j_, + tecINSUF_RESERVE_LINE); + lowSponsor && !isTesSuccess(ret)) + { + return ret; + } + } + // Set reserve for low account. - adjustOwnerCount(view(), sleLowAccount, 1, viewJ); + increaseOwnerCount(view(), sleLowAccount, lowSponsor, 1, viewJ); uFlagsOut |= lsfLowReserve; + addSponsorToLedgerEntry(sleRippleState, lowSponsor, sfLowSponsor); + if (!bHigh) bReserveIncrease = true; } @@ -523,16 +561,41 @@ TrustSet::doApply() if (bLowReserveClear && bLowReserved) { // Clear reserve for low account. - adjustOwnerCount(view(), sleLowAccount, -1, viewJ); + decreaseOwnerCount(view(), sleLowAccount, currentLowSponsor, 1, viewJ); uFlagsOut &= ~lsfLowReserve; + + removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor); } if (bHighReserveSet && !bHighReserved) { + SLE::pointer const highSponsor = getSponsor(uHighAccountID); + + // should be checked PreFunded Sponsor before increaseOwnerCount() + // For PreFunded sponsors, we need to check if there are sufficient reserves before + // calling increaseOwnerCount(). + if (view().rules().enabled(featureSponsor)) + { + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleHighAccount, + preFeeBalance_, + highSponsor, + {.ownerCountDelta = 1}, + j_, + tecINSUF_RESERVE_LINE); + highSponsor && !isTesSuccess(ret)) + { + return ret; + } + } + // Set reserve for high account. - adjustOwnerCount(view(), sleHighAccount, 1, viewJ); + increaseOwnerCount(view(), sleHighAccount, highSponsor, 1, viewJ); uFlagsOut |= lsfHighReserve; + addSponsorToLedgerEntry(sleRippleState, highSponsor, sfHighSponsor); + if (bHigh) bReserveIncrease = true; } @@ -540,34 +603,75 @@ TrustSet::doApply() if (bHighReserveClear && bHighReserved) { // Clear reserve for high account. - adjustOwnerCount(view(), sleHighAccount, -1, viewJ); + decreaseOwnerCount(view(), sleHighAccount, currentHighSponsor, 1, viewJ); uFlagsOut &= ~lsfHighReserve; + + removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor); } if (uFlagsIn != uFlagsOut) sleRippleState->setFieldU32(sfFlags, uFlagsOut); - if (bDefault || badCurrency() == currency) + if (view().rules().enabled(featureSponsor)) { - // Delete. + if (bDefault || badCurrency() == currency) + { + // Delete. - terResult = trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); - } - // Reserve is not scaled by load. - else if (bReserveIncrease && preFeeBalance_ < reserveCreate) - { - JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " - "add trust line."; + terResult = + trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); + } + // Reserve is not scaled by load + else if ( + auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sle, + preFeeBalance_, + sponsorSle, + {}, + j_, + tecINSUF_RESERVE_LINE); + !freeTrustLine && bReserveIncrease && !isTesSuccess(ret)) + { + JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " + "add trust line."; - // Another transaction could provide XRP to the account and then - // this transaction would succeed. - terResult = tecINSUF_RESERVE_LINE; + // Another transaction could provide XRP to the account and then + // this transaction would succeed. + terResult = ret; + } + else + { + view().update(sleRippleState); + + JLOG(j_.trace()) << "Modify ripple line"; + } } else { - view().update(sleRippleState); + if (bDefault || badCurrency() == currency) + { + // Delete. - JLOG(j_.trace()) << "Modify ripple line"; + terResult = + trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); + } + // Reserve is not scaled by load. + else if (bReserveIncrease && preFeeBalance_ < reserveCreate) + { + JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " + "add trust line."; + + // Another transaction could provide XRP to the account and then + // this transaction would succeed. + terResult = tecINSUF_RESERVE_LINE; + } + else + { + view().update(sleRippleState); + + JLOG(j_.trace()) << "Modify ripple line"; + } } } // Line does not exist. @@ -582,8 +686,8 @@ TrustSet::doApply() JLOG(j_.trace()) << "Redundant: Setting non-existent ripple line to defaults."; return tecNO_LINE_REDUNDANT; } - else if (preFeeBalance_ < reserveCreate) // Reserve is not scaled by - // load. + // reserve is not scaled by load + else if (!view().rules().enabled(featureSponsor) && preFeeBalance_ < reserveCreate) { JLOG(j_.trace()) << "Delay transaction: Line does not exist. " "Insufficent reserve to create line."; @@ -592,6 +696,25 @@ TrustSet::doApply() // transaction would succeed. terResult = tecNO_LINE_INSUF_RESERVE; } + else if ( + auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sle, + preFeeBalance_, + sponsorSle, + {.ownerCountDelta = 1}, + j_, + tecNO_LINE_INSUF_RESERVE); + view().rules().enabled(featureSponsor) && !freeTrustLine && + !isTesSuccess(ret)) // Reserve is not scaled by load. + { + JLOG(j_.trace()) << "Delay transaction: Line does not exist. " + "Insufficent reserve to create line."; + + // Another transaction could create the account and then this + // transaction would succeed. + terResult = ret; + } else { // Zero balance in currency. @@ -617,6 +740,7 @@ TrustSet::doApply() saLimitAllow, // Limit for who is being charged. uQualityIn, uQualityOut, + sponsorSle, viewJ); } diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp index 541660c98f..d77286b667 100644 --- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp @@ -389,8 +389,8 @@ VaultClawback::doApply() auto const& vaultAccount = vault->at(sfAccount); // Transfer shares from holder to vault. - if (auto const ter = - accountSend(view(), holder, vaultAccount, sharesDestroyed, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), holder, vaultAccount, sharesDestroyed, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -399,7 +399,8 @@ VaultClawback::doApply() // Keep MPToken if holder is the vault owner. if (holder != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_); + if (auto const ter = + removeEmptyHolding(ctx_.getApplyViewContext(), holder, sharesDestroyed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // @@ -425,7 +426,7 @@ VaultClawback::doApply() { // Transfer assets from vault to issuer. if (auto const ter = accountSend( - view(), vaultAccount, accountID_, assetsRecovered, j_, WaiveTransferFee::Yes); + view(), vaultAccount, accountID_, assetsRecovered, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index d262132b6f..e1f5873a89 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -146,6 +146,7 @@ VaultCreate::doApply() // we can consider downgrading them to `tef` or `tem`. auto const& tx = ctx_.tx; + auto applyViewContext = ctx_.getApplyViewContext(); auto const sequence = tx.getSeqValue(); auto const owner = view().peek(keylet::account(accountID_)); if (owner == nullptr) @@ -156,9 +157,8 @@ VaultCreate::doApply() if (auto ter = dirLink(view(), accountID_, vault)) return ter; // We will create Vault and PseudoAccount, hence increase OwnerCount by 2 - adjustOwnerCount(view(), owner, 2, j_); - auto const ownerCount = owner->at(sfOwnerCount); - if (preFeeBalance_ < view().fees().accountReserve(ownerCount)) + increaseOwnerCount(view(), owner, {}, 2, j_); + if (preFeeBalance_ < accountReserve(view(), owner, j_)) return tecINSUFFICIENT_RESERVE; auto maybePseudo = createPseudoAccount(view(), vault->key(), sfVaultID); @@ -168,7 +168,8 @@ VaultCreate::doApply() AccountID const pseudoId = pseudo->at(sfAccount); auto const asset = tx[sfAsset]; - if (auto ter = addEmptyHolding(view(), pseudoId, preFeeBalance_, asset, j_); !isTesSuccess(ter)) + if (auto ter = addEmptyHolding(applyViewContext, pseudoId, preFeeBalance_, asset, j_); + !isTesSuccess(ter)) return ter; std::uint8_t const scale = (asset.holds() || asset.native()) @@ -197,7 +198,7 @@ VaultCreate::doApply() : keylet::trustLine(pseudoId, asset.get()).key; }(); auto const maybeShare = MPTokenIssuanceCreate::create( - view(), + applyViewContext, j_, { .priorBalance = std::nullopt, @@ -243,8 +244,8 @@ VaultCreate::doApply() view().insert(vault); // Explicitly create MPToken for the vault owner - if (auto const err = - authorizeMPToken(view(), preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); + if (auto const err = authorizeMPToken( + applyViewContext, preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); !isTesSuccess(err)) return err; @@ -252,7 +253,13 @@ VaultCreate::doApply() if (tx.isFlag(tfVaultPrivate)) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, accountID_); + applyViewContext, + preFeeBalance_, + mptIssuanceID, + pseudoId, + ctx_.journal, + {}, + accountID_); !isTesSuccess(err)) return err; } diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index fa38ae278b..497a2f2465 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -96,13 +96,15 @@ TER VaultDelete::doApply() { auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE // Destroy the asset holding. auto asset = vault->at(sfAsset); - if (auto ter = removeEmptyHolding(view(), vault->at(sfAccount), asset, j_); !isTesSuccess(ter)) + if (auto ter = removeEmptyHolding(applyViewContext, vault->at(sfAccount), asset, j_); + !isTesSuccess(ter)) return ter; auto const& pseudoID = vault->at(sfAccount); @@ -130,7 +132,8 @@ VaultDelete::doApply() // Try to remove MPToken for vault shares for the vault owner if it exists. if (auto const mptoken = view().peek(keylet::mptoken(shareMPTID, accountID_))) { - if (auto const ter = removeEmptyHolding(view(), accountID_, MPTIssue(shareMPTID), j_); + if (auto const ter = + removeEmptyHolding(applyViewContext, accountID_, MPTIssue(shareMPTID), j_); !isTesSuccess(ter)) { // LCOV_EXCL_START @@ -151,7 +154,7 @@ VaultDelete::doApply() return tefBAD_LEDGER; // LCOV_EXCL_STOP } - adjustOwnerCount(view(), pseudoAcct, -1, j_); + decreaseOwnerCountForObject(view(), pseudoAcct, mpt, 1, j_); view().erase(mpt); @@ -210,7 +213,7 @@ VaultDelete::doApply() } // We are destroying Vault and PseudoAccount, hence decrease by 2 - adjustOwnerCount(view(), owner, -2, j_); + decreaseOwnerCountForObject(view(), owner, vault, 2, j_); // Destroy the vault. view().erase(vault); diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index d5eb80b84d..aa9cfc8537 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -23,6 +23,7 @@ #include #include +#include #include namespace xrpl { @@ -196,6 +197,7 @@ VaultDeposit::doApply() { bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0); auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE auto const vaultAsset = vault->at(sfAsset); @@ -230,7 +232,7 @@ VaultDeposit::doApply() if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner)) { if (auto const err = enforceMPTokenAuthorization( - ctx_.view(), mptIssuanceID, accountID_, preFeeBalance_, j_); + applyViewContext, mptIssuanceID, accountID_, preFeeBalance_, j_); !isTesSuccess(err)) return err; } @@ -240,7 +242,11 @@ VaultDeposit::doApply() if (!view().exists(keylet::mptoken(mptIssuanceID, accountID_))) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID->value(), accountID_, ctx_.journal); + applyViewContext, + preFeeBalance_, + mptIssuanceID->value(), + accountID_, + ctx_.journal); !isTesSuccess(err)) return err; } @@ -252,7 +258,7 @@ VaultDeposit::doApply() XRPL_ASSERT( accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner"); if (auto const err = authorizeMPToken( - view(), + applyViewContext, preFeeBalance_, // priorBalance mptIssuanceID->value(), // mptIssuanceID sleIssuance->at(sfIssuer), // account @@ -319,7 +325,7 @@ VaultDeposit::doApply() // Transfer assets from depositor to vault. if (auto const ter = accountSend( - view(), accountID_, vaultAccount, assetsDeposited, j_, WaiveTransferFee::Yes); + view(), accountID_, vaultAccount, assetsDeposited, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -346,8 +352,8 @@ VaultDeposit::doApply() } // Transfer shares from vault to depositor. - if (auto const ter = - accountSend(view(), vaultAccount, accountID_, sharesCreated, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), vaultAccount, accountID_, sharesCreated, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index ad91c55723..353b72c30d 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -194,6 +194,7 @@ TER VaultWithdraw::doApply() { auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + auto applyViewContext = ctx_.getApplyViewContext(); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE @@ -349,9 +350,10 @@ VaultWithdraw::doApply() view().update(vault); auto const& vaultAccount = vault->at(sfAccount); + // Transfer shares from depositor to vault. if (auto const ter = accountSend( - view(), accountID_, vaultAccount, sharesRedeemed, j_, WaiveTransferFee::Yes); + view(), accountID_, vaultAccount, sharesRedeemed, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -360,7 +362,8 @@ VaultWithdraw::doApply() // Keep MPToken if holder is the vault owner. if (accountID_ != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), accountID_, sharesRedeemed.asset(), j_); + if (auto const ter = + removeEmptyHolding(applyViewContext, accountID_, sharesRedeemed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // @@ -386,7 +389,7 @@ VaultWithdraw::doApply() auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_); return doWithdraw( - view(), ctx_.tx, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_); + applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_); } void diff --git a/src/test/app/AMMCalc_test.cpp b/src/test/app/AMMCalc_test.cpp index 3c4a62bfd3..74080e669c 100644 --- a/src/test/app/AMMCalc_test.cpp +++ b/src/test/app/AMMCalc_test.cpp @@ -31,7 +31,8 @@ namespace xrpl::test { -/** AMM Calculator. Uses AMM formulas to simulate the payment engine +/** + * AMM Calculator. Uses AMM formulas to simulate the payment engine * expected results. Assuming the formulas are correct some unit-tests can * be verified. Currently supported operations are: * - swapIn, find out given in. in can flow through multiple AMM/Offer steps. diff --git a/src/test/app/AMMExtendedMPT_test.cpp b/src/test/app/AMMExtendedMPT_test.cpp index e6dcc95733..f04ea39f2b 100644 --- a/src/test/app/AMMExtendedMPT_test.cpp +++ b/src/test/app/AMMExtendedMPT_test.cpp @@ -463,7 +463,7 @@ private: // Provide micro amounts to compensate for fees to make results round // nice. auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(2) + env.current()->fees().base * 3; + XRP(100) + env.current()->fees().accountReserve(2, 1) + env.current()->fees().base * 3; env.fund(startingXrp, gw_, alice_); env.fund(XRP(2'000), bob_); @@ -2296,8 +2296,10 @@ private: // 1,400e12 - 56.3368e12*1.25 = 1400e12 - 70.4210e12 = // 1329.5789e12GBP env.require(Balance(alice_, gbp(1'329'578'947'368'420))); - //// 25% on 56.3368e12ETH is paid in tr fee 56.3368e12*1.25 - ///= 70.4210e12ETH + /** + * / 25% on 56.3368e12ETH is paid in tr fee 56.3368e12*1.25 + * = 70.4210e12ETH + */ // 56.3368e12GBP is swapped in for 53.3322e12ETH BEAST_EXPECT(amm.expectBalances( gbp(1'056'336'842'105'264), eth(946'667'729'591'836), amm.tokens())); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index a0a7d0fb15..bb532b361a 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -546,7 +546,7 @@ private: // 1 for each trust limit == 3 (alice_ < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(3) + env.current()->fees().base * 4; + XRP(100) + env.current()->fees().accountReserve(3, 1) + env.current()->fees().base * 4; env.fund(startingXrp, gw1, gw2, gw3, localAlice); env.fund(XRP(2'000), localBob); @@ -2420,8 +2420,10 @@ private: // 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP BEAST_EXPECT( expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'329'578947368421), -12})); - //// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 - ///= 70.4210EUR + /** + * / 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 + * = 70.4210EUR + */ // 56.3368GBP is swapped in for 53.3322EUR BEAST_EXPECT(amm.expectBalances( STAmount{GBP, UINT64_C(1'056'336842105263), -12}, @@ -2435,8 +2437,10 @@ private: // 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP BEAST_EXPECT( expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'329'57894736842), -11})); - //// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 - ///= 70.4210EUR + /** + * / 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 + * = 70.4210EUR + */ // 56.3368GBP is swapped in for 53.3322EUR BEAST_EXPECT(amm.expectBalances( STAmount{GBP, UINT64_C(1'056'336842105264), -12}, diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 0787db310e..9c70e1873c 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -1002,7 +1002,7 @@ private: // Insufficient reserve, XRP/MPT { - Env env(*this); + Env env(*this, features); auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 4; env.fund(XRP(10'000), gw_); env.fund(XRP(10'000), alice_); diff --git a/src/test/app/AccountSet_test.cpp b/src/test/app/AccountSet_test.cpp index 362113aec2..d94fb66e50 100644 --- a/src/test/app/AccountSet_test.cpp +++ b/src/test/app/AccountSet_test.cpp @@ -1,4 +1,3 @@ - #include #include #include @@ -453,7 +452,7 @@ public: // We'll insert a replacement for the account root // with the higher (currently invalid) transfer rate. - auto replacement = std::make_shared(*sle, sle->key()); + auto replacement = std::make_shared(*sle); (*replacement)[sfTransferRate] = static_cast(transferRate * QUALITY_ONE); view.rawReplace(replacement); diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index f9c156fe06..5085ad6172 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -56,7 +56,6 @@ #include #include #include -#include #include #include #include @@ -72,6 +71,7 @@ #include #include #include +#include #include #include #include @@ -313,8 +313,8 @@ class Batch_test : public beast::unit_test::Suite env.close(); } - // DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. - // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + // An oversized batch (more than kMaxBatchTxCount inners) fails STTx + // construction, so the transaction cannot be built. { auto const seq = env.seq(alice); auto const batchFee = batch::calcBatchFee(env, 0, 9); @@ -328,7 +328,7 @@ class Batch_test : public beast::unit_test::Suite batch::Inner(pay(alice, bob, XRP(1)), seq + 7), batch::Inner(pay(alice, bob, XRP(1)), seq + 8), batch::Inner(pay(alice, bob, XRP(1)), seq + 9), - Ter(telENV_RPC_FAILED)); + Ter(temMALFORMED)); env.close(); } @@ -345,15 +345,15 @@ class Batch_test : public beast::unit_test::Suite env.close(); } - // DEFENSIVE: temINVALID: Batch: batch cannot have inner batch txn. - // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + // A batch may not contain a batch: the nested inner fails STTx + // construction, so the transaction cannot be built. { auto const seq = env.seq(alice); auto const batchFee = batch::calcBatchFee(env, 0, 2); env(batch::outer(alice, seq, batchFee, tfAllOrNothing), batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq), batch::Inner(pay(alice, bob, XRP(1)), seq + 2), - Ter(telENV_RPC_FAILED)); + Ter(temMALFORMED)); env.close(); } @@ -938,80 +938,41 @@ class Batch_test : public beast::unit_test::Suite env.fund(XRP(10000), alice, bob); + // An inner missing a required field can no longer be submitted: the + // outer STTx builds and validates each inner at construction, so + // building the batch (as signing does) throws. Returns true if the + // build fails. + auto batchCtorFails = [&](json::StaticString const& field) -> bool { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(field); + try + { + // Env::st swallows the construction failure and yields a null + // stx, so a malformed inner shows up as no transaction built. + auto const jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::Inner(pay(alice, bob, XRP(10)), seq + 2)); + return jt.stx == nullptr; + } + catch (std::exception const&) + { + return true; + } + }; + // Invalid: sfTransactionType - { - auto const batchFee = batch::calcBatchFee(env, 1, 2); - auto const seq = env.seq(alice); - auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1); - tx1.removeMember(jss::TransactionType); - auto jt = env.jtnofill( - batch::outer(alice, seq, batchFee, tfAllOrNothing), - tx1, - batch::Inner(pay(alice, bob, XRP(10)), seq + 2)); - - env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED)); - env.close(); - } - + BEAST_EXPECT(batchCtorFails(jss::TransactionType)); // Invalid: sfAccount - { - auto const batchFee = batch::calcBatchFee(env, 1, 2); - auto const seq = env.seq(alice); - auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1); - tx1.removeMember(jss::Account); - auto jt = env.jtnofill( - batch::outer(alice, seq, batchFee, tfAllOrNothing), - tx1, - batch::Inner(pay(alice, bob, XRP(10)), seq + 2)); - - env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED)); - env.close(); - } - + BEAST_EXPECT(batchCtorFails(jss::Account)); // Invalid: sfSequence - { - auto const batchFee = batch::calcBatchFee(env, 1, 2); - auto const seq = env.seq(alice); - auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1); - tx1.removeMember(jss::Sequence); - auto jt = env.jtnofill( - batch::outer(alice, seq, batchFee, tfAllOrNothing), - tx1, - batch::Inner(pay(alice, bob, XRP(10)), seq + 2)); - - env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED)); - env.close(); - } - + BEAST_EXPECT(batchCtorFails(jss::Sequence)); // Invalid: sfFee - { - auto const batchFee = batch::calcBatchFee(env, 1, 2); - auto const seq = env.seq(alice); - auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1); - tx1.removeMember(jss::Fee); - auto jt = env.jtnofill( - batch::outer(alice, seq, batchFee, tfAllOrNothing), - tx1, - batch::Inner(pay(alice, bob, XRP(10)), seq + 2)); - - env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED)); - env.close(); - } - + BEAST_EXPECT(batchCtorFails(jss::Fee)); // Invalid: sfSigningPubKey - { - auto const batchFee = batch::calcBatchFee(env, 1, 2); - auto const seq = env.seq(alice); - auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1); - tx1.removeMember(jss::SigningPubKey); - auto jt = env.jtnofill( - batch::outer(alice, seq, batchFee, tfAllOrNothing), - tx1, - batch::Inner(pay(alice, bob, XRP(10)), seq + 2)); - - env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED)); - env.close(); - } + BEAST_EXPECT(batchCtorFails(jss::SigningPubKey)); // Inner OfferCreate with MPT TakerPays. Valid under featureMPTokensV2. { @@ -1512,11 +1473,13 @@ class Batch_test : public beast::unit_test::Suite batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), - Ter(telENV_RPC_FAILED)); + Ter(temMALFORMED)); env.close(); } - // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + // An oversized batch (more than kMaxBatchTxCount inners) fails STTx + // construction, so it never reaches apply or checkValidity - Env::st + // swallows the failure and yields a null stx. { Env env{*this, features}; @@ -1539,11 +1502,44 @@ class Batch_test : public beast::unit_test::Suite batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), batch::Inner(pay(alice, bob, XRP(1)), aliceSeq)); - env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) { - auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j); - BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE); - return result.applied; - }); + BEAST_EXPECT(jt.stx == nullptr); + } + + // An oversized batch cannot slip through as validly signed even when it + // carries a BatchSigners array: the oversized inner array fails STTx + // construction before any signature is checked, so the batch can't be + // built at all (building it - as signing does - throws). + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 9); + bool threw = false; + try + { + env.jtnofill( + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::Sig(std::vector(kMaxBatchSigners + 1, bob))); + } + catch (std::exception const&) + { + threw = true; + } + BEAST_EXPECT(threw); } // Regression: the relay-boundary local check (isBatchRawTransactionOkay) @@ -1598,6 +1594,14 @@ class Batch_test : public beast::unit_test::Suite BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE); return result.applied; }); + + // Regression (uncapped batch-signer verification): the relay + // boundary (checkValidity) rejects the oversized signers array via + // the checkBatchSign guard, BEFORE verifying a single signature. + auto const [valid, reason] = + xrpl::checkValidity(env.app().getHashRouter(), *jt.stx, env.current()->rules()); + BEAST_EXPECT(valid == xrpl::Validity::SigBad); + BEAST_EXPECT(reason == "BatchSigners array exceeds max entries."); } } @@ -5524,7 +5528,8 @@ class Batch_test : public beast::unit_test::Suite return Batch::calculateBaseFee(*env.current(), *jtx.stx); }; - // bad: Inner Batch transaction found + // bad: a batch may not contain a batch - the nested inner fails STTx + // construction, so the transaction cannot be built. { auto const seq = env.seq(alice); XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2); @@ -5532,11 +5537,11 @@ class Batch_test : public beast::unit_test::Suite batch::outer(alice, seq, batchFee, tfAllOrNothing), batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq), batch::Inner(pay(alice, bob, XRP(1)), seq + 2)); - XRPAmount const txBaseFee = getBaseFee(jtx); - BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp)); + BEAST_EXPECT(jtx.stx == nullptr); } - // bad: Raw Transactions array exceeds max entries. + // bad: an oversized batch (more than kMaxBatchTxCount inners) fails + // STTx construction, so it cannot be built. { auto const seq = env.seq(alice); XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2); @@ -5553,8 +5558,7 @@ class Batch_test : public beast::unit_test::Suite batch::Inner(pay(alice, bob, XRP(1)), seq + 8), batch::Inner(pay(alice, bob, XRP(1)), seq + 9)); - XRPAmount const txBaseFee = getBaseFee(jtx); - BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp)); + BEAST_EXPECT(jtx.stx == nullptr); } // bad: Signers array exceeds max entries. @@ -5567,8 +5571,9 @@ class Batch_test : public beast::unit_test::Suite batch::Inner(pay(alice, bob, XRP(10)), seq + 1), batch::Inner(pay(alice, bob, XRP(5)), seq + 2), batch::Sig(std::vector(kMaxBatchSigners + 1, bob))); + // Failure paths fall back to the ledger base fee. XRPAmount const txBaseFee = getBaseFee(jtx); - BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp)); + BEAST_EXPECT(txBaseFee == env.current()->fees().base); } // good: diff --git a/src/test/app/CheckMPT_test.cpp b/src/test/app/CheckMPT_test.cpp index d4abe7f495..7161559038 100644 --- a/src/test/app/CheckMPT_test.cpp +++ b/src/test/app/CheckMPT_test.cpp @@ -410,7 +410,7 @@ class CheckMPT_test : public beast::unit_test::Suite // Insufficient reserve. Account const cheri{"cheri"}; - env.fund(env.current()->fees().accountReserve(1) - drops(1), cheri); + env.fund(env.current()->fees().accountReserve(1, 1) - drops(1), cheri); env(check::create(cheri, bob, usd(50)), Fee(drops(env.current()->fees().base)), @@ -820,15 +820,6 @@ class CheckMPT_test : public beast::unit_test::Suite env(check::create(alice, bob, usd(125))); env.close(); - // alice writes another check that won't get cashed until the transfer - // rate changes so we can see the rate applies when the check is - // cashed, not when it is created. -#if 0 - uint256 const chkId120{getCheckIndex(alice, env.Seq(alice))}; - env(check::create(alice, bob, USD(120))); - env.close(); -#endif - // bob attempts to cash the check for face value. Should fail. env(check::cash(bob, chkId125, usd(125)), Ter(tecPATH_PARTIAL)); env.close(); @@ -844,20 +835,6 @@ class CheckMPT_test : public beast::unit_test::Suite BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); -#if 0 - // Adjust gw's rate... - env(rate(gw, 1.2)); - env.close(); - - // bob cashes the second check for less than the face value. The new - // rate applies to the actual value transferred. - env(check::cash(bob, chkId120, USD(50))); - env.close(); - env.Require(Balance(alice, USD(1000 - 125 - 60))); - env.Require(Balance(bob, USD(0 + 100 + 50))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); -#endif // With the maximum transfer fee, this is the largest output whose // fee-adjusted debit is still within SendMax. @@ -1464,7 +1441,8 @@ class CheckMPT_test : public beast::unit_test::Suite return acct.id(); } - /** Create MPTTester if it doesn't exist for the given MPT. + /** + * Create MPTTester if it doesn't exist for the given MPT. * Increment owners if created since it creates MPTokenIssuance */ MPT diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 3ad1e3fa4e..840c06bd84 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -470,7 +470,7 @@ class Check_test : public beast::unit_test::Suite // Insufficient reserve. Account const cheri{"cheri"}; - env.fund(env.current()->fees().accountReserve(1) - drops(1), cheri); + env.fund(env.current()->fees().accountReserve(1, 1) - drops(1), cheri); env.close(); env(check::create(cheri, bob, usd(50)), @@ -1257,6 +1257,13 @@ class Check_test : public beast::unit_test::Suite env.close(); } + // Can't run pre-amendment behavior due to assertion failure. + if (features[fixCleanup3_3_0]) + { + env(check::cash(bob, uint256{}, usd(20)), Ter(temMALFORMED)); + env.close(); + } + // alice creates her checks ahead of time. uint256 const chkIdU{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(20))); @@ -1704,6 +1711,13 @@ class Check_test : public beast::unit_test::Suite // Non-existent check. env(check::cancel(bob, getCheckIndex(alice, env.seq(alice))), Ter(tecNO_ENTRY)); env.close(); + + // Can't run pre-amendment behavior due to assertion failure. + if (features[fixCleanup3_3_0]) + { + env(check::cancel(bob, uint256{}), Ter(temMALFORMED)); + env.close(); + } } void diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 456a53bc01..1f6ec012c8 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -638,8 +638,8 @@ struct Credentials_test : public beast::unit_test::Suite { Env env{*this, features}; - env.fund(drops(env.current()->fees().accountReserve(1)), issuer); - env.fund(drops(env.current()->fees().accountReserve(0)), subject); + env.fund(drops(env.current()->fees().accountReserve(1, 1)), issuer); + env.fund(drops(env.current()->fees().accountReserve(0, 1)), subject); env.close(); { diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 6f1d2ce7de..257ed33619 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -275,7 +275,7 @@ class Delegate_test : public beast::unit_test::Suite Account const bob{"bob"}; auto const txFee = env.current()->fees().base; - env.fund(env.current()->fees().accountReserve(0) + txFee, alice); + env.fund(env.current()->fees().accountReserve(0, 1) + txFee, alice); env.fund(XRP(100000), bob); env.close(); @@ -292,7 +292,7 @@ class Delegate_test : public beast::unit_test::Suite auto const txFee = env.current()->fees().base; - env.fund(env.current()->fees().accountReserve(1) + (txFee * 4), alice); + env.fund(env.current()->fees().accountReserve(1, 1) + (txFee * 4), alice); env.fund(XRP(100000), bob, carol); env.close(); @@ -318,8 +318,8 @@ class Delegate_test : public beast::unit_test::Suite Account const alice{"alice"}; Account const bob{"bob"}; - env.fund(drops(env.current()->fees().accountReserve(1)), alice); - env.fund(drops(env.current()->fees().accountReserve(2)), bob); + env.fund(drops(env.current()->fees().accountReserve(1, 1)), alice); + env.fund(drops(env.current()->fees().accountReserve(2, 1)), bob); env.close(); // alice gives bob permission @@ -422,7 +422,7 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const reserve = env.current()->fees().accountReserve(1); + auto const reserve = env.current()->fees().accountReserve(1, 1); auto const paymentAmount = XRP(1); auto const highFee = reserve + baseFee; BEAST_EXPECT(highFee > reserve); @@ -488,9 +488,9 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const baseReserve = env.current()->fees().accountReserve(0); + auto const baseReserve = env.current()->fees().accountReserve(0, 1); - env.fund(env.current()->fees().accountReserve(1) + baseFee + XRP(1), alice); + env.fund(env.current()->fees().accountReserve(1, 1) + baseFee + XRP(1), alice); env.fund(baseReserve, bob); env.fund(XRP(1000), carol); env.close(); @@ -523,7 +523,7 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const reserve = env.current()->fees().accountReserve(1); + auto const reserve = env.current()->fees().accountReserve(1, 1); // Alice is funded with (reserve + baseFee): after DelegateSet she has // exactly 'reserve', which is insufficient to send XRP(10) while keeping @@ -2612,7 +2612,9 @@ class Delegate_test : public beast::unit_test::Suite {"CredentialDelete", featureCredentials}, {"NFTokenModify", featureDynamicNFT}, {"PermissionedDomainSet", featurePermissionedDomains}, - {"PermissionedDomainDelete", featurePermissionedDomains}}; + {"PermissionedDomainDelete", featurePermissionedDomains}, + {"SponsorshipSet", featureSponsor}, + }; // Can not delegate tx if any required feature disabled. { @@ -2747,7 +2749,7 @@ class Delegate_test : public beast::unit_test::Suite // DO NOT modify expectedDelegableCount unless all scenarios, including // edge cases, have been fully tested and verified. // ==================================================================== - std::size_t const expectedDelegableCount = 56; + std::size_t const expectedDelegableCount = 57; BEAST_EXPECTS( delegableCount == expectedDelegableCount, diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 3496e67b54..c75bdeaf3a 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -51,7 +51,7 @@ namespace xrpl::test { static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } // Helper function that returns true if acct has the lsfDepositAuth flag set. @@ -1025,7 +1025,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite { // not enough reserve Account const john{"john"}; - env.fund(env.current()->fees().accountReserve(0), john); + env.fund(env.current()->fees().accountReserve(0, 1), john); env.close(); auto jv = deposit::authCredentials(john, {{.issuer = issuer, .credType = credType}}); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 4c8c64c5f6..d5b58c5918 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -937,7 +937,7 @@ struct EscrowToken_test : public beast::unit_test::Suite auto const expectedResult = env.current()->rules().enabled(fixCleanup3_2_0) ? Ter(tesSUCCESS) - : Ter(tefEXCEPTION); + : Ter(tefINTERNAL); env(escrow::cancel(alice, alice, seq), Fee(baseFee), expectedResult); env.close(); diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp index 39d722da65..302e55a2cc 100644 --- a/src/test/app/FlowMPT_test.cpp +++ b/src/test/app/FlowMPT_test.cpp @@ -726,7 +726,7 @@ struct FlowMPT_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } // Helper function that returns the Offers on an account. diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 72e39ab890..8d5162394e 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -706,7 +706,7 @@ struct Flow_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } // Helper function that returns the Offers on an account. diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 79fdbb54ed..076e39a42b 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -101,7 +101,8 @@ class Invariants_test : public beast::unit_test::Suite return xrpl::test::jtx::testableAmendments() | fixCleanup3_1_3 | fixCleanup3_2_0; } - /** Run a specific test case to put the ledger into a state that will be + /** + * Run a specific test case to put the ledger into a state that will be * detected by an invariant. Simulates the actions of a transaction that * would violate an invariant. * @@ -337,7 +338,89 @@ class Invariants_test : public beast::unit_test::Suite // check. sleA1->at(sfBalance) = beast::kZero; BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0); - adjustOwnerCount(ac.view(), sleA1, 1, ac.journal); + increaseOwnerCount(ac.view(), sleA1, {}, 1, ac.journal); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoredOwnerCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoringOwnerCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const a1Id = a1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1Id)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoringAccountCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setAccountID(sfSponsor, a2.id()); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + Env{*this, FeatureBitset{featureSponsor}}, + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setAccountID(sfSponsor, a2.id()); ac.view().erase(sleA1); @@ -1771,6 +1854,7 @@ class Invariants_test : public beast::unit_test::Suite "pseudo-account sequence changed" "pseudo-account flags are not set" "pseudo-account has a regular key" + "pseudo-account has a sponsorship field" */ struct Mod { @@ -1798,6 +1882,22 @@ class Invariants_test : public beast::unit_test::Suite .expectedFailure = "pseudo-account has a regular key", .func = [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); }, }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoredOwnerCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoringOwnerCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoringAccountCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsor) = Account("sponsor").id(); }, + }, }); for (auto const& mod : mods) @@ -2658,7 +2758,7 @@ class Invariants_test : public beast::unit_test::Suite } void - testVault() + testVault() // NOLINT(readability-function-size) { using namespace test::jtx; @@ -4358,6 +4458,525 @@ class Invariants_test : public beast::unit_test::Suite return true; }); + // Invalid IOU clawback delta must fail once MPTokensV2 enforces before/after validation. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + env.trust(usd(100), holder); + env(pay(issuer, holder, usd(100))); + env.close(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: trustline clawback balance change is invalid"}}, + [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) { + auto sle = + ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency)); + if (!sle) + return false; + + STAmount balance{Issue{usd.currency, issuer.id()}, 80}; + if (holder.id() > issuer.id()) + balance.negate(); + sle->setFieldAmount(sfBalance, balance); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // Full IOU clawback may delete the trustline; missing after-SLE represents zero balance. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + env.trust(usd(100), holder); + env(pay(issuer, holder, usd(100))); + env.close(); + + doInvariantCheck( + std::move(env), + holder, + other, + {}, + [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) { + auto const sle = + ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency)); + if (!sle) + return false; + + ac.view().erase(sle); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 100}; + }}, + {tesSUCCESS, tesSUCCESS}); + } + + // Pre-MPTokensV2 invalid IOU clawback delta logs but remains non-enforcing. + { + Env env(*this, defaultAmendments() - featureMPTokensV2); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + env.trust(usd(100), holder); + env(pay(issuer, holder, usd(100))); + env.close(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: trustline clawback balance change is invalid"}}, + [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) { + auto sle = + ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency)); + if (!sle) + return false; + + STAmount balance{Issue{usd.currency, issuer.id()}, 80}; + if (holder.id() > issuer.id()) + balance.negate(); + sle->setFieldAmount(sfBalance, balance); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10}; + }}, + {tesSUCCESS, tesSUCCESS}); + } + + // Invalid MPT clawback delta must fail when raw MPToken debit mismatches sfAmount. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + MPTTester const mpt( + {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100}); + auto const id = mpt.issuanceID(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: MPT clawback balance change is invalid"}}, + [id](Account const& holder, Account const&, ApplyContext& ac) { + auto const sleToken = ac.view().peek(keylet::mptoken(id, holder)); + auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id)); + if (!sleToken || !sleIssuance) + return false; + + sleToken->setFieldU64(sfMPTAmount, 80); + sleIssuance->setFieldU64(sfOutstandingAmount, 80); + ac.view().update(sleToken); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfHolder] = holder.id(); + tx[sfAmount] = STAmount{MPTIssue{id}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // A clawback that mutates both IOU and MPT entries must fail under MPTokensV2. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + env.trust(usd(100), holder); + env(pay(issuer, holder, usd(100))); + MPTTester const mpt( + {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100}); + auto const id = mpt.issuanceID(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: trustline and MPToken both changed"}}, + [issuer, usd, id](Account const& holder, Account const&, ApplyContext& ac) { + auto const sleLine = + ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency)); + auto const sleToken = ac.view().peek(keylet::mptoken(id, holder.id())); + auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id)); + if (!sleLine || !sleToken || !sleIssuance) + return false; + + STAmount balance{Issue{usd.currency, issuer.id()}, 90}; + if (holder.id() > issuer.id()) + balance.negate(); + sleLine->setFieldAmount(sfBalance, balance); + sleToken->setFieldU64(sfMPTAmount, 90); + sleIssuance->setFieldU64(sfOutstandingAmount, 90); + ac.view().update(sleLine); + ac.view().update(sleToken); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfHolder] = holder.id(); + tx[sfAmount] = STAmount{MPTIssue{id}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // Clawback that modifies a trustline other than the one implied by the + // tx amount: clawbackTrustLineBalanceInHolderTerms returns nullopt for + // the mismatched line. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + auto const eur = issuer["EUR"]; + env.trust(eur(100), holder); + env(pay(issuer, holder, eur(100))); + env.close(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: trustline clawback changed the wrong line"}}, + [issuer, eur](Account const& holder, Account const&, ApplyContext& ac) { + auto sle = + ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), eur.currency)); + if (!sle) + return false; + STAmount balance{Issue{eur.currency, issuer.id()}, 90}; + if (holder.id() > issuer.id()) + balance.negate(); + sle->setFieldAmount(sfBalance, balance); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // Clawback leaving the holder's balance negative. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + env.trust(usd(100), holder); + env(pay(issuer, holder, usd(100))); + env.close(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: trustline or MPT balance is negative"}}, + [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) { + auto sle = + ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency)); + if (!sle) + return false; + // Make the holder's balance negative from their perspective. + STAmount balance{Issue{usd.currency, issuer.id()}, 80}; + if (holder.id() < issuer.id()) + balance.negate(); + sle->setFieldAmount(sfBalance, balance); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // IOU-amount clawback while only an MPToken changed: no trustline was + // recorded, so iou_.before is empty. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + MPTTester const mpt( + {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100}); + auto const id = mpt.issuanceID(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: trustline clawback changed the wrong line"}}, + [id](Account const& holder, Account const&, ApplyContext& ac) { + auto const sleToken = ac.view().peek(keylet::mptoken(id, holder)); + auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id)); + if (!sleToken || !sleIssuance) + return false; + sleToken->setFieldU64(sfMPTAmount, 90); + sleIssuance->setFieldU64(sfOutstandingAmount, 90); + ac.view().update(sleToken); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // Valid trustline change but a zero clawback amount. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + auto const usd = issuer["USD"]; + env.trust(usd(100), holder); + env(pay(issuer, holder, usd(100))); + env.close(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: trustline clawback amount is invalid"}}, + [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) { + auto sle = + ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency)); + if (!sle) + return false; + STAmount balance{Issue{usd.currency, issuer.id()}, 90}; + if (holder.id() > issuer.id()) + balance.negate(); + sle->setFieldAmount(sfBalance, balance); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 0}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // MPT clawback tx missing the Holder field. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + MPTTester const mpt( + {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100}); + auto const id = mpt.issuanceID(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: MPT clawback missing holder"}}, + [id](Account const& holder, Account const&, ApplyContext& ac) { + auto const sleToken = ac.view().peek(keylet::mptoken(id, holder)); + auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id)); + if (!sleToken || !sleIssuance) + return false; + sleToken->setFieldU64(sfMPTAmount, 90); + sleIssuance->setFieldU64(sfOutstandingAmount, 90); + ac.view().update(sleToken); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfAmount] = STAmount{MPTIssue{id}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // MPT clawback where the holder's MPToken was deleted (after is empty). + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + MPTTester const mpt( + {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100}); + auto const id = mpt.issuanceID(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: MPT clawback token is missing"}}, + [id](Account const& holder, Account const&, ApplyContext& ac) { + auto const sleToken = ac.view().peek(keylet::mptoken(id, holder)); + auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id)); + if (!sleToken || !sleIssuance) + return false; + // Keep the issuance consistent after removing the token. + sleIssuance->setFieldU64(sfOutstandingAmount, 0); + ac.view().update(sleIssuance); + ac.view().erase(sleToken); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfHolder] = holder.id(); + tx[sfAmount] = STAmount{MPTIssue{id}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // MPT clawback that changed a different holder's MPToken. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + MPTTester const mpt( + {.env = env, + .issuer = issuer, + .holders = {holder, other}, + .pay = 100, + .maxAmt = 200}); + auto const id = mpt.issuanceID(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: MPT clawback changed the wrong token"}}, + [id](Account const&, Account const& other, ApplyContext& ac) { + auto const sleToken = ac.view().peek(keylet::mptoken(id, other)); + auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id)); + if (!sleToken || !sleIssuance) + return false; + sleToken->setFieldU64(sfMPTAmount, 90); + sleIssuance->setFieldU64(sfOutstandingAmount, 190); + ac.view().update(sleToken); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfHolder] = holder.id(); + tx[sfAmount] = STAmount{MPTIssue{id}, 10}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + + // Valid MPToken change but a zero MPT clawback amount. + { + Env env(*this, defaultAmendments()); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Account const other{"other"}; + env.fund(XRP(1'000), issuer, holder, other); + MPTTester const mpt( + {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100}); + auto const id = mpt.issuanceID(); + + doInvariantCheck( + std::move(env), + holder, + other, + {{"Invariant failed: MPT clawback amount is invalid"}}, + [id](Account const& holder, Account const&, ApplyContext& ac) { + auto const sleToken = ac.view().peek(keylet::mptoken(id, holder)); + auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id)); + if (!sleToken || !sleIssuance) + return false; + sleToken->setFieldU64(sfMPTAmount, 90); + sleIssuance->setFieldU64(sfOutstandingAmount, 90); + ac.view().update(sleToken); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ + ttCLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = issuer.id(); + tx[sfHolder] = holder.id(); + tx[sfAmount] = STAmount{MPTIssue{id}, 0}; + }}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + } + // More MPTokens created than expected std::array, 4> const tests = { std::make_pair(ttAMM_WITHDRAW, 2), @@ -4948,11 +5567,10 @@ class Invariants_test : public beast::unit_test::Suite std::vector values; }; - for (auto const mantissaScale : { - MantissaRange::MantissaScale::LargeLegacy, - MantissaRange::MantissaScale::Large, - }) + for (auto const mantissaScale : MantissaRange::getAllScales()) { + if (mantissaScale == MantissaRange::MantissaScale::Small) + continue; NumberMantissaScaleGuard const g{mantissaScale}; auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo { @@ -5065,6 +5683,111 @@ class Invariants_test : public beast::unit_test::Suite } } + void + testSponsorship() + { + using namespace test::jtx; + using namespace std::string_literals; + testcase("Sponsorship"); + { + auto const expectMessage = + "SponsoredOwnerCount does not equal SponsoringOwnerCount delta."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoredOwnerCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringOwnerCount, 1); + ac.view().update(sle); + return true; + }); + } + + { + auto const expectMessage = + "OwnerCount must be greater than or equal to SponsoredOwnerCount."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfOwnerCount, 0); + sle->setFieldU32(sfSponsoredOwnerCount, 1); + ac.view().update(sle); + + auto const sle2 = ac.view().peek(keylet::account(a2.id())); + if (!sle2) + return false; + sle2->setFieldU32(sfSponsoringOwnerCount, 1); + ac.view().update(sle2); + return true; + }); + } + + { + auto const expectMessage = + "SponsoredObjectOwnerCount does not equal SponsoredOwnerCount delta."; + uint256 checkID; + + doInvariantCheck( + {{expectMessage}}, + [&](Account const&, Account const& a2, ApplyContext& ac) { + auto const check = ac.view().peek(keylet::check(checkID)); + if (!check) + return false; + check->setAccountID(sfSponsor, a2.id()); + ac.view().update(check); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&checkID](Account const& a1, Account const& a2, Env& env) { + checkID = keylet::check(a1.id(), env.seq(a1)).key; + env(check::create(a1, a2, XRP(1))); + return true; + }); + } + + { + auto const expectMessage = + "Invariant failed: Net delta of SponsoringAccountCount does " + "not match net delta of sfSponsor presence."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringAccountCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setAccountID(sfSponsor, a2.id()); + ac.view().update(sle); + return true; + }); + } + } + void testObjectHasPseudoAccount() { @@ -5467,6 +6190,7 @@ public: testVaultComputeCoarsestScale(); testAMM(); testObjectHasPseudoAccount(); + testSponsorship(); } }; diff --git a/src/test/app/LedgerHistory_test.cpp b/src/test/app/LedgerHistory_test.cpp index 3d0e546678..f8688899e0 100644 --- a/src/test/app/LedgerHistory_test.cpp +++ b/src/test/app/LedgerHistory_test.cpp @@ -29,13 +29,13 @@ namespace xrpl::test { class LedgerHistory_test : public beast::unit_test::Suite { public: - /** Generate a new ledger by hand, applying a specific close time offset - and optionally inserting a transaction. - - If prev is nullptr, then the genesis ledger is made and no offset or - transaction is applied. - - */ + /** + * Generate a new ledger by hand, applying a specific close time offset + * and optionally inserting a transaction. + * + * If prev is nullptr, then the genesis ledger is made and no offset or + * transaction is applied. + */ static std::shared_ptr makeLedger( std::shared_ptr const& prev, diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 2a4de18a9c..f6f85a0cca 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ #include #include +#include #include #include #include @@ -1078,7 +1080,7 @@ class LoanBroker_test : public beast::unit_test::Suite } auto const amt = - env.balance(alice) - env.current()->fees().accountReserve(env.ownerCount(alice)); + env.balance(alice) - accountReserve(*env.current(), alice.id(), env.journal); env(pay(alice, issuer, amt)); // preclaim:: tecINSUFFICIENT_RESERVE @@ -1436,18 +1438,77 @@ class LoanBroker_test : public beast::unit_test::Suite env(tx2, Ter(temINVALID)); } + env.setParseFailureExpected(true); + try { - auto const dm = power(2, 63) - 1; - BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); - tx2[sfDebtMaximum] = dm; + tx2[sfDebtMaximum] = "9223372036854775808"; env(tx2, Ter(temINVALID)); + // should throw in parser + fail(); } - + catch (std::exception const& e) { - auto const dm = power(2, 63) - 3; - BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); - tx2[sfDebtMaximum] = dm; - env(tx2, Ter(tesSUCCESS)); + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.DebtMaximum' has invalid data."); + } + env.setParseFailureExpected(false); + + if (Number::getMantissaScale() >= MantissaRange::MantissaScale::Large330) + { + // For the Large330 scale, 2^63 rounds _down_ to Number::kMaxRep + { + auto const dm = power(2, 63); + BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) + Number{1, -1}; + BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) + 3; + BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(temINVALID)); + } + } + else + { + // For other scales, 2^63 rounds _up_ to Number::kMaxRepUp. Subtracting 1 rounds up + // again. + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(temINVALID)); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } } { diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 3e62af48ff..371fcae54f 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -322,7 +324,8 @@ protected: TenthBips32 const interestRate{}; }; - /** Helper class to compare the expected state of a loan and loan broker + /** + * Helper class to compare the expected state of a loan and loan broker * against the data in the ledger. */ struct VerifyLoanStatus @@ -342,7 +345,8 @@ protected: { } - /** Checks the expected broker state against the ledger + /** + * Checks the expected broker state against the ledger */ void checkBroker( @@ -413,7 +417,9 @@ protected: } } - /** Checks both the loan and broker expect states against the ledger */ + /** + * Checks both the loan and broker expect states against the ledger + */ void operator()( std::uint32_t previousPaymentDate, @@ -473,7 +479,9 @@ protected: } } - /** Checks both the loan and broker expect states against the ledger */ + /** + * Checks both the loan and broker expect states against the ledger + */ void operator()(LoanState const& state) const { @@ -539,7 +547,9 @@ protected: return {asset, keylet, vaultKeylet, params}; } - /// Get the state without checking anything + /** + * Get the state without checking anything + */ LoanState getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet) { @@ -567,8 +577,10 @@ protected: return LoanState{}; } - /// Get the state and check the values against the parameters used in - /// `lifecycle` + /** + * Get the state and check the values against the parameters used in + * `lifecycle` + */ LoanState getCurrentState( jtx::Env const& env, @@ -671,11 +683,11 @@ protected: case AssetType::MPT: { // Enough to cover initial fees if (!env.le(keylet::account(issuer))) - env.fund(env.current()->fees().accountReserve(10) * 10, issuer); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); if (!env.le(keylet::account(lender))) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); if (!env.le(keylet::account(borrower))) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); MPTTester mptt{env, issuer, kMptInitNoFund}; mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); @@ -760,11 +772,11 @@ protected: using namespace jtx; // Enough to cover initial fees - env.fund(env.current()->fees().accountReserve(10) * 10, issuer); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); if (lender != issuer) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); if (borrower != issuer && borrower != lender) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower)); + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower); @@ -841,11 +853,10 @@ protected: // Add extra for transaction fees and reserves, if appropriate, or a // tiny amount for the extra paid in each transaction auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) + - (broker.asset.native() - ? Number( - baseFee * state.paymentRemaining + - env.current()->fees().accountReserve(env.ownerCount(borrower))) - : broker.asset(15).number()); + (broker.asset.native() ? Number( + baseFee * state.paymentRemaining + + accountReserve(*env.current(), borrower.id(), env.journal)) + : broker.asset(15).number()); auto const shortage = totalNeeded - borrowerBalance.number(); @@ -1241,7 +1252,8 @@ protected: PaymentParameters{.showStepBalances = true}); } - /** Runs through the complete lifecycle of a loan + /** + * Runs through the complete lifecycle of a loan * * 1. Create a loan. * 2. Test a bunch of transaction failure conditions. @@ -1559,7 +1571,8 @@ protected: return "Unknown"; } - /** Wrapper to run a series of lifecycle tests for a given asset and loan + /** + * Wrapper to run a series of lifecycle tests for a given asset and loan * amount * * Will be used in the future to vary the loan parameters. For now, it is @@ -3071,7 +3084,7 @@ protected: auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -4419,11 +4432,12 @@ protected: Account const lender{"lender"}; Account const issuer{"issuer"}; Account const borrower{"borrower"}; + Account const sponsor{"sponsor"}; auto const iou = issuer["IOU"]; auto testWrapper = [&](auto&& test) { Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); + env.fund(XRP(1'000), lender, issuer, borrower, sponsor); env(trust(lender, iou(10'000'000))); env(pay(issuer, lender, iou(5'000'000))); BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; @@ -4438,6 +4452,15 @@ protected: BrokerInfo const& brokerInfo, jtx::Fee const& loanSetFee, Number const& debtMaximumRequest) { + for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) + { + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sponsor::As(sponsor, sponsorFlags), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + } + // first temBAD_SIGNER: TODO // invalid grace period { @@ -4528,8 +4551,8 @@ protected: BrokerInfo const& brokerInfo, jtx::Fee const& loanSetFee, Number const& debtMaximumRequest) { - auto const amt = env.balance(borrower) - - env.current()->fees().accountReserve(env.ownerCount(borrower)); + auto const amt = + env.balance(borrower) - accountReserve(*env.current(), borrower.id(), env.journal); env(pay(borrower, issuer, amt)); // tecINSUFFICIENT_RESERVE diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index bbf1c54ab1..befc46e2ae 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -2108,6 +2108,15 @@ class MPToken_test : public beast::unit_test::Suite reward = STAmount{sfSignatureReward, usd(10)}; minAmount = STAmount{sfMinAccountCreateAmount, mpt}; } + // SponsorshipSet + { + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = alice.human(); + jv[sfSponsee.fieldName] = carol.human(); + jv[sfFeeAmount.fieldName] = mpt.getJson(JsonOptions::Values::None); + test(jv, sfFeeAmount.fieldName); + } } BEAST_EXPECT(txWithAmounts.empty()); } @@ -7563,7 +7572,7 @@ class MPToken_test : public beast::unit_test::Suite void testFixDoubleOwnerCount(FeatureBitset all) { - testcase("Fix Double adjustOwnerCount in AMMWithdraw"); + testcase("Fix Double OwnerCount in AMMWithdraw"); using namespace jtx; diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index 19bf58f247..7dd0b14fe5 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -1052,508 +1052,3 @@ BEAST_DEFINE_TESTSUITE_PRIO(NFTokenDir, app, xrpl, 1); // // sp6JS7f14BuwFY8MwFe95Vpi9Znjs // - -// Sets of related accounts. -// -// Identifying the seeds of accounts that generate account IDs with the -// same low 32 bits takes a while. However several sets of accounts with -// that relationship have been located. In case these sets of accounts are -// needed for future testing scenarios they are recorded below. -#if 0 -34 account seeds that produce account IDs with low 32-bits 0x399187e9: - sp6JS7f14BuwFY8Mw5EYu5z86hKDL - sp6JS7f14BuwFY8Mw5PUAMwc5ygd7 - sp6JS7f14BuwFY8Mw5R3xUBcLSeTs - sp6JS7f14BuwFY8Mw5W6oS5sdC3oF - sp6JS7f14BuwFY8Mw5pYc3D9iuLcw - sp6JS7f14BuwFY8Mw5pfGVnhcdp3b - sp6JS7f14BuwFY8Mw6jS6RdEqXqrN - sp6JS7f14BuwFY8Mw6krt6AKbvRXW - sp6JS7f14BuwFY8Mw6mnVBQq7cAN2 - sp6JS7f14BuwFY8Mw8ECJxPjmkufQ - sp6JS7f14BuwFY8Mw8asgzcceGWYm - sp6JS7f14BuwFY8MwF6J3FXnPCgL8 - sp6JS7f14BuwFY8MwFEud2w5czv5q - sp6JS7f14BuwFY8MwFNxKVqJnx8P5 - sp6JS7f14BuwFY8MwFnTCXg3eRidL - sp6JS7f14BuwFY8Mwj47hv1vrDge6 - sp6JS7f14BuwFY8Mwj6TYekeeyukh - sp6JS7f14BuwFY8MwjFjsRDerz7jb - sp6JS7f14BuwFY8Mwjrj9mHTLBrcX - sp6JS7f14BuwFY8MwkKcJi3zMzAea - sp6JS7f14BuwFY8MwkYTDdnYRm9z4 - sp6JS7f14BuwFY8Mwkq8ei4D8uPNd - sp6JS7f14BuwFY8Mwm2pFruxbnJRd - sp6JS7f14BuwFY8MwmJV2ZnAjpC2g - sp6JS7f14BuwFY8MwmTFMPHQHfVYF - sp6JS7f14BuwFY8MwmkG2jXEgqiud - sp6JS7f14BuwFY8Mwms3xEh5tMDTw - sp6JS7f14BuwFY8MwmtipW4D8giZ9 - sp6JS7f14BuwFY8MwoRQBZm4KUUeE - sp6JS7f14BuwFY8MwoVey94QpXcrc - sp6JS7f14BuwFY8MwoZiuUoUTo3VG - sp6JS7f14BuwFY8MwonFFDLT4bHAZ - sp6JS7f14BuwFY8MwooGphD4hefBQ - sp6JS7f14BuwFY8MwoxDp3dmX6q5N - -34 account seeds that produce account IDs with low 32-bits 0x473f2c9a: - sp6JS7f14BuwFY8Mw53ktgqmv5Bmz - sp6JS7f14BuwFY8Mw5KPb2Kz7APFX - sp6JS7f14BuwFY8Mw5Xx4A6HRTPEE - sp6JS7f14BuwFY8Mw5y6qZFNAo358 - sp6JS7f14BuwFY8Mw6kdaBg1QrZfn - sp6JS7f14BuwFY8Mw8QmTfLMAZ5K1 - sp6JS7f14BuwFY8Mw8cbRRVcCEELr - sp6JS7f14BuwFY8Mw8gQvJebmxvDG - sp6JS7f14BuwFY8Mw8qPQurwu3P7Y - sp6JS7f14BuwFY8MwFS4PEVKmuPy5 - sp6JS7f14BuwFY8MwFUQM1rAsQ8tS - sp6JS7f14BuwFY8MwjJBZCkuwsRnM - sp6JS7f14BuwFY8MwjTdS8vZhX5E9 - sp6JS7f14BuwFY8MwjhSmWCbNhd25 - sp6JS7f14BuwFY8MwjwkpqwZsDBw9 - sp6JS7f14BuwFY8MwjyET4p6eqd5J - sp6JS7f14BuwFY8MwkMNAe4JhnG7E - sp6JS7f14BuwFY8MwkRRpnT93UWWS - sp6JS7f14BuwFY8MwkY9CvB22RvUe - sp6JS7f14BuwFY8Mwkhw9VxXqmTr7 - sp6JS7f14BuwFY8MwkmgaTat7eFa7 - sp6JS7f14BuwFY8Mwkq5SxGGv1oLH - sp6JS7f14BuwFY8MwmCBM5p5bTg6y - sp6JS7f14BuwFY8MwmmmXaVah64dB - sp6JS7f14BuwFY8Mwo7R7Cn614v9V - sp6JS7f14BuwFY8MwoCAG1na7GR2M - sp6JS7f14BuwFY8MwoDuPvJS4gG7C - sp6JS7f14BuwFY8MwoMMowSyPQLfy - sp6JS7f14BuwFY8MwoRqDiwTNsTBm - sp6JS7f14BuwFY8MwoWbBWtjpB7pg - sp6JS7f14BuwFY8Mwoi1AEeELGecF - sp6JS7f14BuwFY8MwopGP6Lo5byuj - sp6JS7f14BuwFY8MwoufkXGHp2VW8 - sp6JS7f14BuwFY8MwowGeagFQY32k - -34 account seeds that produce account IDs with low 32-bits 0x4d59f0d1: - sp6JS7f14BuwFY8Mw5CsNgH64zxK7 - sp6JS7f14BuwFY8Mw5Dg4wi2E344h - sp6JS7f14BuwFY8Mw5ErV949Zh2PX - sp6JS7f14BuwFY8Mw5p4nsQvEUE1s - sp6JS7f14BuwFY8Mw8LGnkbaP68Gn - sp6JS7f14BuwFY8Mw8aq6RCBc3iHo - sp6JS7f14BuwFY8Mw8bkWaGoKYT6e - sp6JS7f14BuwFY8Mw8qrCuXnzAXVj - sp6JS7f14BuwFY8MwFDKcPAHPHJTm - sp6JS7f14BuwFY8MwFUXJs4unfgNu - sp6JS7f14BuwFY8MwFj9Yv5LjshD9 - sp6JS7f14BuwFY8Mwj3H73nmq5UaC - sp6JS7f14BuwFY8MwjHSYShis1Yhk - sp6JS7f14BuwFY8MwjpfE1HVo8UP1 - sp6JS7f14BuwFY8Mwk6JE1SXUuiNc - sp6JS7f14BuwFY8MwkASgxEjEnFmU - sp6JS7f14BuwFY8MwkGNY8kg7R6RK - sp6JS7f14BuwFY8MwkHinNZ8SYBQu - sp6JS7f14BuwFY8MwkXLCW1hbhGya - sp6JS7f14BuwFY8MwkZ7mWrYK9YtU - sp6JS7f14BuwFY8MwkdFSqNB5DbKL - sp6JS7f14BuwFY8Mwm3jdBaCAx8H6 - sp6JS7f14BuwFY8Mwm3rk5hEwDRtY - sp6JS7f14BuwFY8Mwm77a2ULuwxu4 - sp6JS7f14BuwFY8MwmJpY7braKLaN - sp6JS7f14BuwFY8MwmKHQjG4XiZ6g - sp6JS7f14BuwFY8Mwmmv8Y3wyUDzs - sp6JS7f14BuwFY8MwmucFe1WgqtwG - sp6JS7f14BuwFY8Mwo1EjdU1bznZR - sp6JS7f14BuwFY8MwoJiqankkU5uR - sp6JS7f14BuwFY8MwoLnvQ6zdqbKw - sp6JS7f14BuwFY8MwoUGeJ319eu48 - sp6JS7f14BuwFY8MwoYf135tQjHP4 - sp6JS7f14BuwFY8MwogeF6M6SAyid - -34 account seeds that produce account IDs with low 32-bits 0xabb11898: - sp6JS7f14BuwFY8Mw5DgiYaNVSb1G - sp6JS7f14BuwFY8Mw5k6e94TMvuox - sp6JS7f14BuwFY8Mw5tTSN7KzYxiT - sp6JS7f14BuwFY8Mw61XV6m33utif - sp6JS7f14BuwFY8Mw87jKfrjiENCb - sp6JS7f14BuwFY8Mw8AFtxxFiRtJG - sp6JS7f14BuwFY8Mw8cosAVExzbeE - sp6JS7f14BuwFY8Mw8fmkQ63zE8WQ - sp6JS7f14BuwFY8Mw8iYSsxNbDN6D - sp6JS7f14BuwFY8Mw8wTZdGRJyyM1 - sp6JS7f14BuwFY8Mw8z7xEh3qBGr7 - sp6JS7f14BuwFY8MwFL5gpKQWZj7g - sp6JS7f14BuwFY8MwFPeZchXQnRZ5 - sp6JS7f14BuwFY8MwFSPxWSJVoU29 - sp6JS7f14BuwFY8MwFYyVkqX8kvRm - sp6JS7f14BuwFY8MwFcbVikUEwJvk - sp6JS7f14BuwFY8MwjF7NcZk1NctK - sp6JS7f14BuwFY8MwjJCwYr9zSfAv - sp6JS7f14BuwFY8MwjYa5yLkgCLuT - sp6JS7f14BuwFY8MwjenxuJ3TH2Bc - sp6JS7f14BuwFY8MwjriN7Ui11NzB - sp6JS7f14BuwFY8Mwk3AuoJNSEo34 - sp6JS7f14BuwFY8MwkT36hnRv8hTo - sp6JS7f14BuwFY8MwkTQixEXfi1Cr - sp6JS7f14BuwFY8MwkYJaZM1yTJBF - sp6JS7f14BuwFY8Mwkc4k1uo85qp2 - sp6JS7f14BuwFY8Mwkf7cFhF1uuxx - sp6JS7f14BuwFY8MwmCK2un99wb4e - sp6JS7f14BuwFY8MwmETztNHYu2Bx - sp6JS7f14BuwFY8MwmJws9UwRASfR - sp6JS7f14BuwFY8MwoH5PQkGK8tEb - sp6JS7f14BuwFY8MwoVXtP2yCzjJV - sp6JS7f14BuwFY8MwobxRXA9vsTeX - sp6JS7f14BuwFY8Mwos3pc5Gb3ihU - -34 account seeds that produce account IDs with low 32-bits 0xce627322: - sp6JS7f14BuwFY8Mw5Ck6i83pGNh3 - sp6JS7f14BuwFY8Mw5FKuwTxjAdH1 - sp6JS7f14BuwFY8Mw5FVKkEn6TkLH - sp6JS7f14BuwFY8Mw5NbQwLwHDd5v - sp6JS7f14BuwFY8Mw5X1dbz3msZaZ - sp6JS7f14BuwFY8Mw6qv6qaXNeP74 - sp6JS7f14BuwFY8Mw81SXagUeutCw - sp6JS7f14BuwFY8Mw84Ph7Qa8kwwk - sp6JS7f14BuwFY8Mw8Hp4gFyU3Qko - sp6JS7f14BuwFY8Mw8Kt8bAKredSx - sp6JS7f14BuwFY8Mw8XHK3VKRQ7v7 - sp6JS7f14BuwFY8Mw8eGyWxZGHY6v - sp6JS7f14BuwFY8Mw8iU5CLyHVcD2 - sp6JS7f14BuwFY8Mw8u3Zr26Ar914 - sp6JS7f14BuwFY8MwF2Kcdxtjzjv8 - sp6JS7f14BuwFY8MwFLmPWb6rbxNg - sp6JS7f14BuwFY8MwFUu8s7UVuxuJ - sp6JS7f14BuwFY8MwFYBaatwHxAJ8 - sp6JS7f14BuwFY8Mwjg6hFkeHwoqG - sp6JS7f14BuwFY8MwjjycJojy2ufk - sp6JS7f14BuwFY8MwkEWoxcSKGPXv - sp6JS7f14BuwFY8MwkMe7wLkEUsQT - sp6JS7f14BuwFY8MwkvyKLaPUc4FS - sp6JS7f14BuwFY8Mwm8doqXPKZmVQ - sp6JS7f14BuwFY8Mwm9r3No8yQ8Tx - sp6JS7f14BuwFY8Mwm9w6dks68W9B - sp6JS7f14BuwFY8MwmMPrv9sCdbpS - sp6JS7f14BuwFY8MwmPAvs3fcQNja - sp6JS7f14BuwFY8MwmS5jasapfcnJ - sp6JS7f14BuwFY8MwmU2L3qJEhnuA - sp6JS7f14BuwFY8MwoAQYmiBnW7fM - sp6JS7f14BuwFY8MwoBkkkXrPmkKF - sp6JS7f14BuwFY8MwonfmxPo6tkvC - sp6JS7f14BuwFY8MwouZFwhiNcYq6 - -34 account seeds that produce account IDs with low 32-bits 0xe29643e8: - sp6JS7f14BuwFY8Mw5EfAavcXAh2k - sp6JS7f14BuwFY8Mw5LhFjLkFSCVF - sp6JS7f14BuwFY8Mw5bRfEv5HgdBh - sp6JS7f14BuwFY8Mw5d6sPcKzypKN - sp6JS7f14BuwFY8Mw5rcqDtk1fACP - sp6JS7f14BuwFY8Mw5xkxRq1Notzv - sp6JS7f14BuwFY8Mw66fbkdw5WYmt - sp6JS7f14BuwFY8Mw6diEG8sZ7Fx7 - sp6JS7f14BuwFY8Mw6v2r1QhG7xc1 - sp6JS7f14BuwFY8Mw6zP6DHCTx2Fd - sp6JS7f14BuwFY8Mw8B3n39JKuFkk - sp6JS7f14BuwFY8Mw8FmBvqYw7uqn - sp6JS7f14BuwFY8Mw8KEaftb1eRwu - sp6JS7f14BuwFY8Mw8WJ1qKkegj9N - sp6JS7f14BuwFY8Mw8r8cAZEkq2BS - sp6JS7f14BuwFY8MwFKPxxwF65gZh - sp6JS7f14BuwFY8MwFKhaF8APcN5H - sp6JS7f14BuwFY8MwFN2buJn4BgYC - sp6JS7f14BuwFY8MwFUTe175MjP3x - sp6JS7f14BuwFY8MwFZhmRDb53NNb - sp6JS7f14BuwFY8MwFa2Azn5nU2WS - sp6JS7f14BuwFY8MwjNNt91hwgkn7 - sp6JS7f14BuwFY8MwjdiYt6ChACe7 - sp6JS7f14BuwFY8Mwk5qFVQ48Mmr9 - sp6JS7f14BuwFY8MwkGvCj7pNf1zG - sp6JS7f14BuwFY8MwkY9UcN2D2Fzs - sp6JS7f14BuwFY8MwkpGvSk9G9RyT - sp6JS7f14BuwFY8MwmGQ7nJf1eEzV - sp6JS7f14BuwFY8MwmQLjGsYdyAmV - sp6JS7f14BuwFY8MwmZ8usztKvikT - sp6JS7f14BuwFY8MwobyMLC2hQdFR - sp6JS7f14BuwFY8MwoiRtwUecZeJ5 - sp6JS7f14BuwFY8MwojHjKsUzj1KJ - sp6JS7f14BuwFY8Mwop29anGAjidU - -33 account seeds that produce account IDs with low 32-bits 0x115d0525: - sp6JS7f14BuwFY8Mw56vZeiBuhePx - sp6JS7f14BuwFY8Mw5BodF9tGuTUe - sp6JS7f14BuwFY8Mw5EnhC1cg84J7 - sp6JS7f14BuwFY8Mw5P913Cunr2BK - sp6JS7f14BuwFY8Mw5Pru7eLo1XzT - sp6JS7f14BuwFY8Mw61SLUC8UX2m8 - sp6JS7f14BuwFY8Mw6AsBF9TpeMpq - sp6JS7f14BuwFY8Mw84XqrBZkU2vE - sp6JS7f14BuwFY8Mw89oSU6dBk3KB - sp6JS7f14BuwFY8Mw89qUKCyDmyzj - sp6JS7f14BuwFY8Mw8GfqQ9VRZ8tm - sp6JS7f14BuwFY8Mw8LtW3VqrqMks - sp6JS7f14BuwFY8Mw8ZrAkJc2sHew - sp6JS7f14BuwFY8Mw8jpkYSNrD3ah - sp6JS7f14BuwFY8MwF2mshd786m3V - sp6JS7f14BuwFY8MwFHfXq9x5NbPY - sp6JS7f14BuwFY8MwFrjWq5LAB8NT - sp6JS7f14BuwFY8Mwj4asgSh6hQZd - sp6JS7f14BuwFY8Mwj7ipFfqBSRrE - sp6JS7f14BuwFY8MwjHqtcvGav8uW - sp6JS7f14BuwFY8MwjLp4sk5fmzki - sp6JS7f14BuwFY8MwjioHuYb3Ytkx - sp6JS7f14BuwFY8MwkRjHPXWi7fGN - sp6JS7f14BuwFY8MwkdVdPV3LjNN1 - sp6JS7f14BuwFY8MwkxUtVY5AXZFk - sp6JS7f14BuwFY8Mwm4jQzdfTbY9F - sp6JS7f14BuwFY8MwmCucYAqNp4iF - sp6JS7f14BuwFY8Mwo2bgdFtxBzpF - sp6JS7f14BuwFY8MwoGwD7v4U6qBh - sp6JS7f14BuwFY8MwoUczqFADMoXi - sp6JS7f14BuwFY8MwoY1xZeGd3gAr - sp6JS7f14BuwFY8MwomVCbfkv4kYZ - sp6JS7f14BuwFY8MwoqbrPSr4z13F - -33 account seeds that produce account IDs with low 32-bits 0x304033aa: - sp6JS7f14BuwFY8Mw5DaUP9agF5e1 - sp6JS7f14BuwFY8Mw5ohbtmPN4yGN - sp6JS7f14BuwFY8Mw5rRsA5fcoTAQ - sp6JS7f14BuwFY8Mw6zpYHMY3m6KT - sp6JS7f14BuwFY8Mw86BzQq4sTnoW - sp6JS7f14BuwFY8Mw8CCpnfvmGdV7 - sp6JS7f14BuwFY8Mw8DRjUDaBcFco - sp6JS7f14BuwFY8Mw8cL7GPo3zZN7 - sp6JS7f14BuwFY8Mw8y6aeYVtH6qt - sp6JS7f14BuwFY8MwFZR3PtVTCdUH - sp6JS7f14BuwFY8MwFcdcdbgz7m3s - sp6JS7f14BuwFY8MwjdnJDiUxEBRR - sp6JS7f14BuwFY8MwjhxWgSntqrFe - sp6JS7f14BuwFY8MwjrSHEhZ8CUM1 - sp6JS7f14BuwFY8MwjzkEeSTc9ZYf - sp6JS7f14BuwFY8MwkBZSk9JhaeCB - sp6JS7f14BuwFY8MwkGfwNY4i2iiU - sp6JS7f14BuwFY8MwknjtZd2oU2Ff - sp6JS7f14BuwFY8Mwkszsqd3ok9NE - sp6JS7f14BuwFY8Mwm58A81MAMvgZ - sp6JS7f14BuwFY8MwmiPTWysuDJCH - sp6JS7f14BuwFY8MwmxhiNeLfD76r - sp6JS7f14BuwFY8Mwo7SPdkwpGrFH - sp6JS7f14BuwFY8MwoANq4F1Sj3qH - sp6JS7f14BuwFY8MwoVjcHufAkd6L - sp6JS7f14BuwFY8MwoVxHBXdaxzhm - sp6JS7f14BuwFY8MwoZ2oTjBNfLpm - sp6JS7f14BuwFY8Mwoc9swzyotFVD - sp6JS7f14BuwFY8MwogMqVRwVEcQ9 - sp6JS7f14BuwFY8MwohMm7WxwnFqH - sp6JS7f14BuwFY8MwopUcpZHuF8BH - sp6JS7f14BuwFY8Mwor6rW6SS7tiB - sp6JS7f14BuwFY8MwoxyaqYz4Ngsb - -33 account seeds that produce account IDs with low 32-bits 0x42d4e09c: - sp6JS7f14BuwFY8Mw58NSZH9EaUxQ - sp6JS7f14BuwFY8Mw5JByk1pgPpL7 - sp6JS7f14BuwFY8Mw5YrJJuXnkHVB - sp6JS7f14BuwFY8Mw5kZe2ZzNSnKR - sp6JS7f14BuwFY8Mw6eXHTsbwi1U7 - sp6JS7f14BuwFY8Mw6gqN7HHDDKSh - sp6JS7f14BuwFY8Mw6zw8L1sSSR53 - sp6JS7f14BuwFY8Mw8E4WqSKKbksy - sp6JS7f14BuwFY8MwF3V9gemqJtND - sp6JS7f14BuwFY8Mwj4j46LHWZuY6 - sp6JS7f14BuwFY8MwjF5i8vh4Ezjy - sp6JS7f14BuwFY8MwjJZpEKgMpUAt - sp6JS7f14BuwFY8MwjWL7LfnzNUuh - sp6JS7f14BuwFY8Mwk7Y1csGuqAhX - sp6JS7f14BuwFY8MwkB1HVH17hN5W - sp6JS7f14BuwFY8MwkBntH7BZZupu - sp6JS7f14BuwFY8MwkEy4rMbNHG9P - sp6JS7f14BuwFY8MwkKz4LYesZeiN - sp6JS7f14BuwFY8MwkUrXyo9gMDPM - sp6JS7f14BuwFY8MwkV2hySsxej1G - sp6JS7f14BuwFY8MwkozhTVN12F9C - sp6JS7f14BuwFY8MwkpkzGB3sFJw5 - sp6JS7f14BuwFY8Mwks3zDZLGrhdn - sp6JS7f14BuwFY8MwktG1KCS7L2wW - sp6JS7f14BuwFY8Mwm1jVFsafwcYx - sp6JS7f14BuwFY8Mwm8hmrU6g5Wd6 - sp6JS7f14BuwFY8MwmFvstfRF7e2f - sp6JS7f14BuwFY8MwmeRohi6m5fs8 - sp6JS7f14BuwFY8MwmmU96RHUaRZL - sp6JS7f14BuwFY8MwoDFzteYqaUh4 - sp6JS7f14BuwFY8MwoPkTf5tDykPF - sp6JS7f14BuwFY8MwoSbMaDtiMoDN - sp6JS7f14BuwFY8MwoVL1vY1CysjR - -33 account seeds that produce account IDs with low 32-bits 0x9a8ebed3: - sp6JS7f14BuwFY8Mw5FnqmbciPvH6 - sp6JS7f14BuwFY8Mw5MBGbyMSsXLp - sp6JS7f14BuwFY8Mw5S4PnDyBdKKm - sp6JS7f14BuwFY8Mw6kcXpM2enE35 - sp6JS7f14BuwFY8Mw6tuuSMMwyJ44 - sp6JS7f14BuwFY8Mw8E8JWLQ1P8pt - sp6JS7f14BuwFY8Mw8WwdgWkCHhEx - sp6JS7f14BuwFY8Mw8XDUYvU6oGhQ - sp6JS7f14BuwFY8Mw8ceVGL4M1zLQ - sp6JS7f14BuwFY8Mw8fdSwLCZWDFd - sp6JS7f14BuwFY8Mw8zuF6Fg65i1E - sp6JS7f14BuwFY8MwF2k7bihVfqes - sp6JS7f14BuwFY8MwF6X24WXGn557 - sp6JS7f14BuwFY8MwFMpn7strjekg - sp6JS7f14BuwFY8MwFSdy9sYVrwJs - sp6JS7f14BuwFY8MwFdMcLy9UkrXn - sp6JS7f14BuwFY8MwFdbwFm1AAboa - sp6JS7f14BuwFY8MwFdr5AhKThVtU - sp6JS7f14BuwFY8MwjFc3Q9YatvAw - sp6JS7f14BuwFY8MwjRXcNs1ozEXn - sp6JS7f14BuwFY8MwkQGUKL7v1FBt - sp6JS7f14BuwFY8Mwkamsoxx1wECt - sp6JS7f14BuwFY8Mwm3hus1dG6U8y - sp6JS7f14BuwFY8Mwm589M8vMRpXF - sp6JS7f14BuwFY8MwmJTRJ4Fqz1A3 - sp6JS7f14BuwFY8MwmRfy8fer4QbL - sp6JS7f14BuwFY8MwmkkFx1HtgWRx - sp6JS7f14BuwFY8MwmwP9JFdKa4PS - sp6JS7f14BuwFY8MwoXWJLB3ciHfo - sp6JS7f14BuwFY8MwoYc1gTtT2mWL - sp6JS7f14BuwFY8MwogXtHH7FNVoo - sp6JS7f14BuwFY8MwoqYoA9P8gf3r - sp6JS7f14BuwFY8MwoujwMJofGnsA - -33 account seeds that produce account IDs with low 32-bits 0xa1dcea4a: - sp6JS7f14BuwFY8Mw5Ccov2N36QTy - sp6JS7f14BuwFY8Mw5CuSemVb5p7w - sp6JS7f14BuwFY8Mw5Ep8wpsTfpSz - sp6JS7f14BuwFY8Mw5WtutJc2H45M - sp6JS7f14BuwFY8Mw6vsDeaSKeUJZ - sp6JS7f14BuwFY8Mw83t5BPWUAzzF - sp6JS7f14BuwFY8Mw8FYGnK35mgkV - sp6JS7f14BuwFY8Mw8huo1x5pfKKJ - sp6JS7f14BuwFY8Mw8mPStxfMDrZa - sp6JS7f14BuwFY8Mw8yC3A7aQJytK - sp6JS7f14BuwFY8MwFCWCDmo9o3t8 - sp6JS7f14BuwFY8MwFjapa4gKxPhR - sp6JS7f14BuwFY8Mwj8CWtG29uw71 - sp6JS7f14BuwFY8MwjHyU5KpEMLVT - sp6JS7f14BuwFY8MwjMZSN7LZuWD8 - sp6JS7f14BuwFY8Mwja2TXJNBhKHU - sp6JS7f14BuwFY8Mwjf3xNTopHKTF - sp6JS7f14BuwFY8Mwjn5RAhedPeuM - sp6JS7f14BuwFY8MwkJdr4d6QoE8K - sp6JS7f14BuwFY8MwkmBryo3SUoLm - sp6JS7f14BuwFY8MwkrPdsc4tR8yw - sp6JS7f14BuwFY8Mwkttjcw2a65Fi - sp6JS7f14BuwFY8Mwm19n3rSaNx5S - sp6JS7f14BuwFY8Mwm3ryr4Xp2aQX - sp6JS7f14BuwFY8MwmBnDmgnJLB6B - sp6JS7f14BuwFY8MwmHgPjzrYjthq - sp6JS7f14BuwFY8MwmeV55DAnWKdd - sp6JS7f14BuwFY8Mwo49hK6BGrauT - sp6JS7f14BuwFY8Mwo56vfKY9aoWu - sp6JS7f14BuwFY8MwoU7tTTXLQTrh - sp6JS7f14BuwFY8MwoXpogSF2KaZB - sp6JS7f14BuwFY8MwoY9JYQAR16pc - sp6JS7f14BuwFY8MwoozLzKNAEXKM - -33 account seeds that produce account IDs with low 32-bits 0xbd2116db: - sp6JS7f14BuwFY8Mw5GrpkmPuA3Bw - sp6JS7f14BuwFY8Mw5r1sLoQJZDc6 - sp6JS7f14BuwFY8Mw68zzRmezLdd6 - sp6JS7f14BuwFY8Mw6jDSyaiF1mRp - sp6JS7f14BuwFY8Mw813wU9u5D6Uh - sp6JS7f14BuwFY8Mw8BBvpf2JFGoJ - sp6JS7f14BuwFY8Mw8F7zXxAiT263 - sp6JS7f14BuwFY8Mw8XG7WuVGHP2N - sp6JS7f14BuwFY8Mw8eyWrcz91cz6 - sp6JS7f14BuwFY8Mw8yNVKFVYyk9u - sp6JS7f14BuwFY8MwF2oA6ePqvZWP - sp6JS7f14BuwFY8MwF9VkcSNh3keq - sp6JS7f14BuwFY8MwFYsMWajgEf2j - sp6JS7f14BuwFY8Mwj3Gu43jYoJ4n - sp6JS7f14BuwFY8MwjJ5iRmYDHrW4 - sp6JS7f14BuwFY8MwjaUSSga93CiM - sp6JS7f14BuwFY8MwjxgLh2FY4Lvt - sp6JS7f14BuwFY8Mwk9hQdNZUgmTB - sp6JS7f14BuwFY8MwkcMXqtFp1sMx - sp6JS7f14BuwFY8MwkzZCDc56jsUB - sp6JS7f14BuwFY8Mwm5Zz7fP24Qym - sp6JS7f14BuwFY8MwmDWqizXSoJRG - sp6JS7f14BuwFY8MwmKHmkNYdMqqi - sp6JS7f14BuwFY8MwmRfAWHxWpGNK - sp6JS7f14BuwFY8MwmjCdXwyhphZ1 - sp6JS7f14BuwFY8MwmmukDAm1w6FL - sp6JS7f14BuwFY8Mwmmz2SzaR9TRH - sp6JS7f14BuwFY8Mwmz2z5mKHXzfn - sp6JS7f14BuwFY8Mwo2xNe5629r5k - sp6JS7f14BuwFY8MwoKy8tZxZrfJw - sp6JS7f14BuwFY8MwoLyQ9aMsq8Dm - sp6JS7f14BuwFY8MwoqqYkewuyZck - sp6JS7f14BuwFY8MwouvvhREVp6Pp - -33 account seeds that produce account IDs with low 32-bits 0xd80df065: - sp6JS7f14BuwFY8Mw5B7ERyhAfgHA - sp6JS7f14BuwFY8Mw5VuW3cF7bm2v - sp6JS7f14BuwFY8Mw5py3t1j7YbFT - sp6JS7f14BuwFY8Mw5qc84SzB6RHr - sp6JS7f14BuwFY8Mw5vGHW1G1hAy8 - sp6JS7f14BuwFY8Mw6gVa8TYukws6 - sp6JS7f14BuwFY8Mw8K9w1RoUAv1w - sp6JS7f14BuwFY8Mw8KvKtB7787CA - sp6JS7f14BuwFY8Mw8Y7WhRbuFzRq - sp6JS7f14BuwFY8Mw8cipw7inRmMn - sp6JS7f14BuwFY8MwFM5fAUNLNB13 - sp6JS7f14BuwFY8MwFSe1zAsht3X3 - sp6JS7f14BuwFY8MwFYNdigqQuHZM - sp6JS7f14BuwFY8MwjWkejj7V4V5Q - sp6JS7f14BuwFY8Mwjd2JGpsjvynq - sp6JS7f14BuwFY8Mwjg1xkducn751 - sp6JS7f14BuwFY8Mwjsp6LnaJvL1W - sp6JS7f14BuwFY8MwjvSbLc9593yH - sp6JS7f14BuwFY8Mwjw2h5wx7U6vZ - sp6JS7f14BuwFY8MwjxKUjtRsmPLH - sp6JS7f14BuwFY8Mwk1Yy8ginDfqv - sp6JS7f14BuwFY8Mwk2HrWhWwZP12 - sp6JS7f14BuwFY8Mwk4SsqiexvpWs - sp6JS7f14BuwFY8Mwk66zCs5ACpE6 - sp6JS7f14BuwFY8MwkCwx6vY97Nwh - sp6JS7f14BuwFY8MwknrbjnhTTWU8 - sp6JS7f14BuwFY8MwkokDy2ShRzQx - sp6JS7f14BuwFY8Mwm3BxnRPNxsuu - sp6JS7f14BuwFY8MwmY9EWdQQsFVr - sp6JS7f14BuwFY8MwmYTWjrDhmk8S - sp6JS7f14BuwFY8Mwo9skXt9Y5BVS - sp6JS7f14BuwFY8MwoZYKZybJ1Crp - sp6JS7f14BuwFY8MwoyXqkhySfSmF - -33 account seeds that produce account IDs with low 32-bits 0xe2e44294: - sp6JS7f14BuwFY8Mw53dmvTgNtBwi - sp6JS7f14BuwFY8Mw5Wrxsqn6WrXW - sp6JS7f14BuwFY8Mw5fGDT31RCXgC - sp6JS7f14BuwFY8Mw5nKRkubwrLWM - sp6JS7f14BuwFY8Mw5nXMajwKjriB - sp6JS7f14BuwFY8Mw5xZybggrC9NG - sp6JS7f14BuwFY8Mw5xea8f6dBMV5 - sp6JS7f14BuwFY8Mw5zDGofAHy5Lb - sp6JS7f14BuwFY8Mw6eado41rQNVG - sp6JS7f14BuwFY8Mw6yqKXQsQJPuU - sp6JS7f14BuwFY8Mw83MSN4FDzSGH - sp6JS7f14BuwFY8Mw8B3pUbzQqHe2 - sp6JS7f14BuwFY8Mw8WwRLnhBRvfk - sp6JS7f14BuwFY8Mw8hDBpKbpJwJX - sp6JS7f14BuwFY8Mw8jggRSZACe7M - sp6JS7f14BuwFY8Mw8mJRpU3qWbwC - sp6JS7f14BuwFY8MwFDnVozykN21u - sp6JS7f14BuwFY8MwFGGRGY9fctgv - sp6JS7f14BuwFY8MwjKznfChH9DQb - sp6JS7f14BuwFY8MwjbC5GvngRCk6 - sp6JS7f14BuwFY8Mwk3Lb7FPe1629 - sp6JS7f14BuwFY8MwkCeS41BwVrBD - sp6JS7f14BuwFY8MwkDnnvRyuWJ7d - sp6JS7f14BuwFY8MwkbkRNnzDEFpf - sp6JS7f14BuwFY8MwkiNhaVhGNk6v - sp6JS7f14BuwFY8Mwm1X4UJXRZx3p - sp6JS7f14BuwFY8Mwm7da9q5vfq7J - sp6JS7f14BuwFY8MwmPLqfBPrHw5H - sp6JS7f14BuwFY8MwmbJpxvVjEwm2 - sp6JS7f14BuwFY8MwoAVeA7ka37cD - sp6JS7f14BuwFY8MwoTFFTAwFKmVM - sp6JS7f14BuwFY8MwoYsne51VpDE3 - sp6JS7f14BuwFY8MwohLVnU1VTk5h - -#endif // 0 diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 844bb4ee87..acd54ae26a 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -452,7 +452,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Just for sanity's sake we'll check that the current value // of sfMintedNFTokens matches what we expect. - auto replacement = std::make_shared(*sle, sle->key()); + auto replacement = std::make_shared(*sle); if (replacement->getFieldU32(sfMintedNFTokens) != 1) return false; // Unexpected test conditions. diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index 800b14877a..d03b1b8e93 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -59,7 +59,7 @@ class OfferMPT_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } static std::uint32_t @@ -1794,7 +1794,8 @@ public: // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const base = env.current()->fees().base; - auto const startingXrp = XRP(100) + env.current()->fees().accountReserve(3) + base * 4; + auto const startingXrp = + XRP(100) + env.current()->fees().accountReserve(3, 1) + base * 4; env.fund(startingXrp, gw1, gw2, gw3, alice, bob); env.close(); @@ -1814,7 +1815,8 @@ public: env(offer(alice, usD1(200), XRP(200))); BEAST_EXPECT(env.balance(alice, usD1) == usD1(100)); - BEAST_EXPECT(env.balance(alice) == STAmount(env.current()->fees().accountReserve(3))); + BEAST_EXPECT( + env.balance(alice) == STAmount(env.current()->fees().accountReserve(3, 1))); BEAST_EXPECT(env.balance(bob, usD1) == usD1(400)); }; @@ -1865,7 +1867,7 @@ public: auto const bob = Account{"bob"}; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); @@ -1884,7 +1886,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryMPT(env, bob, usd); BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "400"); @@ -1904,7 +1906,7 @@ public: auto const bob = Account{"bob"}; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); @@ -1925,7 +1927,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryMPT(env, bob, usd); BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "300"); @@ -1946,7 +1948,7 @@ public: auto const base = env.current()->fees().base; auto const startingXrp = - XRP(100.1) + env.current()->fees().accountReserve(1) + base * 2; + XRP(100.1) + env.current()->fees().accountReserve(1, 1) + base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 2724a6474b..7fc7161e36 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -60,7 +60,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } static std::uint32_t @@ -1962,7 +1962,7 @@ public: // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(3) + env.current()->fees().base * 4; + XRP(100) + env.current()->fees().accountReserve(3, 1) + env.current()->fees().base * 4; env.fund(startingXrp, gw1, gw2, gw3, alice, bob); env.close(); @@ -1985,7 +1985,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(3)).getText()); + STAmount(env.current()->fees().accountReserve(3, 1)).getText()); jrr = ledgerEntryState(env, bob, gw1, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); @@ -2045,7 +2045,7 @@ public: auto const usd = gw["USD"]; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); @@ -2066,7 +2066,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryState(env, bob, gw, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); @@ -2087,7 +2087,7 @@ public: auto const usd = gw["USD"]; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); @@ -2110,7 +2110,7 @@ public: jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(env.current()->fees().accountReserve(1, 1)).getText()); jrr = ledgerEntryState(env, bob, gw, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-300"); @@ -2131,8 +2131,8 @@ public: auto const xts = gw["XTS"]; auto const xxx = gw["XXX"]; - auto const startingXrp = - XRP(100.1) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + auto const startingXrp = XRP(100.1) + env.current()->fees().accountReserve(1, 1) + + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index 7e7f5e9bd0..b8d9fb32d1 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -60,7 +60,7 @@ private: // Insufficient reserve { Env env(*this); - env.fund(env.current()->fees().accountReserve(0), owner); + env.fund(env.current()->fees().accountReserve(0, 1), owner); Oracle const oracle( env, {.owner = owner, @@ -71,7 +71,7 @@ private: { Env env(*this); env.fund( - env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2, owner); Oracle oracle( env, {.owner = owner, .fee = static_cast(env.current()->fees().base.drops())}); BEAST_EXPECT(oracle.exists()); @@ -640,7 +640,7 @@ private: Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund( - env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); + env.current()->fees().accountReserve(1, 1) + env.current()->fees().base * 2, owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); oracle.set(UpdateArg{.series = {{"XRP", "USD", 742, 2}}, .fee = baseFee}); } diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index ddbcfe8b70..2a4b884668 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -37,10 +37,10 @@ #include #include +#include #include #include #include -#include #include #include #include @@ -127,12 +127,7 @@ class ElementComboIter [[nodiscard]] bool hasAny(std::initializer_list sb) const { - for (auto const s : sb) - { - if (has(s)) - return true; - } - return false; + return std::ranges::any_of(sb, [this](auto const s) { return has(s); }); } [[nodiscard]] size_t @@ -309,31 +304,26 @@ struct ExistingElementPool currencyNames.clear(); currencyNames.reserve(numCur); - static constexpr size_t kBufSize = 32; - char buf[kBufSize]; - for (size_t id = 0; id < numAct; ++id) - { - snprintf(buf, kBufSize, "A%zu", id); - accounts.emplace_back(buf); - } + accounts.emplace_back("A" + std::to_string(id)); for (size_t id = 0; id < numCur; ++id) { + std::string name; if (id < 10) { - snprintf(buf, kBufSize, "CC%zu", id); + name = "CC" + std::to_string(id); } else if (id < 100) { - snprintf(buf, kBufSize, "C%zu", id); + name = "C" + std::to_string(id); } else { - snprintf(buf, kBufSize, "%zu", id); + name = std::to_string(id); } - currencies.emplace_back(toCurrency(buf)); - currencyNames.emplace_back(buf); + currencies.emplace_back(toCurrency(name)); + currencyNames.emplace_back(name); } for (auto const& a : accounts) diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index 8578b8a9ba..998b7b1c7f 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -82,13 +83,9 @@ class PermissionedDEX_test : public beast::unit_test::Suite return false; auto const& indexes = page->getFieldV256(sfIndexes); - for (auto const& index : indexes) - { - if (index == keylet::offer(account, offerSeq).key) - return true; - } - - return false; + return std::ranges::any_of(indexes, [&](auto const& index) { + return index == keylet::offer(account, offerSeq).key; + }); }; auto const sle = env.le(keylet::offer(account.id(), offerSeq)); diff --git a/src/test/app/ReducedOffer_test.cpp b/src/test/app/ReducedOffer_test.cpp index 47a2c1294c..dea197590c 100644 --- a/src/test/app/ReducedOffer_test.cpp +++ b/src/test/app/ReducedOffer_test.cpp @@ -144,20 +144,6 @@ public: Quality(Amounts{tweakedTakerPays, reducedTakerGets}).rate(); BEAST_EXPECT(tweakedRate > initialRate); } -#if 0 - std::cout << "Placed rate: " << initialRate - << "; in-ledger rate: " << inLedgerRate - << "; TakerPays: " << reducedTakerPays - << "; TakerGets: " << reducedTakerGets - << "; bob already got: " << bobGot << std::endl; -// #else - std::string_view filler = - inLedgerRate > initialRate ? "**" : " "; - std::cout << "| `" << reducedTakerGets << "` | `" - << reducedTakerPays << "` | `" << initialRate - << "` | " << filler << "`" << inLedgerRate << "`" - << filler << " |`" << std::endl; -#endif } // In preparation for the next iteration make sure the two @@ -275,21 +261,6 @@ public: Quality(Amounts{tweakedTakerPays, reducedTakerGets}).rate(); BEAST_EXPECT(tweakedRate > initialRate); } -#if 0 - std::cout << "Placed rate: " << initialRate - << "; in-ledger rate: " << inLedgerRate - << "; TakerPays: " << reducedTakerPays - << "; TakerGets: " << reducedTakerGets - << "; alice already got: " << aliceGot - << std::endl; -// #else - std::string_view filler = badRate ? "**" : " "; - std::cout << "| `" << reducedTakerGets << "` | `" - << reducedTakerPays << "` | `" << initialRate - << "` | " << filler << "`" << inLedgerRate << "`" - << filler << " | `" << aliceGot << "` |" - << std::endl; -#endif } // In preparation for the next iteration make sure the two @@ -463,13 +434,6 @@ public: { bool const bobOfferGone = !offerInLedger(env, bob, bobOfferSeq); STAmount const aliceBalanceUSD = env.balance(alice, usd); -#if 0 - std::cout - << "bob initial: " << initialBobUSD - << "; alice final: " << aliceBalanceUSD - << "; bob offer: " << bobOfferJson.toStyledString() - << std::endl; -#endif // Sanity check the ledger if alice got USD. if (aliceBalanceUSD.signum() > 0) { @@ -619,19 +583,6 @@ public: Quality(Amounts{aliceReducedOffer.in, tweakedTakerGets}).rate(); BEAST_EXPECT(tweakedRate > initialRate); } -#if 0 - std::cout << "Placed rate: " << initialRate - << "; in-ledger rate: " << inLedgerRate - << "; TakerPays: " << aliceReducedOffer.in - << "; TakerGets: " << aliceReducedOffer.out - << std::endl; -// #else - std::string_view filler = badRate ? "**" : " "; - std::cout << "| " << aliceReducedOffer.in << "` | `" - << aliceReducedOffer.out << "` | `" << initialRate - << "` | " << filler << "`" << inLedgerRate << "`" - << filler << std::endl; -#endif } // In preparation for the next iteration make sure all three diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp new file mode 100644 index 0000000000..f20aac68f9 --- /dev/null +++ b/src/test/app/Sponsor_test.cpp @@ -0,0 +1,5511 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +static STAmount +accountReserve(jtx::Env& env, std::uint32_t count = 1) +{ + return env.current()->fees().reserve * count; +} + +static STAmount +reserve(jtx::Env& env, std::uint32_t count) +{ + return env.current()->fees().accountReserve(count, 1); +} + +static void +adjustAccountXRPBalance(jtx::Env& env, jtx::Account const& account, STAmount const& balanceTo) +{ + using namespace test::jtx; + XRPL_ASSERT(isXRP(balanceTo), "adjustAccountXRPBalance: balanceTo must be XRP"); + auto const currentBalance = env.balance(account); + if (currentBalance == balanceTo) + return; + + auto const baseFee = env.current()->fees().base; + if (currentBalance > balanceTo) + { + env(pay(account, env.master, currentBalance - balanceTo), + Fee(XRP(1)), + sponsor::As(env.master, spfSponsorFee), + Sig(sfSponsorSignature, env.master)); + } + else + { + env(pay(env.master, account, balanceTo - currentBalance), Fee(baseFee)); + } + + env.close(); +} + +class Sponsor_test : public beast::unit_test::Suite +{ +public: + void + testDisabled() + { + testcase("Disabled"); + using namespace test::jtx; + Env env{*this, testableAmendments() - featureSponsor}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + + // check Sponsor fields + auto const jt = noop(alice); + auto jt1 = jt; + jt1[sfSponsor.jsonName] = sponsor.human(); + env(jt1, Ter(temDISABLED)); + env(jt, Sig(sfSponsorSignature, sponsor), Ter(temDISABLED)); + + auto jt2 = jt; + jt2[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve; + env(jt2, Ter(temDISABLED)); + + // check Sponsor transactions + env(sponsor::transfer(alice, 0), Ter(temDISABLED)); + env(sponsor::set(sponsor, 0), Ter(temDISABLED)); + } + + void + testInvalidSponsorshipSet() + { + testcase("Invalid SponsorshipSet"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const noFunded("noFunded"); + Account const gw("gw"); + + auto const usd = gw["usd"]; + env.fund(XRP(10000), alice, sponsor, gw); + env.close(); + + // + // preflight + // + + // Invalid flags + { + env(sponsor::set(sponsor, ~tfSponsorshipSetMask - tfInnerBatchTxn), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForReserve | + tfSponsorshipClearRequireSignForReserve), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + for (auto flag : + {tfSponsorshipSetRequireSignForFee, + tfSponsorshipClearRequireSignForFee, + tfSponsorshipSetRequireSignForReserve, + tfSponsorshipClearRequireSignForReserve}) + { + env(sponsor::set(sponsor, tfDeleteObject | flag), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + } + } + + // invalid SponsorAccount / Sponsee + // Account = Sponsor + env(sponsor::set(alice, tfDeleteObject), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + // Account = Sponsee + env(sponsor::set(alice, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(temMALFORMED)); + // Both Sponsor and Sponsee are specified + env(sponsor::set(alice, 0), + sponsor::CounterpartySponsor(sponsor), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + + // Invalid feeAmount + for (auto const& amt : {XRP(-1), usd(1)}) + { + env(sponsor::set_fee(sponsor, 0, amt), sponsor::SponseeAcc(alice), Ter(temBAD_AMOUNT)); + } + // Invalid MaxFee + for (auto const& amt : {XRP(-1), usd(1)}) + { + env(sponsor::set_fee(sponsor, 0, XRP(1), amt), + sponsor::SponseeAcc(alice), + Ter(temBAD_AMOUNT)); + } + + // Invalid Delete operation + env(sponsor::set_reserve(sponsor, tfDeleteObject, 1), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + env(sponsor::set_fee(sponsor, tfDeleteObject, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + env(sponsor::set_max_fee(sponsor, tfDeleteObject, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + + // Invalid SponsorAccount with non-Delete operation + env(sponsor::set_reserve(sponsor, 0, 100), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + env(sponsor::set_fee(sponsor, 0, XRP(1), XRP(1)), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + + // + // preclaim + // + + // Invalid Sponsee + env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST)); + env.close(); + + // Invalid Sponsor + env(sponsor::set(sponsor, tfDeleteObject), + sponsor::CounterpartySponsor(noFunded), + Ter(tecNO_DST)); + env.close(); + + // Invalid Delete operation (sponsorship not found) + env(sponsor::set(sponsor, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(tecNO_ENTRY)); + env.close(); + + // insufficient balance to sponsor Fee + adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); + env(sponsor::set_fee(sponsor, 0, XRP(4)), sponsor::SponseeAcc(alice), Ter(tecUNFUNDED)); + env.close(); + + // insufficient reserve to create sponsorship + adjustAccountXRPBalance(env, sponsor, XRP(100) + XRP(1) + reserve(env, 1) - drops(1)); + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + + // FeeAmount + Fee > Balance + // Balance = 1000XRP, FeeAmount = 1001XRP + adjustAccountXRPBalance(env, sponsor, XRP(1000)); + env(sponsor::set_fee(sponsor, 0, XRP(1001)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + // Balance = 1000XRP, FeeAmount = 999XRP, Fee=2XRP + adjustAccountXRPBalance(env, sponsor, XRP(1000)); + env(sponsor::set_fee(sponsor, 0, XRP(999)), + sponsor::SponseeAcc(alice), + Fee(XRP(2)), + Ter(tecUNFUNDED)); + env.close(); + + // create sponsor to use above tests + // need feeAmount(1000) + Fee(1) + reserve(~250) = ~1251 + adjustAccountXRPBalance(env, sponsor, XRP(1000) + XRP(1) + reserve(env, 1)); + env(sponsor::set(sponsor, 0, 100, XRP(1000)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + // delta-based balance check + // After create: sponsor balance ~ 0, feeAmount = XRP(1000) + + // Decreasing feeAmount should succeed (refund, negative delta) + adjustAccountXRPBalance(env, sponsor, XRP(500)); + env(sponsor::set_fee(sponsor, 0, XRP(800)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + // balance was 500, delta = 800-1000 = -200 (refund), balance = 500+200-1 = 699 + + // Increasing feeAmount within delta budget should succeed + adjustAccountXRPBalance(env, sponsor, XRP(500)); + env(sponsor::set_fee(sponsor, 0, XRP(850)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + // balance was 500, delta = 850-800 = 50, balance = 500-50-1 = 449 + + // Increasing feeAmount where delta exceeds balance should fail + adjustAccountXRPBalance(env, sponsor, XRP(310)); + env(sponsor::set_fee(sponsor, 0, XRP(1200)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + + // Increasing feeAmount to reach insufficient reserve + auto const currentFeeAmount = env.le(keylet::sponsorship(sponsor.id(), alice.id())) + ->getFieldAmount(sfFeeAmount) + .xrp(); + adjustAccountXRPBalance(env, sponsor, XRP(310)); + env(sponsor::set_fee(sponsor, 0, currentFeeAmount + XRP(309)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + } + + void + testPseudoAccountSponsorship() + { + testcase("Pseudo account sponsorship"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sp("sponsor"); + + Asset const asset = gw["IOU"].asset(); + + env.fund(XRP(1000000), alice, bob, gw, sp); + env.close(); + + // Create a vault to get a pseudo account + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + auto const vaultSle = env.le(keylet); + BEAST_EXPECT(vaultSle); + Account const pseudoAcc("vault", vaultSle->getAccountID(sfAccount)); + env.memoize(pseudoAcc); + + // Sponsee is a pseudo account -> tecNO_PERMISSION + env(sponsor::set(sp, 0, 100, XRP(100)), + sponsor::SponseeAcc(pseudoAcc), + Ter(tecNO_PERMISSION)); + env.close(); + + // Sponsor is a pseudo account -> tecNO_PERMISSION + // (submitted by bob with counterpartySponsor pointing to pseudo account) + env(sponsor::set(bob, tfDeleteObject), + sponsor::CounterpartySponsor(pseudoAcc), + Ter(tecNO_PERMISSION)); + env.close(); + } + + void + testSingleSigning() + { + testcase("Single signing"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const invalid("invalid"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // Signature doesn't exist + auto tx = noop(alice); + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = strHex(sponsor.pk().slice()); + + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // Invalid signature + tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // Signer account doesn't exist + env(noop(alice), + Fee(XRP(1)), + sponsor::As(invalid, spfSponsorReserve), + Sig(sfSponsorSignature, invalid), + Ter(terNO_ACCOUNT)); + + // Success + env(noop(alice), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + } + + void + testMultiSigning() + { + testcase("Multi signing"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const signer1("signer1"); + Account const signer2("signer2"); + + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(signers(sponsor, 1, {{signer1, 1}, {signer2, 1}})); + env.close(); + + // Invalid signature + auto tx = noop(alice); + auto& signers1 = tx[sfSponsorSignature.jsonName][sfSigners.jsonName][0U][sfSigner.jsonName]; + signers1[sfAccount.jsonName] = signer1.human(); + signers1[sfSigningPubKey.jsonName] = strHex(signer1.pk().slice()); + signers1[sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // bob is not a multi-signing account. + env(noop(alice), + Fee(XRP(1)), + sponsor::As(bob, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1}), + Ter(tefNOT_MULTI_SIGNING)); + + env(noop(alice), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1}), + Ter(tesSUCCESS)); + env.close(); + + env(signers(sponsor, 2, {{signer1, 1}, {signer2, 1}})); + env.close(); + + // test calculateBaseFee for multisigned sponsor + auto const baseFee = env.current()->fees().base; + env(noop(alice), + Fee(baseFee + 2 * baseFee - 1), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1, signer2}), + Ter(telINSUF_FEE_P)); + + env(noop(alice), + Fee(baseFee + 2 * baseFee), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1, signer2}), + Ter(tesSUCCESS)); + } + + void + testInvalidSponsorField() + { + testcase("Invalid Sponsor Field"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const noFunded("noFunded"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // Invalid Sponsor Account (Account = Sponsor.Account) + env(noop(alice), sponsor::As(alice, spfSponsorFee), Ter(temMALFORMED)); + + // Invalid Sponsor Account + // (SponsorSignature is specified but Sponsor.Account is not specified) + env(noop(alice), Sig(sfSponsorSignature, sponsor), Ter(temMALFORMED)); + + // Invalid Sponsor Account (Sponsor.Account doesn't exist) + env(noop(alice), sponsor::As(noFunded, spfSponsorReserve), Ter(terNO_ACCOUNT)); + env(noop(alice), + sponsor::As(noFunded, spfSponsorReserve), + Sig(sfSponsorSignature, noFunded), + Ter(terNO_ACCOUNT)); + + // Invalid Flags + env(noop(alice), + sponsor::As(sponsor, (spfSponsorFee | spfSponsorReserve) + 1), + Ter(temINVALID_FLAG)); + + // SponsorFlags=0 with valid sponsor (no sponsorship purpose) + env(noop(alice), sponsor::As(sponsor, 0), Ter(temINVALID_FLAG)); + + // no SponsorFlag with valid sponsor + auto tx = noop(alice); + tx[sfSponsor.jsonName] = sponsor.human(); + env(tx, Ter(temINVALID_FLAG)); + + // Invalid Flags without sponsor + tx = noop(alice); + tx[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve; + env(tx, Ter(temINVALID_FLAG)); + } + + void + testSimpleSponsorshipSet() + { + testcase("Simple SponsorshipSet"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + { + // create sponsorship + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve, + 100, + XRP(100), + XRP(1)), + Fee(XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForFee)); + BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForReserve)); + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(1)); + + // update sponsorship (decrement) + env(sponsor::set(sponsor, 0, 50, XRP(50), XRP(0.5)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 50); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(50)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(0.5)); + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(2)); + + // update sponsorship (increment) + env(sponsor::set(sponsor, 0, 200, XRP(200), XRP(2)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 200); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(200)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(2)); + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(3)); + + // delete from sponsor + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - XRP(4)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve, + 100, + XRP(100), + XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // delete from sponsee + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + // Cannot create sponsorship with no fee or reserve budget. MaxFee + // and flags do not make a sponsorship object useful by themselves. + env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + env(sponsor::set_max_fee(sponsor, 0, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + // update sponsorship with non-zero value + env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), + sponsor::SponseeAcc(alice), + Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + + // update sponsorship flags + auto testFlagUpdate = [&](auto setFlag, auto clearFlag, auto ledgerFlag) { + env(sponsor::set(sponsor, setFlag), sponsor::SponseeAcc(alice), Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFlag(ledgerFlag)); + + env(sponsor::set(sponsor, clearFlag), sponsor::SponseeAcc(alice), Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFlag(ledgerFlag)); + }; + + testFlagUpdate( + tfSponsorshipSetRequireSignForFee, + tfSponsorshipClearRequireSignForFee, + lsfSponsorshipRequireSignForFee); + testFlagUpdate( + tfSponsorshipSetRequireSignForReserve, + tfSponsorshipClearRequireSignForReserve, + lsfSponsorshipRequireSignForReserve); + + // Cannot update sponsorship so both fee and reserve budgets are absent. + env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecNO_PERMISSION)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + } + + { + // Removing one budget field while the other remains keeps the + // Sponsorship valid. Starting state (from above): + // RemainingOwnerCount = 100, FeeAmount = XRP(100). + + // Remove only FeeAmount (set to 0); RemainingOwnerCount remains. + env(sponsor::set_fee(sponsor, 0, XRP(0)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + + // Re-add FeeAmount, then remove only RemainingOwnerCount; + // FeeAmount remains. + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 0), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + } + + { + // Update Sponsorship (FeeAmount) + // set empty FeeAmount + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + // add FeeAmount + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // Update Sponsorship (ReserveCount) + // set empty ReserveCount + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // add ReserveCount + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // delete Sponsorship (only with FeeAmount) + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // delete Sponsorship (only with ReserveCount) + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + } + + void + testPreFundAndCosign() + { + testcase("PreFund and Cosign"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const sponsor("sponsor"); + + { + // Both pre-funded and co-signed; the pre-funded value is used. + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + auto const checkSeq = env.seq(alice); + env(check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); + + env(check::cancel(alice, keylet::check(alice, checkSeq).key), Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not restored + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); + } + + { + // if pre-funded value is not enough, error + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, charlie, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // Fee insufficient + env(check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(11)), + Ter(terINSUF_FEE_B)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 0), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + // reserve insufficient + env(check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(1)), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + } + + void + testSponsoredFreeTierReserve() + { + testcase("Sponsored Free-Tier Reserve"); + using namespace test::jtx; + Account const alice("alice"); + Account const issuer("issuer"); + Account const sponsor("sponsor"); + + // Trust lines and MPTokens normally skip the reserve check when the + // holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the + // tx is sponsored, that shortcut must not apply — the sponsor must + // still cover the reserve. + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, issuer); + // Sponsor is funded just below the reserve required to cover a single + // sponsored item. + env.fund(reserve(env, 1) - drops(1), sponsor); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + MPTTester mptt(env, issuer, {.fund = false}); + mptt.create(); + + // Free-tier trust line cosigned by an undercapitalized sponsor must + // fail — the holder's free-first-two-items shortcut does not let the + // sponsor skip the reserve check. + env(trust(alice, issuer["USD"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + // Free-tier MPTokenAuthorize must also fail for the same reason. + env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + void + testTransferSponsor() + { + testcase("Transfer Sponsor"); + using namespace test::jtx; + + // Verify preflight checks + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // Test invalid flags for SponsorshipTransfer + { + // Invalid flag is provided + env(sponsor::transfer( + alice, + (tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd) + 1), + Ter(temINVALID_FLAG)); + + // No SponsorshipTransfer flag is provided + env(sponsor::transfer(alice, 0), Ter(temINVALID_FLAG)); + + // Only one of the three valid flags can be set. + // Setting more than one flag is invalid + for (auto flag : { + tfSponsorshipCreate | tfSponsorshipReassign, + tfSponsorshipCreate | tfSponsorshipEnd, + tfSponsorshipReassign | tfSponsorshipEnd, + tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd, + }) + env(sponsor::transfer(alice, flag), Ter(temINVALID_FLAG)); + } + + // Malformed tests for tfSponsorshipCreate + { + // No sponsor field present + env(sponsor::transfer(alice, tfSponsorshipCreate), Ter(temMALFORMED)); + + // Sponsor field present without spfSponsorReserve + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorFee), + Ter(temINVALID_FLAG)); + + // Sponsee field present + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::SponseeAcc(bob), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temMALFORMED)); + } + + // Malformed tests for tfSponsorshipReassign + { + // No sponsor field present + env(sponsor::transfer(alice, tfSponsorshipReassign), Ter(temMALFORMED)); + + // Sponsor field present without spfSponsorReserve + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor, spfSponsorFee), + Ter(temINVALID_FLAG)); + + // Sponsee field present + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::SponseeAcc(bob), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temMALFORMED)); + } + + // Malformed tests for tfSponsorshipEnd + { + // Sponsor field present + env(sponsor::transfer(alice, tfSponsorshipEnd), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temMALFORMED)); + + // SponsorFlags field present + auto tx = sponsor::transfer(alice, tfSponsorshipEnd); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + env(tx, Ter(temINVALID_FLAG)); + + // Account = Sponsee + env(sponsor::transfer(alice, tfSponsorshipEnd), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + } + } + + { + // Invalid SponsorshipEnd permission (sponsor object/sponsor account) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + { + // sponsor object + env.fund(XRP(1000), charlie); + env.close(); + env(deposit::auth(alice, charlie), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const keylet = keylet::depositPreauth(alice, charlie); + env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + } + { + // sponsor object + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(sponsor::transfer(bob, tfSponsorshipEnd), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + } + { + // The provided sfSponsee account does not exist + // when ending sponsorship. + Account const ghost("ghost"); // never funded, absent from ledger + env(sponsor::transfer(sponsor, tfSponsorshipEnd), + sponsor::SponseeAcc(ghost), + Ter(terNO_ACCOUNT)); + } + } + + { + // sponsor account + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + + // sfSponsor provided but sfSponsorSignature not provided + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(temMALFORMED)); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2)); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 1); + auto const sle1 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id()); + + // transfer sponsor + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2)); + + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + BEAST_EXPECT( + !env.le(keylet::account(sponsor1))->isFieldPresent(sfSponsoringAccountCount)); + auto const sle2 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); + + // Reassign to the current sponsor is a no-op and is rejected + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env.close(); + + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + + // sponsor 2 accounts + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 3)); + env(sponsor::transfer(bob, tfSponsorshipCreate), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + // dissolve sponsors + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, alice, accountReserve(env, 1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + auto const sle3 = env.le(keylet::account(alice)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor)); + + env(sponsor::transfer(bob, tfSponsorshipEnd)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, bob) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringAccountCount)); + auto const sle4 = env.le(keylet::account(bob)); + BEAST_EXPECT(!sle4->isFieldPresent(sfSponsor)); + + // not sponsored + env(sponsor::transfer(bob, tfSponsorshipEnd), Ter(tecNO_PERMISSION)); + env.close(); + } + { + // dissolve account sponsorship from sponsor + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(alice)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0); + } + + { + // sponsor object (co-signing) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, reserve(env, 1) - drops(1)); + adjustAccountXRPBalance(env, sponsor2, reserve(env, 1) - drops(1)); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env(pay(alice, sponsor1, drops(1))); + env.close(); + + // Invalid ObjectID (not found) + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet::check(alice, 0).key), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_ENTRY)); + env.close(); + + // Invalid Owner + env(sponsor::transfer(bob, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + // Reassign an object that is not sponsored yet + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + // Valid Owner + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + auto const sle1 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id()); + + // Create on an object that is already sponsored + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env.close(); + + // Reassign to the current sponsor is a no-op and is rejected + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecINSUFFICIENT_RESERVE)); + + env(pay(alice, sponsor2, drops(1))); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + auto const sle2 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); + + // dissolve sponsor: ending an object sponsorship succeeds even + // when the sponsee lacks sufficient reserve to reclaim the object. + adjustAccountXRPBalance(env, alice, reserve(env, 1) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount)); + auto const sle3 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor)); + + // Ending sponsorship on an object that is not sponsored (a ticket, + // which cannot be sponsored) is rejected. + adjustAccountXRPBalance(env, alice, reserve(env, 2)); + auto const ticketSeq = env.seq(alice); + env(ticket::create(alice, 1)); + env.close(); + auto ticketId = keylet::ticket(alice, ticketSeq + 1).key; + BEAST_EXPECT(env.le(keylet::unchecked(ticketId))); + env(sponsor::transfer(alice, tfSponsorshipEnd, ticketId), Ter(tecNO_PERMISSION)); + env.close(); + env(noop(alice), ticket::Use(ticketSeq + 1)); + env.close(); + } + { + // sponsor object (pre-funded + no ltSponsorship entry) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(terNO_PERMISSION)); + env.close(); + + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(terNO_PERMISSION)); + env.close(); + } + { + // sponsor object (pre-funded) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + // insufficient reserve count + env(sponsor::set_fee(sponsor1, 0, XRP(100)), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env(sponsor::set_reserve(sponsor1, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + auto checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor1.id()); + auto sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + + // Reassign to the current sponsor is rejected and must not draw + // down the pre-funded reserve budget + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(tecNO_PERMISSION)); + env.close(); + + sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + + // transfer sponsor + env(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor2.id()); + sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + auto sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99); + + // dissolve sponsor + adjustAccountXRPBalance(env, alice, reserve(env, 1)); + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount)); + checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(!checkSle->isFieldPresent(sfSponsor)); + sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99); + } + + { + // Dissolve object sponsorship from sponsor(no-ltSponsorship) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // not the owner of the object + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION)); + env.close(); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + { + // Dissolve object sponsorship from sponsor (with ltSponsorship) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == + 100); + + // not the owner of the object + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION)); + env.close(); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == + 100); + } + + { + // sponsor trustline + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + auto const& highAcc = alice > bob ? alice : bob; + auto const& lowAcc = alice > bob ? bob : alice; + + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + env(trust(user, issuer["usd"](100))); + env.close(); + + auto const trustId = keylet::trustLine(user, issuer, currency); + BEAST_EXPECT(env.le(trustId)); + + // transfer sponsor + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(trustId)); + + BEAST_EXPECT( + env.le(trustId)->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) == + sponsor.id()); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + + // dissolve sponsor + env(sponsor::transfer(user, tfSponsorshipEnd, trustId.key)); + env.close(); + + BEAST_EXPECT(env.le(trustId)); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfLowSponsor : sfHighSponsor)); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + } + } + + { + // invalid transfer + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // create owner dir + env(ticket::create(alice, 1)); + env.close(); + + // AccountRoot + // Amendments + // LedgerHashes + // FeeSettings + // NegativeUNL + // DirNode + auto const keylets = { + keylet::account(alice), + // keylet::amendments(), + keylet::skip(), + keylet::feeSettings(), + // keylet::negativeUNL(), + keylet::ownerDir(alice), + }; + for (auto const& keylet : keylets) + { + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + } + } + + { + // existing owner objects that are outside the v1 SponsorshipTransfer + // object allow-list + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + auto const checkBlocked = [&](Account const& account, uint256 const& objectID) { + env(sponsor::transfer(account, tfSponsorshipCreate, objectID), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + env.close(); + }; + + auto const ticketSeq = env.seq(alice); + env(ticket::create(alice, 1)); + env.close(); + auto const ticketID = keylet::ticket(alice, ticketSeq + 1).key; + BEAST_EXPECT(env.le(keylet::unchecked(ticketID))); + checkBlocked(alice, ticketID); + + env(did::setValid(alice)); + env.close(); + auto const didKeylet = keylet::did(alice.id()); + BEAST_EXPECT(env.le(didKeylet)); + checkBlocked(alice, didKeylet.key); + + env(token::mint(alice, 0u)); + env.close(); + auto const nftPageKeylet = keylet::nftokenPageMax(alice); + BEAST_EXPECT(env.le(nftPageKeylet)); + checkBlocked(alice, nftPageKeylet.key); + + Account const borrower("borrower"); + env.fund(XRP(1000000), borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset}); + env(vaultTx); + env.close(); + + env(vault.deposit( + {.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + env(loanBroker::set(alice, vaultKeylet.key), + loanBroker::kDebtMaximum(xrpAsset(1000).value()), + loanBroker::kManagementFeeRate(TenthBips16{0}), + loanBroker::kCoverRateMinimum(TenthBips32{0}), + loanBroker::kCoverRateLiquidation(TenthBips32{0})); + env.close(); + + auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + env(loan::set(borrower, brokerKeylet.key, xrpAsset(100).value()), + Sig(sfCounterpartySignature, alice), + Fee(env.current()->fees().base * 2)); + env.close(); + BEAST_EXPECT(env.le(loanKeylet)); + checkBlocked(borrower, loanKeylet.key); + } + } + + void + testSponsorFee() + { + using namespace test::jtx; + + testcase("Sponsor Fee"); + + { + // co-signing + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob); + env.close(); + + { + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terNO_ACCOUNT)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + env.fund(XRP(1000), sponsor); + env.close(); + + { + // Sponsor pays the Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + env(pay(alice, bob, sendAmt), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + { + // insufficient balance to pay Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + { + // Fee is paid by Sponsor + // on context reset (tec error) + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto const feeAmt = XRP(10); + + env(pay(alice, bob, XRP(20000)), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + { + // A co-signed sponsor pays the fee from its own balance, but + // must never be charged into its own account reserve. With a + // balance of exactly reserve + fee the fee is still payable, + // charging the sponsor down to precisely its reserve. + auto const feeAmt = XRP(10); + adjustAccountXRPBalance(env, sponsor, reserve(env, 0) + feeAmt); + auto const sponsorBalance = env.balance(sponsor); + + env(noop(alice), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + BEAST_EXPECT(env.balance(sponsor) == reserve(env, 0)); + } + + { + // below reserve + adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); + + env(noop(alice), + Fee(env.current()->fees().base), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + + env(noop(alice), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + } + } + + { + // pre funded + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + { + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(alice)); + env.close(); + + { + // Sponsor pays the Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + env(pay(alice, bob, sendAmt), Fee(feeAmt), sponsor::As(sponsor, spfSponsorFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT( + sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee - feeAmt); + } + + { + // insufficient balance to pay Fee + { + // > FeeAmount + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(90) + drops(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terINSUF_FEE_B)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee); + } + // use all FeeAmount + { + // = FeeAmount + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(90)), + sponsor::As(sponsor, spfSponsorFee), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100)); + BEAST_EXPECT(env.balance(bob) == bobBalance + XRP(100)); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT( + !env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + + // reset FeeAmount and MaxFee + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::set_fee(sponsor, 0, XRP(10), XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + { + // > MaxFee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(1) + drops(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terINSUF_FEE_B)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee); + } + } + + { + // Fee is paid by Sponsor + // on context reset (tec error) + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + auto const feeAmt = XRP(1); + + env(pay(alice, bob, XRP(20000)), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT( + sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee - feeAmt); + } + + // make sfFeeAmount absent if tec error and all Fee is paid + { + // reset FeeAmount and MaxFee + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env(sponsor::set_fee(sponsor, 0, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + auto sponsorAvailableFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice); + env(check::cancel(alice, uint256(1)), + Fee(sponsorAvailableFee), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecNO_ENTRY)); + env.close(); + BEAST_EXPECT( + !env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + } + + // MaxFee cap is enforced in reset() for tec-failing transactions. + // On a closed ledger view (!view.open()), checkFee returns tecINSUFF_FEE when + // Fee > MaxFee (not terINSUF_FEE_B), triggering reset() + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const carol("sponsor"); + + env.fund(XRP(10000), alice, carol); + env.close(); + + // FeeAmount=1000 drops, MaxFee=10 drops + env(sponsor::set_fee(carol, 0, drops(1000), drops(10)), sponsor::SponseeAcc(alice)); + env.close(); + + // Apply directly against the closed ledger view (open_ = false) so that + // checkFee returns tecINSUFF_FEE and reset() is invoked. + OpenView overlay(&*env.closed()); + + auto jt = env.jt( + noop(alice), + Fee(drops(1000)), + Seq(env.seq(alice)), + sponsor::As(carol, spfSponsorFee)); + + auto const result = xrpl::apply(env.app(), overlay, *jt.stx, TapNone, env.journal); + BEAST_EXPECT(result.ter == tecINSUFF_FEE); + BEAST_EXPECT(result.applied); + + // Only MaxFee (10 drops) must be deducted, not the full 1000 drops. + auto const sle = overlay.read(keylet::sponsorship(carol.id(), alice.id())); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10) + } + + // LedgerStateFix charges an owner-reserve fee and can claim that fee + // while returning tecFAILED_PROCESSING. That path must be safe when the + // fee is pre-funded by a sponsorship object. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + env(ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecFAILED_PROCESSING)); + + if (auto const sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + } + + // If preclaim saw spendable sponsored FeeAmount but the apply view no + // longer has it, the fee path should fail cleanly instead of throwing. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + OpenView overlay(&*env.closed()); + auto jt = env.jt( + ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee)); + + auto const pf = preflight(env.app(), overlay.rules(), *jt.stx, TapNone, env.journal); + BEAST_EXPECT(isTesSuccess(pf.ter)); + auto const pc = preclaim(pf, env.app(), overlay); + BEAST_EXPECT(isTesSuccess(pc.ter)); + + auto const original = overlay.read(keylet::sponsorship(sponsor, alice)); + if (BEAST_EXPECT(original)) + { + auto sle = std::make_shared(*original); + sle->makeFieldAbsent(sfFeeAmount); + overlay.rawReplace(sle); + } + + auto const result = doApply(pc, env.app(), overlay); + BEAST_EXPECT(result.ter == terINSUF_FEE_B); + BEAST_EXPECT(!result.applied); + } + + // test lsfSponsorshipRequireSignForFee + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == + XRP(10)); + + // clear flag + env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // Payment is re-applied + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + + // RequireSignForFee: co-signing should succeed + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // pre-funded (no sig) should fail + env(pay(alice, bob, XRP(100)), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + env.close(); + + // co-signing (with sig) should succeed + env(pay(alice, bob, XRP(100)), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(9)); + } + } + + void + testSponsorAccount() + { + testcase("Sponsor Account"); + using namespace test::jtx; + + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + Account const sponsor3("sponsor3"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const dave("dave"); + Account const gw("gw"); + auto const usd = gw["usd"]; + + { + // Disabled + Env env{*this, testableAmendments() - featureSponsor}; + env.fund(XRP(10000), alice, sponsor); + env.close(); + env(pay(alice, bob, XRP(100)), Txflags(tfSponsorCreatedAccount), Ter(temDISABLED)); + env.close(); + } + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, sponsor, sponsor2, sponsor3); + env.close(); + + // Invalid flags + for (auto flag : { + tfNoRippleDirect, + tfPartialPayment, + tfLimitQuality, + }) + { + env(pay(alice, bob, XRP(100)), + Txflags(tfSponsorCreatedAccount | flag), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Invalid amount(iou) + env(pay(alice, bob, usd(100)), Txflags(tfSponsorCreatedAccount), Ter(temBAD_AMOUNT)); + env.close(); + + // Sponsored account creation is reserve sponsorship and is only supported for direct XRP + // payments. + env(pay(alice, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Sendmax(usd(2)), + Ter(temINVALID)); + env.close(); + + env(pay(alice, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Path(~XRP), + Ter(temINVALID)); + env.close(); + + // Account is not sponsored by normal Sponsor specification + { + env(pay(alice, bob, drops(env.current()->fees().accountReserve(0, 1))), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const bobSle = env.le(keylet::account(bob)); + BEAST_EXPECT(!bobSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0); + } + + // Use tfSponsorCreatedAccount to sponsor an account + { + // to funded account + env(pay(sponsor2, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tecNO_SPONSOR_PERMISSION)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor2) == XRP(9999)); + + // to non-funded account / insufficient balance for reserve + env(pay(sponsor2, charlie, XRP(9999) - env.current()->fees().reserve + drops(1)), + Txflags(tfSponsorCreatedAccount), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + // to non-funded account + auto const sponsor2BalanceBefore = env.balance(sponsor2); + env(pay(sponsor2, charlie, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1))); + env.close(); + + auto const charlieSle = env.le(keylet::account(charlie)); + BEAST_EXPECT(charlieSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(charlieSle->getAccountID(sfSponsor) == sponsor2.id()); + BEAST_EXPECT(sponsoredOwnerCount(env, charlie) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + // verify sponsor balance decreased by payment + Fee + BEAST_EXPECT(env.balance(sponsor2) == sponsor2BalanceBefore - drops(1) - XRP(1)); + } + { + // insufficient reserve to sponsor acount + + auto const sendAmount = drops(1); + // 2 account reserve + send amount + auto const requireBalance = accountReserve(env, 2) + sendAmount; + adjustAccountXRPBalance(env, sponsor3, requireBalance - drops(1)); + env(pay(sponsor3, dave, sendAmount), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + adjustAccountXRPBalance(env, sponsor3, requireBalance); + env(pay(sponsor3, dave, sendAmount), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + } + } + + void + testRequireFlag() + { + using namespace test::jtx; + { + testcase("SponsorshipRequireSignForReserve"); + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_reserve(sponsor, tfSponsorshipSetRequireSignForReserve, 10), + sponsor::SponseeAcc(alice)); + env.close(); + + env(check::create(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorReserve), + Ter(terNO_PERMISSION)); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // clear flag + env(sponsor::set_reserve(sponsor, tfSponsorshipClearRequireSignForReserve, 1), + sponsor::SponseeAcc(alice)); + env.close(); + + // CheckCreate is re-applied + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + + { + testcase("SponsorshipRequireSignForFee"); + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + env(check::create(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_PERMISSION)); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == + XRP(10)); + + // clear flag + env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // CheckCreate is re-applied + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + } + + void + testSponsorReserveSimple(bool cosigning) + { + testcase("SponsorReserveSimple"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 1) - drops(1)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 250), sponsor::SponseeAcc(alice)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + + env(check::create(alice, bob, XRP(100)), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tesSUCCESS)); + env.close(); + } + } + + // test helper for both cosigning and pre-funded sponsorship + template + void + testEachSponsorship( + test::jtx::Env& env, + bool cosigning, + jtx::Account const& sponsor, + jtx::Account const& sponsee, + uint32_t reserveCount, + uint32_t sponsorReserveCount, + TER insufficientReserveResult, + SubmitCallback callback, + std::optional> expected = std::nullopt) + { + using namespace test::jtx; + auto const sponseeOwnerCountBefore = ownerCount(env, sponsee); + auto const sponseeSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsee); + auto const sponseeSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsee); + auto const sponsorOwnerCountBefore = ownerCount(env, sponsor); + auto const sponsorSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsor); + auto const sponsorSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsor); + + std::optional sponsorSig = + cosigning ? std::optional(Sig(sfSponsorSignature, sponsor)) : std::nullopt; + + auto const sponsorCurrentOwnerCount = ownerCount(env, sponsor) - + sponsoredOwnerCount(env, sponsor) + sponsoringOwnerCount(env, sponsor); + + auto submit = [&](TER ter) { + return [&, ter](json::Value const& jv, auto const&... fN) { + if (sponsorSig) + { + env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), *sponsorSig, Ter(ter)); + } + else + { + env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), Ter(ter)); + } + }; + }; + + // Insufficient Reserve + { + if (cosigning) + { + adjustAccountXRPBalance( + env, + sponsor, + reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount) - drops(1)); + } + else + { + // cleanup previous sponsorship + if (env.le(keylet::sponsorship(sponsor, sponsee))) + { + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env.close(); + } + + if (sponsorReserveCount > 1) + { + env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)), + sponsor::SponseeAcc(sponsee)); + } + else + { + // just create sponsor object + env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)), + sponsor::SponseeAcc(sponsee)); + } + env.close(); + } + + // A failed sponsored create must not consume prefunded reserve or mutate owner counts. + auto const sponseeOwnerCountBeforeAttempt = ownerCount(env, sponsee); + auto const sponseeSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsee); + auto const sponseeSponsoringOwnerCountBeforeAttempt = + sponsoringOwnerCount(env, sponsee); + auto const sponsorOwnerCountBeforeAttempt = ownerCount(env, sponsor); + auto const sponsorSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsor); + auto const sponsorSponsoringOwnerCountBeforeAttempt = + sponsoringOwnerCount(env, sponsor); + auto const sponsorshipSleBeforeAttempt = env.le(keylet::sponsorship(sponsor, sponsee)); + bool const reserveCountPresentBeforeAttempt = sponsorshipSleBeforeAttempt && + sponsorshipSleBeforeAttempt->isFieldPresent(sfRemainingOwnerCount); + std::uint32_t const reserveCountBeforeAttempt = reserveCountPresentBeforeAttempt + ? sponsorshipSleBeforeAttempt->getFieldU32(sfRemainingOwnerCount) + : 0; + + callback(env, submit(insufficientReserveResult)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == sponseeOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsee) == sponseeSponsoredOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsee) == sponseeSponsoringOwnerCountBeforeAttempt); + BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) == sponsorSponsoringOwnerCountBeforeAttempt); + + auto const sponsorshipSleAfterAttempt = env.le(keylet::sponsorship(sponsor, sponsee)); + BEAST_EXPECT( + static_cast(sponsorshipSleAfterAttempt) == + static_cast(sponsorshipSleBeforeAttempt)); + if (sponsorshipSleAfterAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->isFieldPresent(sfRemainingOwnerCount) == + reserveCountPresentBeforeAttempt); + if (reserveCountPresentBeforeAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->getFieldU32(sfRemainingOwnerCount) == + reserveCountBeforeAttempt); + } + } + } + + // Success + { + if (cosigning) + { + adjustAccountXRPBalance( + env, sponsor, reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount)); + } + else + { + // reset sponsorship + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env(sponsor::set(sponsor, 0, sponsorReserveCount, XRP(1)), + sponsor::SponseeAcc(sponsee)); + env.close(); + } + callback(env, submit(tesSUCCESS)); + env.close(); + + if (!cosigning) + { + // Prefunded success consumes the reserved owner slot before cleanup. + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, sponsee)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT( + !sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) || + sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0); + + // cleanup sponsorship + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env.close(); + } + } + + if (expected) + { + (*expected)(); + } + else + { + BEAST_EXPECT(ownerCount(env, sponsee) - sponseeOwnerCountBefore == reserveCount); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsee) - sponseeSponsoredOwnerCountBefore == + sponsorReserveCount); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsee) - sponseeSponsoringOwnerCountBefore == 0); + BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBefore); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBefore); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) - sponsorSponsoringOwnerCountBefore == + sponsorReserveCount); + } + }; + + void + testCheck(bool cosigning) + { + testcase("Check"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const usd = gw["usd"]; + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor, sponsor2); + env.close(); + + // CheckCreate -> Check -> CheckCancel + + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit(check::create(alice, bob, XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // Check + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + auto const keylet = keylet::check(alice, seq); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + if (cosigning) + { + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); // Check + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // CheckCancel + env(check::cancel(alice, keylet.key)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // CheckCreate -> CheckCash + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + // CheckCash + auto const checkId2 = keylet::check(alice, seq2).key; + env(check::cash(bob, checkId2, XRP(1))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + // RippleState sponsor (CheckCashMakesTrustLine) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env.trust(usd(100), alice); + env.close(); + env(pay(gw, alice, usd(100))); + env.close(); + + // CheckCreate -> CheckCash + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, usd(1))); + }); + + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + auto const keylet = keylet::check(alice, seq2); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // CheckCash + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(check::cash(bob, keylet.key, usd(1))); }, + [&]() { + BEAST_EXPECT(ownerCount(env, alice) == 1); // RippleState + BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // the newly-created trust line carries the sponsor on + // bob's side, and no sponsor on the counterparty side + bool const bobLow = bob.id() < gw.id(); + auto const lineSle = env.le(keylet::trustLine(bob, gw, usd.currency)); + BEAST_EXPECT(lineSle); + if (lineSle) + { + BEAST_EXPECT( + lineSle->getAccountID(bobLow ? sfLowSponsor : sfHighSponsor) == + sponsor.id()); + BEAST_EXPECT( + !lineSle->isFieldPresent(bobLow ? sfHighSponsor : sfLowSponsor)); + } + }); + } + + // MPT sponsor: cashing an MPT check creates a sponsored MPToken for + // the casher. Unlike the trust-line path, this returns + // tecINSUFFICIENT_RESERVE (not tecNO_LINE_INSUF_RESERVE) on shortfall. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), bob, sponsor, sponsor2); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // CheckCreate (alice -> bob) paying the MPT + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, mpt(1))); + }); + + auto const checkKeylet = keylet::check(alice, seq2); + BEAST_EXPECT(env.le(checkKeylet)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, bob) == 0); + + // CheckCash by bob (no MPToken yet) creates a sponsored MPToken + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(check::cash(bob, checkKeylet.key, mpt(1))); + }, + [&]() { + BEAST_EXPECT(ownerCount(env, bob) == 1); // MPToken + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT( + env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) == + sponsor.id()); + }); + } + } + + void + testDelegate(bool cosigning) + { + testcase("Delegate"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // DelegateSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(delegate::set(alice, bob, {"Payment"})); + }); + + // the created Delegate object carries the sponsor + auto const keylet = keylet::delegate(alice, bob); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // delete + env(delegate::set(alice, bob, {})); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testDepositPreauth(bool cosigning) + { + testcase("DepositPreauth"); + using namespace test::jtx; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + auto const credType = std::string("credType"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + // DepositPreauthSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(deposit::auth(alice, sponsor)); }); + + // the created DepositPreauth object carries the sponsor + auto const keylet = keylet::depositPreauth(alice, sponsor); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + // No sponsor signature here: this exercises the prefunded reassign path. + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + auto const sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle); + if (sponsor2Sle) + { + BEAST_EXPECT( + !sponsor2Sle->isFieldPresent(sfRemainingOwnerCount) || + sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 0); + } + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // DepositPreauthDelete + env(deposit::unauth(alice, sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor); + env.close(); + auto const authCreds = std::vector{ + {.issuer = sponsor, .credType = credType}}; + auto const preauthKeylet = keylet::depositPreauth( + alice.id(), + std::set>{ + {sponsor.id(), Slice(credType.data(), credType.size())}}); + + // Cover DepositPreauth's sfAuthorizeCredentials sponsor-reserve branch. + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env&, auto const& submit) { + submit(deposit::authCredentials(alice, authCreds)); + }); + + // Cover sfUnauthorizeCredentials cleanup for a sponsored preauth object. + BEAST_EXPECT(env.le(preauthKeylet)); + BEAST_EXPECT(env.le(preauthKeylet)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + env(deposit::unauthCredentials(alice, authCreds)); + env.close(); + + BEAST_EXPECT(!env.le(preauthKeylet)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testCredential(bool cosigning) + { + testcase("Credential"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + auto const credType = std::string("test"); + + // Self-issued credential: alice creates for herself, sponsor covers reserve + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(alice, alice, credType)); + }); + + auto const credKeylet = credentials::keylet(alice, alice, credType); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // CredentialDelete + env(credentials::deleteCred(alice, alice, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // Issuer creates for subject; sponsorship transfers to subject on accept + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // alice (issuer) creates credential for bob (subject), sponsor covers alice's reserve + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(bob, alice, credType)); + }); + + // Before accept: alice (issuer) owns the credential + auto const credKeylet = credentials::keylet(bob, alice, credType); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + // Bob accepts: issuer's sponsorship ends, bob becomes unsponsored owner + env(credentials::accept(bob, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); // original sponsorship ended + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); // bob owns it without a sponsor + + // CredentialDelete by subject + env(credentials::deleteCred(bob, bob, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + // Issuer creates for subject; accept tx itself is reserve-sponsored + // This exercises the checkReserve() call in CredentialAccept::doApply() + // that guards the sponsor's reserve when featureSponsor is enabled. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // alice (issuer) creates credential for bob (subject) without sponsor + env(credentials::create(bob, alice, credType)); + env.close(); + + auto const credKeylet = credentials::keylet(bob, alice, credType); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + + // Bob accepts with a sponsored reserve; the first attempt uses an + // undercapitalized sponsor and must fail with tecINSUFFICIENT_RESERVE; + // the second attempt uses a properly funded sponsor and must succeed. + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::accept(bob, alice, credType)); + }); + + // After successful accept: alice (issuer) no longer owns the credential, + // bob owns it and sponsor covers his reserve. + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + // Cleanup + env(credentials::deleteCred(bob, bob, alice, credType)); + env.close(); + } + } + + void + testEscrow(bool cosigning) + { + testcase("Escrow"); + using namespace test::jtx; + using namespace std::chrono_literals; + + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + { + // Native Escrow + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // EscrowCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, XRP(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor2.id()); + + // EscrowFinish + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + Account const gw("gw"); + auto const usd = gw["usd"]; + { + // IOU Escrow + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000000), alice); + env.close(); + env(pay(gw, alice, usd(10000))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // EscrowCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, usd(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + // EscrowFinish + testEachSponsorship( + env, + cosigning, + sponsor2, + bob, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { + submit( + escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150)); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + BEAST_EXPECT( + env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) == + sponsor2.id()); + } + { + // IOU EscrowFinish recycles reserve when the same sponsor backs + // the escrow being removed and the destination line being created. + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000000), alice); + env.close(); + env(pay(gw, alice, usd(10000))); + env.close(); + + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, usd(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + } + + if (cosigning) + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + else + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + BEAST_EXPECT( + env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) == + sponsor.id()); + } + { + // IOU Escrow cancel re-creates the owner's trust line, and the + // cancel transaction's sponsor can cover that new line's reserve. + // Creating an IOU escrow moves the sender's balance to the issuer, + // so the sender can delete their now-zero trust line while the + // escrow is pending. Cancelling returns the funds, re-creating the + // line. + Env env{*this, testableAmendments()}; + + env.fund(XRP(1000000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000000), alice); + env.close(); + env(pay(gw, alice, usd(10000))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // alice escrows her entire balance so the trust line can be removed + auto const cancelAfter = env.now() + 100s; + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, usd(10000)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(cancelAfter)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); // trust line + escrow + + // alice deletes her now-zero trust line while the escrow is pending + env(trust(alice, usd(0))); + env.close(); + BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(ownerCount(env, alice) == 1); // just the escrow + + // advance the ledger past the cancel time + for (; env.now() < cancelAfter; env.close()) + { + } + + // EscrowCancel by alice re-creates her trust line; the sponsor + // covers the new line's reserve. On the insufficient-reserve + // attempt the whole transaction rolls back, leaving the escrow + // intact to be cancelled on the success attempt. + bool const aliceLow = alice.id() < gw.id(); + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(escrow::cancel(alice, alice, seq)); }, + [&]() { + BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + auto const trustSle = env.le(keylet::trustLine(alice, gw, usd.currency)); + BEAST_EXPECT(trustSle); + if (trustSle) + { + BEAST_EXPECT( + trustSle->getAccountID(aliceLow ? sfLowSponsor : sfHighSponsor) == + sponsor.id()); + BEAST_EXPECT( + !trustSle->isFieldPresent(aliceLow ? sfHighSponsor : sfLowSponsor)); + } + BEAST_EXPECT(ownerCount(env, alice) == 1); // re-created trust line + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + }); + } + { + // MPT Escrow + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // create Escrow from alice to bob + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // finish Escrow: bob has no MPToken, so finishing creates one and + // the finish transaction's sponsor covers its reserve. The MPT + // create-token path returns tecINSUFFICIENT_RESERVE (not + // tecNO_LINE_INSUF_RESERVE) when the sponsor is underfunded. + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit( + escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT( + env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) == + sponsor.id()); + } + { + // MPT EscrowFinish has the same reserve recycling behavior as IOU + // when it creates the destination MPToken. + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, mpt(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + } + + if (cosigning) + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + else + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + BEAST_EXPECT( + env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) == + sponsor.id()); + } + + // A sponsored EscrowCreate must still verify that the source + // can fund the escrow amount and stay above its own base + // reserve. The sponsor covers the new object's owner + // increment, but cannot cover the source's base reserve. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // alice's balance is just above the base reserve. After + // locking escrowAmount she would dip below it. + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1)); + + auto const escrowAmount = XRP(2); + auto const seq = env.seq(alice); + + if (cosigning) + { + env(escrow::create(alice, bob, escrowAmount), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED)); + } + else + { + env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + env(escrow::create(alice, bob, escrowAmount), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecUNFUNDED)); + } + env.close(); + + BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testMPToken(bool cosigning) + { + testcase("MPToken"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // MPTokenIssuanceCreate + json::Value jv = {}; + jv[sfAccount] = alice.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + MPTID mptid; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + mptid = makeMptID(env.seq(alice), alice.id()); + submit(jv); + }); + + // transfer sponsor + auto const mptIssuanceKeylet = keylet::mptokenIssuance(mptid); + + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, mptIssuanceKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, mptIssuanceKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // MPTokenAuthorize + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free mptoken + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(jv); }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + + // transfer sponsor + auto const mptTokenKeylet = keylet::mptoken(mptid, bob); + if (cosigning) + { + env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + + env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2); + + // MPTokenAuthorize Unauthorize + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + jv[sfFlags] = tfMPTUnauthorize; + env(jv); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // MPTokenIssuanceDestroy + jv = {}; + jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; + jv[sfAccount] = alice.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + env(jv); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + { + // check INSUFFICIENT_RESERVE for MPToken + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // MPTokenAuthorize + json::Value jv = {}; + jv[sfAccount] = alice.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + auto const mptid = makeMptID(env.seq(alice), alice.id()); + env(jv); + env.close(); + + // Create tickets so the sponsor is past free-tier reserve behavior. + std::uint32_t const ticketSeq{env.seq(sponsor) + 1}; + env(ticket::create(sponsor, 2)); + env.close(); + + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + // error (non-free mptoken) + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - drops(1)); + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + else + { + env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)), sponsor::SponseeAcc(bob)); + env.close(); + + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + env(noop(sponsor), ticket::Use(ticketSeq)); + env.close(); + + // pass (free-tier mptoken for the holder, but the sponsor is still + // charged a reserve increment regardless of the ownerCount < 2 shortcut). + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tesSUCCESS)); + env.close(); + } + } + } + + void + testPayChan(bool cosigning) + { + testcase("PayChan"); + using namespace test::jtx; + using namespace std::literals::chrono_literals; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // PayChanCreate + auto const pk = alice.pk(); + auto const settleDelay = 10s; + uint256 chan; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + chan = paychan::channel(alice, bob, env.seq(alice)); + submit(paychan::create(alice, bob, XRP(100), settleDelay, pk)); + }); + + // the created PayChannel object carries the sponsor + BEAST_EXPECT(env.le(Keylet(ltPAYCHAN, chan))->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, chan), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, chan), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(Keylet(ltPAYCHAN, chan))->getAccountID(sfSponsor) == sponsor2.id()); + + env.close(env.now() + settleDelay); + // PayChanClaim (delete PayChan) + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // A sponsored PaymentChannelCreate must still verify that the + // source can fund the channel amount and stay above its own + // base reserve. The sponsor covers the new object's owner + // increment, but cannot cover the source's base reserve. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // alice's balance is just above the base reserve. After + // locking channelAmount she would dip below it. + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1)); + + auto const pk = alice.pk(); + auto const settleDelay = 10s; + auto const channelAmount = XRP(2); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + + if (cosigning) + { + env(paychan::create(alice, bob, channelAmount, settleDelay, pk), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED)); + } + else + { + env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + env(paychan::create(alice, bob, channelAmount, settleDelay, pk), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecUNFUNDED)); + } + env.close(); + + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testSignerList(bool cosigning) + { + testcase("SignerList"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // SignerListSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(signers(alice, 1, {{bob, 1}})); }); + + // the created SignerList object carries the sponsor + BEAST_EXPECT(env.le(keylet::signerList(alice))->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + // invalid signer list owner 1 + // account doesn't have signer list but specified signer list exists + env(sponsor::transfer(bob, tfSponsorshipReassign, keylet::signerList(alice).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + // invalid signer list owner 2 + // account has signer list and specified signer list exists + env(signers(bob, 1, {{alice, 1}})); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(bob).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(alice).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(alice).key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(keylet::signerList(alice))->getAccountID(sfSponsor) == sponsor2.id()); + + // Delete + env(signers(alice, NoneT())); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // Legacy (pre-MultiSignReserve) SignerLists lack lsfOneOwnerCount and cost + // 2 + signer_count owner units, whereas modern lists cost 1. + void + testLegacySignerListReserve() + { + testcase("Legacy SignerList sponsorship reserve"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + Account const sponsor("sponsor"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol, dave, sponsor); + env.close(); + + // Modern 3-signer list: weight 1, lsfOneOwnerCount set. + env(signers(alice, 1, {{bob, 1}, {carol, 1}, {dave, 1}})); + env.close(); + + auto const signerListKeylet = keylet::signerList(alice.id()); + auto const sponsorKeylet = keylet::sponsorship(sponsor.id(), alice.id()); + std::uint32_t const legacyWeight = 5; // 2 + 3 signers + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // Pre-fund exactly the legacy weight + env(sponsor::set_reserve(sponsor, 0, legacyWeight), sponsor::SponseeAcc(alice)); + env.close(); + if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == legacyWeight); + + // Synthesize a pre-MultiSignReserve list: clear lsfOneOwnerCount and + // restore the owner's OwnerCount to the legacy weight. + env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool { + auto signerList = std::make_shared(*view.read(signerListKeylet)); + auto account = std::make_shared(*view.read(keylet::account(alice.id()))); + signerList->clearFlag(lsfOneOwnerCount); + account->setFieldU32(sfOwnerCount, legacyWeight); + view.rawReplace(signerList); + view.rawReplace(account); + return true; + }); + if (auto const sle = env.le(signerListKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT((sle->getFlags() & lsfOneOwnerCount) == 0); + BEAST_EXPECT(ownerCount(env, alice) == legacyWeight); + + // Create must charge the full legacy weight (5), not 1: the bug bumped + // the counters by 1 and left 4 pre-funded units unspent. + env(sponsor::transfer(alice, tfSponsorshipCreate, signerListKeylet.key), + sponsor::As(sponsor, spfSponsorReserve)); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == legacyWeight); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == legacyWeight); + // All pre-funded units consumed (drained to absent). + if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + + // Delete unwinds the legacy weight; create bumped by the same amount, so + // the counters return to 0 (the bug bumped by 1 -> underflow on delete). + env(signers(alice, NoneT())); + + BEAST_EXPECT(!env.le(signerListKeylet)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + } + + void + testSponsoredTrustLineNoFreeReserve() + { + // An account with ownerCount < 2 may create its first trust lines even + // without meeting the reserve. In any case, the sponsor pays the full + // reserve in all cases, even for the sponsee's very first trust line. + testcase("Sponsored trust line gets no free-reserve exception"); + using namespace test::jtx; + + Account const issuer("issuer"); + Account const alice("alice"); + Account const sponsor("sponsor"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), issuer, alice, sponsor); + env.close(); + + auto const usd = issuer["usd"]; + auto const lineKeylet = keylet::trustLine(alice, issuer, usd.currency); + + // Sponsor funded for exactly its base reserve + adjustAccountXRPBalance(env, sponsor, reserve(env, 0)); + + // alice's ownerCount is 0, so an unsponsored first trust line would be + // free; but because it is sponsored, the reserve check is enforced + // against the sponsor, which is one increment short. + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + BEAST_EXPECT(!env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Give the sponsor has exactly one owner-reserve increment; the same + // sponsored first trust line now succeeds and the sponsor pays for it. + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 1); + } + + void + testCoSignReserveBoundedBySponsorshipBudget() + { + // sponsor co-signs, so a fee-only object (ReserveCount == 0) makes a co-signed + // reserve sponsorship fail -- with no fallback to the sponsor's balance. + testcase("Co-signed reserve sponsorship is bounded by Sponsorship budget"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const sponsor("sponsor"); + Account const sponsee("sponsee"); + env.fund(XRP(10000), sponsor, sponsee); + env.close(); + + // Prefund a FEE-only Sponsorship for the sponsee; ReserveCount + // defaults to 0. + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + BEAST_EXPECT(env.le(keylet::sponsorship(sponsor, sponsee))); + + // Sponsee creates a Check with the sponsor co-signing the reserve. The + // fee-only Sponsorship's has ReserveCount (0), so this fails + // with tecINSUFFICIENT_RESERVE + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 0); + + // Bumping the Sponsorship's ReserveCount budget makes the same + // co-signed reserve sponsorship succeed, the budget is what gates it. + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(sponsee)); + env.close(); + + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 1); + } + + void + testTrustSet(bool cosigning) + { + testcase("TrustSet"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const validateSponsoredTrustline = + [&](std::shared_ptr const& sle, bool isIssuerHigh, Account const& sponsor) { + BEAST_EXPECT( + sle->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) == sponsor.id()); + BEAST_EXPECT(!sle->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + }; + + auto const& highAcc = alice > bob ? alice : bob; + auto const& lowAcc = alice > bob ? bob : alice; + + // create and delete + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, charlie, sponsor, sponsor2); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + // create TrustLine + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free trustline + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + user, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(trust(user, usd(100))); }); + + auto const keylet = keylet::trustLine(user, issuer, currency); + + if (cosigning) + { + // invalid owner + env(sponsor::transfer(charlie, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + // invalid reserve owner + env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(user)); + env.close(); + env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // delete TrustLine + env(trust(user, usd(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, user) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + BEAST_EXPECT(!env.le(keylet)); + } + + // update + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + // create TrustLine from issuer + env(trust(issuer, user["usd"](100))); + env.close(); + + BEAST_EXPECT(env.le(keylet::trustLine(user, issuer, currency))); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free trustline + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + user, + 1, + 1, + tecINSUF_RESERVE_LINE, + [&](Env& env, auto const& submit) { submit(trust(user, usd(100))); }); + + auto const line = env.le(keylet::trustLine(user, issuer, currency)); + validateSponsoredTrustline(line, isIssuerHigh, sponsor); + + // update TrustLine from user to clear reserve + env(trust(user, usd(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, user) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(env.le(keylet::trustLine(user, issuer, currency))); + + // remove TrustLine from issuer + env(trust(issuer, user["usd"](0))); + env.close(); + BEAST_EXPECT(!env.le(keylet::trustLine(user, issuer, currency))); + } + + // both High and Low sponsored + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // create TrustLines + env(trust(alice, bob["usd"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + env(trust(bob, alice["usd"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto sle = env.le(keylet::trustLine(alice, bob, alice["usd"].currency)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFlag(lsfHighReserve)); + BEAST_EXPECT(sle->isFlag(lsfLowReserve)); + BEAST_EXPECT(sle->getAccountID(sfHighSponsor) == sponsor.id()); + BEAST_EXPECT(sle->getAccountID(sfLowSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + + // clear TrustLines + env(trust(alice, bob["usd"](0))); + env.close(); + env(trust(bob, alice["usd"](0))); + env.close(); + + sle = env.le(keylet::trustLine(alice, bob, alice["usd"].currency)); + BEAST_EXPECT(!sle); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testAccountDelete() + { + testcase("AccountDelete"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + { + // Delete Sponsor/Sponsee Account with ltSponsorship (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // set sponsor + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + incLgrSeqForAccDel(env, sponsor); + + auto const keylet = keylet::sponsorship(sponsor, alice); + auto const sponsorObj = env.le(keylet); + BEAST_EXPECT(sponsorObj); + + // AccountDelete + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + } + + { + // Delete SponsoredAccount + Env env{*this, testableAmendments()}; + env.memoize(alice); + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + // create SponsoredAccount + env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount)); + env.close(); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = non-sponsor + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecNO_SPONSOR_PERMISSION)); + + // The failed delete must leave the account sponsored by the original sponsor. + auto const aliceSle = env.le(keylet::account(alice)); + BEAST_EXPECT(aliceSle); + if (aliceSle) + BEAST_EXPECT(aliceSle->getAccountID(sfSponsor) == sponsor.id()); + + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = sponsor + env(acctdelete(alice, sponsor), Fee(requiredFee), Ter(tesSUCCESS)); + + auto const sponsorSle2 = env.le(keylet::account(sponsor)); + BEAST_EXPECT(!sponsorSle2->isFieldPresent(sfSponsoringAccountCount)); + } + + { + // Sponsor with sfSponsoringOwnerCount cannot delete (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + Account const gw("gw"); + env.fund(XRP(1000000), alice, bob, sponsor, gw); + env.close(); + + auto const usd = gw["usd"]; + + // Create a trust line for alice + env(trust(alice, usd(1000))); + env.close(); + + // Transfer reserve sponsorship of trust line to sponsor + auto const trustId = keylet::trustLine(alice, gw, usd.currency); + BEAST_EXPECT(env.le(trustId)); + + env(sponsor::transfer(alice, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // Verify sfSponsoringOwnerCount is set on sponsor + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringOwnerCount)); + auto const sponsoringOwnerCount = sponsorSle->getFieldU32(sfSponsoringOwnerCount); + BEAST_EXPECT(sponsoringOwnerCount >= 1); + + incLgrSeqForAccDel(env, sponsor); + + // AccountDelete should fail + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + // The failed delete must not decrement the outstanding sponsored-object count. + auto const sponsorSleAfter = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringOwnerCount)); + BEAST_EXPECT( + sponsorSleAfter->getFieldU32(sfSponsoringOwnerCount) == sponsoringOwnerCount); + } + + { + // Sponsor with sfSponsoringAccountCount cannot delete (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + env.memoize(alice); + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + // Create SponsoredAccount (sets sfSponsoringAccountCount on sponsor) + env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount)); + env.close(); + + // Verify sfSponsoringAccountCount is set on sponsor + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringAccountCount)); + auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount); + BEAST_EXPECT(sponsoringAccountCount == 1); + + incLgrSeqForAccDel(env, sponsor); + + // AccountDelete should fail + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + // The failed delete must not decrement the outstanding sponsored-account count. + auto const sponsorSleAfter = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringAccountCount)); + BEAST_EXPECT( + sponsorSleAfter->getFieldU32(sfSponsoringAccountCount) == sponsoringAccountCount); + } + + { + // Account with sponsored objects should be deletable + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // Create sponsored delegate (a non-deletion-blocker) + env(deposit::auth(alice, bob), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + + { + auto const sponsorSle = env.le(keylet::account(sponsor)); + auto const aliceSle = env.le(keylet::account(alice)); + BEAST_EXPECT(sponsorSle->at(sfSponsoringOwnerCount) == 1); + BEAST_EXPECT(aliceSle->at(sfOwnerCount) == 1); + BEAST_EXPECT(aliceSle->at(sfSponsoredOwnerCount) == 1); + } + + incLgrSeqForAccDel(env, alice); + + // AccountDelete should succeed + { + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS)); + BEAST_EXPECT(!env.le(keylet::account(alice))); + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->at(sfSponsoringOwnerCount) == 0); + } + } + } + + void + testDelegatePermission() + { + testcase("DelegatePermission"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + + // + // SponsorshipTransfer is not delegable. + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const keylet = keylet::check(alice, seq); + + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), + sponsor::As(bob, spfSponsorReserve), + Sig(sfSponsorSignature, bob), + delegate::As(carol), + Ter(temINVALID)); + + env(delegate::set(alice, carol, {"SponsorshipTransfer"}), Ter(temMALFORMED)); + } + // + // test send SponsorshipSet on other's behalf + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(terNO_DELEGATE_PERMISSION)); + + env(delegate::set(alice, carol, {"SponsorshipSet"})); + env.close(); + + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(tesSUCCESS)); + env.close(); + } + } + + void + testDelegateBlockReserveSponsor() + { + testcase("Delegate Block Reserve Sponsor"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const sponsor("sponsor"); + + // Co-signed reserve sponsorship is blocked for delegated transactions. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"CheckCreate"})); + env.close(); + + env(check::create(alice, carol, XRP(1)), + delegate::As(bob), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID)); + } + + // Pre-funded reserve sponsorship is blocked for delegated transactions. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"CheckCreate"})); + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + + env(check::create(alice, carol, XRP(1)), + delegate::As(bob), + sponsor::As(sponsor, spfSponsorReserve), + Ter(temINVALID)); + } + } + + void + testDelegateSponsorFeePayer() + { + testcase("Delegate Sponsor Fee Payer"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const sponsor("sponsor"); + + // Co-signing: the sponsor account pays the delegated transaction fee. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const aliceBalance = env.balance(alice); + auto const bobBalance = env.balance(bob); + auto const carolBalance = env.balance(carol); + auto const sponsorBalance = env.balance(sponsor); + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + + env(pay(alice, carol, sendAmt), + delegate::As(bob), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt); + // sponsor pays the fee + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + // Pre-funded: the Sponsorship object for sponsorship(sponsor, delegate) + // pays the fee. + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, carol, sponsor); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(bob)); + env.close(); + + auto const aliceBalance = env.balance(alice); + auto const bobBalance = env.balance(bob); + auto const carolBalance = env.balance(carol); + auto const sponsorBalance = env.balance(sponsor); + auto const sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, bob); + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + + // verify sponsorship(sponsor, alice) is not present, because we are testing + // sponsorship(sponsor, bob) will pay the fee. bob is sfDelegate. + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + env(pay(alice, carol, sendAmt), + delegate::As(bob), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + // sponsorship(sponsor, bob) pays the fee, bob is sfDelegate + BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, bob) == sponsorFee - feeAmt); + } + } + + void + testBatch() + { + testcase("Batch"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + // + // outer transaction + // + { + // test outer transaction with co-signing sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // does not affect reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Fee is paid by sponsor + BEAST_EXPECT(env.balance(alice) == XRP(1000)); + BEAST_EXPECT(env.balance(sponsor) == XRP(1000 - 1)); + } + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + // spfSponsorReserve on outer Batch is rejected + for (auto const flags : {spfSponsorReserve | spfSponsorFee, spfSponsorReserve}) + { + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(noop(alice), seq + 2), + sponsor::As(sponsor, flags), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + env.close(); + } + } + { + // test outer transaction with prefunded sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob); + env.fund(XRP(1001), sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Ter(tesSUCCESS)); + env.close(); + + // does not affect reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Fee is paid by sponsor object + BEAST_EXPECT(env.balance(alice) == XRP(1000)); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100 - 1)); + BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 100); + } + // + // Inner transaction + // + { + // test invalid Inner transaction with co-signing sponsor + Account const signerAccount("signer"); + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor, signerAccount); + env.close(); + + env(signers(sponsor, 1, {Signer(signerAccount, 1)})); + env.close(); + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + jt.jv.removeMember(sfTxnSignature.jsonName); + + auto const seq = env.seq(alice); + // should fail because Inner transaction cannot include SponsorSignature with + // TxnSignature + BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfTxnSignature.jsonName)); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNATURE)); + } + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Msig(sfSponsorSignature, sponsor, signerAccount)); + jt.jv.removeMember(sfTxnSignature.jsonName); + + auto const seq = env.seq(alice); + // should fail because Inner transaction cannot include SponsorSignature with + // Signers + BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfSigners.jsonName)); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNER)); + } + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + jt.jv.removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + auto const seq = env.seq(alice); + // should fail because BatchSigners does not have signer for SponsorSignature + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNER)); + } + } + + { + // test Inner transaction with prefunded sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob); + env.fund(XRP(1001), sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + auto jt = env.jtnofill( + check::create(alice, bob, XRP(1)), sponsor::As(sponsor, spfSponsorReserve)); + // remove txn signature since it is filled by env.jtnofill() + jt.jv.removeMember(jss::TxnSignature); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(jt.jv, seq + 2), + Ter(tesSUCCESS)); + env.close(); + + // affect sponsor reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Fee is paid by outer transaction originator (alice) + BEAST_EXPECT(env.balance(alice) == XRP(999)); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + // reserve count is decreased + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 99); + } + + { + // test Inner transaction with co-signing sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto jt = env.jtnofill( + check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + // remove txn signature since it is filled by env.jtnofill() + jt.jv.removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(jt.jv, seq + 2), + batch::Sig(sponsor), + Ter(tesSUCCESS)); + env.close(); + + // affect sponsor reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Fee is paid by outer transaction originator (alice) + BEAST_EXPECT(env.balance(alice) == XRP(999)); + BEAST_EXPECT(env.balance(sponsor) == XRP(1000)); + } + + // Inner tx with sfSponsor + spfSponsorFee (pre-funded) is rejected + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + // Create pre-funded sponsorship + env(sponsor::set(sponsor, 0, 0, XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1))); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + // Create inner transaction with sponsor fee + auto innerPay = pay(alice, bob, XRP(1)); + innerPay[sfSponsor.jsonName] = sponsor.human(); + innerPay[sfSponsorFlags.jsonName] = static_cast(spfSponsorFee); + + // Should be rejected with temBAD_FEE + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(innerPay, seq + 1), + batch::Inner(noop(alice), seq + 2), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Inner tx with sfSponsor + spfSponsorFee (co-signed) is rejected + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + auto innerPay = pay(alice, bob, XRP(1)); + innerPay[sfSponsor.jsonName] = sponsor.human(); + innerPay[sfSponsorFlags.jsonName] = static_cast(spfSponsorFee); + + // Should be rejected with temBAD_FEE + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(innerPay, seq + 1), + batch::Inner(noop(alice), seq + 2), + batch::Sig(sponsor), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Inner tx with spfSponsorFee + spfSponsorReserve is rejected + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + auto innerTx = check::create(alice, bob, XRP(1)); + innerTx[sfSponsor.jsonName] = sponsor.human(); + innerTx[sfSponsorFlags.jsonName] = + static_cast(spfSponsorFee | spfSponsorReserve); + + // Should be rejected with temBAD_FEE (fee sponsorship check comes first) + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(innerTx, seq + 1), + batch::Inner(noop(alice), seq + 2), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Outer batch tx with sponsor fee is allowed + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + // Outer batch with sponsor fee should work fine + env(batch::outer(bob, seq, batchFee, tfAllOrNothing), + batch::Inner(noop(bob), seq + 1), + batch::Inner(noop(bob), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // Sponsor paid the fee + BEAST_EXPECT(env.balance(bob) == XRP(1000)); + } + } + + // Verify that the central allow-list in preflight1Sponsor rejects + // spfSponsorReserve for transaction types that v1 does not permit. + void + testReserveSponsorGate() + { + testcase("Reserve sponsor allow-list gate"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 10, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + auto checkBlocked = [&](json::Value const& jv) { + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + }; + + checkBlocked(ticket::create(alice, 1)); + checkBlocked(offer(alice, XRP(100), bob["USD"](100))); + checkBlocked(did::setValid(alice)); + checkBlocked(token::mint(alice, 0u)); + checkBlocked(sponsor::set(alice, 0, 10, XRP(10))); + checkBlocked(acctdelete(alice, bob)); + checkBlocked(loan::set(alice, uint256(1), Number{1})); + } + + void + testSponsorReserve(bool cosigning) + { + testRequireFlag(); + testSponsorReserveSimple(cosigning); + testCheck(cosigning); + testCredential(cosigning); + testDelegate(cosigning); + testDepositPreauth(cosigning); + testEscrow(cosigning); + testMPToken(cosigning); + testPayChan(cosigning); + testSignerList(cosigning); + testTrustSet(cosigning); + } + + void + testZeroBalanceSponsoredPaymentFeePayerCheck() + { + // Zero-balance sponsored Payment: getFeePayer() consistency check + testcase("Sponsored Payment: minimal-balance account with sponsor-pays-fee"); + + using namespace jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const dest("dest"); + + auto const baseFee = env.current()->fees().base; + auto const baseReserve = env.current()->fees().reserve; + + // Fund sponsor and dest generously, alice with base reserve + 1 XRP for payment + env.fund(XRP(10000), sponsor, dest); + env.fund(baseReserve + XRP(1), alice); + env.close(); + + // Precondition: alice has base reserve + 1 XRP (enough for payment but not fee) + BEAST_EXPECT(env.balance(alice) == baseReserve + XRP(1)); + + // Alice tries to send a Payment to dest where sponsor pays the fee via spfSponsorFee. + // Passed even alice balance doesn't have enough to pay fee. + auto const preDest = env.balance(dest); + auto const preSponsor = env.balance(sponsor); + + // Alice sends 1 XRP to dest, sponsor pays the fee + env(pay(alice, dest, XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(baseFee)); + env.close(); + + // Payment succeeded + // Alice's balance decreased by 1 XRP (the payment amount, NOT the fee) + BEAST_EXPECT(env.balance(alice) == baseReserve); + + // Dest received 1 XRP + BEAST_EXPECT(env.balance(dest) == preDest + XRP(1)); + + // Sponsor paid the fee (NOT alice) + BEAST_EXPECT(env.balance(sponsor) == preSponsor - baseFee); + } + + void + testTrustSetCounterpartySponsorMisroute() + { + // TrustSet's modify path applies the tx-level reserve sponsor to whichever + // side has its reserve gate trip on this update, regardless of whether that + // side belongs to the tx submitter. trustCreate only sets the submitter's + // reserve flag and snapshots the counterparty's asfDefaultRipple state into + // the line's NoRipple bit; if the counterparty later toggles asfDefaultRipple + // (the canonical issuer flow), the line and account flags disagree and on + // the submitter's next TrustSet the counterparty-side gate fires. Sponsor still will be + // checked if it can be applied to that end of the trustLine. + + testcase("TrustSet modify with sponsor does not misroute onto counterparty side"); + + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice{"alice_t2178"}; + Account const bob{"bob_t2178"}; + Account const carol{"carol_t2178"}; + + // Fund without auto-setting asfDefaultRipple + env.fund(XRP(100'000), alice, bob, carol); + env.close(); + + // Determine account ordering + bool const aliceIsHigh = alice.id() > bob.id(); + + // To trigger the bug, we need the COUNTERPARTY's reserve gate to trip + // We use issuer/holder terminology where: + // - holder creates the trust line (their reserve is set first) + // - issuer enables DefaultRipple after (creates flag mismatch) + // - holder's second TrustSet triggers issuer's reserve gate + + auto const issuer = aliceIsHigh ? bob : alice; + auto const holder = aliceIsHigh ? alice : bob; + auto const usd = issuer["USD"]; + + // Issuer must NOT have DefaultRipple set initially + // Clear it explicitly (env.fund may have set it) + env(fclear(issuer, asfDefaultRipple)); + env.close(); + + // Holder creates the trust line first (holder's reserve flag is set) + // At this point, issuer does NOT have DefaultRipple set, so + // the NoRipple bit on issuer's side is set according to issuer's current flag + env(trust(holder, usd(1'000))); + env.close(); + + // Issuer now enables asfDefaultRipple (canonical issuer flow) + // This creates a mismatch: issuer's account flag says DefaultRipple=true + // but the trust line's NoRipple bit on issuer's side is still set + env(fset(issuer, asfDefaultRipple)); + env.close(); + + SF_ACCOUNT const& issuerSponsorField = aliceIsHigh ? sfLowSponsor : sfHighSponsor; + SF_ACCOUNT const& holderSponsorField = aliceIsHigh ? sfHighSponsor : sfLowSponsor; + + auto const lineKey = keylet::trustLine(alice, bob, usd.currency); + auto const sleLineBefore = env.le(lineKey); + if (!BEAST_EXPECT(sleLineBefore)) + return; + BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfLowSponsor)); + BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfHighSponsor)); + + auto const carolBefore = sponsoringOwnerCount(env, carol); + BEAST_EXPECT(carolBefore == 0); + auto const issuerSponsoredBefore = sponsoredOwnerCount(env, issuer); + BEAST_EXPECT(issuerSponsoredBefore == 0); + + // Holder modifies the trust line with Carol as sponsor + // This should trigger the issuer's reserve gate because of the DefaultRipple mismatch + // Carol (sponsor) should NOT be applied to issuer's side (issuer != tx submitter) + env(trust(holder, usd(2'000)), + sponsor::As(carol, spfSponsorReserve), + Sig(sfSponsorSignature, carol), + Ter(tesSUCCESS)); + env.close(); + + auto const sleLineAfter = env.le(lineKey); + if (!BEAST_EXPECT(sleLineAfter)) + return; + + // Carol only agreed to back the holder, not the issuer + BEAST_EXPECT(!sleLineAfter->isFieldPresent(issuerSponsorField)); + + // Holder's side also has no sponsor because holder's reserve flag was + // already set on the FIRST TrustSet (no sponsor in scope then) + BEAST_EXPECT(!sleLineAfter->isFieldPresent(holderSponsorField)); + + // Carol's sponsoring count should remain unchanged (no misroute) + auto const carolAfter = sponsoringOwnerCount(env, carol); + BEAST_EXPECT(carolAfter == carolBefore); + + // Issuer's sponsored count should remain unchanged (no misroute) + auto const issuerSponsoredAfter = sponsoredOwnerCount(env, issuer); + BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore); + } + + void + testSelfEscrowFinishReserveGate() + { + testcase("Self-escrow finish reserve order gated by amendment"); + using namespace test::jtx; + using namespace std::chrono_literals; + + // Finishing a self-escrow (source == destination) whose trust line + // was deleted while the escrow was outstanding auto-creates the line, + // and the outcome of that reserve check depends on whether the escrow + // reserve is released before delivery (Sponsor) or after (legacy). + // With the source's balance in the one-increment window + // [reserve(1), reserve(2)), the legacy order requires reserve(2) and + // fails, while the Sponsor order requires reserve(1) and succeeds. + auto runTest = [&](FeatureBitset features, TER expected) { + Account const alice("alice"); + Account const gw("gw"); + auto const usd = gw["usd"]; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(10000), alice, gw); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000), alice); + env.close(); + env(pay(gw, alice, usd(100))); + env.close(); + + // Escrow alice's entire USD balance to herself. The escrowed + // IOUs return to the issuer, zeroing the line balance. + auto const seq = env.seq(alice); + env(escrow::create(alice, alice, usd(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + env.close(); + + // Delete the now-empty trust line. Both accounts have + // DefaultRipple set (jtx fund does that), so a plain limit-0 + // TrustSet returns the line to its default state. + env(trust(alice, usd(0))); + env.close(); + BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(ownerCount(env, alice) == 1); // just the escrow + + // Put alice's balance in the window. Pay the excess away + // directly: adjustAccountXRPBalance needs the Sponsor amendment. + STAmount const target = reserve(env, 1) + XRP(1); + env(pay(alice, env.master, env.balance(alice) - target - baseFee), Fee(baseFee)); + env.close(); + BEAST_EXPECT(env.balance(alice) == target); + + env(escrow::finish(alice, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150), + Ter(expected)); + env.close(); + + if (expected == tesSUCCESS) + { + BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(env.balance(alice, usd) == usd(100)); + BEAST_EXPECT(ownerCount(env, alice) == 1); // the new line + } + else + { + BEAST_EXPECT(env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency))); + BEAST_EXPECT(ownerCount(env, alice) == 1); // still the escrow + } + }; + + // Pre-amendment: legacy order — the escrow still counts against the + // reserve while the auto-created line is checked. + runTest(testableAmendments() - featureSponsor, tecNO_LINE_INSUF_RESERVE); + + // Post-amendment: the escrow reserve is recycled into the new line. + runTest(testableAmendments(), tesSUCCESS); + } + + void + testFeeSponsoredVaultInvariant() + { + // The ValidVault invariant checks that the vault's balance and the + // depositor's balance change by equal amounts. For XRP vaults it adds the + // fee back into the depositor's balance change: normally the depositor + // pays the fee, so their balance drops by (deposit amount + fee), and + // adding the fee back leaves just the deposit amount to compare against + // the vault. But when a fee sponsor pays, the depositor's balance drops + // by only the deposit amount, so the fee must NOT be added back or the + // equal-amount check fails. + testcase("Fee-sponsored VaultDeposit/VaultWithdraw pass ValidVault invariant"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset}); + env(vaultTx); + env.close(); + + // Control: the same deposit shape, unsponsored, succeeds. + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(100)}), + Ter(tesSUCCESS)); + env.close(); + + // Fee-sponsored (co-signed) deposit succeeds. + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(100)}), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // The same helper (deltaAssetsTxAccount) drives the withdraw path, so a + // fee-sponsored withdrawal back to the depositor's own account also + // passes on the destination side. + env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(50)}), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + + void + testSponsoredObjectDeletionRefund() + { + // Deleting a co-signed reserve-sponsored object must + // refund the sponsor's SponsoringOwnerCount back to zero. + testcase("Sponsored object deletion refunds sponsor owner count"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const sponsor("sponsor"); + env.fund(XRP(100000), sponsor); + env.close(); + + // Sponsored Check deletion - verify decreaseOwnerCountForObject + // (used by CheckCancel) reads the object's sfSponsor field and refunds + // the sponsor's owner count. + { + testcase(" — Check deletion"); + + Account const checkOwner("check_owner"); + Account const dest("check_dest"); + env.fund(XRP(100000), checkOwner, dest); + env.close(); + + // Create a check with co-signed reserve sponsorship. This bumps the + // sponsor's sfSponsoringOwnerCount rather than the check owner's. + auto const checkSeq = env.seq(checkOwner); + env(check::create(checkOwner, dest, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto sponsorCountBefore = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountBefore == 1); // check costs 1 owner count + + // Cancel (delete) the check. + env(check::cancel(checkOwner, keylet::check(checkOwner, checkSeq).key)); + env.close(); + + auto sponsorCountAfter = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountAfter == 0); // fully refunded + } + + // Sponsored TrustSet (trust line) deletion - verify the sponsor + // refund works when a sponsored trust line is deleted + { + testcase(" — TrustLine deletion"); + + Account const issuer("issuer"); + Account const holder("holder"); + env.fund(XRP(100000), issuer, holder); + env.close(); + + auto const usd = issuer["USD"]; + + // Create trust line with sponsorship on the holder side + env(trust(holder, usd(1000)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto sponsorCountBefore = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountBefore == 1); // trust line costs 1 + + // Delete the trust line by clearing it to default + env(trust(holder, usd(0))); + env.close(); + + auto sponsorCountAfter = sponsoringOwnerCount(env, sponsor); + BEAST_EXPECT(sponsorCountAfter == 0); // fully refunded + } + } + +protected: + void + testSponsor() + { + testDisabled(); + testInvalidSponsorshipSet(); + testPseudoAccountSponsorship(); + + testSingleSigning(); + testMultiSigning(); + + testInvalidSponsorField(); + + testSimpleSponsorshipSet(); + + testPreFundAndCosign(); + testSponsoredFreeTierReserve(); + + testTransferSponsor(); + testLegacySignerListReserve(); + testSponsorFee(); + testSponsorAccount(); + + testAccountDelete(); + + testDelegatePermission(); + testDelegateBlockReserveSponsor(); + testDelegateSponsorFeePayer(); + + testBatch(); + + testSponsoredTrustLineNoFreeReserve(); + testCoSignReserveBoundedBySponsorshipBudget(); + testReserveSponsorGate(); + + testZeroBalanceSponsoredPaymentFeePayerCheck(); + testTrustSetCounterpartySponsorMisroute(); + testSelfEscrowFinishReserveGate(); + + testFeeSponsoredVaultInvariant(); + testSponsoredObjectDeletionRefund(); + } + + void + testTxSponsor(bool cosigning) + { + testSponsorReserve(cosigning); + } + +public: + void + run() override + { + testSponsor(); + } +}; + +class SponsorTxCosigning_test : public Sponsor_test +{ + void + run() override + { + testTxSponsor(true); + } +}; + +class SponsorTxPrefunded_test : public Sponsor_test +{ + void + run() override + { + testTxSponsor(false); + } +}; + +BEAST_DEFINE_TESTSUITE(Sponsor, app, xrpl); +BEAST_DEFINE_TESTSUITE(SponsorTxCosigning, app, xrpl); +BEAST_DEFINE_TESTSUITE(SponsorTxPrefunded, app, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/Ticket_test.cpp b/src/test/app/Ticket_test.cpp index 626cdbe44d..f14afd5990 100644 --- a/src/test/app/Ticket_test.cpp +++ b/src/test/app/Ticket_test.cpp @@ -47,9 +47,11 @@ namespace xrpl { class Ticket_test : public beast::unit_test::Suite { - /// @brief Validate metadata for a successful TicketCreate transaction. - /// - /// @param env current jtx env (tx and meta are extracted using it) + /** + * @brief Validate metadata for a successful TicketCreate transaction. + * + * @param env current jtx env (tx and meta are extracted using it) + */ void checkTicketCreateMeta(test::jtx::Env& env) { @@ -233,11 +235,13 @@ class Ticket_test : public beast::unit_test::Suite BEAST_EXPECT(*ticketSeqs.rbegin() == acctRootFinalSeq - 1); } - /// @brief Validate metadata for a ticket using transaction. - /// - /// The transaction may have been successful or failed with a tec. - /// - /// @param env current jtx env (tx and meta are extracted using it) + /** + * @brief Validate metadata for a ticket using transaction. + * + * The transaction may have been successful or failed with a tec. + * + * @param env current jtx env (tx and meta are extracted using it) + */ void checkTicketConsumeMeta(test::jtx::Env& env) { @@ -503,7 +507,7 @@ class Ticket_test : public beast::unit_test::Suite Account const alice{"alice"}; // Fund alice not quite enough to make the reserve for a Ticket. - env.fund(env.current()->fees().accountReserve(1) - drops(1), alice); + env.fund(env.current()->fees().accountReserve(1, 1) - drops(1), alice); env.close(); env(ticket::create(alice, 1), Ter(tecINSUFFICIENT_RESERVE)); @@ -511,7 +515,8 @@ class Ticket_test : public beast::unit_test::Suite env.require(Owners(alice, 0), tickets(alice, 0)); // Give alice enough to exactly meet the reserve for one Ticket. - env(pay(env.master, alice, env.current()->fees().accountReserve(1) - env.balance(alice))); + env(pay( + env.master, alice, env.current()->fees().accountReserve(1, 1) - env.balance(alice))); env.close(); env(ticket::create(alice, 1)); @@ -524,7 +529,7 @@ class Ticket_test : public beast::unit_test::Suite env( pay(env.master, alice, - env.current()->fees().accountReserve(250) - drops(1) - env.balance(alice))); + env.current()->fees().accountReserve(250, 1) - drops(1) - env.balance(alice))); env.close(); // alice doesn't quite have the reserve for a total of 250 @@ -535,7 +540,8 @@ class Ticket_test : public beast::unit_test::Suite // Give alice enough so she can make the reserve for all 250 // Tickets. - env(pay(env.master, alice, env.current()->fees().accountReserve(250) - env.balance(alice))); + env(pay( + env.master, alice, env.current()->fees().accountReserve(250, 1) - env.balance(alice))); env.close(); std::uint32_t const ticketSeq{env.seq(alice) + 1}; diff --git a/src/test/app/TrustSet_test.cpp b/src/test/app/TrustSet_test.cpp index 15abc3bd06..e24373fd21 100644 --- a/src/test/app/TrustSet_test.cpp +++ b/src/test/app/TrustSet_test.cpp @@ -191,7 +191,7 @@ public: auto const txFee = env.current()->fees().base; auto const baseReserve = env.current()->fees().reserve; - auto const threelineReserve = env.current()->fees().accountReserve(3); + auto const threelineReserve = env.current()->fees().accountReserve(3, 1); env.fund(XRP(10000), gwA, gwB, assistor); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 91eb26da7e..3175e742d9 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -2335,6 +2337,43 @@ public: BEAST_EXPECT(env.balance(alice) == drops(5)); } + void + testSponsorTxCannotQueue() + { + using namespace jtx; + testcase("disallow sponsored transaction from being queued"); + + Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}})); + + auto sponsor = Account("sponsor"); + auto sponsee = Account("sponsee"); + auto filler = Account("filler"); + + env.fund(XRP(50000), noripple(sponsor, sponsee)); + env.close(); + env.fund(XRP(50000), noripple(filler)); + env.close(); + + fillQueue(env, filler); + checkMetrics(*this, env, 0, 6, 4, 3); + + // Sponsored transactions are not allowed to be queued. + env(noop(sponsee), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(telCAN_NOT_QUEUE)); + checkMetrics(*this, env, 0, 6, 4, 3); + + // Sponsored transactions may still apply directly if they pay the + // open ledger fee. They just cannot be held in the queue. + env(noop(sponsee), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(openLedgerCost(env)), + Ter(tesSUCCESS)); + checkMetrics(*this, env, 0, 6, 5, 3); + } + void testDelegateTxCannotQueue() { @@ -4698,6 +4737,7 @@ public: testBlockersSeq(); testBlockersTicket(); testInFlightBalance(); + testSponsorTxCannotQueue(); testDelegateTxCannotQueue(); testConsequences(); } diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 8077c21863..60228f6723 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -2596,7 +2596,7 @@ private: } void - testQuorumDisabled() + testQuorumDisabled() // NOLINT(readability-function-size) { testcase("Test quorum disabled"); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 3877e08f7e..617820c89c 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,7 @@ #include #include +#include #include #include #include @@ -1804,7 +1806,7 @@ class Vault_test : public beast::unit_test::Suite auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -2686,7 +2688,7 @@ class Vault_test : public beast::unit_test::Suite auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -5245,11 +5247,15 @@ class Vault_test : public beast::unit_test::Suite auto const maxInt64 = std::to_string(std::numeric_limits::max()); BEAST_EXPECT(maxInt64 == "9223372036854775807"); - // Naming things is hard auto const maxInt64Plus1 = std::to_string( static_cast(std::numeric_limits::max()) + 1); BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808"); + // Naming things is hard + auto const maxInt64Plus2 = std::to_string( + static_cast(std::numeric_limits::max()) + 2); + BEAST_EXPECT(maxInt64Plus2 == "9223372036854775809"); + auto const initialXRP = to_string(kInitialXrp); BEAST_EXPECT(initialXRP == "100000000000000000"); @@ -5276,25 +5282,58 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; - env(tx, Ter(tefEXCEPTION)); - env.close(); + // There are several parse failures expected in this function, so just disable it once. + env.setParseFailureExpected(true); + try + { + tx[sfAssetsMaximum] = maxInt64Plus1; + env(tx, Ter(tefEXCEPTION)); + env.close(); + // should throw in parser + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); + } + + try + { + tx[sfAssetsMaximum] = maxInt64Plus2; + env(tx, Ter(tefEXCEPTION)); + // should throw in parser + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); + } - // This value will be rounded - auto const insertAt = maxInt64Plus1.size() - 3; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 1000 - BEAST_EXPECT(decimalTest == "9223372036854775.808"); - tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); - env(tx); - env.close(); + try + { + auto const insertAt = maxInt64Plus2.size() - 3; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+2) / 1000 + BEAST_EXPECT(decimalTest == "9223372036854775.809"); + tx[sfAssetsMaximum] = decimalTest; + env(tx); + // should throw in parser + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); + } auto const vaultSle = env.le(newKeylet); - if (!BEAST_EXPECT(vaultSle)) - return; - - BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 9223372036854776); + BEAST_EXPECT(!vaultSle); } { @@ -5328,25 +5367,41 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; - env(tx, Ter(tefEXCEPTION)); - env.close(); + try + { + tx[sfAssetsMaximum] = maxInt64Plus2; + env(tx, Ter(tefEXCEPTION)); + // should throw in parser + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); + } - // This value will be rounded - auto const insertAt = maxInt64Plus1.size() - 1; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 - BEAST_EXPECT(decimalTest == "922337203685477580.8"); - tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); - env(tx); - env.close(); + try + { + auto const insertAt = maxInt64Plus2.size() - 1; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+2) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.9"); + tx[sfAssetsMaximum] = decimalTest; + env(tx); + // should throw in parser + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); + } auto const vaultSle = env.le(newKeylet); - if (!BEAST_EXPECT(vaultSle)) - return; - - BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 922337203685477581); + BEAST_EXPECT(!vaultSle); } { @@ -5373,9 +5428,22 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; - env(tx); - env.close(); + // Since several tests are expected to have parser failures, leave this flag set for the + // remainder of this function. + env.setParseFailureExpected(true); + try + { + tx[sfAssetsMaximum] = maxInt64Plus2; + env(tx); + // should throw in parser + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); + } tx[sfAssetsMaximum] = "1000000000000000e80"; env.close(); @@ -5385,22 +5453,27 @@ class Vault_test : public beast::unit_test::Suite // These values will be rounded to 15 significant digits { - auto const insertAt = maxInt64Plus1.size() - 1; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 - BEAST_EXPECT(decimalTest == "922337203685477580.8"); - tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); - env(tx); - env.close(); + try + { + auto const insertAt = maxInt64Plus2.size() - 1; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+2) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.9"); + tx[sfAssetsMaximum] = decimalTest; + env(tx); + // should throw in parser + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + std::string(e.what()) == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); + } auto const vaultSle = env.le(newKeylet); - if (!BEAST_EXPECT(vaultSle)) - return; - - BEAST_EXPECT( - (vaultSle->at(sfAssetsMaximum) == - Number{9223372036854776, 2, Number::Normalized{}})); + BEAST_EXPECT(!vaultSle); } { tx[sfAssetsMaximum] = "9223372036854775807e40"; // max int64 * 10^40 @@ -5927,8 +6000,9 @@ class Vault_test : public beast::unit_test::Suite token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength)); sb.update(token); + auto const dummyTx = *env.jt(noop(holder)).stx; BEAST_EXPECT( - removeEmptyHolding(sb, holder.id(), MPTIssue(mpt.issuanceID()), j) == + removeEmptyHolding({sb, dummyTx}, holder.id(), MPTIssue(mpt.issuanceID()), j) == tecHAS_OBLIGATIONS); BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr); } diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index 437c329e02..d1f9cd2722 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -141,7 +141,7 @@ struct SEnv XRPAmount reserve(std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } XRPAmount @@ -245,9 +245,9 @@ struct Balance T& env; STAmount startAmount; - Balance(T& env, jtx::Account const& account) : account(account), env(env) + Balance(T& env, jtx::Account const& account) + : account(account), env(env), startAmount(env.balance(account)) { - startAmount = env.balance(account); } [[nodiscard]] STAmount @@ -370,7 +370,7 @@ struct XChain_test : public beast::unit_test::Suite, public jtx::XChainBridgeObj XRPAmount reserve(std::uint32_t count) { - return XEnv(*this).env.current()->fees().accountReserve(count); + return XEnv(*this).env.current()->fees().accountReserve(count, 1); } XRPAmount @@ -3923,12 +3923,10 @@ private: [[nodiscard]] bool verify() const { - for (auto const& [acct, state] : accounts) - { - if (!state.verify(env, acct)) - return false; - } - return true; + return std::ranges::all_of(accounts, [&](auto const& entry) { + auto const& [acct, state] = entry; + return state.verify(env, acct); + }); } struct BridgeCounters diff --git a/src/test/basics/Buffer_test.cpp b/src/test/basics/Buffer_test.cpp deleted file mode 100644 index c748a3f9dd..0000000000 --- a/src/test/basics/Buffer_test.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace xrpl::test { - -struct Buffer_test : beast::unit_test::Suite -{ - static bool - sane(Buffer const& b) - { - if (b.empty()) - return b.data() == nullptr; - - return b.data() != nullptr; - } - - void - run() override - { - std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, - 0x71, 0x6d, 0x2a, 0x18, 0xb4, 0x70, 0xcb, 0xf5, - 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, 0xf0, 0x2c, - 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; - - Buffer const b0; - BEAST_EXPECT(sane(b0)); - BEAST_EXPECT(b0.empty()); - - Buffer b1{0}; - BEAST_EXPECT(sane(b1)); - BEAST_EXPECT(b1.empty()); - std::memcpy(b1.alloc(16), data, 16); - BEAST_EXPECT(sane(b1)); - BEAST_EXPECT(!b1.empty()); - BEAST_EXPECT(b1.size() == 16); - - Buffer b2{b1.size()}; - BEAST_EXPECT(sane(b2)); - BEAST_EXPECT(!b2.empty()); - BEAST_EXPECT(b2.size() == b1.size()); - std::memcpy(b2.data(), data + 16, 16); - - Buffer b3{data, sizeof(data)}; - BEAST_EXPECT(sane(b3)); - BEAST_EXPECT(!b3.empty()); - BEAST_EXPECT(b3.size() == sizeof(data)); - BEAST_EXPECT(std::memcmp(b3.data(), data, b3.size()) == 0); - - // Check equality and inequality comparisons - BEAST_EXPECT(b0 == b0); - BEAST_EXPECT(b0 != b1); - BEAST_EXPECT(b1 == b1); - BEAST_EXPECT(b1 != b2); - BEAST_EXPECT(b2 != b3); - - // Check copy constructors and copy assignments: - { - testcase("Copy Construction / Assignment"); - - Buffer x{b0}; - BEAST_EXPECT(x == b0); - BEAST_EXPECT(sane(x)); - Buffer y{b1}; - BEAST_EXPECT(y == b1); - BEAST_EXPECT(sane(y)); - x = b2; - BEAST_EXPECT(x == b2); - BEAST_EXPECT(sane(x)); - x = y; - BEAST_EXPECT(x == y); - BEAST_EXPECT(sane(x)); - y = b3; - BEAST_EXPECT(y == b3); - BEAST_EXPECT(sane(y)); - x = b0; - BEAST_EXPECT(x == b0); - BEAST_EXPECT(sane(x)); -#if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wself-assign-overloaded" -#endif - - x = x; - BEAST_EXPECT(x == b0); - BEAST_EXPECT(sane(x)); - y = y; - BEAST_EXPECT(y == b3); - BEAST_EXPECT(sane(y)); - -#if defined(__clang__) -#pragma clang diagnostic pop -#endif - } - - // Check move constructor & move assignments: - { - testcase("Move Construction / Assignment"); - - static_assert(std::is_nothrow_move_constructible_v); - static_assert(std::is_nothrow_move_assignable_v); - - { // Move-construct from empty buf - Buffer x; - Buffer const y{std::move(x)}; - BEAST_EXPECT(sane(x)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(x.empty()); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y.empty()); - BEAST_EXPECT(x == y); // NOLINT(bugprone-use-after-move) - } - - { // Move-construct from non-empty buf - Buffer x{b1}; - Buffer const y{std::move(x)}; - BEAST_EXPECT(sane(x)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(x.empty()); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y == b1); - } - - { // Move assign empty buf to empty buf - Buffer x; - Buffer y; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to empty buf - Buffer x; - Buffer y{b1}; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x == b1); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign empty buf to non-empty buf - Buffer x{b1}; - Buffer y; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to non-empty buf - Buffer x{b1}; - Buffer y{b2}; - Buffer z{b3}; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(!x.empty()); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - - x = std::move(z); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(!x.empty()); - BEAST_EXPECT(sane(z)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(z.empty()); // NOLINT(bugprone-use-after-move) - } - } - - { - testcase("Slice Conversion / Construction / Assignment"); - - Buffer w{static_cast(b0)}; - BEAST_EXPECT(sane(w)); - BEAST_EXPECT(w == b0); - - Buffer x{static_cast(b1)}; - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x == b1); - - Buffer y{static_cast(b2)}; - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y == b2); - - Buffer z{static_cast(b3)}; - BEAST_EXPECT(sane(z)); - BEAST_EXPECT(z == b3); - - // Assign empty slice to empty buffer - w = static_cast(b0); - BEAST_EXPECT(sane(w)); - BEAST_EXPECT(w == b0); - - // Assign non-empty slice to empty buffer - w = static_cast(b1); - BEAST_EXPECT(sane(w)); - BEAST_EXPECT(w == b1); - - // Assign non-empty slice to non-empty buffer - x = static_cast(b2); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x == b2); - - // Assign non-empty slice to non-empty buffer - y = static_cast(z); - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y == z); - - // Assign empty slice to non-empty buffer: - z = static_cast(b0); - BEAST_EXPECT(sane(z)); - BEAST_EXPECT(z == b0); - } - - { - testcase("Allocation, Deallocation and Clearing"); - - auto test = [this](Buffer const& b, std::size_t i) { - Buffer x{b}; - - // Try to allocate some number of bytes, possibly - // zero (which means clear) and sanity check - x(i); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.size() == i); - BEAST_EXPECT((x.data() == nullptr) == (i == 0)); - - // Try to allocate some more data (always non-zero) - x(i + 1); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.size() == i + 1); - BEAST_EXPECT(x.data() != nullptr); - - // Try to clear: - x.clear(); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(x.data() == nullptr); - - // Try to clear again: - x.clear(); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(x.data() == nullptr); - }; - - for (std::size_t i = 0; i < 16; ++i) - { - test(b0, i); - test(b1, i); - } - } - } -}; - -BEAST_DEFINE_TESTSUITE(Buffer, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/test/basics/FileUtilities_test.cpp b/src/test/basics/FileUtilities_test.cpp deleted file mode 100644 index 0e050b5168..0000000000 --- a/src/test/basics/FileUtilities_test.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include - -#include -#include -#include - -#include -#include - -namespace xrpl { - -class FileUtilities_test : public beast::unit_test::Suite -{ -public: - void - testGetFileContents() - { - using namespace xrpl::detail; - using namespace boost::system; - - static constexpr char const* kExpectedContents = - "This file is very short. That's all we need."; - - FileDirGuard const file( - *this, "test_file", "test.txt", "This is temporary text that should get overwritten"); - - error_code ec; - auto const path = file.file(); - - writeFileContents(ec, path, kExpectedContents); - BEAST_EXPECT(!ec); - - { - // Test with no max - auto const good = getFileContents(ec, path); - BEAST_EXPECT(!ec); - BEAST_EXPECT(good == kExpectedContents); - } - - { - // Test with large max - auto const good = getFileContents(ec, path, kilobytes(1)); - BEAST_EXPECT(!ec); - BEAST_EXPECT(good == kExpectedContents); - } - - { - // Test with small max - auto const bad = getFileContents(ec, path, 16); - BEAST_EXPECT(ec && ec.value() == boost::system::errc::file_too_large); - BEAST_EXPECT(bad.empty()); - } - } - - void - run() override - { - testGetFileContents(); - } -}; - -BEAST_DEFINE_TESTSUITE(FileUtilities, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/IOUAmount_test.cpp b/src/test/basics/IOUAmount_test.cpp deleted file mode 100644 index b652d27625..0000000000 --- a/src/test/basics/IOUAmount_test.cpp +++ /dev/null @@ -1,276 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl { - -class IOUAmount_test : public beast::unit_test::Suite -{ -public: - void - testZero() - { - testcase("zero"); - - IOUAmount const z(0, 0); - - BEAST_EXPECT(z.mantissa() == 0); - BEAST_EXPECT(z.exponent() == -100); - BEAST_EXPECT(!z); - BEAST_EXPECT(z.signum() == 0); - BEAST_EXPECT(z == beast::kZero); - - BEAST_EXPECT((z + z) == z); - BEAST_EXPECT((z - z) == z); - BEAST_EXPECT(z == -z); - - IOUAmount const zz(beast::kZero); - BEAST_EXPECT(z == zz); - - // https://github.com/XRPLF/rippled/issues/5170 - IOUAmount const zzz{}; - BEAST_EXPECT(zzz == beast::kZero); - // BEAST_EXPECT(zzz == zz); - } - - void - testSigNum() - { - testcase("signum"); - - IOUAmount const neg(-1, 0); - BEAST_EXPECT(neg.signum() < 0); - - IOUAmount const zer(0, 0); - BEAST_EXPECT(zer.signum() == 0); - - IOUAmount const pos(1, 0); - BEAST_EXPECT(pos.signum() > 0); - } - - void - testBeastZero() - { - testcase("beast::Zero Comparisons"); - - using beast::kZero; - - { - IOUAmount const z(kZero); - BEAST_EXPECT(z == kZero); - BEAST_EXPECT(z >= kZero); - BEAST_EXPECT(z <= kZero); - unexpected(z != kZero); - unexpected(z > kZero); - unexpected(z < kZero); - } - - { - IOUAmount const neg(-2, 0); - BEAST_EXPECT(neg < kZero); - BEAST_EXPECT(neg <= kZero); - BEAST_EXPECT(neg != kZero); - unexpected(neg == kZero); - } - - { - IOUAmount const pos(2, 0); - BEAST_EXPECT(pos > kZero); - BEAST_EXPECT(pos >= kZero); - BEAST_EXPECT(pos != kZero); - unexpected(pos == kZero); - } - } - - void - testComparisons() - { - testcase("IOU Comparisons"); - - IOUAmount const n(-2, 0); - IOUAmount const z(0, 0); - IOUAmount const p(2, 0); - - BEAST_EXPECT(z == z); - BEAST_EXPECT(z >= z); - BEAST_EXPECT(z <= z); - BEAST_EXPECT(z == -z); - // NOLINTBEGIN(misc-redundant-expression) - unexpected(z > z); - unexpected(z < z); - unexpected(z != z); - // NOLINTEND(misc-redundant-expression) - unexpected(z != -z); - - BEAST_EXPECT(n < z); - BEAST_EXPECT(n <= z); - BEAST_EXPECT(n != z); - unexpected(n > z); - unexpected(n >= z); - unexpected(n == z); - - BEAST_EXPECT(p > z); - BEAST_EXPECT(p >= z); - BEAST_EXPECT(p != z); - unexpected(p < z); - unexpected(p <= z); - unexpected(p == z); - - BEAST_EXPECT(n < p); - BEAST_EXPECT(n <= p); - BEAST_EXPECT(n != p); - unexpected(n > p); - unexpected(n >= p); - unexpected(n == p); - - BEAST_EXPECT(p > n); - BEAST_EXPECT(p >= n); - BEAST_EXPECT(p != n); - unexpected(p < n); - unexpected(p <= n); - unexpected(p == n); - - BEAST_EXPECT(p > -p); - BEAST_EXPECT(p >= -p); - BEAST_EXPECT(p != -p); - - BEAST_EXPECT(n < -n); - BEAST_EXPECT(n <= -n); - BEAST_EXPECT(n != -n); - } - - void - testToString() - { - testcase("IOU strings"); - - auto test = [this](IOUAmount const& n, std::string const& expected) { - auto const result = to_string(n); - std::stringstream ss; - ss << "to_string(" << result << "). Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); - }; - - for (auto const mantissaSize : MantissaRange::getAllScales()) - { - NumberMantissaScaleGuard const mg(mantissaSize); - - test(IOUAmount(-2, 0), "-2"); - test(IOUAmount(0, 0), "0"); - test(IOUAmount(2, 0), "2"); - test(IOUAmount(25, -3), "0.025"); - test(IOUAmount(-25, -3), "-0.025"); - test(IOUAmount(25, 1), "250"); - test(IOUAmount(-25, 1), "-250"); - test(IOUAmount(2, 20), "2e20"); - test(IOUAmount(-2, -20), "-2e-20"); - } - } - - void - testMulRatio() - { - testcase("mulRatio"); - - /* The range for the mantissa when normalized */ - static constexpr std::int64_t kMinMantissa = 1000000000000000ull; - static constexpr std::int64_t kMaxMantissa = 9999999999999999ull; - // log(2,maxMantissa) ~ 53.15 - /* The range for the exponent when normalized */ - static constexpr int kMinExponent = -96; - static constexpr int kMaxExponent = 80; - constexpr auto kMaxUInt = std::numeric_limits::max(); - - { - // multiply by a number that would overflow the mantissa, then - // divide by the same number, and check we didn't lose any value - IOUAmount const bigMan(kMaxMantissa, 0); - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); - } - { - // Similar test as above, but for negative values - IOUAmount const bigMan(-kMaxMantissa, 0); - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); - } - - { - // small amounts - IOUAmount const tiny(kMinMantissa, kMinExponent); - // Round up should give the smallest allowable number - BEAST_EXPECT(tiny == mulRatio(tiny, 1, kMaxUInt, true)); - BEAST_EXPECT(tiny == mulRatio(tiny, kMaxUInt - 1, kMaxUInt, true)); - // rounding down should be zero - BEAST_EXPECT(beast::kZero == mulRatio(tiny, 1, kMaxUInt, false)); - BEAST_EXPECT(beast::kZero == mulRatio(tiny, kMaxUInt - 1, kMaxUInt, false)); - - // tiny negative numbers - IOUAmount const tinyNeg(-kMinMantissa, kMinExponent); - // Round up should give zero - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, 1, kMaxUInt, true)); - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, true)); - // rounding down should be tiny - BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, 1, kMaxUInt, false)); - BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, false)); - } - - { // rounding - { - IOUAmount const one(1, 0); - auto const rup = mulRatio(one, kMaxUInt - 1, kMaxUInt, true); - auto const rdown = mulRatio(one, kMaxUInt - 1, kMaxUInt, false); - BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1); - } - { - IOUAmount const big(kMaxMantissa, kMaxExponent); - auto const rup = mulRatio(big, kMaxUInt - 1, kMaxUInt, true); - auto const rdown = mulRatio(big, kMaxUInt - 1, kMaxUInt, false); - BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1); - } - - { - IOUAmount const negOne(-1, 0); - auto const rup = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, true); - auto const rdown = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, false); - BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1); - } - } - - { - // division by zero - IOUAmount one(1, 0); - except([&] { mulRatio(one, 1, 0, true); }); - } - - { - // overflow - IOUAmount big(kMaxMantissa, kMaxExponent); - except([&] { mulRatio(big, 2, 0, true); }); - } - } // namespace xrpl - - //-------------------------------------------------------------------------- - - void - run() override - { - testZero(); - testSigNum(); - testBeastZero(); - testComparisons(); - testToString(); - testMulRatio(); - } -}; - -BEAST_DEFINE_TESTSUITE(IOUAmount, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/IntrusiveShared_test.cpp b/src/test/basics/IntrusiveShared_test.cpp deleted file mode 100644 index 185b877f23..0000000000 --- a/src/test/basics/IntrusiveShared_test.cpp +++ /dev/null @@ -1,879 +0,0 @@ - -#include // IWYU pragma: keep -#include // IWYU pragma: keep -#include -#include - -#include -#include -#include -#include -#include // IWYU pragma: keep -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::tests { - -/** -Experimentally, we discovered that using std::barrier performs extremely -poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS -environments. To unblock our macOS CI pipeline, we replaced std::barrier with a -custom mutex-based barrier (Barrier) that significantly improves performance -without compromising correctness. For future reference, if we ever consider -reintroducing std::barrier, the following configuration is known to exhibit the -problem: - - Model Name: Mac mini - Model Identifier: Mac14,3 - Model Number: Z16K000R4LL/A - Chip: Apple M2 - Total Number of Cores: 8 (4 performance and 4 efficiency) - Memory: 24 GB - System Firmware Version: 11881.41.5 - OS Loader Version: 11881.1.1 - Apple clang version 16.0.0 (clang-1600.0.26.3) - Target: arm64-apple-darwin24.0.0 - Thread model: posix - - */ -struct Barrier -{ - std::mutex mtx; - std::condition_variable cv; - int count; - int const initial; - - Barrier(int n) : count(n), initial(n) - { - } - - void - arriveAndWait() - { - std::unique_lock lock(mtx); - if (--count == 0) - { - count = initial; - cv.notify_all(); - } - else - { - cv.wait(lock, [&] { return count == initial; }); - } - } -}; - -namespace { -enum class TrackedState : std::uint8_t { - Uninitialized, - Alive, - PartiallyDeletedStarted, - PartiallyDeleted, - DeletedStarted, - Deleted -}; - -class TIBase : public IntrusiveRefCounts -{ -public: - static constexpr std::size_t kMaxStates = 128; - static std::array, kMaxStates> state; - static std::atomic nextId; - static TrackedState - getState(int id) - { - assert(id < state.size()); - return state[id].load(std::memory_order_acquire); - } - static void - resetStates(bool resetCallback) - { - for (int i = 0; i < kMaxStates; ++i) - { - state[i].store(TrackedState::Uninitialized, std::memory_order_release); - } - nextId.store(0, std::memory_order_release); - if (resetCallback) - TIBase::tracingCallback = [](TrackedState, std::optional) {}; - } - - struct ResetStatesGuard - { - bool resetCallback{false}; - - ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback} - { - TIBase::resetStates(resetCallback); - } - ~ResetStatesGuard() - { - TIBase::resetStates(resetCallback); - } - }; - - TIBase() : id{checkoutID()} - { - assert(state.size() > id); - state[id].store(TrackedState::Alive, std::memory_order_relaxed); - } - ~TIBase() override - { - using enum TrackedState; - - assert(state.size() > id); - tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted); - - assert(state.size() > id); - // Use relaxed memory order to try to avoid atomic operations from - // adding additional memory synchronizations that may hide threading - // errors in the underlying shared pointer class. - state[id].store(DeletedStarted, std::memory_order_relaxed); - - tracingCallback(DeletedStarted, Deleted); - - assert(state.size() > id); - state[id].store(TrackedState::Deleted, std::memory_order_relaxed); - - tracingCallback(TrackedState::Deleted, std::nullopt); - } - - void - partialDestructor() const - { - using enum TrackedState; - - assert(state.size() > id); - tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted); - - assert(state.size() > id); - state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed); - - tracingCallback(PartiallyDeletedStarted, PartiallyDeleted); - - assert(state.size() > id); - state[id].store(PartiallyDeleted, std::memory_order_relaxed); - - tracingCallback(PartiallyDeleted, std::nullopt); - } - - static std::function)> tracingCallback; - - int id; - -private: - static int - checkoutID() - { - return nextId.fetch_add(1, std::memory_order_acq_rel); - } -}; - -std::array, TIBase::kMaxStates> TIBase::state; -std::atomic TIBase::nextId{0}; - -std::function)> TIBase::tracingCallback = - [](TrackedState, std::optional) {}; - -} // namespace - -class IntrusiveShared_test : public beast::unit_test::Suite -{ -public: - void - testBasics() - { - testcase("Basics"); - - { - TIBase::ResetStatesGuard const rsg{true}; - - TIBase const b; - BEAST_EXPECT(b.useCount() == 1); - b.addWeakRef(); - BEAST_EXPECT(b.useCount() == 1); - auto s = b.releaseStrongRef(); - BEAST_EXPECT(s == ReleaseStrongRefAction::PartialDestroy); - BEAST_EXPECT(b.useCount() == 0); - TIBase const* pb = &b; - partialDestructorFinished(&pb); - BEAST_EXPECT(!pb); - auto w = b.releaseWeakRef(); - BEAST_EXPECT(w == ReleaseWeakRefAction::Destroy); - } - - std::vector> strong; - std::vector> weak; - { - TIBase::ResetStatesGuard const rsg{true}; - - using enum TrackedState; - auto b = makeSharedIntrusive(); - auto id = b->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(b->useCount() == 1); - for (int i = 0; i < 10; ++i) - { - strong.push_back(b); - } - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == Alive); - strong.resize(strong.size() - 1); - BEAST_EXPECT(TIBase::getState(id) == Alive); - strong.clear(); - BEAST_EXPECT(TIBase::getState(id) == Deleted); - - b = makeSharedIntrusive(); - id = b->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(b->useCount() == 1); - for (int i = 0; i < 10; ++i) - { - weak.emplace_back(b); - BEAST_EXPECT(b->useCount() == 1); - } - BEAST_EXPECT(TIBase::getState(id) == Alive); - weak.resize(weak.size() - 1); - BEAST_EXPECT(TIBase::getState(id) == Alive); - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - while (!weak.empty()) - { - weak.resize(weak.size() - 1); - if (!weak.empty()) - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - } - BEAST_EXPECT(TIBase::getState(id) == Deleted); - } - { - TIBase::ResetStatesGuard const rsg{true}; - - using enum TrackedState; - auto b = makeSharedIntrusive(); - auto id = b->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - WeakIntrusive w{b}; - BEAST_EXPECT(TIBase::getState(id) == Alive); - auto s = w.lock(); - BEAST_EXPECT(s && s->useCount() == 2); - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(s && s->useCount() == 1); - s.reset(); - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - BEAST_EXPECT(w.expired()); - s = w.lock(); - // Cannot convert a weak pointer to a strong pointer if object is - // already partially deleted - BEAST_EXPECT(!s); - w.reset(); - BEAST_EXPECT(TIBase::getState(id) == Deleted); - } - { - TIBase::ResetStatesGuard const rsg{true}; - - using enum TrackedState; - using swu = SharedWeakUnion; - swu b = makeSharedIntrusive(); - BEAST_EXPECT(b.isStrong() && b.useCount() == 1); - auto id = b.get()->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - swu w = b; - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(w.isStrong() && b.useCount() == 2); - w.convertToWeak(); - BEAST_EXPECT(w.isWeak() && b.useCount() == 1); - swu s = w; - BEAST_EXPECT(s.isWeak() && b.useCount() == 1); - s.convertToStrong(); - BEAST_EXPECT(s.isStrong() && b.useCount() == 2); - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(s.useCount() == 1); - BEAST_EXPECT(!w.expired()); - s.reset(); - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - BEAST_EXPECT(w.expired()); - w.convertToStrong(); - // Cannot convert a weak pointer to a strong pointer if object is - // already partially deleted - BEAST_EXPECT(w.isWeak()); - w.reset(); - BEAST_EXPECT(TIBase::getState(id) == Deleted); - } - { - // Testing SharedWeakUnion assignment operator - - TIBase::ResetStatesGuard const rsg{true}; - - auto strong1 = makeSharedIntrusive(); - auto strong2 = makeSharedIntrusive(); - - auto id1 = strong1->id; - auto id2 = strong2->id; - - BEAST_EXPECT(id1 != id2); - - SharedWeakUnion union1 = strong1; - SharedWeakUnion union2 = strong2; - - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(union2.isStrong()); - BEAST_EXPECT(union1.get() == strong1.get()); - BEAST_EXPECT(union2.get() == strong2.get()); - - // 1) Normal assignment: explicitly calls SharedWeakUnion assignment - union1 = union2; - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(union2.isStrong()); - BEAST_EXPECT(union1.get() == union2.get()); - BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive); - BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Alive); - - // 2) Test self-assignment - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive); - int const initialRefCount = strong1->useCount(); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wself-assign-overloaded" - union1 = union1; // Self-assignment -#pragma clang diagnostic pop - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive); - BEAST_EXPECT(strong1->useCount() == initialRefCount); - - // 3) Test assignment from null union pointer - union1 = SharedWeakUnion(); - BEAST_EXPECT(union1.get() == nullptr); - - // 4) Test assignment to expired union pointer - strong2.reset(); - union2.reset(); - union1 = union2; - BEAST_EXPECT(union1.get() == nullptr); - BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Deleted); - } - } - - void - testPartialDelete() - { - testcase("Partial Delete"); - - // This test creates two threads. One with a strong pointer and one - // with a weak pointer. The strong pointer is reset while the weak - // pointer still holds a reference, triggering a partial delete. - // While the partial delete function runs (a sleep is inserted) the - // weak pointer is reset. The destructor should wait to run until - // after the partial delete function has completed running. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - auto strong = makeSharedIntrusive(); - WeakIntrusive weak{strong}; - bool destructorRan = false; - bool partialDeleteRan = false; - std::latch partialDeleteStartedSyncPoint{2}; - strong->tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - if (next == DeletedStarted) - { - // strong goes out of scope while weak is still in scope - // This checks that partialDelete has run to completion - // before the destructor is called. A sleep is inserted - // inside the partial delete to make sure the destructor is - // given an opportunity to run during partial delete. - BEAST_EXPECT(cur == PartiallyDeleted); - } - if (next == PartiallyDeletedStarted) - { - partialDeleteStartedSyncPoint.arrive_and_wait(); - using namespace std::chrono_literals; - // Sleep and let the weak pointer go out of scope, - // potentially triggering a destructor while partial delete - // is running. The test is to make sure that doesn't happen. - std::this_thread::sleep_for(800ms); - } - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - partialDeleteRan = true; - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - destructorRan = true; - } - }; - std::thread t1{[&] { - partialDeleteStartedSyncPoint.arrive_and_wait(); - weak.reset(); // Trigger a full delete as soon as the partial - // delete starts - }}; - std::thread t2{[&] { - strong.reset(); // Trigger a partial delete - }}; - t1.join(); - t2.join(); - - BEAST_EXPECT(destructorRan && partialDeleteRan); - } - - void - testDestructor() - { - testcase("Destructor"); - - // This test creates two threads. One with a strong pointer and one - // with a weak pointer. The weak pointer is reset while the strong - // pointer still holds a reference. Then the strong pointer is - // reset. Only the destructor should run. The partial destructor - // should not be called. Since the weak reset runs to completion - // before the strong pointer is reset, threading doesn't add much to - // this test, but there is no harm in keeping it. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - auto strong = makeSharedIntrusive(); - WeakIntrusive weak{strong}; - bool destructorRan = false; - bool partialDeleteRan = false; - std::latch weakResetSyncPoint{2}; - strong->tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - partialDeleteRan = true; - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - destructorRan = true; - } - }; - std::thread t1{[&] { - weak.reset(); - weakResetSyncPoint.arrive_and_wait(); - }}; - std::thread t2{[&] { - weakResetSyncPoint.arrive_and_wait(); - strong.reset(); // Trigger a partial delete - }}; - t1.join(); - t2.join(); - - BEAST_EXPECT(destructorRan && !partialDeleteRan); - } - - void - testMultithreadedClearMixedVariant() - { - testcase("Multithreaded Clear Mixed Variant"); - - // This test creates and destroys many strong and weak pointers in a - // loop. There is a random mix of strong and weak pointers stored in - // a vector (held as a variant). Both threads clear all the pointers - // and check that the invariants hold. - - using enum TrackedState; - TIBase::ResetStatesGuard const rsg{true}; - - std::atomic destructionState{0}; - // returns destructorRan and partialDestructorRan (in that order) - auto getDestructorState = [&]() -> std::pair { - int const s = destructionState.load(std::memory_order_relaxed); - return {(s & 1) != 0, (s & 2) != 0}; - }; - auto setDestructorRan = [&]() -> void { - destructionState.fetch_or(1, std::memory_order_acq_rel); - }; - auto setPartialDeleteRan = [&]() -> void { - destructionState.fetch_or(2, std::memory_order_acq_rel); - }; - auto tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - setDestructorRan(); - } - }; - auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) - -> std::vector, WeakIntrusive>> { - std::vector, WeakIntrusive>> result; - std::uniform_int_distribution<> toCreateDist(4, 64); - std::uniform_int_distribution<> isStrongDist(0, 1); - auto numToCreate = toCreateDist(eng); - result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) - { - if (isStrongDist(eng)) - { - result.emplace_back(SharedIntrusive(toClone)); - } - else - { - result.emplace_back(WeakIntrusive(toClone)); - } - } - return result; - }; - static constexpr int kLoopIters = 2 * 1024; - static constexpr int kNumThreads = 16; - std::vector> toClone; - Barrier loopStartSyncPoint{kNumThreads}; - Barrier postCreateToCloneSyncPoint{kNumThreads}; - Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; - auto engines = [&]() -> std::vector { - std::random_device rd; - std::vector result; - result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - result.emplace_back(rd()); - return result; - }(); - - // cloneAndDestroy clones the strong pointer into a vector of mixed - // strong and weak pointers and destroys them all at once. - // threadId==0 is special. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) - { - // ------ Sync Point ------ - loopStartSyncPoint.arriveAndWait(); - - // only thread 0 should reset the state - std::optional rsg; - if (threadId == 0) - { - // Thread 0 is the genesis thread. It creates the strong - // pointers to be cloned by the other threads. This - // thread will also check that the destructor ran and - // clear the temporary variables. - - rsg.emplace(false); - auto [destructorRan, partialDeleteRan] = getDestructorState(); - BEAST_EXPECT(!i || destructorRan); - destructionState.store(0, std::memory_order_release); - - toClone.clear(); - toClone.resize(kNumThreads); - auto strong = makeSharedIntrusive(); - strong->tracingCallback = tracingCallback; - std::ranges::fill(toClone, strong); - } - - // ------ Sync Point ------ - postCreateToCloneSyncPoint.arriveAndWait(); - - auto v = createVecOfPointers(toClone[threadId], engines[threadId]); - toClone[threadId].reset(); - - // ------ Sync Point ------ - postCreateVecOfPointersSyncPoint.arriveAndWait(); - - v.clear(); - } - }; - std::vector threads; - threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - { - threads.emplace_back(cloneAndDestroy, i); - } - for (int i = 0; i < kNumThreads; ++i) - { - threads[i].join(); - } - } - - void - testMultithreadedClearMixedUnion() - { - testcase("Multithreaded Clear Mixed Union"); - - // This test creates and destroys many SharedWeak pointers in a - // loop. All the pointers start as strong and a loop randomly - // convert them between strong and weak pointers. Both threads clear - // all the pointers and check that the invariants hold. - // - // Note: This test also differs from the test above in that the pointers - // randomly change from strong to weak and from weak to strong in a - // loop. This can't be done in the variant test above because variant is - // not thread safe while the SharedWeakUnion is thread safe. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - std::atomic destructionState{0}; - // returns destructorRan and partialDestructorRan (in that order) - auto getDestructorState = [&]() -> std::pair { - int const s = destructionState.load(std::memory_order_relaxed); - return {(s & 1) != 0, (s & 2) != 0}; - }; - auto setDestructorRan = [&]() -> void { - destructionState.fetch_or(1, std::memory_order_acq_rel); - }; - auto setPartialDeleteRan = [&]() -> void { - destructionState.fetch_or(2, std::memory_order_acq_rel); - }; - auto tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - setDestructorRan(); - } - }; - auto createVecOfPointers = - [&](auto const& toClone, - std::default_random_engine& eng) -> std::vector> { - std::vector> result; - std::uniform_int_distribution<> toCreateDist(4, 64); - auto numToCreate = toCreateDist(eng); - result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) - result.emplace_back(SharedIntrusive(toClone)); - return result; - }; - static constexpr int kLoopIters = 2 * 1024; - static constexpr int kFlipPointersLoopIters = 256; - static constexpr int kNumThreads = 16; - std::vector> toClone; - Barrier loopStartSyncPoint{kNumThreads}; - Barrier postCreateToCloneSyncPoint{kNumThreads}; - Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; - Barrier postFlipPointersLoopSyncPoint{kNumThreads}; - auto engines = [&]() -> std::vector { - std::random_device rd; - std::vector result; - result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - result.emplace_back(rd()); - return result; - }(); - - // cloneAndDestroy clones the strong pointer into a vector of - // mixed strong and weak pointers, runs a loop that randomly - // changes strong pointers to weak pointers, and destroys them - // all at once. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) - { - // ------ Sync Point ------ - loopStartSyncPoint.arriveAndWait(); - - // only thread 0 should reset the state - std::optional rsg; - if (threadId == 0) - { - // threadId 0 is the genesis thread. It creates the - // strong point to be cloned by the other threads. This - // thread will also check that the destructor ran and - // clear the temporary variables. - rsg.emplace(false); - auto [destructorRan, partialDeleteRan] = getDestructorState(); - BEAST_EXPECT(!i || destructorRan); - destructionState.store(0, std::memory_order_release); - - toClone.clear(); - toClone.resize(kNumThreads); - auto strong = makeSharedIntrusive(); - strong->tracingCallback = tracingCallback; - std::ranges::fill(toClone, strong); - } - - // ------ Sync Point ------ - postCreateToCloneSyncPoint.arriveAndWait(); - - auto v = createVecOfPointers(toClone[threadId], engines[threadId]); - toClone[threadId].reset(); - - // ------ Sync Point ------ - postCreateVecOfPointersSyncPoint.arriveAndWait(); - - std::uniform_int_distribution<> isStrongDist(0, 1); - for (int f = 0; f < kFlipPointersLoopIters; ++f) - { - for (auto& p : v) - { - if (isStrongDist(engines[threadId])) - { - p.convertToStrong(); - } - else - { - p.convertToWeak(); - } - } - } - - // ------ Sync Point ------ - postFlipPointersLoopSyncPoint.arriveAndWait(); - - v.clear(); - } - }; - std::vector threads; - threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - { - threads.emplace_back(cloneAndDestroy, i); - } - for (int i = 0; i < kNumThreads; ++i) - { - threads[i].join(); - } - } - - void - testMultithreadedLockingWeak() - { - testcase("Multithreaded Locking Weak"); - - // This test creates a single shared atomic pointer that multiple thread - // create weak pointers from. The threads then lock the weak pointers. - // Both threads clear all the pointers and check that the invariants - // hold. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - std::atomic destructionState{0}; - // returns destructorRan and partialDestructorRan (in that order) - auto getDestructorState = [&]() -> std::pair { - int const s = destructionState.load(std::memory_order_relaxed); - return {(s & 1) != 0, (s & 2) != 0}; - }; - auto setDestructorRan = [&]() -> void { - destructionState.fetch_or(1, std::memory_order_acq_rel); - }; - auto setPartialDeleteRan = [&]() -> void { - destructionState.fetch_or(2, std::memory_order_acq_rel); - }; - auto tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - setDestructorRan(); - } - }; - - static constexpr int kLoopIters = 2 * 1024; - static constexpr int kLockWeakLoopIters = 256; - static constexpr int kNumThreads = 16; - std::vector> toLock; - Barrier loopStartSyncPoint{kNumThreads}; - Barrier postCreateToLockSyncPoint{kNumThreads}; - Barrier postLockWeakLoopSyncPoint{kNumThreads}; - - // lockAndDestroy creates weak pointers from the strong pointer - // and runs a loop that locks the weak pointer. At the end of the loop - // all the pointers are destroyed all at once. - auto lockAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) - { - // ------ Sync Point ------ - loopStartSyncPoint.arriveAndWait(); - - // only thread 0 should reset the state - std::optional rsg; - if (threadId == 0) - { - // threadId 0 is the genesis thread. It creates the - // strong point to be locked by the other threads. This - // thread will also check that the destructor ran and - // clear the temporary variables. - rsg.emplace(false); - auto [destructorRan, partialDeleteRan] = getDestructorState(); - BEAST_EXPECT(!i || destructorRan); - destructionState.store(0, std::memory_order_release); - - toLock.clear(); - toLock.resize(kNumThreads); - auto strong = makeSharedIntrusive(); - strong->tracingCallback = tracingCallback; - std::ranges::fill(toLock, strong); - } - - // ------ Sync Point ------ - postCreateToLockSyncPoint.arriveAndWait(); - - // Multiple threads all create a weak pointer from the same - // strong pointer - WeakIntrusive const weak{toLock[threadId]}; - for (int wi = 0; wi < kLockWeakLoopIters; ++wi) - { - BEAST_EXPECT(!weak.expired()); - auto strong = weak.lock(); - BEAST_EXPECT(strong); - } - - // ------ Sync Point ------ - postLockWeakLoopSyncPoint.arriveAndWait(); - - toLock[threadId].reset(); - } - }; - std::vector threads; - threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - { - threads.emplace_back(lockAndDestroy, i); - } - for (int i = 0; i < kNumThreads; ++i) - { - threads[i].join(); - } - } - - void - run() override - { - testBasics(); - testPartialDelete(); - testDestructor(); - testMultithreadedClearMixedVariant(); - testMultithreadedClearMixedUnion(); - testMultithreadedLockingWeak(); - } -}; // namespace tests - -BEAST_DEFINE_TESTSUITE(IntrusiveShared, basics, xrpl); -} // namespace xrpl::tests diff --git a/src/test/basics/KeyCache_test.cpp b/src/test/basics/KeyCache_test.cpp deleted file mode 100644 index e83ae4c277..0000000000 --- a/src/test/basics/KeyCache_test.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include - -#include -#include // IWYU pragma: keep -#include -#include -#include - -#include - -namespace xrpl { - -class KeyCache_test : public beast::unit_test::Suite -{ -public: - void - run() override - { - using namespace std::chrono_literals; - TestStopwatch clock; - clock.set(0); - - using Key = std::string; - using Cache = TaggedCache; - - test::SuiteJournal j("KeyCacheTest", *this); - - // Insert an item, retrieve it, and age it so it gets purged. - { - Cache c("test", LedgerIndex(1), 2s, clock, j); - - BEAST_EXPECT(c.size() == 0); - BEAST_EXPECT(c.insert("one")); - BEAST_EXPECT(!c.insert("one")); - BEAST_EXPECT(c.size() == 1); - BEAST_EXPECT(c.touchIfExists("one")); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 1); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 0); - BEAST_EXPECT(!c.touchIfExists("one")); - } - - // Insert two items, have one expire - { - Cache c("test", LedgerIndex(2), 2s, clock, j); - - BEAST_EXPECT(c.insert("one")); - BEAST_EXPECT(c.size() == 1); - BEAST_EXPECT(c.insert("two")); - BEAST_EXPECT(c.size() == 2); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 2); - BEAST_EXPECT(c.touchIfExists("two")); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 1); - } - - // Insert three items (1 over limit), sweep - { - Cache c("test", LedgerIndex(2), 3s, clock, j); - - BEAST_EXPECT(c.insert("one")); - ++clock; - BEAST_EXPECT(c.insert("two")); - ++clock; - BEAST_EXPECT(c.insert("three")); - ++clock; - BEAST_EXPECT(c.size() == 3); - c.sweep(); - BEAST_EXPECT(c.size() < 3); - } - } -}; - -BEAST_DEFINE_TESTSUITE(KeyCache, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp deleted file mode 100644 index 9383fd6808..0000000000 --- a/src/test/basics/Number_test.cpp +++ /dev/null @@ -1,2006 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -// NOLINTNEXTLINE(misc-include-cleaner) -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl { - -class Number_test : public beast::unit_test::Suite -{ - using BigInt = boost::multiprecision::cpp_int; - - static std::string - fmt(BigInt const& value) - { - auto s = to_string(value); - std::string out; - int count = 0; - for (char const& ch : std::views::reverse(s)) - { - if (count != 0 && count % 3 == 0 && (isdigit(ch) != 0)) - out.insert(out.begin(), '_'); - out.insert(out.begin(), ch); - ++count; - } - return out; - } - - using dec = boost::multiprecision::cpp_dec_float_50; - - template - static T - pow10(int n) - { - if (n == 0) - return 1; - if (n == 1) - return 10; - - if (n > 1) - { - auto r = pow10(n / 2); - r *= r; - if (n % 2 != 0) - r *= 10; - return r; - } - - // n < 0 - T p = 1; - p /= pow10(-n); - return p; - } - - static std::string - fmt(dec const& v) - { - std::ostringstream os; - os << std::setprecision(40) << v; - return os.str(); - } - -public: - void - testZero() - { - testcase << "zero " << to_string(Number::getMantissaScale()); - - for (Number const& z : {Number{0, 0}, Number{0}}) - { - BEAST_EXPECT(z.mantissa() == 0); - BEAST_EXPECT(z.exponent() == Number{}.exponent()); - - BEAST_EXPECT((z + z) == z); - BEAST_EXPECT((z - z) == z); - BEAST_EXPECT(z == -z); - } - } - - void - testLimits() - { - auto const scale = Number::getMantissaScale(); - testcase << "test_limits " << to_string(scale); - bool caught = false; - auto const minMantissa = Number::minMantissa(); - try - { - [[maybe_unused]] Number const x = - Number{false, minMantissa * 10, 32768, Number::Normalized{}}; - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - - auto test = [this](auto const& x, auto const& y, int line) { - auto const result = x == y; - std::stringstream ss; - ss << x << " == " << y << " -> " << (result ? "true" : "false"); - expect(result, ss.str(), __FILE__, line); - }; - - test( - Number{false, minMantissa * 10, 32767, Number::Normalized{}}, - Number{false, minMantissa, 32768, Number::Normalized{}}, - __LINE__); - test(Number{false, minMantissa, -32769, Number::Normalized{}}, Number{}, __LINE__); - test( - Number{false, minMantissa, 32000, Number::Normalized{}} * 1'000 + - Number{false, 1'500, 32000, Number::Normalized{}}, - Number{false, minMantissa + 2, 32003, Number::Normalized{}}, - __LINE__); - // 9,223,372,036,854,775,808 - - test( - Number{std::numeric_limits::min()}, - scale == MantissaRange::MantissaScale::Small - ? Number{-9'223'372'036'854'776, 3} - : Number{true, 9'223'372'036'854'775'808ULL, 0, Number::Normalized{}}, - __LINE__); - test( - Number{std::numeric_limits::min() + 1}, - scale == MantissaRange::MantissaScale::Small ? Number{-9'223'372'036'854'776, 3} - : Number{-9'223'372'036'854'775'807}, - __LINE__); - test( - Number{std::numeric_limits::max()}, - Number{ - scale == MantissaRange::MantissaScale::Small - ? 9'223'372'036'854'776 - : std::numeric_limits::max(), - 18 - Number::mantissaLog()}, - __LINE__); - caught = false; - try - { - [[maybe_unused]] - Number const q = Number{false, minMantissa, 32767, Number::Normalized{}} * 100; - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - - void - testAdd() - { - auto const scale = Number::getMantissaScale(); - testcase << "test_add " << to_string(scale); - - using Case = std::tuple; - auto const cSmall = std::to_array( - {{Number{1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'066, -15}}, - {Number{-1'000'000'000'000'000, -15}, - Number{-6'555'555'555'555'555, -29}, - Number{-1'000'000'000'000'066, -15}}, - {Number{-1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{-9'999'999'999'999'344, -16}}, - {Number{-6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'344, -16}}, - {Number{}, Number{5}, Number{5}}, - {Number{5}, Number{}, Number{5}}, - {Number{5'555'555'555'555'555, -32768}, - Number{-5'555'555'555'555'554, -32768}, - Number{0}}, - {Number{-9'999'999'999'999'999, -31}, - Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'990, -16}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items from C - // with larger mantissa - { - {Number{1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'065'556, -18}}, - {Number{-1'000'000'000'000'000, -15}, - Number{-6'555'555'555'555'555, -29}, - Number{-1'000'000'000'000'065'556, -18}}, - {Number{-1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{-6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'000, -15}, - Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{}, Number{5}, Number{5}}, - {Number{5}, Number{}, Number{5}}, - {Number{5'555'555'555'555'555'000, -32768}, - Number{-5'555'555'555'555'554'000, -32768}, - Number{0}}, - {Number{-9'999'999'999'999'999, -31}, - Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'990, -16}}, - // Items from cSmall expanded for the larger mantissa - {Number{1'000'000'000'000'000'000, -18}, - Number{6'555'555'555'555'555'555, -35}, - Number{1'000'000'000'000'000'066, -18}}, - {Number{-1'000'000'000'000'000'000, -18}, - Number{-6'555'555'555'555'555'555, -35}, - Number{-1'000'000'000'000'000'066, -18}}, - {Number{-1'000'000'000'000'000'000, -18}, - Number{6'555'555'555'555'555'555, -35}, - Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}}, - {Number{-6'555'555'555'555'555'555, -35}, - Number{1'000'000'000'000'000'000, -18}, - Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}}, - {Number{}, Number{5}, Number{5}}, - {Number{5'555'555'555'555'555'555, -32768}, - Number{-5'555'555'555'555'555'554, -32768}, - Number{0}}, - {Number{true, 9'999'999'999'999'999'999ULL, -37, Number::Normalized{}}, - Number{1'000'000'000'000'000'000, -18}, - Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}}, - {Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}}, - // Test extremes - { - // Each Number operand rounds up, so the actual mantissa is - // minMantissa - Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}}, - Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}}, - Number{2, 19}, - }, - { - // Does not round. Mantissas are going to be > kMaxRep, 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 - // kMaxRep. - Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, - Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, - Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}}, - }, - }); - auto const cLargeLegacy = std::to_array({ - {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}}, - }); - auto const cLargeCorrected = std::to_array({ - {Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}}, - }); - auto test = [this](auto const& c) { - for (auto const& [x, y, z] : c) - { - auto const result = x + y; - std::stringstream ss; - ss << x << " + " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); - } - }; - if (scale == MantissaRange::MantissaScale::Small) - { - test(cSmall); - } - else - { - test(cLarge); - if (scale == MantissaRange::MantissaScale::LargeLegacy) - { - test(cLargeLegacy); - } - else - { - test(cLargeCorrected); - } - } - { - bool caught = false; - try - { - Number{false, Number::maxMantissa(), 32768, Number::Normalized{}} + - Number{false, Number::minMantissa(), 32767, Number::Normalized{}} * 5; - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - } - - void - testSub() - { - auto const scale = Number::getMantissaScale(); - testcase << "test_sub " << to_string(scale); - - using Case = std::tuple; - auto const cSmall = std::to_array( - {{Number{1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{9'999'999'999'999'344, -16}}, - {Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'000, -15}, - Number{-9'999'999'999'999'344, -16}}, - {Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}}, - {Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'001, -15}, - Number{-1'000'000'000'000'000, -30}}, - {Number{1'000'000'000'000'001, -15}, - Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'000, -30}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items from C - // with larger mantissa - { - {Number{1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'000, -15}, - Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}}, - {Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'001, -15}, - Number{-1'000'000'000'000'000, -30}}, - {Number{1'000'000'000'000'001, -15}, - Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'000, -30}}, - // Items from cSmall expanded for the larger mantissa - {Number{1'000'000'000'000'000'000, -18}, - Number{6'555'555'555'555'555'555, -32}, - Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{6'555'555'555'555'555'555, -32}, - Number{1'000'000'000'000'000'000, -18}, - Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{1'000'000'000'000'000'000, -18}, - Number{1'000'000'000'000'000'000, -18}, - Number{0}}, - {Number{1'000'000'000'000'000'000, -18}, - Number{1'000'000'000'000'000'001, -18}, - Number{-1'000'000'000'000'000'000, -36}}, - {Number{1'000'000'000'000'000'001, -18}, - Number{1'000'000'000'000'000'000, -18}, - Number{1'000'000'000'000'000'000, -36}}, - {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}}, - {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, - Number{1, 0}, - Number{(Number::kMaxRep / 10) + 1, 1}}, - {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, - Number{3, 0}, - Number{Number::kMaxRep}}, - {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}}, - }); - auto test = [this](auto const& c) { - for (auto const& [x, y, z] : c) - { - auto const result = x - y; - std::stringstream ss; - ss << x << " - " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); - } - }; - if (scale == MantissaRange::MantissaScale::Small) - { - test(cSmall); - } - else - { - test(cLarge); - } - } - - void - testMul() - { - auto const scale = Number::getMantissaScale(); - testcase << "test_mul " << to_string(scale); - - using Case = std::tuple; - auto test = [this](auto const& c) { - for (auto const& [x, y, z] : c) - { - auto const result = x * y; - std::stringstream ss; - ss << x << " * " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); - } - }; - auto tests = [&](auto const& cSmall, auto const& cLarge) { - if (scale == MantissaRange::MantissaScale::Small) - { - test(cSmall); - } - else - { - test(cLarge); - } - }; - auto const maxMantissa = Number::maxMantissa(); - - SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; - { - auto const cSmall = std::to_array({ - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{2000000000000000, -15}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-2000000000000000, -15}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{2000000000000000, -15}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{1000000000000000, -14}}, - {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}, - // Maximum mantissa range - {Number{9'999'999'999'999'999, 0}, - Number{9'999'999'999'999'999, 0}, - Number{9'999'999'999'999'998, 16}}, - }); - auto const cLarge = std::to_array({ - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999862, -18}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999862, -18}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999862, -18}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{false, 9'999'999'999'999'999'579ULL, -18, Number::Normalized{}}}, - {Number{1000000000000000000, -32768}, - Number{1000000000000000000, -32768}, - Number{0}}, - // Items from cSmall expanded for the larger mantissa, - // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 - // with higher precision - {Number{1414213562373095049, -18}, - Number{1414213562373095049, -18}, - Number{2000000000000000001, -18}}, - {Number{-1414213562373095048, -18}, - Number{1414213562373095048, -18}, - Number{-1999999999999999998, -18}}, - {Number{-1414213562373095048, -18}, - Number{-1414213562373095049, -18}, - Number{1999999999999999999, -18}}, - {Number{3214285714285714278, -18}, Number{3111111111111111119, -18}, Number{10, 0}}, - // Maximum mantissa range - rounds up to 1e19 - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{1, 38}}, - // Maximum int64 range - {Number{Number::kMaxRep, 0}, - Number{Number::kMaxRep, 0}, - Number{85'070'591'730'234'615'85, 19}}, - }); - tests(cSmall, cLarge); - } - Number::setround(Number::RoundingMode::TowardsZero); - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " towards_zero"; - { - auto const cSmall = std::to_array( - {{Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{9999999999999999, -15}}, - {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - { - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999861, -18}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999861, -18}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999861, -18}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{false, 9999999999999999579ULL, -18, Number::Normalized{}}}, - {Number{1000000000000000000, -32768}, - Number{1000000000000000000, -32768}, - Number{0}}, - // Items from cSmall expanded for the larger mantissa, - // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 - // with higher precision - {Number{1414213562373095049, -18}, - Number{1414213562373095049, -18}, - Number{2, 0}}, - {Number{-1414213562373095048, -18}, - Number{1414213562373095048, -18}, - Number{-1999999999999999997, -18}}, - {Number{-1414213562373095048, -18}, - Number{-1414213562373095049, -18}, - Number{1999999999999999999, -18}}, - {Number{3214285714285714278, -18}, - Number{3111111111111111119, -18}, - Number{10, 0}}, - // Maximum mantissa range - rounds down to maxMantissa/10e1 - // 99'999'999'999'999'999'800'000'000'000'000'000'100 - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}}}, - // Maximum int64 range - // 85'070'591'730'234'615'847'396'907'784'232'501'249 - {Number{Number::kMaxRep, 0}, - Number{Number::kMaxRep, 0}, - Number{85'070'591'730'234'615'84, 19}}, - }); - tests(cSmall, cLarge); - } - Number::setround(Number::RoundingMode::Downward); - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " downward"; - { - auto const cSmall = std::to_array( - {{Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-2000000000000000, -15}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999, -15}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{9999999999999999, -15}}, - {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - { - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999861, -18}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999862, -18}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999861, -18}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{false, 9'999'999'999'999'999'579ULL, -18, Number::Normalized{}}}, - {Number{1000000000000000000, -32768}, - Number{1000000000000000000, -32768}, - Number{0}}, - // Items from cSmall expanded for the larger mantissa, - // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 - // with higher precision - {Number{1414213562373095049, -18}, - Number{1414213562373095049, -18}, - Number{2, 0}}, - {Number{-1414213562373095048, -18}, - Number{1414213562373095048, -18}, - Number{-1999999999999999998, -18}}, - {Number{-1414213562373095048, -18}, - Number{-1414213562373095049, -18}, - Number{1999999999999999999, -18}}, - {Number{3214285714285714278, -18}, - Number{3111111111111111119, -18}, - Number{10, 0}}, - // Maximum mantissa range - rounds down to maxMantissa/10e1 - // 99'999'999'999'999'999'800'000'000'000'000'000'100 - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}}}, - // Maximum int64 range - // 85'070'591'730'234'615'847'396'907'784'232'501'249 - {Number{Number::kMaxRep, 0}, - Number{Number::kMaxRep, 0}, - Number{85'070'591'730'234'615'84, 19}}, - }); - tests(cSmall, cLarge); - } - Number::setround(Number::RoundingMode::Upward); - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " upward"; - { - auto const cSmall = std::to_array( - {{Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{2000000000000000, -15}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999, -15}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{2000000000000000, -15}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{1000000000000000, -14}}, - {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - { - {Number{7}, Number{8}, Number{56}}, - {Number{1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{1999999999999999862, -18}}, - {Number{-1414213562373095, -15}, - Number{1414213562373095, -15}, - Number{-1999999999999999861, -18}}, - {Number{-1414213562373095, -15}, - Number{-1414213562373095, -15}, - Number{1999999999999999862, -18}}, - {Number{3214285714285706, -15}, - Number{3111111111111119, -15}, - Number{999999999999999958, -17}}, - {Number{1000000000000000000, -32768}, - Number{1000000000000000000, -32768}, - Number{0}}, - // Items from cSmall expanded for the larger mantissa, - // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 - // with higher precision - {Number{1414213562373095049, -18}, - Number{1414213562373095049, -18}, - Number{2000000000000000001, -18}}, - {Number{-1414213562373095048, -18}, - Number{1414213562373095048, -18}, - Number{-1999999999999999997, -18}}, - {Number{-1414213562373095048, -18}, - Number{-1414213562373095049, -18}, - Number{2, 0}}, - {Number{3214285714285714278, -18}, - Number{3111111111111111119, -18}, - Number{1000000000000000001, -17}}, - // Maximum mantissa range - rounds up to minMantissa*10 - // 1e19*1e19=1e38 - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{1, 38}}, - // Maximum int64 range - // 85'070'591'730'234'615'847'396'907'784'232'501'249 - {Number{Number::kMaxRep, 0}, - Number{Number::kMaxRep, 0}, - Number{85'070'591'730'234'615'85, 19}}, - }); - tests(cSmall, cLarge); - } - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " overflow"; - { - bool caught = false; - try - { - Number{false, maxMantissa, 32768, Number::Normalized{}} * - Number{false, Number::minMantissa() * 5, 32767, Number::Normalized{}}; - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - } - - void - testDiv() - { - auto const scale = Number::getMantissaScale(); - testcase << "test_div " << to_string(scale); - - using Case = std::tuple; - auto test = [this](auto const& c) { - for (auto const& [x, y, z] : c) - { - auto const result = x / y; - std::stringstream ss; - ss << x << " / " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); - } - }; - auto const maxMantissa = Number::maxMantissa(); - auto tests = [&](auto const& cSmall, auto const& cLarge) { - if (scale == MantissaRange::MantissaScale::Small) - { - test(cSmall); - } - else - { - test(cLarge); - } - }; - SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; - { - auto const cSmall = std::to_array( - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666'667, -19}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'667, -19}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, - // Items from cSmall expanded for the larger mantissa, except - // duplicates. - {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{1'000'000'000'000'000'000}, - Number{false, maxMantissa, -18, Number::Normalized{}}}}); - tests(cSmall, cLarge); - } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " towards_zero"; - Number::setround(Number::RoundingMode::TowardsZero); - { - auto const cSmall = std::to_array( - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666'666, -19}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'666, -19}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, - // Items from cSmall expanded for the larger mantissa, except - // duplicates. - {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{1'000'000'000'000'000'000}, - Number{false, maxMantissa, -18, Number::Normalized{}}}}); - tests(cSmall, cLarge); - } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " downward"; - Number::setround(Number::RoundingMode::Downward); - { - auto const cSmall = std::to_array( - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666'666, -19}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'667, -19}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, - // Items from cSmall expanded for the larger mantissa, except - // duplicates. - {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{1'000'000'000'000'000'000}, - Number{false, maxMantissa, -18, Number::Normalized{}}}}); - tests(cSmall, cLarge); - } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " upward"; - Number::setround(Number::RoundingMode::Upward); - { - auto const cSmall = std::to_array( - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'429, -16}}}); - auto const cLarge = std::to_array( - // Note that items with extremely large mantissas need to be - // calculated, because otherwise they overflow uint64. Items - // from C with larger mantissa - {{Number{1}, Number{2}, Number{5, -1}}, - {Number{1}, Number{10}, Number{1, -1}}, - {Number{1}, Number{-10}, Number{-1, -1}}, - {Number{0}, Number{100}, Number{0}}, - {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, - {Number{9'999'999'999'999'999}, - Number{1'000'000'000'000'000}, - Number{9'999'999'999'999'999, -15}}, - {Number{2}, Number{3}, Number{6'666'666'666'666'666'667, -19}}, - {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'666, -19}}, - {Number{1}, Number{7}, Number{1'428'571'428'571'428'572, -19}}, - // Items from cSmall expanded for the larger mantissa, except - // duplicates. - {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, - {Number{false, maxMantissa, 0, Number::Normalized{}}, - Number{1'000'000'000'000'000'000}, - Number{false, maxMantissa, -18, Number::Normalized{}}}}); - tests(cSmall, cLarge); - } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " overflow"; - bool caught = false; - try - { - Number{1000000000000000, -15} / Number{0}; - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - - void - testRoot() - { - auto const scale = Number::getMantissaScale(); - testcase << "test_root " << to_string(scale); - - using Case = std::tuple; - auto test = [this](auto const& c) { - for (auto const& [x, y, z] : c) - { - auto const result = root(x, y); - std::stringstream ss; - ss << "root(" << x << ", " << y << ") = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); - } - }; - /* - auto tests = [&](auto const& cSmall, auto const& cLarge) { - test(cSmall); - if (scale != MantissaRange::MantissaScale::Small) - test(cLarge); - }; - */ - - auto const cSmall = std::to_array( - {{Number{2}, 2, Number{1414213562373095049, -18}}, - {Number{2'000'000}, 2, Number{1414213562373095049, -15}}, - {Number{2, -30}, 2, Number{1414213562373095049, -33}}, - {Number{-27}, 3, Number{-3}}, - {Number{1}, 5, Number{1}}, - {Number{-1}, 0, Number{1}}, - {Number{5, -1}, 0, Number{0}}, - {Number{0}, 5, Number{0}}, - {Number{5625, -4}, 2, Number{75, -2}}}); - auto const cLarge = std::to_array({ - {Number{false, Number::maxMantissa() - 9, -1, Number::Normalized{}}, - 2, - Number{false, 999'999'999'999'999'999, -9, Number::Normalized{}}}, - {Number{false, Number::maxMantissa() - 9, 0, Number::Normalized{}}, - 2, - Number{false, 3'162'277'660'168'379'330, -9, Number::Normalized{}}}, - {Number{Number::kMaxRep}, - 2, - Number{false, 3'037'000'499'976049692, -9, Number::Normalized{}}}, - {Number{Number::kMaxRep}, - 4, - Number{false, 55'108'98747006743627, -14, Number::Normalized{}}}, - }); - test(cSmall); - if (Number::getMantissaScale() != MantissaRange::MantissaScale::Small) - { - NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); - test(cLarge); - } - bool caught = false; - try - { - (void)root(Number{-2}, 0); - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - caught = false; - try - { - (void)root(Number{-2}, 4); - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - - void - testRoot2() - { - auto const scale = Number::getMantissaScale(); - testcase << "test_root2 " << to_string(scale); - - auto test = [this](auto const& c) { - for (auto const& x : c) - { - auto const expected = root(x, 2); - auto const result = root2(x); - std::stringstream ss; - ss << "root2(" << x << ") = " << result << ". Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); - } - }; - - auto const cSmall = std::to_array({ - Number{2}, - Number{2'000'000}, - Number{2, -30}, - Number{27}, - Number{1}, - Number{5, -1}, - Number{0}, - Number{5625, -4}, - Number{Number::kMaxRep}, - }); - test(cSmall); - bool caught = false; - try - { - (void)root2(Number{-2}); - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - - void - testPower1() - { - testcase << "test_power1 " << to_string(Number::getMantissaScale()); - using Case = std::tuple; - Case const c[]{ - {Number{64}, 0, Number{1}}, - {Number{64}, 1, Number{64}}, - {Number{64}, 2, Number{4096}}, - {Number{-64}, 2, Number{4096}}, - {Number{64}, 3, Number{262144}}, - {Number{-64}, 3, Number{-262144}}, - {Number{64}, 11, Number{false, 7378697629483820646ULL, 1, Number::Normalized{}}}, - {Number{-64}, 11, Number{true, 7378697629483820646ULL, 1, Number::Normalized{}}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT((power(x, y) == z)); - } - - void - testPower2() - { - testcase << "test_power2 " << to_string(Number::getMantissaScale()); - using Case = std::tuple; - Case const c[]{ - {Number{1}, 3, 7, Number{1}}, - {Number{-1}, 1, 0, Number{1}}, - {Number{-1, -1}, 1, 0, Number{0}}, - {Number{16}, 0, 5, Number{1}}, - {Number{34}, 3, 3, Number{34}}, - {Number{4}, 3, 2, Number{8}}}; - for (auto const& [x, n, d, z] : c) - BEAST_EXPECT((power(x, n, d) == z)); - bool caught = false; - try - { - (void)power(Number{7}, 0, 0); - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - caught = false; - try - { - (void)power(Number{7}, 1, 0); - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - caught = false; - try - { - (void)power(Number{-1, -1}, 3, 2); - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - - void - testConversions() - { - testcase << "testConversions " << to_string(Number::getMantissaScale()); - - IOUAmount const x{5, 6}; - Number const y = x; - BEAST_EXPECT((y == Number{5, 6})); - IOUAmount const z{y}; - BEAST_EXPECT(x == z); - XRPAmount const xrp{500}; - STAmount const st = xrp; - Number const n = st; - BEAST_EXPECT(XRPAmount{n} == xrp); - IOUAmount const x0{0, 0}; - Number const y0 = x0; - BEAST_EXPECT((y0 == Number{0})); - IOUAmount const z0{y0}; - BEAST_EXPECT(x0 == z0); - XRPAmount const xrp0{0}; - Number const n0 = xrp0; - BEAST_EXPECT(n0 == Number{0}); - // NOLINTNEXTLINE(misc-confusable-identifiers) - XRPAmount const xrp1{n0}; - BEAST_EXPECT(xrp1 == xrp0); - } - - void - testToInteger() - { - testcase << "test_to_integer " << to_string(Number::getMantissaScale()); - using Case = std::tuple; - SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; - { - Case const c[]{ - {Number{0}, 0}, - {Number{1}, 1}, - {Number{2}, 2}, - {Number{3}, 3}, - {Number{-1}, -1}, - {Number{-2}, -2}, - {Number{-3}, -3}, - {Number{10}, 10}, - {Number{99}, 99}, - {Number{1155}, 1155}, - {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, - {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, - {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, - {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, - {Number{15, -1}, 2}, - {Number{14, -1}, 1}, - {Number{16, -1}, 2}, - {Number{25, -1}, 2}, - {Number{6, -1}, 1}, - {Number{5, -1}, 0}, - {Number{4, -1}, 0}, - {Number{-15, -1}, -2}, - {Number{-14, -1}, -1}, - {Number{-16, -1}, -2}, - {Number{-25, -1}, -2}, - {Number{-6, -1}, -1}, - {Number{-5, -1}, 0}, - {Number{-4, -1}, 0}}; - for (auto const& [x, y] : c) - { - auto j = static_cast(x); - BEAST_EXPECT(j == y); - } - } - auto prevMode = Number::setround(Number::RoundingMode::TowardsZero); - BEAST_EXPECT(prevMode == Number::RoundingMode::ToNearest); - { - Case const c[]{ - {Number{0}, 0}, - {Number{1}, 1}, - {Number{2}, 2}, - {Number{3}, 3}, - {Number{-1}, -1}, - {Number{-2}, -2}, - {Number{-3}, -3}, - {Number{10}, 10}, - {Number{99}, 99}, - {Number{1155}, 1155}, - {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, - {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, - {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, - {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, - {Number{15, -1}, 1}, - {Number{14, -1}, 1}, - {Number{16, -1}, 1}, - {Number{25, -1}, 2}, - {Number{6, -1}, 0}, - {Number{5, -1}, 0}, - {Number{4, -1}, 0}, - {Number{-15, -1}, -1}, - {Number{-14, -1}, -1}, - {Number{-16, -1}, -1}, - {Number{-25, -1}, -2}, - {Number{-6, -1}, 0}, - {Number{-5, -1}, 0}, - {Number{-4, -1}, 0}}; - for (auto const& [x, y] : c) - { - auto j = static_cast(x); - BEAST_EXPECT(j == y); - } - } - prevMode = Number::setround(Number::RoundingMode::Downward); - BEAST_EXPECT(prevMode == Number::RoundingMode::TowardsZero); - { - Case const c[]{ - {Number{0}, 0}, - {Number{1}, 1}, - {Number{2}, 2}, - {Number{3}, 3}, - {Number{-1}, -1}, - {Number{-2}, -2}, - {Number{-3}, -3}, - {Number{10}, 10}, - {Number{99}, 99}, - {Number{1155}, 1155}, - {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, - {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, - {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, - {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, - {Number{15, -1}, 1}, - {Number{14, -1}, 1}, - {Number{16, -1}, 1}, - {Number{25, -1}, 2}, - {Number{6, -1}, 0}, - {Number{5, -1}, 0}, - {Number{4, -1}, 0}, - {Number{-15, -1}, -2}, - {Number{-14, -1}, -2}, - {Number{-16, -1}, -2}, - {Number{-25, -1}, -3}, - {Number{-6, -1}, -1}, - {Number{-5, -1}, -1}, - {Number{-4, -1}, -1}}; - for (auto const& [x, y] : c) - { - auto j = static_cast(x); - BEAST_EXPECT(j == y); - } - } - prevMode = Number::setround(Number::RoundingMode::Upward); - BEAST_EXPECT(prevMode == Number::RoundingMode::Downward); - { - Case const c[]{ - {Number{0}, 0}, - {Number{1}, 1}, - {Number{2}, 2}, - {Number{3}, 3}, - {Number{-1}, -1}, - {Number{-2}, -2}, - {Number{-3}, -3}, - {Number{10}, 10}, - {Number{99}, 99}, - {Number{1155}, 1155}, - {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, - {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, - {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, - {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, - {Number{15, -1}, 2}, - {Number{14, -1}, 2}, - {Number{16, -1}, 2}, - {Number{25, -1}, 3}, - {Number{6, -1}, 1}, - {Number{5, -1}, 1}, - {Number{4, -1}, 1}, - {Number{-15, -1}, -1}, - {Number{-14, -1}, -1}, - {Number{-16, -1}, -1}, - {Number{-25, -1}, -2}, - {Number{-6, -1}, 0}, - {Number{-5, -1}, 0}, - {Number{-4, -1}, 0}}; - for (auto const& [x, y] : c) - { - auto j = static_cast(x); - BEAST_EXPECT(j == y); - } - } - bool caught = false; - try - { - (void)static_cast(Number{9223372036854776, 3}); - } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); - } - - void - testSquelch() - { - testcase << "test_squelch " << to_string(Number::getMantissaScale()); - Number const limit{1, -6}; - BEAST_EXPECT((squelch(Number{2, -6}, limit) == Number{2, -6})); - BEAST_EXPECT((squelch(Number{1, -6}, limit) == Number{1, -6})); - BEAST_EXPECT((squelch(Number{9, -7}, limit) == Number{0})); - BEAST_EXPECT((squelch(Number{-2, -6}, limit) == Number{-2, -6})); - BEAST_EXPECT((squelch(Number{-1, -6}, limit) == Number{-1, -6})); - BEAST_EXPECT((squelch(Number{-9, -7}, limit) == Number{0})); - } - - void - testToString() - { - auto const scale = Number::getMantissaScale(); - testcase << "testToString " << to_string(scale); - - auto test = [this](Number const& n, std::string const& expected) { - auto const result = to_string(n); - std::stringstream ss; - ss << "to_string(" << result << "). Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); - }; - - test(Number(-2, 0), "-2"); - test(Number(0, 0), "0"); - test(Number(2, 0), "2"); - test(Number(25, -3), "0.025"); - test(Number(-25, -3), "-0.025"); - test(Number(25, 1), "250"); - test(Number(-25, 1), "-250"); - test(Number(2, 20), "2e20"); - test(Number(-2, -20), "-2e-20"); - // Test the edges - // ((exponent < -(25)) || (exponent > -(5))))) - // or ((exponent < -(28)) || (exponent > -(8))))) - test(Number(2, -10), "0.0000000002"); - test(Number(2, -11), "2e-11"); - - test(Number(-2, 10), "-20000000000"); - test(Number(-2, 11), "-2e11"); - - switch (scale) - { - case MantissaRange::MantissaScale::Small: - - test(Number::min(), "1e-32753"); - test(Number::max(), "9999999999999999e32768"); - test(Number::lowest(), "-9999999999999999e32768"); - { - NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); - - auto const maxMantissa = Number::maxMantissa(); - BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999); - test( - Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, - "9999999999999999"); - test( - Number{true, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, - "-9999999999999999"); - - test(Number{std::numeric_limits::max(), -3}, "9223372036854775"); - test( - -(Number{std::numeric_limits::max(), -3}), - "-9223372036854775"); - - test( - Number{std::numeric_limits::min(), 0}, "-9223372036854775e3"); - test( - -(Number{std::numeric_limits::min(), 0}), - "9223372036854775e3"); - } - break; - case MantissaRange::MantissaScale::LargeLegacy: - case MantissaRange::MantissaScale::Large: - // Test the edges - // ((exponent < -(28)) || (exponent > -(8))))) - test(Number::min(), "1e-32750"); - test(Number::max(), "9223372036854775807e32768"); - test(Number::lowest(), "-9223372036854775807e32768"); - { - NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); - - auto const maxMantissa = Number::maxMantissa(); - BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL); - test( - Number{false, maxMantissa, 0, Number::Normalized{}}, "9999999999999999990"); - test( - Number{true, maxMantissa, 0, Number::Normalized{}}, "-9999999999999999990"); - - test( - Number{std::numeric_limits::max(), 0}, "9223372036854775807"); - test( - -(Number{std::numeric_limits::max(), 0}), - "-9223372036854775807"); - - // Because the absolute value of min is larger than max, it - // will be scaled down to fit under max. Since we're - // rounding towards zero, the 8 at the end is dropped. - test( - Number{std::numeric_limits::min(), 0}, - "-9223372036854775800"); - test( - -(Number{std::numeric_limits::min(), 0}), - "9223372036854775800"); - } - - test( - Number{std::numeric_limits::max(), 0} + 1, "9223372036854775810"); - test( - -(Number{std::numeric_limits::max(), 0} + 1), - "-9223372036854775810"); - break; - default: - BEAST_EXPECT(false); - } - } - - void - testRelationals() - { - testcase << "test_relationals " << to_string(Number::getMantissaScale()); - - { - auto test = [this](auto const& nums) { - BEAST_EXPECT(std::ranges::is_sorted(nums)); - - for (auto iter1 = nums.begin(); iter1 != nums.end(); ++iter1) - { - auto iter2 = iter1; - for (++iter2; iter2 != nums.end(); ++iter2) - { - Number const& smaller = *iter1; - Number const& larger = *iter2; - std::stringstream ss; - ss << smaller << " < " << larger; - auto const str = ss.str(); - - // The ==/!= operators use a completely different code path than <, etc. - // This helps detect a breakage in one but not the other. It also helps - // verify that the values are being ordered correctly. - BEAST_EXPECTS(smaller != larger, str + " (!=)"); - BEAST_EXPECTS(!(smaller == larger), str + " (==)"); - - // true results using operator< and derived operators - BEAST_EXPECTS(smaller < larger, str + " (<)"); - BEAST_EXPECTS(larger > smaller, str + " (>)"); - BEAST_EXPECTS(larger >= smaller, str + " (>=)"); - BEAST_EXPECTS(smaller <= larger, str + " (<=)"); - - // false results using operator< and derived operators - BEAST_EXPECTS(!(larger < smaller), str + " (! <)"); - BEAST_EXPECTS(!(smaller > larger), str + " (! >)"); - BEAST_EXPECTS(!(smaller >= larger), str + " (! >=)"); - BEAST_EXPECTS(!(larger <= smaller), str + " (! <=)"); - } - } - }; - - auto const intNums = [this]() { - // Inequality test cases are built from a list of sorted integers - auto const values = - std::to_array({-100, -50, -20, -10, -1, 0, 1, 10, 20, 50, 100}); - // Check this list is sorted before converting it to Numbers. - // That way if any of the other tests fail, we know it's because of code and not the - // source data. - BEAST_EXPECT(std::ranges::is_sorted(values)); - - std::vector result; - result.reserve(values.size()); - for (auto const v : values) - result.emplace_back(v); - return result; - }(); - - auto const otherNums = std::to_array({ - Number{-5, 100}, - Number{-1, 100}, - Number{-7, -10}, - Number{-2, -10}, - Number{0}, - Number{2, -10}, - Number{7, -10}, - Number{1, 100}, - Number{5, 100}, - }); - - test(intNums); - test(otherNums); - } - - { - // Equality test cases are . Number will be compared against itself - using Case = std::pair; - auto const c = std::to_array({ - {700, __LINE__}, - {50, __LINE__}, - {1, __LINE__}, - {0, __LINE__}, - {-1, __LINE__}, - {-30, __LINE__}, - {-600, __LINE__}, - }); - for (auto const& [n, line] : c) - { - auto const str = to_string(n); - - // NOLINTBEGIN(misc-redundant-expression) Explicitly testing operators with - // equivalent values - expect(n == n, str + " ==", __FILE__, line); - expect(!(n != n), str + " !=", __FILE__, line); - - expect(!(n < n), str + " < ", __FILE__, line); - expect(!(n > n), str + " >", __FILE__, line); - expect(n >= n, str + " >=", __FILE__, line); - expect(n <= n, str + " <=", __FILE__, line); - // NOLINTEND(misc-redundant-expression) - } - } - } - - void - testStream() - { - testcase << "test_stream " << to_string(Number::getMantissaScale()); - Number const x{100}; - std::ostringstream os; - os << x; - BEAST_EXPECT(os.str() == to_string(x)); - } - - void - testIncDec() - { - testcase << "test_inc_dec " << to_string(Number::getMantissaScale()); - Number x{100}; - Number const y = +x; - BEAST_EXPECT(x == y); - BEAST_EXPECT(x++ == y); - BEAST_EXPECT(x == Number{101}); - BEAST_EXPECT(x-- == Number{101}); - BEAST_EXPECT(x == y); - } - - void - testToStAmount() - { - Issue const issue; - Number const n{7'518'783'80596, -5}; - SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; - auto res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518784}); - - Number::setround(Number::RoundingMode::TowardsZero); - res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518783}); - - Number::setround(Number::RoundingMode::Downward); - res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518783}); - - Number::setround(Number::RoundingMode::Upward); - res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518784}); - } - - void - testTruncate() - { - BEAST_EXPECT(Number(25, +1).truncate() == Number(250, 0)); - BEAST_EXPECT(Number(25, 0).truncate() == Number(25, 0)); - BEAST_EXPECT(Number(25, -1).truncate() == Number(2, 0)); - BEAST_EXPECT(Number(25, -2).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(99, -2).truncate() == Number(0, 0)); - - BEAST_EXPECT(Number(-25, +1).truncate() == Number(-250, 0)); - BEAST_EXPECT(Number(-25, 0).truncate() == Number(-25, 0)); - BEAST_EXPECT(Number(-25, -1).truncate() == Number(-2, 0)); - BEAST_EXPECT(Number(-25, -2).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(-99, -2).truncate() == Number(0, 0)); - - BEAST_EXPECT(Number(0, 0).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(0, 30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(0, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(100, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(100, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); - } - - void - testRounding() - { - // Test that rounding works as expected. - testcase("Rounding"); - - using NumberRoundings = std::map; - - std::map const expected{ - // Positive numbers - {Number{13, -1}, - {{Number::RoundingMode::ToNearest, 1}, - {Number::RoundingMode::TowardsZero, 1}, - {Number::RoundingMode::Downward, 1}, - {Number::RoundingMode::Upward, 2}}}, - {Number{23, -1}, - {{Number::RoundingMode::ToNearest, 2}, - {Number::RoundingMode::TowardsZero, 2}, - {Number::RoundingMode::Downward, 2}, - {Number::RoundingMode::Upward, 3}}}, - {Number{15, -1}, - {{Number::RoundingMode::ToNearest, 2}, - {Number::RoundingMode::TowardsZero, 1}, - {Number::RoundingMode::Downward, 1}, - {Number::RoundingMode::Upward, 2}}}, - {Number{25, -1}, - {{Number::RoundingMode::ToNearest, 2}, - {Number::RoundingMode::TowardsZero, 2}, - {Number::RoundingMode::Downward, 2}, - {Number::RoundingMode::Upward, 3}}}, - {Number{152, -2}, - {{Number::RoundingMode::ToNearest, 2}, - {Number::RoundingMode::TowardsZero, 1}, - {Number::RoundingMode::Downward, 1}, - {Number::RoundingMode::Upward, 2}}}, - {Number{252, -2}, - {{Number::RoundingMode::ToNearest, 3}, - {Number::RoundingMode::TowardsZero, 2}, - {Number::RoundingMode::Downward, 2}, - {Number::RoundingMode::Upward, 3}}}, - {Number{17, -1}, - {{Number::RoundingMode::ToNearest, 2}, - {Number::RoundingMode::TowardsZero, 1}, - {Number::RoundingMode::Downward, 1}, - {Number::RoundingMode::Upward, 2}}}, - {Number{27, -1}, - {{Number::RoundingMode::ToNearest, 3}, - {Number::RoundingMode::TowardsZero, 2}, - {Number::RoundingMode::Downward, 2}, - {Number::RoundingMode::Upward, 3}}}, - - // Negative numbers - {Number{-13, -1}, - {{Number::RoundingMode::ToNearest, -1}, - {Number::RoundingMode::TowardsZero, -1}, - {Number::RoundingMode::Downward, -2}, - {Number::RoundingMode::Upward, -1}}}, - {Number{-23, -1}, - {{Number::RoundingMode::ToNearest, -2}, - {Number::RoundingMode::TowardsZero, -2}, - {Number::RoundingMode::Downward, -3}, - {Number::RoundingMode::Upward, -2}}}, - {Number{-15, -1}, - {{Number::RoundingMode::ToNearest, -2}, - {Number::RoundingMode::TowardsZero, -1}, - {Number::RoundingMode::Downward, -2}, - {Number::RoundingMode::Upward, -1}}}, - {Number{-25, -1}, - {{Number::RoundingMode::ToNearest, -2}, - {Number::RoundingMode::TowardsZero, -2}, - {Number::RoundingMode::Downward, -3}, - {Number::RoundingMode::Upward, -2}}}, - {Number{-152, -2}, - {{Number::RoundingMode::ToNearest, -2}, - {Number::RoundingMode::TowardsZero, -1}, - {Number::RoundingMode::Downward, -2}, - {Number::RoundingMode::Upward, -1}}}, - {Number{-252, -2}, - {{Number::RoundingMode::ToNearest, -3}, - {Number::RoundingMode::TowardsZero, -2}, - {Number::RoundingMode::Downward, -3}, - {Number::RoundingMode::Upward, -2}}}, - {Number{-17, -1}, - {{Number::RoundingMode::ToNearest, -2}, - {Number::RoundingMode::TowardsZero, -1}, - {Number::RoundingMode::Downward, -2}, - {Number::RoundingMode::Upward, -1}}}, - {Number{-27, -1}, - {{Number::RoundingMode::ToNearest, -3}, - {Number::RoundingMode::TowardsZero, -2}, - {Number::RoundingMode::Downward, -3}, - {Number::RoundingMode::Upward, -2}}}, - }; - - for (auto const& [num, roundings] : expected) - { - for (auto const& [mode, val] : roundings) - { - NumberRoundModeGuard const g{mode}; - auto const res = static_cast(num); - BEAST_EXPECTS( - res == val, - to_string(num) + " with mode " + std::to_string(static_cast(mode)) + - " expected " + std::to_string(val) + " got " + std::to_string(res)); - } - } - } - - void - testInt64() - { - auto const scale = Number::getMantissaScale(); - testcase << "std::int64_t " << to_string(scale); - - // Control case - BEAST_EXPECT(Number::maxMantissa() > 10); - Number const ten{10}; - BEAST_EXPECT(ten.exponent() <= 0); - - if (scale == MantissaRange::MantissaScale::Small) - { - BEAST_EXPECT(std::numeric_limits::max() > kInitialXrp.drops()); - BEAST_EXPECT(Number::maxMantissa() < kInitialXrp.drops()); - Number const initalXrp{kInitialXrp}; - BEAST_EXPECT(initalXrp.exponent() > 0); - - Number const maxInt64{Number::kMaxRep}; - BEAST_EXPECT(maxInt64.exponent() > 0); - // 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits - BEAST_EXPECT((power(maxInt64, 2) == Number{85'070'591'730'234'62, 22})); - - Number const max = Number{false, Number::maxMantissa(), 0, Number::Normalized{}}; - BEAST_EXPECT(max.exponent() <= 0); - // 99'999'999'999'999'980'000'000'000'000'001 - 32 digits - BEAST_EXPECT((power(max, 2) == Number{99'999'999'999'999'98, 16})); - } - else - { - BEAST_EXPECT(std::numeric_limits::max() > kInitialXrp.drops()); - BEAST_EXPECT(Number::maxMantissa() > kInitialXrp.drops()); - Number const initalXrp{kInitialXrp}; - BEAST_EXPECT(initalXrp.exponent() <= 0); - - Number const maxInt64{Number::kMaxRep}; - 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 const mg(Number::RoundingMode::TowardsZero); - - auto const maxMantissa = Number::maxMantissa(); - Number const max = Number{false, maxMantissa, 0, Number::Normalized{}}; - BEAST_EXPECT(max.mantissa() == maxMantissa / 10); - BEAST_EXPECT(max.exponent() == 1); - // 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38 - // digits - BEAST_EXPECT( - (power(max, 2) == Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}})); - } - } - - void - testUpwardRoundsDown() - { - auto const scale = Number::getMantissaScale(); - { - testcase << "upward rounding produces a value below exact at kMaxRep cusp " - << to_string(scale); - - NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; - - constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; - constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL; - - Number const a = kAValue; - Number const b = kBValue; - Number const product = a * b; - - // Exact reference in BigInt. - BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); - - // What Number actually stored. - BigInt storedValue = BigInt(product.mantissa()); - for (int i = 0; i < product.exponent(); ++i) - storedValue *= 10; - - BigInt const signedDifference = storedValue - exactProduct; - - log << "\n" - << " a = " << fmt(BigInt(kAValue)) << "\n" - << " b = " << fmt(BigInt(kBValue)) << "\n" - << " exact a*b = " << fmt(exactProduct) << "\n" - << " stored = " << fmt(storedValue) << "\n" - << " stored - exact = " << fmt(signedDifference) << "\n" - << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n" - << " stored.mantissa = " << product.mantissa() << "\n" - << " stored.exponent = " << product.exponent() << "\n"; - log.flush(); - - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(signedDifference >= 0); - BEAST_EXPECT(signedDifference < pow10(product.exponent())); - BEAST_EXPECT( - product.mantissa() == (std::numeric_limits::max() / 10) + 1); - BEAST_EXPECT(product.exponent() == 19); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(signedDifference < 0); - BEAST_EXPECT( - product.mantissa() == - (std::numeric_limits::max() / 100) * 100); - BEAST_EXPECT(product.exponent() == 18); - break; - - case MantissaRange::MantissaScale::Small: - // The seemingly weird rounding here is because - // a & b are both normalized, and both round up when - // being converted to Number, so you're really - // getting 1_000_000_000_000_050 * 9_223_372_036_854_316 - BEAST_EXPECT(signedDifference >= 0); - BEAST_EXPECT( - product.mantissa() == - (std::numeric_limits::max() / 1000) + 3); - BEAST_EXPECT(product.exponent() == 21); - break; - } - } - - { - /* Companion regression for the kMaxRep cusp behavior, but for - * `operator/=` on the cusp-fix-ENABLED `Large` scale. - * - * Before the dropped-remainder fix, `operator/=` with Upward - * rounding could return a value STRICTLY LESS than the exact quotient, - * violating Upward's directional invariant. - * - * Mechanism (fix-enabled path): - * 1. `operator/=` computes `numerator = nm * 10^17` and - * `zm = numerator / dm` (integer division, truncates remainder). - * 2. If `remainder != 0`, the correction block runs: - * zm *= 100000 - * correction = (remainder * 100000) / dm // also truncates - * zm += correction - * ze -= 5 - * The truncation in `correction` discards a sub-1/100000 residual. - * 3. `normalize`'s shift loop reduces zm to fit, but the discarded - * residual is BELOW the Guard's visibility, so the Guard sees fraction = 0. - * 4. Under Upward + positive, `round()` returns -1 (no round-up), and - * the algorithm returns the truncated zm - */ - testcase << "operator/= Upward on Large returns value < truth " << to_string(scale); - - NumberRoundModeGuard const roundGuard{Number::RoundingMode::Upward}; - - constexpr std::int64_t aValue = 2LL; - constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL; - // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). - - Number const a{aValue, 0}; - Number const b{bValue, 0}; - Number const quotient = a / b; - - dec const exact = dec(aValue) / dec(bValue); - dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); - dec const diff = stored - exact; - - log << "\n" - << " a = " << aValue << "\n" - << " b = " << bValue << "\n" - << " exact a/b = " << fmt(exact) << "\n" - << " stored a/b = " << fmt(stored) << "\n" - << " stored - exact = " << fmt(diff) - << " (negative => Upward gave value BELOW truth)\n" - << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; - log.flush(); - - // Upward invariant: stored >= exact. Bug: stored < exact. - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(stored >= exact); - BEAST_EXPECT(diff < pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(stored < exact); - BEAST_EXPECT(diff >= -pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::Small: - // Small mantissa doesn't have the correction for - // dropped remainders - BEAST_EXPECT(stored < exact); - break; - } - } - { - /* Companion test case for Upward positive operator/=: Downward negative - */ - testcase << "operator/= Downward on Large returns value < truth " << to_string(scale); - - NumberRoundModeGuard const roundGuard{Number::RoundingMode::Downward}; - - constexpr std::int64_t aValue = -2LL; - constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL; - // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). - - Number const a{aValue, 0}; - Number const b{bValue, 0}; - Number const quotient = a / b; - - dec const exact = dec(aValue) / dec(bValue); - dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); - dec const diff = stored - exact; - - log << "\n" - << " a = " << aValue << "\n" - << " b = " << bValue << "\n" - << " exact a/b = " << fmt(exact) << "\n" - << " stored a/b = " << fmt(stored) << "\n" - << " stored - exact = " << fmt(diff) - << " (positive => Downward gave value ABOVE truth)\n" - << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; - log.flush(); - - // invariant: stored <= exact. Bug: stored > exact. - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(stored <= exact); - BEAST_EXPECT(diff > -pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(stored > exact); - BEAST_EXPECT(diff <= pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::Small: - // Small mantissa doesn't have the correction for - // dropped remainders - BEAST_EXPECT(stored < exact); - break; - } - } - { - /* Companion test case for Upward positive operator/=: ToNearest - * - * With ToNearest, if the dropped digits are exactly "5", then the mantissa will be - * rounded to even. The numbers below result in a value where the unrounded mantissa - * ends in an even digit, and "infinite precision" would drop - * "500000000000000000145...", but doNormalize only sees "5". Without the rounding fix, - * doNormalize rounds down to the even value. With the rounding fix, doNormalize knows - * there are more digits beyond "5", and so rounds _up_ to the odd value. - */ - testcase << "operator/= ToNearest on Large returns value < truth " << to_string(scale); - - NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; - - constexpr std::int64_t aValue = 1'269'917'268'816'087'809LL; - constexpr std::int64_t bValue = 3'458'525'013'821'685'511LL; - // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). - - Number const a{aValue, 0}; - Number const b{bValue, 0}; - Number const quotient = a / b; - - dec const exact = dec(aValue) / dec(bValue); - dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); - dec const diff = stored - exact; - - log << "\n" - << " a = " << aValue << "\n" - << " b = " << bValue << "\n" - << " exact a/b = " << fmt(exact) << "\n" - << " stored a/b = " << fmt(stored) << "\n" - << " stored - exact = " << fmt(diff) - << " (negative => ToNearest gave value BELOW truth)\n" - << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; - log.flush(); - - // invariant: stored >= exact. Bug: stored < exact. - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(stored >= exact); - BEAST_EXPECT(diff < pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(stored < exact); - BEAST_EXPECT(diff >= -pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::Small: - // Small mantissa doesn't have the correction for - // dropped remainders - BEAST_EXPECT(stored < exact); - break; - } - } - } - - void - run() override - { - for (auto const scale : MantissaRange::getAllScales()) - { - NumberMantissaScaleGuard const sg(scale); - testZero(); - testLimits(); - testToString(); - testAdd(); - testSub(); - testMul(); - testDiv(); - testRoot(); - testRoot2(); - testPower1(); - testPower2(); - testConversions(); - testToInteger(); - testSquelch(); - testRelationals(); - testStream(); - testIncDec(); - testToStAmount(); - testTruncate(); - testRounding(); - testInt64(); - - testUpwardRoundsDown(); - } - } -}; - -BEAST_DEFINE_TESTSUITE(Number, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/StringUtilities_test.cpp b/src/test/basics/StringUtilities_test.cpp deleted file mode 100644 index e12bc53857..0000000000 --- a/src/test/basics/StringUtilities_test.cpp +++ /dev/null @@ -1,309 +0,0 @@ -#include -#include -#include -#include - -#include - -namespace xrpl { - -class StringUtilities_test : public beast::unit_test::Suite -{ -public: - void - testUnHexSuccess(std::string const& strIn, std::string const& strExpected) - { - auto rv = strUnHex(strIn); - BEAST_EXPECT(rv); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(makeSlice(*rv) == makeSlice(strExpected)); - } - - void - testUnHexFailure(std::string const& strIn) - { - auto rv = strUnHex(strIn); - BEAST_EXPECT(!rv); - } - - void - testUnHex() - { - testcase("strUnHex"); - - testUnHexSuccess("526970706c6544", "RippleD"); - testUnHexSuccess("A", "\n"); - testUnHexSuccess("0A", "\n"); - testUnHexSuccess("D0A", "\r\n"); - testUnHexSuccess("0D0A", "\r\n"); - testUnHexSuccess("200D0A", " \r\n"); - testUnHexSuccess("282A2B2C2D2E2F29", "(*+,-./)"); - - // Check for things which contain some or only invalid characters - testUnHexFailure("123X"); - testUnHexFailure("V"); - testUnHexFailure("XRP"); - } - - void - testParseUrl() - { - testcase("parseUrl"); - - // Expected passes. - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain.empty()); - BEAST_EXPECT(!pUrl.port); - // RFC 3986: - // > In general, a URI that uses the generic syntax for authority - // with an empty path should be normalized to a path of "/". - // Do we want to normalize paths? - BEAST_EXPECT(pUrl.path.empty()); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme:///")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain.empty()); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "lower://domain")); - BEAST_EXPECT(pUrl.scheme == "lower"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path.empty()); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "UPPER://domain:234/")); - BEAST_EXPECT(pUrl.scheme == "upper"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 234); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "Mixed://domain/path")); - BEAST_EXPECT(pUrl.scheme == "mixed"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://[::1]:123/path")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "::1"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/path"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user@domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user@domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://:pass@domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme:///path/to/file")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain.empty()); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path/to/file"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path/with/an@sign"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://domain/path/with/an@sign")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path/with/an@sign"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://:999/")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == ":999"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "http://::1:1234/validators")); - BEAST_EXPECT(pUrl.scheme == "http"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "::0.1.18.52"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/validators"); - } - - // Expected fails. - { - ParsedUrl pUrl; - BEAST_EXPECT(!parseUrl(pUrl, "")); - BEAST_EXPECT(!parseUrl(pUrl, "nonsense")); - BEAST_EXPECT(!parseUrl(pUrl, "://")); - BEAST_EXPECT(!parseUrl(pUrl, ":///")); - BEAST_EXPECT(!parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:23498765/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:0/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:+7/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:-7234/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:@#$56!/")); - } - - { - std::string const strUrl("s://" + std::string(8192, ':')); - ParsedUrl pUrl; - BEAST_EXPECT(!parseUrl(pUrl, strUrl)); - } - } - - void - testToString() - { - testcase("toString"); - auto result = to_string("hello"); - BEAST_EXPECT(result == "hello"); - } - - void - run() override - { - testParseUrl(); - testUnHex(); - testToString(); - } -}; - -BEAST_DEFINE_TESTSUITE(StringUtilities, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/TaggedCache_test.cpp b/src/test/basics/TaggedCache_test.cpp deleted file mode 100644 index 26564a4de8..0000000000 --- a/src/test/basics/TaggedCache_test.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include - -#include -#include -#include -#include // IWYU pragma: keep -#include -#include -#include -#include - -#include -#include - -namespace xrpl { - -/* -I guess you can put some items in, make sure they're still there. Let some -time pass, make sure they're gone. Keep a strong pointer to one of them, make -sure you can still find it even after time passes. Create two objects with -the same key, canonicalize them both and make sure you get the same object. -Put an object in but keep a strong pointer to it, advance the clock a lot, -then canonicalize a new object with the same key, make sure you get the -original object. -*/ - -class TaggedCache_test : public beast::unit_test::Suite -{ -public: - void - run() override - { - using namespace std::chrono_literals; - using beast::Severity; - test::SuiteJournal journal("TaggedCache_test", *this); - - TestStopwatch clock; - clock.set(0); - - using Key = LedgerIndex; - using Value = std::string; - using Cache = TaggedCache; - - Cache c("test", 1, 1s, clock, journal); - - // Insert an item, retrieve it, and age it so it gets purged. - { - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - BEAST_EXPECT(!c.insert(1, "one")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - - { - std::string s; - BEAST_EXPECT(c.retrieve(1, s)); - BEAST_EXPECT(s == "one"); - } - - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - - // Insert an item, maintain a strong pointer, age it, and - // verify that the entry still exists. - { - BEAST_EXPECT(!c.insert(2, "two")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - - { - auto p = c.fetch(2); - BEAST_EXPECT(p != nullptr); - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 1); - } - - // Make sure its gone now that our reference is gone - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - - // Insert the same key/value pair and make sure we get the same result - { - BEAST_EXPECT(!c.insert(3, "three")); - - { - auto const p1 = c.fetch(3); - auto p2 = std::make_shared("three"); - c.canonicalizeReplaceClient(3, p2); - BEAST_EXPECT(p1.get() == p2.get()); - } - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - - // Put an object in but keep a strong pointer to it, advance the clock a - // lot, then canonicalize a new object with the same key, make sure you - // get the original object. - { - // Put an object in - BEAST_EXPECT(!c.insert(4, "four")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - - { - // Keep a strong pointer to it - auto const p1 = c.fetch(4); - BEAST_EXPECT(p1 != nullptr); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - // Advance the clock a lot - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 1); - // Canonicalize a new object with the same key - auto p2 = std::make_shared("four"); - BEAST_EXPECT(c.canonicalizeReplaceClient(4, p2)); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - // Make sure we get the original object - BEAST_EXPECT(p1.get() == p2.get()); - } - - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - { - BEAST_EXPECT(!c.insert(5, "five")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.size() == 1); - - { - auto const p1 = c.fetch(5); - BEAST_EXPECT(p1 != nullptr); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.size() == 1); - - // Advance the clock a lot - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.size() == 1); - - auto p2 = std::make_shared("five_2"); - BEAST_EXPECT(c.canonicalizeReplaceCache(5, p2)); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.size() == 1); - // Make sure the caller's original pointer is unchanged - BEAST_EXPECT(p1.get() != p2.get()); - BEAST_EXPECT(*p2 == "five_2"); - - auto const p3 = c.fetch(5); - BEAST_EXPECT(p3 != nullptr); - BEAST_EXPECT(p3.get() == p2.get()); - BEAST_EXPECT(p3.get() != p1.get()); - } - - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.size() == 0); - } - - { - testcase("intrptr"); - - struct MyRefCountObject : IntrusiveRefCounts - { - std::string data; - - // Needed to support weak intrusive pointers - virtual void - partialDestructor() {}; - - MyRefCountObject() = default; - explicit MyRefCountObject(std::string data) : data(std::move(data)) - { - } - - bool - operator==(std::string const& other) const - { - return data == other; - } - }; - - using IntrPtrCache = TaggedCache< - Key, - MyRefCountObject, - /*IsKeyCache*/ false, - intr_ptr::SharedWeakUnionPtr, - intr_ptr::SharedPtr>; - - IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal); - - intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared("one")); - BEAST_EXPECT(intrPtrCache.getCacheSize() == 1); - BEAST_EXPECT(intrPtrCache.size() == 1); - - { - { - intrPtrCache.canonicalizeReplaceCache( - 1, intr_ptr::makeShared("one_replaced")); - - auto p = intrPtrCache.fetch(1); - BEAST_EXPECT(*p == "one_replaced"); - - // Advance the clock a lot - ++clock; - intrPtrCache.sweep(); - BEAST_EXPECT(intrPtrCache.getCacheSize() == 0); - BEAST_EXPECT(intrPtrCache.size() == 1); - - intrPtrCache.canonicalizeReplaceCache( - 1, intr_ptr::makeShared("one_replaced_2")); - - auto p2 = intrPtrCache.fetch(1); - BEAST_EXPECT(*p2 == "one_replaced_2"); - - intrPtrCache.del(1, true); - } - - intrPtrCache.canonicalizeReplaceCache( - 1, intr_ptr::makeShared("one_replaced_3")); - auto p3 = intrPtrCache.fetch(1); - BEAST_EXPECT(*p3 == "one_replaced_3"); - } - - ++clock; - intrPtrCache.sweep(); - BEAST_EXPECT(intrPtrCache.getCacheSize() == 0); - BEAST_EXPECT(intrPtrCache.size() == 0); - } - } -}; - -BEAST_DEFINE_TESTSUITE(TaggedCache, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/Units_test.cpp b/src/test/basics/Units_test.cpp deleted file mode 100644 index dc780166c6..0000000000 --- a/src/test/basics/Units_test.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl::test { - -class units_test : public beast::unit_test::Suite -{ -private: - void - testTypes() - { - using FeeLevel32 = FeeLevel; - - { - XRPAmount const x{100}; - BEAST_EXPECT(x.drops() == 100); - BEAST_EXPECT((std::is_same_v)); - auto y = 4u * x; - BEAST_EXPECT(y.value() == 400); - BEAST_EXPECT((std::is_same_v)); - - auto z = 4 * y; - BEAST_EXPECT(z.value() == 1600); - BEAST_EXPECT((std::is_same_v)); - - FeeLevel32 const f{10}; - FeeLevel32 const baseFee{100}; - - auto drops = mulDiv(baseFee, x, f); - - BEAST_EXPECT(drops); - BEAST_EXPECT(drops.value() == 1000); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT((std::is_same_v< - std::remove_reference_t::unit_type, - unit::dropTag>)); - - BEAST_EXPECT((std::is_same_v, XRPAmount>)); - } - { - XRPAmount const x{100}; - BEAST_EXPECT(x.value() == 100); - BEAST_EXPECT((std::is_same_v)); - auto y = 4u * x; - BEAST_EXPECT(y.value() == 400); - BEAST_EXPECT((std::is_same_v)); - - FeeLevel64 const f{10}; - FeeLevel64 const baseFee{100}; - - auto drops = mulDiv(baseFee, x, f); - - BEAST_EXPECT(drops); - BEAST_EXPECT(drops.value() == 1000); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT((std::is_same_v< - std::remove_reference_t::unit_type, - unit::dropTag>)); - BEAST_EXPECT((std::is_same_v, XRPAmount>)); - } - { - FeeLevel64 const x{1024}; - BEAST_EXPECT(x.value() == 1024); - BEAST_EXPECT((std::is_same_v)); - std::uint64_t const m = 4; - auto y = m * x; - BEAST_EXPECT(y.value() == 4096); - BEAST_EXPECT((std::is_same_v)); - - XRPAmount const basefee{10}; - FeeLevel64 const referencefee{256}; - - auto drops = mulDiv(x, basefee, referencefee); - - BEAST_EXPECT(drops); - BEAST_EXPECT(drops.value() == 40); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT((std::is_same_v< - std::remove_reference_t::unit_type, - unit::dropTag>)); - BEAST_EXPECT((std::is_same_v, XRPAmount>)); - } - } - - void - testJson() - { - // Json value functionality - using FeeLevel32 = FeeLevel; - - { - FeeLevel32 const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{x.fee()}); - } - - { - FeeLevel32 const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{x.fee()}); - } - - { - FeeLevel64 const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{std::numeric_limits::max()}); - } - - { - FeeLevel64 const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{0}); - } - - { - FeeLevelDouble const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Real); - BEAST_EXPECT(y == json::Value{std::numeric_limits::max()}); - } - - { - FeeLevelDouble const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Real); - BEAST_EXPECT(y == json::Value{std::numeric_limits::min()}); - } - - { - XRPAmount const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Int); - BEAST_EXPECT(y == json::Value{std::numeric_limits::max()}); - } - - { - XRPAmount const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Int); - BEAST_EXPECT(y == json::Value{std::numeric_limits::min()}); - } - } - - void - testFunctions() - { - // Explicitly test every defined function for the ValueUnit class - // since some of them are templated, but not used anywhere else. - using FeeLevel32 = FeeLevel; - - { - auto make = [&](auto x) -> FeeLevel64 { return x; }; - auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; }; - - [[maybe_unused]] - FeeLevel64 const defaulted{}; - FeeLevel64 test{0}; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(beast::kZero); - BEAST_EXPECT(test.fee() == 0); - - test = beast::kZero; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(100u); - BEAST_EXPECT(test.fee() == 100); - - FeeLevel64 const targetSame{200u}; - FeeLevel32 const targetOther{300u}; - test = make(targetSame); - BEAST_EXPECT(test.fee() == 200); - BEAST_EXPECT(test == targetSame); - BEAST_EXPECT(test < FeeLevel64{1000}); - BEAST_EXPECT(test > FeeLevel64{100}); - test = make(targetOther); - BEAST_EXPECT(test.fee() == 300); - BEAST_EXPECT(test == targetOther); - - test = std::uint64_t(200); - BEAST_EXPECT(test.fee() == 200); - test = std::uint32_t(300); - BEAST_EXPECT(test.fee() == 300); - - test = targetSame; - BEAST_EXPECT(test.fee() == 200); - test = targetOther.fee(); - BEAST_EXPECT(test.fee() == 300); - BEAST_EXPECT(test == targetOther); - - test = targetSame * 2; - BEAST_EXPECT(test.fee() == 400); - test = 3 * targetSame; - BEAST_EXPECT(test.fee() == 600); - test = targetSame / 10; - BEAST_EXPECT(test.fee() == 20); - - test += targetSame; - BEAST_EXPECT(test.fee() == 220); - - test -= targetSame; - BEAST_EXPECT(test.fee() == 20); - - test++; - BEAST_EXPECT(test.fee() == 21); - ++test; - BEAST_EXPECT(test.fee() == 22); - test--; - BEAST_EXPECT(test.fee() == 21); - --test; - BEAST_EXPECT(test.fee() == 20); - - test *= 5; - BEAST_EXPECT(test.fee() == 100); - test /= 2; - BEAST_EXPECT(test.fee() == 50); - test %= 13; - BEAST_EXPECT(test.fee() == 11); - - /* - // illegal with unsigned - test = -test; - BEAST_EXPECT(test.fee() == -11); - BEAST_EXPECT(test.signum() == -1); - BEAST_EXPECT(to_string(test) == "-11"); - */ - - BEAST_EXPECT(test); - test = 0; - BEAST_EXPECT(!test); - BEAST_EXPECT(test.signum() == 0); - test = targetSame; - BEAST_EXPECT(test.signum() == 1); - BEAST_EXPECT(to_string(test) == "200"); - } - { - auto make = [&](auto x) -> FeeLevelDouble { return x; }; - auto explicitmake = [&](auto x) -> FeeLevelDouble { return FeeLevelDouble{x}; }; - - [[maybe_unused]] - FeeLevelDouble const defaulted{}; - FeeLevelDouble test{0}; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(beast::kZero); - BEAST_EXPECT(test.fee() == 0); - - test = beast::kZero; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(100.0); - BEAST_EXPECT(test.fee() == 100); - - FeeLevelDouble const targetSame{200.0}; - FeeLevel64 const targetOther{300}; - test = make(targetSame); - BEAST_EXPECT(test.fee() == 200); - BEAST_EXPECT(test == targetSame); - BEAST_EXPECT(test < FeeLevelDouble{1000.0}); - BEAST_EXPECT(test > FeeLevelDouble{100.0}); - test = targetOther.fee(); - BEAST_EXPECT(test.fee() == 300); - BEAST_EXPECT(test == targetOther); - - test = 200.0; - BEAST_EXPECT(test.fee() == 200); - test = std::uint64_t(300); - BEAST_EXPECT(test.fee() == 300); - - test = targetSame; - BEAST_EXPECT(test.fee() == 200); - - test = targetSame * 2; - BEAST_EXPECT(test.fee() == 400); - test = 3 * targetSame; - BEAST_EXPECT(test.fee() == 600); - test = targetSame / 10; - BEAST_EXPECT(test.fee() == 20); - - test += targetSame; - BEAST_EXPECT(test.fee() == 220); - - test -= targetSame; - BEAST_EXPECT(test.fee() == 20); - - test++; - BEAST_EXPECT(test.fee() == 21); - ++test; - BEAST_EXPECT(test.fee() == 22); - test--; - BEAST_EXPECT(test.fee() == 21); - --test; - BEAST_EXPECT(test.fee() == 20); - - test *= 5; - BEAST_EXPECT(test.fee() == 100); - test /= 2; - BEAST_EXPECT(test.fee() == 50); - /* illegal with floating - test %= 13; - BEAST_EXPECT(test.fee() == 11); - */ - - // legal with signed - test = -test; - BEAST_EXPECT(test.fee() == -50); - BEAST_EXPECT(test.signum() == -1); - BEAST_EXPECT(to_string(test) == "-50.000000"); - - BEAST_EXPECT(test); - test = 0; - BEAST_EXPECT(!test); - BEAST_EXPECT(test.signum() == 0); - test = targetSame; - BEAST_EXPECT(test.signum() == 1); - BEAST_EXPECT(to_string(test) == "200.000000"); - } - } - -public: - void - run() override - { - BEAST_EXPECT(kInitialXrp.drops() == 100'000'000'000'000'000); - BEAST_EXPECT(kInitialXrp == XRPAmount{100'000'000'000'000'000}); - - testTypes(); - testJson(); - testFunctions(); - } -}; - -BEAST_DEFINE_TESTSUITE(units, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/test/basics/XRPAmount_test.cpp b/src/test/basics/XRPAmount_test.cpp deleted file mode 100644 index f393003365..0000000000 --- a/src/test/basics/XRPAmount_test.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include -#include -#include - -#include -#include - -namespace xrpl { - -class XRPAmount_test : public beast::unit_test::Suite -{ -public: - void - testSigNum() - { - testcase("signum"); - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - if (i < 0) - { - BEAST_EXPECT(x.signum() < 0); - } - else if (i > 0) - { - BEAST_EXPECT(x.signum() > 0); - } - else - { - BEAST_EXPECT(x.signum() == 0); - } - } - } - - void - testBeastZero() - { - testcase("beast::Zero Comparisons"); - - using beast::kZero; - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - BEAST_EXPECT((i == 0) == (x == kZero)); - BEAST_EXPECT((i != 0) == (x != kZero)); - BEAST_EXPECT((i < 0) == (x < kZero)); - BEAST_EXPECT((i > 0) == (x > kZero)); - BEAST_EXPECT((i <= 0) == (x <= kZero)); - BEAST_EXPECT((i >= 0) == (x >= kZero)); - - BEAST_EXPECT((0 == i) == (kZero == x)); - BEAST_EXPECT((0 != i) == (kZero != x)); - BEAST_EXPECT((0 < i) == (kZero < x)); - BEAST_EXPECT((0 > i) == (kZero > x)); - BEAST_EXPECT((0 <= i) == (kZero <= x)); - BEAST_EXPECT((0 >= i) == (kZero >= x)); - } - } - - void - testComparisons() - { - testcase("XRP Comparisons"); - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - for (auto j : {-1, 0, 1}) - { - XRPAmount const y(j); - - BEAST_EXPECT((i == j) == (x == y)); - BEAST_EXPECT((i != j) == (x != y)); - BEAST_EXPECT((i < j) == (x < y)); - BEAST_EXPECT((i > j) == (x > y)); - BEAST_EXPECT((i <= j) == (x <= y)); - BEAST_EXPECT((i >= j) == (x >= y)); - } - } - } - - void - testAddSub() - { - testcase("Addition & Subtraction"); - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - for (auto j : {-1, 0, 1}) - { - XRPAmount const y(j); - - BEAST_EXPECT(XRPAmount(i + j) == (x + y)); - BEAST_EXPECT(XRPAmount(i - j) == (x - y)); - - BEAST_EXPECT((x + y) == (y + x)); // addition is commutative - } - } - } - - void - testDecimal() - { - // Tautology - BEAST_EXPECT(kDropsPerXrp.decimalXRP() == 1); - - XRPAmount test{1}; - BEAST_EXPECT(test.decimalXRP() == 0.000001); - - test = -test; - BEAST_EXPECT(test.decimalXRP() == -0.000001); - - test = 100'000'000; - BEAST_EXPECT(test.decimalXRP() == 100); - - test = -test; - BEAST_EXPECT(test.decimalXRP() == -100); - } - - void - testFunctions() - { - // Explicitly test every defined function for the XRPAmount class - // since some of them are templated, but not used anywhere else. - auto make = [&](auto x) -> XRPAmount { return XRPAmount{x}; }; - - XRPAmount const defaulted{}; - (void)defaulted; - XRPAmount test{0}; - BEAST_EXPECT(test.drops() == 0); - - test = make(beast::kZero); - BEAST_EXPECT(test.drops() == 0); - - test = beast::kZero; - BEAST_EXPECT(test.drops() == 0); - - test = make(100); - BEAST_EXPECT(test.drops() == 100); - - test = make(100u); - BEAST_EXPECT(test.drops() == 100); - - XRPAmount const targetSame{200u}; - test = make(targetSame); - BEAST_EXPECT(test.drops() == 200); - BEAST_EXPECT(test == targetSame); - BEAST_EXPECT(test < XRPAmount{1000}); - BEAST_EXPECT(test > XRPAmount{100}); - - test = std::int64_t(200); - BEAST_EXPECT(test.drops() == 200); - test = std::uint32_t(300); - BEAST_EXPECT(test.drops() == 300); - - test = targetSame; - BEAST_EXPECT(test.drops() == 200); - auto testOther = test.dropsAs(); - BEAST_EXPECT(testOther); - BEAST_EXPECT(*testOther == 200); // NOLINT(bugprone-unchecked-optional-access) - test = std::numeric_limits::max(); - testOther = test.dropsAs(); - BEAST_EXPECT(!testOther); - test = -1; - testOther = test.dropsAs(); - BEAST_EXPECT(!testOther); - - test = targetSame * 2; - BEAST_EXPECT(test.drops() == 400); - test = 3 * targetSame; - BEAST_EXPECT(test.drops() == 600); - test = 20; - BEAST_EXPECT(test.drops() == 20); - - test += targetSame; - BEAST_EXPECT(test.drops() == 220); - - test -= targetSame; - BEAST_EXPECT(test.drops() == 20); - - test *= 5; - BEAST_EXPECT(test.drops() == 100); - test = 50; - BEAST_EXPECT(test.drops() == 50); - test -= 39; - BEAST_EXPECT(test.drops() == 11); - - // legal with signed - test = -test; - BEAST_EXPECT(test.drops() == -11); - BEAST_EXPECT(test.signum() == -1); - BEAST_EXPECT(to_string(test) == "-11"); - - BEAST_EXPECT(test); - test = 0; - BEAST_EXPECT(!test); - BEAST_EXPECT(test.signum() == 0); - test = targetSame; - BEAST_EXPECT(test.signum() == 1); - BEAST_EXPECT(to_string(test) == "200"); - } - - void - testMulRatio() - { - testcase("mulRatio"); - - constexpr auto kMaxUInt32 = std::numeric_limits::max(); - constexpr auto kMaxXrp = std::numeric_limits::max(); - constexpr auto kMinXrp = std::numeric_limits::min(); - - { - // multiply by a number that would overflow then divide by the same - // number, and check we didn't lose any value - XRPAmount big(kMaxXrp); - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, false)); - - // multiply and divide by values that would overflow if done - // naively, and check that it gives the correct answer - big -= 0xf; // Subtract a little so it's divisible by 4 - BEAST_EXPECT(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3); - BEAST_EXPECT(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3); - BEAST_EXPECT((big.value() * 3) / 4 != (big.value() / 4) * 3); - } - - { - // Similar test as above, but for negative values - XRPAmount big(kMinXrp); // NOLINT TODO - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, false)); - - // multiply and divide by values that would overflow if done - // naively, and check that it gives the correct answer - BEAST_EXPECT(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3); - BEAST_EXPECT(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3); - BEAST_EXPECT((big.value() * 3) / 4 != (big.value() / 4) * 3); - } - - { - // small amounts - XRPAmount const tiny(1); - // Round up should give the smallest allowable number - BEAST_EXPECT(tiny == mulRatio(tiny, 1, kMaxUInt32, true)); - // rounding down should be zero - BEAST_EXPECT(beast::kZero == mulRatio(tiny, 1, kMaxUInt32, false)); - BEAST_EXPECT(beast::kZero == mulRatio(tiny, kMaxUInt32 - 1, kMaxUInt32, false)); - - // tiny negative numbers - XRPAmount const tinyNeg(-1); - // Round up should give zero - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, 1, kMaxUInt32, true)); - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, true)); - // rounding down should be tiny - BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, false)); - } - - { // rounding - { - XRPAmount const one(1); - auto const rup = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, true); - auto const rdown = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, false); - BEAST_EXPECT(rup.drops() - rdown.drops() == 1); - } - - { - XRPAmount const big(kMaxXrp); - auto const rup = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, true); - auto const rdown = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, false); - BEAST_EXPECT(rup.drops() - rdown.drops() == 1); - } - - { - XRPAmount const negOne(-1); - auto const rup = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, true); - auto const rdown = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, false); - BEAST_EXPECT(rup.drops() - rdown.drops() == 1); - } - } - - { - // division by zero - XRPAmount one(1); - except([&] { mulRatio(one, 1, 0, true); }); - } - - { - // overflow - XRPAmount big(kMaxXrp); - except([&] { mulRatio(big, 2, 1, true); }); - } - - { - // underflow - XRPAmount const bigNegative(kMinXrp + 10); - BEAST_EXPECT(mulRatio(bigNegative, 2, 1, true) == kMinXrp); - } - } // namespace xrpl - - //-------------------------------------------------------------------------- - - void - run() override - { - testSigNum(); - testBeastZero(); - testComparisons(); - testAddSub(); - testDecimal(); - testFunctions(); - testMulRatio(); - } -}; - -BEAST_DEFINE_TESTSUITE(XRPAmount, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/base58_test.cpp b/src/test/basics/base58_test.cpp deleted file mode 100644 index d2c8d9eabc..0000000000 --- a/src/test/basics/base58_test.cpp +++ /dev/null @@ -1,440 +0,0 @@ -#include -#include - -#include // IWYU pragma: keep - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifndef _MSC_VER - -#include -#include - -#include -#include -#include -#include -#include - -namespace xrpl::test { -namespace { - -[[nodiscard]] inline auto -randEngine() -> std::mt19937& -{ - static std::mt19937 kR = [] { - std::random_device rd; - return std::mt19937{rd()}; - }(); - return kR; -} - -constexpr int kNumTokenTypeIndexes = 9; - -[[nodiscard]] inline auto -tokenTypeAndSize(int i) -> std::tuple -{ - assert(i < kNumTokenTypeIndexes); - - switch (i) - { - using enum xrpl::TokenType; - case 0: - return {None, 20}; - case 1: - return {NodePublic, 32}; - case 2: - return {NodePublic, 33}; - case 3: - return {NodePrivate, 32}; - case 4: - return {AccountID, 20}; - case 5: - return {AccountPublic, 32}; - case 6: - return {AccountPublic, 33}; - case 7: - return {AccountSecret, 32}; - case 8: - return {FamilySeed, 16}; - default: - throw std::invalid_argument( - "Invalid token selection passed to tokenTypeAndSize() " - "in " __FILE__); - } -} - -[[nodiscard]] inline auto -randomTokenTypeAndSize() -> std::tuple -{ - using namespace xrpl; - auto& rng = randEngine(); - std::uniform_int_distribution<> d(0, 8); - return tokenTypeAndSize(d(rng)); -} - -// Return the token type and subspan of `d` to use as test data. -[[nodiscard]] inline auto -randomB256TestData(std::span d) - -> std::tuple> -{ - auto& rng = randEngine(); - std::uniform_int_distribution dist(0, 255); - auto [tokType, tokSize] = randomTokenTypeAndSize(); - std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); }); - return {tokType, d.subspan(0, tokSize)}; -} - -inline void -printAsChar(std::span a, std::span b) -{ - auto asString = [](std::span s) { - std::string r; - r.resize(s.size()); - std::ranges::copy(s, r.begin()); - return r; - }; - auto sa = asString(a); - auto sb = asString(b); - std::cerr << "\n\n" << sa << "\n" << sb << "\n"; -} - -inline void -printAsInt(std::span a, std::span b) -{ - auto asString = [](std::span s) -> std::string { - std::stringstream sstr; - for (auto i : s) - { - sstr << std::setw(3) << int(i) << ','; - } - return sstr.str(); - }; - auto sa = asString(a); - auto sb = asString(b); - std::cerr << "\n\n" << sa << "\n" << sb << "\n"; -} - -} // namespace - -namespace multiprecision_utils { - -boost::multiprecision::checked_uint512_t -toBoostMP(std::span in) -{ - boost::multiprecision::checked_uint512_t mbp = 0; - for (auto& word : std::views::reverse(in)) - { - mbp <<= 64; - mbp += word; - } - return mbp; -} - -std::vector -randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5) -{ - auto eng = randEngine(); - std::uniform_int_distribution numCoeffDist(minSize, maxSize); - std::uniform_int_distribution dist; - auto const numCoeff = numCoeffDist(eng); - std::vector coeffs; - coeffs.reserve(numCoeff); - for (int i = 0; i < numCoeff; ++i) - { - coeffs.push_back(dist(eng)); - } - return coeffs; -} -} // namespace multiprecision_utils - -class base58_test : public beast::unit_test::Suite -{ - void - testMultiprecision() - { - testcase("b58_multiprecision"); - - using namespace boost::multiprecision; - - static constexpr std::size_t kIters = 100000; - auto eng = randEngine(); - std::uniform_int_distribution dist; - std::uniform_int_distribution dist1(1); - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist(eng); - if (d == 0u) - continue; - auto bigInt = multiprecision_utils::randomBigInt(); - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refDiv = boostBigInt / d; - auto const refMod = boostBigInt % d; - - auto const mod = b58_fast::detail::inplaceBigintDivRem( - std::span(bigInt.data(), bigInt.size()), d); - auto const foundDiv = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refMod.convert_to() == mod); - BEAST_EXPECT(foundDiv == refDiv); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist(eng); - auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2); - if (bigInt[bigInt.size() - 1] == std::numeric_limits::max()) - { - bigInt[bigInt.size() - 1] -= 1; // Prevent overflow - } - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refAdd = boostBigInt + d; - - auto const result = b58_fast::detail::inplaceBigintAdd( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::Success); - auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refAdd == foundAdd); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist1(eng); - // Force overflow - std::vector bigInt(5, std::numeric_limits::max()); - - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refAdd = boostBigInt + d; - - auto const result = b58_fast::detail::inplaceBigintAdd( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::OverflowAdd); - auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refAdd != foundAdd); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist(eng); - auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2); - // inplace mul requires the most significant coeff to be zero to - // hold the result. - bigInt[bigInt.size() - 1] = 0; - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refMul = boostBigInt * d; - - auto const result = b58_fast::detail::inplaceBigintMul( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::Success); - auto const foundMul = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refMul == foundMul); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist1(eng); - // Force overflow - std::vector bigInt(5, std::numeric_limits::max()); - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refMul = boostBigInt * d; - - auto const result = b58_fast::detail::inplaceBigintMul( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::InputTooLarge); - auto const foundMul = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refMul != foundMul); - } - } - - void - testFastMatchesRef() - { - testcase("fast_matches_ref"); - auto testRawEncode = [&](std::span const& b256Data) { - std::array b58ResultBuf[2]; - std::array, 2> b58Result; - - std::array b256ResultBuf[2]; - std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b58ResultBuf[i]}; - if (i == 0) - { - auto const r = xrpl::b58_fast::detail::b256ToB58Be(b256Data, outBuf); - BEAST_EXPECT(r); - b58Result[i] = r.value(); - } - else - { - std::array tmpBuf{}; - std::string const s = xrpl::b58_ref::detail::encodeBase58( - b256Data.data(), b256Data.size(), tmpBuf.data(), tmpBuf.size()); - BEAST_EXPECT(s.size()); - b58Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b58Result[i].begin()); - } - } - if (BEAST_EXPECT(b58Result[0].size() == b58Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0)) - { - printAsChar(b58Result[0], b58Result[1]); - } - } - - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; - if (i == 0) - { - std::string const in( - b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); - auto const r = xrpl::b58_fast::detail::b58ToB256Be(in, outBuf); - BEAST_EXPECT(r); - b256Result[i] = r.value(); - } - else - { - std::string const st(b58Result[i].begin(), b58Result[i].end()); - std::string const s = xrpl::b58_ref::detail::decodeBase58(st); - BEAST_EXPECT(s.size()); - b256Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b256Result[i].begin()); - } - } - - if (BEAST_EXPECT(b256Result[0].size() == b256Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == - 0)) - { - printAsInt(b256Result[0], b256Result[1]); - } - } - }; - - auto testTokenEncode = [&](xrpl::TokenType const tokType, - std::span const& b256Data) { - std::array b58ResultBuf[2]; - std::array, 2> b58Result; - - std::array b256ResultBuf[2]; - std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()}; - if (i == 0) - { - auto const r = xrpl::b58_fast::encodeBase58Token(tokType, b256Data, outBuf); - BEAST_EXPECT(r); - b58Result[i] = r.value(); - } - else - { - std::string const s = - xrpl::b58_ref::encodeBase58Token(tokType, b256Data.data(), b256Data.size()); - BEAST_EXPECT(s.size()); - b58Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b58Result[i].begin()); - } - } - if (BEAST_EXPECT(b58Result[0].size() == b58Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0)) - { - printAsChar(b58Result[0], b58Result[1]); - } - } - - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; - if (i == 0) - { - std::string const in( - b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); - auto const r = xrpl::b58_fast::decodeBase58Token(tokType, in, outBuf); - BEAST_EXPECT(r); - b256Result[i] = r.value(); - } - else - { - std::string const st(b58Result[i].begin(), b58Result[i].end()); - std::string const s = xrpl::b58_ref::decodeBase58Token(st, tokType); - BEAST_EXPECT(s.size()); - b256Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b256Result[i].begin()); - } - } - - if (BEAST_EXPECT(b256Result[0].size() == b256Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == - 0)) - { - printAsInt(b256Result[0], b256Result[1]); - } - } - }; - - auto testIt = [&](xrpl::TokenType const tokType, std::span const& b256Data) { - testRawEncode(b256Data); - testTokenEncode(tokType, b256Data); - }; - - // test every token type with data where every byte is the same and the - // bytes range from 0-255 - for (int i = 0; i < kNumTokenTypeIndexes; ++i) - { - std::array b256DataBuf{}; - auto const [tokType, tokSize] = tokenTypeAndSize(i); - for (int d = 0; d <= 255; ++d) - { - memset(b256DataBuf.data(), d, tokSize); - testIt(tokType, std::span(b256DataBuf.data(), tokSize)); - } - } - - // test with random data - static constexpr std::size_t kIters = 100000; - for (int i = 0; i < kIters; ++i) - { - std::array b256DataBuf{}; - auto const [tokType, b256Data] = randomB256TestData(b256DataBuf); - testIt(tokType, b256Data); - } - } - - void - run() override - { - testMultiprecision(); - testFastMatchesRef(); - } -}; - -BEAST_DEFINE_TESTSUITE(base58, basics, xrpl); - -} // namespace xrpl::test - -#endif // _MSC_VER diff --git a/src/test/basics/base_uint_test.cpp b/src/test/basics/base_uint_test.cpp deleted file mode 100644 index c6572a0028..0000000000 --- a/src/test/basics/base_uint_test.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -// a non-hashing Hasher that just copies the bytes. -// Used to test hash_append in base_uint -template -struct Nonhash -{ - static constexpr auto kEndian = boost::endian::order::big; - static constexpr std::size_t kWidth = Bits / 8; - - std::array data; - - Nonhash() = default; - - void - operator()(void const* key, std::size_t len) noexcept - { - assert(len == kWidth); - memcpy(data.data(), key, len); - } - - explicit - operator std::size_t() noexcept - { - return kWidth; - } -}; - -struct base_uint_test : beast::unit_test::Suite -{ - using test96 = BaseUInt<96>; - static_assert(std::is_copy_constructible_v); - static_assert(std::is_copy_assignable_v); - - void - testComparisons() - { - { - static constexpr std::array, 6> kTestArgs{ - {{"0000000000000000", "0000000000000001"}, - {"0000000000000000", "ffffffffffffffff"}, - {"1234567812345678", "2345678923456789"}, - {"8000000000000000", "8000000000000001"}, - {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, - {"fffffffffffffffe", "ffffffffffffffff"}}}; - - for (auto const& arg : kTestArgs) - { - xrpl::BaseUInt<64> const u{arg.first}, v{arg.second}; - BEAST_EXPECT(u < v); - BEAST_EXPECT(u <= v); - BEAST_EXPECT(u != v); - BEAST_EXPECT(!(u == v)); - BEAST_EXPECT(!(u > v)); - BEAST_EXPECT(!(u >= v)); - BEAST_EXPECT(!(v < u)); - BEAST_EXPECT(!(v <= u)); - BEAST_EXPECT(v != u); - BEAST_EXPECT(!(v == u)); - BEAST_EXPECT(v > u); - BEAST_EXPECT(v >= u); - BEAST_EXPECT(u == u); - BEAST_EXPECT(v == v); - } - } - - { - static constexpr std::array, 6> kTestArgs{ - { - {"000000000000000000000000", "000000000000000000000001"}, - {"000000000000000000000000", "ffffffffffffffffffffffff"}, - {"0123456789ab0123456789ab", "123456789abc123456789abc"}, - {"555555555555555555555555", "55555555555a555555555555"}, - {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, - {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, - }}; - - for (auto const& arg : kTestArgs) - { - xrpl::BaseUInt<96> const u{arg.first}, v{arg.second}; - BEAST_EXPECT(u < v); - BEAST_EXPECT(u <= v); - BEAST_EXPECT(u != v); - BEAST_EXPECT(!(u == v)); - BEAST_EXPECT(!(u > v)); - BEAST_EXPECT(!(u >= v)); - BEAST_EXPECT(!(v < u)); - BEAST_EXPECT(!(v <= u)); - BEAST_EXPECT(v != u); - BEAST_EXPECT(!(v == u)); - BEAST_EXPECT(v > u); - BEAST_EXPECT(v >= u); - BEAST_EXPECT(u == u); - BEAST_EXPECT(v == v); - } - } - } - - void - testFromRawSizeMismatch() - { - testcase("base_uint: fromRaw size mismatch"); - - // Container larger than the base_uint (16 bytes vs 12 bytes for test96). - // Only the first 12 bytes are copied; the extra bytes are ignored. - { - Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - test96 const result = test96::fromRaw(tooBig); - BEAST_EXPECT(to_string(result) == "0102030405060708090A0B0C"); - } - } - - void - run() override - { - testcase("base_uint: general purpose tests"); - -#ifdef NDEBUG - testFromRawSizeMismatch(); -#endif - - static_assert(!std::is_constructible_v>); - static_assert(!std::is_assignable_v>); - - testComparisons(); - - // used to verify set insertion (hashing required) - std::unordered_set> uset; - - Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; - BEAST_EXPECT(test96::kBytes == raw.size()); - - test96 u = test96::fromRaw(raw); - uset.insert(u); - BEAST_EXPECT(raw.size() == u.size()); - BEAST_EXPECT(to_string(u) == "0102030405060708090A0B0C"); - BEAST_EXPECT(toShortString(u) == "01020304..."); - BEAST_EXPECT(*u.data() == 1); - BEAST_EXPECT(u.signum() == 1); - BEAST_EXPECT(!!u); - BEAST_EXPECT(!u.isZero()); - BEAST_EXPECT(u.isNonZero()); - unsigned char t = 0; - for (auto& d : u) - { - BEAST_EXPECT(d == ++t); - } - - // Test hash_append by "hashing" with a no-op hasher (h) - // and then extracting the bytes that were written during hashing - // back into another base_uint (w) for comparison with the original - Nonhash<96> h{}; - hash_append(h, u); - test96 const w = test96::fromRaw(std::vector(h.data.begin(), h.data.end())); - BEAST_EXPECT(w == u); - - test96 v{~u}; - uset.insert(v); - BEAST_EXPECT(to_string(v) == "FEFDFCFBFAF9F8F7F6F5F4F3"); - BEAST_EXPECT(toShortString(v) == "FEFDFCFB..."); - BEAST_EXPECT(*v.data() == 0xfe); - BEAST_EXPECT(v.signum() == 1); - BEAST_EXPECT(!!v); - BEAST_EXPECT(!v.isZero()); - BEAST_EXPECT(v.isNonZero()); - t = 0xff; - for (auto& d : v) - { - BEAST_EXPECT(d == --t); - } - - BEAST_EXPECT(u < v); - BEAST_EXPECT(v > u); - - v = u; - BEAST_EXPECT(v == u); - - test96 z{beast::kZero}; - uset.insert(z); - BEAST_EXPECT(to_string(z) == "000000000000000000000000"); - BEAST_EXPECT(toShortString(z) == "00000000..."); - BEAST_EXPECT(*z.data() == 0); - BEAST_EXPECT(*z.begin() == 0); - BEAST_EXPECT(*std::prev(z.end(), 1) == 0); - BEAST_EXPECT(z.signum() == 0); - BEAST_EXPECT(!z); - BEAST_EXPECT(z.isZero()); - BEAST_EXPECT(!z.isNonZero()); - for (auto& d : z) - { - BEAST_EXPECT(d == 0); - } - - test96 n{z}; - n++; - BEAST_EXPECT(n == test96(1)); - n--; - BEAST_EXPECT(n == beast::kZero); - BEAST_EXPECT(n == z); - n--; - BEAST_EXPECT(to_string(n) == "FFFFFFFFFFFFFFFFFFFFFFFF"); - BEAST_EXPECT(toShortString(n) == "FFFFFFFF..."); - n = beast::kZero; - BEAST_EXPECT(n == z); - - test96 zp1{z}; - zp1++; - test96 zm1{z}; - zm1--; - test96 const x{zm1 ^ zp1}; - uset.insert(x); - BEAST_EXPECTS(to_string(x) == "FFFFFFFFFFFFFFFFFFFFFFFE", to_string(x)); - BEAST_EXPECTS(toShortString(x) == "FFFFFFFF...", toShortString(x)); - - BEAST_EXPECT(uset.size() == 4); - - test96 tmp; - BEAST_EXPECT(tmp.parseHex(to_string(u))); - BEAST_EXPECT(tmp == u); - tmp = z; - - // fails with extra char - BEAST_EXPECT(!tmp.parseHex("A" + to_string(u))); - tmp = z; - - // fails with extra char at end - BEAST_EXPECT(!tmp.parseHex(to_string(u) + "A")); - - // fails with a non-hex character at some point in the string: - tmp = z; - - for (std::size_t i = 0; i != 24; ++i) - { - std::string x = to_string(z); - x[i] = ('G' + (i % 10)); - BEAST_EXPECT(!tmp.parseHex(x)); - } - - // Walking 1s: - for (std::size_t i = 0; i != 24; ++i) - { - std::string s1 = "000000000000000000000000"; - s1[i] = '1'; - - BEAST_EXPECT(tmp.parseHex(s1)); - BEAST_EXPECT(to_string(tmp) == s1); - } - - // Walking 0s: - for (std::size_t i = 0; i != 24; ++i) - { - std::string s1 = "111111111111111111111111"; - s1[i] = '0'; - - BEAST_EXPECT(tmp.parseHex(s1)); - BEAST_EXPECT(to_string(tmp) == s1); - } - - // Constexpr constructors - { - static_assert(test96{}.signum() == 0); - static_assert(test96("0").signum() == 0); - static_assert(test96("000000000000000000000000").signum() == 0); - static_assert(test96("000000000000000000000001").signum() == 1); - static_assert(test96("800000000000000000000000").signum() == 1); - -// Everything within the #if should fail during compilation. -#if 0 - // Too few characters - static_assert(test96("00000000000000000000000").signum() == 0); - - // Too many characters - static_assert(test96("0000000000000000000000000").signum() == 0); - - // Non-hex characters - static_assert(test96("00000000000000000000000 ").signum() == 1); - static_assert(test96("00000000000000000000000/").signum() == 1); - static_assert(test96("00000000000000000000000:").signum() == 1); - static_assert(test96("00000000000000000000000@").signum() == 1); - static_assert(test96("00000000000000000000000G").signum() == 1); - static_assert(test96("00000000000000000000000`").signum() == 1); - static_assert(test96("00000000000000000000000g").signum() == 1); - static_assert(test96("00000000000000000000000~").signum() == 1); -#endif // 0 - - // Using the constexpr constructor in a non-constexpr context - // with an error in the parsing throws an exception. - { - // Invalid length for string. - bool caught = false; - try - { - // Try to prevent constant evaluation. - std::vector str(23, '7'); - std::string_view const sView(str.data(), str.size()); - [[maybe_unused]] test96 const t96(sView); - } - catch (std::invalid_argument const& e) - { - BEAST_EXPECT(e.what() == std::string("invalid length for hex string")); - caught = true; - } - BEAST_EXPECT(caught); - } - { - // Invalid character in string. - bool caught = false; - try - { - // Try to prevent constant evaluation. - std::vector str(23, '7'); - str.push_back('G'); - std::string_view const sView(str.data(), str.size()); - [[maybe_unused]] test96 const t96(sView); - } - catch (std::range_error const& e) - { - BEAST_EXPECT(e.what() == std::string("invalid hex character")); - caught = true; - } - BEAST_EXPECT(caught); - } - - // Verify that constexpr base_uints interpret a string the same - // way parseHex() does. - struct StrBaseUInt - { - char const* const str; - test96 tst; - - constexpr StrBaseUInt(char const* s) : str(s), tst(s) - { - } - }; - static constexpr StrBaseUInt kTestCases[] = { - "000000000000000000000000", - "000000000000000000000001", - "fedcba9876543210ABCDEF91", - "19FEDCBA0123456789abcdef", - "800000000000000000000000", - "fFfFfFfFfFfFfFfFfFfFfFfF"}; - - for (StrBaseUInt const& t : kTestCases) - { - test96 t96; - BEAST_EXPECT(t96.parseHex(t.str)); - BEAST_EXPECT(t96 == t.tst); - } - } - } -}; - -BEAST_DEFINE_TESTSUITE(base_uint, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/test/basics/join_test.cpp b/src/test/basics/join_test.cpp deleted file mode 100644 index 1ee81f786a..0000000000 --- a/src/test/basics/join_test.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -struct join_test : beast::unit_test::Suite -{ - void - run() override - { - auto test = [this](auto collectionanddelimiter, std::string expected) { - std::stringstream ss; - // Put something else in the buffer before and after to ensure that - // the << operator returns the stream correctly. - ss << "(" << collectionanddelimiter << ")"; - auto const str = ss.str(); - BEAST_EXPECT(str.substr(1, str.length() - 2) == expected); - BEAST_EXPECT(str.front() == '('); - BEAST_EXPECT(str.back() == ')'); - }; - - // C++ array - test(CollectionAndDelimiter(std::array{2, -1, 5, 10}, "/"), "2/-1/5/10"); - // One item C++ array edge case - test(CollectionAndDelimiter(std::array{"test"}, " & "), "test"); - // Empty C++ array edge case - test(CollectionAndDelimiter(std::array{}, ","), ""); - { - // C-style array - char letters[4]{'w', 'a', 's', 'd'}; - test(CollectionAndDelimiter(letters, std::to_string(0)), "w0a0s0d"); - } - { - // Auto sized C-style array - std::string words[]{"one", "two", "three", "four"}; - test(CollectionAndDelimiter(words, "\n"), "one\ntwo\nthree\nfour"); - } - { - // One item C-style array edge case - std::string words[]{"thing"}; - test(CollectionAndDelimiter(words, "\n"), "thing"); - } - // Initializer list - test(CollectionAndDelimiter(std::initializer_list{19, 25}, "+"), "19+25"); - // vector - test(CollectionAndDelimiter(std::vector{0, 42}, std::to_string(99)), "09942"); - { - // vector with one item edge case - using namespace jtx; - test( - CollectionAndDelimiter(std::vector{Account::kMaster}, "xxx"), - Account::kMaster.human()); - } - // empty vector edge case - test(CollectionAndDelimiter(std::vector{}, ","), ""); - // C-style string - test(CollectionAndDelimiter("string", " "), "s t r i n g"); - // Empty C-style string edge case - test(CollectionAndDelimiter("", "*"), ""); - // Single char C-style string edge case - test(CollectionAndDelimiter("x", "*"), "x"); - // std::string - test(CollectionAndDelimiter(std::string{"string"}, "-"), "s-t-r-i-n-g"); - // Empty std::string edge case - test(CollectionAndDelimiter(std::string{""}, "*"), ""); - // Single char std::string edge case - test(CollectionAndDelimiter(std::string{"y"}, "*"), "y"); - } -}; // namespace test - -BEAST_DEFINE_TESTSUITE(join, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/test/beast/IPEndpoint_test.cpp b/src/test/beast/IPEndpoint_test.cpp index b1cd4709b4..bc04087891 100644 --- a/src/test/beast/IPEndpoint_test.cpp +++ b/src/test/beast/IPEndpoint_test.cpp @@ -305,6 +305,15 @@ public: BEAST_EXPECT(!isLoopback(ep)); BEAST_EXPECTS(to_string(ep) == "fd00::1", to_string(ep)); + // unspecified IPv6 (::) + ep = Endpoint(AddressV6{}); + BEAST_EXPECT(isUnspecified(ep)); + BEAST_EXPECT(!isPublic(ep)); + BEAST_EXPECT(!isPrivate(ep)); + BEAST_EXPECT(!isMulticast(ep)); + BEAST_EXPECT(!isLoopback(ep)); + BEAST_EXPECTS(to_string(ep) == "::", to_string(ep)); + { ep = Endpoint::fromString("192.0.2.112"); BEAST_EXPECT(!isUnspecified(ep)); diff --git a/src/test/beast/define_print.cpp b/src/test/beast/define_print.cpp index 569b06ca67..e6b24e5cf2 100644 --- a/src/test/beast/define_print.cpp +++ b/src/test/beast/define_print.cpp @@ -15,7 +15,9 @@ namespace beast::unit_test { -/** A suite that prints the list of globally defined suites. */ +/** + * A suite that prints the list of globally defined suites. + */ class print_test : public Suite { public: diff --git a/src/test/consensus/Consensus_test.cpp b/src/test/consensus/Consensus_test.cpp index 92a4c67e32..45f58d16ba 100644 --- a/src/test/consensus/Consensus_test.cpp +++ b/src/test/consensus/Consensus_test.cpp @@ -1034,16 +1034,6 @@ public: // slow ledger is generated UndoDelay undoDelay{behind}; sim.collectors.add(undoDelay); - -#if 0 - // Have all beast::journal output printed to stdout - for (Peer* p : network) - p->sink.threshold(beast::Severity::All); - - // Print ledger accept and fully validated events to stdout - StreamCollector sc{std::cout}; - sim.collectors.add(sc); -#endif // Run the simulation for 100 seconds of simulation time with std::chrono::nanoseconds const simDuration = 100s; diff --git a/src/test/consensus/DistributedValidatorsSim_test.cpp b/src/test/consensus/DistributedValidatorsSim_test.cpp index 1def09db13..437ad81ee0 100644 --- a/src/test/consensus/DistributedValidatorsSim_test.cpp +++ b/src/test/consensus/DistributedValidatorsSim_test.cpp @@ -21,7 +21,8 @@ namespace xrpl::test { -/** In progress simulations for diversifying and distributing validators +/** + * In progress simulations for diversifying and distributing validators */ class DistributedValidators_test : public beast::unit_test::Suite { diff --git a/src/test/consensus/LedgerTrie_test.cpp b/src/test/consensus/LedgerTrie_test.cpp index 0ddf1bf82f..a4eb7bc087 100644 --- a/src/test/consensus/LedgerTrie_test.cpp +++ b/src/test/consensus/LedgerTrie_test.cpp @@ -519,17 +519,18 @@ class LedgerTrie_test : public beast::unit_test::Suite // Changing largestSeq perspective changes preferred branch { - /** Build the tree below with initial tip support annotated - A - / \ - B(1) C(1) - / | | - H D F(1) - | - E(2) - | - G - */ + /** + * Build the tree below with initial tip support annotated + * A + * / \ + * B(1) C(1) + * / | | + * H D F(1) + * | + * E(2) + * | + * G + */ LedgerTrie t; LedgerHistoryHelper h; t.insert(h["ab"]); @@ -548,17 +549,18 @@ class LedgerTrie_test : public beast::unit_test::Suite BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["a"].id()); // NOLINTEND(bugprone-unchecked-optional-access) - /** One of E advancing to G doesn't change anything - A - / \ - B(1) C(1) - / | | - H D F(1) - | - E(1) - | - G(1) - */ + /** + * One of E advancing to G doesn't change anything + * A + * / \ + * B(1) C(1) + * / | | + * H D F(1) + * | + * E(1) + * | + * G(1) + */ t.remove(h["abde"]); t.insert(h["abdeg"]); @@ -570,17 +572,18 @@ class LedgerTrie_test : public beast::unit_test::Suite BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["a"].id()); // NOLINTEND(bugprone-unchecked-optional-access) - /** C advancing to H does advance the seq 3 preferred ledger - A - / \ - B(1) C - / | | - H(1)D F(1) - | - E(1) - | - G(1) - */ + /** + * C advancing to H does advance the seq 3 preferred ledger + * A + * / \ + * B(1) C + * / | | + * H(1)D F(1) + * | + * E(1) + * | + * G(1) + */ t.remove(h["ac"]); t.insert(h["abh"]); @@ -592,17 +595,18 @@ class LedgerTrie_test : public beast::unit_test::Suite BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["a"].id()); // NOLINTEND(bugprone-unchecked-optional-access) - /** F advancing to E also moves the preferred ledger forward - A - / \ - B(1) C - / | | - H(1)D F - | - E(2) - | - G(1) - */ + /** + * F advancing to E also moves the preferred ledger forward + * A + * / \ + * B(1) C + * / | | + * H(1)D F + * | + * E(2) + * | + * G(1) + */ t.remove(h["acf"]); t.insert(h["abde"]); diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index 38c14b8ac5..e98a0e1e88 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -124,7 +124,7 @@ backend=sqlite } /** - Write an xrpld config file and remove when done. + * Write an xrpld config file and remove when done. */ class FileCfgGuard : public xrpl::detail::FileDirGuard { @@ -235,7 +235,7 @@ more-xrpl-validators.net } /** - Write a validators.txt file and remove when done. + * Write a validators.txt file and remove when done. */ class ValidatorsTxtGuard : public detail::FileDirGuard { @@ -1475,6 +1475,7 @@ r.ripple.com:51235 std::string toLoad(R"xrpldConfig( [amendment_majority_time] )xrpldConfig"); + // NOLINTNEXTLINE(performance-inefficient-string-concatenation) toLoad += std::to_string(val) + space + unit; space = space.empty() ? " " : ""; diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp index d37569d6cd..373ec66cd1 100644 --- a/src/test/core/SociDB_test.cpp +++ b/src/test/core/SociDB_test.cpp @@ -229,63 +229,7 @@ public: fail(); } // There are too many issues when working with soci::row and - // boost::tuple. DO NOT USE soci row! I had a set of workarounds to - // make soci row less error prone, I'm keeping these tests in case I - // try to add soci::row and boost::tuple back into soci. -#if 0 - try - { - std::int32_t ig = 0; - std::uint32_t uig = 0; - std::int64_t big = 0; - std::uint64_t ubig = 0; - soci::row r; - s << "SELECT I, UI, BI, UBI from STT", soci::into (r); - ig = r.get(0); - uig = r.get(1); - big = r.get(2); - ubig = r.get(3); - BEAST_EXPECT(ig == id[0] && uig == uid[0] && big == bid[0] && - ubig == ubid[0]); - } - catch (std::exception&) - { - fail (); - } - try - { - std::int32_t ig = 0; - std::uint32_t uig = 0; - std::int64_t big = 0; - std::uint64_t ubig = 0; - soci::row r; - s << "SELECT I, UI, BI, UBI from STT", soci::into (r); - ig = r.get("I"); - uig = r.get("UI"); - big = r.get("BI"); - ubig = r.get("UBI"); - BEAST_EXPECT(ig == id[0] && uig == uid[0] && big == bid[0] && - ubig == ubid[0]); - } - catch (std::exception&) - { - fail (); - } - try - { - boost::tuple d; - s << "SELECT I, UI, BI, UBI from STT", soci::into (d); - BEAST_EXPECT(get<0>(d) == id[0] && get<1>(d) == uid[0] && - get<2>(d) == bid[0] && get<3>(d) == ubid[0]); - } - catch (std::exception&) - { - fail (); - } -#endif + // boost::tuple. DO NOT USE soci row! } { namespace bfs = boost::filesystem; diff --git a/src/test/csf/BasicNetwork.h b/src/test/csf/BasicNetwork.h index 4b88592128..0428475504 100644 --- a/src/test/csf/BasicNetwork.h +++ b/src/test/csf/BasicNetwork.h @@ -6,59 +6,59 @@ #include namespace xrpl::test::csf { -/** Peer to peer network simulator. - - The network is formed from a set of Peer objects representing - vertices and configurable connections representing edges. - The caller is responsible for creating the Peer objects ahead - of time. - - Peer objects cannot be destroyed once the BasicNetwork is - constructed. To handle peers going online and offline, - callers can simply disconnect all links and reconnect them - later. Connections are directed, one end is the inbound - Peer and the other is the outbound Peer. - - Peers may send messages along their connections. To simulate - the effects of latency, these messages can be delayed by a - configurable duration set when the link is established. - Messages always arrive in the order they were sent on a - particular connection. - - A message is modeled using a lambda function. The caller - provides the code to execute upon delivery of the message. - If a Peer is disconnected, all messages pending delivery - at either end of the connection will not be delivered. - - When creating the Peer set, the caller needs to provide a - Scheduler object for managing the timing and delivery - of messages. After constructing the network, and establishing - connections, the caller uses the scheduler's step* functions - to drive messages through the network. - - The graph of peers and connections is internally represented - using Digraph. Clients have - const access to that graph to perform additional operations not - directly provided by BasicNetwork. - - Peer Requirements: - - Peer should be a lightweight type, cheap to copy - and/or move. A good candidate is a simple pointer to - the underlying user defined type in the simulation. - - Expression Type Requirements - ---------- ---- ------------ - P Peer - u, v Values of type P - P u(v) CopyConstructible - u.~P() Destructible - u == v bool EqualityComparable - u < v bool LessThanComparable - std::hash

class std::hash is defined for P - ! u bool true if u is not-a-peer - -*/ +/** + * Peer to peer network simulator. + * + * The network is formed from a set of Peer objects representing + * vertices and configurable connections representing edges. + * The caller is responsible for creating the Peer objects ahead + * of time. + * + * Peer objects cannot be destroyed once the BasicNetwork is + * constructed. To handle peers going online and offline, + * callers can simply disconnect all links and reconnect them + * later. Connections are directed, one end is the inbound + * Peer and the other is the outbound Peer. + * + * Peers may send messages along their connections. To simulate + * the effects of latency, these messages can be delayed by a + * configurable duration set when the link is established. + * Messages always arrive in the order they were sent on a + * particular connection. + * + * A message is modeled using a lambda function. The caller + * provides the code to execute upon delivery of the message. + * If a Peer is disconnected, all messages pending delivery + * at either end of the connection will not be delivered. + * + * When creating the Peer set, the caller needs to provide a + * Scheduler object for managing the timing and delivery + * of messages. After constructing the network, and establishing + * connections, the caller uses the scheduler's step* functions + * to drive messages through the network. + * + * The graph of peers and connections is internally represented + * using Digraph. Clients have + * const access to that graph to perform additional operations not + * directly provided by BasicNetwork. + * + * Peer Requirements: + * + * Peer should be a lightweight type, cheap to copy + * and/or move. A good candidate is a simple pointer to + * the underlying user defined type in the simulation. + * + * Expression Type Requirements + * ---------- ---- ------------ + * P Peer + * u, v Values of type P + * P u(v) CopyConstructible + * u.~P() Destructible + * u == v bool EqualityComparable + * u < v bool LessThanComparable + * std::hash

class std::hash is defined for P + * ! u bool true if u is not-a-peer + */ template class BasicNetwork { @@ -92,79 +92,84 @@ public: BasicNetwork(Scheduler& s); - /** Connect two peers. - - The link is directed, with `from` establishing - the outbound connection and `to` receiving the - incoming connection. - - Preconditions: - - from != to (self connect disallowed). - - A link between from and to does not - already exist (duplicates disallowed). - - Effects: - - Creates a link between from and to. - - @param `from` The source of the outgoing connection - @param `to` The recipient of the incoming connection - @param `delay` The time delay of all delivered messages - @return `true` if a new connection was established - */ + /** + * Connect two peers. + * + * The link is directed, with `from` establishing + * the outbound connection and `to` receiving the + * incoming connection. + * + * Preconditions: + * + * from != to (self connect disallowed). + * + * A link between from and to does not + * already exist (duplicates disallowed). + * + * Effects: + * + * Creates a link between from and to. + * + * @param `from` The source of the outgoing connection + * @param `to` The recipient of the incoming connection + * @param `delay` The time delay of all delivered messages + * @return `true` if a new connection was established + */ bool connect(Peer const& from, Peer const& to, duration const& delay = std::chrono::seconds{0}); - /** Break a link. - - Effects: - - If a connection is present, both ends are - disconnected. - - Any pending messages on the connection - are discarded. - - @return `true` if a connection was broken. - */ + /** + * Break a link. + * + * Effects: + * + * If a connection is present, both ends are + * disconnected. + * + * Any pending messages on the connection + * are discarded. + * + * @return `true` if a connection was broken. + */ bool disconnect(Peer const& peer1, Peer const& peer2); - /** Send a message to a peer. - - Preconditions: - - A link exists between from and to. - - Effects: - - If the link is not broken when the - link's `delay` time has elapsed, - the function will be invoked with - no arguments. - - @note Its the caller's responsibility to - ensure that the body of the function performs - activity consistent with `from`'s receipt of - a message from `to`. - */ + /** + * Send a message to a peer. + * + * Preconditions: + * + * A link exists between from and to. + * + * Effects: + * + * If the link is not broken when the + * link's `delay` time has elapsed, + * the function will be invoked with + * no arguments. + * + * @note Its the caller's responsibility to + * ensure that the body of the function performs + * activity consistent with `from`'s receipt of + * a message from `to`. + */ template void send(Peer const& from, Peer const& to, Function&& f); - /** Return the range of active links. - - @return A random access range over Digraph::Edge instances - */ + /** + * Return the range of active links. + * + * @return A random access range over Digraph::Edge instances + */ auto links(Peer const& from) { return links_.outEdges(from); } - /** Return the underlying digraph + /** + * Return the underlying digraph */ [[nodiscard]] Digraph const& graph() const diff --git a/src/test/csf/CollectorRef.h b/src/test/csf/CollectorRef.h index 5bc55c7515..3aef4d617f 100644 --- a/src/test/csf/CollectorRef.h +++ b/src/test/csf/CollectorRef.h @@ -12,45 +12,45 @@ namespace xrpl::test::csf { -/** Holds a type-erased reference to an arbitrary collector. - - A collector is any class that implements - - on(NodeID, SimTime, Event) - - for all events emitted by a Peer. - - This class is used to type-erase the actual collector used by each peer in - the simulation. The idea is to compose complicated and typed collectors - using the helpers in collectors.h, then only type erase at the higher-most - level when adding to the simulation. - - The example code below demonstrates the reason for storing the collector - as a reference. The collector's lifetime will generally be longer than - the simulation; perhaps several simulations are run for a single collector - instance. The collector potentially stores lots of data as well, so the - simulation needs to point to the single instance, rather than requiring - collectors to manage copying that data efficiently in their design. - - @code - // Initialize a specific collector that might write to a file. - SomeFancyCollector collector{"out.file"}; - - // Setup your simulation - Sim sim(trustgraph, topology, collector); - - // Run the simulation - sim.run(100); - - // do any reported related to the collector - collector.report(); - - @endcode - - @note If a new event type is added, it needs to be added to the interfaces - below. - -*/ +/** + * Holds a type-erased reference to an arbitrary collector. + * + * A collector is any class that implements + * + * on(NodeID, SimTime, Event) + * + * for all events emitted by a Peer. + * + * This class is used to type-erase the actual collector used by each peer in + * the simulation. The idea is to compose complicated and typed collectors + * using the helpers in collectors.h, then only type erase at the higher-most + * level when adding to the simulation. + * + * The example code below demonstrates the reason for storing the collector + * as a reference. The collector's lifetime will generally be longer than + * the simulation; perhaps several simulations are run for a single collector + * instance. The collector potentially stores lots of data as well, so the + * simulation needs to point to the single instance, rather than requiring + * collectors to manage copying that data efficiently in their design. + * + * @code + * // Initialize a specific collector that might write to a file. + * SomeFancyCollector collector{"out.file"}; + * + * // Setup your simulation + * Sim sim(trustgraph, topology, collector); + * + * // Run the simulation + * sim.run(100); + * + * // do any reported related to the collector + * collector.report(); + * + * @endcode + * + * @note If a new event type is added, it needs to be added to the interfaces + * below. + */ class CollectorRef { using tp = SimTime; @@ -296,16 +296,17 @@ public: } }; -/** A container of CollectorRefs - - A set of CollectorRef instances that process the same events. An event is - processed by collectors in the order the collectors were added. - - This class type-erases the collector instances. By contract, the - Collectors/collectors class/helper in collectors.h are not type erased and - offer an opportunity for type transformations and combinations with - improved compiler optimizations. -*/ +/** + * A container of CollectorRefs + * + * A set of CollectorRef instances that process the same events. An event is + * processed by collectors in the order the collectors were added. + * + * This class type-erases the collector instances. By contract, the + * Collectors/collectors class/helper in collectors.h are not type erased and + * offer an opportunity for type transformations and combinations with + * improved compiler optimizations. + */ class CollectorRefs { std::vector collectors_; diff --git a/src/test/csf/Digraph.h b/src/test/csf/Digraph.h index 82ed713561..b1b49404b5 100644 --- a/src/test/csf/Digraph.h +++ b/src/test/csf/Digraph.h @@ -21,16 +21,16 @@ struct NoEdgeData namespace test::csf { -/** Directed graph - -Basic directed graph that uses an adjacency list to represent out edges. - -Instances of Vertex uniquely identify vertices in the graph. Instances of -EdgeData is any data to store in the edge connecting two vertices. - -Both Vertex and EdgeData should be lightweight and cheap to copy. - -*/ +/** + * Directed graph + * + * Basic directed graph that uses an adjacency list to represent out edges. + * + * Instances of Vertex uniquely identify vertices in the graph. Instances of + * EdgeData is any data to store in the edge connecting two vertices. + * + * Both Vertex and EdgeData should be lightweight and cheap to copy. + */ template class Digraph { @@ -42,41 +42,42 @@ class Digraph Links empty_; public: - /** Connect two vertices - - @param source The source vertex - @param target The target vertex - @param e The edge data - @return true if the edge was created - - */ + /** + * Connect two vertices + * + * @param source The source vertex + * @param target The target vertex + * @param e The edge data + * @return true if the edge was created + */ bool connect(Vertex source, Vertex target, EdgeData e) { return graph_[source].emplace(target, e).second; } - /** Connect two vertices using default constructed edge data - - @param source The source vertex - @param target The target vertex - @return true if the edge was created - - */ + /** + * Connect two vertices using default constructed edge data + * + * @param source The source vertex + * @param target The target vertex + * @return true if the edge was created + */ bool connect(Vertex source, Vertex target) { return connect(source, target, EdgeData{}); } - /** Disconnect two vertices - - @param source The source vertex - @param target The target vertex - @return true if an edge was removed - - If source is not connected to target, this function does nothing. - */ + /** + * Disconnect two vertices + * + * @param source The source vertex + * @param target The target vertex + * @return true if an edge was removed + * + * If source is not connected to target, this function does nothing. + */ bool disconnect(Vertex source, Vertex target) { @@ -88,13 +89,13 @@ public: return false; } - /** Return edge data between two vertices - - @param source The source vertex - @param target The target vertex - @return optional which is std::nullopt if no edge exists - - */ + /** + * Return edge data between two vertices + * + * @param source The source vertex + * @param target The target vertex + * @return optional which is std::nullopt if no edge exists + */ [[nodiscard]] std::optional edge(Vertex source, Vertex target) const { @@ -108,23 +109,25 @@ public: return std::nullopt; } - /** Check if two vertices are connected - - @param source The source vertex - @param target The target vertex - @return true if the source has an out edge to target - */ + /** + * Check if two vertices are connected + * + * @param source The source vertex + * @param target The target vertex + * @return true if the source has an out edge to target + */ [[nodiscard]] bool connected(Vertex source, Vertex target) const { return edge(source, target) != std::nullopt; } - /** Range over vertices in the graph - - @return A boost transformed range over the vertices with out edges in - the graph - */ + /** + * Range over vertices in the graph + * + * @return A boost transformed range over the vertices with out edges in + * the graph + */ [[nodiscard]] auto outVertices() const { @@ -132,10 +135,11 @@ public: graph_, [](Graph::value_type const& v) { return v.first; }); } - /** Range over target vertices - - @param source The source vertex - @return A boost transformed range over the target vertices of source. + /** + * Range over target vertices + * + * @param source The source vertex + * @return A boost transformed range over the target vertices of source. */ [[nodiscard]] auto outVertices(Vertex source) const @@ -148,7 +152,8 @@ public: return boost::adaptors::transform(empty_, transform); } - /** Vertices and data associated with an Edge + /** + * Vertices and data associated with an Edge */ struct Edge { @@ -157,12 +162,13 @@ public: EdgeData data; }; - /** Range of out edges - - @param source The source vertex - @return A boost transformed range of Edge type for all out edges of - source. - */ + /** + * Range of out edges + * + * @param source The source vertex + * @return A boost transformed range of Edge type for all out edges of + * source. + */ [[nodiscard]] auto outEdges(Vertex source) const { @@ -177,11 +183,12 @@ public: return boost::adaptors::transform(empty_, transform); } - /** Vertex out-degree - - @param source The source vertex - @return The number of outgoing edges from source - */ + /** + * Vertex out-degree + * + * @param source The source vertex + * @return The number of outgoing edges from source + */ [[nodiscard]] std::size_t outDegree(Vertex source) const { @@ -191,14 +198,15 @@ public: return 0; } - /** Save GraphViz dot file - - Save a GraphViz dot description of the graph - @param fileName The output file (creates) - @param vertexName A invocable T vertexName(Vertex const &) that - returns the name target use for the vertex in the file - T must be ostream-able - */ + /** + * Save GraphViz dot file + * + * Save a GraphViz dot description of the graph + * @param fileName The output file (creates) + * @param vertexName A invocable T vertexName(Vertex const &) that + * returns the name target use for the vertex in the file + * T must be ostream-able + */ template void saveDot(std::ostream& out, VertexName&& vertexName) const diff --git a/src/test/csf/Histogram.h b/src/test/csf/Histogram.h index 60cb9a132e..62791c10f3 100644 --- a/src/test/csf/Histogram.h +++ b/src/test/csf/Histogram.h @@ -8,17 +8,16 @@ namespace xrpl::test::csf { -/** Basic histogram. - - Histogram for a type `T` that satisfies - - Default construction: T{} - - Comparison : T a, b; bool res = a < b - - Addition: T a, b; T c = a + b; - - Multiplication : T a, std::size_t b; T c = a * b; - - Division: T a; std::size_t b; T c = a/b; - - -*/ +/** + * Basic histogram. + * + * Histogram for a type `T` that satisfies + * - Default construction: T{} + * - Comparison : T a, b; bool res = a < b + * - Addition: T a, b; T c = a + b; + * - Multiplication : T a, std::size_t b; T c = a * b; + * - Division: T a; std::size_t b; T c = a/b; + */ template > class Histogram { @@ -28,7 +27,9 @@ class Histogram std::size_t samples_ = 0; public: - /** Insert an sample */ + /** + * Insert an sample + */ void insert(T const& s) { @@ -36,35 +37,45 @@ public: ++samples_; } - /** The number of samples */ + /** + * The number of samples + */ [[nodiscard]] std::size_t size() const { return samples_; } - /** The number of distinct samples (bins) */ + /** + * The number of distinct samples (bins) + */ [[nodiscard]] std::size_t numBins() const { return counts_.size(); } - /** Minimum observed value */ + /** + * Minimum observed value + */ [[nodiscard]] T minValue() const { return counts_.empty() ? T{} : counts_.begin()->first; } - /** Maximum observed value */ + /** + * Maximum observed value + */ [[nodiscard]] T maxValue() const { return counts_.empty() ? T{} : counts_.rbegin()->first; } - /** Histogram average */ + /** + * Histogram average + */ [[nodiscard]] T avg() const { @@ -80,12 +91,13 @@ public: return tmp / samples_; } - /** Calculate the given percentile of the distribution. - - @param p Percentile between 0 and 1, e.g. 0.50 is 50-th percentile - If the percentile falls between two bins, uses the nearest bin. - @return The given percentile of the distribution - */ + /** + * Calculate the given percentile of the distribution. + * + * @param p Percentile between 0 and 1, e.g. 0.50 is 50-th percentile + * If the percentile falls between two bins, uses the nearest bin. + * @return The given percentile of the distribution + */ [[nodiscard]] T percentile(float p) const { diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index 9d29704172..79bffec9cb 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -42,24 +42,26 @@ namespace xrpl::test::csf { namespace bc = boost::container; -/** A single peer in the simulation. - - This is the main work-horse of the consensus simulation framework and is - where many other components are integrated. The peer - - - Implements the Callbacks required by Consensus - - Manages trust & network connections with other peers - - Issues events back to the simulation based on its actions for analysis - by Collectors - - Exposes most internal state for forcibly simulating arbitrary scenarios -*/ +/** + * A single peer in the simulation. + * + * This is the main work-horse of the consensus simulation framework and is + * where many other components are integrated. The peer + * + * - Implements the Callbacks required by Consensus + * - Manages trust & network connections with other peers + * - Issues events back to the simulation based on its actions for analysis + * by Collectors + * - Exposes most internal state for forcibly simulating arbitrary scenarios + */ struct Peer { - /** Basic wrapper of a proposed position taken by a peer. - - For real consensus, this would add additional data for serialization - and signing. For simulation, nothing extra is needed. - */ + /** + * Basic wrapper of a proposed position taken by a peer. + * + * For real consensus, this would add additional data for serialization + * and signing. For simulation, nothing extra is needed. + */ class Position { public: @@ -89,16 +91,21 @@ struct Peer Proposal proposal_; }; - /** Simulated delays in internal peer processing. + /** + * Simulated delays in internal peer processing. */ struct ProcessingDelays { - //! Delay in consensus calling doAccept to accepting and issuing - //! validation - //! TODO: This should be a function of the number of transactions + /** + * Delay in consensus calling doAccept to accepting and issuing + * validation + * TODO: This should be a function of the number of transactions + */ std::chrono::milliseconds ledgerAccept{0}; - //! Delay in processing validations from remote peers + /** + * Delay in processing validations from remote peers + */ std::chrono::milliseconds recvValidation{0}; // Return the receive delay for message type M, default is no delay @@ -122,7 +129,8 @@ struct Peer { }; - /** Generic Validations adaptor that simply ignores recently stale + /** + * Generic Validations adaptor that simply ignores recently stale * validations */ class ValAdaptor @@ -165,7 +173,9 @@ struct Peer } }; - //! Type definitions for generic consensus + /** + * Type definitions for generic consensus + */ using Ledger_t = Ledger; using NodeID_t = PeerID; using NodeKey_t = PeerKey; @@ -174,74 +184,114 @@ struct Peer using Result = ConsensusResult; using NodeKey = Validation::NodeKey; - //! Logging support that prefixes messages with the peer ID + /** + * Logging support that prefixes messages with the peer ID + */ beast::WrappedSink sink; beast::Journal j; - //! Generic consensus + /** + * Generic consensus + */ Consensus consensus; - //! Our unique ID + /** + * Our unique ID + */ PeerID id; - //! Current signing key + /** + * Current signing key + */ PeerKey key; - //! The oracle that manages unique ledgers + /** + * The oracle that manages unique ledgers + */ LedgerOracle& oracle; - //! Scheduler of events + /** + * Scheduler of events + */ Scheduler& scheduler; - //! Handle to network for sending messages + /** + * Handle to network for sending messages + */ BasicNetwork& net; - //! Handle to Trust graph of network + /** + * Handle to Trust graph of network + */ TrustGraph& trustGraph; - //! openTxs that haven't been closed in a ledger yet + /** + * openTxs that haven't been closed in a ledger yet + */ TxSetType openTxs; - //! The last ledger closed by this node + /** + * The last ledger closed by this node + */ Ledger lastClosedLedger; - //! Ledgers this node has closed or loaded from the network + /** + * Ledgers this node has closed or loaded from the network + */ hash_map ledgers; - //! Validations from trusted nodes + /** + * Validations from trusted nodes + */ Validations validations; - //! The most recent ledger that has been fully validated by the network from - //! the perspective of this Peer + /** + * The most recent ledger that has been fully validated by the network from + * the perspective of this Peer + */ Ledger fullyValidatedLedger; //------------------------------------------------------------------------- // Store most network messages; these could be purged if memory use ever // becomes problematic - //! Map from Ledger::ID to vector of Positions with that ledger - //! as the prior ledger + /** + * Map from Ledger::ID to vector of Positions with that ledger + * as the prior ledger + */ bc::flat_map> peerPositions; - //! TxSet associated with a TxSet::ID + /** + * TxSet associated with a TxSet::ID + */ bc::flat_map txSets; // Ledgers/TxSets we are acquiring and when that request times out bc::flat_map acquiringLedgers; bc::flat_map acquiringTxSets; - //! The number of ledgers this peer has completed + /** + * The number of ledgers this peer has completed + */ int completedLedgers = 0; - //! The number of ledgers this peer should complete before stopping to run + /** + * The number of ledgers this peer should complete before stopping to run + */ int targetLedgers = std::numeric_limits::max(); - //! Skew of time relative to the common scheduler clock + /** + * Skew of time relative to the common scheduler clock + */ std::chrono::seconds clockSkew{0}; - //! Simulated delays to use for internal processing + /** + * Simulated delays to use for internal processing + */ ProcessingDelays delays; - //! Whether to simulate running as validator or a tracking node + /** + * Whether to simulate running as validator or a tracking node + */ bool runAsValidator = true; // TODO: Consider removing these two, they are only a convenience for tests @@ -259,20 +309,22 @@ struct Peer // Simulation parameters ConsensusParms consensusParms; - //! The collectors to report events to + /** + * The collectors to report events to + */ CollectorRefs& collectors; - /** Constructor - - @param i Unique PeerID - @param s Simulation Scheduler - @param o Simulation Oracle - @param n Simulation network - @param tg Simulation trust graph - @param c Simulation collectors - @param jIn Simulation journal - - */ + /** + * Constructor + * + * @param i Unique PeerID + * @param s Simulation Scheduler + * @param o Simulation Oracle + * @param n Simulation network + * @param tg Simulation trust graph + * @param c Simulation collectors + * @param jIn Simulation journal + */ Peer( PeerID i, Scheduler& s, @@ -302,9 +354,10 @@ struct Peer trustGraph.trust(this, this); } - /** Schedule the provided callback in `when` duration, but if - `when` is 0, call immediately - */ + /** + * Schedule the provided callback in `when` duration, but if + * `when` is 0, call immediately + */ template void schedule(std::chrono::nanoseconds when, T&& what) @@ -360,22 +413,19 @@ struct Peer bool trusts(PeerID const& oId) { - for (auto const p : trustGraph.trustedPeers(this)) - { - if (p->id == oId) - return true; - } - return false; + return std::ranges::any_of( + trustGraph.trustedPeers(this), [&oId](auto const p) { return p->id == oId; }); } - /** Create network connection - - Creates a new outbound connection to another Peer if none exists - - @param o The peer with the inbound connection - @param dur The fixed delay for messages between the two Peers - @return Whether the connection was created. - */ + /** + * Create network connection + * + * Creates a new outbound connection to another Peer if none exists + * + * @param o The peer with the inbound connection + * @param dur The fixed delay for messages between the two Peers + * @return Whether the connection was created. + */ bool connect(Peer& o, SimDuration dur) @@ -383,13 +433,14 @@ struct Peer return net.connect(this, &o, dur); } - /** Remove a network connection - - Removes a connection between peers if one exists - - @param o The peer we disconnect from - @return Whether the connection was removed - */ + /** + * Remove a network connection + * + * Removes a connection between peers if one exists + * + * @param o The peer we disconnect from + * @return Whether the connection was removed + */ bool disconnect(Peer& o) { @@ -653,7 +704,9 @@ struct Peer //-------------------------------------------------------------------------- // Validation members - /** Add a trusted validation and return true if it is worth forwarding */ + /** + * Add a trusted validation and return true if it is worth forwarding + */ bool addTrustedValidation(Validation v) { @@ -670,7 +723,9 @@ struct Peer return true; } - /** Check if a new ledger can be deemed fully validated */ + /** + * Check if a new ledger can be deemed fully validated + */ void checkFullyValidated(Ledger const& ledger) { @@ -872,7 +927,9 @@ struct Peer //-------------------------------------------------------------------------- // Simulation "driver" members - //! Heartbeat timer call + /** + * Heartbeat timer call + */ void timerEntry() { @@ -939,16 +996,17 @@ struct Peer // TODO: Make this more robust hash_map txInjections; - /** Inject non-consensus Tx - - Injects a transactionsinto the ledger following prevLedger's sequence - number. - - @param prevLedger The ledger we are building the new ledger on top of - @param src The Consensus TxSet - @return Consensus TxSet with inject transactions added if prevLedger.seq - matches a previously registered Tx. - */ + /** + * Inject non-consensus Tx + * + * Injects a transactionsinto the ledger following prevLedger's sequence + * number. + * + * @param prevLedger The ledger we are building the new ledger on top of + * @param src The Consensus TxSet + * @return Consensus TxSet with inject transactions added if prevLedger.seq + * matches a previously registered Tx. + */ TxSet injectTxs(Ledger prevLedger, TxSet const& src) { diff --git a/src/test/csf/PeerGroup.h b/src/test/csf/PeerGroup.h index e5efecac34..1c31209ef3 100644 --- a/src/test/csf/PeerGroup.h +++ b/src/test/csf/PeerGroup.h @@ -17,14 +17,15 @@ namespace xrpl::test::csf { -/** A group of simulation Peers - - A PeerGroup is a convenient handle for logically grouping peers together, - and then creating trust or network relations for the group at large. Peer - groups may also be combined to build out more complex structures. - - The PeerGroup provides random access style iterators and operator[] -*/ +/** + * A group of simulation Peers + * + * A PeerGroup is a convenient handle for logically grouping peers together, + * and then creating trust or network relations for the group at large. Peer + * groups may also be combined to build out more complex structures. + * + * The PeerGroup provides random access style iterators and operator[] + */ class PeerGroup { using peers_type = std::vector; @@ -102,12 +103,13 @@ public: return peers_.size(); } - /** Establish trust - - Establish trust from all peers in this group to all peers in o - - @param o The group of peers to trust - */ + /** + * Establish trust + * + * Establish trust from all peers in this group to all peers in o + * + * @param o The group of peers to trust + */ void trust(PeerGroup const& o) { @@ -120,12 +122,13 @@ public: } } - /** Revoke trust - - Revoke trust from all peers in this group to all peers in o - - @param o The group of peers to untrust - */ + /** + * Revoke trust + * + * Revoke trust from all peers in this group to all peers in o + * + * @param o The group of peers to untrust + */ void untrust(PeerGroup const& o) { @@ -138,16 +141,15 @@ public: } } - /** Establish network connection - - Establish outbound connections from all peers in this group to all peers - in o. If a connection already exists, no new connection is established. - - @param o The group of peers to connect to (will get inbound connections) - @param delay The fixed messaging delay for all established connections - - - */ + /** + * Establish network connection + * + * Establish outbound connections from all peers in this group to all peers + * in o. If a connection already exists, no new connection is established. + * + * @param o The group of peers to connect to (will get inbound connections) + * @param delay The fixed messaging delay for all established connections + */ void connect(PeerGroup const& o, SimDuration delay) { @@ -162,12 +164,13 @@ public: } } - /** Destroy network connection - - Destroy connections from all peers in this group to all peers in o - - @param o The group of peers to disconnect from - */ + /** + * Destroy network connection + * + * Destroy connections from all peers in this group to all peers in o + * + * @param o The group of peers to disconnect from + */ void disconnect(PeerGroup const& o) { @@ -180,14 +183,15 @@ public: } } - /** Establish trust and network connection - - Establish trust and create a network connection with fixed delay - from all peers in this group to all peers in o - - @param o The group of peers to trust and connect to - @param delay The fixed messaging delay for all established connections - */ + /** + * Establish trust and network connection + * + * Establish trust and create a network connection with fixed delay + * from all peers in this group to all peers in o + * + * @param o The group of peers to trust and connect to + * @param delay The fixed messaging delay for all established connections + */ void trustAndConnect(PeerGroup const& o, SimDuration delay) { @@ -195,15 +199,15 @@ public: connect(o, delay); } - /** Establish network connections based on trust relations - - For each peers in this group, create outbound network connection - to the set of peers it trusts. If a connection already exists, it is - not recreated. - - @param delay The fixed messaging delay for all established connections - - */ + /** + * Establish network connections based on trust relations + * + * For each peers in this group, create outbound network connection + * to the set of peers it trusts. If a connection already exists, it is + * not recreated. + * + * @param delay The fixed messaging delay for all established connections + */ void connectFromTrust(SimDuration delay) { @@ -253,25 +257,25 @@ public: } }; -/** Randomly generate peer groups according to ranks. - - Generates random peer groups based on a provided ranking of peers. This - mimics a process of randomly generating UNLs, where more "important" peers - are more likely to appear in a UNL. - - `numGroups` subgroups are generated by randomly sampling without without - replacement from peers according to the `ranks`. - - - - @param peers The group of peers - @param ranks The relative importance of each peer, must match the size of - peers. Higher relative rank means more likely to be sampled. - @param numGroups The number of peer link groups to generate - @param sizeDist The distribution that determines the size of a link group - @param g The uniform random bit generator - -*/ +/** + * Randomly generate peer groups according to ranks. + * + * Generates random peer groups based on a provided ranking of peers. This + * mimics a process of randomly generating UNLs, where more "important" peers + * are more likely to appear in a UNL. + * + * `numGroups` subgroups are generated by randomly sampling without without + * replacement from peers according to the `ranks`. + * + * + * + * @param peers The group of peers + * @param ranks The relative importance of each peer, must match the size of + * peers. Higher relative rank means more likely to be sampled. + * @param numGroups The number of peer link groups to generate + * @param sizeDist The distribution that determines the size of a link group + * @param g The uniform random bit generator + */ template std::vector randomRankedGroups( @@ -295,10 +299,11 @@ randomRankedGroups( return groups; } -/** Generate random trust groups based on peer rankings. - - @see randomRankedGroups for descriptions of the arguments -*/ +/** + * Generate random trust groups based on peer rankings. + * + * @see randomRankedGroups for descriptions of the arguments + */ template void randomRankedTrust( @@ -318,10 +323,11 @@ randomRankedTrust( } } -/** Generate random network groups based on peer rankings. - - @see randomRankedGroups for descriptions of the arguments -*/ +/** + * Generate random network groups based on peer rankings. + * + * @see randomRankedGroups for descriptions of the arguments + */ template void randomRankedConnect( diff --git a/src/test/csf/Proposal.h b/src/test/csf/Proposal.h index 06486daed3..ecf430ae8d 100644 --- a/src/test/csf/Proposal.h +++ b/src/test/csf/Proposal.h @@ -7,9 +7,10 @@ #include namespace xrpl::test::csf { -/** Proposal is a position taken in the consensus process and is represented - directly from the generic types. -*/ +/** + * Proposal is a position taken in the consensus process and is represented + * directly from the generic types. + */ using Proposal = ConsensusProposal; } // namespace xrpl::test::csf diff --git a/src/test/csf/Scheduler.h b/src/test/csf/Scheduler.h index 3fc187e1e7..b1ac2bb5d4 100644 --- a/src/test/csf/Scheduler.h +++ b/src/test/csf/Scheduler.h @@ -12,17 +12,18 @@ namespace xrpl::test::csf { -/** Simulated discrete-event scheduler. - - Simulates the behavior of events using a single common clock. - - An event is modeled using a lambda function and is scheduled to occur at a - specific time. Events may be canceled using a token returned when the - event is scheduled. - - The caller uses one or more of the step, stepOne, stepFor, stepUntil and - stepWhile functions to process scheduled events. -*/ +/** + * Simulated discrete-event scheduler. + * + * Simulates the behavior of events using a single common clock. + * + * An event is modeled using a lambda function and is scheduled to occur at a + * specific time. Events may be canceled using a token returned when the + * event is scheduled. + * + * The caller uses one or more of the step, stepOne, stepFor, stepUntil and + * stepWhile functions to process scheduled events. + */ class Scheduler { public: @@ -135,116 +136,127 @@ public: Scheduler(); - /** Return the clock. (aged_containers want a non-const ref =( */ + /** + * Return the clock. (aged_containers want a non-const ref =( + */ clock_type& clock() const; - /** Return the current network time. - - @note The epoch is unspecified - */ + /** + * Return the current network time. + * + * @note The epoch is unspecified + */ time_point now() const; // Used to cancel timers struct CancelToken; - /** Schedule an event at a specific time - - Effects: - - When the network time is reached, - the function will be called with - no arguments. - */ + /** + * Schedule an event at a specific time + * + * Effects: + * + * When the network time is reached, + * the function will be called with + * no arguments. + */ template CancelToken at(time_point const& when, Function&& f); - /** Schedule an event after a specified duration passes - - Effects: - - When the specified time has elapsed, - the function will be called with - no arguments. - */ + /** + * Schedule an event after a specified duration passes + * + * Effects: + * + * When the specified time has elapsed, + * the function will be called with + * no arguments. + */ template CancelToken in(duration const& delay, Function&& f); - /** Cancel a timer. - - Preconditions: - - `token` was the return value of a call - timer() which has not yet been invoked. - */ + /** + * Cancel a timer. + * + * Preconditions: + * + * `token` was the return value of a call + * timer() which has not yet been invoked. + */ void cancel(CancelToken const& token); - /** Run the scheduler for up to one event. - - Effects: - - The clock is advanced to the time - of the last delivered event. - - @return `true` if an event was processed. - */ + /** + * Run the scheduler for up to one event. + * + * Effects: + * + * The clock is advanced to the time + * of the last delivered event. + * + * @return `true` if an event was processed. + */ bool stepOne(); - /** Run the scheduler until no events remain. - - Effects: - - The clock is advanced to the time - of the last event. - - @return `true` if an event was processed. - */ + /** + * Run the scheduler until no events remain. + * + * Effects: + * + * The clock is advanced to the time + * of the last event. + * + * @return `true` if an event was processed. + */ bool step(); - /** Run the scheduler while a condition is true. - - Function takes no arguments and will be called - repeatedly after each event is processed to - decide whether to continue. - - Effects: - - The clock is advanced to the time - of the last delivered event. - - @return `true` if any event was processed. - */ + /** + * Run the scheduler while a condition is true. + * + * Function takes no arguments and will be called + * repeatedly after each event is processed to + * decide whether to continue. + * + * Effects: + * + * The clock is advanced to the time + * of the last delivered event. + * + * @return `true` if any event was processed. + */ template bool stepWhile(Function&& func); - /** Run the scheduler until the specified time. - - Effects: - - The clock is advanced to the - specified time. - - @return `true` if any event remain. - */ + /** + * Run the scheduler until the specified time. + * + * Effects: + * + * The clock is advanced to the + * specified time. + * + * @return `true` if any event remain. + */ bool stepUntil(time_point const& until); - /** Run the scheduler until time has elapsed. - - Effects: - - The clock is advanced by the - specified duration. - - @return `true` if any event remain. - */ + /** + * Run the scheduler until time has elapsed. + * + * Effects: + * + * The clock is advanced by the + * specified duration. + * + * @return `true` if any event remain. + */ template bool stepFor(std::chrono::duration const& amount); diff --git a/src/test/csf/Sim.h b/src/test/csf/Sim.h index d2a63f6507..94d26d5e06 100644 --- a/src/test/csf/Sim.h +++ b/src/test/csf/Sim.h @@ -21,7 +21,9 @@ namespace xrpl::test::csf { -/** Sink that prepends simulation time to messages */ +/** + * Sink that prepends simulation time to messages + */ class BasicSink : public beast::Journal::Sink { Scheduler::clock_type const& clock_; @@ -65,28 +67,29 @@ public: TrustGraph trustGraph; CollectorRefs collectors; - /** Create a simulation - - Creates a new simulation. The simulation has no peers, no trust links - and no network connections. - - */ + /** + * Create a simulation + * + * Creates a new simulation. The simulation has no peers, no trust links + * and no network connections. + */ // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test Sim() : sink{scheduler.clock()}, j{sink}, net{scheduler} { } - /** Create a new group of peers. - - Creates a new group of peers. The peers do not have any trust relations - or network connections by default. Those must be configured by the - client. - - @param numPeers The number of peers in the group - @return PeerGroup representing these new peers - - @note This increases the number of peers in the simulation by numPeers. - */ + /** + * Create a new group of peers. + * + * Creates a new group of peers. The peers do not have any trust relations + * or network connections by default. Those must be configured by the + * client. + * + * @param numPeers The number of peers in the group + * @return PeerGroup representing these new peers + * + * @note This increases the number of peers in the simulation by numPeers. + */ PeerGroup createGroup(std::size_t numPeers) { @@ -109,48 +112,57 @@ public: return res; } - //! The number of peers in the simulation + /** + * The number of peers in the simulation + */ std::size_t size() const { return peers_.size(); } - /** Run consensus protocol to generate the provided number of ledgers. - - Has each peer run consensus until it closes `ledgers` more ledgers. - - @param ledgers The number of additional ledgers to close - */ + /** + * Run consensus protocol to generate the provided number of ledgers. + * + * Has each peer run consensus until it closes `ledgers` more ledgers. + * + * @param ledgers The number of additional ledgers to close + */ void run(int ledgers); - /** Run consensus for the given duration */ + /** + * Run consensus for the given duration + */ void run(SimDuration const& dur); - /** Check whether all peers in the group are synchronized. - - Nodes in the group are synchronized if they share the same last - fully validated and last generated ledger. - */ + /** + * Check whether all peers in the group are synchronized. + * + * Nodes in the group are synchronized if they share the same last + * fully validated and last generated ledger. + */ static bool synchronized(PeerGroup const& g); - /** Check whether all peers in the network are synchronized + /** + * Check whether all peers in the network are synchronized */ bool synchronized() const; - /** Calculate the number of branches in the group. - - A branch occurs if two nodes in the group have fullyValidatedLedgers - that are not on the same chain of ledgers. - */ + /** + * Calculate the number of branches in the group. + * + * A branch occurs if two nodes in the group have fullyValidatedLedgers + * that are not on the same chain of ledgers. + */ std::size_t branches(PeerGroup const& g) const; - /** Calculate the number of branches in the network + /** + * Calculate the number of branches in the network */ std::size_t branches() const; diff --git a/src/test/csf/TrustGraph.h b/src/test/csf/TrustGraph.h index a10e318706..d46a887364 100644 --- a/src/test/csf/TrustGraph.h +++ b/src/test/csf/TrustGraph.h @@ -10,13 +10,14 @@ namespace xrpl::test::csf { -/** Trust graph - - Trust is a directed relationship from a node i to node j. - If node i trusts node j, then node i has node j in its UNL. - This class wraps a digraph representing the trust relationships for all - peers in the simulation. -*/ +/** + * Trust graph + * + * Trust is a directed relationship from a node i to node j. + * If node i trusts node j, then node i has node j in its UNL. + * This class wraps a digraph representing the trust relationships for all + * peers in the simulation. + */ template class TrustGraph { @@ -25,7 +26,8 @@ class TrustGraph Graph graph_; public: - /** Create an empty trust graph + /** + * Create an empty trust graph */ TrustGraph() = default; @@ -35,29 +37,30 @@ public: return graph_; } - /** Create trust - - Establish trust between Peer `from` and Peer `to`; as if `from` put `to` - in its UNL. - - @param from The peer granting trust - @param to The peer receiving trust - - */ + /** + * Create trust + * + * Establish trust between Peer `from` and Peer `to`; as if `from` put `to` + * in its UNL. + * + * @param from The peer granting trust + * @param to The peer receiving trust + */ void trust(Peer const& from, Peer const& to) { graph_.connect(from, to); } - /** Remove trust - - Revoke trust from Peer `from` to Peer `to`; as if `from` removed `to` - from its UNL. - - @param from The peer revoking trust - @param to The peer being revoked - */ + /** + * Remove trust + * + * Revoke trust from Peer `from` to Peer `to`; as if `from` removed `to` + * from its UNL. + * + * @param from The peer revoking trust + * @param to The peer being revoked + */ void untrust(Peer const& from, Peer const& to) { @@ -71,19 +74,21 @@ public: return graph_.connected(from, to); } - /** Range over trusted peers - - @param a The node granting trust - @return boost transformed range over nodes `a` trusts, i.e. the nodes - in its UNL - */ + /** + * Range over trusted peers + * + * @param a The node granting trust + * @return boost transformed range over nodes `a` trusts, i.e. the nodes + * in its UNL + */ [[nodiscard]] auto trustedPeers(Peer const& a) const { return graph_.outVertices(a); } - /** An example of nodes that fail the whitepaper no-forking condition + /** + * An example of nodes that fail the whitepaper no-forking condition */ struct ForkInfo { @@ -133,9 +138,10 @@ public: return res; } - /** Check whether this trust graph satisfies the whitepaper no-forking - condition - */ + /** + * Check whether this trust graph satisfies the whitepaper no-forking + * condition + */ [[nodiscard]] bool canFork(double quorum) const { diff --git a/src/test/csf/Tx.h b/src/test/csf/Tx.h index 75254f865a..0412d346f3 100644 --- a/src/test/csf/Tx.h +++ b/src/test/csf/Tx.h @@ -17,7 +17,9 @@ namespace xrpl::test::csf { -//! A single transaction +/** + * A single transaction + */ class Tx { public: @@ -56,11 +58,15 @@ private: ID id_; }; -//!------------------------------------------------------------------------- -//! All sets of Tx are represented as a flat_set for performance. +/** + * ------------------------------------------------------------------------- + * All sets of Tx are represented as a flat_set for performance. + */ using TxSetType = boost::container::flat_set; -//! TxSet is a set of transactions to consider including in the ledger +/** + * TxSet is a set of transactions to consider including in the ledger + */ class TxSet { public: @@ -135,10 +141,11 @@ public: return id_; } - /** @return Map of Tx::ID that are missing. True means - it was in this set and not other. False means - it was in the other set and not this - */ + /** + * @return Map of Tx::ID that are missing. True means + * it was in this set and not other. False means + * it was in the other set and not this + */ [[nodiscard]] std::map compare(TxSet const& other) const { @@ -160,10 +167,14 @@ public: } private: - //! The set contains the actual transactions + /** + * The set contains the actual transactions + */ TxSetType txs_; - //! The unique ID of this tx set + /** + * The unique ID of this tx set + */ ID id_{}; }; diff --git a/src/test/csf/Validation.h b/src/test/csf/Validation.h index 73b6784534..0b9fc94890 100644 --- a/src/test/csf/Validation.h +++ b/src/test/csf/Validation.h @@ -16,15 +16,17 @@ struct PeerIDTag; //< Uniquely identifies a peer using PeerID = TaggedInteger; -/** The current key of a peer - - Eventually, the second entry in the pair can be used to model ephemeral - keys. Right now, the convention is to have the second entry 0 as the - master key. -*/ +/** + * The current key of a peer + * + * Eventually, the second entry in the pair can be used to model ephemeral + * keys. Right now, the convention is to have the second entry 0 as the + * master key. + */ using PeerKey = std::pair; -/** Validation of a specific ledger by a specific Peer. +/** + * Validation of a specific ledger by a specific Peer. */ class Validation { diff --git a/src/test/csf/collectors.h b/src/test/csf/collectors.h index 1100f6c690..f85854e5dd 100644 --- a/src/test/csf/collectors.h +++ b/src/test/csf/collectors.h @@ -32,12 +32,13 @@ namespace xrpl::test::csf { // This file contains helper functions for composing different collectors // and also defines several standard collectors available for simulations. -/** Group of collectors. - - Presents a group of collectors as a single collector which process an event - by calling each collector sequentially. This is analogous to CollectorRefs - in CollectorRef.h, but does *not* erase the type information of the combined - collectors. +/** + * Group of collectors. + * + * Presents a group of collectors as a single collector which process an event + * by calling each collector sequentially. This is analogous to CollectorRefs + * in CollectorRef.h, but does *not* erase the type information of the combined + * collectors. */ template class Collectors @@ -59,10 +60,11 @@ class Collectors } public: - /** Constructor - - @param cs References to the collectors to call together - */ + /** + * Constructor + * + * @param cs References to the collectors to call together + */ Collectors(Cs&... cs) : cs_(std::tie(cs...)) { } @@ -75,7 +77,9 @@ public: } }; -/** Create an instance of Collectors */ +/** + * Create an instance of Collectors + */ template Collectors makeCollectors(Cs&... cs) @@ -83,14 +87,15 @@ makeCollectors(Cs&... cs) return Collectors(cs...); } -/** Maintain an instance of a Collector per peer - - For each peer that emits events, this class maintains a corresponding - instance of CollectorType, only forwarding events emitted by the peer to - the related instance. - - CollectorType should be default constructible. -*/ +/** + * Maintain an instance of a Collector per peer + * + * For each peer that emits events, this class maintains a corresponding + * instance of CollectorType, only forwarding events emitted by the peer to + * the related instance. + * + * CollectorType should be default constructible. + */ template struct CollectByNode { @@ -115,7 +120,9 @@ struct CollectByNode } }; -/** Collector which ignores all events */ +/** + * Collector which ignores all events + */ struct NullCollector { template @@ -125,7 +132,9 @@ struct NullCollector } }; -/** Tracks the overall duration of a simulation */ +/** + * Tracks the overall duration of a simulation + */ struct SimDurationCollector { bool init = false; @@ -148,15 +157,16 @@ struct SimDurationCollector } }; -/** Tracks the submission -> accepted -> validated evolution of transactions. - - This collector tracks transactions through the network by monitoring the - *first* time the transaction is seen by any node in the network, or - seen by any node's accepted or fully validated ledger. - - If transactions submitted to the network do not have unique IDs, this - collector will not track subsequent submissions. -*/ +/** + * Tracks the submission -> accepted -> validated evolution of transactions. + * + * This collector tracks transactions through the network by monitoring the + * *first* time the transaction is seen by any node in the network, or + * seen by any node's accepted or fully validated ledger. + * + * If transactions submitted to the network do not have unique IDs, this + * collector will not track subsequent submissions. + */ struct TxCollector { // Counts @@ -381,12 +391,12 @@ struct TxCollector } }; -/** Tracks the accepted -> validated evolution of ledgers. - - This collector tracks ledgers through the network by monitoring the - *first* time the ledger is accepted or fully validated by ANY node. - -*/ +/** + * Tracks the accepted -> validated evolution of ledgers. + * + * This collector tracks ledgers through the network by monitoring the + * *first* time the ledger is accepted or fully validated by ANY node. + */ struct LedgerCollector { std::size_t accepted{0}; @@ -580,11 +590,12 @@ struct LedgerCollector } }; -/** Write out stream of ledger activity - - Writes information about every accepted and fully-validated ledger to a - provided std::ostream. -*/ +/** + * Write out stream of ledger activity + * + * Writes information about every accepted and fully-validated ledger to a + * provided std::ostream. + */ struct StreamCollector { std::ostream& out; @@ -611,11 +622,12 @@ struct StreamCollector } }; -/** Saves information about Jumps for closed and fully validated ledgers. A - jump occurs when a node closes/fully validates a new ledger that is not the - immediate child of the prior closed/fully validated ledgers. This includes - jumps across branches and jumps ahead in the same branch of ledger history. -*/ +/** + * Saves information about Jumps for closed and fully validated ledgers. A + * jump occurs when a node closes/fully validates a new ledger that is not the + * immediate child of the prior closed/fully validated ledgers. This includes + * jumps across branches and jumps ahead in the same branch of ledger history. + */ struct JumpCollector { struct Jump diff --git a/src/test/csf/events.h b/src/test/csf/events.h index dfedd2627a..2cf4fd9e9b 100644 --- a/src/test/csf/events.h +++ b/src/test/csf/events.h @@ -29,58 +29,81 @@ namespace xrpl::test::csf { // CollectorRef.f defines a type-erased holder for arbitrary Collectors. If // any new events are added, the interface there needs to be updated. -/** A value to be flooded to all other peers starting from this peer. +/** + * A value to be flooded to all other peers starting from this peer. */ template struct Share { - //! Event that is shared + /** + * Event that is shared + */ V val; }; -/** A value relayed to another peer as part of flooding +/** + * A value relayed to another peer as part of flooding */ template struct Relay { - //! Peer relaying to + /** + * Peer relaying to + */ PeerID to; - //! The value to relay + /** + * The value to relay + */ V val; }; -/** A value received from another peer as part of flooding +/** + * A value received from another peer as part of flooding */ template struct Receive { - //! Peer that sent the value + /** + * Peer that sent the value + */ PeerID from; - //! The received value + /** + * The received value + */ V val; }; -/** A transaction submitted to a peer */ +/** + * A transaction submitted to a peer + */ struct SubmitTx { - //! The submitted transaction + /** + * The submitted transaction + */ Tx tx; }; -/** Peer starts a new consensus round +/** + * Peer starts a new consensus round */ struct StartRound { - //! The preferred ledger for the start of consensus + /** + * The preferred ledger for the start of consensus + */ Ledger::ID bestLedger{}; - //! The prior ledger on hand + /** + * The prior ledger on hand + */ Ledger prevLedger; }; -/** Peer closed the open ledger +/** + * Peer closed the open ledger */ struct CloseLedger { @@ -91,7 +114,9 @@ struct CloseLedger TxSetType txs; }; -//! Peer accepted consensus results +/** + * Peer accepted consensus results + */ struct AcceptLedger { // The newly created ledger @@ -101,7 +126,9 @@ struct AcceptLedger Ledger prior; }; -//! Peer detected a wrong prior ledger during consensus +/** + * Peer detected a wrong prior ledger during consensus + */ struct WrongPrevLedger { // ID of wrong ledger we had @@ -110,14 +137,20 @@ struct WrongPrevLedger Ledger::ID right; }; -//! Peer fully validated a new ledger +/** + * Peer fully validated a new ledger + */ struct FullyValidateLedger { - //! The new fully validated ledger + /** + * The new fully validated ledger + */ Ledger ledger; - //! The prior fully validated ledger - //! This is a jump if prior.id() != ledger.parentID() + /** + * The prior fully validated ledger + * This is a jump if prior.id() != ledger.parentID() + */ Ledger prior; }; diff --git a/src/test/csf/ledgers.h b/src/test/csf/ledgers.h index 4873519dee..09f5fa54de 100644 --- a/src/test/csf/ledgers.h +++ b/src/test/csf/ledgers.h @@ -22,27 +22,28 @@ namespace xrpl::test::csf { -/** A ledger is a set of observed transactions and a sequence number - identifying the ledger. - - Peers in the consensus process are trying to agree on a set of transactions - to include in a ledger. For simulation, each transaction is a single - integer and the ledger is the set of observed integers. This means future - ledgers have prior ledgers as subsets, e.g. - - Ledger 0 : {} - Ledger 1 : {1,4,5} - Ledger 2 : {1,2,4,5,10} - .... - - Ledgers are immutable value types. All ledgers with the same sequence - number, transactions, close time, etc. will have the same ledger ID. The - LedgerOracle class below manages ID assignments for a simulation and is the - only way to close and create a new ledger. Since the parent ledger ID is - part of type, this also means ledgers with distinct histories will have - distinct ids, even if they have the same set of transactions, sequence - number and close time. -*/ +/** + * A ledger is a set of observed transactions and a sequence number + * identifying the ledger. + * + * Peers in the consensus process are trying to agree on a set of transactions + * to include in a ledger. For simulation, each transaction is a single + * integer and the ledger is the set of observed integers. This means future + * ledgers have prior ledgers as subsets, e.g. + * + * Ledger 0 : {} + * Ledger 1 : {1,4,5} + * Ledger 2 : {1,2,4,5,10} + * .... + * + * Ledgers are immutable value types. All ledgers with the same sequence + * number, transactions, close time, etc. will have the same ledger ID. The + * LedgerOracle class below manages ID assignments for a simulation and is the + * only way to close and create a new ledger. Since the parent ledger ID is + * part of type, this also means ledgers with distinct histories will have + * distinct ids, even if they have the same set of transactions, sequence + * number and close time. + */ class Ledger { friend class LedgerOracle; @@ -74,21 +75,31 @@ private: // Resolution used to determine close time NetClock::duration closeTimeResolution = kLedgerDefaultTimeResolution; - //! When the ledger closed (up to closeTimeResolution) + /** + * When the ledger closed (up to closeTimeResolution) + */ NetClock::time_point closeTime; - //! Whether consensus agreed on the close time + /** + * Whether consensus agreed on the close time + */ bool closeTimeAgree = true; - //! Parent ledger id + /** + * Parent ledger id + */ ID parentID{0}; - //! Parent ledger close time + /** + * Parent ledger close time + */ NetClock::time_point parentCloseTime; - //! IDs of this ledgers ancestors. Since each ledger already has unique - //! ancestors based on the parentID, this member is not needed for any - //! of the operators below. + /** + * IDs of this ledgers ancestors. Since each ledger already has unique + * ancestors based on the parentID, this member is not needed for any + * of the operators below. + */ std::vector ancestors; [[nodiscard]] auto @@ -199,16 +210,20 @@ public: return instance_->txs; } - /** Determine whether ancestor is really an ancestor of this ledger */ + /** + * Determine whether ancestor is really an ancestor of this ledger + */ [[nodiscard]] bool isAncestor(Ledger const& ancestor) const; - /** Return the id of the ancestor with the given seq (if exists/known) + /** + * Return the id of the ancestor with the given seq (if exists/known) */ ID operator[](Seq seq) const; - /** Return the sequence number of the first mismatching ancestor + /** + * Return the sequence number of the first mismatching ancestor */ friend Ledger::Seq mismatch(Ledger const& a, Ledger const& o); @@ -227,7 +242,8 @@ private: Instance const* instance_; }; -/** Oracle maintaining unique ledgers for a simulation. +/** + * Oracle maintaining unique ledgers for a simulation. */ class LedgerOracle { @@ -246,18 +262,21 @@ class LedgerOracle public: LedgerOracle(); - /** Find the ledger with the given ID */ + /** + * Find the ledger with the given ID + */ [[nodiscard]] std::optional lookup(Ledger::ID const& id) const; - /** Accept the given txs and generate a new ledger - - @param curr The current ledger - @param txs The transactions to apply to the current ledger - @param closeTimeResolution Resolution used in determining close time - @param consensusCloseTime The consensus agreed close time, no valid time - if 0 - */ + /** + * Accept the given txs and generate a new ledger + * + * @param curr The current ledger + * @param txs The transactions to apply to the current ledger + * @param closeTimeResolution Resolution used in determining close time + * @param consensusCloseTime The consensus agreed close time, no valid time + * if 0 + */ Ledger accept( Ledger const& curr, @@ -272,38 +291,39 @@ public: return accept(curr, TxSetType{tx}, curr.closeTimeResolution(), curr.closeTime() + 1s); } - /** Determine the number of distinct branches for the set of ledgers. - - Ledgers A and B are on different branches if A != B, A is not an - ancestor of B and B is not an ancestor of A, e.g. - - /--> A - O - \--> B - */ + /** + * Determine the number of distinct branches for the set of ledgers. + * + * Ledgers A and B are on different branches if A != B, A is not an + * ancestor of B and B is not an ancestor of A, e.g. + * + * /--> A + * O + * \--> B + */ static std::size_t branches(std::set const& ledgers); }; -/** Helper for writing unit tests with controlled ledger histories. - - This class allows clients to refer to distinct ledgers as strings, where - each character in the string indicates a unique ledger. It enforces the - uniqueness at runtime, but this simplifies creation of alternate ledger - histories, e.g. - - HistoryHelper hh; - hh["a"] - hh["ab"] - hh["ac"] - hh["abd"] - - Creates a history like - b - d - / - a - c - -*/ +/** + * Helper for writing unit tests with controlled ledger histories. + * + * This class allows clients to refer to distinct ledgers as strings, where + * each character in the string indicates a unique ledger. It enforces the + * uniqueness at runtime, but this simplifies creation of alternate ledger + * histories, e.g. + * + * HistoryHelper hh; + * hh["a"] + * hh["ab"] + * hh["ac"] + * hh["abd"] + * + * Creates a history like + * b - d + * / + * a - c + */ struct LedgerHistoryHelper { LedgerOracle oracle; @@ -316,11 +336,12 @@ struct LedgerHistoryHelper ledgers[""] = Ledger{Ledger::MakeGenesis{}}; } - /** Get or create the ledger with the given string history. - - Creates any necessary intermediate ledgers, but asserts if - a letter is re-used (e.g. "abc" then "adc" would assert) - */ + /** + * Get or create the ledger with the given string history. + * + * Creates any necessary intermediate ledgers, but asserts if + * a letter is re-used (e.g. "abc" then "adc" would assert) + */ Ledger const& operator[](std::string const& s) { diff --git a/src/test/csf/random.h b/src/test/csf/random.h index 9fc51f1217..f8df253642 100644 --- a/src/test/csf/random.h +++ b/src/test/csf/random.h @@ -8,15 +8,16 @@ namespace xrpl::test::csf { -/** Return a randomly shuffled copy of vector based on weights w. - - @param v The set of values - @param w The set of weights of each value - @param g A pseudo-random number generator - @return A vector with entries randomly sampled without replacement - from the original vector based on the provided weights. - I.e. res[0] comes from sample v[i] with weight w[i]/suk_ w[k] -*/ +/** + * Return a randomly shuffled copy of vector based on weights w. + * + * @param v The set of values + * @param w The set of weights of each value + * @param g A pseudo-random number generator + * @return A vector with entries randomly sampled without replacement + * from the original vector based on the provided weights. + * I.e. res[0] comes from sample v[i] with weight w[i]/suk_ w[k] + */ template std::vector randomWeightedShuffle(std::vector v, std::vector w, G& g) @@ -34,14 +35,15 @@ randomWeightedShuffle(std::vector v, std::vector w, G& g) return v; } -/** Generate a vector of random samples - - @param size the size of the sample - @param dist the distribution to sample - @param g the pseudo-random number generator - - @return vector of samples -*/ +/** + * Generate a vector of random samples + * + * @param size the size of the sample + * @param dist the distribution to sample + * @param g the pseudo-random number generator + * + * @return vector of samples + */ template std::vector sample(std::size_t size, RandomNumberDistribution dist, Generator& g) @@ -51,13 +53,14 @@ sample(std::size_t size, RandomNumberDistribution dist, Generator& g) return res; } -/** Invocable that returns random samples from a range according to a discrete - distribution - - Given a pair of random access iterators begin and end, each call to the - instance of Selector returns a random entry in the range (begin,end) - according to the weights provided at construction. -*/ +/** + * Invocable that returns random samples from a range according to a discrete + * distribution + * + * Given a pair of random access iterators begin and end, each call to the + * instance of Selector returns a random entry in the range (begin,end) + * according to the weights provided at construction. + */ template class Selector { @@ -66,12 +69,13 @@ class Selector Generator g_; public: - /** Constructor - @param first Random access iterator to the start of the range - @param last Random access iterator to the end of the range - @param w Vector of weights of size list-first - @param g the pseudo-random number generator - */ + /** + * Constructor + * @param first Random access iterator to the start of the range + * @param last Random access iterator to the end of the range + * @param w Vector of weights of size list-first + * @param g the pseudo-random number generator + */ Selector(RAIter first, RAIter last, std::vector const& w, Generator& g) : first_{first}, last_{last}, dd_{w.begin(), w.end()}, g_{g} { @@ -100,7 +104,8 @@ makeSelector(Iter first, Iter last, std::vector const& w, Generator& g) //------------------------------------------------------------------------------ // Additional distributions of interest not defined in -/** Constant "distribution" that always returns the same value +/** + * Constant "distribution" that always returns the same value */ class ConstantDistribution { @@ -119,11 +124,12 @@ public: } }; -/** Power-law distribution with PDF - - P(x) = (x/xmin)^-a - - for a >= 1 and xmin >= 1 +/** + * Power-law distribution with PDF + * + * P(x) = (x/xmin)^-a + * + * for a >= 1 and xmin >= 1 */ class PowerLawDistribution { @@ -135,9 +141,8 @@ class PowerLawDistribution public: using result_type = double; - PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a} + PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a}, inv_(1.0 / (1.0 - a_)) { - inv_ = 1.0 / (1.0 - a_); } template diff --git a/src/test/csf/submitters.h b/src/test/csf/submitters.h index a71c849fb7..160d0bcd9f 100644 --- a/src/test/csf/submitters.h +++ b/src/test/csf/submitters.h @@ -13,7 +13,9 @@ namespace xrpl::test::csf { // Submitters are classes for simulating submission of transactions to the // network -/** Represents rate as a count/duration */ +/** + * Represents rate as a count/duration + */ struct Rate { std::size_t count; @@ -26,23 +28,24 @@ struct Rate } }; -/** Submits transactions to a specified peer - - Submits successive transactions beginning at start, then spaced according - to successive calls of distribution(), until stop. - - @tparam Distribution is a `UniformRandomBitGenerator` from the STL that - is used by random distributions to generate random samples - @tparam Generator is an object with member - - T operator()(Generator &g) - - which generates the delay T in SimDuration units to the next - transaction. For the current definition of SimDuration, this is - currently the number of nanoseconds. Submitter internally casts - arithmetic T to SimDuration::rep units to allow using standard - library distributions as a Distribution. -*/ +/** + * Submits transactions to a specified peer + * + * Submits successive transactions beginning at start, then spaced according + * to successive calls of distribution(), until stop. + * + * @tparam Distribution is a `UniformRandomBitGenerator` from the STL that + * is used by random distributions to generate random samples + * @tparam Generator is an object with member + * + * T operator()(Generator &g) + * + * which generates the delay T in SimDuration units to the next + * transaction. For the current definition of SimDuration, this is + * currently the number of nanoseconds. Submitter internally casts + * arithmetic T to SimDuration::rep units to allow using standard + * library distributions as a Distribution. + */ template class Submitter { diff --git a/src/test/csf/timers.h b/src/test/csf/timers.h index 2f86fe7729..4f13b21b25 100644 --- a/src/test/csf/timers.h +++ b/src/test/csf/timers.h @@ -12,7 +12,8 @@ namespace xrpl::test::csf { // Timers are classes that schedule repeated events and are mostly independent // of simulation-specific details. -/** Gives heartbeat of simulation to signal simulation progression +/** + * Gives heartbeat of simulation to signal simulation progression */ class HeartbeatTimer { diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 605f15f812..68b6d9f745 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -131,7 +131,8 @@ struct ClawbackArg std::optional err = std::nullopt; }; -/** Convenience class to test AMM functionality. +/** + * Convenience class to test AMM functionality. */ class AMM { @@ -187,7 +188,8 @@ public: STAmount const& asset2, std::uint16_t const& tfee); - /** Send amm_info RPC command + /** + * Send amm_info RPC command */ [[nodiscard]] json::Value ammRpcInfo( @@ -209,7 +211,8 @@ public: bool ignoreParams, unsigned apiVersion) const; - /** Verify the AMM balances. + /** + * Verify the AMM balances. */ [[nodiscard]] bool expectBalances( @@ -218,7 +221,8 @@ public: IOUAmount const& lpt, std::optional const& account = std::nullopt) const; - /** Get AMM balances for the token pair. + /** + * Get AMM balances for the token pair. */ [[nodiscard]] std::tuple balances( diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index 971cc5db84..9385313225 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -103,7 +103,8 @@ public: } protected: - /** testAMM() funds 30,000XRP and 30,000IOU + /** + * testAMM() funds 30,000XRP and 30,000IOU * for each non-XRP asset to Alice and Carol */ void diff --git a/src/test/jtx/AbstractClient.h b/src/test/jtx/AbstractClient.h index 7c7107ca79..f9d8de0768 100644 --- a/src/test/jtx/AbstractClient.h +++ b/src/test/jtx/AbstractClient.h @@ -20,21 +20,24 @@ public: AbstractClient& operator=(AbstractClient const&) = delete; - /** Submit a command synchronously. - - The arguments to the function and the returned JSON - are in a normalized format, the same whether the client - is using the JSON-RPC over HTTP/S or WebSocket transport. - - @param cmd The command to execute - @param params json::Value of null or object type - with zero or more key/value pairs. - @return The server response in normalized format. - */ + /** + * Submit a command synchronously. + * + * The arguments to the function and the returned JSON + * are in a normalized format, the same whether the client + * is using the JSON-RPC over HTTP/S or WebSocket transport. + * + * @param cmd The command to execute + * @param params json::Value of null or object type + * with zero or more key/value pairs. + * @return The server response in normalized format. + */ virtual json::Value invoke(std::string const& cmd, json::Value const& params = {}) = 0; - /// Get RPC 1.0 or RPC 2.0 + /** + * Get RPC 1.0 or RPC 2.0 + */ [[nodiscard]] virtual unsigned version() const = 0; }; diff --git a/src/test/jtx/Account.h b/src/test/jtx/Account.h index 264a39f08b..60d07beebd 100644 --- a/src/test/jtx/Account.h +++ b/src/test/jtx/Account.h @@ -14,7 +14,9 @@ namespace xrpl::test::jtx { class IOU; -/** Immutable cryptographic account descriptor. */ +/** + * Immutable cryptographic account descriptor. + */ class Account { private: @@ -24,7 +26,9 @@ private: }; public: - /** The master account. */ + /** + * The master account. + */ static Account const kMaster; Account() = delete; @@ -35,7 +39,9 @@ public: Account& operator=(Account&&) = default; - /** Create an account from a simple string name. */ + /** + * Create an account from a simple string name. + */ /** @{ */ Account(std::string name, KeyType type = KeyType::Secp256k1); @@ -50,63 +56,79 @@ public: /** @} */ - /** Create an Account from an account ID. Should only be used when the - * secret key is unavailable, such as for pseudo-accounts. */ + /** + * Create an Account from an account ID. Should only be used when the + * secret key is unavailable, such as for pseudo-accounts. + */ explicit Account(std::string name, AccountID const& id); enum class AcctStringType { Base58Seed, Other }; - /** Create an account from a base58 seed string. Throws on invalid seed. */ + /** + * Create an account from a base58 seed string. Throws on invalid seed. + */ Account(AcctStringType stringType, std::string base58SeedStr); - /** Return the name */ + /** + * Return the name + */ [[nodiscard]] std::string const& name() const { return name_; } - /** Return the public key. */ + /** + * Return the public key. + */ [[nodiscard]] PublicKey const& pk() const { return pk_; } - /** Return the secret key. */ + /** + * Return the secret key. + */ [[nodiscard]] SecretKey const& sk() const { return sk_; } - /** Returns the Account ID. - - The Account ID is the uint160 hash of the public key. - */ + /** + * Returns the Account ID. + * + * The Account ID is the uint160 hash of the public key. + */ [[nodiscard]] AccountID id() const { return id_; } - /** Returns the human readable public key. */ + /** + * Returns the human readable public key. + */ [[nodiscard]] std::string const& human() const { return human_; } - /** Implicit conversion to AccountID. - - This allows passing an Account - where an AccountID is expected. - */ + /** + * Implicit conversion to AccountID. + * + * This allows passing an Account + * where an AccountID is expected. + */ operator AccountID() const { return id_; } - /** Returns an IOU for the specified gateway currency. */ + /** + * Returns an IOU for the specified gateway currency. + */ IOU operator[](std::string const& s) const; diff --git a/src/test/jtx/CheckMessageLogs.h b/src/test/jtx/CheckMessageLogs.h index fc46f41671..24b2c24a73 100644 --- a/src/test/jtx/CheckMessageLogs.h +++ b/src/test/jtx/CheckMessageLogs.h @@ -9,7 +9,8 @@ namespace xrpl::test { -/** Log manager that searches for a specific message substring +/** + * Log manager that searches for a specific message substring */ class CheckMessageLogs : public Logs { @@ -41,12 +42,13 @@ class CheckMessageLogs : public Logs }; public: - /** Constructor - - @param msg The message string to search for - @param pFound Pointer to the variable to set to true if the message is - found - */ + /** + * Constructor + * + * @param msg The message string to search for + * @param pFound Pointer to the variable to set to true if the message is + * found + */ CheckMessageLogs(std::string msg, bool* pFound) : Logs{beast::Severity::Debug}, msg_{std::move(msg)}, pFound_{pFound} { diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index ba28d19738..7e22cdd571 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -55,14 +55,15 @@ namespace xrpl::test::jtx { -/** Wrapper that captures std::source_location when implicitly constructed. - This solves the problem of combining std::source_location with variadic - templates. The std::source_location default argument is evaluated at the - call site when the wrapper is constructed via implicit conversion. - - This is a template struct that holds the value directly, allowing implicit - conversion without template argument deduction issues via CTAD. -*/ +/** + * Wrapper that captures std::source_location when implicitly constructed. + * This solves the problem of combining std::source_location with variadic + * templates. The std::source_location default argument is evaluated at the + * call site when the wrapper is constructed via implicit conversion. + * + * This is a template struct that holds the value directly, allowing implicit + * conversion without template argument deduction issues via CTAD. + */ template struct WithSourceLocation { @@ -77,7 +78,9 @@ struct WithSourceLocation } }; -/** Designate accounts as no-ripple in Env::fund */ +/** + * Designate accounts as no-ripple in Env::fund + */ template std::array noripple(Account const& account, Args const&... args) @@ -151,7 +154,9 @@ public: //------------------------------------------------------------------------------ -/** A transaction testing environment. */ +/** + * A transaction testing environment. + */ class Env { public: @@ -159,7 +164,9 @@ public: Account const& master = Account::kMaster; - /// Used by parseResult() and postConditions() + /** + * Used by parseResult() and postConditions() + */ struct ParsedResult { std::optional ter; @@ -308,11 +315,12 @@ public: return *bundle_.timeKeeper; } - /** Returns the current network time - - @note This is manually advanced when ledgers - close or by callers. - */ + /** + * Returns the current network time + * + * @note This is manually advanced when ledgers + * close or by callers. + */ NetClock::time_point // NOLINTNEXTLINE(readability-make-member-function-const) now() @@ -320,7 +328,9 @@ public: return timeKeeper().now(); } - /** Returns the connected client. */ + /** + * Returns the connected client. + */ AbstractClient& // NOLINTNEXTLINE(readability-make-member-function-const) client() @@ -328,11 +338,12 @@ public: return *bundle_.client; } - /** Execute an RPC command. - - The command is examined and used to build - the correct JSON as per the arguments. - */ + /** + * Execute an RPC command. + * + * The command is examined and used to build + * the correct JSON as per the arguments. + */ template json::Value rpc(unsigned apiVersion, @@ -354,61 +365,64 @@ public: json::Value rpc(std::string const& cmd, Args&&... args); - /** Returns the current ledger. - - This is a non-modifiable snapshot of the - open ledger at the moment of the call. - Transactions applied after the call to open() - will not be visible. - - */ + /** + * Returns the current ledger. + * + * This is a non-modifiable snapshot of the + * open ledger at the moment of the call. + * Transactions applied after the call to open() + * will not be visible. + */ [[nodiscard]] std::shared_ptr current() const { return app().getOpenLedger().current(); } - /** Returns the last closed ledger. - - The open ledger is built on top of the - last closed ledger. When the open ledger - is closed, it becomes the new closed ledger - and a new open ledger takes its place. - */ + /** + * Returns the last closed ledger. + * + * The open ledger is built on top of the + * last closed ledger. When the open ledger + * is closed, it becomes the new closed ledger + * and a new open ledger takes its place. + */ std::shared_ptr closed(); - /** Close and advance the ledger. - - The resulting close time will be different and - greater than the previous close time, and at or - after the passed-in close time. - - Effects: - - Creates a new closed ledger from the last - closed ledger. - - All transactions that made it into the open - ledger are applied to the closed ledger. - - The Application network time is set to - the close time of the resulting ledger. - - @return true if no error, false if error - */ + /** + * Close and advance the ledger. + * + * The resulting close time will be different and + * greater than the previous close time, and at or + * after the passed-in close time. + * + * Effects: + * + * Creates a new closed ledger from the last + * closed ledger. + * + * All transactions that made it into the open + * ledger are applied to the closed ledger. + * + * The Application network time is set to + * the close time of the resulting ledger. + * + * @return true if no error, false if error + */ bool close( NetClock::time_point closeTime, std::optional consensusDelay = std::nullopt); - /** Close and advance the ledger. - - The time is calculated as the duration from - the previous ledger closing time. - - @return true if no error, false if error - */ + /** + * Close and advance the ledger. + * + * The time is calculated as the duration from + * the previous ledger closing time. + * + * @return true if no error, false if error + */ template bool close(std::chrono::duration const& elapsed) @@ -417,13 +431,14 @@ public: return close(now() + elapsed); } - /** Close and advance the ledger. - - The time is calculated as five seconds from - the previous ledger closing time. - - @return true if no error, false if error - */ + /** + * Close and advance the ledger. + * + * The time is calculated as five seconds from + * the previous ledger closing time. + * + * @return true if no error, false if error + */ bool close() { @@ -431,34 +446,35 @@ public: return close(std::chrono::seconds(5)); } - /** Close and advance the ledger, then synchronize with the server's - io_context to ensure all async operations initiated by the close have - been started. - - This function performs the same ledger close as close(), but additionally - ensures that all tasks posted to the server's io_context (such as - WebSocket subscription message sends) have been initiated before returning. - - What it guarantees: - - All async operations posted before syncClose() have been STARTED - - For WebSocket sends: async_write_some() has been called - - The actual I/O completion may still be pending (async) - - What it does NOT guarantee: - - Async operations have COMPLETED - - WebSocket messages have been received by clients - - However, for localhost connections, the remaining latency is typically - microseconds, making tests reliable - - Use this instead of close() when: - - Test code immediately checks for subscription messages - - Race conditions between test and worker threads must be avoided - - Deterministic test behavior is required - - @param timeout Maximum time to wait for the barrier task to execute - @return true if close succeeded and barrier executed within timeout, - false otherwise - */ + /** + * Close and advance the ledger, then synchronize with the server's + * io_context to ensure all async operations initiated by the close have + * been started. + * + * This function performs the same ledger close as close(), but additionally + * ensures that all tasks posted to the server's io_context (such as + * WebSocket subscription message sends) have been initiated before returning. + * + * What it guarantees: + * - All async operations posted before syncClose() have been STARTED + * - For WebSocket sends: async_write_some() has been called + * - The actual I/O completion may still be pending (async) + * + * What it does NOT guarantee: + * - Async operations have COMPLETED + * - WebSocket messages have been received by clients + * - However, for localhost connections, the remaining latency is typically + * microseconds, making tests reliable + * + * Use this instead of close() when: + * - Test code immediately checks for subscription messages + * - Race conditions between test and worker threads must be avoided + * - Deterministic test behavior is required + * + * @param timeout Maximum time to wait for the barrier task to execute + * @return true if close succeeded and barrier executed within timeout, + * false otherwise + */ [[nodiscard]] bool syncClose(std::chrono::steady_clock::duration timeout = std::chrono::seconds{1}) { @@ -473,16 +489,19 @@ public: return result && status == std::future_status::ready; } - /** Turn on JSON tracing. - With no arguments, trace all - */ + /** + * Turn on JSON tracing. + * With no arguments, trace all + */ void trace(int howMany = -1) { trace_ = howMany; } - /** Turn off JSON tracing. */ + /** + * Turn off JSON tracing. + */ void notrace() { @@ -495,7 +514,9 @@ public: parseFailureExpected_ = b; } - /** Turn off signature checks. */ + /** + * Turn off signature checks. + */ void disableSigs() { @@ -516,11 +537,15 @@ public: return retries_; } - /** Associate AccountID with account. */ + /** + * Associate AccountID with account. + */ void memoize(Account const& account); - /** Returns the Account given the AccountID. */ + /** + * Returns the Account given the AccountID. + */ /** @{ */ [[nodiscard]] Account const& lookup(AccountID const& id) const; @@ -529,51 +554,84 @@ public: lookup(std::string const& base58ID) const; /** @} */ - /** Returns the XRP balance on an account. - Returns 0 if the account does not exist. - */ + /** + * Returns the XRP balance on an account. + * Returns 0 if the account does not exist. + */ [[nodiscard]] PrettyAmount balance(Account const& account) const; - /** Returns the next sequence number on account. - Exceptions: - Throws if the account does not exist - */ + /** + * Returns the next sequence number on account. + * + * @throws if the account does not exist + */ [[nodiscard]] std::uint32_t seq(Account const& account) const; - /** Return the balance on an account. - Returns 0 if the trust line does not exist. - */ + /** + * Return the balance on an account. + * Returns 0 if the trust line does not exist. + */ // VFALCO NOTE This should return a unit-less amount [[nodiscard]] PrettyAmount balance(Account const& account, Asset const& asset) const; - /** Returns the IOU limit on an account. - Returns 0 if the trust line does not exist. - */ + /** + * Returns the IOU limit on an account. + * Returns 0 if the trust line does not exist. + */ [[nodiscard]] PrettyAmount limit(Account const& account, Issue const& issue) const; - /** Return the number of objects owned by an account. + /** + * Return the number of objects owned by an account. * Returns 0 if the account does not exist. */ [[nodiscard]] std::uint32_t ownerCount(Account const& account) const; - /** Return an account root. - @return empty if the account does not exist. - */ + /** + * Return the number of sponsored objects owned by an account. + * + * @throws if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoredOwnerCount(Account const& account) const; + + /** + * Return the number of sponsoring objects owned by an account. + * + * @throws if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoringOwnerCount(Account const& account) const; + + /** + * Return the number of sponsoring accounts owned by an account. + * + * @throws if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoringAccountCount(Account const& account) const; + + /** + * Return an account root. + * @return empty if the account does not exist. + */ [[nodiscard]] SLE::const_pointer le(Account const& account) const; - /** Return a ledger entry. - @return empty if the ledger entry does not exist - */ + /** + * Return a ledger entry. + * @return empty if the ledger entry does not exist + */ [[nodiscard]] SLE::const_pointer le(Keylet const& k) const; - /** Create a JTx from parameters. */ + /** + * Create a JTx from parameters. + */ template JTx jt(JsonValue&& jv, FN const&... fN) @@ -585,7 +643,9 @@ public: return jt; } - /** Create a JTx from parameters. */ + /** + * Create a JTx from parameters. + */ template JTx jtnofill(JsonValue&& jv, FN const&... fN) @@ -597,9 +657,10 @@ public: return jt; } - /** Create JSON from parameters. - This will apply funclets and autofill. - */ + /** + * Create JSON from parameters. + * This will apply funclets and autofill. + */ template json::Value json(JsonValue&& jv, FN const&... fN) @@ -608,11 +669,12 @@ public: return std::move(tj.jv); } - /** Check a set of requirements. - - The requirements are formed - from condition functors. - */ + /** + * Check a set of requirements. + * + * The requirements are formed + * from condition functors. + */ template void require(Args const&... args) @@ -620,29 +682,33 @@ public: jtx::required(args...)(*this); } - /** Gets the TER result and `didApply` flag from a RPC Json result object. + /** + * Gets the TER result and `didApply` flag from a RPC Json result object. */ static ParsedResult parseResult(json::Value const& jr); - /** Submit an existing JTx. - This calls postconditions. - */ + /** + * Submit an existing JTx. + * This calls postconditions. + */ virtual void submit(JTx const& jt, std::source_location const& loc = std::source_location::current()); - /** Use the submit RPC command with a provided JTx object. - This calls postconditions. - */ + /** + * Use the submit RPC command with a provided JTx object. + * This calls postconditions. + */ void signAndSubmit( JTx const& jt, json::Value params = json::ValueType::Null, std::source_location const& loc = std::source_location::current()); - /** Check expected postconditions - of JTx submission. - */ + /** + * Check expected postconditions + * of JTx submission. + */ void postconditions( JTx const& jt, @@ -650,7 +716,9 @@ public: json::Value const& jr = json::Value(), std::source_location const& loc = std::source_location::current()); - /** Apply funclets and submit. */ + /** + * Apply funclets and submit. + */ /** @{ */ template Env& @@ -683,38 +751,42 @@ public: } /** @} */ - /** Return the TER for the last JTx. */ + /** + * Return the TER for the last JTx. + */ [[nodiscard]] TER ter() const { return ter_; } - /** Return metadata for the last JTx. + /** + * Return metadata for the last JTx. * - * NOTE: this has a side effect of closing the open ledger. - * The ledger will only be closed if it includes transactions. + * NOTE: this has a side effect of closing the open ledger. + * The ledger will only be closed if it includes transactions. * - * Effects: + * Effects: * - * The open ledger is closed as if by a call - * to close(). The metadata for the last - * transaction ID, if any, is returned. + * The open ledger is closed as if by a call + * to close(). The metadata for the last + * transaction ID, if any, is returned. */ std::shared_ptr meta(); - /** Return the tx data for the last JTx. - - Effects: - - The tx data for the last transaction - ID, if any, is returned. No side - effects. - - @note Only necessary for JTx submitted - with via sign-and-submit method. - */ + /** + * Return the tx data for the last JTx. + * + * Effects: + * + * The tx data for the last transaction + * ID, if any, is returned. No side + * effects. + * + * @note Only necessary for JTx submitted + * with via sign-and-submit method. + */ [[nodiscard]] std::shared_ptr tx() const; @@ -749,32 +821,33 @@ private: } public: - /** Create a new account with some XRP. - - These convenience functions are for easy set-up - of the environment, they bypass fee, seq, and sig - settings. The XRP is transferred from the master - account. - - Preconditions: - The account must not already exist - - Effects: - The asfDefaultRipple on the account is set, - and the sequence number is incremented, unless - the account is wrapped with a call to noripple. - - The account's XRP balance is set to amount. - - Generates a test that the balance is set. - - @param amount The amount of XRP to transfer to - each account. - - @param args A heterogeneous list of accounts to fund - or calls to noripple with lists of accounts - to fund. - */ + /** + * Create a new account with some XRP. + * + * These convenience functions are for easy set-up + * of the environment, they bypass fee, seq, and sig + * settings. The XRP is transferred from the master + * account. + * + * Preconditions: + * The account must not already exist + * + * Effects: + * The asfDefaultRipple on the account is set, + * and the sequence number is incremented, unless + * the account is wrapped with a call to noripple. + * + * The account's XRP balance is set to amount. + * + * Generates a test that the balance is set. + * + * @param amount The amount of XRP to transfer to + * each account. + * + * @param args A heterogeneous list of accounts to fund + * or calls to noripple with lists of accounts + * to fund. + */ template void fund(STAmount const& amount, Arg const& arg, Args const&... args) @@ -784,23 +857,24 @@ public: fund(amount, args...); } - /** Establish trust lines. - - These convenience functions are for easy set-up - of the environment, they bypass fee, seq, and sig - settings. - - Preconditions: - The account must already exist - - Effects: - A trust line is added for the account. - The account's sequence number is incremented. - The account is refunded for the transaction fee - to set the trust line. - - The refund comes from the master account. - */ + /** + * Establish trust lines. + * + * These convenience functions are for easy set-up + * of the environment, they bypass fee, seq, and sig + * settings. + * + * Preconditions: + * The account must already exist + * + * Effects: + * A trust line is added for the account. + * The account's sequence number is incremented. + * The account is refunded for the transaction fee + * to set the trust line. + * + * The refund comes from the master account. + */ /** @{ */ void trust(STAmount const& amount, Account const& account); @@ -814,10 +888,11 @@ public: } /** @} */ - /** Create a STTx from a JTx without sanitizing - Use to inject bogus values into test transactions by first - editing the JSON. - */ + /** + * Create a STTx from a JTx without sanitizing + * Use to inject bogus values into test transactions by first + * editing the JSON. + */ std::shared_ptr ust(JTx const& jt); @@ -841,13 +916,14 @@ protected: virtual void autofill(JTx& jt); - /** Create a STTx from a JTx - The framework requires that JSON is valid. - On a parse error, the JSON is logged and - an exception thrown. - Throws: - ParseError - */ + /** + * Create a STTx from a JTx + * The framework requires that JSON is valid. + * On a parse error, the JSON is logged and + * an exception thrown. + * + * @throws ParseError + */ std::shared_ptr st(JTx const& jt); diff --git a/src/test/jtx/Env_ss.h b/src/test/jtx/Env_ss.h index 16e1cdbc82..ca298f0069 100644 --- a/src/test/jtx/Env_ss.h +++ b/src/test/jtx/Env_ss.h @@ -10,10 +10,11 @@ namespace xrpl::test::jtx { -/** A transaction testing environment wrapper. - Transactions submitted in sign-and-submit mode - by default. -*/ +/** + * A transaction testing environment wrapper. + * Transactions submitted in sign-and-submit mode + * by default. + */ class EnvSs { private: diff --git a/src/test/jtx/JSONRPCClient.h b/src/test/jtx/JSONRPCClient.h index 2864cbbdc8..10e567e91f 100644 --- a/src/test/jtx/JSONRPCClient.h +++ b/src/test/jtx/JSONRPCClient.h @@ -8,7 +8,9 @@ namespace xrpl::test { -/** Returns a client using JSON-RPC over HTTP/S. */ +/** + * Returns a client using JSON-RPC over HTTP/S. + */ std::unique_ptr makeJSONRPCClient(Config const& cfg, unsigned rpcVersion = 2); diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index da105e01aa..794334bec7 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -19,9 +19,10 @@ namespace xrpl::test::jtx { class Env; -/** Execution context for applying a JSON transaction. - This augments the transaction with various settings. -*/ +/** + * Execution context for applying a JSON transaction. + * This augments the transaction with various settings. + */ struct JTx { json::Value jv; @@ -37,7 +38,7 @@ struct JTx // Functions that sign the transaction from the Account std::vector> mainSigners; // Functions that sign something else after the mainSigners, such as - // sfCounterpartySignature + // sfCounterpartySignature and sfSponsorSignature std::vector> postSigners; JTx() = default; @@ -63,10 +64,11 @@ struct JTx return jv[key]; } - /** Return a property if it exists - - @return nullptr if the Prop does not exist - */ + /** + * Return a property if it exists + * + * @return nullptr if the Prop does not exist + */ /** @{ */ template Prop* @@ -93,10 +95,11 @@ struct JTx } /** @} */ - /** Set a property - If the property already exists, - it is replaced. - */ + /** + * Set a property + * If the property already exists, + * it is replaced. + */ /** @{ */ void set(std::unique_ptr p) diff --git a/src/test/jtx/Oracle.h b/src/test/jtx/Oracle.h index 2c296ba705..d0fe8104e5 100644 --- a/src/test/jtx/Oracle.h +++ b/src/test/jtx/Oracle.h @@ -102,7 +102,8 @@ struct RemoveArg // validation {close-maxLastUpdateTimeDelta,close+maxLastUpdateTimeDelta}. static constexpr std::chrono::seconds kTestStartTime = kEpochOffset + std::chrono::seconds(10'000); -/** Oracle class facilitates unit-testing of the Price Oracle feature. +/** + * Oracle class facilitates unit-testing of the Price Oracle feature. * It defines functions to create, update, and delete the Oracle object, * to query for various states, and to call APIs. */ diff --git a/src/test/jtx/PathSet.h b/src/test/jtx/PathSet.h index fa3f0d40f9..42d391c46f 100644 --- a/src/test/jtx/PathSet.h +++ b/src/test/jtx/PathSet.h @@ -18,7 +18,8 @@ namespace xrpl::test { -/** Count offer +/** + * Count offer */ inline std::size_t countOffers( @@ -52,7 +53,8 @@ countOffers( return count; } -/** An offer exists +/** + * An offer exists */ inline bool isOffer( @@ -64,7 +66,8 @@ isOffer( return countOffers(env, account, takerPays, takerGets) > 0; } -/** An offer exists +/** + * An offer exists */ inline bool isOffer(jtx::Env& env, jtx::Account const& account, Asset const& takerPays, Asset const& takerGets) diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index cb2193db99..e7a2808f07 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -51,11 +51,12 @@ namespace xrpl::test::jtx { -/** Generic helper class for helper classes that set a field on a JTx. - - Not every helper will be able to use this because of conversions and other - issues, but for classes where it's straightforward, this can simplify things. -*/ +/** + * Generic helper class for helper classes that set a field on a JTx. + * + * Not every helper will be able to use this because of conversions and other + * issues, but for classes where it's straightforward, this can simplify things. + */ template < class SField, // NOLINTNEXTLINE(readability-redundant-typename): typename required by MSVC @@ -306,7 +307,8 @@ using valueUnitWrapper = JTxFieldWrapper using simpleField = JTxFieldWrapper>; -/** General field definitions, or fields used in multiple transaction namespaces +/** + * General field definitions, or fields used in multiple transaction namespaces */ auto const kData = JTxFieldWrapper(sfData); @@ -379,6 +381,18 @@ checkArraySize(json::Value const& val, unsigned int size); std::uint32_t ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); +// Helper function that returns the sponsored owner count on an account. +std::uint32_t +sponsoredOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account); + +// Helper function that returns the sponsoring owner count on an account. +std::uint32_t +sponsoringOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account); + +// Helper function that returns the sponsoring account count on an account. +std::uint32_t +sponsoringAccountCount(test::jtx::Env const& env, test::jtx::Account const& account); + [[nodiscard]] inline bool checkVL(Slice const& result, std::string const& expected) @@ -450,12 +464,8 @@ same(STPathSet const& st1, Args const&... args) if (st1.size() != st2.size()) return false; - for (auto const& p : st2) - { - if (std::ranges::find(st1, p) == st1.end()) - return false; - } - return true; + return std::ranges::all_of( + st2, [&st1](auto const& p) { return std::ranges::find(st1, p) != st1.end(); }); } json::Value @@ -741,7 +751,9 @@ equal(Strand const& strand, Args&&... args) /***************************************************************/ namespace check { -/** Create a check. */ +/** + * Create a check. + */ template requires std::is_same_v json::Value @@ -961,7 +973,9 @@ pay(AccountID const& account, } // namespace loan -/** Set Expiration on a JTx. */ +/** + * Set Expiration on a JTx. + */ class Expiration { private: @@ -980,7 +994,9 @@ public: } }; -/** Set SourceTag on a JTx. */ +/** + * Set SourceTag on a JTx. + */ class SourceTag { private: @@ -998,7 +1014,9 @@ public: } }; -/** Set DestinationTag on a JTx. */ +/** + * Set DestinationTag on a JTx. + */ class DestTag { private: diff --git a/src/test/jtx/WSClient.h b/src/test/jtx/WSClient.h index 4aa41e072c..ee5a46e9ae 100644 --- a/src/test/jtx/WSClient.h +++ b/src/test/jtx/WSClient.h @@ -18,18 +18,24 @@ namespace xrpl::test { class WSClient : public AbstractClient { public: - /** Retrieve a message. */ + /** + * Retrieve a message. + */ virtual std::optional getMsg(std::chrono::milliseconds const& timeout = std::chrono::milliseconds{0}) = 0; - /** Retrieve a message that meets the predicate criteria. */ + /** + * Retrieve a message that meets the predicate criteria. + */ virtual std::optional findMsg( std::chrono::milliseconds const& timeout, std::function pred) = 0; }; -/** Returns a client operating through WebSockets/S. */ +/** + * Returns a client operating through WebSockets/S. + */ std::unique_ptr makeWSClient( Config const& cfg, diff --git a/src/test/jtx/acctdelete.h b/src/test/jtx/acctdelete.h index 3f8b9f1ed2..935a97ac65 100644 --- a/src/test/jtx/acctdelete.h +++ b/src/test/jtx/acctdelete.h @@ -9,7 +9,9 @@ namespace xrpl::test::jtx { -/** Delete account. If successful transfer remaining XRP to dest. */ +/** + * Delete account. If successful transfer remaining XRP to dest. + */ json::Value acctdelete(Account const& account, Account const& dest); diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index db7dbd1cc1..57a4502db9 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -69,10 +69,11 @@ struct None // could change that value (however unlikely). constexpr XRPAmount kJtxDropsPerXrp{1'000'000}; -/** Represents an XRP, IOU, or MPT quantity - This customizes the string conversion and supports - XRP conversions from integer and floating point. -*/ +/** + * Represents an XRP, IOU, or MPT quantity + * This customizes the string conversion and supports + * XRP conversions from integer and floating point. + */ struct PrettyAmount { private: @@ -91,7 +92,9 @@ public: { } - /** drops */ + /** + * drops + */ template PrettyAmount(T v) requires(sizeof(T) >= sizeof(int) && std::is_integral_v && std::is_signed_v) @@ -99,7 +102,9 @@ public: { } - /** drops */ + /** + * drops + */ template PrettyAmount(T v) requires(sizeof(T) >= sizeof(int) && std::is_unsigned_v) @@ -107,7 +112,9 @@ public: { } - /** drops */ + /** + * drops + */ PrettyAmount(XRPAmount v) : amount_(v) { } @@ -253,11 +260,12 @@ struct BookSpec struct XrpT { - /** Implicit conversion to Issue. - - This allows passing XRP where - an Issue is expected. - */ + /** + * Implicit conversion to Issue. + * + * This allows passing XRP where + * an Issue is expected. + */ operator Issue() const { return xrpIssue(); @@ -273,11 +281,12 @@ struct XrpT return true; } - /** Returns an amount of XRP as PrettyAmount, - which is trivially convertible to STAmount - - @param v The number of XRP (not drops) - */ + /** + * Returns an amount of XRP as PrettyAmount, + * which is trivially convertible to STAmount + * + * @param v The number of XRP (not drops) + */ /** @{ */ template PrettyAmount @@ -288,11 +297,12 @@ struct XrpT return {TOut{v} * kJtxDropsPerXrp}; } - /** Returns an amount of XRP as PrettyAmount, - which is trivially convertible to STAmount - - @param v The Number of XRP (not drops). May be fractional. - */ + /** + * Returns an amount of XRP as PrettyAmount, + * which is trivially convertible to STAmount + * + * @param v The Number of XRP (not drops). May be fractional. + */ PrettyAmount operator()(Number v) const { @@ -321,7 +331,9 @@ struct XrpT } /** @} */ - /** Returns None-of-XRP */ + /** + * Returns None-of-XRP + */ None operator()(NoneT) const { @@ -335,19 +347,21 @@ struct XrpT } }; -/** Converts to XRP Issue or STAmount. - - Examples: - XRP Converts to the XRP Issue - XRP(10) Returns STAmount of 10 XRP -*/ +/** + * Converts to XRP Issue or STAmount. + * + * Examples: + * XRP Converts to the XRP Issue + * XRP(10) Returns STAmount of 10 XRP + */ extern XrpT const XRP; // NOLINT(readability-identifier-naming) -/** Returns an XRP PrettyAmount, which is trivially convertible to STAmount. - - Example: - drops(10) Returns PrettyAmount of 10 drops -*/ +/** + * Returns an XRP PrettyAmount, which is trivially convertible to STAmount. + * + * Example: + * drops(10) Returns PrettyAmount of 10 drops + */ template PrettyAmount drops(Integer i) @@ -356,11 +370,12 @@ drops(Integer i) return {i}; } -/** Returns an XRP PrettyAmount, which is trivially convertible to STAmount. - -Example: -drops(view->fee().basefee) Returns PrettyAmount of 10 drops -*/ +/** + * Returns an XRP PrettyAmount, which is trivially convertible to STAmount. + * + * Example: + * drops(view->fee().basefee) Returns PrettyAmount of 10 drops + */ inline PrettyAmount drops(XRPAmount i) { @@ -383,13 +398,14 @@ struct EpsilonT static EpsilonT const kEpsilon; -/** Converts to IOU Issue or STAmount. - - Examples: - IOU Converts to the underlying Issue - IOU(10) Returns STAmount of 10 of - the underlying Issue. -*/ +/** + * Converts to IOU Issue or STAmount. + * + * Examples: + * IOU Converts to the underlying Issue + * IOU(10) Returns STAmount of 10 of + * the underlying Issue. + */ class IOU { public: @@ -417,11 +433,12 @@ public: return issue().integral(); } - /** Implicit conversion to Issue or Asset. - - This allows passing an IOU - value where an Issue or Asset is expected. - */ + /** + * Implicit conversion to Issue or Asset. + * + * This allows passing an IOU + * value where an Issue or Asset is expected. + */ operator Issue() const { return issue(); @@ -453,7 +470,9 @@ public: // VFALCO TODO // STAmount operator()(char const* s) const; - /** Returns None-of-Issue */ + /** + * Returns None-of-Issue + */ None operator()(NoneT) const { @@ -472,13 +491,14 @@ operator<<(std::ostream& os, IOU const& iou); //------------------------------------------------------------------------------ -/** Converts to MPT Issue or STAmount. - - Examples: - MPT Converts to the underlying Issue - MPT(10) Returns STAmount of 10 of - the underlying MPT -*/ +/** + * Converts to MPT Issue or STAmount. + * + * Examples: + * MPT Converts to the underlying Issue + * MPT(10) Returns STAmount of 10 of + * the underlying MPT + */ class MPT { public: @@ -504,7 +524,8 @@ public: return issuanceID; } - /** Explicit conversion to MPTIssue or asset. + /** + * Explicit conversion to MPTIssue or asset. */ [[nodiscard]] xrpl::MPTIssue mptIssue() const @@ -522,11 +543,12 @@ public: return true; } - /** Implicit conversion to MPTIssue or asset. - - This allows passing an MPT - value where an MPTIssue is expected. - */ + /** + * Implicit conversion to MPTIssue or asset. + * + * This allows passing an MPT + * value where an MPTIssue is expected. + */ operator xrpl::MPTIssue() const { return mptIssue(); @@ -558,7 +580,9 @@ public: PrettyAmount operator()(detail::EpsilonMultiple) const; - /** Returns None-of-Issue */ + /** + * Returns None-of-Issue + */ None operator()(NoneT) const { @@ -583,7 +607,9 @@ struct AnyT operator()(STAmount const& sta) const; }; -/** Amount specifier with an option for any issuer. */ +/** + * Amount specifier with an option for any issuer. + */ struct AnyAmount { bool isAny; @@ -618,9 +644,10 @@ AnyT::operator()(STAmount const& sta) const return AnyAmount(sta, this); } -/** Returns an amount representing "any issuer" - @note With respect to what the recipient will accept -*/ +/** + * Returns an amount representing "any issuer" + * @note With respect to what the recipient will accept + */ extern AnyT const kAny; } // namespace test::jtx diff --git a/src/test/jtx/balance.h b/src/test/jtx/balance.h index 3f39e9f0fb..9d7937369b 100644 --- a/src/test/jtx/balance.h +++ b/src/test/jtx/balance.h @@ -11,14 +11,15 @@ namespace xrpl::test::jtx { -/** A balance matches. - - This allows "none" which means either the account - doesn't exist (no XRP) or the trust line does not - exist. If an amount is specified, the SLE must - exist even if the amount is 0, or else the test - fails. -*/ +/** + * A balance matches. + * + * This allows "none" which means either the account + * doesn't exist (no XRP) or the trust line does not + * exist. If an amount is specified, the SLE must + * exist even if the amount is 0, or else the test + * fails. + */ class Balance { private: diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h index bb1deec1a2..140fc84ab1 100644 --- a/src/test/jtx/batch.h +++ b/src/test/jtx/batch.h @@ -18,7 +18,9 @@ #include #include -/** @brief Helpers for constructing Batch test transactions. */ +/** + * @brief Helpers for constructing Batch test transactions. + */ namespace xrpl::test::jtx::batch { /** @@ -57,7 +59,9 @@ calcConfidentialBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32 json::Value outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags); -/** @brief Adds an inner Batch transaction to a JTx and autofills it. */ +/** + * @brief Adds an inner Batch transaction to a JTx and autofills it. + */ class Inner { private: @@ -106,7 +110,9 @@ public: } }; -/** @brief Sets the Batch transaction signers on a JTx. */ +/** + * @brief Sets the Batch transaction signers on a JTx. + */ class Sig { public: @@ -129,7 +135,9 @@ public: operator()(Env&, JTx& jt) const; }; -/** @brief Sets a nested multi-signature for a Batch transaction on a JTx. */ +/** + * @brief Sets a nested multi-signature for a Batch transaction on a JTx. + */ class Msig { public: diff --git a/src/test/jtx/check.h b/src/test/jtx/check.h index f66d802247..a5d1f079c4 100644 --- a/src/test/jtx/check.h +++ b/src/test/jtx/check.h @@ -12,14 +12,20 @@ namespace xrpl::test::jtx { -/** Check operations. */ +/** + * Check operations. + */ namespace check { -/** Cash a check requiring that a specific amount be delivered. */ +/** + * Cash a check requiring that a specific amount be delivered. + */ json::Value cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount); -/** Type used to specify DeliverMin for cashing a check. */ +/** + * Type used to specify DeliverMin for cashing a check. + */ struct DeliverMin { STAmount value; @@ -28,17 +34,23 @@ struct DeliverMin } }; -/** Cash a check requiring that at least a minimum amount be delivered. */ +/** + * Cash a check requiring that at least a minimum amount be delivered. + */ json::Value cash(jtx::Account const& dest, uint256 const& checkId, DeliverMin const& atLeast); -/** Cancel a check. */ +/** + * Cancel a check. + */ json::Value cancel(jtx::Account const& dest, uint256 const& checkId); } // namespace check -/** Match the number of checks on the account. */ +/** + * Match the number of checks on the account. + */ using checks = OwnerCount; } // namespace xrpl::test::jtx diff --git a/src/test/jtx/delivermin.h b/src/test/jtx/delivermin.h index 29256e37bd..db5014bc07 100644 --- a/src/test/jtx/delivermin.h +++ b/src/test/jtx/delivermin.h @@ -9,7 +9,9 @@ namespace xrpl::test::jtx { -/** Sets the DeliverMin on a JTx. */ +/** + * Sets the DeliverMin on a JTx. + */ class DeliverMin { private: diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h index 5ba12648e2..3039167cee 100644 --- a/src/test/jtx/deposit.h +++ b/src/test/jtx/deposit.h @@ -10,14 +10,20 @@ #include #include -/** Deposit preauthorize operations */ +/** + * Deposit preauthorize operations + */ namespace xrpl::test::jtx::deposit { -/** Preauthorize for deposit. Invoke as deposit::auth. */ +/** + * Preauthorize for deposit. Invoke as deposit::auth. + */ json::Value auth(Account const& account, Account const& auth); -/** Remove pre-authorization for deposit. Invoke as deposit::unauth. */ +/** + * Remove pre-authorization for deposit. Invoke as deposit::unauth. + */ json::Value unauth(Account const& account, Account const& unauth); diff --git a/src/test/jtx/did.h b/src/test/jtx/did.h index 30c89fd879..da10b476e8 100644 --- a/src/test/jtx/did.h +++ b/src/test/jtx/did.h @@ -10,7 +10,9 @@ #include -/** DID operations. */ +/** + * DID operations. + */ namespace xrpl::test::jtx::did { json::Value @@ -19,7 +21,9 @@ set(jtx::Account const& account); json::Value setValid(jtx::Account const& account); -/** Sets the optional DIDDocument on a DIDSet. */ +/** + * Sets the optional DIDDocument on a DIDSet. + */ class Document { private: @@ -37,7 +41,9 @@ public: } }; -/** Sets the optional URI on a DIDSet. */ +/** + * Sets the optional URI on a DIDSet. + */ class Uri { private: @@ -55,7 +61,9 @@ public: } }; -/** Sets the optional Data on a DIDSet. */ +/** + * Sets the optional Data on a DIDSet. + */ class Data { private: diff --git a/src/test/jtx/directory.h b/src/test/jtx/directory.h index 13473f949e..294cfcc43c 100644 --- a/src/test/jtx/directory.h +++ b/src/test/jtx/directory.h @@ -13,7 +13,9 @@ #include #include -/** Directory operations. */ +/** + * Directory operations. + */ namespace xrpl::test::jtx::directory { enum class Error { @@ -25,14 +27,16 @@ enum class Error { AdjustmentError }; -/// Move the position of the last page in the user's directory on open ledger to -/// newLastPage. Requirements: -/// - directory must have at least two pages (root and one more) -/// - adjust should be used to update owner nodes of the objects affected -/// - newLastPage must be greater than index of the last page in the directory -/// -/// Use this to test tecDIR_FULL errors in open ledger. -/// NOTE: effects will be DISCARDED on env.close() +/** + * Move the position of the last page in the user's directory on open ledger to + * newLastPage. Requirements: + * - directory must have at least two pages (root and one more) + * - adjust should be used to update owner nodes of the objects affected + * - newLastPage must be greater than index of the last page in the directory + * + * Use this to test tecDIR_FULL errors in open ledger. + * NOTE: effects will be DISCARDED on env.close() + */ auto bumpLastPage( Env& env, @@ -40,10 +44,12 @@ bumpLastPage( Keylet directory, std::function adjust) -> std::expected; -/// Implementation of adjust for the most common ledger entry, i.e. one where -/// page index is stored in sfOwnerNode (and only there). Pass this function -/// to bumpLastPage if the last page of directory has only objects -/// of this kind (e.g. ticket, DID, offer, deposit preauth, MPToken etc.) +/** + * Implementation of adjust for the most common ledger entry, i.e. one where + * page index is stored in sfOwnerNode (and only there). Pass this function + * to bumpLastPage if the last page of directory has only objects + * of this kind (e.g. ticket, DID, offer, deposit preauth, MPToken etc.) + */ bool adjustOwnerNode(ApplyView& view, uint256 key, std::uint64_t page); diff --git a/src/test/jtx/domain.h b/src/test/jtx/domain.h index ebcfdb662a..c6061c9795 100644 --- a/src/test/jtx/domain.h +++ b/src/test/jtx/domain.h @@ -7,7 +7,9 @@ namespace xrpl::test::jtx { -/** Set the domain on a JTx. */ +/** + * Set the domain on a JTx. + */ class Domain { private: diff --git a/src/test/jtx/envconfig.h b/src/test/jtx/envconfig.h index 0079605637..1f920fca58 100644 --- a/src/test/jtx/envconfig.h +++ b/src/test/jtx/envconfig.h @@ -17,18 +17,22 @@ getEnvLocalhostAddr() return gEnvUseIPv4 ? "127.0.0.1" : "::1"; } -/// @brief initializes a config object for use with jtx::Env -/// -/// @param config the configuration object to be initialized +/** + * @brief initializes a config object for use with jtx::Env + * + * @param config the configuration object to be initialized + */ extern void setupConfigForUnitTests(Config& config); namespace jtx { -/// @brief creates and initializes a default -/// configuration for jtx::Env -/// -/// @return unique_ptr to Config instance +/** + * @brief creates and initializes a default + * configuration for jtx::Env + * + * @return unique_ptr to Config instance + */ inline std::unique_ptr envconfig() { @@ -37,18 +41,20 @@ envconfig() return p; } -/// @brief creates and initializes a default configuration for jtx::Env and -/// invokes the provided function/lambda with the configuration object. -/// -/// @param modfunc callable function or lambda to modify the default config. -/// The first argument to the function must be std::unique_ptr to -/// xrpl::Config. The function takes ownership of the unique_ptr and -/// relinquishes ownership by returning a unique_ptr. -/// -/// @param args additional arguments that will be passed to -/// the config modifier function (optional) -/// -/// @return unique_ptr to Config instance +/** + * @brief creates and initializes a default configuration for jtx::Env and + * invokes the provided function/lambda with the configuration object. + * + * @param modfunc callable function or lambda to modify the default config. + * The first argument to the function must be std::unique_ptr to + * xrpl::Config. The function takes ownership of the unique_ptr and + * relinquishes ownership by returning a unique_ptr. + * + * @param args additional arguments that will be passed to + * the config modifier function (optional) + * + * @return unique_ptr to Config instance + */ template std::unique_ptr envconfig(F&& modfunc, Args&&... args) @@ -56,14 +62,16 @@ envconfig(F&& modfunc, Args&&... args) return modfunc(envconfig(), std::forward(args)...); } -/// @brief adjust config so no admin ports are enabled -/// -/// this is intended for use with envconfig, as in -/// envconfig(noAdmin) -/// -/// @param cfg config instance to be modified -/// -/// @return unique_ptr to Config instance +/** + * @brief adjust config so no admin ports are enabled + * + * this is intended for use with envconfig, as in + * envconfig(noAdmin) + * + * @param cfg config instance to be modified + * + * @return unique_ptr to Config instance + */ std::unique_ptr noAdmin(std::unique_ptr); std::unique_ptr secureGateway(std::unique_ptr); @@ -74,61 +82,71 @@ std::unique_ptr secureGatewayLocalnet(std::unique_ptr); std::unique_ptr singleThreadIo(std::unique_ptr); -/// @brief adjust configuration with params needed to be a validator -/// -/// this is intended for use with envconfig, as in -/// envconfig(validator, myseed) -/// -/// @param cfg config instance to be modified -/// @param seed seed string for use in secret key generation. A fixed default -/// value will be used if this string is empty -/// -/// @return unique_ptr to Config instance +/** + * @brief adjust configuration with params needed to be a validator + * + * this is intended for use with envconfig, as in + * envconfig(validator, myseed) + * + * @param cfg config instance to be modified + * @param seed seed string for use in secret key generation. A fixed default + * value will be used if this string is empty + * + * @return unique_ptr to Config instance + */ std::unique_ptr validator(std::unique_ptr, std::string const&); -/// @brief add a grpc address and port to config -/// -/// This is intended for use with envconfig, for tests that require a grpc -/// server. If this function is not called, grpc server will not start -/// -/// -/// @param cfg config instance to be modified +/** + * @brief add a grpc address and port to config + * + * This is intended for use with envconfig, for tests that require a grpc + * server. If this function is not called, grpc server will not start + * + * + * @param cfg config instance to be modified + */ std::unique_ptr addGrpcConfig(std::unique_ptr); -/// @brief add a grpc address, port and secureGateway to config -/// -/// This is intended for use with envconfig, for tests that require a grpc -/// server. If this function is not called, grpc server will not start -/// -/// -/// @param cfg config instance to be modified +/** + * @brief add a grpc address, port and secureGateway to config + * + * This is intended for use with envconfig, for tests that require a grpc + * server. If this function is not called, grpc server will not start + * + * + * @param cfg config instance to be modified + */ std::unique_ptr addGrpcConfigWithSecureGateway(std::unique_ptr, std::string const& secureGateway); -/// @brief add a grpc address, port and TLS certificate/key paths to config -/// -/// This is intended for use with envconfig, for tests that require a grpc -/// server with TLS enabled. -/// -/// @param cfg config instance to be modified -/// @param certPath path to SSL certificate file -/// @param keyPath path to SSL private key file +/** + * @brief add a grpc address, port and TLS certificate/key paths to config + * + * This is intended for use with envconfig, for tests that require a grpc + * server with TLS enabled. + * + * @param cfg config instance to be modified + * @param certPath path to SSL certificate file + * @param keyPath path to SSL private key file + */ std::unique_ptr addGrpcConfigWithTLS( std::unique_ptr, std::string const& certPath, std::string const& keyPath); -/// @brief add a grpc address, port and TLS certificate/key/client CA paths to config -/// -/// This is intended for use with envconfig, for tests that require a grpc -/// server with mutual TLS (client certificate verification) enabled. -/// -/// @param cfg config instance to be modified -/// @param certPath path to SSL certificate file -/// @param keyPath path to SSL private key file -/// @param clientCAPath path to SSL client CA certificate file for mTLS +/** + * @brief add a grpc address, port and TLS certificate/key/client CA paths to config + * + * This is intended for use with envconfig, for tests that require a grpc + * server with mutual TLS (client certificate verification) enabled. + * + * @param cfg config instance to be modified + * @param certPath path to SSL certificate file + * @param keyPath path to SSL private key file + * @param clientCAPath path to SSL client CA certificate file for mTLS + */ std::unique_ptr addGrpcConfigWithTLSAndClientCA( std::unique_ptr, @@ -136,15 +154,17 @@ addGrpcConfigWithTLSAndClientCA( std::string const& keyPath, std::string const& clientCAPath); -/// @brief add a grpc address, port and TLS with server cert chain to config -/// -/// This is intended for use with envconfig, for tests that require a grpc -/// server with TLS enabled and intermediate CA certificates. -/// -/// @param cfg config instance to be modified -/// @param certPath path to SSL certificate file -/// @param keyPath path to SSL private key file -/// @param certChainPath path to SSL intermediate CA certificate(s) file +/** + * @brief add a grpc address, port and TLS with server cert chain to config + * + * This is intended for use with envconfig, for tests that require a grpc + * server with TLS enabled and intermediate CA certificates. + * + * @param cfg config instance to be modified + * @param certPath path to SSL certificate file + * @param keyPath path to SSL private key file + * @param certChainPath path to SSL intermediate CA certificate(s) file + */ std::unique_ptr addGrpcConfigWithTLSAndCertChain( std::unique_ptr, diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h index 728b2e3643..68737c2e35 100644 --- a/src/test/jtx/escrow.h +++ b/src/test/jtx/escrow.h @@ -13,7 +13,9 @@ #include #include -/** Escrow operations. */ +/** + * Escrow operations. + */ namespace xrpl::test::jtx::escrow { json::Value @@ -70,10 +72,14 @@ std::array const kCb3 = { 0x3F, 0xA6, 0x3B, 0x1B, 0x60, 0x6F, 0x2D, 0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD, 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; -/** Set the "FinishAfter" time tag on a JTx */ +/** + * Set the "FinishAfter" time tag on a JTx + */ auto const kFinishTime = JTxFieldWrapper(sfFinishAfter); -/** Set the "CancelAfter" time tag on a JTx */ +/** + * Set the "CancelAfter" time tag on a JTx + */ auto const kCancelTime = JTxFieldWrapper(sfCancelAfter); auto const kCondition = JTxFieldWrapper(sfCondition); diff --git a/src/test/jtx/fee.h b/src/test/jtx/fee.h index ad3002dc75..3754036479 100644 --- a/src/test/jtx/fee.h +++ b/src/test/jtx/fee.h @@ -13,7 +13,9 @@ namespace xrpl::test::jtx { -/** Set the fee on a JTx. */ +/** + * Set the fee on a JTx. + */ class Fee { private: diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index 83470f9a1b..5bbe3c8d12 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -97,18 +97,24 @@ namespace test::jtx { // JSON generators -/** Add and/or remove flag. */ +/** + * Add and/or remove flag. + */ json::Value fset(Account const& account, std::uint32_t on, std::uint32_t off = 0); -/** Remove account flag. */ +/** + * Remove account flag. + */ inline json::Value fclear(Account const& account, std::uint32_t off) { return fset(account, 0, off); } -/** Match set account flags */ +/** + * Match set account flags + */ class Flags : private xrpl::detail::FlagsHelper { private: @@ -124,7 +130,9 @@ public: operator()(Env& env) const; }; -/** Match clear account flags */ +/** + * Match clear account flags + */ class Nflags : private xrpl::detail::FlagsHelper { private: diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 5c815d4226..8effce288e 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -326,13 +326,10 @@ AMM::expectAuctionSlot(std::vector const& authAccounts) const { return expectAuctionSlot( [&](std::uint32_t, std::optional, IOUAmount const&, STArray const& accounts) { - for (auto const& account : accounts) - { - if (std::ranges::find(authAccounts, account.getAccountID(sfAccount)) == - authAccounts.end()) - return false; - } - return true; + return std::ranges::all_of(accounts, [&](auto const& account) { + return std::ranges::find(authAccounts, account.getAccountID(sfAccount)) != + authAccounts.end(); + }); }); } diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 46c5e44bd3..3b4aae20e2 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -197,7 +197,7 @@ AMMTestBase::testAMM(std::function const& cb, TestAM XRPAmount AMMTest::reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); } XRPAmount diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index db45deb6de..3f6aca9fcb 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -271,6 +271,33 @@ Env::ownerCount(Account const& account) const return sle->getFieldU32(sfOwnerCount); } +std::uint32_t +Env::sponsoredOwnerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoredOwnerCount); +} + +std::uint32_t +Env::sponsoringOwnerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoringOwnerCount); +} + +std::uint32_t +Env::sponsoringAccountCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoringAccountCount); +} + std::uint32_t Env::seq(Account const& account) const { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 95c8a68436..4d3869b4f9 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -94,6 +94,24 @@ ownerCount(Env const& env, Account const& account) return env.ownerCount(account); } +std::uint32_t +sponsoredOwnerCount(Env const& env, Account const& account) +{ + return env.sponsoredOwnerCount(account); +} + +std::uint32_t +sponsoringOwnerCount(Env const& env, Account const& account) +{ + return env.sponsoringOwnerCount(account); +} + +std::uint32_t +sponsoringAccountCount(Env const& env, Account const& account) +{ + return env.sponsoringAccountCount(account); +} + /* Path finding */ /******************************************************************************/ void diff --git a/src/test/jtx/impl/dids.cpp b/src/test/jtx/impl/dids.cpp index d82250ca34..9c27ea21e3 100644 --- a/src/test/jtx/impl/dids.cpp +++ b/src/test/jtx/impl/dids.cpp @@ -7,7 +7,9 @@ #include #include -/** DID operations. */ +/** + * DID operations. + */ namespace xrpl::test::jtx::did { json::Value diff --git a/src/test/jtx/impl/directory.cpp b/src/test/jtx/impl/directory.cpp index 9fb06e25df..5eaf0be196 100644 --- a/src/test/jtx/impl/directory.cpp +++ b/src/test/jtx/impl/directory.cpp @@ -18,7 +18,9 @@ #include #include -/** Directory operations. */ +/** + * Directory operations. + */ namespace xrpl::test::jtx::directory { auto diff --git a/src/test/jtx/impl/escrow.cpp b/src/test/jtx/impl/escrow.cpp index 007f492849..61c260a5d0 100644 --- a/src/test/jtx/impl/escrow.cpp +++ b/src/test/jtx/impl/escrow.cpp @@ -14,7 +14,9 @@ #include -/** Escrow operations. */ +/** + * Escrow operations. + */ namespace xrpl::test::jtx::escrow { json::Value diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index 66c5a4f27d..d948042bda 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -64,7 +64,8 @@ Msig::operator()(Env& env, JTx& jt) const { auto const mySigners = signers; auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) { - // Where to put the signature. Supports sfCounterPartySignature. + // Where to put the signature. Supports sfCounterPartySignature and + // sfSponsorSignature. auto& sigObject = subField ? jtx[*subField] : jtx.jv; // The signing pub key is only required at the top level. diff --git a/src/test/jtx/impl/owners.cpp b/src/test/jtx/impl/owners.cpp index 2ff93757f0..ed10ca6930 100644 --- a/src/test/jtx/impl/owners.cpp +++ b/src/test/jtx/impl/owners.cpp @@ -44,6 +44,24 @@ Owners::operator()(Env& env) const env.test.expect(env.le(account_)->getFieldU32(sfOwnerCount) == value_); } +void +SponsoredOwners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoredOwnerCount) == value_); +} + +void +SponsoringOwners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringOwnerCount) == value_); +} + +void +SponsoringAccountCount::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringAccountCount) == value_); +} + } // namespace test::jtx } // namespace xrpl diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index 7140fad3de..e0123073b1 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -18,7 +18,7 @@ Sig::operator()(Env&, JTx& jt) const // VFALCO Inefficient pre-C++14 auto const account = *account_; auto callback = [subField = subField_, account](Env&, JTx& jtx) { - // Where to put the signature. Supports sfCounterPartySignature. + // Where to put the signature. Supports sfCounterPartySignature and sfSponsorSignature. auto& sigObject = subField ? jtx[*subField] : jtx.jv; jtx::sign(jtx.jv, account, sigObject); diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp new file mode 100644 index 0000000000..cdf68800f5 --- /dev/null +++ b/src/test/jtx/impl/sponsor.cpp @@ -0,0 +1,98 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::test::jtx::sponsor { + +json::Value +set(jtx::Account const& account, + uint32_t flags, + std::optional const reserveCount, + std::optional const feeAmount, + std::optional const maxFee) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + if (reserveCount) + jv[sfRemainingOwnerCount.jsonName] = *reserveCount; + if (feeAmount) + jv[sfFeeAmount.jsonName] = feeAmount->getJson(JsonOptions::Values::None); + if (maxFee) + jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::Values::None); + return jv; +} + +json::Value +del(jtx::Account const& account) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = tfDeleteObject; + return jv; +} + +json::Value +transfer(jtx::Account const& account, uint32_t flags, std::optional const& index) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipTransfer; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + if (index) + jv[sfObjectID.jsonName] = to_string(*index); + return jv; +} + +void +CounterpartySponsor::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfCounterpartySponsor.jsonName] = sponsor_.human(); +} + +void +SponseeAcc::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsee.jsonName] = sponsee_.human(); +} + +void +As::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsor.jsonName] = sponsor_.human(); + jt.jv[sfSponsorFlags.jsonName] = flags_; +} + +json::Value +ledgerEntry(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee) +{ + json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::sponsorship][jss::sponsor] = sponsor.human(); + jvParams[jss::sponsorship][jss::sponsee] = sponsee.human(); + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +STAmount +sponsorshipFeeBalance(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee) +{ + return env.le(keylet::sponsorship(sponsor, sponsee))->getFieldAmount(sfFeeAmount).xrp(); +} + +} // namespace xrpl::test::jtx::sponsor diff --git a/src/test/jtx/jtx_json.h b/src/test/jtx/jtx_json.h index b22d4c30e1..1ef5c155a8 100644 --- a/src/test/jtx/jtx_json.h +++ b/src/test/jtx/jtx_json.h @@ -9,7 +9,9 @@ namespace xrpl::test::jtx { -/** Inject raw JSON. */ +/** + * Inject raw JSON. + */ class Json { private: diff --git a/src/test/jtx/ledgerStateFix.h b/src/test/jtx/ledgerStateFix.h index dd1ac19f04..2fe5c8accc 100644 --- a/src/test/jtx/ledgerStateFix.h +++ b/src/test/jtx/ledgerStateFix.h @@ -5,14 +5,20 @@ #include #include -/** LedgerStateFix operations. */ +/** + * LedgerStateFix operations. + */ namespace xrpl::test::jtx::ledgerStateFix { -/** Repair the links in an NFToken directory. */ +/** + * Repair the links in an NFToken directory. + */ json::Value nftPageLinks(jtx::Account const& acct, jtx::Account const& owner); -/** Repair sfExchangeRate on a book directory's first page. */ +/** + * Repair sfExchangeRate on a book directory's first page. + */ json::Value bookExchangeRate(jtx::Account const& acct, uint256 const& bookDir); diff --git a/src/test/jtx/memo.h b/src/test/jtx/memo.h index cea4923d38..4e342816ca 100644 --- a/src/test/jtx/memo.h +++ b/src/test/jtx/memo.h @@ -8,11 +8,12 @@ namespace xrpl::test::jtx { -/** Add a memo to a JTx. - - If a memo already exists, the new - memo is appended to the array. -*/ +/** + * Add a memo to a JTx. + * + * If a memo already exists, the new + * memo is appended to the array. + */ class Memo { private: diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 1a7fe94785..c6532ab14a 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -65,7 +65,9 @@ gMakeZeroBuffer(std::size_t size) return b; } -/** @brief Test helper that checks MPT flag settings after creation. */ +/** + * @brief Test helper that checks MPT flag settings after creation. + */ class MptFlags { private: @@ -86,7 +88,9 @@ public: operator()(Env& env) const; }; -/** @brief Test helper that checks MPT issuance or holder balances. */ +/** + * @brief Test helper that checks MPT issuance or holder balances. + */ class MptBalance { private: @@ -104,7 +108,9 @@ public: operator()(Env& env) const; }; -/** @brief Test helper that accepts any condition supplied by a callback. */ +/** + * @brief Test helper that accepts any condition supplied by a callback. + */ class RequireAny { private: @@ -121,7 +127,9 @@ public: using Holders = std::vector; -/** @brief Arguments for building an MPTokenIssuanceCreate test transaction. */ +/** + * @brief Arguments for building an MPTokenIssuanceCreate test transaction. + */ struct MPTCreate { static inline std::vector allHolders = {}; @@ -145,7 +153,9 @@ struct MPTCreate std::optional err = std::nullopt; }; -/** @brief Arguments for initializing funded MPT test accounts and issuance. */ +/** + * @brief Arguments for initializing funded MPT test accounts and issuance. + */ struct MPTInit { // Default-initialized so designated-initializer call sites that omit @@ -161,7 +171,9 @@ struct MPTInit }; static MPTInit const kMptInitNoFund{.fund = false}; -/** @brief Full constructor arguments for MPTTester initialization. */ +/** + * @brief Full constructor arguments for MPTTester initialization. + */ struct MPTInitDef { Env& env; @@ -179,7 +191,9 @@ struct MPTInitDef std::optional err = std::nullopt; }; -/** @brief Arguments for building an MPTokenIssuanceDestroy test transaction. */ +/** + * @brief Arguments for building an MPTokenIssuanceDestroy test transaction. + */ struct MPTDestroy { std::optional issuer = std::nullopt; @@ -190,7 +204,9 @@ struct MPTDestroy std::optional err = std::nullopt; }; -/** @brief Arguments for building an MPTokenAuthorize test transaction. */ +/** + * @brief Arguments for building an MPTokenAuthorize test transaction. + */ struct MPTAuthorize { std::optional account = std::nullopt; @@ -202,7 +218,9 @@ struct MPTAuthorize std::optional err = std::nullopt; }; -/** @brief Arguments for building an MPTokenIssuanceSet test transaction. */ +/** + * @brief Arguments for building an MPTokenIssuanceSet test transaction. + */ struct MPTSet { std::optional account = std::nullopt; @@ -222,7 +240,9 @@ struct MPTSet std::optional err = std::nullopt; }; -/** @brief Arguments for building a ConfidentialMPTConvert test transaction. */ +/** + * @brief Arguments for building a ConfidentialMPTConvert test transaction. + */ struct MPTConvert { std::optional account = std::nullopt; @@ -250,7 +270,9 @@ struct MPTConvert std::optional err = std::nullopt; }; -/** @brief Arguments for building a ConfidentialMPTMergeInbox test transaction. */ +/** + * @brief Arguments for building a ConfidentialMPTMergeInbox test transaction. + */ struct MPTMergeInbox { std::optional account = std::nullopt; @@ -264,7 +286,9 @@ struct MPTMergeInbox std::optional err = std::nullopt; }; -/** @brief Arguments for building a ConfidentialMPTSend test transaction. */ +/** + * @brief Arguments for building a ConfidentialMPTSend test transaction. + */ struct MPTConfidentialSend { std::optional account = std::nullopt; @@ -293,7 +317,9 @@ struct MPTConfidentialSend std::optional err = std::nullopt; }; -/** @brief Arguments for building a ConfidentialMPTConvertBack test transaction. */ +/** + * @brief Arguments for building a ConfidentialMPTConvertBack test transaction. + */ struct MPTConvertBack { std::optional account = std::nullopt; @@ -316,7 +342,9 @@ struct MPTConvertBack std::optional err = std::nullopt; }; -/** @brief Arguments for building a ConfidentialMPTClawback test transaction. */ +/** + * @brief Arguments for building a ConfidentialMPTClawback test transaction. + */ struct MPTConfidentialClawback { std::optional account = std::nullopt; @@ -339,16 +367,24 @@ struct MPTConfidentialClawback */ struct PedersenProofParams { - /** @brief The Pedersen commitment used by the proof. */ + /** + * @brief The Pedersen commitment used by the proof. + */ Buffer const pedersenCommitment; - /** @brief Either the spending balance or the value being transferred. */ + /** + * @brief Either the spending balance or the value being transferred. + */ uint64_t const amt; - /** @brief The encrypted amount linked to the Pedersen commitment. */ + /** + * @brief The encrypted amount linked to the Pedersen commitment. + */ Buffer const encryptedAmt; - /** @brief The blinding factor used to create the Pedersen commitment. */ + /** + * @brief The blinding factor used to create the Pedersen commitment. + */ Buffer const blindingFactor; }; @@ -365,13 +401,19 @@ struct PedersenProofParams */ struct ConfidentialSendChainState { - /** @brief Decrypted spending balance after the previous send. */ + /** + * @brief Decrypted spending balance after the previous send. + */ std::uint64_t spending; - /** @brief Encrypted spending balance after the previous send. */ + /** + * @brief Encrypted spending balance after the previous send. + */ Buffer encSpending; - /** @brief sfConfidentialBalanceVersion after the previous send. */ + /** + * @brief sfConfidentialBalanceVersion after the previous send. + */ std::uint32_t version; }; diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index c90e28537a..65e39f971d 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -20,7 +20,9 @@ namespace xrpl::test::jtx { -/** A signer in a SignerList */ +/** + * A signer in a SignerList + */ struct Signer { std::uint32_t weight; @@ -36,24 +38,31 @@ struct Signer json::Value signers(Account const& account, std::uint32_t quorum, std::vector const& v); -/** Remove a signer list. */ +/** + * Remove a signer list. + */ json::Value signers(Account const& account, NoneT); //------------------------------------------------------------------------------ -/** Set a multisignature on a JTx. */ +/** + * Set a multisignature on a JTx. + */ class Msig { public: std::vector signers; - /** Alternative transaction object field in which to place the signer list. + /** + * Alternative transaction object field in which to place the signer list. * * subField is only supported if an account_ is provided as well. */ SField const* const subField = nullptr; - /// Used solely as a convenience placeholder for ctors that do _not_ specify - /// a subfield. + /** + * Used solely as a convenience placeholder for ctors that do _not_ specify + * a subfield. + */ static constexpr SField const* kTopLevel = nullptr; Msig(SField const* subField, std::vector signers) @@ -103,7 +112,9 @@ public: //------------------------------------------------------------------------------ -/** The number of signer lists matches. */ +/** + * The number of signer lists matches. + */ using siglists = OwnerCount; } // namespace xrpl::test::jtx diff --git a/src/test/jtx/noop.h b/src/test/jtx/noop.h index c38dfdde25..2ca97a1916 100644 --- a/src/test/jtx/noop.h +++ b/src/test/jtx/noop.h @@ -7,7 +7,9 @@ namespace xrpl::test::jtx { -/** The null transaction. */ +/** + * The null transaction. + */ inline json::Value noop(Account const& account) { diff --git a/src/test/jtx/offer.h b/src/test/jtx/offer.h index f3e7277933..140e16da83 100644 --- a/src/test/jtx/offer.h +++ b/src/test/jtx/offer.h @@ -9,7 +9,9 @@ namespace xrpl::test::jtx { -/** Create an offer. */ +/** + * Create an offer. + */ json::Value offer( Account const& account, @@ -17,7 +19,9 @@ offer( STAmount const& takerGets, std::uint32_t flags = 0); -/** Cancel an offer. */ +/** + * Cancel an offer. + */ json::Value offerCancel(Account const& account, std::uint32_t offerSeq); diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h index 0fb38b407c..1e12603429 100644 --- a/src/test/jtx/owners.h +++ b/src/test/jtx/owners.h @@ -48,7 +48,9 @@ public: } }; -/** Match the number of items in the account's owner directory */ +/** + * Match the number of items in the account's owner directory + */ class Owners { private: @@ -64,13 +66,79 @@ public: operator()(Env& env) const; }; -/** Match the number of trust lines in the account's owner directory */ +/** + * Match the account's SponsoredOwnerCount field: the number of owned + * objects whose reserve is sponsored by another account. + */ +class SponsoredOwners +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoredOwners(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** + * Match the account's SponsoringOwnerCount field: the number of objects + * (owned by other accounts) whose reserve this account sponsors. + */ +class SponsoringOwners +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoringOwners(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** + * Match the account's SponsoringAccountCount field: the number of accounts + * whose base reserve this account sponsors. + */ +class SponsoringAccountCount +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoringAccountCount(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** + * Match the number of trust lines in the account's owner directory + */ using lines = OwnerCount; -/** Match the number of offers in the account's owner directory */ +/** + * Match the number of offers in the account's owner directory + */ using offers = OwnerCount; -/** Match the number of MPToken in the account's owner directory */ +/** + * Match the number of MPToken in the account's owner directory + */ using mptokens = OwnerCount; } // namespace test::jtx diff --git a/src/test/jtx/paths.h b/src/test/jtx/paths.h index b141300a66..07d0117f8f 100644 --- a/src/test/jtx/paths.h +++ b/src/test/jtx/paths.h @@ -16,7 +16,9 @@ class STPath; namespace test::jtx { -/** Set Paths, SendMax on a JTx. */ +/** + * Set Paths, SendMax on a JTx. + */ class Paths { private: @@ -36,10 +38,11 @@ public: //------------------------------------------------------------------------------ -/** Add a path. - - If no paths are present, a new one is created. -*/ +/** + * Add a path. + * + * If no paths are present, a new one is created. + */ class Path { private: diff --git a/src/test/jtx/pay.h b/src/test/jtx/pay.h index fa193e7b02..04ccf27f70 100644 --- a/src/test/jtx/pay.h +++ b/src/test/jtx/pay.h @@ -8,7 +8,9 @@ namespace xrpl::test::jtx { -/** Create a payment. */ +/** + * Create a payment. + */ json::Value pay(AccountID const& account, AccountID const& to, AnyAmount amount); json::Value diff --git a/src/test/jtx/prop.h b/src/test/jtx/prop.h index e85751e4c9..24dca21ca0 100644 --- a/src/test/jtx/prop.h +++ b/src/test/jtx/prop.h @@ -8,7 +8,9 @@ namespace xrpl::test::jtx { -/** Set a property on a JTx. */ +/** + * Set a property on a JTx. + */ template struct Prop { diff --git a/src/test/jtx/quality.h b/src/test/jtx/quality.h index c7896fadf9..a15d319b40 100644 --- a/src/test/jtx/quality.h +++ b/src/test/jtx/quality.h @@ -7,7 +7,9 @@ namespace xrpl::test::jtx { -/** Sets the literal QualityIn on a trust JTx. */ +/** + * Sets the literal QualityIn on a trust JTx. + */ class QualityIn { private: @@ -22,7 +24,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the QualityIn on a trust JTx. */ +/** + * Sets the QualityIn on a trust JTx. + */ class QualityInPercent { private: @@ -35,7 +39,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the literal QualityOut on a trust JTx. */ +/** + * Sets the literal QualityOut on a trust JTx. + */ class QualityOut { private: @@ -50,7 +56,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the QualityOut on a trust JTx as a percentage. */ +/** + * Sets the QualityOut on a trust JTx as a percentage. + */ class QualityOutPercent { private: diff --git a/src/test/jtx/rate.h b/src/test/jtx/rate.h index f76c2dd538..f8caf39fd8 100644 --- a/src/test/jtx/rate.h +++ b/src/test/jtx/rate.h @@ -6,7 +6,9 @@ namespace xrpl::test::jtx { -/** Set a transfer rate. */ +/** + * Set a transfer rate. + */ json::Value rate(Account const& account, double multiplier); diff --git a/src/test/jtx/regkey.h b/src/test/jtx/regkey.h index 676633b2b7..8aa5bd9709 100644 --- a/src/test/jtx/regkey.h +++ b/src/test/jtx/regkey.h @@ -7,11 +7,15 @@ namespace xrpl::test::jtx { -/** Disable the regular key. */ +/** + * Disable the regular key. + */ json::Value regkey(Account const& account, DisabledT); -/** Set a regular key. */ +/** + * Set a regular key. + */ json::Value regkey(Account const& account, Account const& signer); diff --git a/src/test/jtx/require.h b/src/test/jtx/require.h index 19e161b938..eba20b613c 100644 --- a/src/test/jtx/require.h +++ b/src/test/jtx/require.h @@ -23,7 +23,9 @@ requireArgs(test::jtx::requires_t& vec, Cond const& cond, Args const&... args) namespace test::jtx { -/** Compose many condition functors into one */ +/** + * Compose many condition functors into one + */ template require_t required(Args const&... args) @@ -36,12 +38,13 @@ required(Args const&... args) }; } -/** Check a set of conditions. - - The conditions are checked after a JTx is - applied, and only if the resulting TER - matches the expected TER. -*/ +/** + * Check a set of conditions. + * + * The conditions are checked after a JTx is + * applied, and only if the resulting TER + * matches the expected TER. + */ class Require { private: diff --git a/src/test/jtx/rpc.h b/src/test/jtx/rpc.h index bc05450909..9bd99c15f8 100644 --- a/src/test/jtx/rpc.h +++ b/src/test/jtx/rpc.h @@ -12,9 +12,10 @@ namespace xrpl::test::jtx { -/** Set the expected result code for a JTx - The test will fail if the code doesn't match. -*/ +/** + * Set the expected result code for a JTx + * The test will fail if the code doesn't match. + */ class Rpc { private: @@ -24,13 +25,17 @@ private: std::optional errorException_; public: - /// If there's an error code, we expect an error message + /** + * If there's an error code, we expect an error message + */ explicit Rpc(ErrorCodeI code, std::optional m = {}) : code_(code), errorMessage_(std::move(m)) { } - /// If there is not a code, we expect an exception message + /** + * If there is not a code, we expect an exception message + */ explicit Rpc(std::string error, std::optional exceptionMessage = {}) : error_(error), errorException_(std::move(exceptionMessage)) { diff --git a/src/test/jtx/sendmax.h b/src/test/jtx/sendmax.h index 1241d76b91..672959d1aa 100644 --- a/src/test/jtx/sendmax.h +++ b/src/test/jtx/sendmax.h @@ -9,7 +9,9 @@ namespace xrpl::test::jtx { -/** Sets the SendMax on a JTx. */ +/** + * Sets the SendMax on a JTx. + */ class Sendmax { private: diff --git a/src/test/jtx/seq.h b/src/test/jtx/seq.h index 956c0a77e8..323fb33bb2 100644 --- a/src/test/jtx/seq.h +++ b/src/test/jtx/seq.h @@ -9,7 +9,9 @@ namespace xrpl::test::jtx { -/** Set the sequence number on a JTx. */ +/** + * Set the sequence number on a JTx. + */ struct Seq { private: diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h index 76f0a34dff..1f20dcf942 100644 --- a/src/test/jtx/sig.h +++ b/src/test/jtx/sig.h @@ -11,26 +11,31 @@ namespace xrpl::test::jtx { -/** Set the regular signature on a JTx. - @note For multisign, use msig. -*/ +/** + * Set the regular signature on a JTx. + * @note For multisign, use msig. + */ class Sig { private: bool manual_ = true; - /** Alternative transaction object field in which to place the signature. + /** + * Alternative transaction object field in which to place the signature. * * subField is only supported if an account_ is provided as well. */ SField const* const subField_ = nullptr; - /** Account that will generate the signature. + /** + * Account that will generate the signature. * * If not provided, no signature will be added by this helper. See also * Env::autofillSig. */ std::optional account_; - /// Used solely as a convenience placeholder for ctors that do _not_ specify - /// a subfield. + /** + * Used solely as a convenience placeholder for ctors that do _not_ specify + * a subfield. + */ static constexpr SField const* kTopLevel = nullptr; public: diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h new file mode 100644 index 0000000000..43d55d7246 --- /dev/null +++ b/src/test/jtx/sponsor.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl::test::jtx::sponsor { + +json::Value +set(jtx::Account const& account, + std::uint32_t flags, + std::optional const reserveCount = std::nullopt, + std::optional const feeAmount = std::nullopt, + std::optional const maxFee = std::nullopt); + +inline json::Value +set_fee( + jtx::Account const& account, + std::uint32_t flags, + STAmount feeAmount, + std::optional maxFee = std::nullopt) +{ + return set(account, flags, std::nullopt, std::move(feeAmount), std::move(maxFee)); +} + +inline json::Value +set_reserve(jtx::Account const& account, std::uint32_t flags, std::uint32_t reserveCount) +{ + return set(account, flags, reserveCount); +} + +inline json::Value +set_max_fee(jtx::Account const& account, std::uint32_t flags, STAmount maxFee) +{ + return set(account, flags, std::nullopt, std::nullopt, std::move(maxFee)); +} + +json::Value +del(jtx::Account const& account); + +json::Value +transfer( + jtx::Account const& account, + uint32_t flags, + std::optional const& index = std::nullopt); + +struct CounterpartySponsor +{ +private: + jtx::Account sponsor_; + +public: + CounterpartySponsor(jtx::Account account) : sponsor_(std::move(account)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct SponseeAcc +{ +private: + jtx::Account sponsee_; + +public: + SponseeAcc(jtx::Account account) : sponsee_(std::move(account)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct As +{ +private: + jtx::Account sponsor_; + std::uint32_t flags_; + +public: + As(jtx::Account account, std::uint32_t flags = 0) : sponsor_(std::move(account)), flags_(flags) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +json::Value +ledgerEntry(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee); + +STAmount +sponsorshipFeeBalance(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee); + +} // namespace xrpl::test::jtx::sponsor diff --git a/src/test/jtx/tag.h b/src/test/jtx/tag.h index 77870367a9..b9a10e55f4 100644 --- a/src/test/jtx/tag.h +++ b/src/test/jtx/tag.h @@ -7,7 +7,9 @@ namespace xrpl::test::jtx { -/** Set the destination tag on a JTx*/ +/** + * Set the destination tag on a JTx + */ struct Dtag { private: @@ -22,7 +24,9 @@ public: operator()(Env&, JTx& jt) const; }; -/** Set the source tag on a JTx*/ +/** + * Set the source tag on a JTx + */ struct Stag { private: diff --git a/src/test/jtx/tags.h b/src/test/jtx/tags.h index 4f4afbd1fa..1915c57b5c 100644 --- a/src/test/jtx/tags.h +++ b/src/test/jtx/tags.h @@ -20,7 +20,9 @@ struct DisabledT }; static DisabledT const kDisabled; -/** Used for Fee() calls that use an owner reserve increment */ +/** + * Used for Fee() calls that use an owner reserve increment + */ struct IncrementT { IncrementT() = default; diff --git a/src/test/jtx/ter.h b/src/test/jtx/ter.h index 880711dca0..0ac65e62ee 100644 --- a/src/test/jtx/ter.h +++ b/src/test/jtx/ter.h @@ -10,9 +10,10 @@ namespace xrpl::test::jtx { -/** Set the expected result code for a JTx - The test will fail if the code doesn't match. -*/ +/** + * Set the expected result code for a JTx + * The test will fail if the code doesn't match. + */ class Ter { private: diff --git a/src/test/jtx/ticket.h b/src/test/jtx/ticket.h index 1035be7674..9c9a698eaa 100644 --- a/src/test/jtx/ticket.h +++ b/src/test/jtx/ticket.h @@ -18,14 +18,20 @@ namespace xrpl::test::jtx { without changing the base declarations. */ -/** Ticket operations */ +/** + * Ticket operations + */ namespace ticket { -/** Create one of more tickets */ +/** + * Create one of more tickets + */ json::Value create(Account const& account, std::uint32_t count); -/** Set a ticket sequence on a JTx. */ +/** + * Set a ticket sequence on a JTx. + */ class Use { private: @@ -42,7 +48,9 @@ public: } // namespace ticket -/** Match the number of tickets on the account. */ +/** + * Match the number of tickets on the account. + */ using tickets = OwnerCount; } // namespace xrpl::test::jtx diff --git a/src/test/jtx/token.h b/src/test/jtx/token.h index 97f968fdfc..e94cfd2e12 100644 --- a/src/test/jtx/token.h +++ b/src/test/jtx/token.h @@ -16,11 +16,15 @@ namespace xrpl::test::jtx::token { -/** Mint an NFToken. */ +/** + * Mint an NFToken. + */ json::Value mint(jtx::Account const& account, std::uint32_t tokenTaxon = 0); -/** Sets the optional TransferFee on an NFTokenMint. */ +/** + * Sets the optional TransferFee on an NFTokenMint. + */ class XferFee { private: @@ -35,7 +39,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the optional Issuer on an NFTokenMint. */ +/** + * Sets the optional Issuer on an NFTokenMint. + */ class Issuer { private: @@ -50,7 +56,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the optional URI on an NFTokenMint. */ +/** + * Sets the optional URI on an NFTokenMint. + */ class Uri { private: @@ -65,7 +73,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the optional amount field on an NFTokenMint. */ +/** + * Sets the optional amount field on an NFTokenMint. + */ class Amount { private: @@ -80,7 +90,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Get the next NFTokenID that will be issued. */ +/** + * Get the next NFTokenID that will be issued. + */ uint256 getNextID( jtx::Env const& env, @@ -89,7 +101,9 @@ getNextID( std::uint16_t flags = 0, std::uint16_t xferFee = 0); -/** Get the NFTokenID for a particular nftSequence. */ +/** + * Get the NFTokenID for a particular nftSequence. + */ uint256 getID( jtx::Env const& env, @@ -99,15 +113,21 @@ getID( std::uint16_t flags = 0, std::uint16_t xferFee = 0); -/** Burn an NFToken. */ +/** + * Burn an NFToken. + */ json::Value burn(jtx::Account const& account, uint256 const& nftokenID); -/** Create an NFTokenOffer. */ +/** + * Create an NFTokenOffer. + */ json::Value createOffer(jtx::Account const& account, uint256 const& nftokenID, STAmount const& amount); -/** Sets the optional Owner on an NFTokenOffer. */ +/** + * Sets the optional Owner on an NFTokenOffer. + */ class Owner { private: @@ -122,7 +142,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the optional Expiration field on an NFTokenOffer. */ +/** + * Sets the optional Expiration field on an NFTokenOffer. + */ class Expiration { private: @@ -137,7 +159,9 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Sets the optional Destination field on an NFTokenOffer. */ +/** + * Sets the optional Destination field on an NFTokenOffer. + */ class Destination { private: @@ -152,14 +176,18 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Cancel NFTokenOffers. */ +/** + * Cancel NFTokenOffers. + */ json::Value cancelOffer(jtx::Account const& account, std::initializer_list const& nftokenOffers = {}); json::Value cancelOffer(jtx::Account const& account, std::vector const& nftokenOffers); -/** Sets the optional RootIndex field when canceling NFTokenOffers. */ +/** + * Sets the optional RootIndex field when canceling NFTokenOffers. + */ class RootIndex { private: @@ -174,22 +202,30 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Accept an NFToken buy offer. */ +/** + * Accept an NFToken buy offer. + */ json::Value acceptBuyOffer(jtx::Account const& account, uint256 const& offerIndex); -/** Accept an NFToken sell offer. */ +/** + * Accept an NFToken sell offer. + */ json::Value acceptSellOffer(jtx::Account const& account, uint256 const& offerIndex); -/** Broker two NFToken offers. */ +/** + * Broker two NFToken offers. + */ json::Value brokerOffers( jtx::Account const& account, uint256 const& buyOfferIndex, uint256 const& sellOfferIndex); -/** Sets the optional NFTokenBrokerFee field in a brokerOffer transaction. */ +/** + * Sets the optional NFTokenBrokerFee field in a brokerOffer transaction. + */ class BrokerFee { private: @@ -204,15 +240,21 @@ public: operator()(Env&, JTx& jtx) const; }; -/** Set the authorized minter on an account root. */ +/** + * Set the authorized minter on an account root. + */ json::Value setMinter(jtx::Account const& account, jtx::Account const& minter); -/** Clear any authorized minter from an account root. */ +/** + * Clear any authorized minter from an account root. + */ json::Value clearMinter(jtx::Account const& account); -/** Modify an NFToken. */ +/** + * Modify an NFToken. + */ json::Value modify(jtx::Account const& account, uint256 const& nftokenID); diff --git a/src/test/jtx/trust.h b/src/test/jtx/trust.h index 034f80fcec..235780c825 100644 --- a/src/test/jtx/trust.h +++ b/src/test/jtx/trust.h @@ -10,11 +10,15 @@ namespace xrpl::test::jtx { -/** Modify a trust line. */ +/** + * Modify a trust line. + */ json::Value trust(Account const& account, STAmount const& amount, std::uint32_t flags = 0); -/** Change flags on a trust line. */ +/** + * Change flags on a trust line. + */ json::Value trust(Account const& account, STAmount const& amount, Account const& peer, std::uint32_t flags); diff --git a/src/test/jtx/txflags.h b/src/test/jtx/txflags.h index 7f5b31b2ac..c940681d82 100644 --- a/src/test/jtx/txflags.h +++ b/src/test/jtx/txflags.h @@ -7,7 +7,9 @@ namespace xrpl::test::jtx { -/** Set the flags on a JTx. */ +/** + * Set the flags on a JTx. + */ class Txflags { private: diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index f1cf3f7ae8..289afec8b3 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -13,7 +13,9 @@ namespace xrpl::test::jtx { -/** Thrown when parse fails. */ +/** + * Thrown when parse fails. + */ struct ParseError : std::logic_error { template @@ -22,35 +24,44 @@ struct ParseError : std::logic_error } }; -/** Convert JSON to STObject. - This throws on failure, the JSON must be correct. - @note Testing malformed JSON is beyond the scope of - this set of unit test routines. -*/ +/** + * Convert JSON to STObject. + * This throws on failure, the JSON must be correct. + * @note Testing malformed JSON is beyond the scope of + * this set of unit test routines. + */ STObject parse(json::Value const& jv); -/** Sign automatically into a specific Json field of the jv object. - @note This only works on accounts with multi-signing off. -*/ +/** + * Sign automatically into a specific Json field of the jv object. + * @note This only works on accounts with multi-signing off. + */ void sign(json::Value& jv, Account const& account, json::Value& sigObject); -/** Sign automatically. - @note This only works on accounts with multi-signing off. -*/ +/** + * Sign automatically. + * @note This only works on accounts with multi-signing off. + */ void sign(json::Value& jv, Account const& account); -/** Set the fee automatically. */ +/** + * Set the fee automatically. + */ void fillFee(json::Value& jv, ReadView const& view); -/** Set the sequence number automatically. */ +/** + * Set the sequence number automatically. + */ void fillSeq(json::Value& jv, ReadView const& view); -/** Given an xrpld unit test rpc command, return the corresponding JSON. */ +/** + * Given an xrpld unit test rpc command, return the corresponding JSON. + */ json::Value cmdToJSONRPC(std::vector const& args, beast::Journal j, unsigned int apiVersion); } // namespace xrpl::test::jtx diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h index 4e6b90fe1f..e72eae89b7 100644 --- a/src/test/jtx/vault.h +++ b/src/test/jtx/vault.h @@ -27,7 +27,9 @@ struct Vault std::nullopt; // NOLINT(readability-redundant-member-init) }; - /** Return a VaultCreate transaction and the Vault's expected keylet. */ + /** + * Return a VaultCreate transaction and the Vault's expected keylet. + */ [[nodiscard]] std::tuple create(CreateArgs const& args) const; diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index f59b75091a..75f7410f94 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -1,17 +1,23 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -19,7 +25,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -236,7 +244,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite auto const startingAmount = accountHolds(pv, alice, iss.currency, iss.account, FreezeHandling::IgnoreFreeze, j); - BEAST_EXPECT(issueIOU(pv, alice, toCredit, iss, j) == tesSUCCESS); + BEAST_EXPECT(issueIOU(pv, alice, toCredit, iss, {}, j) == tesSUCCESS); BEAST_EXPECT( accountHolds( pv, alice, iss.currency, iss.account, FreezeHandling::IgnoreFreeze, j) == @@ -330,7 +338,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite }; auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount { - return env.current()->fees().accountReserve(count); + return env.current()->fees().accountReserve(count, 1); }; Env env(*this, features); @@ -389,6 +397,217 @@ class PaymentSandbox_test : public beast::unit_test::Suite BEAST_EXPECT(balance.getIssuer() == usd.account.id()); } + void + testOwnerCountHook(FeatureBitset features) + { + // Test that PaymentSandbox::adjustOwnerCountHook and ownerCountHook + // correctly track and return the maximum owner counts during a payment. + testcase("ownerCountHook"); + + using namespace jtx; + Env env(*this, features); + Account const alice("alice"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + ApplyViewImpl av(&*env.current(), TapNone); + PaymentSandbox sb(&av); + + // Test basic owner count hook without sponsor + { + auto const aliceSle = sb.peek(keylet::account(alice)); + BEAST_EXPECT(aliceSle); + + OwnerCounts const initial(aliceSle); + OwnerCounts updated = initial; + updated.owner = initial.owner + 2; + + // Simulate adjusting owner count + sb.adjustOwnerCountHook(alice, initial, updated); + + // ownerCountHook should return the max value + OwnerCounts const retrieved = sb.ownerCountHook(alice, initial); + BEAST_EXPECT(retrieved.owner == updated.owner); + BEAST_EXPECT(retrieved.sponsored == updated.sponsored); + BEAST_EXPECT(retrieved.sponsoring == updated.sponsoring); + } + + // Test owner count hook with sponsor-related counts + { + auto const sponsorSle = sb.peek(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle); + + OwnerCounts const sponsorInitial(sponsorSle); + OwnerCounts sponsorUpdated = sponsorInitial; + sponsorUpdated.owner = sponsorInitial.owner + 1; + sponsorUpdated.sponsoring = sponsorInitial.sponsoring + 1; + + sb.adjustOwnerCountHook(sponsor, sponsorInitial, sponsorUpdated); + + OwnerCounts const sponsorRetrieved = sb.ownerCountHook(sponsor, sponsorInitial); + BEAST_EXPECT(sponsorRetrieved.owner == sponsorUpdated.owner); + BEAST_EXPECT(sponsorRetrieved.sponsoring == sponsorUpdated.sponsoring); + } + + // Test with stacked PaymentSandboxes + { + PaymentSandbox sb2(&sb); + + auto const aliceSle = sb2.peek(keylet::account(alice)); + OwnerCounts const current(aliceSle); + OwnerCounts further = current; + further.owner = current.owner + 3; + + sb2.adjustOwnerCountHook(alice, current, further); + + // The nested sandbox should see the max from both levels + OwnerCounts const retrieved = sb2.ownerCountHook(alice, OwnerCounts()); + BEAST_EXPECT(retrieved.owner >= further.owner); + } + + // Test that max logic works correctly + { + auto const aliceSle = sb.peek(keylet::account(alice)); + OwnerCounts const current(aliceSle); + OwnerCounts lower = current; + lower.owner = (current.owner > 0) ? current.owner - 1 : 0; + + // Adjusting to a lower value + sb.adjustOwnerCountHook(alice, current, lower); + + // Should still return the higher value seen previously + OwnerCounts const retrieved = sb.ownerCountHook(alice, OwnerCounts()); + BEAST_EXPECT(retrieved.owner >= lower.owner); + } + } + + void + testOwnerCountWithTransaction(FeatureBitset features) + { + // Test that owner count hooks work correctly during actual transactions. + // This verifies that when transactions modify owner counts (by creating + // or deleting ledger objects), the hooks properly track these changes. + testcase( + std::string("ownerCountWithTransaction") + + (features[featureSponsor] ? " with sponsor" : " without sponsor")); + + using namespace jtx; + + auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount { + return env.current()->fees().accountReserve(count, 1); + }; + + Env env(*this, features); + Account const gw("gw"); + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + auto const usd = gw["USD"]; + + // Fund accounts. Alice starts with exactly enough for base reserve + 2 objects + env.fund(XRP(10000), gw, bob, sponsor); + env.fund(reserve(env, 3) + XRP(100), alice); // Base + 2 objects + extra for fees + env.close(); + + // Verify initial state - no owner count + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + + // Create a trust line - this increases owner count + env(trust(alice, usd(1000))); + env.close(); + + // alice now has 1 object (owner count = 1) + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // Create an offer - this further increases owner count + env(trust(bob, usd(1000))); + env(pay(gw, alice, usd(100))); + env.close(); + + auto const aliceOfferSeq = env.seq(alice); // Capture the sequence before creating offer + env(offer(alice, usd(50), XRP(50))); + env.close(); + + // alice now has 2 objects (trust line + offer) + BEAST_EXPECT(ownerCount(env, alice) == 2); + + // If sponsor feature is enabled, test sponsorship transfer + if (features[featureSponsor]) + { + auto const trustId = keylet::trustLine(alice, gw, usd.currency); + BEAST_EXPECT(env.le(trustId)); + + // Transfer sponsorship - sponsor now sponsors alice's trust line + env(sponsor::transfer(alice, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // alice still has 2 objects but 1 is sponsored + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + // sponsor's sponsoring count should increase + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + + // Verify alice's available balance respects the reserve + auto const aliceSle = env.le(keylet::account(alice)); + BEAST_EXPECT(aliceSle); + + auto const aliceBalance = aliceSle->getFieldAmount(sfBalance); + // With sponsor, 1 object is sponsored so only 1 counts for reserve + auto const aliceReserve = reserve(env, features[featureSponsor] ? 1 : 2); + + // alice should have limited available balance after accounting for reserve + auto const available = aliceBalance.xrp() - aliceReserve; + if (features[featureSponsor]) + { + // With sponsor, alice has more available (1 sponsored object = less reserve) + BEAST_EXPECT(available > XRP(150)); + } + else + { + BEAST_EXPECT(available < XRP(150)); // Most of the balance is in reserve + } + + // Try to send nearly all balance - should fail due to reserve in both cases + auto const tooMuch = aliceBalance.xrp() - XRP(1); + env(pay(alice, bob, tooMuch), Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + // Verify owner count hasn't changed + BEAST_EXPECT(ownerCount(env, alice) == 2); + + // Cancel the offer - this decreases owner count + env(offerCancel(alice, aliceOfferSeq)); + env.close(); + + // alice now has 1 object (just the trust line) + BEAST_EXPECT(ownerCount(env, alice) == 1); + + if (features[featureSponsor]) + { + // Verify sponsored count stayed the same (trust line is still sponsored) + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + + // Now alice should have more available balance (less reserve needed) + auto const aliceSle2 = env.le(keylet::account(alice)); + auto const aliceBalance2 = aliceSle2->getFieldAmount(sfBalance); + // With sponsor, trust line is still sponsored so 0 objects for reserve + // Without sponsor, 1 object for reserve + auto const aliceReserve2 = reserve(env, features[featureSponsor] ? 0 : 1); + auto const available2 = aliceBalance2.xrp() - aliceReserve2; + + // available2 should be greater than available (less reserve needed) + BEAST_EXPECT(available2 > available); + } + public: void run() override @@ -399,11 +618,16 @@ public: testTinyBalance(features); testReserve(features); testBalanceHook(features); + testOwnerCountHook(features); }; using namespace jtx; auto const sa = testableAmendments(); testAll(sa - featurePermissionedDEX); testAll(sa); + + // Test owner count with transactions + testOwnerCountWithTransaction(sa - featureSponsor); + testOwnerCountWithTransaction(sa); } }; diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h index a1245ec963..235e76501f 100644 --- a/src/test/nodestore/TestBase.h +++ b/src/test/nodestore/TestBase.h @@ -20,13 +20,14 @@ namespace xrpl::NodeStore { -/** Binary function that satisfies the strict-weak-ordering requirement. - - This compares the hashes of both objects and returns true if - the first hash is considered to go before the second. - - @see std::sort -*/ +/** + * Binary function that satisfies the strict-weak-ordering requirement. + * + * This compares the hashes of both objects and returns true if + * the first hash is considered to go before the second. + * + * @see std::sort + */ struct LessThan { bool @@ -37,7 +38,9 @@ struct LessThan } }; -/** Returns `true` if objects are identical. */ +/** + * Returns `true` if objects are identical. + */ inline bool isSame(std::shared_ptr const& lhs, std::shared_ptr const& rhs) { diff --git a/src/test/nodestore/Timing_test.cpp b/src/test/nodestore/Timing_test.cpp index 1c296ca44c..1f282d9d7f 100644 --- a/src/test/nodestore/Timing_test.cpp +++ b/src/test/nodestore/Timing_test.cpp @@ -692,9 +692,6 @@ public: #if XRPL_ROCKSDB_AVAILABLE ";type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256," "file_size_mb=8,file_size_mult=2" -#endif -#if 0 - ";type=memory|path=NodeStore" #endif ; diff --git a/src/test/overlay/cluster_test.cpp b/src/test/overlay/cluster_test.cpp index 6c2114b7de..0a51f98594 100644 --- a/src/test/overlay/cluster_test.cpp +++ b/src/test/overlay/cluster_test.cpp @@ -150,7 +150,7 @@ public: { auto member = c->member(node); BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare(name) == 0); // NOLINT(bugprone-unchecked-optional-access) + BEAST_EXPECT(*member == name); // NOLINT(bugprone-unchecked-optional-access) } // Updating the name (non-empty doesn't go to empty) @@ -159,7 +159,7 @@ public: { auto member = c->member(node); BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare(name) == 0); // NOLINT(bugprone-unchecked-optional-access) + BEAST_EXPECT(*member == name); // NOLINT(bugprone-unchecked-optional-access) } // Updating the name (non-empty updates to new non-empty) @@ -168,8 +168,7 @@ public: { auto member = c->member(node); BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT( - member->compare("test") == 0); // NOLINT(bugprone-unchecked-optional-access) + BEAST_EXPECT(*member == "test"); // NOLINT(bugprone-unchecked-optional-access) } } diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index 1c9bf1f9c1..2f42313037 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -64,7 +64,8 @@ static constexpr std::uint32_t kMaxPeers = 10; static constexpr std::uint32_t kMaxValidators = 10; static constexpr std::uint32_t kMaxMessages = 200000; -/** Simulate two entities - peer directly connected to the server +/** + * Simulate two entities - peer directly connected to the server * (via squelch in PeerSim) and PeerImp (via Overlay) */ class PeerPartial : public Peer @@ -192,7 +193,9 @@ public: } }; -/** Manually advanced clock. */ +/** + * Manually advanced clock. + */ class ManualClock { public: @@ -238,7 +241,9 @@ private: inline static time_point kNow = time_point(seconds(0)); }; -/** Simulate server's OverlayImpl */ +/** + * Simulate server's OverlayImpl + */ class Overlay { public: @@ -260,7 +265,8 @@ public: class Validator; -/** Simulate link from a validator to a peer directly connected +/** + * Simulate link from a validator to a peer directly connected * to the server. */ class Link @@ -317,18 +323,19 @@ private: bool up_{true}; }; -/** Simulate Validator */ +/** + * Simulate Validator + */ class Validator { using Links = std::unordered_map; public: - Validator() : pkey_(std::get<0>(randomKeyPair(KeyType::Ed25519))) + Validator() : pkey_(std::get<0>(randomKeyPair(KeyType::Ed25519))), id_(sid++) { protocol::TMValidation v; v.set_validation("validation"); message_ = std::make_shared(v, protocol::mtVALIDATION, pkey_); - id_ = sid++; } Validator(Validator const&) = default; Validator(Validator&&) = default; @@ -401,14 +408,18 @@ public: } } - /** Send to specific peers */ + /** + * Send to specific peers + */ void send(std::vector peers, SquelchCB f) { forLinks(peers, [&](Link& link, MessageSPtr m) { link.send(m, f); }); } - /** Send to all peers */ + /** + * Send to all peers + */ void send(SquelchCB f) { @@ -457,7 +468,6 @@ public: using id_t = Peer::id_t; PeerSim(Overlay& overlay, beast::Journal journal) : overlay_(overlay), squelch_(journal) { - id_ = sid++; } ~PeerSim() override = default; @@ -480,7 +490,9 @@ public: sid = 0; } - /** Local Peer (PeerImp) */ + /** + * Local Peer (PeerImp) + */ void onMessage(MessageSPtr const& m, SquelchCB f) override { @@ -493,7 +505,9 @@ public: {}, *validator, id(), f); // NOLINT(bugprone-unchecked-optional-access) } - /** Remote Peer (Directly connected Peer) */ + /** + * Remote Peer (Directly connected Peer) + */ void onMessage(protocol::TMSquelch const& squelch) override { @@ -512,7 +526,7 @@ public: private: inline static id_t sid = 0; std::string fingerprint_; - id_t id_; + id_t id_{sid++}; Overlay& overlay_; reduce_relay::Squelch squelch_; }; @@ -838,19 +852,18 @@ public: } } - /** Is peer in Selected state in any of the slots */ + /** + * Is peer in Selected state in any of the slots + */ bool isSelected(Peer::id_t id) { - for (auto& v : validators_) - { - if (overlay_.isSelected(v, id)) - return true; - } - return false; + return std::ranges::any_of( + validators_, [&](auto& v) { return overlay_.isSelected(v, id); }); } - /** Check if there are peers to unsquelch - peer is in Selected + /** + * Check if there are peers to unsquelch - peer is in Selected * state in any of the slots and there are peers in Squelched state * in those slots. */ @@ -894,7 +907,9 @@ protected: std::cout << std::endl; } - /** Send squelch (if duration is set) or unsquelch (if duration not set) */ + /** + * Send squelch (if duration is set) or unsquelch (if duration not set) + */ static Peer::id_t sendSquelch( PublicKey const& validator, @@ -932,7 +947,8 @@ protected: bool handled = false; }; - /** Randomly brings the link between a validator and a peer down. + /** + * Randomly brings the link between a validator and a peer down. * Randomly disconnects a peer. Those events are generated one at a time. */ void @@ -1110,7 +1126,8 @@ protected: f(log); } - /** Initial counting round: three peers receive message "faster" then + /** + * Initial counting round: three peers receive message "faster" then * others. Once the message count for the three peers reaches threshold * the rest of the peers are squelched and the slot for the given validator * is in Selected state. @@ -1121,7 +1138,8 @@ protected: doTest("Initial Round", log, [this](bool log) { BEAST_EXPECT(propagateAndSquelch(log)); }); } - /** Receiving message from squelched peer too soon should not change the + /** + * Receiving message from squelched peer too soon should not change the * slot's state to Counting. */ void @@ -1132,7 +1150,8 @@ protected: }); } - /** Receiving message from squelched peer should change the + /** + * Receiving message from squelched peer should change the * slot's state to Counting. */ void @@ -1144,7 +1163,9 @@ protected: }); } - /** Propagate enough messages to generate one squelch event */ + /** + * Propagate enough messages to generate one squelch event + */ bool propagateAndSquelch(bool log, bool purge = true, bool resetClock = true) { @@ -1178,7 +1199,9 @@ protected: return n == 1 && res; } - /** Send fewer message so that squelch event is not generated */ + /** + * Send fewer message so that squelch event is not generated + */ bool propagateNoSquelch( bool log, @@ -1205,7 +1228,8 @@ protected: return !squelched && res; } - /** Receiving a message from new peer should change the + /** + * Receiving a message from new peer should change the * slot's state to Counting. */ void @@ -1218,8 +1242,10 @@ protected: }); } - /** Selected peer disconnects. Should change the state to counting and - * unsquelch squelched peers. */ + /** + * Selected peer disconnects. Should change the state to counting and + * unsquelch squelched peers. + */ void testSelectedPeerDisconnects(bool log) { @@ -1237,8 +1263,10 @@ protected: }); } - /** Selected peer stops relaying. Should change the state to counting and - * unsquelch squelched peers. */ + /** + * Selected peer stops relaying. Should change the state to counting and + * unsquelch squelched peers. + */ void testSelectedPeerStopsRelaying(bool log) { @@ -1257,7 +1285,8 @@ protected: }); } - /** Squelched peer disconnects. Should not change the state to counting. + /** + * Squelched peer disconnects. Should not change the state to counting. */ void testSquelchedPeerDisconnects(bool log) diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp index c4f129c1a3..cf91800951 100644 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ b/src/test/peerfinder/PeerFinder_test.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include namespace xrpl::PeerFinder { @@ -64,8 +65,9 @@ public: void asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) { + // NOLINTNEXTLINE(misc-const-correctness) boost::system::error_code ec; - handler(ep, ep, ec); + handler(ec); } }; @@ -368,6 +370,259 @@ public: } } + void + testIsValidAddress() + { + testcase("is_valid_address"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + + auto const pass = [&](std::string const& s) { + BEAST_EXPECT(logic.isValidAddress(beast::IP::Endpoint::fromString(s))); + }; + auto const fail = [&](std::string const& s) { + BEAST_EXPECT(!logic.isValidAddress(beast::IP::Endpoint::fromString(s))); + }; + + // Invalid: port 0 + fail("65.0.0.1:0"); + + // --- IPv4 ranges --- + // For each range: 1 before (pass), first (fail), last (fail), + // 1 after (pass) + + // 0.0.0.0/8 - "This network" + // No "before" - nothing before 0.0.0.0 + fail("0.0.0.0:8080"); + fail("0.255.255.255:8080"); + pass("1.0.0.0:8080"); + + // 10.0.0.0/8 - Private (RFC 1918) + pass("9.255.255.255:8080"); + fail("10.0.0.0:8080"); + fail("10.255.255.255:8080"); + pass("11.0.0.0:8080"); + + // 100.64.0.0/10 - Shared Address Space / CGNAT (RFC 6598) + pass("100.63.255.255:8080"); + fail("100.64.0.0:8080"); + fail("100.127.255.255:8080"); + pass("100.128.0.0:8080"); + + // 127.0.0.0/8 - Loopback + pass("126.255.255.255:8080"); + fail("127.0.0.0:8080"); + fail("127.255.255.255:8080"); + pass("128.0.0.0:8080"); + + // 169.254.0.0/16 - Link-local + pass("169.253.255.255:8080"); + fail("169.254.0.0:8080"); + fail("169.254.255.255:8080"); + pass("169.255.0.0:8080"); + + // 172.16.0.0/12 - Private (RFC 1918) + pass("172.15.255.255:8080"); + fail("172.16.0.0:8080"); + fail("172.31.255.255:8080"); + pass("172.32.0.0:8080"); + + // 192.0.0.0/24 - IETF Protocol Assignments (RFC 6890) + pass("191.255.255.255:8080"); + fail("192.0.0.0:8080"); + fail("192.0.0.255:8080"); + pass("192.0.1.0:8080"); + + // 192.0.2.0/24 - TEST-NET-1 (RFC 5737) + pass("192.0.1.255:8080"); + fail("192.0.2.0:8080"); + fail("192.0.2.255:8080"); + pass("192.0.3.0:8080"); + + // 192.88.99.0/24 - 6to4 Relay Anycast (RFC 7526) + pass("192.88.98.255:8080"); + fail("192.88.99.0:8080"); + fail("192.88.99.255:8080"); + pass("192.88.100.0:8080"); + + // 192.168.0.0/16 - Private (RFC 1918) + pass("192.167.255.255:8080"); + fail("192.168.0.0:8080"); + fail("192.168.255.255:8080"); + pass("192.169.0.0:8080"); + + // 198.18.0.0/15 - Benchmarking (RFC 2544) + pass("198.17.255.255:8080"); + fail("198.18.0.0:8080"); + fail("198.19.255.255:8080"); + pass("198.20.0.0:8080"); + + // 198.51.100.0/24 - TEST-NET-2 (RFC 5737) + pass("198.51.99.255:8080"); + fail("198.51.100.0:8080"); + fail("198.51.100.255:8080"); + pass("198.51.101.0:8080"); + + // 203.0.113.0/24 - TEST-NET-3 (RFC 5737) + pass("203.0.112.255:8080"); + fail("203.0.113.0:8080"); + fail("203.0.113.255:8080"); + pass("203.0.114.0:8080"); + + // 224.0.0.0/4 - Multicast + pass("223.255.255.255:8080"); + fail("224.0.0.0:8080"); + fail("239.255.255.255:8080"); + // 240.0.0.0 (after multicast) is also blocked (reserved) + + // 240.0.0.0/4 - Reserved (RFC 1112) + // 239.255.255.255 (before reserved) is also blocked (multicast) + fail("240.0.0.0:8080"); + fail("255.255.255.255:8080"); + + // --- IPv6 ranges --- + + // ::1 - Loopback (single address) + fail("[::1]:8080"); + + // :: - Unspecified (single address) + fail("[::]:8080"); + + // fc00::/7 - Unique Local Address (ULA) + pass("[fb00::1]:8080"); + fail("[fc00::1]:8080"); + fail("[fdff::1]:8080"); + pass("[fe00::1]:8080"); + + // fe80::/10 - Link-local + pass("[fe7f::1]:8080"); + fail("[fe80::1]:8080"); + fail("[febf::1]:8080"); + pass("[fec0::1]:8080"); + + // ff00::/8 - Multicast + pass("[feff::1]:8080"); + fail("[ff00::1]:8080"); + fail("[ffff::1]:8080"); + // No "after" - ffff:... is the highest IPv6 range + + // 100::/64 - Discard prefix (RFC 6666) + pass("[ff::1]:8080"); + fail("[100::]:8080"); + fail("[100::ffff:ffff:ffff:ffff]:8080"); + pass("[100:0:0:1::1]:8080"); + + // 2001::/32 - IETF Protocol Assignments / Teredo (RFC 4380) + pass("[2000:ffff::1]:8080"); + fail("[2001::]:8080"); + fail("[2001:0:ffff::1]:8080"); + pass("[2001:1::1]:8080"); + + // 2001:20::/28 - ORCHIDv2 (RFC 7343) + pass("[2001:1f::1]:8080"); + fail("[2001:20::1]:8080"); + fail("[2001:2f::1]:8080"); + pass("[2001:30::1]:8080"); + + // 2001:db8::/32 - Documentation (RFC 3849) + pass("[2001:db7::1]:8080"); + fail("[2001:db8::1]:8080"); + fail("[2001:db8:ffff::1]:8080"); + pass("[2001:db9::1]:8080"); + + // 2002::/16 - 6to4 (RFC 3056, deprecated) + pass("[2001:ffff::1]:8080"); + fail("[2002::1]:8080"); + fail("[2002:ffff::1]:8080"); + pass("[2003::1]:8080"); + + // --- IPv6 v4-mapped (delegates to IPv4 checks) --- + fail("[::ffff:10.0.0.1]:8080"); + fail("[::ffff:100.64.0.1]:8080"); + fail("[::ffff:169.254.1.1]:8080"); + fail("[::ffff:192.0.2.1]:8080"); + fail("[::ffff:198.18.0.1]:8080"); + fail("[::ffff:224.0.0.1]:8080"); + fail("[::ffff:240.0.0.1]:8080"); + + // --- Valid public addresses --- + pass("8.8.8.8:443"); + pass("65.0.0.1:8080"); + pass("[2001:4860:4860::8888]:8080"); + pass("[2606:4700:4700::1111]:8080"); + } + + void + testVerifyEndpoints() + { + // Helper that sets up a Logic instance, creates and activates a slot, + // then calls on_endpoints with the given list and returns the + // livecache size afterwards. + auto run = [&](bool verifyEndpoints, Endpoints eps) -> std::size_t { + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + { + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + c.verifyEndpoints = verifyEndpoints; + logic.config(c); + } + + auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); + auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); + + auto const [slot, r] = logic.newOutboundSlot(remote); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(r == Result::Success); + BEAST_EXPECT(logic.onConnected(slot, local)); + + PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first); + BEAST_EXPECT(logic.activate(slot, pk, false) == Result::Success); + + logic.onEndpoints(slot, std::move(eps)); + + auto const size = logic.livecache.size(); + logic.onClosed(slot); + return size; + }; + + { + testcase("verify_endpoints enabled"); + + // Valid public addresses + Endpoints eps; + eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); + eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); + // Invalid: private address + eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); + // Invalid: port 0 + eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); + + // With verification enabled, only the 2 valid endpoints survive + BEAST_EXPECT(run(true, eps) == 2); + } + { + testcase("verify_endpoints disabled"); + + Endpoints eps; + eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); + eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); + // Private address — kept when verification is off + eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); + // Port 0 — kept when verification is off + eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); + + // Without verification, all 4 endpoints survive + BEAST_EXPECT(run(false, eps) == 4); + } + } + void testOnConnectedSelfConnection() { @@ -524,6 +779,8 @@ public: testActivateInboundDisabled(); testAddFixedPeerNoPort(); testOnConnectedSelfConnection(); + testIsValidAddress(); + testVerifyEndpoints(); } }; diff --git a/src/test/protocol/Issue_test.cpp b/src/test/protocol/Issue_test.cpp index 44a36b7dcf..a6a1fdd341 100644 --- a/src/test/protocol/Issue_test.cpp +++ b/src/test/protocol/Issue_test.cpp @@ -1,10 +1,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -863,6 +865,101 @@ public: //-------------------------------------------------------------------------- + void + testIssueFromJson() + { + testcase("issueFromJson"); + + // Valid XRP — no issuer field + { + json::Value jv; + jv[jss::currency] = "XRP"; + auto const issue = issueFromJson(jv); + BEAST_EXPECT(isXRP(issue)); + } + + // Valid IOU — legitimate issuer + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + auto const issue = issueFromJson(jv); + BEAST_EXPECT(!isXRP(issue)); + BEAST_EXPECT(issue.account != noAccount()); + } + + // noAccount() is the MPT sentinel in binary serialization - must be + // rejected + try + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(noAccount()); + issueFromJson(jv); + fail("noAccount() accepted as IOU issuer"); + } + catch (...) + { + pass(); + } + + // xrpAccount() is the XRP sentinel (all zeros) - must be rejected + // as IOU issuer + try + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(xrpAccount()); + issueFromJson(jv); + fail("xrpAccount() accepted as IOU issuer"); + } + catch (...) + { + pass(); + } + + // Invalid base58 — must be rejected + try + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = "not_a_valid_address"; + issueFromJson(jv); + fail("invalid base58 accepted as IOU issuer"); + } + catch (...) + { + pass(); + } + + // Non-XRP currency with no issuer field — must be rejected + try + { + json::Value jv; + jv[jss::currency] = "USD"; + issueFromJson(jv); + fail("missing issuer accepted"); + } + catch (...) + { + pass(); + } + + // XRP with an issuer field — must be rejected + try + { + json::Value jv; + jv[jss::currency] = "XRP"; + jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + issueFromJson(jv); + fail("XRP with issuer accepted"); + } + catch (...) + { + pass(); + } + } + void run() override { @@ -897,6 +994,9 @@ public: // --- testIssueDomainSets(); testIssueDomainMaps(); + + // --- + testIssueFromJson(); } }; diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index 720fafa8f2..f6c5a94752 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -537,51 +537,6 @@ public: { // VFALCO TODO There are no actual tests here, just printed output? // Change this to actually do something. - -#if 0 - beginTestCase ("rounding "); - - std::uint64_t value = 25000000000000000ull; - int offset = -14; - canonicalizeRound (false, value, offset, true); - - STAmount one (noIssue(), 1); - STAmount two (noIssue(), 2); - STAmount three (noIssue(), 3); - - STAmount oneThird1 = divRound (one, three, noIssue(), false); - STAmount oneThird2 = divide (one, three, noIssue()); - STAmount oneThird3 = divRound (one, three, noIssue(), true); - log << oneThird1; - log << oneThird2; - log << oneThird3; - - STAmount twoThird1 = divRound (two, three, noIssue(), false); - STAmount twoThird2 = divide (two, three, noIssue()); - STAmount twoThird3 = divRound (two, three, noIssue(), true); - log << twoThird1; - log << twoThird2; - log << twoThird3; - - STAmount oneA = mulRound (oneThird1, three, noIssue(), false); - STAmount oneB = multiply (oneThird2, three, noIssue()); - STAmount oneC = mulRound (oneThird3, three, noIssue(), true); - log << oneA; - log << oneB; - log << oneC; - - STAmount fourThirdsB = twoThird2 + twoThird2; - log << fourThirdsA; - log << fourThirdsB; - log << fourThirdsC; - - STAmount dripTest1 = mulRound (twoThird2, two, xrpIssue (), false); - STAmount dripTest2 = multiply (twoThird2, two, xrpIssue ()); - STAmount dripTest3 = mulRound (twoThird2, two, xrpIssue (), true); - log << dripTest1; - log << dripTest2; - log << dripTest3; -#endif } void diff --git a/src/test/protocol/STIssue_test.cpp b/src/test/protocol/STIssue_test.cpp index 1d6d750355..b7cc944e6b 100644 --- a/src/test/protocol/STIssue_test.cpp +++ b/src/test/protocol/STIssue_test.cpp @@ -1,15 +1,24 @@ #include +#include #include // IWYU pragma: keep +#include + +#include #include #include +#include +#include #include #include #include #include #include #include +#include + +#include namespace xrpl::test { @@ -137,12 +146,143 @@ public: "000000000000000000000000000000000000000000000002"); } + void + testNoAccountIssuerRpc() + { + testcase("noAccount issuer rejected via RPC sign"); + + using namespace jtx; + Env env{*this, envconfig([](std::unique_ptr cfg) { + cfg->loadFromString("[signing_support]\ntrue"); + return cfg; + })}; + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + json::Value txJson; + txJson[jss::TransactionType] = "AMMDelete"; + txJson[jss::Account] = alice.human(); + txJson[jss::Asset][jss::currency] = "USD"; + txJson[jss::Asset][jss::issuer] = to_string(noAccount()); + txJson[jss::Asset2][jss::currency] = "XRP"; + + json::Value req; + req[jss::tx_json] = txJson; + req[jss::secret] = alice.name(); + + auto const result = env.rpc("json", "sign", to_string(req))[jss::result]; + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data."); + } + + void + testNoAccountIssuer() + { + testcase("noAccount issuer rejection"); + + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(noAccount()); + + try + { + issueFromJson(sfAsset, jv); + fail("issueFromJson accepted noAccount() as IOU issuer"); + } + catch (...) + { + pass(); + } + } + + { + Serializer s; + s.addBitString(toCurrency("USD")); + s.addBitString(noAccount()); + SerialIter iter(s.slice()); + + try + { + STIssue const stissue(iter, sfAsset); + fail( + "STIssue deserialization of [USD][noAccount()] should " + "throw"); + } + catch (...) + { + pass(); + } + } + } + + void + testXrpAccountIssuerRpc() + { + testcase("xrpAccount issuer rejected via RPC sign"); + + using namespace jtx; + Env env{*this, envconfig([](std::unique_ptr cfg) { + cfg->loadFromString("[signing_support]\ntrue"); + return cfg; + })}; + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + json::Value txJson; + txJson[jss::TransactionType] = "AMMDelete"; + txJson[jss::Account] = alice.human(); + txJson[jss::Asset][jss::currency] = "USD"; + txJson[jss::Asset][jss::issuer] = to_string(xrpAccount()); + txJson[jss::Asset2][jss::currency] = "XRP"; + + json::Value req; + req[jss::tx_json] = txJson; + req[jss::secret] = alice.name(); + + auto const result = env.rpc("json", "sign", to_string(req))[jss::result]; + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data."); + } + + void + testXrpAccountIssuer() + { + testcase("xrpAccount issuer rejection"); + + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(xrpAccount()); + + try + { + issueFromJson(sfAsset, jv); + fail("issueFromJson accepted xrpAccount() as IOU issuer"); + } + catch (...) + { + pass(); + } + } + } + void run() override { // compliments other unit tests to ensure complete coverage testConstructor(); testCompare(); + testNoAccountIssuerRpc(); + testNoAccountIssuer(); + testXrpAccountIssuerRpc(); + testXrpAccountIssuer(); } }; diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index 5c9c3fd83c..74792e0a70 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -101,6 +102,39 @@ struct STNumber_test : public beast::unit_test::Suite BEAST_EXPECT(numberFromJson(sfNumber, "-0.000e6") == STNumber(sfNumber, 0)); { + auto const parseNumber = [](std::string const& boundary) { + return numberFromJson(sfNumber, boundary); + }; + auto const expectParseThrows = [this, &parseNumber](std::string const& boundary) { + try + { + parseNumber(boundary); + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT(std::string(e.what()) == "number cannot be represented"); + } + }; + + // Small rejects this; large scales parse it as 9223372036854775800e-1. + auto constexpr positiveBoundary = "922337203685477580"; + auto constexpr negativeBoundary = "-922337203685477580"; + if (Number::getMantissaScale() == MantissaRange::MantissaScale::Small) + { + expectParseThrows(positiveBoundary); + expectParseThrows(negativeBoundary); + } + else + { + BEAST_EXPECT( + parseNumber(positiveBoundary) == + STNumber(sfNumber, Number{922'337'203'685'477'580, 0})); + BEAST_EXPECT( + parseNumber(negativeBoundary) == + STNumber(sfNumber, Number{-922'337'203'685'477'580, 0})); + } + NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); // maxint64 9,223,372,036,854,775,807 auto const maxInt = std::to_string(std::numeric_limits::max()); @@ -108,23 +142,19 @@ struct STNumber_test : public beast::unit_test::Suite auto const minInt = std::to_string(std::numeric_limits::min()); if (Number::getMantissaScale() == MantissaRange::MantissaScale::Small) { - BEAST_EXPECT( - numberFromJson(sfNumber, maxInt) == - STNumber(sfNumber, Number{9'223'372'036'854'775, 3})); - BEAST_EXPECT( - numberFromJson(sfNumber, minInt) == - STNumber(sfNumber, Number{-9'223'372'036'854'775, 3})); + // min/maxInt can't be exactly represented with the small mantissa, so they + // don't parse, and are expected to throw. + expectParseThrows(maxInt); + expectParseThrows(minInt); } else { + // with large mantissas, maxint is fine BEAST_EXPECT( - numberFromJson(sfNumber, maxInt) == + parseNumber(maxInt) == STNumber(sfNumber, Number{9'223'372'036'854'775'807, 0})); - BEAST_EXPECT( - numberFromJson(sfNumber, minInt) == - STNumber( - sfNumber, - Number{true, 9'223'372'036'854'775'808ULL, 0, Number::Normalized{}})); + // but minint's mantissa is > kMaxRep, and so rounds, and thus can't be parsed + expectParseThrows(minInt); } } diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 24981053f5..c2030627c7 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -1979,7 +1980,7 @@ class STParsedJSON_test : public beast::unit_test::Suite json::Value j; json::Value obj(json::ValueType::Object); json::Value* current = &obj; - for (int i = 0; i < 63; ++i) + for (std::size_t i = 1; i < kMaxParsedJsonDepth; ++i) { json::Value const next(json::ValueType::Object); (*current)[sfTransactionMetaData] = next; @@ -1998,7 +1999,7 @@ class STParsedJSON_test : public beast::unit_test::Suite json::Value j; json::Value obj(json::ValueType::Object); json::Value* current = &obj; - for (int i = 0; i < 64; ++i) + for (std::size_t i = 1; i <= kMaxParsedJsonDepth; ++i) { json::Value const next(json::ValueType::Object); (*current)[sfTransactionMetaData] = next; @@ -2153,6 +2154,191 @@ class STParsedJSON_test : public beast::unit_test::Suite } } + void + testArrayBoundsChecking() + { + testcase("Array bounds checking"); + + auto const limitStr = std::to_string(kMaxParsedJsonArraySize) + " elements per field."; + + // parseArray rejects oversized STI_ARRAY (SignerEntries) + { + json::Value jv; + json::Value entries(json::ValueType::Array); + for (std::size_t i = 0; i <= kMaxParsedJsonArraySize; ++i) + { + json::Value entry; + entry["SignerEntry"]["Account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + entry["SignerEntry"]["SignerWeight"] = 1; + entries.append(entry); + } + jv[sfSignerEntries] = entries; + + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.SignerEntries' exceeds allowed JSON array size of " + limitStr); + } + + // parseObject rejects oversized STI_VECTOR256 (Amendments) + { + json::Value jv; + json::Value amendments(json::ValueType::Array); + std::string const hash(64, '0'); + for (std::size_t i = 0; i <= kMaxParsedJsonArraySize; ++i) + amendments.append(hash); + jv[sfAmendments] = amendments; + + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Amendments' exceeds allowed JSON array size of " + limitStr); + } + + // parseObject accepts exactly kMaxParsedJsonArraySize STI_VECTOR256 (Amendments) + { + json::Value jv; + json::Value amendments(json::ValueType::Array); + std::string const hash(64, '0'); + for (std::size_t i = 0; i < kMaxParsedJsonArraySize; ++i) + amendments.append(hash); + jv[sfAmendments] = amendments; + + STParsedJSONObject const parsed("test", jv); + BEAST_EXPECT(parsed.object); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + auto const arrSize = parsed.object->getFieldV256(sfAmendments).size(); + BEAST_EXPECT(arrSize == kMaxParsedJsonArraySize); + } + + // parseObject rejects oversized STI_PATHSET (outer array) + { + json::Value jv; + json::Value paths(json::ValueType::Array); + for (std::size_t i = 0; i <= kMaxParsedJsonArraySize; ++i) + { + json::Value path(json::ValueType::Array); + json::Value hop; + hop["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + path.append(hop); + paths.append(path); + } + jv[sfPaths] = paths; + + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Paths' exceeds allowed JSON array size of " + limitStr); + } + + // parseObject accepts exactly kMaxParsedJsonArraySize STI_PATHSET (outer array) + { + json::Value jv; + json::Value paths(json::ValueType::Array); + for (std::size_t i = 0; i < kMaxParsedJsonArraySize; ++i) + { + json::Value path(json::ValueType::Array); + json::Value hop; + hop["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + path.append(hop); + paths.append(path); + } + jv[sfPaths] = paths; + + STParsedJSONObject const parsed("test", jv); + BEAST_EXPECT(parsed.object); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + auto const arrSize = parsed.object->getFieldPathSet(sfPaths).size(); + BEAST_EXPECT(arrSize == kMaxParsedJsonArraySize); + } + + // parseObject rejects oversized STI_PATHSET (inner path hop array) + { + json::Value jv; + json::Value paths(json::ValueType::Array); + json::Value path(json::ValueType::Array); + json::Value hop; + hop["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + for (std::size_t i = 0; i <= kMaxParsedJsonArraySize; ++i) + path.append(hop); + paths.append(path); + jv[sfPaths] = paths; + + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Paths[0]' exceeds allowed JSON array size of " + limitStr); + } + + // parseObject accepts exactly kMaxParsedJsonArraySize hops in a single STI_PATHSET path + { + json::Value jv; + json::Value paths(json::ValueType::Array); + json::Value path(json::ValueType::Array); + json::Value hop; + hop["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + for (std::size_t i = 0; i < kMaxParsedJsonArraySize; ++i) + path.append(hop); + paths.append(path); + jv[sfPaths] = paths; + + STParsedJSONObject const parsed("test", jv); + BEAST_EXPECT(parsed.object); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + auto const arrSize = parsed.object->getFieldPathSet(sfPaths)[0].size(); + BEAST_EXPECT(arrSize == kMaxParsedJsonArraySize); + } + + // parseArray accepts exactly kMaxParsedJsonArraySize Memos (boundary) + { + json::Value jv; + json::Value memos(json::ValueType::Array); + for (std::size_t i = 0; i < kMaxParsedJsonArraySize; ++i) + { + json::Value memo; + memo["Memo"] = json::Value(json::ValueType::Object); + memo["Memo"]["MemoData"] = "00"; + memos.append(memo); + } + jv[sfMemos] = memos; + + STParsedJSONObject const parsed("test", jv); + BEAST_EXPECT(parsed.object); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + auto const arrSize = parsed.object->getFieldArray(sfMemos).size(); + BEAST_EXPECT(arrSize == kMaxParsedJsonArraySize); + } + + // parseArray rejects one more than kMaxParsedJsonArraySize Memos + { + json::Value jv; + json::Value memos(json::ValueType::Array); + for (std::size_t i = 0; i <= kMaxParsedJsonArraySize; ++i) + { + json::Value memo; + memo["Memo"] = json::Value(json::ValueType::Object); + memo["Memo"]["MemoData"] = "00"; + memos.append(memo); + } + jv[sfMemos] = memos; + + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Memos' exceeds allowed JSON array size of " + limitStr); + } + } + void testEdgeCases() { @@ -2420,6 +2606,7 @@ class STParsedJSON_test : public beast::unit_test::Suite testNumber(); testObject(); testArray(); + testArrayBoundsChecking(); testEdgeCases(); } }; diff --git a/src/test/protocol/STTx_test.cpp b/src/test/protocol/STTx_test.cpp index 21518d1406..777234be83 100644 --- a/src/test/protocol/STTx_test.cpp +++ b/src/test/protocol/STTx_test.cpp @@ -21,6 +21,7 @@ #include +#include #include #include #include @@ -50,15 +51,10 @@ public: run() override { testMalformedSerializedForm(); - - testcase("secp256k1 signatures"); testSTTx(KeyType::Secp256k1); - - testcase("ed25519 signatures"); testSTTx(KeyType::Ed25519); - - testcase("STObject constructor errors"); testObjectCtorErrors(); + testBatchInnerCtorErrors(); } void @@ -1328,6 +1324,8 @@ public: void testSTTx(KeyType keyType) { + testcase(std::string(to_string(keyType)) + " signatures"); + auto const keypair = randomKeyPair(keyType); STTx j(ttACCOUNT_SET, [&keypair](auto& obj) { @@ -1366,6 +1364,7 @@ public: { fail("Unable to build object from json"); } + // NOLINTNEXTLINE(cppcoreguidelines-slicing) else if (STObject(j) != parsed.object) { log << "ORIG: " << j.getJson(JsonOptions::Values::None) << '\n' @@ -1381,6 +1380,8 @@ public: void testObjectCtorErrors() { + testcase("STObject constructor errors"); + auto const kp1 = randomKeyPair(KeyType::Secp256k1); auto const id1 = calcAccountID(kp1.first); @@ -1462,6 +1463,75 @@ public: BEAST_EXPECT(got == "Field 'Fee' is required but missing."); } } + + void + testBatchInnerCtorErrors() + { + testcase("Batch inner transaction validation"); + + auto const kp1 = randomKeyPair(KeyType::Secp256k1); + auto const id1 = calcAccountID(kp1.first); + + auto const kp2 = randomKeyPair(KeyType::Secp256k1); + auto const id2 = calcAccountID(kp2.first); + + // A raw inner transaction object of the given transaction type. + auto makeInner = [&](std::uint16_t txType) { + STObject inner(sfRawTransaction); + inner.setFieldU16(sfTransactionType, txType); + inner.setAccountID(sfAccount, id1); + inner.setAccountID(sfDestination, id2); + inner.setFieldAmount(sfAmount, STAmount(10000000000ull)); + inner.setFieldAmount(sfFee, STAmount(0ull)); + inner.setFieldU32(sfSequence, 1); + inner.setFieldVL(sfSigningPubKey, Slice(kp1.first.data(), kp1.first.size())); + return inner; + }; + + // An outer Batch STObject wrapping the given inner. + auto makeBatch = [&](STObject inner) { + STArray rawTxns(sfRawTransactions); + rawTxns.push_back(std::move(inner)); + + STObject batch(sfGeneric); + batch.setFieldU16(sfTransactionType, ttBATCH); + batch.setAccountID(sfAccount, id1); + batch.setFieldAmount(sfFee, STAmount(20ull)); + batch.setFieldU32(sfSequence, 1); + batch.setFieldVL(sfSigningPubKey, Slice(kp1.first.data(), kp1.first.size())); + batch.setFieldArray(sfRawTransactions, rawTxns); + return batch; + }; + + { + // A batch whose inner is a well-formed transaction constructs. + std::string errorMsg; + try + { + STTx{makeBatch(makeInner(ttPAYMENT))}; + } + catch (std::exception const& err) + { + errorMsg = err.what(); + } + BEAST_EXPECT(errorMsg.empty()); + } + { + // A batch whose inner carries an unregistered transaction type is + // rejected at construction, rather than surviving as a raw STObject + // and throwing later from an unprotected fee-calculation path. + std::string errorMsg; + try + { + STTx{makeBatch(makeInner(60000))}; + } + catch (std::exception const& err) + { + errorMsg = err.what(); + } + BEAST_EXPECT(matches(errorMsg.c_str(), "Invalid transaction type 60000")); + } + } }; class InnerObjectFormatsSerializer_test : public beast::unit_test::Suite diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 31b20b37d4..c656c97a4c 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -9,8 +9,11 @@ #include // IWYU pragma: keep #include #include +#include +#include #include #include +#include #include #include @@ -22,7 +25,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -32,6 +37,7 @@ #include #include #include +#include #include namespace xrpl::test { @@ -200,6 +206,15 @@ public: resp[jss::result][jss::error_message] == "Invalid field 'limit', not unsigned integer."); } + // test error on sponsored param not a boolean + { + json::Value params; + params[jss::account] = bob.human(); + params[jss::sponsored] = "true"; + auto resp = env.rpc("json", "account_objects", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == "Invalid field 'sponsored', not boolean."); + } // test errors on marker { Account const gw{"G"}; @@ -605,6 +620,7 @@ public: BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::amm), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::did), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::permissioned_domain), 0)); + BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::sponsorship), 0)); // we expect invalid field type reported for the following types BEAST_EXPECT(acctObjsTypeIsInvalid(acctObjs(gw, jss::amendments))); @@ -926,6 +942,30 @@ public: BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == seq + 1); } + { + // Create a sponsorship + env(sponsor::set(alice, tfSponsorshipSetRequireSignForFee, 200, XRP(100), drops(10)), + sponsor::SponseeAcc(gw)); + env.close(); + + // Find the sponsorship. + for (auto const& acct : {alice, gw}) + { + json::Value const resp = acctObjs(acct, jss::sponsorship); + BEAST_EXPECT(acctObjsIsSize(resp, 1)); + + auto const& sponsorship = resp[jss::result][jss::account_objects][0u]; + + BEAST_EXPECT(sponsorship[sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(sponsorship[sfSponsee.jsonName] == gw.human()); + BEAST_EXPECT( + sponsorship[sfFlags.jsonName].asUInt() == tfSponsorshipSetRequireSignForFee); + BEAST_EXPECT(sponsorship[sfRemainingOwnerCount.jsonName].asUInt() == 200); + BEAST_EXPECT(sponsorship[sfFeeAmount.jsonName].asUInt() == 100000000); + BEAST_EXPECT(sponsorship[sfMaxFee.jsonName].asUInt() == 10); + } + } + { // See how "deletion_blockers_only" handles gw's directory. json::Value params; @@ -940,7 +980,8 @@ public: jss::NFTokenPage.cStr(), jss::RippleState.cStr(), jss::PayChannel.cStr(), - jss::PermissionedDomain.cStr()}; + jss::PermissionedDomain.cStr(), + jss::Sponsorship.cStr()}; std::ranges::sort(v); return v; }(); @@ -1350,6 +1391,313 @@ public: } } + void + testSponsoredFilter() + { + testcase("SponsoredFilter"); + using namespace jtx; + + Env env(*this, testableAmendments()); + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const gw("gw"); + auto const usd = gw["USD"]; + + env.fund(XRP(10000), alice, bob, sponsor1, gw); + env.close(); + + // Helper to call account_objects with sponsored filter + auto acctObjsSponsored = [](Env& testEnv, + AccountID const& acct, + bool sponsored, + std::optional const& type = std::nullopt) { + json::Value params; + params[jss::account] = to_string(acct); + params[jss::sponsored] = sponsored; + if (type) + params[jss::type] = *type; + params[jss::ledger_index] = "validated"; + return testEnv.rpc("json", "account_objects", to_string(params)); + }; + + // Create a trust line for bob (not sponsored) + env(trust(bob, usd(1000))); + env.close(); + + // sponsored=true should not find any objects for bob (doesn't have any sponsored objects) + { + auto const resp = acctObjsSponsored(env, bob.id(), true); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + + // Now sponsor bob's trust line + auto const trustId = keylet::trustLine(bob, gw, usd.currency); + if (!BEAST_EXPECT(env.le(trustId))) + return; + + env(sponsor::transfer(bob, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + // Verify trust line has sponsor field + { + auto const sle = env.le(trustId); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(sle->isFieldPresent(sfHighSponsor) || sle->isFieldPresent(sfLowSponsor)); + } + + // sponsored=true on bob should include the sponsored trust line + { + auto const resp = acctObjsSponsored(env, bob.id(), true); + auto const& objs = resp[jss::result][jss::account_objects]; + if (!BEAST_EXPECT(objs.size() == 1)) + return; + + auto const& obj = objs[0u]; + BEAST_EXPECT(obj[sfLedgerEntryType.jsonName] == jss::RippleState); + BEAST_EXPECT( + obj.isMember(sfHighSponsor.jsonName) || obj.isMember(sfLowSponsor.jsonName)); + } + + // sponsored=false on bob should NOT include the sponsored trust line + { + auto const resp = acctObjsSponsored(env, bob.id(), false); + BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0); + } + + // A trust line sponsored on either side is classified as sponsored + // for both parties. + { + Env env(*this, testableAmendments()); + Account const issuer("issuer"); + Account const user("user"); + Account const sponsor("sponsor"); + auto const usd = issuer["USD"]; + + env.fund(XRP(10000), issuer, user, sponsor); + env.close(); + + env(trust(issuer, user["USD"](100))); + env.close(); + + env(trust(user, usd(100))); + env.close(); + + auto const trustId = keylet::trustLine(user, issuer, usd.currency); + if (!BEAST_EXPECT(env.le(trustId))) + return; + + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const line = env.le(trustId); + if (!BEAST_EXPECT(line)) + return; + + auto const userIsHigh = line->getFieldAmount(sfHighLimit).getIssuer() == user.id(); + auto const& userSponsorField = userIsHigh ? sfHighSponsor : sfLowSponsor; + auto const& issuerSponsorField = userIsHigh ? sfLowSponsor : sfHighSponsor; + + BEAST_EXPECT(line->isFieldPresent(userSponsorField)); + BEAST_EXPECT(!line->isFieldPresent(issuerSponsorField)); + + { + auto const resp = acctObjsSponsored(env, user.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + { + auto const resp = acctObjsSponsored(env, user.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + } + + // A sponsored Check is classified as sponsored in both the writer's + // and the destination's results. + { + Env env(*this, testableAmendments()); + Account const owner("owner"); + Account const dest("dest"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), owner, dest, sponsor); + env.close(); + + auto const checkSeq = env.seq(owner); + env(check::create(owner, dest, XRP(1))); + env.close(); + + auto const checkId = keylet::check(owner, checkSeq); + if (!BEAST_EXPECT(env.le(checkId))) + return; + + env(sponsor::transfer(owner, tfSponsorshipCreate, checkId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + { + auto const sle = env.le(checkId); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(sle->isFieldPresent(sfSponsor)); + } + + for (auto const& acct : {owner.id(), dest.id()}) + { + { + auto const resp = acctObjsSponsored(env, acct, true, jss::check); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Check); + } + { + auto const resp = acctObjsSponsored(env, acct, false, jss::check); + BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0); + } + } + } + + // A Sponsorship object is visible to both sides. + { + Env env(*this, testableAmendments()); + Account const owner("owner"); + Account const sponsee("sponsee"); + + env.fund(XRP(10000), owner, sponsee); + env.close(); + + env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + + auto const sponsorshipKeylet = keylet::sponsorship(owner, sponsee); + if (!BEAST_EXPECT(env.le(sponsorshipKeylet))) + return; + + { + auto const resp = acctObjsSponsored(env, owner.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + { + auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + } + } + + void + testAccountObjectDoesntShowCancelledOffers() + { + testcase("AccountObjectDoesntShowCancelledOffers"); + + using namespace jtx; + Env env(*this); + + Account const alice{"alice"}; + Account const bob{"bob"}; + auto const eur = bob["EUR"]; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const rpcAccountObjects = [&](std::optional limit = std::nullopt) { + json::Value params; + params[jss::account] = alice.human(); + if (limit.has_value()) + { + params[jss::limit] = *limit; + } + return env.rpc("json", "account_objects", to_string(params)); + }; + + auto const numEntries = 33; + std::vector seqs; + seqs.reserve(numEntries); + for ([[maybe_unused]] auto _ : std::ranges::iota_view{0, numEntries}) + { + json::Value params; + params[jss::secret] = toBase58(generateSeed("alice")); + params[jss::tx_json] = offer(alice, eur(1), XRP(2)); + auto const res = env.rpc("json", "submit", to_string(params))[jss::result]; + BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS"); + seqs.push_back(env.seq(alice)); + } + + auto res = rpcAccountObjects(); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == numEntries); + BEAST_EXPECT(not res[jss::result].isMember(jss::limit)); + BEAST_EXPECT(not res[jss::result].isMember(jss::marker)); + + for (auto const s : std::views::all(seqs) | std::views::take(numEntries - 1)) + { + json::Value params; + params[jss::secret] = toBase58(generateSeed("alice")); + params[jss::tx_json] = offerCancel(alice, s - 1); + auto const res = env.rpc("json", "submit", to_string(params))[jss::result]; + BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS"); + } + + res = rpcAccountObjects(); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1); + BEAST_EXPECT(not res[jss::result].isMember(jss::limit)); + BEAST_EXPECT(not res[jss::result].isMember(jss::marker)); + + { + json::Value params; + params[jss::secret] = toBase58(generateSeed("alice")); + json::Value txJson; + txJson[jss::TransactionType] = jss::NFTokenMint; + txJson[jss::Account] = to_string(alice.id()); + txJson["NFTokenTaxon"] = 1; + params[jss::tx_json] = txJson; + auto const res = env.rpc("json", "submit", to_string(params))[jss::result]; + BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS"); + } + env.close(); + + res = rpcAccountObjects(); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 2); + BEAST_EXPECT(not res[jss::result].isMember(jss::limit)); + BEAST_EXPECT(not res[jss::result].isMember(jss::marker)); + + res = rpcAccountObjects(1); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1); + BEAST_EXPECT(res[jss::result][jss::limit].asUInt() == 1); + BEAST_EXPECT(res[jss::result].isMember(jss::marker)); + } + void run() override { @@ -1360,6 +1708,8 @@ public: testNFTsMarker(); testAccountNFTs(); testAccountObjectMarker(); + testSponsoredFilter(); + testAccountObjectDoesntShowCancelledOffers(); } }; diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index ace8743e1e..f1fbc2871b 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -5,6 +5,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +47,7 @@ #include #include #include +#include #include #include @@ -773,7 +777,7 @@ class AccountTx_test : public beast::unit_test::Suite // All it takes is a large enough XRP payment to resurrect // becky's account. Try too small a payment. - env(pay(alice, becky, drops(env.current()->fees().accountReserve(0)) - drops(1)), + env(pay(alice, becky, drops(env.current()->fees().accountReserve(0, 1)) - drops(1)), Ter(tecNO_DST_INSUF_XRP)); env.close(); @@ -888,6 +892,502 @@ class AccountTx_test : public beast::unit_test::Suite checkAliceAcctTx(9, jss::Payment); } + void + testDelegation() + { + testcase("Delegation Filtering"); + + using namespace test::jtx; + + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // Normal TX: Alice pays Carol (Signed by Alice's Master Key) + env(pay(alice, carol, XRP(10))); + env.close(); + + // Setup Delegation: Alice allows Bob to sign Payments for her + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + // Delegated TX: Alice pays Bob (Signed by Bob using Delegation) + env(pay(alice, bob, XRP(20)), delegate::As(bob)); + env.close(); + + // Normal TX: Bob pays Carol (Signed by Bob for himself) + env(pay(bob, carol, XRP(30))); + env.close(); + + auto const countTxs = [&](AccountID const& account, + json::Value const& delegateParams, + std::optional const limit = std::nullopt) -> int { + int count = 0; + json::Value marker; + bool haveMarker = false; + int pages = 0; + + while (true) + { + json::Value params; + params[jss::account] = toBase58(account); + params[jss::ledger_index_min] = -1; + params[jss::ledger_index_max] = -1; + + if (!delegateParams.isNull()) + params[jss::delegate] = delegateParams; + if (limit) + params[jss::limit] = *limit; + if (haveMarker) + params[jss::marker] = marker; + + auto const res = env.rpc("json", "account_tx", to_string(params)); + auto const& result = res[jss::result]; + + if (result.isMember(jss::transactions)) + count += result[jss::transactions].size(); + + if (!limit || !result.isMember(jss::marker)) + break; + + marker = result[jss::marker]; + haveMarker = true; + ++pages; + if (!BEAST_EXPECT(pages < 20)) + break; + } + + return count; + }; + + auto const checkError = [&](json::Value const& delegateParams, + std::string const& errToken) { + json::Value params; + params[jss::account] = alice.human(); + params[jss::delegate] = delegateParams; + auto res = env.rpc("json", "account_tx", to_string(params)); + BEAST_EXPECT(res[jss::result][jss::error] == errToken); + }; + + // Filter: Delegatee. Expects TX #2 (Signed by Bob) + { + json::Value p; + p[jss::delegate_filter] = "actor"; + BEAST_EXPECT(countTxs(alice.id(), p) == 1); + } + + // Filter: Delegatee + Counterparty Bob. Expects TX #2. + { + json::Value p; + p[jss::delegate_filter] = "actor"; + p[jss::counter_party] = bob.human(); + BEAST_EXPECT(countTxs(alice.id(), p) == 1); + } + + // Filter: Delegatee + Counterparty Carol. Expects 0. + { + json::Value p; + p[jss::delegate_filter] = "actor"; + p[jss::counter_party] = carol.human(); + BEAST_EXPECT(countTxs(alice.id(), p) == 0); + } + + // Filter: Delegator. Expects TX #2. + // (Bob signed it, but Alice is the owner). + { + json::Value p; + p[jss::delegate_filter] = "authorizer"; + BEAST_EXPECT(countTxs(bob.id(), p) == 1); + } + + // Filter: Delegator + Counterparty Alice. Expects TX #2. + { + json::Value p; + p[jss::delegate_filter] = "authorizer"; + p[jss::counter_party] = alice.human(); + BEAST_EXPECT(countTxs(bob.id(), p) == 1); + } + + // Filter: Authorizer. Expect: None. + // TX #2 has sfDelegate present, but Alice is the delegator/owner, not + // the delegate signer + { + json::Value p; + p[jss::delegate_filter] = "authorizer"; + BEAST_EXPECT(countTxs(alice.id(), p) == 0); + } + + // Query Bob (Signer), Filter: Delegator, Counterparty: Carol + // Expect: None (Alice is Owner, not Carol) + { + json::Value p; + p[jss::delegate_filter] = "authorizer"; + p[jss::counter_party] = carol.human(); + BEAST_EXPECT(countTxs(bob.id(), p) == 0); + } + + // Query Bob (Signer), Filter: Delegatee + // Expect: None. Bob did not employ a delegatee for his own TXs (TX C). + { + json::Value p; + p[jss::delegate_filter] = "actor"; + BEAST_EXPECT(countTxs(bob.id(), p) == 0); + } + + // "delegate" is not an object (e.g., string) + { + json::Value const p = "not_an_object"; + checkError(p, "invalidParams"); + } + + // Missing "delegate_filter" inside object + { + json::Value const p(json::ValueType::Object); + checkError(p, "invalidParams"); + } + + // "delegate_filter" is not a string (e.g., int) + { + json::Value p; + p[jss::delegate_filter] = 123; + checkError(p, "invalidParams"); + } + + // "delegate_filter" has invalid value + { + json::Value p; + p[jss::delegate_filter] = "random_string"; + checkError(p, "invalidParams"); + } + + // "counterparty" is not a string + { + json::Value p; + p[jss::delegate_filter] = "actor"; + p[jss::counter_party] = 123; + checkError(p, "invalidParams"); + } + + // "counterparty" is malformed base58 + { + json::Value p; + p[jss::delegate_filter] = "actor"; + p[jss::counter_party] = "not_an_account"; + checkError(p, "actMalformed"); + } + + // Multi-signed non-delegated TX: Alice pays Carol via multi-sig. + // sfDelegate is absent and sfSigningPubKey is empty — the filter + // must skip it without crashing. + { + Account const daria{"daria"}; + Account const edward{"edward"}; + env.fund(XRP(1000), daria, edward); + env.close(); + env(signers(alice, 2, {{daria, 1}, {edward, 1}})); + env.close(); + env(pay(alice, carol, XRP(1)), + Fee(drops(env.current()->fees().increment * 2)), + Msig(daria, edward)); + env.close(); + + // Alice's actor filter should still see only the 1 delegated tx, + // not the multi-signed one. + json::Value p; + p[jss::delegate_filter] = "actor"; + BEAST_EXPECT(countTxs(alice.id(), p) == 1); + } + + // Regular-key-signed non-delegated TX: Alice pays Bob, signed by Bob + // as Alice's regular key. This must not be treated as delegation. + { + env(regkey(alice, bob)); + env.close(); + env(pay(alice, bob, XRP(1))); + env.close(); + + json::Value actorFilter; + actorFilter[jss::delegate_filter] = "actor"; + BEAST_EXPECT(countTxs(alice.id(), actorFilter) == 1); + // limit: 1 forces pagination past newer non-delegated rows. + BEAST_EXPECT(countTxs(alice.id(), actorFilter, 1) == 1); + + actorFilter[jss::counter_party] = bob.human(); + BEAST_EXPECT(countTxs(alice.id(), actorFilter) == 1); + BEAST_EXPECT(countTxs(alice.id(), actorFilter, 1) == 1); + + json::Value authorizerFilter; + authorizerFilter[jss::delegate_filter] = "authorizer"; + BEAST_EXPECT(countTxs(bob.id(), authorizerFilter) == 1); + BEAST_EXPECT(countTxs(bob.id(), authorizerFilter, 1) == 1); + + authorizerFilter[jss::counter_party] = alice.human(); + BEAST_EXPECT(countTxs(bob.id(), authorizerFilter) == 1); + BEAST_EXPECT(countTxs(bob.id(), authorizerFilter, 1) == 1); + } + + // Pagination marker/delegate-filter consistency. A marker returned by a + // delegate-filtered query carries a `delegate` flag and is only valid + // for a follow-up request that repeats the filter; mixing the two + // marker conventions must be rejected with invalidParams. + { + json::Value actorFilter; + actorFilter[jss::delegate_filter] = "actor"; + + // Obtain a delegate-filtered marker (limit 1 forces pagination). + json::Value dp; + dp[jss::account] = alice.human(); + dp[jss::ledger_index_min] = -1; + dp[jss::ledger_index_max] = -1; + dp[jss::delegate] = actorFilter; + dp[jss::limit] = 1; + auto const dRes = env.rpc("json", "account_tx", to_string(dp)); + BEAST_EXPECT(dRes[jss::result].isMember(jss::marker)); + json::Value const delegateMarker = dRes[jss::result][jss::marker]; + BEAST_EXPECT( + delegateMarker.isMember(jss::delegate) && delegateMarker[jss::delegate].asBool()); + + // Reusing a delegate marker without the delegate filter is rejected. + { + json::Value p; + p[jss::account] = alice.human(); + p[jss::ledger_index_min] = -1; + p[jss::ledger_index_max] = -1; + p[jss::limit] = 1; + p[jss::marker] = delegateMarker; + auto const r = env.rpc("json", "account_tx", to_string(p)); + BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams"); + } + + // Obtain a non-delegate marker; it must not carry the flag. + json::Value np; + np[jss::account] = alice.human(); + np[jss::ledger_index_min] = -1; + np[jss::ledger_index_max] = -1; + np[jss::limit] = 1; + auto const nRes = env.rpc("json", "account_tx", to_string(np)); + BEAST_EXPECT(nRes[jss::result].isMember(jss::marker)); + json::Value const normalMarker = nRes[jss::result][jss::marker]; + BEAST_EXPECT(!normalMarker.isMember(jss::delegate)); + + // Reusing a non-delegate marker with a delegate filter is rejected. + { + json::Value p; + p[jss::account] = alice.human(); + p[jss::ledger_index_min] = -1; + p[jss::ledger_index_max] = -1; + p[jss::limit] = 1; + p[jss::delegate] = actorFilter; + p[jss::marker] = normalMarker; + auto const r = env.rpc("json", "account_tx", to_string(p)); + BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams"); + } + } + } + + void + testDelegationMultiSign() + { + testcase("Delegation filter with multi-signed delegatee"); + using namespace test::jtx; + + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + Account const daria{"daria"}; + Account const edward{"edward"}; + + env.fund(XRP(10000), alice, bob, carol, daria, edward); + env.close(); + + // Bob's identity is established via multi-sig (daria + edward) + env(signers(bob, 2, {{daria, 1}, {edward, 1}})); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + // Delegated tx: Alice pays Carol, Bob signs via multi-sig + env(pay(alice, carol, XRP(10)), Fee(XRP(1)), delegate::As(bob), Msig(daria, edward)); + env.close(); + + auto const countTxs = [&](AccountID const& account, + json::Value const& delegateParams) -> int { + json::Value params; + params[jss::account] = toBase58(account); + params[jss::ledger_index_min] = -1; + params[jss::ledger_index_max] = -1; + params[jss::delegate] = delegateParams; + + auto const res = env.rpc("json", "account_tx", to_string(params)); + + if (res[jss::result].isMember(jss::transactions)) + return res[jss::result][jss::transactions].size(); + return 0; + }; + + // Alice (owner) finds the tx with actor filter + { + json::Value p; + p[jss::delegate_filter] = "actor"; + BEAST_EXPECT(countTxs(alice.id(), p) == 1); + } + + // Alice (owner) + counterparty Bob finds the tx + { + json::Value p; + p[jss::delegate_filter] = "actor"; + p[jss::counter_party] = bob.human(); + BEAST_EXPECT(countTxs(alice.id(), p) == 1); + } + + // Bob (delegatee) finds the tx with authorizer filter + { + json::Value p; + p[jss::delegate_filter] = "authorizer"; + BEAST_EXPECT(countTxs(bob.id(), p) == 1); + } + + // Bob (delegatee) + counterparty Alice finds the tx + { + json::Value p; + p[jss::delegate_filter] = "authorizer"; + p[jss::counter_party] = alice.human(); + BEAST_EXPECT(countTxs(bob.id(), p) == 1); + } + } + + void + testDelegationMarkerWithinPage() + { + testcase("Delegation filter marker within a single query page"); + + using namespace test::jtx; + + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const startLedger = env.closed()->header().seq + 1; + + env(pay(alice, carol, XRP(1)), delegate::As(bob)); + env.close(); + env(pay(alice, carol, XRP(1)), delegate::As(bob)); + env.close(); + + json::Value p; + p[jss::delegate_filter] = "actor"; + + json::Value params; + params[jss::account] = alice.human(); + params[jss::ledger_index_min] = startLedger; + params[jss::ledger_index_max] = -1; + params[jss::delegate] = p; + params[jss::limit] = 1; + + auto const res = env.rpc("json", "account_tx", to_string(params)); + auto const& result = res[jss::result]; + + // The first page emits only the first delegated payment. (as page limit is set to 1) + BEAST_EXPECT(result[jss::transactions].size() == 1); + BEAST_EXPECT(result.isMember(jss::marker)); + + // Following the marker resumes right after the first payment and + // returns the second one. + json::Value page2 = params; + page2[jss::marker] = result[jss::marker]; + auto const res2 = env.rpc("json", "account_tx", to_string(page2)); + BEAST_EXPECT(res2[jss::result][jss::transactions].size() == 1); + } + + void + testSponsorship() + { + // test all sponsored transactions are in sponsor and sponsee's account + // tx list + testcase("Sponsorship"); + + using namespace test::jtx; + Env env(*this); + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, sponsor, sponsor2); + env.close(); + + // check the latest sponsorship-related txn is in account tx list + auto const checkTx = [&](Account const& account, json::StaticString txType) { + json::Value params; + params[jss::account] = account.human(); + params[jss::limit] = 100; + auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result]; + + auto const& tx0(jv[jss::transactions][0u][jss::tx]); + BEAST_EXPECT(tx0[jss::TransactionType] == txType); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()}; + BEAST_EXPECT(tx0[jss::hash] == txHash); + }; + + // fee sponsorship + env(noop(alice), sponsor::As(sponsor, spfSponsorFee), Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::AccountSet); + checkTx(sponsor, jss::AccountSet); + + // set sponsor + env(sponsor::set(sponsor, 0, 100, XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + checkTx(alice, jss::SponsorshipSet); + checkTx(sponsor, jss::SponsorshipSet); + + // create an object with sponsor + auto const checkId = keylet::check(alice, env.seq(alice)).key; + env(check::create(alice, sponsor, XRP(1)), sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + checkTx(alice, jss::CheckCreate); + checkTx(sponsor, jss::CheckCreate); + + // transfer object sponsorship + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + checkTx(alice, jss::SponsorshipTransfer); + checkTx(sponsor, jss::SponsorshipTransfer); + checkTx(sponsor2, jss::SponsorshipTransfer); + + // delete the sponsored object + env(check::cancel(alice, checkId), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::CheckCancel); + checkTx(sponsor, jss::CheckCancel); + checkTx(sponsor2, jss::CheckCancel); + + // account sponsorship + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::SponsorshipTransfer); + checkTx(sponsor, jss::SponsorshipTransfer); + } + public: void run() override @@ -896,6 +1396,10 @@ public: testContents(); testAccountDelete(); testMPT(); + testDelegation(); + testDelegationMultiSign(); + testDelegationMarkerWithinPage(); + testSponsorship(); } }; BEAST_DEFINE_TESTSUITE(AccountTx, rpc, xrpl); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index c7ef5cef2d..7adb5a4518 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -93,6 +94,8 @@ std::vector> gMappings{ {jss::oracle_document_id, FieldType::UInt32Field}, {jss::owner, FieldType::AccountField}, {jss::seq, FieldType::UInt32Field}, + {jss::sponsor, FieldType::AccountField}, + {jss::sponsee, FieldType::AccountField}, {jss::subject, FieldType::AccountField}, {jss::ticket_seq, FieldType::UInt32Field}, }; @@ -107,7 +110,7 @@ getFieldType(json::StaticString fieldName) return it->second; } - Throw("`mappings` is missing field " + std::string(fieldName.cStr())); + Throw("`gMappings` is missing field " + std::string(fieldName.cStr())); } std::string @@ -1884,6 +1887,59 @@ class LedgerEntry_test : public beast::unit_test::Suite runLedgerEntryTest(env, jss::signer_list); } + void + testSponsorship() + { + testcase("Sponsorship"); + + using namespace test::jtx; + + Env env{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + env(sponsor::set(alice, 0, 100), sponsor::SponseeAcc(bob)); + env.close(); + std::string const ledgerHash{to_string(env.closed()->header().hash)}; + auto const sponsorshipIndex = to_string(keylet::sponsorship(alice.id(), bob.id()).key); + + { + // Request by sponsor and sponsee. + json::Value jvParams; + jvParams[jss::sponsorship][jss::sponsor] = alice.human(); + jvParams[jss::sponsorship][jss::sponsee] = bob.human(); + jvParams[jss::ledger_hash] = ledgerHash; + auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship); + BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human()); + BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString()); + } + { + // Request by index. + json::Value jvParams; + jvParams[jss::sponsorship] = sponsorshipIndex; + jvParams[jss::ledger_hash] = ledgerHash; + json::Value const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship); + BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human()); + BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString()); + } + { + // Check all malformed cases. + runLedgerEntryTest( + env, + jss::sponsorship, + { + {.fieldName = jss::sponsor, .malformedErrorMsg = "malformedSponsor"}, + {.fieldName = jss::sponsee, .malformedErrorMsg = "malformedSponsee"}, + }); + } + } + void testTicket() { @@ -2232,7 +2288,9 @@ class LedgerEntry_test : public beast::unit_test::Suite } } - /// Test the ledger entry types that don't take parameters + /** + * Test the ledger entry types that don't take parameters + */ void testFixed() { @@ -2248,7 +2306,8 @@ class LedgerEntry_test : public beast::unit_test::Suite env.close(); - /** Verifies that the RPC result has the expected data + /** + * Verifies that the RPC result has the expected data * * @param good: Indicates that the request should have succeeded * and returned a ledger object of `expectedType` type. @@ -2283,7 +2342,8 @@ class LedgerEntry_test : public beast::unit_test::Suite } }; - /** Runs a series of tests for a given fixed-position ledger + /** + * Runs a series of tests for a given fixed-position ledger * entry. * * @param field: The Json request field to use. @@ -2441,7 +2501,8 @@ class LedgerEntry_test : public beast::unit_test::Suite env.close(); - /** Verifies that the RPC result has the expected data + /** + * Verifies that the RPC result has the expected data * * @param good: Indicates that the request should have succeeded * and returned a ledger object of `expectedType` type. @@ -2481,7 +2542,8 @@ class LedgerEntry_test : public beast::unit_test::Suite } }; - /** Runs a series of tests for a given ledger index. + /** + * Runs a series of tests for a given ledger index. * * @param ledger: The ledger index value of the "hashes" request * parameter. May not necessarily be a number. @@ -2676,6 +2738,7 @@ public: testPayChan(); testRippleState(); testSignerList(); + testSponsorship(); testTicket(); testDID(); testInvalidOracleLedgerEntry(); diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index af56a9e9ba..3a2c957691 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -258,10 +258,12 @@ class LedgerRPC_test : public beast::unit_test::Suite BEAST_EXPECT(jrr[jss::ledger][jss::accountState].size() == 3u); } - /// @brief ledger RPC requests as a way to drive - /// input options to lookupLedger. The point of this test is - /// coverage for lookupLedger, not so much the ledger - /// RPC request. + /** + * @brief ledger RPC requests as a way to drive + * input options to lookupLedger. The point of this test is + * coverage for lookupLedger, not so much the ledger + * RPC request. + */ void testLookupLedger() { diff --git a/src/test/rpc/ServerDefinitions_test.cpp b/src/test/rpc/ServerDefinitions_test.cpp index bf345e6fdf..0cf6b315e0 100644 --- a/src/test/rpc/ServerDefinitions_test.cpp +++ b/src/test/rpc/ServerDefinitions_test.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -61,14 +62,12 @@ public: // check exception SFields { auto const fieldExists = [&](std::string name) { - for (auto& field : result[jss::result][jss::FIELDS]) - { - if (field[0u].asString() == name) - { - return true; - } - } - return false; + auto& fields = result[jss::result][jss::FIELDS]; + // json::Value is not a std::ranges range, so the iterator form is used. + // NOLINTNEXTLINE(modernize-use-ranges) + return std::any_of(fields.begin(), fields.end(), [&](auto& field) { + return field[0u].asString() == name; + }); }; BEAST_EXPECT(fieldExists("Generic")); BEAST_EXPECT(fieldExists("Invalid")); diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 0d00360a58..16df733a66 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -364,6 +364,37 @@ class Simulate_test : public beast::unit_test::Suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Signers[0]'."); } + { + // Non-object SponsorSignature field + json::Value params; + json::Value txJson = json::ValueType::Object; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + txJson[sfSponsorSignature] = ""; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'SponsorSignature', not object."); + } + { + // Invalid SponsorSignature.Signers field + json::Value params; + json::Value txJson = json::ValueType::Object; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + json::Value sponsorSignature = json::ValueType::Object; + sponsorSignature[sfSigners] = "1"; + txJson[sfSponsorSignature] = sponsorSignature; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == + "Invalid field 'tx.SponsorSignature.Signers'.", + resp.toStyledString()); + } { // Invalid transaction json::Value params; @@ -561,6 +592,75 @@ class Simulate_test : public beast::unit_test::Suite // test without autofill testTx(env, tx, validateOutput); } + + { + // autofill sponsor signature + + auto validateOutput = [&](json::Value const& resp, json::Value const& tx) { + auto result = resp[jss::result]; + checkBasicReturnValidity( + result, tx, env.seq(env.master), env.current()->fees().base); + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].size() == 2); + + auto node = metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT(node.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = node[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto previousFields = modifiedNode[sfPreviousFields]; + BEAST_EXPECT(!previousFields.isMember(sfBalance.jsonName)); + } + + auto node2 = metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT(node2.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = node2[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + + auto previousFields = modifiedNode[sfPreviousFields]; + BEAST_EXPECT(previousFields.isMember(sfBalance.jsonName)); + } + } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + Account const sponsor("sponsor"); + env.fund(XRP(10000), sponsor); + env.close(); + + json::Value tx; + + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain.jsonName] = kNewDomain; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = json::ValueType::Object; + + // test with autofill + testTx(env, tx, validateOutput); + + tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = ""; + tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + // test without autofill + testTx(env, tx, validateOutput); + } } void @@ -763,6 +863,58 @@ class Simulate_test : public beast::unit_test::Suite } } + void + testSuccessfulSponsoredTransactionMultisigned() + { + testcase("Successful sponsored multi-signed transaction"); + + using namespace jtx; + Env env(*this); + Account const sponsor("sponsor"); + Account const signer("signer"); + env.fund(XRP(10000), sponsor, signer); + env.close(); + + env(signers(sponsor, 1, {{signer, 1}})); + env.close(); + + auto validateOutput = [&](json::Value const& resp, json::Value const& tx) { + auto const result = resp[jss::result]; + // Verifies Fee autofill counts nested sponsor-signature signers. + auto const expectedFee = env.current()->fees().base * 2; + checkBasicReturnValidity(result, tx, env.seq(env.master), expectedFee); + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + json::Value const metadata = getJsonMetadata(result); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + json::Value tx; + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain] = "123ABC"; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = json::ValueType::Object; + tx[sfSponsorSignature.jsonName][sfSigners.jsonName] = json::ValueType::Array; + + json::Value signerObj; + signerObj[sfSigner][jss::Account] = signer.human(); + tx[sfSponsorSignature.jsonName][sfSigners.jsonName].append(signerObj); + + // Leave Fee unset so simulate must autofill it after sponsor signer normalization. + BEAST_EXPECT(!tx.isMember(jss::Fee)); + testTx(env, tx, validateOutput, false); + } + void testTransactionSigningFailure() { @@ -1195,6 +1347,7 @@ public: testTransactionNonTecFailure(); testTransactionTecFailure(); testSuccessfulTransactionMultisigned(); + testSuccessfulSponsoredTransactionMultisigned(); testTransactionSigningFailure(); testInvalidSingleAndMultiSigningTransaction(); testMultisignedBadPubKey(); diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index c3bf707ec4..4dae475b63 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -880,6 +881,125 @@ class Transaction_test : public beast::unit_test::Suite } } + void + testSignForNetworkIDValidation() + { + testcase("SignFor NetworkID validation"); + using namespace test::jtx; + + Account const owner{"owner"}; + Account const signer{"signer"}; + + auto makeConfig = [](std::uint32_t networkID) { + return envconfig([networkID](std::unique_ptr cfg) { + cfg->networkId = networkID; + return cfg; + }); + }; + + auto setupEnv = [&](Env& env) { + env.fund(XRP(10'000), owner, signer); + env.close(); + env(signers(owner, 1, {{signer, 1}})); + env.close(); + }; + + auto makeTx = [&](Env& env) { + json::Value tx; + tx[jss::TransactionType] = jss::AccountSet; + tx[jss::Account] = owner.human(); + tx[jss::Sequence] = env.seq(owner); + tx[jss::Fee] = "100"; + tx[jss::SigningPubKey] = ""; + return tx; + }; + + auto signFor = [&](Env& env, json::Value const& tx) { + json::Value signReq; + signReq[jss::tx_json] = tx; + signReq[jss::account] = signer.human(); + signReq[jss::secret] = signer.name(); + return env.rpc("json", "sign_for", to_string(signReq))[jss::result]; + }; + + // Test case: NetworkID < 1024 - field is not required + { + Env env{*this, makeConfig(500)}; + setupEnv(env); + + auto tx = makeTx(env); + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::status] == "success"); + BEAST_EXPECT(!result[jss::tx_json].isMember(jss::NetworkID)); + } + + // Test case: NetworkID > 1024 - missing NetworkID field + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Missing field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is not a number + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = "not_a_number"; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is not integral + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = 2040.1; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is different from + // actual NetworkID + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = 9999; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is correct + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = 2040; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::status] == "success"); + BEAST_EXPECT(result[jss::tx_json][jss::NetworkID].asUInt() == 2040); + } + } + public: void run() override @@ -889,6 +1009,8 @@ public: FeatureBitset const all{testableAmendments()}; testWithFeats(all); + + testSignForNetworkIDValidation(); } void diff --git a/src/test/server/ServerStatus_test.cpp b/src/test/server/ServerStatus_test.cpp index 4a9c12c96b..60ea622616 100644 --- a/src/test/server/ServerStatus_test.cpp +++ b/src/test/server/ServerStatus_test.cpp @@ -202,8 +202,6 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En if (ec) return; } - - return; } void @@ -218,7 +216,6 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En auto ip = env.app().config()[Sections::kPortWs].get(Keys::kIp); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) doRequest(yield, makeWSUpgrade(*ip, *port), *ip, *port, secure, resp, ec); - return; } void @@ -235,7 +232,6 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En auto const ip = env.app().config()[Sections::kPortRpc].get(Keys::kIp); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) doRequest(yield, makeHTTPRequest(*ip, *port, body, fields), *ip, *port, secure, resp, ec); - return; } static auto diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h index 3fc5d015b0..b583f821a4 100644 --- a/src/test/unit_test/FileDirGuard.h +++ b/src/test/unit_test/FileDirGuard.h @@ -15,8 +15,8 @@ namespace xrpl::detail { /** - Create a directory and remove it when it's done -*/ + * Create a directory and remove it when it's done + */ class DirGuard { protected: @@ -93,8 +93,8 @@ public: }; /** - Write a file in a directory and remove when done -*/ + * Write a file in a directory and remove when done + */ class FileDirGuard : public DirGuard { protected: diff --git a/src/test/unit_test/multi_runner.h b/src/test/unit_test/multi_runner.h index 3390014cb8..c17fdf3bf0 100644 --- a/src/test/unit_test/multi_runner.h +++ b/src/test/unit_test/multi_runner.h @@ -199,7 +199,8 @@ namespace test { //------------------------------------------------------------------------------ -/** Manager for children running unit tests +/** + * Manager for children running unit tests */ class MultiRunnerParent : private detail::MultiRunnerBase { @@ -234,7 +235,8 @@ public: //------------------------------------------------------------------------------ -/** A class to run a subset of unit tests +/** + * A class to run a subset of unit tests */ class MultiRunnerChild : public beast::unit_test::Runner, private detail::MultiRunnerBase diff --git a/src/test/unit_test/utils.h b/src/test/unit_test/utils.h index 677bbff31b..d4b1e5f7f4 100644 --- a/src/test/unit_test/utils.h +++ b/src/test/unit_test/utils.h @@ -6,9 +6,11 @@ namespace xrpl::test { -/// Compare two SecretKey objects for equality. -/// SecretKey::operator== is deleted, so a named function is used -/// to avoid member-function lookup shadowing free-function overloads. +/** + * Compare two SecretKey objects for equality. + * SecretKey::operator== is deleted, so a named function is used + * to avoid member-function lookup shadowing free-function overloads. + */ inline bool equal(SecretKey const& lhs, SecretKey const& rhs) { diff --git a/src/tests/libxrpl/basics/Buffer.cpp b/src/tests/libxrpl/basics/Buffer.cpp new file mode 100644 index 0000000000..9cdf610282 --- /dev/null +++ b/src/tests/libxrpl/basics/Buffer.cpp @@ -0,0 +1,260 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { + +struct BufferTest : public ::testing::Test +{ + static bool + sane(Buffer const& b) + { + if (b.empty()) + return b.data() == nullptr; + + return b.data() != nullptr; + } +}; + +TEST_F(BufferTest, buffer) +{ + std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, + 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, + 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; + + Buffer const b0; + EXPECT_TRUE(sane(b0)); + EXPECT_TRUE(b0.empty()); + + Buffer b1{0}; + EXPECT_TRUE(sane(b1)); + EXPECT_TRUE(b1.empty()); + std::memcpy(b1.alloc(16), data, 16); + EXPECT_TRUE(sane(b1)); + EXPECT_FALSE(b1.empty()); + EXPECT_EQ(b1.size(), 16); + + Buffer b2{b1.size()}; + EXPECT_TRUE(sane(b2)); + EXPECT_FALSE(b2.empty()); + EXPECT_EQ(b2.size(), b1.size()); + std::memcpy(b2.data(), data + 16, 16); + + Buffer b3{data, sizeof(data)}; + EXPECT_TRUE(sane(b3)); + EXPECT_FALSE(b3.empty()); + EXPECT_EQ(b3.size(), sizeof(data)); + EXPECT_EQ(std::memcmp(b3.data(), data, b3.size()), 0); + + // Check equality and inequality comparisons. + // For code readability, we want to use general + // EXPECT_TRUE instead of specific EXPECT_EQ etc. + EXPECT_TRUE(b0 == b0); + EXPECT_TRUE(b0 != b1); + EXPECT_TRUE(b1 == b1); + EXPECT_TRUE(b1 != b2); + EXPECT_TRUE(b2 != b3); + + // Check copy constructors and copy assignments: + { + Buffer x{b0}; + EXPECT_EQ(x, b0); + EXPECT_TRUE(sane(x)); + Buffer y{b1}; + EXPECT_EQ(y, b1); + EXPECT_TRUE(sane(y)); + x = b2; + EXPECT_EQ(x, b2); + EXPECT_TRUE(sane(x)); + x = y; + EXPECT_EQ(x, y); + EXPECT_TRUE(sane(x)); + y = b3; + EXPECT_EQ(y, b3); + EXPECT_TRUE(sane(y)); + x = b0; + EXPECT_EQ(x, b0); + EXPECT_TRUE(sane(x)); +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + + x = x; + EXPECT_EQ(x, b0); + EXPECT_TRUE(sane(x)); + y = y; + EXPECT_EQ(y, b3); + EXPECT_TRUE(sane(y)); + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + } + + // Check move constructor & move assignments: + { + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + { // Move-construct from empty buf + Buffer x; + Buffer const y{std::move(x)}; + EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(y)); + EXPECT_TRUE(y.empty()); + EXPECT_EQ(x, y); // NOLINT(bugprone-use-after-move) + } + + { // Move-construct from non-empty buf + Buffer x{b1}; + Buffer const y{std::move(x)}; + EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(y)); + EXPECT_EQ(y, b1); + } + + { // Move assign empty buf to empty buf + Buffer x; + Buffer y; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + } + + { // Move assign non-empty buf to empty buf + Buffer x; + Buffer y{b1}; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x, b1); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + } + + { // Move assign empty buf to non-empty buf + Buffer x{b1}; + Buffer y; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + } + + { // Move assign non-empty buf to non-empty buf + Buffer x{b1}; + Buffer y{b2}; + Buffer z{b3}; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_FALSE(x.empty()); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + + x = std::move(z); + EXPECT_TRUE(sane(x)); + EXPECT_FALSE(x.empty()); + EXPECT_TRUE(sane(z)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(z.empty()); // NOLINT(bugprone-use-after-move) + } + } + + { + Buffer w{static_cast(b0)}; + EXPECT_TRUE(sane(w)); + EXPECT_EQ(w, b0); + + Buffer x{static_cast(b1)}; + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x, b1); + + Buffer y{static_cast(b2)}; + EXPECT_TRUE(sane(y)); + EXPECT_EQ(y, b2); + + Buffer z{static_cast(b3)}; + EXPECT_TRUE(sane(z)); + EXPECT_EQ(z, b3); + + // Assign empty slice to empty buffer + w = static_cast(b0); + EXPECT_TRUE(sane(w)); + EXPECT_EQ(w, b0); + + // Assign non-empty slice to empty buffer + w = static_cast(b1); + EXPECT_TRUE(sane(w)); + EXPECT_EQ(w, b1); + + // Assign non-empty slice to non-empty buffer + x = static_cast(b2); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x, b2); + + // Assign non-empty slice to non-empty buffer + y = static_cast(z); + EXPECT_TRUE(sane(y)); + EXPECT_EQ(y, z); + + // Assign empty slice to non-empty buffer: + z = static_cast(b0); + EXPECT_TRUE(sane(z)); + EXPECT_EQ(z, b0); + } + + { + auto test = [](Buffer const& b, std::size_t i) { + Buffer x{b}; + + // Try to allocate some number of bytes, possibly + // zero (which means clear) and sanity check + x(i); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x.size(), i); + EXPECT_EQ((x.data() == nullptr), (i == 0)); + + // Try to allocate some more data (always non-zero) + x(i + 1); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x.size(), i + 1); + EXPECT_NE(x.data(), nullptr); + + // Try to clear: + x.clear(); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_EQ(x.data(), nullptr); + + // Try to clear again: + x.clear(); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_EQ(x.data(), nullptr); + }; + + for (std::size_t i = 0; i < 16; ++i) + { + test(b0, i); + test(b1, i); + } + } +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/basics/FileUtilities.cpp b/src/tests/libxrpl/basics/FileUtilities.cpp new file mode 100644 index 0000000000..cd24abd696 --- /dev/null +++ b/src/tests/libxrpl/basics/FileUtilities.cpp @@ -0,0 +1,94 @@ +#include + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace xrpl { + +namespace { + +class TempFile +{ +public: + explicit TempFile(boost::filesystem::path file, std::string const& contents) + : dir_( + boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path("xrpl-file-utilities-%%%%-%%%%-%%%%")) + , file_(dir_ / file) + { + boost::filesystem::create_directory(dir_); + + std::ofstream output(file_.string()); + if (!output) + throw std::runtime_error("Unable to create temporary test file"); + + output << contents; + } + + ~TempFile() + { + boost::system::error_code ec; + boost::filesystem::remove(file_, ec); + boost::filesystem::remove(dir_, ec); + } + + [[nodiscard]] boost::filesystem::path const& + file() const + { + return file_; + } + +private: + boost::filesystem::path dir_; + boost::filesystem::path file_; +}; + +} // namespace + +TEST(FileUtilitiesTest, get_file_contents) +{ + using namespace boost::system; + + constexpr char const* kExpectedContents = "This file is very short. That's all we need."; + + TempFile const file("test_file", "This is temporary text that should get overwritten"); + + error_code ec; + auto const path = file.file(); + + writeFileContents(ec, path, kExpectedContents); + EXPECT_FALSE(ec); + + { + // Test with no max + auto const good = getFileContents(ec, path); + EXPECT_FALSE(ec); + EXPECT_EQ(good, kExpectedContents); + } + + { + // Test with large max + auto const good = getFileContents(ec, path, kilobytes(1)); + EXPECT_FALSE(ec); + EXPECT_EQ(good, kExpectedContents); + } + + { + // Test with small max + auto const bad = getFileContents(ec, path, 16); + EXPECT_TRUE(ec && ec.value() == boost::system::errc::file_too_large); + EXPECT_TRUE(bad.empty()); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/IOUAmount.cpp b/src/tests/libxrpl/basics/IOUAmount.cpp new file mode 100644 index 0000000000..46d6d7c5e1 --- /dev/null +++ b/src/tests/libxrpl/basics/IOUAmount.cpp @@ -0,0 +1,243 @@ +#include + +#include +#include + +#include + +#include +#include +#include +#include + +namespace xrpl { + +TEST(IOUAmountTest, zero) +{ + IOUAmount const z(0, 0); + + EXPECT_EQ(z.mantissa(), 0); + EXPECT_EQ(z.exponent(), -100); + EXPECT_FALSE(z); + EXPECT_EQ(z.signum(), 0); + EXPECT_EQ(z, beast::kZero); + + EXPECT_EQ((z + z), z); + EXPECT_EQ((z - z), z); + EXPECT_EQ(z, -z); + + IOUAmount const zz(beast::kZero); + EXPECT_EQ(z, zz); + + // https://github.com/XRPLF/rippled/issues/5170 + IOUAmount const zzz{}; + EXPECT_EQ(zzz, beast::kZero); + // EXPECT_EQ(zzz, zz); +} + +TEST(IOUAmountTest, sig_num) +{ + IOUAmount const neg(-1, 0); + EXPECT_LT(neg.signum(), 0); + + IOUAmount const zer(0, 0); + EXPECT_EQ(zer.signum(), 0); + + IOUAmount const pos(1, 0); + EXPECT_GT(pos.signum(), 0); +} + +TEST(IOUAmountTest, beast_zero) +{ + using beast::kZero; + + { + IOUAmount const z(kZero); + EXPECT_TRUE(z == kZero); + EXPECT_TRUE(z >= kZero); + EXPECT_TRUE(z <= kZero); + EXPECT_FALSE(z != kZero); + EXPECT_FALSE(z > kZero); + EXPECT_FALSE(z < kZero); + } + + { + IOUAmount const neg(-2, 0); + EXPECT_TRUE(neg < kZero); + EXPECT_TRUE(neg <= kZero); + EXPECT_TRUE(neg != kZero); + EXPECT_FALSE(neg == kZero); + } + + { + IOUAmount const pos(2, 0); + EXPECT_TRUE(pos > kZero); + EXPECT_TRUE(pos >= kZero); + EXPECT_TRUE(pos != kZero); + EXPECT_FALSE(pos == kZero); + } +} + +TEST(IOUAmountTest, comparisons) +{ + IOUAmount const n(-2, 0); + IOUAmount const z(0, 0); + IOUAmount const p(2, 0); + // For code readability, we want to use general + // EXPECT_TRUE instead of specific EXPECT_EQ etc. + EXPECT_TRUE(z == z); + EXPECT_TRUE(z >= z); + EXPECT_TRUE(z <= z); + EXPECT_TRUE(z == -z); + // NOLINTBEGIN(misc-redundant-expression) + EXPECT_FALSE(z > z); + EXPECT_FALSE(z < z); + EXPECT_FALSE(z != z); + // NOLINTEND(misc-redundant-expression) + EXPECT_FALSE(z != -z); + + EXPECT_TRUE(n < z); + EXPECT_TRUE(n <= z); + EXPECT_TRUE(n != z); + EXPECT_FALSE(n > z); + EXPECT_FALSE(n >= z); + EXPECT_FALSE(n == z); + + EXPECT_TRUE(p > z); + EXPECT_TRUE(p >= z); + EXPECT_TRUE(p != z); + EXPECT_FALSE(p < z); + EXPECT_FALSE(p <= z); + EXPECT_FALSE(p == z); + + EXPECT_TRUE(n < p); + EXPECT_TRUE(n <= p); + EXPECT_TRUE(n != p); + EXPECT_FALSE(n > p); + EXPECT_FALSE(n >= p); + EXPECT_FALSE(n == p); + + EXPECT_TRUE(p > n); + EXPECT_TRUE(p >= n); + EXPECT_TRUE(p != n); + EXPECT_FALSE(p < n); + EXPECT_FALSE(p <= n); + EXPECT_FALSE(p == n); + + EXPECT_TRUE(p > -p); + EXPECT_TRUE(p >= -p); + EXPECT_TRUE(p != -p); + + EXPECT_TRUE(n < -n); + EXPECT_TRUE(n <= -n); + EXPECT_TRUE(n != -n); +} + +TEST(IOUAmountTest, to_string) +{ + auto test = [](IOUAmount const& n, std::string const& expected) { + auto const result = to_string(n); + std::stringstream ss; + ss << "to_string(" << result << "). Expected: " << expected; + EXPECT_EQ(result, expected) << ss.str(); + }; + + for (auto const mantissaSize : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg(mantissaSize); + + test(IOUAmount(-2, 0), "-2"); + test(IOUAmount(0, 0), "0"); + test(IOUAmount(2, 0), "2"); + test(IOUAmount(25, -3), "0.025"); + test(IOUAmount(-25, -3), "-0.025"); + test(IOUAmount(25, 1), "250"); + test(IOUAmount(-25, 1), "-250"); + test(IOUAmount(2, 20), "2e20"); + test(IOUAmount(-2, -20), "-2e-20"); + } +} + +TEST(IOUAmountTest, mul_ratio) +{ + /* The range for the mantissa when normalized */ + constexpr std::int64_t kMinMantissa = 1000000000000000ull; + constexpr std::int64_t kMaxMantissa = 9999999999999999ull; + // log(2,maxMantissa) ~ 53.15 + /* The range for the exponent when normalized */ + constexpr int kMinExponent = -96; + constexpr int kMaxExponent = 80; + constexpr auto kMaxUInt = std::numeric_limits::max(); + + { + // multiply by a number that would overflow the mantissa, then + // divide by the same number, and check we didn't lose any value + IOUAmount const bigMan(kMaxMantissa, 0); + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); + } + { + // Similar test as above, but for negative values + IOUAmount const bigMan(-kMaxMantissa, 0); + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); + } + + { + // small amounts + IOUAmount const tiny(kMinMantissa, kMinExponent); + // Round up should give the smallest allowable number + EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt, true)); + EXPECT_EQ(tiny, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, true)); + // rounding down should be zero + EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt, false)); + EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, false)); + + // tiny negative numbers + IOUAmount const tinyNeg(-kMinMantissa, kMinExponent); + // Round up should give zero + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt, true)); + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, true)); + // rounding down should be tiny + EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, 1, kMaxUInt, false)); + EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, false)); + } + + { // rounding + { + IOUAmount const one(1, 0); + auto const rup = mulRatio(one, kMaxUInt - 1, kMaxUInt, true); + auto const rdown = mulRatio(one, kMaxUInt - 1, kMaxUInt, false); + EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1); + } + { + IOUAmount const big(kMaxMantissa, kMaxExponent); + auto const rup = mulRatio(big, kMaxUInt - 1, kMaxUInt, true); + auto const rdown = mulRatio(big, kMaxUInt - 1, kMaxUInt, false); + EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1); + } + + { + IOUAmount const negOne(-1, 0); + auto const rup = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, true); + auto const rdown = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, false); + EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1); + } + } + + { + // division by zero + IOUAmount const one(1, 0); + EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); }); + } + + { + // overflow + IOUAmount const big(kMaxMantissa, kMaxExponent); + EXPECT_ANY_THROW({ mulRatio(big, 2, 0, true); }); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/IntrusiveShared.cpp b/src/tests/libxrpl/basics/IntrusiveShared.cpp new file mode 100644 index 0000000000..e798cd1ccc --- /dev/null +++ b/src/tests/libxrpl/basics/IntrusiveShared.cpp @@ -0,0 +1,844 @@ +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include + +#include + +#include +#include +#include +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::tests { + +/* + * Experimentally, we discovered that using std::barrier performs extremely + * poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS + * environments. To unblock our macOS CI pipeline, we replaced std::barrier with a + * custom mutex-based barrier (Barrier) that significantly improves performance + * without compromising correctness. For future reference, if we ever consider + * reintroducing std::barrier, the following configuration is known to exhibit the + * problem: + * + * Model Name: Mac mini + * Model Identifier: Mac14,3 + * Model Number: Z16K000R4LL/A + * Chip: Apple M2 + * Total Number of Cores: 8 (4 performance and 4 efficiency) + * Memory: 24 GB + * System Firmware Version: 11881.41.5 + * OS Loader Version: 11881.1.1 + * Apple clang version 16.0.0 (clang-1600.0.26.3) + * Target: arm64-apple-darwin24.0.0 + * Thread model: posix + * + */ +struct Barrier +{ + std::mutex mtx; + std::condition_variable cv; + int count; + int const initial; + std::size_t generation{0}; + + explicit Barrier(int n) : count(n), initial(n) + { + } + + void + arriveAndWait() + { + std::unique_lock lock(mtx); + auto const currentGeneration = generation; + if (--count == 0) + { + ++generation; + count = initial; + cv.notify_all(); + } + else + { + cv.wait(lock, [&] { return generation != currentGeneration; }); + } + } +}; + +namespace { +enum class TrackedState : std::uint8_t { + Uninitialized, + Alive, + PartiallyDeletedStarted, + PartiallyDeleted, + DeletedStarted, + Deleted +}; + +class TIBase : public IntrusiveRefCounts +{ +public: + static constexpr std::size_t kMaxStates = 128; + static std::array, kMaxStates> state; + static std::atomic nextId; + static TrackedState + getState(std::size_t id) + { + if (id >= state.size()) + throw std::out_of_range("TIBase state id out of range"); + + return state[id].load(std::memory_order_acquire); + } + static void + resetStates(bool resetCallback) + { + for (std::size_t i = 0; i < kMaxStates; ++i) + { + state[i].store(TrackedState::Uninitialized, std::memory_order_release); + } + nextId.store(0, std::memory_order_release); + if (resetCallback) + TIBase::tracingCallback = [](TrackedState, std::optional) {}; + } + + struct ResetStatesGuard + { + bool resetCallback{false}; + + ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback} + { + TIBase::resetStates(resetCallback); + } + ~ResetStatesGuard() + { + TIBase::resetStates(resetCallback); + } + }; + + TIBase() : id{checkoutID()} + { + state[id].store(TrackedState::Alive, std::memory_order_relaxed); + } + ~TIBase() override + { + using enum TrackedState; + + tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted); + + // Use relaxed memory order to try to avoid atomic operations from + // adding additional memory synchronizations that may hide threading + // errors in the underlying shared pointer class. + state[id].store(DeletedStarted, std::memory_order_relaxed); + + tracingCallback(DeletedStarted, Deleted); + + state[id].store(TrackedState::Deleted, std::memory_order_relaxed); + + tracingCallback(TrackedState::Deleted, std::nullopt); + } + + void + partialDestructor() const + { + using enum TrackedState; + + tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted); + + state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed); + + tracingCallback(PartiallyDeletedStarted, PartiallyDeleted); + + state[id].store(PartiallyDeleted, std::memory_order_relaxed); + + tracingCallback(PartiallyDeleted, std::nullopt); + } + + static std::function)> tracingCallback; + + std::size_t const id; + +private: + static std::size_t + checkoutID() + { + auto const id = nextId.fetch_add(1, std::memory_order_acq_rel); + if (id >= state.size()) + throw std::out_of_range("TIBase state capacity exceeded"); + + return id; + } +}; + +std::array, TIBase::kMaxStates> TIBase::state; +std::atomic TIBase::nextId{0}; + +std::function)> TIBase::tracingCallback = + [](TrackedState, std::optional) {}; + +} // namespace + +TEST(IntrusiveSharedTest, basics) +{ + { + TIBase::ResetStatesGuard const rsg{true}; + + TIBase const b; + EXPECT_EQ(b.useCount(), 1); + b.addWeakRef(); + EXPECT_EQ(b.useCount(), 1); + auto s = b.releaseStrongRef(); + EXPECT_EQ(s, ReleaseStrongRefAction::PartialDestroy); + EXPECT_EQ(b.useCount(), 0); + TIBase const* pb = &b; + partialDestructorFinished(&pb); + EXPECT_FALSE(pb); + auto w = b.releaseWeakRef(); + EXPECT_EQ(w, ReleaseWeakRefAction::Destroy); + } + + std::vector> strong; + std::vector> weak; + { + TIBase::ResetStatesGuard const rsg{true}; + + using enum TrackedState; + auto b = makeSharedIntrusive(); + auto id = b->id; + EXPECT_EQ(TIBase::getState(id), Alive); + EXPECT_EQ(b->useCount(), 1); + for (int i = 0; i < 10; ++i) + { + strong.push_back(b); + } + b.reset(); + EXPECT_EQ(TIBase::getState(id), Alive); + strong.resize(strong.size() - 1); + EXPECT_EQ(TIBase::getState(id), Alive); + strong.clear(); + EXPECT_EQ(TIBase::getState(id), Deleted); + + b = makeSharedIntrusive(); + id = b->id; + EXPECT_EQ(TIBase::getState(id), Alive); + EXPECT_EQ(b->useCount(), 1); + for (int i = 0; i < 10; ++i) + { + weak.emplace_back(b); + EXPECT_EQ(b->useCount(), 1); + } + EXPECT_EQ(TIBase::getState(id), Alive); + weak.resize(weak.size() - 1); + EXPECT_EQ(TIBase::getState(id), Alive); + b.reset(); + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + while (!weak.empty()) + { + weak.resize(weak.size() - 1); + if (!weak.empty()) + { + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + } + } + EXPECT_EQ(TIBase::getState(id), Deleted); + } + { + TIBase::ResetStatesGuard const rsg{true}; + + using enum TrackedState; + auto b = makeSharedIntrusive(); + auto id = b->id; + EXPECT_EQ(TIBase::getState(id), Alive); + WeakIntrusive w{b}; + EXPECT_EQ(TIBase::getState(id), Alive); + auto s = w.lock(); + EXPECT_TRUE(s && s->useCount() == 2); + b.reset(); + EXPECT_TRUE(TIBase::getState(id) == Alive); + EXPECT_TRUE(s && s->useCount() == 1); + s.reset(); + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + EXPECT_TRUE(w.expired()); + s = w.lock(); + // Cannot convert a weak pointer to a strong pointer if object is + // already partially deleted + EXPECT_FALSE(s); + w.reset(); + EXPECT_EQ(TIBase::getState(id), Deleted); + } + { + TIBase::ResetStatesGuard const rsg{true}; + + using enum TrackedState; + using swu = SharedWeakUnion; + swu b = makeSharedIntrusive(); + EXPECT_TRUE(b.isStrong() && b.useCount() == 1); + auto id = b.get()->id; + EXPECT_EQ(TIBase::getState(id), Alive); + swu w = b; + EXPECT_TRUE(TIBase::getState(id) == Alive); + EXPECT_TRUE(w.isStrong() && b.useCount() == 2); + w.convertToWeak(); + EXPECT_TRUE(w.isWeak() && b.useCount() == 1); + swu s = w; + EXPECT_TRUE(s.isWeak() && b.useCount() == 1); + s.convertToStrong(); + EXPECT_TRUE(s.isStrong() && b.useCount() == 2); + b.reset(); + EXPECT_EQ(TIBase::getState(id), Alive); + EXPECT_EQ(s.useCount(), 1); + EXPECT_FALSE(w.expired()); + s.reset(); + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + EXPECT_TRUE(w.expired()); + w.convertToStrong(); + // Cannot convert a weak pointer to a strong pointer if object is + // already partially deleted + EXPECT_TRUE(w.isWeak()); + w.reset(); + EXPECT_EQ(TIBase::getState(id), Deleted); + } + { + // Testing SharedWeakUnion assignment operator + + TIBase::ResetStatesGuard const rsg{true}; + + auto strong1 = makeSharedIntrusive(); + auto strong2 = makeSharedIntrusive(); + + auto id1 = strong1->id; + auto id2 = strong2->id; + + EXPECT_NE(id1, id2); + + SharedWeakUnion union1 = strong1; + SharedWeakUnion union2 = strong2; + + EXPECT_TRUE(union1.isStrong()); + EXPECT_TRUE(union2.isStrong()); + EXPECT_EQ(union1.get(), strong1.get()); + EXPECT_EQ(union2.get(), strong2.get()); + + // 1) Normal assignment: explicitly calls SharedWeakUnion assignment + union1 = union2; + EXPECT_TRUE(union1.isStrong()); + EXPECT_TRUE(union2.isStrong()); + EXPECT_EQ(union1.get(), union2.get()); + EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive); + EXPECT_EQ(TIBase::getState(id2), TrackedState::Alive); + + // 2) Test self-assignment + EXPECT_TRUE(union1.isStrong()); + EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive); + int const initialRefCount = strong1->useCount(); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-assign-overloaded" + union1 = union1; // Self-assignment +#pragma clang diagnostic pop + EXPECT_TRUE(union1.isStrong()); + EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive); + EXPECT_EQ(strong1->useCount(), initialRefCount); + + // 3) Test assignment from null union pointer + union1 = SharedWeakUnion(); + EXPECT_EQ(union1.get(), nullptr); + + // 4) Test assignment to expired union pointer + strong2.reset(); + union2.reset(); + union1 = union2; + EXPECT_EQ(union1.get(), nullptr); + EXPECT_EQ(TIBase::getState(id2), TrackedState::Deleted); + } +} + +TEST(IntrusiveSharedTest, partial_delete) +{ + // This test creates two threads. One with a strong pointer and one + // with a weak pointer. The strong pointer is reset while the weak + // pointer still holds a reference, triggering a partial delete. + // While the partial delete function runs (a sleep is inserted) the + // weak pointer is reset. The destructor should wait to run until + // after the partial delete function has completed running. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + auto strong = makeSharedIntrusive(); + WeakIntrusive weak{strong}; + std::atomic destructorRan{false}; + std::atomic partialDeleteRan{false}; + std::latch partialDeleteStartedSyncPoint{2}; + strong->tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + if (next == DeletedStarted) + { + // strong goes out of scope while weak is still in scope + // This checks that partialDelete has run to completion + // before the destructor is called. A sleep is inserted + // inside the partial delete to make sure the destructor is + // given an opportunity to run during partial delete. + EXPECT_EQ(cur, PartiallyDeleted); + } + if (next == PartiallyDeletedStarted) + { + partialDeleteStartedSyncPoint.arrive_and_wait(); + using namespace std::chrono_literals; + // Sleep and let the weak pointer go out of scope, + // potentially triggering a destructor while partial delete + // is running. The test is to make sure that doesn't happen. + std::this_thread::sleep_for(800ms); + } + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan.exchange(true)); + } + }; + std::thread t1{[&] { + partialDeleteStartedSyncPoint.arrive_and_wait(); + weak.reset(); // Trigger a full delete as soon as the partial + // delete starts + }}; + std::thread t2{[&] { + strong.reset(); // Trigger a partial delete + }}; + t1.join(); + t2.join(); + + EXPECT_TRUE(destructorRan.load() && partialDeleteRan.load()); +} + +TEST(IntrusiveSharedTest, destructor) +{ + // This test creates two threads. One with a strong pointer and one + // with a weak pointer. The weak pointer is reset while the strong + // pointer still holds a reference. Then the strong pointer is + // reset. Only the destructor should run. The partial destructor + // should not be called. Since the weak reset runs to completion + // before the strong pointer is reset, threading doesn't add much to + // this test, but there is no harm in keeping it. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + auto strong = makeSharedIntrusive(); + WeakIntrusive weak{strong}; + std::atomic destructorRan{false}; + std::atomic partialDeleteRan{false}; + std::latch weakResetSyncPoint{2}; + strong->tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan.exchange(true)); + } + }; + std::thread t1{[&] { + weak.reset(); + weakResetSyncPoint.arrive_and_wait(); + }}; + std::thread t2{[&] { + weakResetSyncPoint.arrive_and_wait(); + strong.reset(); // Trigger a partial delete + }}; + t1.join(); + t2.join(); + + EXPECT_TRUE(destructorRan.load() && !partialDeleteRan.load()); +} + +TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) +{ + // This test creates and destroys many strong and weak pointers in a + // loop. There is a random mix of strong and weak pointers stored in + // a vector (held as a variant). Both threads clear all the pointers + // and check that the invariants hold. + + using enum TrackedState; + TIBase::ResetStatesGuard const rsg{true}; + + std::atomic destructionState{0}; + // returns destructorRan and partialDestructorRan (in that order) + auto getDestructorState = [&]() -> std::pair { + int const s = destructionState.load(std::memory_order_relaxed); + return {(s & 1) != 0, (s & 2) != 0}; + }; + auto setDestructorRan = [&]() -> void { + destructionState.fetch_or(1, std::memory_order_acq_rel); + }; + auto setPartialDeleteRan = [&]() -> void { + destructionState.fetch_or(2, std::memory_order_acq_rel); + }; + auto tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + auto [destructorRan, partialDeleteRan] = getDestructorState(); + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan); + setDestructorRan(); + } + }; + auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) + -> std::vector, WeakIntrusive>> { + std::vector, WeakIntrusive>> result; + std::uniform_int_distribution<> toCreateDist(4, 64); + std::uniform_int_distribution<> isStrongDist(0, 1); + auto numToCreate = toCreateDist(eng); + result.reserve(numToCreate); + for (int i = 0; i < numToCreate; ++i) + { + if (isStrongDist(eng)) + { + result.emplace_back(SharedIntrusive(toClone)); + } + else + { + result.emplace_back(WeakIntrusive(toClone)); + } + } + return result; + }; + constexpr int kLoopIters = 2 * 1024; + constexpr int kNumThreads = 16; + std::vector> toClone; + Barrier loopStartSyncPoint{kNumThreads}; + Barrier postCreateToCloneSyncPoint{kNumThreads}; + Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; + auto engines = [&]() -> std::vector { + std::random_device rd; + std::vector result; + result.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + result.emplace_back(rd()); + return result; + }(); + + // cloneAndDestroy clones the strong pointer into a vector of mixed + // strong and weak pointers and destroys them all at once. + // threadId==0 is special. + auto cloneAndDestroy = [&](int threadId) { + for (int i = 0; i < kLoopIters; ++i) + { + // ------ Sync Point ------ + loopStartSyncPoint.arriveAndWait(); + + // only thread 0 should reset the state + std::optional rsg; + if (threadId == 0) + { + // Thread 0 is the genesis thread. It creates the strong + // pointers to be cloned by the other threads. This + // thread will also check that the destructor ran and + // clear the temporary variables. + + rsg.emplace(false); + auto [destructorRan, partialDeleteRan] = getDestructorState(); + EXPECT_TRUE(i == 0 || destructorRan); + destructionState.store(0, std::memory_order_release); + + toClone.clear(); + toClone.resize(kNumThreads); + auto strong = makeSharedIntrusive(); + strong->tracingCallback = tracingCallback; + std::ranges::fill(toClone, strong); + } + + // ------ Sync Point ------ + postCreateToCloneSyncPoint.arriveAndWait(); + + auto v = createVecOfPointers(toClone[threadId], engines[threadId]); + toClone[threadId].reset(); + + // ------ Sync Point ------ + postCreateVecOfPointersSyncPoint.arriveAndWait(); + + v.clear(); + } + }; + std::vector threads; + threads.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + { + threads.emplace_back(cloneAndDestroy, i); + } + for (int i = 0; i < kNumThreads; ++i) + { + threads[i].join(); + } +} + +TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) +{ + // This test creates and destroys many SharedWeak pointers in a + // loop. All the pointers start as strong and a loop randomly + // convert them between strong and weak pointers. Both threads clear + // all the pointers and check that the invariants hold. + // + // Note: This test also differs from the test above in that the pointers + // randomly change from strong to weak and from weak to strong in a + // loop. This can't be done in the variant test above because variant is + // not thread safe while the SharedWeakUnion is thread safe. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + std::atomic destructionState{0}; + // returns destructorRan and partialDestructorRan (in that order) + auto getDestructorState = [&]() -> std::pair { + int const s = destructionState.load(std::memory_order_relaxed); + return {(s & 1) != 0, (s & 2) != 0}; + }; + auto setDestructorRan = [&]() -> void { + destructionState.fetch_or(1, std::memory_order_acq_rel); + }; + auto setPartialDeleteRan = [&]() -> void { + destructionState.fetch_or(2, std::memory_order_acq_rel); + }; + auto tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + auto [destructorRan, partialDeleteRan] = getDestructorState(); + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan); + setDestructorRan(); + } + }; + auto createVecOfPointers = + [&](auto const& toClone, + std::default_random_engine& eng) -> std::vector> { + std::vector> result; + std::uniform_int_distribution<> toCreateDist(4, 64); + auto numToCreate = toCreateDist(eng); + result.reserve(numToCreate); + for (int i = 0; i < numToCreate; ++i) + result.emplace_back(SharedIntrusive(toClone)); + return result; + }; + constexpr int kLoopIters = 2 * 1024; + constexpr int kFlipPointersLoopIters = 256; + constexpr int kNumThreads = 16; + std::vector> toClone; + Barrier loopStartSyncPoint{kNumThreads}; + Barrier postCreateToCloneSyncPoint{kNumThreads}; + Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; + Barrier postFlipPointersLoopSyncPoint{kNumThreads}; + auto engines = [&]() -> std::vector { + std::random_device rd; + std::vector result; + result.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + result.emplace_back(rd()); + return result; + }(); + + // cloneAndDestroy clones the strong pointer into a vector of + // mixed strong and weak pointers, runs a loop that randomly + // changes strong pointers to weak pointers, and destroys them + // all at once. + auto cloneAndDestroy = [&](int threadId) { + for (int i = 0; i < kLoopIters; ++i) + { + // ------ Sync Point ------ + loopStartSyncPoint.arriveAndWait(); + + // only thread 0 should reset the state + std::optional rsg; + if (threadId == 0) + { + // threadId 0 is the genesis thread. It creates the + // strong point to be cloned by the other threads. This + // thread will also check that the destructor ran and + // clear the temporary variables. + rsg.emplace(false); + auto [destructorRan, partialDeleteRan] = getDestructorState(); + EXPECT_TRUE(i == 0 || destructorRan); + destructionState.store(0, std::memory_order_release); + + toClone.clear(); + toClone.resize(kNumThreads); + auto strong = makeSharedIntrusive(); + strong->tracingCallback = tracingCallback; + std::ranges::fill(toClone, strong); + } + + // ------ Sync Point ------ + postCreateToCloneSyncPoint.arriveAndWait(); + + auto v = createVecOfPointers(toClone[threadId], engines[threadId]); + toClone[threadId].reset(); + + // ------ Sync Point ------ + postCreateVecOfPointersSyncPoint.arriveAndWait(); + + std::uniform_int_distribution<> isStrongDist(0, 1); + for (int f = 0; f < kFlipPointersLoopIters; ++f) + { + for (auto& p : v) + { + if (isStrongDist(engines[threadId])) + { + p.convertToStrong(); + } + else + { + p.convertToWeak(); + } + } + } + + // ------ Sync Point ------ + postFlipPointersLoopSyncPoint.arriveAndWait(); + + v.clear(); + } + }; + std::vector threads; + threads.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + { + threads.emplace_back(cloneAndDestroy, i); + } + for (int i = 0; i < kNumThreads; ++i) + { + threads[i].join(); + } +} + +TEST(IntrusiveSharedTest, multithreaded_locking_weak) +{ + // This test creates a single shared atomic pointer that multiple thread + // create weak pointers from. The threads then lock the weak pointers. + // Both threads clear all the pointers and check that the invariants + // hold. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + std::atomic destructionState{0}; + // returns destructorRan and partialDestructorRan (in that order) + auto getDestructorState = [&]() -> std::pair { + int const s = destructionState.load(std::memory_order_relaxed); + return {(s & 1) != 0, (s & 2) != 0}; + }; + auto setDestructorRan = [&]() -> void { + destructionState.fetch_or(1, std::memory_order_acq_rel); + }; + auto setPartialDeleteRan = [&]() -> void { + destructionState.fetch_or(2, std::memory_order_acq_rel); + }; + auto tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + auto [destructorRan, partialDeleteRan] = getDestructorState(); + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan); + setDestructorRan(); + } + }; + + constexpr int kLoopIters = 2 * 1024; + constexpr int kLockWeakLoopIters = 256; + constexpr int kNumThreads = 16; + std::vector> toLock; + Barrier loopStartSyncPoint{kNumThreads}; + Barrier postCreateToLockSyncPoint{kNumThreads}; + Barrier postLockWeakLoopSyncPoint{kNumThreads}; + + // lockAndDestroy creates weak pointers from the strong pointer + // and runs a loop that locks the weak pointer. At the end of the loop + // all the pointers are destroyed all at once. + auto lockAndDestroy = [&](int threadId) { + for (int i = 0; i < kLoopIters; ++i) + { + // ------ Sync Point ------ + loopStartSyncPoint.arriveAndWait(); + + // only thread 0 should reset the state + std::optional rsg; + if (threadId == 0) + { + // threadId 0 is the genesis thread. It creates the + // strong point to be locked by the other threads. This + // thread will also check that the destructor ran and + // clear the temporary variables. + rsg.emplace(false); + auto [destructorRan, partialDeleteRan] = getDestructorState(); + EXPECT_TRUE(i == 0 || destructorRan); + destructionState.store(0, std::memory_order_release); + + toLock.clear(); + toLock.resize(kNumThreads); + auto strong = makeSharedIntrusive(); + strong->tracingCallback = tracingCallback; + std::ranges::fill(toLock, strong); + } + + // ------ Sync Point ------ + postCreateToLockSyncPoint.arriveAndWait(); + + // Multiple threads all create a weak pointer from the same + // strong pointer + WeakIntrusive const weak{toLock[threadId]}; + for (int wi = 0; wi < kLockWeakLoopIters; ++wi) + { + EXPECT_FALSE(weak.expired()); + auto strong = weak.lock(); + EXPECT_TRUE(strong); + } + + // ------ Sync Point ------ + postLockWeakLoopSyncPoint.arriveAndWait(); + + toLock[threadId].reset(); + } + }; + std::vector threads; + threads.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + { + threads.emplace_back(lockAndDestroy, i); + } + for (int i = 0; i < kNumThreads; ++i) + { + threads[i].join(); + } +} + +} // namespace xrpl::tests diff --git a/src/tests/libxrpl/basics/KeyCache.cpp b/src/tests/libxrpl/basics/KeyCache.cpp new file mode 100644 index 0000000000..061264b11d --- /dev/null +++ b/src/tests/libxrpl/basics/KeyCache.cpp @@ -0,0 +1,81 @@ +#include +#include // IWYU pragma: keep +#include +#include +#include + +#include +#include + +#include + +namespace xrpl { + +class KeyCacheTest : public ::testing::Test +{ +public: +}; + +TEST_F(KeyCacheTest, key_cache) +{ + using namespace std::chrono_literals; + TestStopwatch clock; + clock.set(0); + + using Key = std::string; + using Cache = TaggedCache; + + beast::Journal const j{TestSink::instance()}; + + // Insert an item, retrieve it, and age it so it gets purged. + { + Cache c("test", LedgerIndex(1), 2s, clock, j); + + EXPECT_EQ(c.size(), 0); + EXPECT_TRUE(c.insert("one")); + EXPECT_FALSE(c.insert("one")); + EXPECT_EQ(c.size(), 1); + EXPECT_TRUE(c.touchIfExists("one")); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 1); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 0); + EXPECT_FALSE(c.touchIfExists("one")); + } + + // Insert two items, have one expire + { + Cache c("test", LedgerIndex(2), 2s, clock, j); + + EXPECT_TRUE(c.insert("one")); + EXPECT_EQ(c.size(), 1); + EXPECT_TRUE(c.insert("two")); + EXPECT_EQ(c.size(), 2); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 2); + EXPECT_TRUE(c.touchIfExists("two")); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 1); + } + + // Insert three items (1 over limit), sweep + { + Cache c("test", LedgerIndex(2), 3s, clock, j); + + EXPECT_TRUE(c.insert("one")); + ++clock; + EXPECT_TRUE(c.insert("two")); + ++clock; + EXPECT_TRUE(c.insert("three")); + ++clock; + EXPECT_EQ(c.size(), 3); + c.sweep(); + EXPECT_LT(c.size(), 3); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/Number.cpp b/src/tests/libxrpl/basics/Number.cpp new file mode 100644 index 0000000000..36e1b4a700 --- /dev/null +++ b/src/tests/libxrpl/basics/Number.cpp @@ -0,0 +1,2896 @@ +#include + +#include +#include +#include +#include +#include + +// NOLINTNEXTLINE(misc-include-cleaner) +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +using BigInt = boost::multiprecision::cpp_int; +using Dec = boost::multiprecision::cpp_dec_float_50; + +static std::string +fmt(BigInt const& value) +{ + auto s = to_string(value); + std::string out; + int count = 0; + for (char const& ch : std::views::reverse(s)) + { + if (count != 0 && count % 3 == 0 && (isdigit(ch) != 0)) + out.insert(out.begin(), '_'); + out.insert(out.begin(), ch); + ++count; + } + return out; +} + +BigInt +toBigInt(Number const& n) +{ + BigInt v = n.mantissa(); + auto e = n.exponent(); + + for (; e > 0; --e) + v *= 10; + for (; e < 0; ++e) + { + EXPECT_EQ(v % 10, 0); + v /= 10; + } + return v; +} + +template +static T +pow10(int n) +{ + if (n == 0) + return 1; + if (n == 1) + return 10; + + if (n > 1) + { + auto r = pow10(n / 2); + r *= r; + if (n % 2 != 0) + r *= 10; + return r; + } + + T p = 1; + p /= pow10(-n); + return p; +} + +static std::string +fmt(Dec const& value) +{ + std::ostringstream os; + os << std::setprecision(40) << value; + return os.str(); +} + +TEST(NumberTest, zero) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + for (Number const& z : {Number{0, 0}, Number{0}}) + { + EXPECT_EQ(z.mantissa(), 0); + EXPECT_EQ(z.exponent(), Number{}.exponent()); + + EXPECT_EQ((z + z), z); + EXPECT_EQ((z - z), z); + EXPECT_EQ(z, -z); + } + } +} + +TEST(NumberTest, limits) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto const scale = Number::getMantissaScale(); + bool caught = false; + auto const minMantissa = Number::minMantissa(); + try + { + [[maybe_unused]] Number const x = + Number{false, minMantissa * 10, 32768, Number::Normalized{}}; + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + + auto test = [](auto const& x, auto const& y, int line) { + auto const result = x == y; + std::stringstream ss; + ss << x << " == " << y << " -> " << (result ? "true" : "false"); + EXPECT_TRUE(result) << ss.str() << " (" << __FILE__ << ":" << line << ")"; + }; + + test( + Number{false, minMantissa * 10, 32767, Number::Normalized{}}, + Number{false, minMantissa, 32768, Number::Normalized{}}, + __LINE__); + test(Number{false, minMantissa, -32769, Number::Normalized{}}, Number{}, __LINE__); + test( + Number{false, minMantissa, 32000, Number::Normalized{}} * 1'000 + + Number{false, 1'500, 32000, Number::Normalized{}}, + Number{false, minMantissa + 2, 32003, Number::Normalized{}}, + __LINE__); + // 9,223,372,036,854,775,808 + + test( + Number{std::numeric_limits::min()}, + scale == MantissaRange::MantissaScale::Small + ? Number{-9'223'372'036'854'776, 3} + : Number{true, 9'223'372'036'854'775'808ULL, 0, Number::Normalized{}}, + __LINE__); + test( + Number{std::numeric_limits::min() + 1}, + scale == MantissaRange::MantissaScale::Small ? Number{-9'223'372'036'854'776, 3} + : Number{-9'223'372'036'854'775'807}, + __LINE__); + test( + Number{std::numeric_limits::max()}, + Number{ + scale == MantissaRange::MantissaScale::Small + ? 9'223'372'036'854'776 + : std::numeric_limits::max(), + 18 - Number::mantissaLog()}, + __LINE__); + caught = false; + try + { + [[maybe_unused]] + Number const q = Number{false, minMantissa, 32767, Number::Normalized{}} * 100; + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + + if (scale == MantissaRange::MantissaScale::Large330) + { + // Normalization with the other scales, including the older large mantissa scales, will + // overflow. + Number const bigNum{Number::kMaxRepUp, Number::kMaxExponent, Number::Normalized{}}; + // The display of large exponents won't go above kMaxExponent + EXPECT_EQ(to_string(bigNum), "9223372036854775810e32768") << bigNum; + // Perhaps surprisingly, this is ok, because the exponent range is related to when the + // number is _normalized_, and for mantissas > kMaxRep, the accessors return values that + // are not normalized. + EXPECT_EQ(bigNum.mantissa(), 922337203685477581ULL) << bigNum.mantissa(); + EXPECT_EQ(bigNum.exponent(), 32769) << bigNum.exponent(); + } + else + { + try + { + Number{Number::kMaxRepUp, Number::kMaxExponent, Number::Normalized{}}; + ADD_FAILURE(); + } + catch (std::overflow_error const& e) + { + std::string const expected = + (scale == MantissaRange::MantissaScale::Small ? "Number::normalize 1" + : "Number::normalize 1.5"); + EXPECT_EQ(e.what(), expected) << e.what(); + } + } + } +} + +TEST(NumberTest, add) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto const scale = Number::getMantissaScale(); + + EXPECT_EQ(Number::getround(), Number::RoundingMode::ToNearest) + << to_string(Number::getround()); + + using Case = std::tuple; + // TODO: Move these to the blocks where they're used + auto const cSmall = std::to_array({ + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'066, -15}, + __LINE__}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'066, -15}, + __LINE__}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{-9'999'999'999'999'344, -16}, + __LINE__}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'344, -16}, + __LINE__}, + {Number{}, Number{5}, Number{5}, __LINE__}, + {Number{5}, Number{}, Number{5}, __LINE__}, + {Number{5'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'554, -32768}, + Number{0}, + __LINE__}, + {Number{-9'999'999'999'999'999, -31}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'990, -16}, + __LINE__}, + }); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + { + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'065'556, -18}, + __LINE__}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'065'556, -18}, + __LINE__}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{}, Number{5}, Number{5}, __LINE__}, + {Number{5}, Number{}, Number{5}, __LINE__}, + {Number{5'555'555'555'555'555'000, -32768}, + Number{-5'555'555'555'555'554'000, -32768}, + Number{0}, + __LINE__}, + {Number{-9'999'999'999'999'999, -31}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'990, -16}, + __LINE__}, + // Items from cSmall expanded for the larger mantissa + {Number{1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -35}, + Number{1'000'000'000'000'000'066, -18}, + __LINE__}, + {Number{-1'000'000'000'000'000'000, -18}, + Number{-6'555'555'555'555'555'555, -35}, + Number{-1'000'000'000'000'000'066, -18}, + __LINE__}, + {Number{-1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -35}, + Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{-6'555'555'555'555'555'555, -35}, + Number{1'000'000'000'000'000'000, -18}, + Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{}, Number{5}, Number{5}, __LINE__}, + {Number{5'555'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'555'554, -32768}, + Number{0}, + __LINE__}, + {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{}}, + __LINE__}, + {Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}, __LINE__}, + // Test extremes + { + // Each Number operand rounds up, so the actual mantissa is + // minMantissa + Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}}, + Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}}, + Number{2, 19}, + __LINE__, + }, + { + // 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{}}, + __LINE__, + }, + }); + auto const cLargeLegacy = std::to_array({ + {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__}, + }); + auto const cLarge320 = std::to_array({ + {Number{Number::kMaxRep}, + Number{6, -1}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + }); + auto const cLargeCorrected = std::to_array({ + {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep}, __LINE__}, + }); + auto test = [](auto const& c) { + for (auto const& [x, y, z, line] : c) + { + auto const result = x + y; + std::stringstream ss; + ss << x << " + " << y << " = " << result << ". Expected: " << z; + EXPECT_EQ(result, z) << ss.str() << " Line: " << line; + } + }; + if (scale == MantissaRange::MantissaScale::Small) + { + test(cSmall); + } + else + { + test(cLarge); + if (scale == MantissaRange::MantissaScale::LargeLegacy) + { + test(cLargeLegacy); + } + else if (scale == MantissaRange::MantissaScale::Large320) + { + test(cLarge320); + } + else + { + test(cLargeCorrected); + + // This has to be created in this block, because normalization with the other + // scales, including the older large mantissa scales, will overflow. + Number const bigResult{ + Number::kMaxRepUp, Number::kMaxExponent, Number::Normalized{}}; + auto const cBigNums = std::to_array({ + { + // Add 3 to the mantissa to avoid rounding + Number::max(), + Number{3, Number::kMaxExponent}, + bigResult, + __LINE__, + }, + }); + test(cBigNums); + } + } + { + bool caught = false; + try + { + Number{false, Number::maxMantissa(), 32768, Number::Normalized{}} + + Number{false, Number::minMantissa(), 32767, Number::Normalized{}} * 5; + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + } + } +} + +TEST(NumberTest, sub) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto const scale = Number::getMantissaScale(); + + using Case = std::tuple; + auto const cSmall = std::to_array( + {{Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{9'999'999'999'999'344, -16}, + __LINE__}, + {Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{-9'999'999'999'999'344, -16}, + __LINE__}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}, + __LINE__}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'001, -15}, + Number{-1'000'000'000'000'000, -30}, + __LINE__}, + {Number{1'000'000'000'000'001, -15}, + Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -30}, + __LINE__}}); + auto const cLargeAll = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + { + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}, + __LINE__}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'001, -15}, + Number{-1'000'000'000'000'000, -30}, + __LINE__}, + {Number{1'000'000'000'000'001, -15}, + Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -30}, + __LINE__}, + // Items from cSmall expanded for the larger mantissa + {Number{1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -32}, + Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{6'555'555'555'555'555'555, -32}, + Number{1'000'000'000'000'000'000, -18}, + Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'000, -18}, + Number{0}, + __LINE__}, + {Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'001, -18}, + Number{-1'000'000'000'000'000'000, -36}, + __LINE__}, + {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}, + __LINE__}, + {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __LINE__}, + }); + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + auto const cLarge = std::to_array({ + // Anything larger than kMaxRep rounds up + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + {Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__}, + }); + auto const cLarge330 = std::to_array({ + // kMaxRep + 1 is below the half-way point, so it rounds down to kMaxRep when the Number + // is created. + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{Number::kMaxRep - 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep - 3}, + __LINE__}, + // kMaxRepUp -1 is above the half-way point, so it rounds up to kMaxRepUp when the + // Number is created. Subtracting 1 from that rounds up again. A little non-intuitive. + {Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + // Subtracting 3 gets back down to kMaxRep + {Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + // 2^63 is the same as kMaxRep+1 + {power(2, 63), Number{3, 0}, Number{Number::kMaxRep - 3}, __LINE__}, + }); + auto test = [](auto const& c) { + for (auto const& [x, y, z, line] : c) + { + auto const result = x - y; + std::stringstream ss; + ss << x << " - " << y << " = " << result << ". Expected: " << z; + EXPECT_EQ(result, z) << ss.str() << " Line: " << line; + } + }; + switch (scale) + { + case MantissaRange::MantissaScale::Small: + test(cSmall); + break; + case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large320: + test(cLargeAll); + test(cLarge); + break; + case MantissaRange::MantissaScale::Large330: + test(cLargeAll); + test(cLarge330); + break; + default: + ADD_FAILURE(); + break; + } + } +} + +TEST(NumberTest, mul) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto const scale = Number::getMantissaScale(); + + using Case = std::tuple; + auto test = [](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x * y; + std::stringstream ss; + ss << x << " * " << y << " = " << result << ". Expected: " << z; + EXPECT_EQ(result, z) << ss.str(); + } + }; + auto tests = [&](auto const& cSmall, auto const& cLarge) { + if (scale == MantissaRange::MantissaScale::Small) + { + test(cSmall); + } + else + { + test(cLarge); + } + }; + auto const maxMantissa = Number::maxMantissa(); + + SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; + { + auto const cSmall = std::to_array({ + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{1000000000000000, -14}}, + {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}, + // Maximum mantissa range + {Number{9'999'999'999'999'999, 0}, + Number{9'999'999'999'999'999, 0}, + Number{9'999'999'999'999'998, 16}}, + }); + auto const cLarge = std::to_array({ + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{false, 9'999'999'999'999'999'579ULL, -18, Number::Normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2000000000000000001, -18}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999998, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, Number{3111111111111111119, -18}, Number{10, 0}}, + // Maximum mantissa range - rounds up to 1e19 + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{1, 38}}, + // Maximum int64 range + {Number{Number::kMaxRep, 0}, + Number{Number::kMaxRep, 0}, + Number{85'070'591'730'234'615'85, 19}}, + }); + tests(cSmall, cLarge); + } + Number::setround(Number::RoundingMode::TowardsZero); + { + auto const cSmall = std::to_array( + {{Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{9999999999999999, -15}}, + {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{false, 9999999999999999579ULL, -18, Number::Normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2, 0}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999997, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds down to maxMantissa/10e1 + // 99'999'999'999'999'999'800'000'000'000'000'000'100 + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::kMaxRep, 0}, + Number{Number::kMaxRep, 0}, + Number{85'070'591'730'234'615'84, 19}}, + }); + tests(cSmall, cLarge); + } + Number::setround(Number::RoundingMode::Downward); + { + auto const cSmall = std::to_array( + {{Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{9999999999999999, -15}}, + {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{false, 9'999'999'999'999'999'579ULL, -18, Number::Normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2, 0}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999998, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds down to maxMantissa/10e1 + // 99'999'999'999'999'999'800'000'000'000'000'000'100 + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::kMaxRep, 0}, + Number{Number::kMaxRep, 0}, + Number{85'070'591'730'234'615'84, 19}}, + }); + tests(cSmall, cLarge); + } + Number::setround(Number::RoundingMode::Upward); + { + auto const cSmall = std::to_array( + {{Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{1000000000000000, -14}}, + {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, Number{0}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{999999999999999958, -17}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2000000000000000001, -18}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999997, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{2, 0}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{1000000000000000001, -17}}, + // Maximum mantissa range - rounds up to minMantissa*10 + // 1e19*1e19=1e38 + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{1, 38}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::kMaxRep, 0}, + Number{Number::kMaxRep, 0}, + Number{85'070'591'730'234'615'85, 19}}, + }); + tests(cSmall, cLarge); + } + { + bool caught = false; + try + { + Number{false, maxMantissa, 32768, Number::Normalized{}} * + Number{false, Number::minMantissa() * 5, 32767, Number::Normalized{}}; + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + } + } +} + +TEST(NumberTest, div) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto const scale = Number::getMantissaScale(); + + using Case = std::tuple; + auto test = [](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x / y; + std::stringstream ss; + ss << x << " / " << y << " = " << result << ". Expected: " << z; + EXPECT_EQ(result, z) << ss.str(); + } + }; + auto const maxMantissa = Number::maxMantissa(); + auto tests = [&](auto const& cSmall, auto const& cLarge) { + if (scale == MantissaRange::MantissaScale::Small) + { + test(cSmall); + } + else + { + test(cLarge); + } + }; + SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; + { + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'667, -19}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'667, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::Normalized{}}}}); + tests(cSmall, cLarge); + } + Number::setround(Number::RoundingMode::TowardsZero); + { + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'666, -19}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'666, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::Normalized{}}}}); + tests(cSmall, cLarge); + } + Number::setround(Number::RoundingMode::Downward); + { + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'666, -19}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'667, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::Normalized{}}}}); + tests(cSmall, cLarge); + } + Number::setround(Number::RoundingMode::Upward); + { + auto const cSmall = std::to_array( + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'429, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, Number{1414213562373095, -10}, Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666'667, -19}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666'666, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'572, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, Number{1414213562373095049, -13}, Number{1}}, + {Number{false, maxMantissa, 0, Number::Normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::Normalized{}}}}); + tests(cSmall, cLarge); + } + bool caught = false; + try + { + Number{1000000000000000, -15} / Number{0}; + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + } +} + +TEST(NumberTest, root) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + using Case = std::tuple; + auto test = [](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = root(x, y); + std::stringstream ss; + ss << "root(" << x << ", " << y << ") = " << result << ". Expected: " << z; + EXPECT_EQ(result, z) << ss.str(); + } + }; + /* + auto tests = [&](auto const& cSmall, auto const& cLarge) { + test(cSmall); + if (scale != MantissaRange::mantissa_scale::small) + test(cLarge); + }; + */ + + auto const cSmall = std::to_array( + {{Number{2}, 2, Number{1414213562373095049, -18}}, + {Number{2'000'000}, 2, Number{1414213562373095049, -15}}, + {Number{2, -30}, 2, Number{1414213562373095049, -33}}, + {Number{-27}, 3, Number{-3}}, + {Number{1}, 5, Number{1}}, + {Number{-1}, 0, Number{1}}, + {Number{5, -1}, 0, Number{0}}, + {Number{0}, 5, Number{0}}, + {Number{5625, -4}, 2, Number{75, -2}}}); + auto const cLarge = std::to_array({ + {Number{false, Number::maxMantissa() - 9, -1, Number::Normalized{}}, + 2, + Number{false, 999'999'999'999'999'999, -9, Number::Normalized{}}}, + {Number{false, Number::maxMantissa() - 9, 0, Number::Normalized{}}, + 2, + Number{false, 3'162'277'660'168'379'330, -9, Number::Normalized{}}}, + {Number{Number::kMaxRep}, + 2, + Number{false, 3'037'000'499'976049692, -9, Number::Normalized{}}}, + {Number{Number::kMaxRep}, + 4, + Number{false, 55'108'98747006743627, -14, Number::Normalized{}}}, + }); + test(cSmall); + if (Number::getMantissaScale() != MantissaRange::MantissaScale::Small) + { + NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); + test(cLarge); + } + bool caught = false; + try + { + (void)root(Number{-2}, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + caught = false; + try + { + (void)root(Number{-2}, 4); + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + } +} + +TEST(NumberTest, root2) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto test = [](auto const& c) { + for (auto const& x : c) + { + auto const expected = root(x, 2); + auto const result = root2(x); + std::stringstream ss; + ss << "root2(" << x << ") = " << result << ". Expected: " << expected; + EXPECT_EQ(result, expected) << ss.str(); + } + }; + + auto const cSmall = std::to_array({ + Number{2}, + Number{2'000'000}, + Number{2, -30}, + Number{27}, + Number{1}, + Number{5, -1}, + Number{0}, + Number{5625, -4}, + Number{Number::kMaxRep}, + }); + test(cSmall); + bool caught = false; + try + { + (void)root2(Number{-2}); + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + } +} + +TEST(NumberTest, power1) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + using Case = std::tuple; + Case const c[]{ + {Number{64}, 0, Number{1}}, + {Number{64}, 1, Number{64}}, + {Number{64}, 2, Number{4096}}, + {Number{-64}, 2, Number{4096}}, + {Number{64}, 3, Number{262144}}, + {Number{-64}, 3, Number{-262144}}, + {Number{64}, 11, Number{false, 7378697629483820646ULL, 1, Number::Normalized{}}}, + {Number{-64}, 11, Number{true, 7378697629483820646ULL, 1, Number::Normalized{}}}}; + for (auto const& [x, y, z] : c) + EXPECT_EQ(power(x, y), z); + } +} + +TEST(NumberTest, power2) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + using Case = std::tuple; + Case const c[]{ + {Number{1}, 3, 7, Number{1}}, + {Number{-1}, 1, 0, Number{1}}, + {Number{-1, -1}, 1, 0, Number{0}}, + {Number{16}, 0, 5, Number{1}}, + {Number{34}, 3, 3, Number{34}}, + {Number{4}, 3, 2, Number{8}}}; + for (auto const& [x, n, d, z] : c) + EXPECT_EQ(power(x, n, d), z); + bool caught = false; + try + { + (void)power(Number{7}, 0, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + caught = false; + try + { + (void)power(Number{7}, 1, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + caught = false; + try + { + (void)power(Number{-1, -1}, 3, 2); + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + } +} + +TEST(NumberTest, conversions) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + IOUAmount const x{5, 6}; + Number const y = x; + EXPECT_EQ(y, (Number{5, 6})); + IOUAmount const z{y}; + EXPECT_EQ(x, z); + XRPAmount const xrp{500}; + STAmount const st = xrp; + Number const n = st; + EXPECT_EQ(XRPAmount{n}, xrp); + IOUAmount const x0{0, 0}; + Number const y0 = x0; + EXPECT_EQ(y0, Number{0}); + IOUAmount const z0{y0}; + EXPECT_EQ(x0, z0); + XRPAmount const xrp0{0}; + Number const n0 = xrp0; + EXPECT_EQ(n0, Number{0}); + XRPAmount const xrp1{n0}; // NOLINT misc-confusable-identifiers + EXPECT_EQ(xrp1, xrp0); + } +} + +TEST(NumberTest, to_integer) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + using Case = std::tuple; + SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; + { + Case const c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 2}, + {Number{14, -1}, 1}, + {Number{16, -1}, 2}, + {Number{25, -1}, 2}, + {Number{6, -1}, 1}, + {Number{5, -1}, 0}, + {Number{4, -1}, 0}, + {Number{-15, -1}, -2}, + {Number{-14, -1}, -1}, + {Number{-16, -1}, -2}, + {Number{-25, -1}, -2}, + {Number{-6, -1}, -1}, + {Number{-5, -1}, 0}, + {Number{-4, -1}, 0}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + EXPECT_EQ(j, y); + } + } + auto prevMode = Number::setround(Number::RoundingMode::TowardsZero); + EXPECT_EQ(prevMode, Number::RoundingMode::ToNearest); + { + Case const c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 1}, + {Number{14, -1}, 1}, + {Number{16, -1}, 1}, + {Number{25, -1}, 2}, + {Number{6, -1}, 0}, + {Number{5, -1}, 0}, + {Number{4, -1}, 0}, + {Number{-15, -1}, -1}, + {Number{-14, -1}, -1}, + {Number{-16, -1}, -1}, + {Number{-25, -1}, -2}, + {Number{-6, -1}, 0}, + {Number{-5, -1}, 0}, + {Number{-4, -1}, 0}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + EXPECT_EQ(j, y); + } + } + prevMode = Number::setround(Number::RoundingMode::Downward); + EXPECT_EQ(prevMode, Number::RoundingMode::TowardsZero); + { + Case const c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 1}, + {Number{14, -1}, 1}, + {Number{16, -1}, 1}, + {Number{25, -1}, 2}, + {Number{6, -1}, 0}, + {Number{5, -1}, 0}, + {Number{4, -1}, 0}, + {Number{-15, -1}, -2}, + {Number{-14, -1}, -2}, + {Number{-16, -1}, -2}, + {Number{-25, -1}, -3}, + {Number{-6, -1}, -1}, + {Number{-5, -1}, -1}, + {Number{-4, -1}, -1}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + EXPECT_EQ(j, y); + } + } + prevMode = Number::setround(Number::RoundingMode::Upward); + EXPECT_EQ(prevMode, Number::RoundingMode::Downward); + { + Case const c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 2}, + {Number{14, -1}, 2}, + {Number{16, -1}, 2}, + {Number{25, -1}, 3}, + {Number{6, -1}, 1}, + {Number{5, -1}, 1}, + {Number{4, -1}, 1}, + {Number{-15, -1}, -1}, + {Number{-14, -1}, -1}, + {Number{-16, -1}, -1}, + {Number{-25, -1}, -2}, + {Number{-6, -1}, 0}, + {Number{-5, -1}, 0}, + {Number{-4, -1}, 0}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + EXPECT_EQ(j, y); + } + } + bool caught = false; + try + { + (void)static_cast(Number{9223372036854776, 3}); + } + catch (std::overflow_error const&) + { + caught = true; + } + EXPECT_TRUE(caught); + } +} + +TEST(NumberTest, squelch) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + Number const limit{1, -6}; + EXPECT_EQ(squelch(Number{2, -6}, limit), (Number{2, -6})); + EXPECT_EQ(squelch(Number{1, -6}, limit), (Number{1, -6})); + EXPECT_EQ(squelch(Number{9, -7}, limit), Number{0}); + EXPECT_EQ(squelch(Number{-2, -6}, limit), (Number{-2, -6})); + EXPECT_EQ(squelch(Number{-1, -6}, limit), (Number{-1, -6})); + EXPECT_EQ(squelch(Number{-9, -7}, limit), Number{0}); + } +} + +TEST(NumberTest, to_string) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto const scale = Number::getMantissaScale(); + + auto test = [](Number const& n, std::string const& expected, int line) { + auto const result = to_string(n); + std::stringstream ss; + ss << "to_string(" << result << "). Expected: " << expected; + EXPECT_EQ(result, expected) << ss.str() << " Line: " << line; + }; + + test(Number(-2, 0), "-2", __LINE__); + test(Number(0, 0), "0", __LINE__); + test(Number(2, 0), "2", __LINE__); + test(Number(25, -3), "0.025", __LINE__); + test(Number(-25, -3), "-0.025", __LINE__); + test(Number(25, 1), "250", __LINE__); + test(Number(-25, 1), "-250", __LINE__); + test(Number(2, 20), "2e20", __LINE__); + test(Number(-2, -20), "-2e-20", __LINE__); + // Test the edges + // ((exponent < -(25)) || (exponent > -(5))))) + // or ((exponent < -(28)) || (exponent > -(8))))) + test(Number(2, -10), "0.0000000002", __LINE__); + test(Number(2, -11), "2e-11", __LINE__); + + test(Number(-2, 10), "-20000000000", __LINE__); + test(Number(-2, 11), "-2e11", __LINE__); + test(Number(-2, 11) - 1, "-200000000001", __LINE__); + + switch (scale) + { + case MantissaRange::MantissaScale::Small: + + test(Number::min(), "1e-32753", __LINE__); + test(Number::max(), "9999999999999999e32768", __LINE__); + test(Number::lowest(), "-9999999999999999e32768", __LINE__); + { + NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); + + auto const maxMantissa = Number::maxMantissa(); + EXPECT_EQ(maxMantissa, (9'999'999'999'999'999)); + test( + Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, + "9999999999999999", + __LINE__); + test( + Number{true, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, + "-9999999999999999", + __LINE__); + + test( + Number{std::numeric_limits::max(), -3}, + "9223372036854775", + __LINE__); + test( + -(Number{std::numeric_limits::max(), -3}), + "-9223372036854775", + __LINE__); + + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775e3", + __LINE__); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775e3", + __LINE__); + } + break; + default: + // Test the edges + // ((exponent < -(28)) || (exponent > -(8))))) + test(Number::min(), "1e-32750", __LINE__); + test(Number::max(), "9223372036854775807e32768", __LINE__); + test(Number::lowest(), "-9223372036854775807e32768", __LINE__); + { + NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); + + auto const maxMantissa = Number::maxMantissa(); + EXPECT_EQ((maxMantissa), (9'999'999'999'999'999'999ULL)); + test( + Number{false, maxMantissa, 0, Number::Normalized{}}, + "9999999999999999990", + __LINE__); + test( + Number{true, maxMantissa, 0, Number::Normalized{}}, + "-9999999999999999990", + __LINE__); + + test( + Number{std::numeric_limits::max(), 0}, + "9223372036854775807", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0}), + "-9223372036854775807", + __LINE__); + + switch (scale) + { + case MantissaRange::MantissaScale::Large330: + // Because the absolute value of min() is larger than max(), it + // will be rounded down toward max() + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775807", + __LINE__); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775807", + __LINE__); + break; + default: + // Because the absolute value of min() is larger than max(), it + // will be scaled down to fit under max(). Since we're + // rounding towards zero, the 8 at the end is dropped. + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775800", + __LINE__); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775800", + __LINE__); + break; + } + } + + switch (scale) + { + case MantissaRange::MantissaScale::Large330: + // Rounding to nearest, since the mantissa is below the halfway point from + // kMaxRep to kMaxRepUp, it will be rounded down to kMaxRep + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775807", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775807", + __LINE__); + break; + default: + // Rounding to nearest, since the mantissa is bigger than kMaxRep, the 8 + // will be dropped, and since that is bigger than 5, the result will be + // rounded up from 0 to 1. + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775810", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775810", + __LINE__); + break; + } + // Rounding to nearest, will be rounded up to kMaxRepUp, but for different reasons + // depending on the scale. If older than "Large", it rounds up for the same reason + // "+1" rounds up. For "Large", since the mantissa is above the halfway point from + // kMaxRep to kMaxRepUp, it will be rounded up to kMaxRepUp. + test( + Number{std::numeric_limits::max(), 0} + 2, + "9223372036854775810", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0} + 2), + "-9223372036854775810", + __LINE__); + break; + } + } +} + +TEST(NumberTest, relationals) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + { + auto test = [](auto const& nums) { + EXPECT_TRUE(std::ranges::is_sorted(nums)); + + for (auto iter1 = nums.begin(); iter1 != nums.end(); ++iter1) + { + auto iter2 = iter1; + for (++iter2; iter2 != nums.end(); ++iter2) + { + Number const& smaller = *iter1; + Number const& larger = *iter2; + std::stringstream ss; + ss << smaller << " < " << larger; + auto const str = ss.str(); + + // The ==/!= operators use a completely different code path than <, etc. + // This helps detect a breakage in one but not the other. It also helps + // verify that the values are being ordered correctly. + EXPECT_TRUE(smaller != larger) << str << " (!=)"; + EXPECT_FALSE(smaller == larger) << str << " (==)"; + + // true results using operator< and derived operators + EXPECT_TRUE(smaller < larger) << str << " (<)"; + EXPECT_TRUE(larger > smaller) << str << " (>)"; + EXPECT_TRUE(larger >= smaller) << str << " (>=)"; + EXPECT_TRUE(smaller <= larger) << str << " (<=)"; + + // false results using operator< and derived operators + EXPECT_FALSE(larger < smaller) << str << " (! <)"; + EXPECT_FALSE(smaller > larger) << str << " (! >)"; + EXPECT_FALSE(smaller >= larger) << str << " (! >=)"; + EXPECT_FALSE(larger <= smaller) << str << " (! <=)"; + } + } + }; + + auto const intNums = []() { + // Inequality test cases are built from a list of sorted integers + auto const values = + std::to_array({-100, -50, -20, -10, -1, 0, 1, 10, 20, 50, 100}); + // Check this list is sorted before converting it to Numbers. + // That way if any of the other tests fail, we know it's because of code and not the + // source data. + EXPECT_TRUE(std::ranges::is_sorted(values)); + + std::vector result; + result.reserve(values.size()); + for (auto const v : values) + result.emplace_back(v); + return result; + }(); + + auto const otherNums = std::to_array({ + Number{-5, 100}, + Number{-1, 100}, + Number{-7, -10}, + Number{-2, -10}, + Number{0}, + Number{2, -10}, + Number{7, -10}, + Number{1, 100}, + Number{5, 100}, + }); + + test(intNums); + test(otherNums); + } + + { + // Equality test cases are . Number will be compared against itself + using Case = std::pair; + auto const c = std::to_array({ + {700, __LINE__}, + {50, __LINE__}, + {1, __LINE__}, + {0, __LINE__}, + {-1, __LINE__}, + {-30, __LINE__}, + {-600, __LINE__}, + }); + for (auto const& [n, line] : c) + { + auto const str = to_string(n); + auto const location = + std::string{" ("} + __FILE__ + ":" + std::to_string(line) + ")"; + + // NOLINTBEGIN(misc-redundant-expression) Explicitly testing operators with + // equivalent values + EXPECT_TRUE(n == n) << str << " ==" << location; + EXPECT_FALSE(n != n) << str << " !=" << location; + + EXPECT_FALSE(n < n) << str << " <" << location; + EXPECT_FALSE(n > n) << str << " >" << location; + EXPECT_TRUE(n >= n) << str << " >=" << location; + EXPECT_TRUE(n <= n) << str << " <=" << location; + // NOLINTEND(misc-redundant-expression) + } + } + } +} + +TEST(NumberTest, stream) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + Number const x{100}; + std::ostringstream os; + os << x; + EXPECT_EQ((os.str()), (to_string(x))); + } +} + +TEST(NumberTest, inc_dec) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + Number x{100}; + Number const y = +x; + EXPECT_EQ((x), (y)); + EXPECT_EQ((x++), (y)); + EXPECT_EQ((x), (Number{101})); + EXPECT_EQ((x--), (Number{101})); + EXPECT_EQ((x), (y)); + } +} + +TEST(NumberTest, to_st_amount) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + Issue const issue; + Number const n{7'518'783'80596, -5}; + SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; + auto res2 = STAmount{issue, n}; + EXPECT_EQ((res2), (STAmount{7518784})); + + Number::setround(Number::RoundingMode::TowardsZero); + res2 = STAmount{issue, n}; + EXPECT_EQ((res2), (STAmount{7518783})); + + Number::setround(Number::RoundingMode::Downward); + res2 = STAmount{issue, n}; + EXPECT_EQ((res2), (STAmount{7518783})); + + Number::setround(Number::RoundingMode::Upward); + res2 = STAmount{issue, n}; + EXPECT_EQ((res2), (STAmount{7518784})); + } +} + +TEST(NumberTest, truncate) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + EXPECT_EQ((Number(25, +1).truncate()), (Number(250, 0))); + EXPECT_EQ((Number(25, 0).truncate()), (Number(25, 0))); + EXPECT_EQ((Number(25, -1).truncate()), (Number(2, 0))); + EXPECT_EQ((Number(25, -2).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(99, -2).truncate()), (Number(0, 0))); + + EXPECT_EQ((Number(-25, +1).truncate()), (Number(-250, 0))); + EXPECT_EQ((Number(-25, 0).truncate()), (Number(-25, 0))); + EXPECT_EQ((Number(-25, -1).truncate()), (Number(-2, 0))); + EXPECT_EQ((Number(-25, -2).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(-99, -2).truncate()), (Number(0, 0))); + + EXPECT_EQ((Number(0, 0).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(0, 30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(0, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(100, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(100, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(-100, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(-100, -30000).truncate()), (Number(0, 0))); + } +} + +TEST(NumberTest, rounding) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + // Test that rounding works as expected. + + using NumberRoundings = std::map; + + std::map const expected{ + // Positive numbers + {Number{13, -1}, + {{Number::RoundingMode::ToNearest, 1}, + {Number::RoundingMode::TowardsZero, 1}, + {Number::RoundingMode::Downward, 1}, + {Number::RoundingMode::Upward, 2}}}, + {Number{23, -1}, + {{Number::RoundingMode::ToNearest, 2}, + {Number::RoundingMode::TowardsZero, 2}, + {Number::RoundingMode::Downward, 2}, + {Number::RoundingMode::Upward, 3}}}, + {Number{15, -1}, + {{Number::RoundingMode::ToNearest, 2}, + {Number::RoundingMode::TowardsZero, 1}, + {Number::RoundingMode::Downward, 1}, + {Number::RoundingMode::Upward, 2}}}, + {Number{25, -1}, + {{Number::RoundingMode::ToNearest, 2}, + {Number::RoundingMode::TowardsZero, 2}, + {Number::RoundingMode::Downward, 2}, + {Number::RoundingMode::Upward, 3}}}, + {Number{152, -2}, + {{Number::RoundingMode::ToNearest, 2}, + {Number::RoundingMode::TowardsZero, 1}, + {Number::RoundingMode::Downward, 1}, + {Number::RoundingMode::Upward, 2}}}, + {Number{252, -2}, + {{Number::RoundingMode::ToNearest, 3}, + {Number::RoundingMode::TowardsZero, 2}, + {Number::RoundingMode::Downward, 2}, + {Number::RoundingMode::Upward, 3}}}, + {Number{17, -1}, + {{Number::RoundingMode::ToNearest, 2}, + {Number::RoundingMode::TowardsZero, 1}, + {Number::RoundingMode::Downward, 1}, + {Number::RoundingMode::Upward, 2}}}, + {Number{27, -1}, + {{Number::RoundingMode::ToNearest, 3}, + {Number::RoundingMode::TowardsZero, 2}, + {Number::RoundingMode::Downward, 2}, + {Number::RoundingMode::Upward, 3}}}, + + // Negative numbers + {Number{-13, -1}, + {{Number::RoundingMode::ToNearest, -1}, + {Number::RoundingMode::TowardsZero, -1}, + {Number::RoundingMode::Downward, -2}, + {Number::RoundingMode::Upward, -1}}}, + {Number{-23, -1}, + {{Number::RoundingMode::ToNearest, -2}, + {Number::RoundingMode::TowardsZero, -2}, + {Number::RoundingMode::Downward, -3}, + {Number::RoundingMode::Upward, -2}}}, + {Number{-15, -1}, + {{Number::RoundingMode::ToNearest, -2}, + {Number::RoundingMode::TowardsZero, -1}, + {Number::RoundingMode::Downward, -2}, + {Number::RoundingMode::Upward, -1}}}, + {Number{-25, -1}, + {{Number::RoundingMode::ToNearest, -2}, + {Number::RoundingMode::TowardsZero, -2}, + {Number::RoundingMode::Downward, -3}, + {Number::RoundingMode::Upward, -2}}}, + {Number{-152, -2}, + {{Number::RoundingMode::ToNearest, -2}, + {Number::RoundingMode::TowardsZero, -1}, + {Number::RoundingMode::Downward, -2}, + {Number::RoundingMode::Upward, -1}}}, + {Number{-252, -2}, + {{Number::RoundingMode::ToNearest, -3}, + {Number::RoundingMode::TowardsZero, -2}, + {Number::RoundingMode::Downward, -3}, + {Number::RoundingMode::Upward, -2}}}, + {Number{-17, -1}, + {{Number::RoundingMode::ToNearest, -2}, + {Number::RoundingMode::TowardsZero, -1}, + {Number::RoundingMode::Downward, -2}, + {Number::RoundingMode::Upward, -1}}}, + {Number{-27, -1}, + {{Number::RoundingMode::ToNearest, -3}, + {Number::RoundingMode::TowardsZero, -2}, + {Number::RoundingMode::Downward, -3}, + {Number::RoundingMode::Upward, -2}}}, + }; + + for (auto const& [num, roundings] : expected) + { + for (auto const& [mode, val] : roundings) + { + NumberRoundModeGuard const g{mode}; + auto const res = static_cast(num); + EXPECT_EQ((res), (val)) << to_string(num) + " with mode " + + std::to_string(static_cast(mode)) + " expected " + + std::to_string(val) + " got " + std::to_string(res); + } + } + } +} + +TEST(NumberTest, int64) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const sg(mantissaScale); + + auto const scale = Number::getMantissaScale(); + + // Control case + EXPECT_GT((Number::maxMantissa()), (10)); + Number const ten{10}; + EXPECT_LE((ten.exponent()), (0)); + + if (scale == MantissaRange::MantissaScale::Small) + { + EXPECT_GT((std::numeric_limits::max()), (kInitialXrp.drops())); + EXPECT_LT((Number::maxMantissa()), (kInitialXrp.drops())); + Number const initalXrp{kInitialXrp}; + EXPECT_GT((initalXrp.exponent()), (0)); + + Number const maxInt64{Number::kMaxRep}; + EXPECT_GT((maxInt64.exponent()), (0)); + // 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits + EXPECT_EQ((power(maxInt64, 2)), (Number{85'070'591'730'234'62, 22})); + + Number const max = Number{false, Number::maxMantissa(), 0, Number::Normalized{}}; + EXPECT_LE(max.exponent(), 0); + // 99'999'999'999'999'980'000'000'000'000'001 - 32 digits + EXPECT_EQ(power(max, 2), (Number{99'999'999'999'999'98, 16})); + } + else + { + EXPECT_GT((std::numeric_limits::max()), (kInitialXrp.drops())); + EXPECT_GT((Number::maxMantissa()), (kInitialXrp.drops())); + Number const initalXrp{kInitialXrp}; + EXPECT_LE((initalXrp.exponent()), (0)); + + Number const maxInt64{Number::kMaxRep}; + EXPECT_LE((maxInt64.exponent()), (0)); + // 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits + EXPECT_EQ((power(maxInt64, 2)), (Number{85'070'591'730'234'615'85, 19})); + + NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); + + auto const maxMantissa = Number::maxMantissa(); + Number const max = Number{false, maxMantissa, 0, Number::Normalized{}}; + EXPECT_EQ((max.mantissa()), (maxMantissa / 10)); + EXPECT_EQ((max.exponent()), (1)); + // 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38 + // digits + EXPECT_EQ( + (power(max, 2)), (Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}})); + } + } +} + +TEST(NumberTest, upward_rounding_produces_value_not_below_exact_at_k_max_rep_cusp) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; + + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; + constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL; + + Number const a = kAValue; + Number const b = kBValue; + Number const product = a * b; + + // Exact reference in BigInt. + BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); + + // What Number actually stored. + BigInt const storedValue = toBigInt(product); + + BigInt const signedDifference = storedValue - exactProduct; + + auto const message = [&] { + std::ostringstream os; + os << " a = " << fmt(BigInt(kAValue)) << "\n" + << " b = " << fmt(BigInt(kBValue)) << "\n" + << " exact a*b = " << fmt(exactProduct) << "\n" + << " stored = " << fmt(storedValue) << "\n" + << " stored - exact = " << fmt(signedDifference) << "\n" + << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n" + << " stored.mantissa = " << product.mantissa() << "\n" + << " stored.exponent = " << product.exponent() << "\n\n"; + return os.str(); + }; + + switch (scale) + { + case MantissaRange::MantissaScale::Large320: + case MantissaRange::MantissaScale::Large330: + EXPECT_TRUE(signedDifference >= 0) << message(); + EXPECT_TRUE(signedDifference < pow10(product.exponent())) << message(); + EXPECT_EQ(product.mantissa(), (std::numeric_limits::max() / 10) + 1); + EXPECT_EQ(product.exponent(), 19); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(signedDifference < 0) << message(); + EXPECT_EQ( + product.mantissa(), (std::numeric_limits::max() / 100) * 100); + EXPECT_EQ(product.exponent(), 18); + break; + + case MantissaRange::MantissaScale::Small: + // The seemingly weird rounding here is because a & b are both + // normalized, and both round up when being converted to Number, + // so you're really getting + // 1_000_000_000_000_050 * 9_223_372_036_854_316. + EXPECT_TRUE(signedDifference >= 0) << message(); + EXPECT_EQ( + product.mantissa(), (std::numeric_limits::max() / 1000) + 3); + EXPECT_EQ(product.exponent(), 21); + break; + } + } +} + +/* + * Companion regression for the kMaxRep cusp behavior, but for `operator/=` on + * the cusp-fix-ENABLED `Large` scale. + * + * Before the dropped-remainder fix, `operator/=` with Upward rounding could + * return a value STRICTLY LESS than the exact quotient, violating Upward's + * directional invariant. + * + * Mechanism (fix-enabled path): + * 1. `operator/=` computes `numerator = nm * 10^17` and + * `zm = numerator / dm` (integer division, truncates remainder). + * 2. If `remainder != 0`, the correction block runs: + * zm *= 100000 + * correction = (remainder * 100000) / dm // also truncates + * zm += correction + * ze -= 5 + * The truncation in `correction` discards a sub-1/100000 residual. + * 3. `normalize`'s shift loop reduces zm to fit, but the discarded residual + * is BELOW the Guard's visibility, so the Guard sees fraction = 0. + * 4. Under Upward + positive, `round()` returns -1 (no round-up), and the + * algorithm returns the truncated zm. + */ +TEST(NumberTest, upward_division_returns_value_not_below_exact_on_large_scale) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; + + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = 2LL; + constexpr std::int64_t kBValue = 1'000'000'000'000'000'007LL; + // kBValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). + + Number const a{kAValue, 0}; + Number const b{kBValue, 0}; + Number const quotient = a / b; + + Dec const exact = Dec(kAValue) / Dec(kBValue); + Dec const stored = Dec(quotient.mantissa()) * pow10(quotient.exponent()); + Dec const diff = stored - exact; + + auto const message = [&] { + std::ostringstream os; + os << " a = " << kAValue << "\n" + << " b = " << kBValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (negative => Upward gave value BELOW truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n\n"; + return os.str(); + }; + + // Upward invariant: stored >= exact. Bug: stored < exact. + switch (scale) + { + case MantissaRange::MantissaScale::Large320: + case MantissaRange::MantissaScale::Large330: + EXPECT_TRUE(stored >= exact) << message(); + EXPECT_TRUE(diff < pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(stored < exact) << message(); + EXPECT_TRUE(diff >= -pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for dropped remainders. + EXPECT_TRUE(stored < exact) << message(); + break; + } + } +} + +// Companion test case for Upward positive operator/=: Downward negative. +TEST(NumberTest, downward_division_returns_value_not_above_exact_on_large_scale) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::Downward}; + + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = -2LL; + constexpr std::int64_t kBValue = 1'000'000'000'000'000'007LL; + // kBValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). + + Number const a{kAValue, 0}; + Number const b{kBValue, 0}; + Number const quotient = a / b; + + Dec const exact = Dec(kAValue) / Dec(kBValue); + Dec const stored = Dec(quotient.mantissa()) * pow10(quotient.exponent()); + Dec const diff = stored - exact; + + auto const message = [&] { + std::ostringstream os; + os << " a = " << kAValue << "\n" + << " b = " << kBValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (positive => Downward gave value ABOVE truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n\n"; + return os.str(); + }; + + // invariant: stored <= exact. Bug: stored > exact. + switch (scale) + { + case MantissaRange::MantissaScale::Large320: + case MantissaRange::MantissaScale::Large330: + EXPECT_TRUE(stored <= exact) << message(); + EXPECT_TRUE(diff > -pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(stored > exact) << message(); + EXPECT_TRUE(diff <= pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for dropped remainders. + EXPECT_TRUE(stored < exact) << message(); + break; + } + } +} + +/* + * Companion test case for Upward positive operator/=: ToNearest. + * + * With ToNearest, if the dropped digits are exactly "5", then the mantissa will + * be rounded to even. The numbers below result in a value where the unrounded + * mantissa ends in an even digit, and "infinite precision" would drop + * "500000000000000000145...", but doNormalize only sees "5". Without the + * rounding fix, doNormalize rounds down to the even value. With the rounding + * fix, doNormalize knows there are more digits beyond "5", and so rounds _up_ + * to the odd value. + */ +TEST(NumberTest, to_nearest_division_uses_dropped_digits_on_large_scale) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest}; + + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = 1'269'917'268'816'087'809LL; + constexpr std::int64_t kBValue = 3'458'525'013'821'685'511LL; + // kBValue is prime and in [minMantissa, kMaxRep]. + + Number const a{kAValue, 0}; + Number const b{kBValue, 0}; + Number const quotient = a / b; + + Dec const exact = Dec(kAValue) / Dec(kBValue); + Dec const stored = Dec(quotient.mantissa()) * pow10(quotient.exponent()); + Dec const diff = stored - exact; + + auto const message = [&] { + std::ostringstream os; + os << " a = " << kAValue << "\n" + << " b = " << kBValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (negative => ToNearest gave value BELOW truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n\n"; + return os.str(); + }; + + // invariant: stored >= exact. Bug: stored < exact. + switch (scale) + { + case MantissaRange::MantissaScale::Large320: + case MantissaRange::MantissaScale::Large330: + EXPECT_TRUE(stored >= exact) << message(); + EXPECT_TRUE(diff < pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(stored < exact) << message(); + EXPECT_TRUE(diff >= -pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for dropped remainders. + EXPECT_TRUE(stored < exact) << message(); + break; + } + } +} + +TEST(NumberTest, subtraction_rounding) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest}; + + auto const scale = Number::getMantissaScale(); + + auto const exp = Number::mantissaLog(); + // SubCase is + // * offset: offset from exp + // * extraB: whether to include 1e"exp" in "b" + // * aString: expected string value for "a" + // * bString: expected string value for "b" + // There aren't too many valid combinations for test cases here. If extraB is true, + // offset can really only be 2, because any larger and the mantissa can't be represented + // without loss. Offset can't be less than 2, or there's no error. + using SubCase = std::tuple; + auto const c = std::to_array({ + {2, + true, + scale == MantissaRange::MantissaScale::Small ? "100000000000000000" + : "100000000000000000000", + scale == MantissaRange::MantissaScale::Small ? "-1000000000000001" + : "-1000000000000000001"}, + {2, + false, + scale == MantissaRange::MantissaScale::Small ? "100000000000000000" + : "100000000000000000000", + "-1"}, + {30, + false, + scale == MantissaRange::MantissaScale::Small + ? "1000000000000000000000000000000000000000000000" + : "1000000000000000000000000000000000000000000000000", + "-1"}, + }); + + for (auto const& [offset, extraB, aString, bString] : c) + { + Number const a{1LL, exp + offset}; + Number const b{-((extraB ? Number{1, exp} : kNumZero) + 1)}; + + auto const bigA = toBigInt(a); + auto const bigB = toBigInt(b); + + EXPECT_EQ(bigA, BigInt{aString}); + EXPECT_EQ(bigB, BigInt{bString}); + + auto construct = [&a, &b](Number::RoundingMode r) { + NumberRoundModeGuard const roundGuard{r}; + auto const sum = a + b; + BigInt const stored = toBigInt(sum); + return std::make_pair(r, std::make_pair(stored, sum)); + }; + + BigInt const exact = bigA + bigB; + + auto const sums = [&]() { + std::map> r; + r.emplace(construct(Number::RoundingMode::TowardsZero)); + r.emplace(construct(Number::RoundingMode::Upward)); + r.emplace(construct(Number::RoundingMode::Downward)); + r.emplace(construct(Number::RoundingMode::ToNearest)); + return r; + }(); + + auto const message = [&](auto const& r, auto const& sum) { + std::ostringstream os; + os << " a = " << a << " (" << fmt(bigA) << ")\n b = " << b + << " (" << fmt(bigB) << ")\n exact a + b = " << fmt(exact) << "\n"; + + auto const diff = sum.first - exact; + auto const rLabel = to_string(r); + os << std::string(15 - rLabel.length(), ' ') << rLabel << " = " << fmt(sum.first) + << "\n difference = " << fmt(diff) << "\n\n"; + + return os.str(); + }; + + auto const expectedExponent = + offset - (scale == MantissaRange::MantissaScale::Small && extraB ? 1 : 0); + auto const epsilon = pow10(expectedExponent); + for (auto const& [r, sum] : sums) + { + auto diff = sum.first - exact; + switch (scale) + { + case MantissaRange::MantissaScale::Small: + case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large320: { + // Without the fix, all the results but one round up + if (r == Number::RoundingMode::Downward) + { + // Downward works because the Guard sign is negative, and Downward + // returns Up instead of Down if negative and there's a remainder, + // whereas TowardsZero always returns Down. + EXPECT_LT(sum.first, exact) << message(r, sum); + EXPECT_EQ(diff, -(epsilon - 1)) << message(r, sum); + } + else + { + EXPECT_GT(sum.first, exact) << message(r, sum); + EXPECT_EQ(diff, 1) << message(r, sum); + } + break; + } + default: { + EXPECT_LE(sum.second.exponent(), expectedExponent) << message(r, sum); + switch (r) + { + case Number::RoundingMode::Upward: + case Number::RoundingMode::ToNearest: + EXPECT_GT(sum.first, exact) << message(r, sum); + EXPECT_EQ(diff, 1) << message(r, sum); + break; + default: + EXPECT_LT(sum.first, exact) << message(r, sum); + EXPECT_EQ(diff, -(epsilon - 1)) << message(r, sum); + } + } + } + } + } + } +} + +TEST(NumberTest, normalization_cusp_tonearest_and_downward) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest}; + + auto const scale = Number::getMantissaScale(); + + constexpr auto kMaxRep = Number::kMaxRep; + + // Both ToNearest and Downward should round to `below` + auto constexpr actual = static_cast(kMaxRep) + 1; + Number const below{static_cast(kMaxRep), 0}; + Number const above{false, static_cast(kMaxRep) + 3, 0, Number::Normalized{}}; + + auto construct = [](Number::RoundingMode mode) { + NumberRoundModeGuard const roundGuard{mode}; + return Number(false, actual, 0, Number::Normalized{}); + }; + Number const upward = construct(Number::RoundingMode::Upward); + + Number const toNearest = construct(Number::RoundingMode::ToNearest); + + Number const downward = construct(Number::RoundingMode::Downward); + + auto message = [&] { + std::ostringstream log; + log << " actual = " << actual << " (kMaxRep + 1)\n" + << " below = " << below << " (kMaxRep, distance 1)\n" + << " above = " << above << " (kMaxRep + 3, distance 2)\n" + << " Upward = " << upward << "\n" + << " ToNearest = " << toNearest << "\n" + << " Downward = " << downward << "\n\n"; + return log.str(); + }; + + switch (scale) + { + case MantissaRange::MantissaScale::Small: + // With the small mantissa, everything but Downward rounds UP, including the + // reference values, "above" and "below" + + EXPECT_EQ(below, above) << message(); + EXPECT_EQ(upward, above) << message(); + EXPECT_EQ(toNearest, above) << message(); + + EXPECT_LT(downward, below) << message(); + + break; + + case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large320: + // Upward round UP + EXPECT_EQ(upward, above) << message(); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + EXPECT_EQ(toNearest, above) << message(); + EXPECT_GT(toNearest, below) << message(); + + // Downward undershoots: it returns a value below `below` + EXPECT_LT(downward, below) << message(); + + // Both should have given the same answer, but they differ + EXPECT_GT(toNearest, downward) << message(); + + break; + default: + // Covers "Large" and any newly added scales + + // Upward round UP + EXPECT_EQ(upward, above) << message(); + + // ToNearest rounds to the strictly closer DOWN neighbor + EXPECT_NE(toNearest, above) << message(); + EXPECT_EQ(toNearest, below) << message(); + + // Downward also rounds to `below` + EXPECT_EQ(downward, below) << message(); + + // ToNearest rounds to downward + EXPECT_EQ(toNearest, downward) << message(); + break; + } + } +} + +TEST(NumberTest, number_add_directed_sign_wrong) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest}; + + auto const scale = Number::getMantissaScale(); + { + // Two negative numbers with the same exponent + Number const a{-6, Number::mantissaLog()}; + Number const b{a - 3}; + EXPECT_TRUE(a.exponent() == b.exponent() && abs(b) > abs(a)); + + BigInt const exact = toBigInt(a) + toBigInt(b); + if (scale == MantissaRange::MantissaScale::Small) + { + EXPECT_EQ(exact, BigInt{"-12000000000000003"}); + } + else + { + EXPECT_EQ(exact, BigInt{"-12000000000000000003"}); + } + + Number down, up; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + down = a + b; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + up = a + b; + } + + auto const valueDown = toBigInt(down); + auto const valueUp = toBigInt(up); + auto message = [&] { + std::ostringstream log; + log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n"; + return log.str(); + }; + + if (scale == MantissaRange::MantissaScale::Large330) + { + EXPECT_LE(valueDown, exact) << message(); // Downward should round away from zero + EXPECT_GE(valueUp, exact) << message(); // Upward should round toward 0 + } + else + { + EXPECT_GT(valueDown, exact) + << message(); // Downward rounded toward zero (too high) + EXPECT_LT(valueUp, exact) << message(); // Upward rounded toward -inf (too low) + } + } + + { + // Positive control: the same magnitudes with a positive result round + Number const pa{6, Number::mantissaLog()}; + Number const pb{pa + 3}; + EXPECT_TRUE(pa.exponent() == pb.exponent() && abs(pb) > abs(pa)); + BigInt const pexact = toBigInt(pa) + toBigInt(pb); // 12'000'000'000'000'000'003 + + Number pdown, pup; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + pdown = pa + pb; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + pup = pa + pb; + } + auto const valuePDown = toBigInt(pdown); + auto const valuePUp = toBigInt(pup); + auto message = [&] { + std::ostringstream log; + log << " exact = " << fmt(pexact) << "\n downward = " << fmt(valuePDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valuePUp) + << " (correct rounding: >= exact)\n\n"; + return log.str(); + }; + + EXPECT_LE(valuePDown, pexact) << message(); // correct for positive results + EXPECT_GE(valuePUp, pexact) << message(); + } + + { + // Mixed sign numbers with the same exponent: negative second value + Number const a{1, Number::mantissaLog()}; + Number const b{Number{-9, Number::mantissaLog()} - 3}; + EXPECT_TRUE(a.exponent() == b.exponent() && abs(b) > abs(a)); + + BigInt const exact = toBigInt(a) + toBigInt(b); + if (scale == MantissaRange::MantissaScale::Small) + { + EXPECT_EQ(exact, BigInt{"-8000000000000003"}); + } + else + { + EXPECT_EQ(exact, BigInt{"-8000000000000000003"}); + } + + Number down, up; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + down = a + b; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + up = a + b; + } + + auto const valueDown = toBigInt(down); + auto const valueUp = toBigInt(up); + auto message = [&] { + std::ostringstream log; + log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n"; + return log.str(); + }; + + EXPECT_LE(valueDown, exact) << message(); // Downward should round away from zero + EXPECT_GE(valueUp, exact) << message(); // Upward should round toward 0 + } + + { + // Mixed sign numbers with the same exponent: negative first value + Number const a{-1, Number::mantissaLog()}; + Number const b{Number{9, Number::mantissaLog()} + 3}; + EXPECT_TRUE(a.exponent() == b.exponent() && abs(b) > abs(a)); + + BigInt const exact = toBigInt(a) + toBigInt(b); + if (scale == MantissaRange::MantissaScale::Small) + { + EXPECT_EQ(exact, BigInt{"8000000000000003"}); + } + else + { + EXPECT_EQ(exact, BigInt{"8000000000000000003"}); + } + + Number down, up; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + down = a + b; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + up = a + b; + } + + auto const valueDown = toBigInt(down); + auto const valueUp = toBigInt(up); + auto message = [&] { + std::ostringstream log; + log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n"; + return log.str(); + }; + + EXPECT_LE(valueDown, exact) << message(); // Downward should round away from zero + EXPECT_GE(valueUp, exact) << message(); // Upward should round toward 0 + } + } +} + +TEST(NumberTest, number_add_to_nearest_picks_farther) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest}; + + auto const scale = Number::getMantissaScale(); + + // Case is + using Case = std::pair; + + auto const c = std::to_array({ + {Number{5'175'909'259'972'499'745LL, 22}, -1'074'951'375'311'646'003}, + {Number{1}, -1'074'956'551'220'905'975}, + {Number{1, 10}, -1'074'956'551'220'905'975}, + {Number{1, 20}, -1'074'956'551'220'905'975}, + {Number{1, 27}, -1'074'956'551'220'905'975}, + {Number{1, 28}, -1'074'956'551'220'905'974}, + {Number{1, 31}, -1'074'956'551'220'904'975}, + }); + + for (auto const& [y, expectedQ] : c) + { + Number const x{-1'074'956'551'220'905'975LL, 28}; + Number const res = x + y; + + BigInt const exact = toBigInt(x) + toBigInt(y); + BigInt const vres = toBigInt(res); + + BigInt ulp = 1; + for (int i = 0; i < res.exponent(); ++i) + ulp *= 10; + + BigInt const q = (exact - ulp / 2) / ulp; + Number const normalizedExact{static_cast(q), res.exponent()}; + BigInt const norm = toBigInt(normalizedExact); + + auto message = [&](auto const& comp) { + std::ostringstream log; + log << fmt(q) + " != " + fmt(comp) << "\n" + << " x = " << x << "\n y = " << y + << "\n exact = " << fmt(exact) + << "\n result (x + y) = " << fmt(vres) + << "\n normalize(exact) = " << fmt(norm) << "\n\n"; + return log.str(); + }; + + if (scale == MantissaRange::MantissaScale::Small) + { + auto const comp = toBigInt(Number{expectedQ, -3}); + EXPECT_EQ(q, comp) << message(comp); + } + else + { + EXPECT_EQ(q, expectedQ) << message(BigInt(expectedQ)); + } + EXPECT_EQ(normalizedExact, res); + } + } +} + +TEST(NumberTest, number_cusp_rounding_with_fractional_parts) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + + auto const scale = Number::getMantissaScale(); + + Number const below{static_cast(Number::kMaxRep), 0}; + Number const above{false, Number::kMaxRepUp, 0, Number::Normalized{}}; + + auto header = [&] { + std::ostringstream log; + log << "Scale: " << to_string(mantissaScale) << ", Below: " << below + << ", Above: " << above << "\n"; + return log.str(); + }; + + auto const zeroPointFour = Number(4, -1); + auto const zeroPointFive = Number(5, -1); + auto const zeroPointSix = Number(6, -1); + auto const onePointFour = Number(14, -1); + auto const onePointFive = Number(15, -1); + auto const onePointSix = Number(16, -1); + auto const twoPointFour = Number(24, -1); + auto const twoPointFive = Number(25, -1); + auto const twoPointSix = Number(26, -1); + + auto const operands = std::to_array({ + zeroPointFour, + zeroPointFive, + zeroPointSix, + onePointFour, + onePointFive, + onePointSix, + twoPointFour, + twoPointFive, + twoPointSix, + }); + + auto const modes = std::to_array({ + Number::RoundingMode::ToNearest, + Number::RoundingMode::TowardsZero, + Number::RoundingMode::Downward, + Number::RoundingMode::Upward, + }); + + // Addition cases test kMaxRep + Operand + for (auto const& mode : modes) + { + for (auto const& operand : operands) + { + NumberRoundModeGuard const rg{mode}; + + auto const expectedValue = [&]() { + // Returns "above" by default. The checks here are for exceptions. + if (scale >= MantissaRange::MantissaScale::Large330) + { + if (mode == Number::RoundingMode::ToNearest && operand < onePointFive) + return below; + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + return below; + } + if (scale == MantissaRange::MantissaScale::Large320) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand < zeroPointFive) + return below; + } + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + { + if (operand >= onePointFour) + return below - 7; + return below; + } + } + if (scale == MantissaRange::MantissaScale::LargeLegacy) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand < zeroPointFive) + return below; + if (operand <= zeroPointSix) + return below - 7; + } + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + { + if (operand >= onePointFour) + return below - 7; + return below; + } + if (mode == Number::RoundingMode::Upward && operand <= zeroPointSix) + return below - 7; + } + if (scale == MantissaRange::MantissaScale::Small && + mode == Number::RoundingMode::Upward) + return above + 1000; + return above; + }(); + + Number const actual = below + operand; + + auto message = [&] { + std::stringstream ss; + ss << header() << "kMaxRep + " << operand << " rounded " << to_string(mode) + << " to " << actual << ". Expected: " << expectedValue; + return ss.str(); + }; + EXPECT_EQ(actual, expectedValue) << message(); + } + } + + // Subtraction cases test kMaxRepUp - Operand + for (auto const& mode : modes) + { + for (auto const& operand : operands) + { + NumberRoundModeGuard const rg{mode}; + + auto const expectedValue = [&]() { + if (scale >= MantissaRange::MantissaScale::Large330) + { + if (mode == Number::RoundingMode::ToNearest && operand > onePointFive) + return below; + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + return below; + } + if (scale == MantissaRange::MantissaScale::LargeLegacy || + scale == MantissaRange::MantissaScale::Large320) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand >= twoPointSix) + return below; + } + if (mode == Number::RoundingMode::TowardsZero) + { + if (operand >= onePointFour) + return below - 7; + } + if (mode == Number::RoundingMode::Downward) + { + if (operand <= onePointSix) + return below - 7; + return below; + } + } + if (scale == MantissaRange::MantissaScale::Small) + { + if (mode == Number::RoundingMode::Downward) + return below - 1000; + if (mode == Number::RoundingMode::Upward) + return below; + } + return above; + }(); + + Number const actual = above - operand; + + auto message = [&] { + std::stringstream ss; + ss << header() << "kMaxRepUp - " << operand << " rounded " << to_string(mode) + << " to " << actual << ". Expected: " << expectedValue; + return ss.str(); + }; + EXPECT_EQ(actual, expectedValue) << message(); + } + } + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/StringUtilities.cpp b/src/tests/libxrpl/basics/StringUtilities.cpp new file mode 100644 index 0000000000..a10711abdb --- /dev/null +++ b/src/tests/libxrpl/basics/StringUtilities.cpp @@ -0,0 +1,293 @@ +#include + +#include +#include + +#include + +#include + +namespace xrpl { + +class StringUtilitiesTest : public ::testing::Test +{ +public: + static void + testUnHexSuccess(std::string const& strIn, std::string const& strExpected) + { + auto rv = strUnHex(strIn); + EXPECT_TRUE(rv); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_EQ(makeSlice(*rv), makeSlice(strExpected)); + } + + static void + testUnHexFailure(std::string const& strIn) + { + auto rv = strUnHex(strIn); + EXPECT_FALSE(rv); + } +}; + +TEST_F(StringUtilitiesTest, un_hex) +{ + testUnHexSuccess("526970706c6544", "RippleD"); + testUnHexSuccess("A", "\n"); + testUnHexSuccess("0A", "\n"); + testUnHexSuccess("D0A", "\r\n"); + testUnHexSuccess("0D0A", "\r\n"); + testUnHexSuccess("200D0A", " \r\n"); + testUnHexSuccess("282A2B2C2D2E2F29", "(*+,-./)"); + + // Check for things which contain some or only invalid characters + testUnHexFailure("123X"); + testUnHexFailure("V"); + testUnHexFailure("XRP"); +} + +TEST_F(StringUtilitiesTest, parse_url) +{ + // Expected passes. + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_TRUE(pUrl.domain.empty()); + EXPECT_FALSE(pUrl.port); + // RFC 3986: + // > In general, a URI that uses the generic syntax for authority + // with an empty path should be normalized to a path of "/". + // Do we want to normalize paths? + EXPECT_TRUE(pUrl.path.empty()); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme:///")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_TRUE(pUrl.domain.empty()); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "lower://domain")); + EXPECT_EQ(pUrl.scheme, "lower"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_TRUE(pUrl.path.empty()); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "UPPER://domain:234/")); + EXPECT_EQ(pUrl.scheme, "upper"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 234); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "Mixed://domain/path")); + EXPECT_EQ(pUrl.scheme, "mixed"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://[::1]:123/path")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "::1"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/path"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme:///path/to/file")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_TRUE(pUrl.domain.empty()); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path/to/file"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path/with/an@sign"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/path/with/an@sign")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path/with/an@sign"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://:999/")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, ":999"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "http://::1:1234/validators")); + EXPECT_EQ(pUrl.scheme, "http"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "::0.1.18.52"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/validators"); + } + + // Expected fails. + { + ParsedUrl pUrl; + EXPECT_FALSE(parseUrl(pUrl, "")); + EXPECT_FALSE(parseUrl(pUrl, "nonsense")); + EXPECT_FALSE(parseUrl(pUrl, "://")); + EXPECT_FALSE(parseUrl(pUrl, ":///")); + EXPECT_FALSE(parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:23498765/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:0/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:+7/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:-7234/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:@#$56!/")); + } + + { + std::string const strUrl("s://" + std::string(8192, ':')); + ParsedUrl pUrl; + EXPECT_FALSE(parseUrl(pUrl, strUrl)); + } +} + +TEST_F(StringUtilitiesTest, to_string) +{ + auto result = to_string("hello"); + EXPECT_EQ(result, "hello"); +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/TaggedCache.cpp b/src/tests/libxrpl/basics/TaggedCache.cpp new file mode 100644 index 0000000000..c8ccc415ad --- /dev/null +++ b/src/tests/libxrpl/basics/TaggedCache.cpp @@ -0,0 +1,246 @@ +#include + +#include +#include +#include // IWYU pragma: keep +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace xrpl { + +/* +I guess you can put some items in, make sure they're still there. Let some +time pass, make sure they're gone. Keep a strong pointer to one of them, make +sure you can still find it even after time passes. Create two objects with +the same key, canonicalize them both and make sure you get the same object. +Put an object in but keep a strong pointer to it, advance the clock a lot, +then canonicalize a new object with the same key, make sure you get the +original object. +*/ + +TEST(TaggedCacheTest, tagged_cache) +{ + using namespace std::chrono_literals; + beast::Journal const journal{TestSink::instance()}; + + TestStopwatch clock; + clock.set(0); + + using Key = LedgerIndex; + using Value = std::string; + using Cache = TaggedCache; + + Cache c("test", 1, 1s, clock, journal); + + // Insert an item, retrieve it, and age it so it gets purged. + { + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + EXPECT_FALSE(c.insert(1, "one")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + + { + std::string s; + EXPECT_TRUE(c.retrieve(1, s)); + EXPECT_EQ(s, "one"); + } + + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + // Insert an item, maintain a strong pointer, age it, and + // verify that the entry still exists. + { + EXPECT_FALSE(c.insert(2, "two")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + + { + auto p = c.fetch(2); + EXPECT_NE(p, nullptr); + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 1); + } + + // Make sure its gone now that our reference is gone + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + // Insert the same key/value pair and make sure we get the same result + { + EXPECT_FALSE(c.insert(3, "three")); + + { + auto const p1 = c.fetch(3); + auto p2 = std::make_shared("three"); + c.canonicalizeReplaceClient(3, p2); + EXPECT_EQ(p1.get(), p2.get()); + } + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + // Put an object in but keep a strong pointer to it, advance the clock a + // lot, then canonicalize a new object with the same key, make sure you + // get the original object. + { + // Put an object in + EXPECT_FALSE(c.insert(4, "four")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + + { + // Keep a strong pointer to it + auto const p1 = c.fetch(4); + EXPECT_NE(p1, nullptr); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + // Advance the clock a lot + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 1); + // Canonicalize a new object with the same key + auto p2 = std::make_shared("four"); + EXPECT_TRUE(c.canonicalizeReplaceClient(4, p2)); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + // Make sure we get the original object + EXPECT_EQ(p1.get(), p2.get()); + } + + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + { + EXPECT_FALSE(c.insert(5, "five")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.size(), 1); + + { + auto const p1 = c.fetch(5); + EXPECT_NE(p1, nullptr); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.size(), 1); + + // Advance the clock a lot + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.size(), 1); + + auto p2 = std::make_shared("five_2"); + EXPECT_TRUE(c.canonicalizeReplaceCache(5, p2)); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.size(), 1); + // Make sure the caller's original pointer is unchanged + EXPECT_NE(p1.get(), p2.get()); + EXPECT_EQ(*p2, "five_2"); + + auto const p3 = c.fetch(5); + EXPECT_NE(p3, nullptr); + EXPECT_EQ(p3.get(), p2.get()); + EXPECT_NE(p3.get(), p1.get()); + } + + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.size(), 0); + } + + { + struct MyRefCountObject : IntrusiveRefCounts + { + std::string data; + + // Needed to support weak intrusive pointers + virtual void + partialDestructor() + { + } + + MyRefCountObject() = default; + explicit MyRefCountObject(std::string data) : data(std::move(data)) + { + } + + bool + operator==(std::string const& other) const + { + return data == other; + } + }; + + using IntrPtrCache = TaggedCache< + Key, + MyRefCountObject, + /*IsKeyCache*/ false, + intr_ptr::SharedWeakUnionPtr, + intr_ptr::SharedPtr>; + + IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal); + + intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared("one")); + EXPECT_EQ(intrPtrCache.getCacheSize(), 1); + EXPECT_EQ(intrPtrCache.size(), 1); + + { + { + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced")); + + auto p = intrPtrCache.fetch(1); + EXPECT_EQ(*p, "one_replaced"); + + // Advance the clock a lot + ++clock; + intrPtrCache.sweep(); + EXPECT_EQ(intrPtrCache.getCacheSize(), 0); + EXPECT_EQ(intrPtrCache.size(), 1); + + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced_2")); + + auto p2 = intrPtrCache.fetch(1); + EXPECT_EQ(*p2, "one_replaced_2"); + + intrPtrCache.del(1, true); + } + + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced_3")); + auto p3 = intrPtrCache.fetch(1); + EXPECT_EQ(*p3, "one_replaced_3"); + } + + ++clock; + intrPtrCache.sweep(); + EXPECT_EQ(intrPtrCache.getCacheSize(), 0); + EXPECT_EQ(intrPtrCache.size(), 0); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/Units.cpp b/src/tests/libxrpl/basics/Units.cpp new file mode 100644 index 0000000000..1cf7024fe0 --- /dev/null +++ b/src/tests/libxrpl/basics/Units.cpp @@ -0,0 +1,328 @@ +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace xrpl::test { + +TEST(UnitsTest, types) +{ + using FeeLevel32 = FeeLevel; + + { + XRPAmount const x{100}; + EXPECT_EQ(x.drops(), 100); + EXPECT_TRUE((std::is_same_v)); + auto y = 4u * x; + EXPECT_EQ(y.value(), 400); + EXPECT_TRUE((std::is_same_v)); + + auto z = 4 * y; + EXPECT_EQ(z.value(), 1600); + EXPECT_TRUE((std::is_same_v)); + + FeeLevel32 const f{10}; + FeeLevel32 const baseFee{100}; + + auto drops = mulDiv(baseFee, x, f); + + EXPECT_TRUE(drops); + EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_TRUE( + (std::is_same_v::unit_type, unit::dropTag>)); + + EXPECT_TRUE((std::is_same_v, XRPAmount>)); + } + { + XRPAmount const x{100}; + EXPECT_EQ(x.value(), 100); + EXPECT_TRUE((std::is_same_v)); + auto y = 4u * x; + EXPECT_EQ(y.value(), 400); + EXPECT_TRUE((std::is_same_v)); + + FeeLevel64 const f{10}; + FeeLevel64 const baseFee{100}; + + auto drops = mulDiv(baseFee, x, f); + + EXPECT_TRUE(drops); + EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_TRUE( + (std::is_same_v::unit_type, unit::dropTag>)); + EXPECT_TRUE((std::is_same_v, XRPAmount>)); + } + { + FeeLevel64 const x{1024}; + EXPECT_EQ(x.value(), 1024); + EXPECT_TRUE((std::is_same_v)); + std::uint64_t const m = 4; + auto y = m * x; + EXPECT_EQ(y.value(), 4096); + EXPECT_TRUE((std::is_same_v)); + + XRPAmount const basefee{10}; + FeeLevel64 const referencefee{256}; + + auto drops = mulDiv(x, basefee, referencefee); + + EXPECT_TRUE(drops); + EXPECT_EQ(drops.value(), 40); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_TRUE( + (std::is_same_v::unit_type, unit::dropTag>)); + EXPECT_TRUE((std::is_same_v, XRPAmount>)); + } +} + +TEST(UnitsTest, json) +{ + // Json value functionality + using FeeLevel32 = FeeLevel; + + { + FeeLevel32 const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{x.fee()}); + } + + { + FeeLevel32 const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{x.fee()}); + } + + { + FeeLevel64 const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{std::numeric_limits::max()}); + } + + { + FeeLevel64 const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{0}); + } + + { + FeeLevelDouble const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Real); + EXPECT_EQ(y, json::Value{std::numeric_limits::max()}); + } + + { + FeeLevelDouble const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Real); + EXPECT_EQ(y, json::Value{std::numeric_limits::min()}); + } + + { + XRPAmount const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Int); + EXPECT_EQ(y, json::Value{std::numeric_limits::max()}); + } + + { + XRPAmount const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Int); + EXPECT_EQ(y, json::Value{std::numeric_limits::min()}); + } +} + +TEST(UnitsTest, functions) +{ + // Explicitly test every defined function for the ValueUnit class + // since some of them are templated, but not used anywhere else. + using FeeLevel32 = FeeLevel; + + { + auto make = [&](auto x) -> FeeLevel64 { return x; }; + auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; }; + + [[maybe_unused]] + FeeLevel64 const defaulted{}; + FeeLevel64 test{0}; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(beast::kZero); + EXPECT_EQ(test.fee(), 0); + + test = beast::kZero; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(100u); + EXPECT_EQ(test.fee(), 100); + + FeeLevel64 const targetSame{200u}; + FeeLevel32 const targetOther{300u}; + test = make(targetSame); + EXPECT_EQ(test.fee(), 200); + EXPECT_EQ(test, targetSame); + EXPECT_TRUE(test < FeeLevel64{1000}); + EXPECT_TRUE(test > FeeLevel64{100}); + test = make(targetOther); + EXPECT_EQ(test.fee(), 300); + EXPECT_EQ(test, targetOther); + + test = std::uint64_t(200); + EXPECT_EQ(test.fee(), 200); + test = std::uint32_t(300); + EXPECT_EQ(test.fee(), 300); + + test = targetSame; + EXPECT_EQ(test.fee(), 200); + test = targetOther.fee(); + EXPECT_EQ(test.fee(), 300); + EXPECT_EQ(test, targetOther); + + test = targetSame * 2; + EXPECT_EQ(test.fee(), 400); + test = 3 * targetSame; + EXPECT_EQ(test.fee(), 600); + test = targetSame / 10; + EXPECT_EQ(test.fee(), 20); + + test += targetSame; + EXPECT_EQ(test.fee(), 220); + + test -= targetSame; + EXPECT_EQ(test.fee(), 20); + + test++; + EXPECT_EQ(test.fee(), 21); + ++test; + EXPECT_EQ(test.fee(), 22); + test--; + EXPECT_EQ(test.fee(), 21); + --test; + EXPECT_EQ(test.fee(), 20); + + test *= 5; + EXPECT_EQ(test.fee(), 100); + test /= 2; + EXPECT_EQ(test.fee(), 50); + test %= 13; + EXPECT_EQ(test.fee(), 11); + + /* + // illegal with unsigned + test = -test; + EXPECT_EQ(test.fee(), -11); + EXPECT_EQ(test.signum(), -1); + EXPECT_EQ(to_string(test), "-11"); + */ + + EXPECT_TRUE(test); + test = 0; + EXPECT_FALSE(test); + EXPECT_EQ(test.signum(), 0); + test = targetSame; + EXPECT_EQ(test.signum(), 1); + EXPECT_EQ(to_string(test), "200"); + } + { + auto make = [&](auto x) -> FeeLevelDouble { return x; }; + auto explicitmake = [&](auto x) -> FeeLevelDouble { return FeeLevelDouble{x}; }; + + [[maybe_unused]] + FeeLevelDouble const defaulted{}; + FeeLevelDouble test{0}; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(beast::kZero); + EXPECT_EQ(test.fee(), 0); + + test = beast::kZero; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(100.0); + EXPECT_EQ(test.fee(), 100); + + FeeLevelDouble const targetSame{200.0}; + FeeLevel64 const targetOther{300}; + test = make(targetSame); + EXPECT_EQ(test.fee(), 200); + EXPECT_EQ(test, targetSame); + EXPECT_TRUE(test < FeeLevelDouble{1000.0}); + EXPECT_TRUE(test > FeeLevelDouble{100.0}); + test = targetOther.fee(); + EXPECT_EQ(test.fee(), 300); + EXPECT_EQ(test, targetOther); + + test = 200.0; + EXPECT_EQ(test.fee(), 200); + test = std::uint64_t(300); + EXPECT_EQ(test.fee(), 300); + + test = targetSame; + EXPECT_EQ(test.fee(), 200); + + test = targetSame * 2; + EXPECT_EQ(test.fee(), 400); + test = 3 * targetSame; + EXPECT_EQ(test.fee(), 600); + test = targetSame / 10; + EXPECT_EQ(test.fee(), 20); + + test += targetSame; + EXPECT_EQ(test.fee(), 220); + + test -= targetSame; + EXPECT_EQ(test.fee(), 20); + + test++; + EXPECT_EQ(test.fee(), 21); + ++test; + EXPECT_EQ(test.fee(), 22); + test--; + EXPECT_EQ(test.fee(), 21); + --test; + EXPECT_EQ(test.fee(), 20); + + test *= 5; + EXPECT_EQ(test.fee(), 100); + test /= 2; + EXPECT_EQ(test.fee(), 50); + /* illegal with floating + test %= 13; + EXPECT_EQ(test.fee(), 11); + */ + + // legal with signed + test = -test; + EXPECT_EQ(test.fee(), -50); + EXPECT_EQ(test.signum(), -1); + EXPECT_EQ(to_string(test), "-50.000000"); + + EXPECT_TRUE(test); + test = 0; + EXPECT_FALSE(test); + EXPECT_EQ(test.signum(), 0); + test = targetSame; + EXPECT_EQ(test.signum(), 1); + EXPECT_EQ(to_string(test), "200.000000"); + } +} + +TEST(UnitsTest, initial_xrp) +{ + EXPECT_EQ(kInitialXrp.drops(), 100'000'000'000'000'000); + EXPECT_EQ(kInitialXrp, XRPAmount{100'000'000'000'000'000}); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/basics/XRPAmount.cpp b/src/tests/libxrpl/basics/XRPAmount.cpp new file mode 100644 index 0000000000..243afbd1a0 --- /dev/null +++ b/src/tests/libxrpl/basics/XRPAmount.cpp @@ -0,0 +1,295 @@ +#include + +#include + +#include + +#include +#include + +namespace xrpl { + +TEST(XRPAmountTest, sig_num) +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + if (i < 0) + { + EXPECT_TRUE(x.signum() < 0); + } + else if (i > 0) + { + EXPECT_TRUE(x.signum() > 0); + } + else + { + EXPECT_EQ(x.signum(), 0); + } + } +} + +TEST(XRPAmountTest, beast_zero) +{ + using beast::kZero; + + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + EXPECT_TRUE((i == 0) == (x == kZero)); + EXPECT_TRUE((i != 0) == (x != kZero)); + EXPECT_TRUE((i < 0) == (x < kZero)); + EXPECT_TRUE((i > 0) == (x > kZero)); + EXPECT_TRUE((i <= 0) == (x <= kZero)); + EXPECT_TRUE((i >= 0) == (x >= kZero)); + + EXPECT_TRUE((0 == i) == (kZero == x)); + EXPECT_TRUE((0 != i) == (kZero != x)); + EXPECT_TRUE((0 < i) == (kZero < x)); + EXPECT_TRUE((0 > i) == (kZero > x)); + EXPECT_TRUE((0 <= i) == (kZero <= x)); + EXPECT_TRUE((0 >= i) == (kZero >= x)); + } +} + +TEST(XRPAmountTest, comparisons) +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + for (auto j : {-1, 0, 1}) + { + XRPAmount const y(j); + + EXPECT_EQ((i == j), (x == y)); + EXPECT_EQ((i != j), (x != y)); + EXPECT_EQ((i < j), (x < y)); + EXPECT_EQ((i > j), (x > y)); + EXPECT_EQ((i <= j), (x <= y)); + EXPECT_EQ((i >= j), (x >= y)); + } + } +} + +TEST(XRPAmountTest, add_sub) +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + for (auto j : {-1, 0, 1}) + { + XRPAmount const y(j); + + EXPECT_EQ(XRPAmount(i + j), (x + y)); + EXPECT_EQ(XRPAmount(i - j), (x - y)); + + EXPECT_EQ((x + y), (y + x)); // addition is commutative + } + } +} + +TEST(XRPAmountTest, decimal) +{ + // Tautology + EXPECT_EQ(kDropsPerXrp.decimalXRP(), 1); + + XRPAmount test{1}; + EXPECT_EQ(test.decimalXRP(), 0.000001); + + test = -test; + EXPECT_EQ(test.decimalXRP(), -0.000001); + + test = 100'000'000; + EXPECT_EQ(test.decimalXRP(), 100); + + test = -test; + EXPECT_EQ(test.decimalXRP(), -100); +} + +TEST(XRPAmountTest, functions) +{ + // Explicitly test every defined function for the XRPAmount class + // since some of them are templated, but not used anywhere else. + auto make = [&](auto x) -> XRPAmount { return XRPAmount{x}; }; + + XRPAmount const defaulted{}; + (void)defaulted; + XRPAmount test{0}; + EXPECT_EQ(test.drops(), 0); + + test = make(beast::kZero); + EXPECT_EQ(test.drops(), 0); + + test = beast::kZero; + EXPECT_EQ(test.drops(), 0); + + test = make(100); + EXPECT_EQ(test.drops(), 100); + + test = make(100u); + EXPECT_EQ(test.drops(), 100); + + XRPAmount const targetSame{200u}; + test = make(targetSame); + EXPECT_EQ(test.drops(), 200); + EXPECT_EQ(test, targetSame); + EXPECT_TRUE(test < XRPAmount{1000}); + EXPECT_TRUE(test > XRPAmount{100}); + + test = std::int64_t(200); + EXPECT_EQ(test.drops(), 200); + test = std::uint32_t(300); + EXPECT_EQ(test.drops(), 300); + + test = targetSame; + EXPECT_EQ(test.drops(), 200); + auto testOther = test.dropsAs(); + EXPECT_TRUE(testOther); + EXPECT_EQ(*testOther, 200); // NOLINT(bugprone-unchecked-optional-access) + test = std::numeric_limits::max(); + testOther = test.dropsAs(); + EXPECT_FALSE(testOther); + test = -1; + testOther = test.dropsAs(); + EXPECT_FALSE(testOther); + + test = targetSame * 2; + EXPECT_EQ(test.drops(), 400); + test = 3 * targetSame; + EXPECT_EQ(test.drops(), 600); + test = 20; + EXPECT_EQ(test.drops(), 20); + + test += targetSame; + EXPECT_EQ(test.drops(), 220); + + test -= targetSame; + EXPECT_EQ(test.drops(), 20); + + test *= 5; + EXPECT_EQ(test.drops(), 100); + test = 50; + EXPECT_EQ(test.drops(), 50); + test -= 39; + EXPECT_EQ(test.drops(), 11); + + // legal with signed + test = -test; + EXPECT_EQ(test.drops(), -11); + EXPECT_EQ(test.signum(), -1); + EXPECT_EQ(to_string(test), "-11"); + + EXPECT_TRUE(test); + test = 0; + EXPECT_FALSE(test); + EXPECT_EQ(test.signum(), 0); + test = targetSame; + EXPECT_EQ(test.signum(), 1); + EXPECT_EQ(to_string(test), "200"); +} + +TEST(XRPAmountTest, mul_ratio) +{ + constexpr auto kMaxUInt32 = std::numeric_limits::max(); + constexpr auto kMaxXrp = std::numeric_limits::max(); + constexpr auto kMinXrp = std::numeric_limits::min(); + + { + // multiply by a number that would overflow then divide by the same + // number, and check we didn't lose any value + XRPAmount big(kMaxXrp); + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false)); + + // multiply and divide by values that would overflow if done + // naively, and check that it gives the correct answer + big -= 0xf; // Subtract a little so it's divisible by 4 + EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3); + EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3); + EXPECT_EQ(big.value() % 4, 0); + EXPECT_GT(big.value(), kMaxXrp / 3); + EXPECT_LE(big.value() / 4, kMaxXrp / 3); + } + + { + // Similar test as above, but for negative values + XRPAmount big(kMinXrp); // NOLINT TODO + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false)); + + // multiply and divide by values that would overflow if done + // naively, and check that it gives the correct answer + EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3); + EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3); + EXPECT_EQ(big.value() % 4, 0); + EXPECT_LT(big.value(), kMinXrp / 3); + EXPECT_GE(big.value() / 4, kMinXrp / 3); + } + + { + // small amounts + XRPAmount const tiny(1); + // Round up should give the smallest allowable number + EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt32, true)); + // rounding down should be zero + EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt32, false)); + EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt32 - 1, kMaxUInt32, false)); + + // tiny negative numbers + XRPAmount const tinyNeg(-1); + // Round up should give zero + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt32, true)); + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, true)); + // rounding down should be tiny + EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, false)); + } + + { // rounding + { + XRPAmount const one(1); + auto const rup = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, true); + auto const rdown = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, false); + EXPECT_EQ(rup.drops() - rdown.drops(), 1); + } + + { + XRPAmount const big(kMaxXrp); + auto const rup = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, true); + auto const rdown = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, false); + EXPECT_EQ(rup.drops() - rdown.drops(), 1); + } + + { + XRPAmount const negOne(-1); + auto const rup = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, true); + auto const rdown = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, false); + EXPECT_EQ(rup.drops() - rdown.drops(), 1); + } + } + + { + // division by zero + XRPAmount const one(1); + EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); }); + } + + { + // overflow + XRPAmount const big(kMaxXrp); + EXPECT_ANY_THROW({ mulRatio(big, 2, 1, true); }); + } + + { + // underflow + XRPAmount const bigNegative(kMinXrp + 10); + EXPECT_EQ(mulRatio(bigNegative, 2, 1, true), kMinXrp); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/base58.cpp b/src/tests/libxrpl/basics/base58.cpp new file mode 100644 index 0000000000..d452453f76 --- /dev/null +++ b/src/tests/libxrpl/basics/base58.cpp @@ -0,0 +1,438 @@ +#include + +#include // IWYU pragma: keep + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _MSC_VER + +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { +namespace { + +[[nodiscard]] inline auto +randEngine() -> std::mt19937& +{ + static std::mt19937 kR = [] { + std::random_device rd; + return std::mt19937{rd()}; + }(); + return kR; +} + +constexpr int kNumTokenTypeIndexes = 9; + +[[nodiscard]] inline auto +tokenTypeAndSize(int i) -> std::tuple +{ + assert(i < kNumTokenTypeIndexes); + + switch (i) + { + using enum xrpl::TokenType; + case 0: + return {None, 20}; + case 1: + return {NodePublic, 32}; + case 2: + return {NodePublic, 33}; + case 3: + return {NodePrivate, 32}; + case 4: + return {AccountID, 20}; + case 5: + return {AccountPublic, 32}; + case 6: + return {AccountPublic, 33}; + case 7: + return {AccountSecret, 32}; + case 8: + return {FamilySeed, 16}; + default: + throw std::invalid_argument( + "Invalid token selection passed to tokenTypeAndSize() " + "in " __FILE__); + } +} + +[[nodiscard]] inline auto +randomTokenTypeAndSize() -> std::tuple +{ + using namespace xrpl; + auto& rng = randEngine(); + std::uniform_int_distribution<> d(0, 8); + return tokenTypeAndSize(d(rng)); +} + +// Return the token type and subspan of `d` to use as test data. +[[nodiscard]] inline auto +randomB256TestData(std::span d) + -> std::tuple> +{ + auto& rng = randEngine(); + std::uniform_int_distribution dist(0, 255); + auto [tokType, tokSize] = randomTokenTypeAndSize(); + std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); }); + return {tokType, d.subspan(0, tokSize)}; +} + +inline void +printAsChar(std::span a, std::span b) +{ + auto asString = [](std::span s) { + std::string r; + r.resize(s.size()); + std::ranges::copy(s, r.begin()); + return r; + }; + auto sa = asString(a); + auto sb = asString(b); + std::cerr << "\n\n" << sa << "\n" << sb << "\n"; +} + +inline void +printAsInt(std::span a, std::span b) +{ + auto asString = [](std::span s) -> std::string { + std::stringstream sstr; + for (auto i : s) + { + sstr << std::setw(3) << int(i) << ','; + } + return sstr.str(); + }; + auto sa = asString(a); + auto sb = asString(b); + std::cerr << "\n\n" << sa << "\n" << sb << "\n"; +} + +} // namespace + +namespace multiprecision_utils { + +boost::multiprecision::checked_uint512_t +toBoostMP(std::span in) +{ + boost::multiprecision::checked_uint512_t mbp = 0; + for (auto const& i : std::views::reverse(in)) + { + mbp <<= 64; + mbp += i; + } + return mbp; +} + +std::vector +randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5) +{ + auto eng = randEngine(); + std::uniform_int_distribution numCoeffDist(minSize, maxSize); + std::uniform_int_distribution dist; + auto const numCoeff = numCoeffDist(eng); + std::vector coeffs; + coeffs.reserve(numCoeff); + for (int i = 0; i < numCoeff; ++i) + { + coeffs.push_back(dist(eng)); + } + return coeffs; +} +} // namespace multiprecision_utils + +TEST(Base58Test, multiprecision) +{ + using namespace boost::multiprecision; + + constexpr std::size_t kIters = 100000; + auto eng = randEngine(); + std::uniform_int_distribution dist; + std::uniform_int_distribution dist1(1); + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist(eng); + if (d == 0u) + continue; + auto bigInt = multiprecision_utils::randomBigInt(); + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refDiv = boostBigInt / d; + auto const refMod = boostBigInt % d; + + auto const mod = b58_fast::detail::inplaceBigintDivRem( + std::span(bigInt.data(), bigInt.size()), d); + auto const foundDiv = multiprecision_utils::toBoostMP(bigInt); + EXPECT_EQ(refMod.convert_to(), mod); + EXPECT_EQ(foundDiv, refDiv); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist(eng); + auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2); + if (bigInt[bigInt.size() - 1] == std::numeric_limits::max()) + { + bigInt[bigInt.size() - 1] -= 1; // Prevent overflow + } + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refAdd = boostBigInt + d; + + auto const result = b58_fast::detail::inplaceBigintAdd( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::Success); + auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); + EXPECT_EQ(refAdd, foundAdd); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist1(eng); + // Force overflow + std::vector bigInt(5, std::numeric_limits::max()); + + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refAdd = boostBigInt + d; + + auto const result = b58_fast::detail::inplaceBigintAdd( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::OverflowAdd); + auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); + EXPECT_NE(refAdd, foundAdd); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist(eng); + auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2); + // inplace mul requires the most significant coeff to be zero to + // hold the result. + bigInt[bigInt.size() - 1] = 0; + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refMul = boostBigInt * d; + + auto const result = b58_fast::detail::inplaceBigintMul( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::Success); + auto const foundMul = multiprecision_utils::toBoostMP(bigInt); + EXPECT_EQ(refMul, foundMul); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist1(eng); + // Force overflow + std::vector bigInt(5, std::numeric_limits::max()); + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refMul = boostBigInt * d; + + auto const result = b58_fast::detail::inplaceBigintMul( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::InputTooLarge); + auto const foundMul = multiprecision_utils::toBoostMP(bigInt); + EXPECT_NE(refMul, foundMul); + } +} + +TEST(Base58Test, fast_matches_ref) +{ + auto testRawEncode = [&](std::span const& b256Data) { + std::array b58ResultBuf[2]; + std::array, 2> b58Result; + + std::array b256ResultBuf[2]; + std::array, 2> b256Result; + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b58ResultBuf[i]}; + if (i == 0) + { + auto const r = xrpl::b58_fast::detail::b256ToB58Be(b256Data, outBuf); + EXPECT_TRUE(r); + b58Result[i] = r.value(); + } + else + { + std::array tmpBuf{}; + std::string const s = xrpl::b58_ref::detail::encodeBase58( + b256Data.data(), b256Data.size(), tmpBuf.data(), tmpBuf.size()); + EXPECT_TRUE(s.size()); + b58Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b58Result[i].begin()); + } + } + auto const rawB58SameSize = b58Result[0].size() == b58Result[1].size(); + EXPECT_TRUE(rawB58SameSize); + if (rawB58SameSize) + { + auto const rawB58SameData = + memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0; + EXPECT_TRUE(rawB58SameData); + if (!rawB58SameData) + { + printAsChar(b58Result[0], b58Result[1]); + } + } + + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; + if (i == 0) + { + std::string const in( + b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); + auto const r = xrpl::b58_fast::detail::b58ToB256Be(in, outBuf); + EXPECT_TRUE(r); + b256Result[i] = r.value(); + } + else + { + std::string const st(b58Result[i].begin(), b58Result[i].end()); + std::string const s = xrpl::b58_ref::detail::decodeBase58(st); + EXPECT_TRUE(s.size()); + b256Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b256Result[i].begin()); + } + } + + auto const rawB256SameSize = b256Result[0].size() == b256Result[1].size(); + EXPECT_TRUE(rawB256SameSize); + if (rawB256SameSize) + { + auto const rawB256SameData = + memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0; + EXPECT_TRUE(rawB256SameData); + if (!rawB256SameData) + { + printAsInt(b256Result[0], b256Result[1]); + } + } + }; + + auto testTokenEncode = [&](xrpl::TokenType const tokType, + std::span const& b256Data) { + std::array b58ResultBuf[2]; + std::array, 2> b58Result; + + std::array b256ResultBuf[2]; + std::array, 2> b256Result; + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()}; + if (i == 0) + { + auto const r = xrpl::b58_fast::encodeBase58Token(tokType, b256Data, outBuf); + EXPECT_TRUE(r); + b58Result[i] = r.value(); + } + else + { + std::string const s = + xrpl::b58_ref::encodeBase58Token(tokType, b256Data.data(), b256Data.size()); + EXPECT_TRUE(s.size()); + b58Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b58Result[i].begin()); + } + } + auto const tokenB58SameSize = b58Result[0].size() == b58Result[1].size(); + EXPECT_TRUE(tokenB58SameSize); + if (tokenB58SameSize) + { + auto const tokenB58SameData = + memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0; + EXPECT_TRUE(tokenB58SameData); + if (!tokenB58SameData) + { + printAsChar(b58Result[0], b58Result[1]); + } + } + + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; + if (i == 0) + { + std::string const in( + b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); + auto const r = xrpl::b58_fast::decodeBase58Token(tokType, in, outBuf); + EXPECT_TRUE(r); + b256Result[i] = r.value(); + } + else + { + std::string const st(b58Result[i].begin(), b58Result[i].end()); + std::string const s = xrpl::b58_ref::decodeBase58Token(st, tokType); + EXPECT_TRUE(s.size()); + b256Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b256Result[i].begin()); + } + } + + auto const tokenB256SameSize = b256Result[0].size() == b256Result[1].size(); + EXPECT_TRUE(tokenB256SameSize); + if (tokenB256SameSize) + { + auto const tokenB256SameData = + memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0; + EXPECT_TRUE(tokenB256SameData); + if (!tokenB256SameData) + { + printAsInt(b256Result[0], b256Result[1]); + } + } + }; + + auto testIt = [&](xrpl::TokenType const tokType, std::span const& b256Data) { + testRawEncode(b256Data); + testTokenEncode(tokType, b256Data); + }; + + // test every token type with data where every byte is the same and the + // bytes range from 0-255 + for (int i = 0; i < kNumTokenTypeIndexes; ++i) + { + std::array b256DataBuf{}; + auto const [tokType, tokSize] = tokenTypeAndSize(i); + for (int d = 0; d <= 255; ++d) + { + memset(b256DataBuf.data(), d, tokSize); + testIt(tokType, std::span(b256DataBuf.data(), tokSize)); + } + } + + // test with random data + constexpr std::size_t kIters = 100000; + for (int i = 0; i < kIters; ++i) + { + std::array b256DataBuf{}; + auto const [tokType, b256Data] = randomB256TestData(b256DataBuf); + testIt(tokType, b256Data); + } +} + +} // namespace xrpl::test + +#endif // _MSC_VER diff --git a/src/tests/libxrpl/basics/base_uint_test.cpp b/src/tests/libxrpl/basics/base_uint_test.cpp new file mode 100644 index 0000000000..c9bfc35c94 --- /dev/null +++ b/src/tests/libxrpl/basics/base_uint_test.cpp @@ -0,0 +1,421 @@ +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +// a non-hashing Hasher that just copies the bytes. +// Used to test hash_append in base_uint +template +struct Nonhash +{ + static constexpr auto const kEndian = boost::endian::order::big; + static constexpr std::size_t kWidth = Bits / 8; + + std::array data; + + Nonhash() = default; + + void + operator()(void const* key, std::size_t len) noexcept + { + assert(len == kWidth); + memcpy(data.data(), key, len); + } + + explicit + operator std::size_t() noexcept + { + return kWidth; + } +}; + +struct BaseUintTest : public ::testing::Test +{ + using BaseUInt96 = BaseUInt<96>; + static_assert(std::is_copy_constructible_v); + static_assert(std::is_copy_assignable_v); + + static void + testComparisons() + { + { + static constexpr std::array, 6> kTestArgs{ + {{"0000000000000000", "0000000000000001"}, + {"0000000000000000", "ffffffffffffffff"}, + {"1234567812345678", "2345678923456789"}, + {"8000000000000000", "8000000000000001"}, + {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, + {"fffffffffffffffe", "ffffffffffffffff"}}}; + + for (auto const& arg : kTestArgs) + { + xrpl::BaseUInt<64> const u{arg.first}, v{arg.second}; + // For code readability, we want to use general boolean + // expectations instead of specific EXPECT_LT etc. + EXPECT_TRUE(u < v); + EXPECT_TRUE(u <= v); + EXPECT_TRUE(u != v); + EXPECT_FALSE(u == v); + EXPECT_FALSE(u > v); + EXPECT_FALSE(u >= v); + EXPECT_FALSE(v < u); + EXPECT_FALSE(v <= u); + EXPECT_TRUE(v != u); + EXPECT_FALSE(v == u); + EXPECT_TRUE(v > u); + EXPECT_TRUE(v >= u); + EXPECT_TRUE(u == u); + EXPECT_TRUE(v == v); + } + } + + { + static constexpr std::array, 6> kTestArgs{ + { + {"000000000000000000000000", "000000000000000000000001"}, + {"000000000000000000000000", "ffffffffffffffffffffffff"}, + {"0123456789ab0123456789ab", "123456789abc123456789abc"}, + {"555555555555555555555555", "55555555555a555555555555"}, + {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, + {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, + }}; + + for (auto const& arg : kTestArgs) + { + xrpl::BaseUInt<96> const u{arg.first}, v{arg.second}; + EXPECT_TRUE(u < v); + EXPECT_TRUE(u <= v); + EXPECT_TRUE(u != v); + EXPECT_FALSE(u == v); + EXPECT_FALSE(u > v); + EXPECT_FALSE(u >= v); + EXPECT_FALSE(v < u); + EXPECT_FALSE(v <= u); + EXPECT_TRUE(v != u); + EXPECT_FALSE(v == u); + EXPECT_TRUE(v > u); + EXPECT_TRUE(v >= u); + EXPECT_TRUE(u == u); + EXPECT_TRUE(v == v); + } + } + } +}; + +using BaseUintDeathTest = BaseUintTest; + +TEST_F(BaseUintDeathTest, fromRaw_size_mismatch) +{ + // ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts. Rather than twist + // these tests into knots to make them work, just skip them. +#ifdef ENABLE_VOIDSTAR + GTEST_SKIP() << "ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts."; +#else + auto smallConstruct = [] { + // Container smaller than the base_uint (8 bytes vs 12 bytes for + // test96). Only the first 8 bytes are copied; the remaining 4 bytes + // stay zero. + Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8}; + BaseUInt96 const result = BaseUInt96::fromRaw(tooSmall); + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "010203040506070800000000") << resultText; + }; + EXPECT_DEBUG_DEATH(smallConstruct(), "input size match"); + + auto largeConstruct = [] { + // Container larger than the base_uint (16 bytes vs 12 bytes for + // test96). Only the first 12 bytes are copied; the extra bytes are + // ignored. + Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + BaseUInt96 const result = BaseUInt96::fromRaw(tooBig); + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText; + }; + EXPECT_DEBUG_DEATH(largeConstruct(), "input size match"); + + auto smallCopy = [] { + // Container smaller than the base_uint (8 bytes vs 12 bytes for + // test96). Only the first 8 bytes are copied; the remaining 4 bytes + // stay zero. + Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8}; + BaseUInt96 result{}; + --result; + { + auto const originalText = to_string(result); + EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText; + } + result = tooSmall; + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "010203040506070800000000") << resultText; + }; + EXPECT_DEBUG_DEATH(smallCopy(), "input size match"); + + auto const largeCopy = [] { + // Container larger than the base_uint (16 bytes vs 12 bytes for + // test96). Only the first 12 bytes are copied; the extra bytes are + // ignored. + Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + BaseUInt96 result{}; + --result; + { + auto const originalText = to_string(result); + EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText; + } + result = tooBig; + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText; + }; + EXPECT_DEBUG_DEATH(largeCopy(), "input size match"); +#endif +} + +TEST_F(BaseUintTest, base_uint) +{ + static_assert(!std::is_constructible_v>); + static_assert(!std::is_assignable_v>); + + testComparisons(); + + // used to verify set insertion (hashing required) + std::unordered_set> uset; + + Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + EXPECT_EQ(BaseUInt96::kBytes, raw.size()); + + BaseUInt96 u = BaseUInt96::fromRaw(raw); + uset.insert(u); + EXPECT_EQ(raw.size(), u.size()); + EXPECT_EQ(to_string(u), "0102030405060708090A0B0C"); + EXPECT_EQ(toShortString(u), "01020304..."); + EXPECT_EQ(*u.data(), 1); + EXPECT_EQ(u.signum(), 1); + EXPECT_FALSE(!u); + EXPECT_FALSE(u.isZero()); + EXPECT_TRUE(u.isNonZero()); + unsigned char t = 0; + for (auto& d : u) + { + EXPECT_EQ(d, ++t); + } + + // Test hash_append by "hashing" with a no-op hasher (h) + // and then extracting the bytes that were written during hashing + // back into another base_uint (w) for comparison with the original + Nonhash<96> h{}; + hash_append(h, u); + BaseUInt96 const w = + BaseUInt96::fromRaw(std::vector(h.data.begin(), h.data.end())); + EXPECT_EQ(w, u); + + BaseUInt96 v{~u}; + uset.insert(v); + EXPECT_EQ(to_string(v), "FEFDFCFBFAF9F8F7F6F5F4F3"); + EXPECT_EQ(toShortString(v), "FEFDFCFB..."); + EXPECT_EQ(*v.data(), 0xfe); + EXPECT_EQ(v.signum(), 1); + EXPECT_FALSE(!v); + EXPECT_FALSE(v.isZero()); + EXPECT_TRUE(v.isNonZero()); + + t = 0xff; + for (auto& d : v) + { + EXPECT_EQ(d, --t); + } + + EXPECT_LT(u, v); + EXPECT_GT(v, u); + + v = u; + EXPECT_EQ(v, u); + + BaseUInt96 z{beast::kZero}; + uset.insert(z); + EXPECT_EQ(to_string(z), "000000000000000000000000"); + EXPECT_EQ(toShortString(z), "00000000..."); + EXPECT_EQ(*z.data(), 0); + EXPECT_EQ(*z.begin(), 0); + EXPECT_EQ(*std::prev(z.end(), 1), 0); + EXPECT_EQ(z.signum(), 0); + EXPECT_TRUE(!z); + EXPECT_TRUE(z.isZero()); + EXPECT_FALSE(z.isNonZero()); + for (auto& d : z) + { + EXPECT_EQ(d, 0); + } + + { + // There are several ways to create a zero. beast::kZero is tested above. Test some + // others. + BaseUInt96 const z1; + EXPECT_EQ(z1, z) << to_string(z1); + + BaseUInt96 const z2{}; + EXPECT_EQ(z2, z) << to_string(z2); + + BaseUInt96 const z3{0u}; + EXPECT_EQ(z3, z) << to_string(z3); + } + + BaseUInt96 n{z}; + n++; + EXPECT_EQ(n, BaseUInt96(1)); + n--; + EXPECT_EQ(n, beast::kZero); + EXPECT_EQ(n, z); + n--; + EXPECT_EQ(to_string(n), "FFFFFFFFFFFFFFFFFFFFFFFF"); + EXPECT_EQ(toShortString(n), "FFFFFFFF..."); + n = beast::kZero; + EXPECT_EQ(n, z); + + BaseUInt96 zp1{z}; + zp1++; + BaseUInt96 zm1{z}; + zm1--; + BaseUInt96 const x{zm1 ^ zp1}; + uset.insert(x); + EXPECT_EQ(to_string(x), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(x); + EXPECT_EQ(toShortString(x), "FFFFFFFF...") << toShortString(x); + + EXPECT_EQ(uset.size(), 4); + + BaseUInt96 tmp; + EXPECT_TRUE(tmp.parseHex(to_string(u))); + EXPECT_EQ(tmp, u); + tmp = z; + + // fails with extra char + EXPECT_FALSE(tmp.parseHex("A" + to_string(u))); + tmp = z; + + // fails with extra char at end + EXPECT_FALSE(tmp.parseHex(to_string(u) + "A")); + + // fails with a non-hex character at some point in the string: + tmp = z; + + for (std::size_t i = 0; i != 24; ++i) + { + std::string x = to_string(z); + x[i] = ('G' + (i % 10)); + EXPECT_FALSE(tmp.parseHex(x)); + } + + // Walking 1s: + for (std::size_t i = 0; i != 24; ++i) + { + std::string s1 = "000000000000000000000000"; + s1[i] = '1'; + + EXPECT_TRUE(tmp.parseHex(s1)); + EXPECT_EQ(to_string(tmp), s1); + } + + // Walking 0s: + for (std::size_t i = 0; i != 24; ++i) + { + std::string s1 = "111111111111111111111111"; + s1[i] = '0'; + + EXPECT_TRUE(tmp.parseHex(s1)); + EXPECT_EQ(to_string(tmp), s1); + } + + // Constexpr constructors + { + static_assert(BaseUInt96{}.signum() == 0); + static_assert(BaseUInt96("0").signum() == 0); + static_assert(BaseUInt96("000000000000000000000000").signum() == 0); + static_assert(BaseUInt96("000000000000000000000001").signum() == 1); + static_assert(BaseUInt96("800000000000000000000000").signum() == 1); + + // Using the constexpr constructor in a non-constexpr context + // with an error in the parsing throws an exception. + { + // Invalid length for string. + bool caught = false; + try + { + // Try to prevent constant evaluation. + std::vector str(23, '7'); + std::string_view const sView(str.data(), str.size()); + [[maybe_unused]] BaseUInt96 const t96(sView); + } + catch (std::invalid_argument const& e) + { + EXPECT_EQ(e.what(), std::string("invalid length for hex string")); + caught = true; + } + EXPECT_TRUE(caught); + } + { + // Invalid character in string. + bool caught = false; + try + { + // Try to prevent constant evaluation. + std::vector str(23, '7'); + str.push_back('G'); + std::string_view const sView(str.data(), str.size()); + [[maybe_unused]] BaseUInt96 const t96(sView); + } + catch (std::range_error const& e) + { + EXPECT_EQ(e.what(), std::string("invalid hex character")); + caught = true; + } + EXPECT_TRUE(caught); + } + + // Verify that constexpr base_uints interpret a string the same + // way parseHex() does. + struct StrBaseUInt + { + char const* const str; + BaseUInt96 tst; + + constexpr StrBaseUInt(char const* s) : str(s), tst(s) + { + } + }; + constexpr StrBaseUInt kTestCases[] = { + "000000000000000000000000", + "000000000000000000000001", + "fedcba9876543210ABCDEF91", + "19FEDCBA0123456789abcdef", + "800000000000000000000000", + "fFfFfFfFfFfFfFfFfFfFfFfF", + }; + + for (StrBaseUInt const& t : kTestCases) + { + BaseUInt96 t96; + EXPECT_TRUE(t96.parseHex(t.str)); + EXPECT_EQ(t96, t.tst); + } + } +} + +} // namespace xrpl::test diff --git a/src/test/basics/hardened_hash_test.cpp b/src/tests/libxrpl/basics/hardened_hash.cpp similarity index 84% rename from src/test/basics/hardened_hash_test.cpp rename to src/tests/libxrpl/basics/hardened_hash.cpp index 8b10967932..4f51ce4305 100644 --- a/src/test/basics/hardened_hash_test.cpp +++ b/src/tests/libxrpl/basics/hardened_hash.cpp @@ -1,6 +1,8 @@ #include + #include -#include + +#include #include #include @@ -153,20 +155,20 @@ static_assert(sha256_t::kBits == 256, "sha256_t must have 256 bits"); namespace xrpl { -class hardened_hash_test : public beast::unit_test::Suite +class HardenedHashTest : public ::testing::Test { public: template - void + static void check() { T t{}; HardenedHash<>()(t); - pass(); + SUCCEED(); } template