Compare commits

..

10 Commits

Author SHA1 Message Date
Mayukha Vadari
d5c0f8f0a7 Merge branch 'develop' of https://github.com/XRPLF/rippled into mvadari/rearch/all 2026-07-13 15:03:44 -04:00
Mayukha Vadari
30066af4ac fix issues 2026-07-13 14:10:37 -04:00
Mayukha Vadari
d4cb62e78d clean up OracleEntry 2026-07-13 13:27:41 -04:00
Mayukha Vadari
cb2ab92927 Merge branch 'develop' of https://github.com/XRPLF/rippled into mvadari/rearch/all 2026-07-13 12:48:30 -04:00
Mayukha Vadari
16e7f81110 Merge branch 'develop' into mvadari/rearch/all 2026-07-11 10:28:13 -04:00
Mayukha Vadari
42c93e333e more stuff 2026-07-10 19:56:00 -04:00
Mayukha Vadari
b8b9da92ee add create/destroy helpers 2026-07-10 16:43:52 -04:00
Mayukha Vadari
9d11ba0ca7 migrate 2026-07-10 10:51:08 -04:00
Mayukha Vadari
fb9b10fc03 migrate lending helpers 2026-07-09 22:31:58 -04:00
Mayukha Vadari
628272a11e set up framework 2026-07-09 21:15:57 -04:00
203 changed files with 5379 additions and 7384 deletions

View File

@@ -56,17 +56,32 @@ Checks: "-*,
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-uppercase-literal-suffix
-readability-string-compare,
-readability-uniqueptr-delete-release,
-readability-uppercase-literal-suffix,
-readability-use-anyofallof,
-readability-use-concise-preprocessor-directives
"
# ---
# 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

View File

@@ -65,7 +65,6 @@ words:
- Btrfs
- Buildx
- canonicality
- canonicalised
- changespq
- checkme
- choco
@@ -73,7 +72,6 @@ words:
- citardauq
- clawback
- clawbacks
- clippy
- cmaketoolchain
- coeffs
- coldwallet
@@ -261,9 +259,6 @@ words:
- rocksdb
- Rohrs
- roundings
- rustc
- rustfmt
- rustup
- sahyadri
- Satoshi
- scons
@@ -307,7 +302,6 @@ words:
- takerpays
- ters
- TMEndpointv2
- toolchain
- tparam
- trixie
- tx

View File

@@ -11,9 +11,6 @@ endfunction()
function(create_symbolic_link target link)
endfunction()
function(xrpl_add_benchmark name)
endfunction()
macro(exclude_from_default target_)
endmacro()

View File

@@ -1,6 +1,3 @@
benchmarks.libxrpl > xrpl.basics
benchmarks.libxrpl > xrpl.config
benchmarks.libxrpl > xrpl.nodestore
libxrpl.basics > xrpl.basics
libxrpl.conditions > xrpl.basics
libxrpl.conditions > xrpl.conditions
@@ -162,7 +159,6 @@ 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

View File

@@ -25,16 +25,24 @@ 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."""
@@ -42,11 +50,13 @@ 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
@@ -79,9 +89,11 @@ 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):
@@ -156,18 +168,20 @@ _ARCHS: dict[str, Architecture] = {
}
def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]:
def expand_linux_matrix(
linux: LinuxFile, event: str | None = None
) -> 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. When 'minimal' is
true, only configs flagged as minimal are included.
compiler, build_type, sanitizers, and architecture lists. Configs that
exclude the current event are skipped.
"""
entries: list[MatrixEntry] = []
for distro, configs in linux.configs.items():
for cfg in configs:
if minimal and not cfg.minimal:
if not runs_on_event(cfg.exclude_event_types, event):
continue
# An empty sanitizers list means "one entry with no sanitizer".
effective_sanitizers = cfg.sanitizers or [""]
@@ -226,17 +240,19 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
return entries
def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry]:
def expand_platform_matrix(
pf: PlatformFile, event: str | None = None
) -> list[MatrixEntry]:
"""Expand a PlatformFile (macOS or Windows) into matrix entries.
When 'minimal' is true, only configs flagged as minimal are included.
Configs that exclude the current event are skipped.
"""
platform_name, arch = pf.platform.split("/")
is_windows = platform_name == "windows"
entries: list[MatrixEntry] = []
for cfg in pf.configs:
if minimal and not cfg.minimal:
if not runs_on_event(cfg.exclude_event_types, event):
continue
for build_type in cfg.build_type:
entries.append(
@@ -276,12 +292,12 @@ if __name__ == "__main__":
action="store_true",
)
parser.add_argument(
"-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",
"-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,
)
args = parser.parse_args()
@@ -292,15 +308,15 @@ if __name__ == "__main__":
else:
if args.config in ("linux", None):
matrix += expand_linux_matrix(
LinuxFile.load(THIS_DIR / "linux.json"), args.minimal
LinuxFile.load(THIS_DIR / "linux.json"), args.event
)
if args.config in ("macos", None):
matrix += expand_platform_matrix(
PlatformFile.load(THIS_DIR / "macos.json"), args.minimal
PlatformFile.load(THIS_DIR / "macos.json"), args.event
)
if args.config in ("windows", None):
matrix += expand_platform_matrix(
PlatformFile.load(THIS_DIR / "windows.json"), args.minimal
PlatformFile.load(THIS_DIR / "windows.json"), args.event
)
print(f"matrix={json.dumps({'include': [dataclasses.asdict(e) for e in matrix]})}")

View File

@@ -1,31 +1,17 @@
{
"image_tag": "sha-2e25435",
"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": ["arm64"],
"minimal": false
"arch": ["amd64", "arm64"]
},
{
"compiler": ["gcc", "clang"],
"build_type": ["Debug", "Release"],
"arch": ["amd64"],
"minimal": false,
"sanitizers": ["address", "undefinedbehavior"]
},
@@ -33,7 +19,6 @@
"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"
},
@@ -41,7 +26,6 @@
"compiler": ["clang"],
"build_type": ["Debug"],
"arch": ["amd64"],
"minimal": false,
"suffix": "voidstar",
"extra_cmake_args": "-Dvoidstar=ON"
},
@@ -49,7 +33,6 @@
"compiler": ["clang"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"suffix": "reffee",
"extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=1000"
},
@@ -57,9 +40,9 @@
"compiler": ["gcc"],
"build_type": ["Debug"],
"arch": ["amd64"],
"minimal": false,
"suffix": "unity",
"extra_cmake_args": "-Dunity=ON"
"extra_cmake_args": "-Dunity=ON",
"exclude_event_types": ["pull_request"]
}
],
@@ -67,8 +50,7 @@
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false
"arch": ["amd64"]
}
],
@@ -76,8 +58,7 @@
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false
"arch": ["amd64"]
}
]
},
@@ -87,7 +68,6 @@
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745"
}
],
@@ -97,7 +77,6 @@
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745"
}
]

View File

@@ -4,14 +4,13 @@
"configs": [
{
"build_type": "Release",
"extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"minimal": true
"extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
},
{
"build_type": "Debug",
"extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"build_only": true,
"minimal": false
"exclude_event_types": ["pull_request"]
}
]
}

View File

@@ -2,11 +2,11 @@
"platform": "windows/amd64",
"runner": ["self-hosted", "Windows", "dev-box-windows-2026"],
"configs": [
{ "build_type": "Release", "minimal": true },
{ "build_type": "Release" },
{
"build_type": "Debug",
"build_only": true,
"minimal": false
"exclude_event_types": ["pull_request"]
}
]
}

View File

@@ -14,7 +14,6 @@ permissions:
jobs:
main:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'IgnoreConflicts') }}
runs-on: ubuntu-latest
steps:
- name: Check if PRs are dirty

View File

@@ -1,11 +1,7 @@
# 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,
# 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,
# it also uploads the libxrpl recipe to the Conan remote.
name: PR
@@ -19,16 +15,8 @@ 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
@@ -37,18 +25,11 @@ defaults:
shell: bash
jobs:
# 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.
# 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.
should-run:
if: >-
${{
!github.event.pull_request.draft
|| contains(github.event.pull_request.labels.*.name, 'DraftRunCI')
|| contains(github.event.pull_request.labels.*.name, 'Full CI build')
}}
if: ${{ !github.event.pull_request.draft || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -110,17 +91,15 @@ 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.FULL == 'true' || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}"
echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}"
cat "${GITHUB_OUTPUT}"
outputs:
go: ${{ steps.go.outputs.go == 'true' }}
@@ -163,10 +142,7 @@ jobs:
package:
needs: [should-run, build-test]
# 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')) }}
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-package.yml
upload-recipe:

View File

@@ -41,13 +41,13 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-2e25435
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
with:
enable_ccache: false

View File

@@ -113,7 +113,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
with:
enable_ccache: ${{ inputs.ccache_enabled }}
@@ -124,7 +124,7 @@ jobs:
- name: Check tools
env:
CHECK_TOOLS_SKIP_CLONE: "1"
run: ./bin/check-tools.sh || true
run: ./bin/check-tools.sh
- name: Print build environment
uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574
@@ -324,23 +324,6 @@ jobs:
LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
# Smoke-run every benchmark module with a single repetition to confirm the
# benchmarks still build and execute. This is a correctness check, not a
# performance measurement, so it is skipped for instrumented builds
# (sanitizers/coverage/voidstar), where it would be slow and meaningless,
# and on Windows, where the `install` target does not build them.
- name: Run the benchmarks
if: ${{ !inputs.build_only && runner.os != 'Windows' && env.SANITIZERS_ENABLED == 'false' && env.COVERAGE_ENABLED != 'true' && env.VOIDSTAR_ENABLED != 'true' }}
working-directory: ${{ env.BUILD_DIR }}
run: |
rc=0
while IFS= read -r bench; do
echo "::group::${bench}"
"./${bench}" --benchmark_repetitions=1 || rc=1
echo "::endgroup::"
done < <(find src/benchmarks -type f -perm -u+x -name 'xrpl.bench.*')
exit "${rc}"
- name: Show test failure summary
if: ${{ failure() && !inputs.build_only }}
env:

View File

@@ -34,7 +34,7 @@ jobs:
needs: [determine-files]
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-2e25435"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-e29b523"
permissions:
contents: read
issues: write
@@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
with:
enable_ccache: false

View File

@@ -35,8 +35,5 @@ jobs:
id: generate
env:
GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}', inputs.os) || '' }}
# 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}"
GENERATE_EVENT: ${{ github.event_name }}
run: ./generate.py ${GENERATE_CONFIG} --event="${GENERATE_EVENT}" >>"${GITHUB_OUTPUT}"

View File

@@ -40,7 +40,7 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-2e25435
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}

View File

@@ -68,7 +68,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@ad188deb3dae79dc39816e16ddfdad1e06c6fab2
uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
with:
enable_ccache: false

View File

@@ -32,11 +32,6 @@ 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

View File

@@ -28,9 +28,6 @@ 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)):

View File

@@ -131,10 +131,6 @@ else()
endif()
target_link_libraries(xrpl_libs INTERFACE ${nudb})
if(benchmark)
find_package(benchmark REQUIRED)
endif()
if(coverage)
include(XrplCov)
endif()
@@ -149,7 +145,3 @@ if(tests)
include(CTest)
add_subdirectory(src/tests/libxrpl)
endif()
if(benchmark)
add_subdirectory(src/benchmarks/libxrpl)
endif()

View File

@@ -83,11 +83,8 @@ If you create new source files, they must be organized as follows:
`src/libxrpl`.
- All other non-test files must go under `src/xrpld`.
- All test source files must go under `src/test`.
- All benchmark source files must go under `src/benchmarks`.
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.
The source must be formatted according to the style guide below.
Header includes must be [levelized](.github/scripts/levelization).
@@ -215,61 +212,13 @@ 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`, 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:
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:
```
// clang-format off
@@ -277,21 +226,9 @@ like this:
// clang-format on
```
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 <file>...`
You can format individual files in place by running `clang-format -i <file>...`
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:
@@ -302,6 +239,13 @@ 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).
@@ -323,7 +267,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`](#pre-commit-hooks) hooks, you can run clang-tidy on your staged files using:
If you have already installed the pre-commit hooks (see above), you can run clang-tidy on your staged files using:
```
TIDY=1 pre-commit run clang-tidy

View File

@@ -110,23 +110,6 @@ 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 <sub>`), which
# resolves the matching `cargo-<sub>` 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

View File

@@ -1,46 +1,27 @@
#!/usr/bin/env python3
"""Pre-commit hook that runs clang-tidy on staged files using run-clang-tidy.
"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy.
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/<module>/...), 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.
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`.
"""
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"}
# 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):
def find_run_clang_tidy() -> str | None:
for candidate in (f"run-clang-tidy-{CLANG_TIDY_VERSION}", "run-clang-tidy"):
if path := shutil.which(candidate):
return path
return None
@@ -54,43 +35,23 @@ 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"],
@@ -98,29 +59,6 @@ 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(
@@ -130,23 +68,11 @@ def main():
)
return 1
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
result = subprocess.run(
[run_clang_tidy, "-quiet", "-p", str(build_dir), "-fix", "-allow-no-checks"]
+ files
)
return result.returncode
if __name__ == "__main__":

View File

@@ -1,36 +0,0 @@
include(isolate_headers)
# Define a benchmark executable for the module `name`.
#
# This follows the same general pattern as other build helpers in this repo
# (e.g. `add_module`): create a target and isolate headers, but here the target
# is a benchmark executable and no `add_test(...)` is registered.
#
# `isolate_headers` exposes only `${CMAKE_CURRENT_SOURCE_DIR}/${name}` on the
# include path, rooted at `src`, so a benchmark's own headers are reached as
# `<benchmarks/.../${name}/...>` and nothing else in the tree leaks in.
function(xrpl_add_benchmark name)
set(target ${PROJECT_NAME}.bench.${name})
file(
GLOB_RECURSE sources
CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/${name}/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp"
)
add_executable(${target} ${ARGN} ${sources})
# Benchmark sources register cases through Google Benchmark's static
# registrars (anonymous-namespace lambdas). Merging several such files into
# one unity translation unit collides those internal-linkage entities, so
# keep benchmarks out of the unity build - mirroring xrpl.libpb in
# XrplCore.cmake. Each file compiles fine on its own.
set_target_properties(${target} PROPERTIES UNITY_BUILD OFF)
isolate_headers(
${target}
"${CMAKE_SOURCE_DIR}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/${name}"
PRIVATE
)
endfunction()

View File

@@ -30,8 +30,6 @@ if(tests)
endif()
endif()
option(benchmark "Build benchmarks" ON)
# Enabled by default so every header is compiled on its own as the main file of
# its own compile_commands.json entry - this is what lets clang-tidy (and clangd
# and IDEs) analyse a header's own includes directly. The per-header objects are

View File

@@ -10,23 +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#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355",
"mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828",
"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#f729f6d75992d20f9c72828e9142d62f%1783945160.094135",
"grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616",
"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",
"benchmark/1.9.5#b885dc73ad67b40a55d45684d1c88ad1%1782736613.864841",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
@@ -39,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#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"python_requires": [],
"overrides": {

View File

@@ -15,7 +15,6 @@ class Xrpl(ConanFile):
settings = "os", "compiler", "build_type", "arch"
options = {
"assertions": [True, False],
"benchmark": [True, False],
"coverage": [True, False],
"fPIC": [True, False],
"jemalloc": [True, False],
@@ -47,7 +46,6 @@ class Xrpl(ConanFile):
default_options = {
"assertions": False,
"benchmark": True,
"coverage": False,
"fPIC": True,
"jemalloc": False,
@@ -131,14 +129,12 @@ class Xrpl(ConanFile):
self.options["boost"].without_cobalt = True
def requirements(self):
if self.options.benchmark:
self.requires("benchmark/1.9.5")
self.requires("boost/1.91.0", force=True, transitive_headers=True)
self.requires("date/3.0.4", transitive_headers=True)
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-rc4", transitive_headers=True)
self.requires("mpt-crypto/0.4.0-rc2", transitive_headers=True)
self.requires("protobuf/6.33.5", force=True)
if self.options.rocksdb:
self.requires("rocksdb/10.5.1")
@@ -166,7 +162,6 @@ class Xrpl(ConanFile):
def generate(self):
tc = CMakeToolchain(self)
tc.variables["tests"] = self.options.tests
tc.variables["benchmark"] = self.options.benchmark
tc.variables["assert"] = self.options.assertions
tc.variables["coverage"] = self.options.coverage
tc.variables["jemalloc"] = self.options.jemalloc

View File

@@ -364,8 +364,6 @@ public:
static constexpr internalrep kMaxRep = std::numeric_limits<rep>::max();
static_assert(kMaxRep == 9'223'372'036'854'775'807);
static_assert(-kMaxRep == std::numeric_limits<rep>::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
@@ -593,13 +591,6 @@ public:
std::pair<T, int>
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<std::int64_t>::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
@@ -654,6 +645,13 @@ 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<std::int64_t>::min() flirts with
// UB, and can vary across compilers.
static internalrep
externalToInternal(rep mantissa);
};
constexpr Number::Number(bool negative, internalrep mantissa, int exponent, Unchecked) noexcept

View File

@@ -3,9 +3,6 @@
#include <xrpl/basics/IntrusivePointer.ipp>
#include <xrpl/basics/Log.h> // IWYU pragma: keep
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/scope.h>
#include <algorithm>
namespace xrpl {
@@ -604,42 +601,8 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
std::vector<key_type> v;
{
// 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");
std::scoped_lock const lock(mutex_);
v.reserve(cache_.size());
for (auto const& _ : cache_)
v.push_back(_.first);
}

View File

@@ -308,9 +308,7 @@ public:
XRPL_ASSERT(
c.size() * sizeof(typename Container::value_type) == size(),
"xrpl::BaseUInt::fromRaw(Container auto) : input size match");
std::size_t const canCopy =
std::min(size(), c.size() * sizeof(typename Container::value_type));
std::memcpy(result.data_.data(), c.data(), canCopy);
std::memcpy(result.data_.data(), c.data(), size());
return result;
}
@@ -324,11 +322,7 @@ public:
XRPL_ASSERT(
c.size() * sizeof(typename Container::value_type) == size(),
"xrpl::BaseUInt::operator=(Container auto) : input size match");
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);
std::memcpy(data_.data(), c.data(), size());
return *this;
}

View File

@@ -358,13 +358,12 @@ template <class = void>
bool
tokenInList(boost::string_ref const& value, boost::string_ref const& token)
{
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); });
for (auto const& item : makeList(value))
{
if (ciEqual(item, token))
return true;
}
return false;
}
template <bool IsRequest, class Body, class Fields>

View File

@@ -7,6 +7,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
#include <xrpl/protocol/Protocol.h>
@@ -47,11 +48,9 @@ namespace xrpl {
*/
[[nodiscard]] TER
canApplyToBrokerCover(
ReadView const& view,
SLE::const_ref sleBroker,
LoanBrokerEntry<ReadView> const& sleBroker,
Asset const& vaultAsset,
STAmount const& amount,
beast::Journal j,
std::string_view logPrefix);
// Lending protocol has dependencies, so capture them here.
@@ -217,7 +216,7 @@ adjustImpreciseNumber(
}
inline int
getAssetsTotalScale(SLE::const_ref vaultSle)
getAssetsTotalScale(VaultEntry<ReadView> const& vaultSle)
{
if (!vaultSle)
return Number::kMinExponent - 1; // LCOV_EXCL_LINE
@@ -228,7 +227,10 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
// DebtTotal is a broker-level aggregate maintained at vault scale, so the
// rounding must also use vault scale — never an individual loan's scale.
inline Number
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::const_ref vaultSle)
minimumBrokerCover(
Number const& debtTotal,
TenthBips32 coverRateMinimum,
VaultEntry<ReadView> const& vaultSle)
{
XRPL_ASSERT(
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
@@ -263,11 +265,10 @@ constructLoanState(
Number const& principalOutstanding,
Number const& managementFeeOutstanding);
// Overload of constructLoanState() that reads the three tracked fields
// directly from a Loan ledger object, which always holds rounded values,
// rather than taking them as separate Number arguments.
// Constructs a valid LoanState object from a Loan object, which always has
// rounded values
LoanState
constructLoanState(SLE::const_ref loan);
constructRoundedLoanState(LoanEntry<ReadView> const& loan);
Number
computeManagementFee(
@@ -551,11 +552,9 @@ enum class LoanPaymentType { Regular = 0, Late, Full, Overpayment };
std::expected<LoanPaymentParts, TER>
loanMakePayment(
Asset const& asset,
ApplyView& view,
SLE::ref loan,
SLE::const_ref brokerSle,
LoanEntry<ApplyView>& loan,
LoanBrokerEntry<ReadView> const& brokerSle,
STAmount const& amount,
LoanPaymentType const paymentType,
beast::Journal j);
LoanPaymentType const paymentType);
} // namespace xrpl

View File

@@ -1,5 +1,9 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/helpers/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h> // IWYU pragma: keep
#include <xrpl/protocol/STLedgerEntry.h>
@@ -28,4 +32,35 @@ calculateOracleReserve(SLE::const_ref oracleSle)
return calculateOracleReserve(oracleSle->getFieldArray(sfPriceDataSeries));
}
template <typename ViewT>
class OracleEntry : public SLEBase<ViewT>
{
public:
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using SLEBase<ViewT>::SLEBase;
explicit OracleEntry(
AccountID const& account,
std::uint32_t documentID,
SLEBase<ViewT>::view_ref_type view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: SLEBase<ViewT>(keylet::oracle(account, documentID), view, j)
{
}
[[nodiscard]] SField const&
ownerField() const override
{
return sfOwner;
}
// An Oracle with more than five price-data pairs occupies two reserve slots.
[[nodiscard]] std::uint32_t
reserveCount() const override
{
return calculateOracleReserve(this->sle()->getFieldArray(sfPriceDataSeries));
}
};
} // namespace xrpl

View File

@@ -3,6 +3,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
@@ -23,7 +24,11 @@ namespace xrpl {
* fails; tefINTERNAL if the source account SLE cannot be found.
*/
TER
closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j);
closeChannel(
PayChannelEntry<ApplyView>& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j);
/**
* Add two uint32_t values with saturation at UINT32_MAX.

View File

@@ -0,0 +1,599 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h> // increaseOwnerCount, accountReserve
#include <xrpl/ledger/helpers/DirectoryHelpers.h> // describeOwnerDir
#include <xrpl/ledger/helpers/SponsorHelpers.h> // addSponsorToLedgerEntry, checkReserve
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h> // keylet::account, keylet::ownerDir
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <concepts>
#include <cstdint>
#include <memory>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl {
// Concept to distinguish read-only vs writable view types
template <typename V>
concept WritableView = std::derived_from<V, ApplyView>;
/**
* Describes one directory a ledger entry is linked into, for create()/destroy().
*
* @param owner the account whose owner directory this is (the directory is
* keylet::ownerDir(owner)).
* @param node the field on the entry holding this directory's page index
* (sfOwnerNode, sfDestinationNode, sfSubjectNode, ...).
* @param countsToward whether linking here consumes `owner`'s OwnerCount /
* reserve (true for the owning account, false for
* auxiliary links such as a destination's tracking
* directory).
*/
struct OwnerDirLink
{
AccountID owner;
SField const* node{} {};
bool countsToward{} {};
};
/**
* View-parameterized base class for all ledger entry wrappers.
*
* SLEBase<ReadView> — read-only: holds shared_ptr<SLE const> + ReadView const&
* SLEBase<ApplyView> — writable: holds shared_ptr<SLE> + ApplyView& + Keylet,
* plus insert/update/erase operations
*
* Write-only members are gated by `requires` clauses, providing compile-time
* guarantees that read-only wrappers cannot mutate state.
*
* Derived classes should provide domain-specific accessors that hide
* implementation details of the underlying ledger entry format.
*/
template <typename ViewT>
class SLEBase
{
public:
static constexpr bool kIsWritable = WritableView<ViewT>;
// SLE pointer type: mutable for writable views, const for read-only
using sle_ptr_type = std::conditional_t<kIsWritable, std::shared_ptr<SLE>, SLE::const_pointer>;
// View reference type: ApplyView& for writable, ReadView const& for read-only
using view_ref_type = std::conditional_t<kIsWritable, ApplyView&, ReadView const&>;
virtual ~SLEBase() = default;
SLEBase(SLEBase const&) = default;
SLEBase(SLEBase&&) = default;
SLEBase&
operator=(SLEBase const&) = delete;
SLEBase&
operator=(SLEBase&&) = delete;
SLEBase() = delete;
// --- Common interface (always available) ---
/**
* Returns true if the ledger entry exists
*/
[[nodiscard]] bool
exists() const
{
return sle_ != nullptr;
}
/**
* Explicit conversion to bool for convenient existence checking
*/
explicit
operator bool() const
{
return exists();
}
/**
* Returns the underlying SLE for read access
*/
[[nodiscard]] SLE::const_pointer
sle() const
{
return sle_;
}
/**
* Returns the read view (always available; ApplyView inherits ReadView)
*/
[[nodiscard]] ReadView const&
readView() const
{
return view_;
}
/**
* Const dereference operators (always available)
*/
STLedgerEntry const*
operator->() const
{
XRPL_ASSERT(exists(), "xrpl::SLEBase::operator-> : exists");
return sle_.get();
}
STLedgerEntry const&
operator*() const
{
XRPL_ASSERT(exists(), "xrpl::SLEBase::operator* : exists");
return *sle_;
}
// --- Writable interface (compile-time gated) ---
/**
* Returns a mutable SLE for write operations
*/
[[nodiscard]] sle_ptr_type const&
mutableSle() const
requires kIsWritable
{
return sle_;
}
/**
* Returns true if this wrapper supports write operations
*/
[[nodiscard]] bool
canModify() const
requires kIsWritable
{
return sle_ != nullptr;
}
/**
* Returns the apply view for write operations
*/
[[nodiscard]] ApplyView&
applyView() const
requires kIsWritable
{
return view_;
}
/**
* Mutable dereference operators
*/
STLedgerEntry*
operator->()
requires kIsWritable
{
XRPL_ASSERT(canModify(), "xrpl::SLEBase::operator-> : can modify");
return sle_.get();
}
STLedgerEntry&
operator*()
requires kIsWritable
{
XRPL_ASSERT(canModify(), "xrpl::SLEBase::operator* : can modify");
return *sle_;
}
void
insert()
requires kIsWritable
{
XRPL_ASSERT(canModify(), "xrpl::SLEBase::insert : can modify");
view_.insert(sle_);
}
void
erase()
requires kIsWritable
{
XRPL_ASSERT(canModify(), "xrpl::SLEBase::erase : can modify");
view_.erase(sle_);
}
void
update()
requires kIsWritable
{
XRPL_ASSERT(canModify(), "xrpl::SLEBase::update : can modify");
view_.update(sle_);
}
void
newSLE()
requires kIsWritable
{
XRPL_ASSERT(!canModify(), "xrpl::SLEBase::newSLE : no existing SLE");
sle_ = std::make_shared<SLE>(key_);
}
/**
* The field holding the account that owns this entry (sfAccount, sfOwner,
* sfIssuer, ...).
*
* Single-directory entry types override this to name their owning-account
* field; the default ownerDirs() then links a single owner directory
* keyed on it. Types that live in multiple directories override
* ownerDirs() directly instead. The generic base has no owner.
*/
[[nodiscard]] virtual SField const&
ownerField() const
{
UNREACHABLE("xrpl::SLEBase::ownerField : type does not define an owner field");
return sfAccount; // unreachable; present only to satisfy the return type
}
/**
* The directories this entry is linked into, and where each stores its
* page index.
*
* Default: a single owner directory for ownerField()'s account, recorded
* in sfOwnerNode, counting toward that account's reserve. Types linked
* into more than one directory (e.g. Check/PayChannel/Escrow's
* destination directory, Credential's subject directory) override this to
* list them; only links with `countsToward == true` consume an
* OwnerCount/reserve slot.
*/
[[nodiscard]] virtual std::vector<OwnerDirLink>
ownerDirs() const
{
return {{sle_->getAccountID(ownerField()), &sfOwnerNode, /*countsToward=*/true}};
}
/**
* Number of OwnerCount/reserve slots a counted link consumes (default 1).
* Types whose footprint scales with their contents (e.g. Oracle)
* override.
*/
[[nodiscard]] virtual std::uint32_t
reserveCount() const
{
return 1;
}
/**
* Link this entry into each listed owner directory (keylet::ownerDir),
* and record the assigned page in the link's node field.
*
* This is the shared directory-linking primitive used by create() and by
* the bespoke create() overrides of entries that live in several owner
* directories. The OwnerDirLink::countsToward flag is ignored here —
* reserve and OwnerCount accounting is the caller's responsibility.
* Returns tecDIR_FULL if any directory is full.
*/
[[nodiscard]] TER
linkOwnerDirs(std::vector<OwnerDirLink> const& dirs)
requires kIsWritable
{
for (auto const& d : dirs)
{
auto const page =
view_.dirInsert(keylet::ownerDir(d.owner), sle_->key(), describeOwnerDir(d.owner));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
sle_->setFieldU64(*d.node, *page);
}
return tesSUCCESS;
}
/**
* Unlink this entry from each listed owner directory, using the page
* stored in each link's node field. Inverse of linkOwnerDirs(). Returns
* tefBAD_LEDGER if any removal fails.
*/
[[nodiscard]] TER
unlinkOwnerDirs(std::vector<OwnerDirLink> const& dirs)
requires kIsWritable
{
for (auto const& d : dirs)
{
{
if (!view_.dirRemove(
keylet::ownerDir(d.owner),
sle_->getFieldU64(*d.node),
sle_->key(),
/*keepRoot=*/false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
}
return tesSUCCESS;
}
/**
* Link a freshly-populated entry into its owner directories and insert
* it.
*
* Handles the create-time boilerplate shared by owned ledger entries:
* 1. reserve check against `ownerReserveBalance` (the owner's pre-fee
* XRP balance — pass the transactor's preFeeBalance_ when the entry
* is owned by the transaction submitter). Pass std::nullopt to skip
* the check entirely, e.g. for entries an internal caller installs
* on a pseudo-account (VaultCreate),
* 2. link into each ownerDirs() directory, recording the page in its
* node field,
* 3. bump the OwnerCount of each counted owner by reserveCount(),
* 4. insert the entry into the view.
*
* The caller must have already called newSLE() and populated the entry's
* domain fields (in particular the account fields ownerDirs() reads).
*/
[[nodiscard]] TER
create(std::optional<XRPAmount> ownerReserveBalance)
requires kIsWritable
{
XRPL_ASSERT(canModify(), "xrpl::SLEBase::create : can modify");
auto const dirs = ownerDirs();
Adjustment const adj{.ownerCountDelta = static_cast<std::int32_t>(reserveCount())};
// The reserve sponsor covers only the transaction submitter's own
// objects, so it is resolved from the counted owner (yielding no sponsor
// when the owner is a pseudo-account or otherwise not the submitter).
// Null unless the wrapper was built with an ApplyViewContext (a create
// path) and the transaction carries a reserve sponsor for the owner.
SLE::pointer sponsorSle;
for (auto const& d : dirs)
{
if (!d.countsToward)
continue;
auto const ownerSle = view_.peek(keylet::account(d.owner));
if (!ownerSle)
return tecNO_ENTRY; // LCOV_EXCL_LINE
if (tx_ != nullptr != nullptr)
{
auto const sponsorExp =
getEffectiveTxReserveSponsor(ApplyViewContext{view_, *tx_}, ownerSle);
if (!sponsorExp)
return sponsorExp.error(); // LCOV_EXCL_LINE
sponsorSle = *sponsorExp;
}
if (!ownerReserveBalance)
continue;
// Route the reserve check through checkReserve() so a reserve
// sponsor is honored; otherwise fall back to the account's own
// reserve.
if (tx_ != nullptr != nullptr)
{
if (auto const ret = checkReserve(
ApplyViewContext{view_, *tx_},
ownerSle,
*ownerReserveBalance,
sponsorSle,
adj,
j_);
!isTesSuccess(ret))
return ret;
}
else if (*ownerReserveBalance < accountReserve(view_, ownerSle, j_, adj))
{
{
{
{
return tecINSUFFICIENT_RESERVE;
}
}
}
}
}
if (auto const ter = linkOwnerDirs(dirs); !isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
for (auto const& d : dirs)
{
{
if (d.countsToward)
{
{
increaseOwnerCount(
view_,
view_.peek(keylet::account(d.owner)),
sponsorSle,
reserveCount(),
j_);
}
}
}
}
// Stamp the reserve sponsor (if any) onto the new entry so that a later
// delete refunds the sponsor rather than the owner. A no-op when
// sponsorSle is null.
if (tx_ != nullptr != nullptr)
addSponsorToLedgerEntry(sle_, sponsorSle);
view_.insert(sle_);
return tesSUCCESS;
}
/**
* Unlink an owned entry from its directories and erase it.
*
* Inverse of create(): removes the entry from each ownerDirs() directory
* (using the stored node fields), decrements each counted owner's
* OwnerCount by reserveCount(), and erases the entry.
*/
[[nodiscard]] TER
destroy()
requires kIsWritable
{
XRPL_ASSERT(canModify(), "xrpl::SLEBase::destroy : can modify");
auto const dirs = ownerDirs();
if (auto const ter = unlinkOwnerDirs(dirs); !isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
// decreaseOwnerCountForObject derives the reserve sponsor (if any) from
// the entry's sfSponsor field, refunding the sponsor rather than the
// owner when the object was sponsored.
for (auto const& d : dirs)
{
{
if (d.countsToward)
{
{
if (auto ownerSle = view_.peek(keylet::account(d.owner)))
decreaseOwnerCountForObject(view_, ownerSle, sle_, reserveCount(), j_);
}
}
}
}
view_.erase(sle_);
return tesSUCCESS;
}
[[nodiscard]] beast::Journal
journal() const
{
return j_;
}
// --- Constructors that adopt/resolve an SLE (public so the ReadOnlySLE /
// WritableSLE aliases and the per-type wrappers can be built directly
// from an already-fetched SLE or a keylet). ---
/**
* Constructor for read-only context
*/
explicit SLEBase(
SLE::const_pointer sle,
ReadView const& view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires(!kIsWritable)
: view_(view), sle_(std::move(sle)), j_(j)
{
}
/**
* Constructor for read-only context (read from view by keylet)
*/
explicit SLEBase(
Keylet const& key,
ReadView const& view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires(!kIsWritable)
: view_(view), sle_(view.read(key)), j_(j)
{
}
/**
* Converting constructor: writable → read-only.
* Enables implicit conversion from SLEBase<ApplyView> to
* SLEBase<ReadView>, so functions taking ReadOnlySLE const& can accept
* WritableSLE.
*/
template <WritableView OtherViewT>
SLEBase(SLEBase<OtherViewT> const& other)
requires(!kIsWritable)
: view_(other.readView()), sle_(other.sle()), j_(other.journal())
{
}
/**
* Constructor for writable context (from existing SLE)
*/
explicit SLEBase(
std::shared_ptr<SLE> sle,
ApplyView& view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires kIsWritable
: view_(view)
, key_(sle ? Keylet(sle->getType(), sle->key()) : Keylet(ltANY, uint256{}))
, sle_(std::move(sle))
, j_(j)
{
}
/**
* Constructor for writable context (peek from view by keylet)
*/
explicit SLEBase(
Keylet const& key,
ApplyView& view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires kIsWritable
: view_(view), key_(key), sle_(view_.peek(key)), j_(j)
{
}
/**
* Constructor for writable context carrying the applying transaction
* (from existing SLE). Providing the ApplyViewContext lets create()
* perform reserve-sponsorship-aware accounting.
*/
explicit SLEBase(
std::shared_ptr<SLE> sle,
ApplyViewContext ctx,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires kIsWritable
: view_(ctx.view)
, key_(sle ? Keylet(sle->getType(), sle->key()) : Keylet(ltANY, uint256{}))
, sle_(std::move(sle))
, j_(j)
, tx_(&ctx.tx)
{
}
/**
* Constructor for writable context carrying the applying transaction
* (peek from view by keylet).
*/
explicit SLEBase(
Keylet const& key,
ApplyViewContext ctx,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires kIsWritable
: view_(ctx.view), key_(key), sle_(view_.peek(key)), j_(j), tx_(&ctx.tx)
{
}
protected:
view_ref_type view_{};
// Keylet is only meaningful for writable views, but we conditionally
// include it to avoid wasting space in read-only wrappers.
struct Empty
{
};
[[no_unique_address]]
std::conditional_t<kIsWritable, Keylet, Empty> key_{};
sle_ptr_type sle_{};
beast::Journal j_;
// The applying transaction, when the wrapper was constructed from an
// ApplyViewContext. Only meaningful for writable wrappers on a create path;
// null otherwise. Enables reserve-sponsorship-aware create().
STTx const* tx_ = nullptr;
};
/**
* Generic (any-entry-type) SLE handles.
*
* Use these when the concrete ledger entry type is not known at a given site;
* otherwise prefer the per-type wrappers in SLEWrappers.h.
*
* SLE::const_pointer / SLE::const_ref -> ReadOnlySLE
* SLE::pointer / SLE::ref -> WritableSLE
*/
using ReadOnlySLE = SLEBase<ReadView>;
using WritableSLE = SLEBase<ApplyView>;
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
#pragma once
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <optional>
@@ -21,7 +21,10 @@ namespace xrpl {
* @return The number of shares, or nullopt on error.
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& assets);
assetsToSharesDeposit(
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& assets);
/**
* From the perspective of a vault, return the number of assets to take from
@@ -35,7 +38,10 @@ assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
* @return The number of assets, or nullopt on error.
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares);
sharesToAssetsDeposit(
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& shares);
/**
* Controls whether to truncate shares instead of rounding.
@@ -68,8 +74,8 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesWithdraw(
SLE::const_ref vault,
SLE::const_ref issuance,
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& assets,
TruncateShares truncate = TruncateShares::No,
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
@@ -89,8 +95,8 @@ assetsToSharesWithdraw(
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsWithdraw(
SLE::const_ref vault,
SLE::const_ref issuance,
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& shares,
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
@@ -105,6 +111,9 @@ sharesToAssetsWithdraw(
* both the share MPTID and the outstanding-amount total.
*/
[[nodiscard]] bool
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance);
isSoleShareholder(
ReadView const& view,
AccountID const& account,
MPTokenIssuanceEntry<ReadView> const& issuance);
} // namespace xrpl

View File

@@ -41,19 +41,6 @@ public:
std::unique_ptr<NodeStore::Backend>&& newBackend,
std::function<void(std::string const& writableName, std::string const& archiveName)> 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

View File

@@ -9,7 +9,6 @@
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
@@ -70,22 +69,11 @@ public:
void
sweep() override;
void
setRotationInFlight(bool inFlight) override;
private:
std::shared_ptr<Backend> writableBackend_;
std::shared_ptr<Backend> 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<bool> rotationInFlight_{false};
std::atomic<std::uint64_t> copyForwardCount_{0};
std::shared_ptr<NodeObject>
fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate)
override;

View File

@@ -229,6 +229,13 @@ public:
[[nodiscard]] AccountID
getAccountID(SField const& field) const;
/**
* The account responsible for the authorization: the delegate when
* sfDelegate is present, otherwise the account.
*/
[[nodiscard]] AccountID
getInitiator() const;
[[nodiscard]] Blob
getFieldVL(SField const& field) const;
[[nodiscard]] STAmount const&

View File

@@ -142,26 +142,9 @@ public:
TxnSql status,
std::string const& escapedMetaData) const;
/**
* The IDs of the inner transactions of a Batch.
*/
[[nodiscard]] std::vector<uint256>
[[nodiscard]] std::vector<uint256> const&
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<std::shared_ptr<STTx const>> 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;
@@ -183,16 +166,13 @@ private:
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
checkBatchSingleSign(STObject const& batchSigner) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const;
checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const;
void
buildBatchTxns();
buildBatchTxnIds();
STBase*
copy(std::size_t n, void* buf) const override;
@@ -200,11 +180,11 @@ private:
move(std::size_t n, void* buf) override;
friend class detail::STVar;
std::optional<std::vector<std::shared_ptr<STTx const>>> batchTxns_;
std::optional<std::vector<uint256>> batchTxnIds_;
};
bool
passesLocalChecks(STTx const& tx, std::string&);
passesLocalChecks(STObject const& st, std::string&);
/**
* Sterilize a transaction.

View File

@@ -110,7 +110,6 @@ 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
@@ -134,7 +133,6 @@ 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
@@ -193,7 +191,6 @@ 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
@@ -273,7 +270,6 @@ 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

View File

@@ -46,22 +46,6 @@ 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<AccountID> counterparty;
};
class RelationalDatabase
{
public:
@@ -98,7 +82,6 @@ public:
std::optional<AccountTxMarker> marker;
std::uint32_t limit = 0;
bool bAdmin = false;
std::optional<DelegateFilter> delegate;
};
using AccountTx = std::pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>;
@@ -118,7 +101,6 @@ public:
bool forward = false;
uint32_t limit = 0;
std::optional<AccountTxMarker> marker;
std::optional<DelegateFilter> delegate;
};
struct AccountTxResult
@@ -127,7 +109,6 @@ public:
LedgerRange ledgerRange{};
uint32_t limit = 0;
std::optional<AccountTxMarker> marker;
std::optional<DelegateFilter> delegate;
};
virtual ~RelationalDatabase() = default;

View File

@@ -13,7 +13,7 @@
#include <soci/session.h>
#include <memory>
#ifdef __clang__
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
#endif
@@ -120,6 +120,6 @@ makeCheckpointer(std::uintptr_t id, std::weak_ptr<soci::session>, JobQueue&, Ser
} // namespace xrpl
#ifdef __clang__
#if defined(__clang__)
#pragma clang diagnostic pop
#endif

View File

@@ -71,18 +71,9 @@ 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. `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.
* @param isDelete true if the SLE is being deleted
* @param before ledger entry before modification by the transaction
* @param after ledger entry after modification by the transaction
*/
void
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
@@ -325,26 +316,17 @@ public:
};
/**
* @brief Invariant: Token holder's trustline/MPT balance cannot be invalid
* after Clawback.
* @brief Invariant: Token holder's trustline balance cannot be negative after
* Clawback.
*
* We iterate all the trust lines affected by this transaction and ensure
* that no more than one trustline is modified, and also holder's balance is
* non-negative. When featureMPTokensV2 is enabled, also verify the holder's
* raw trustline/MPToken balance decreased by the clawed amount.
* non-negative.
*/
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
@@ -458,7 +440,7 @@ using InvariantChecks = std::tuple<
ValidLoan,
ValidVault,
ValidConfidentialMPToken,
ValidMPTBalanceChanges,
ValidMPTPayment,
ValidAmounts,
ValidMPTTransfer,
ObjectHasPseudoAccount,

View File

@@ -87,7 +87,7 @@ public:
* OutstandingAmount after application equals OutstandingAmount before
* application plus the net holder balance delta.
*/
class ValidMPTBalanceChanges
class ValidMPTPayment
{
enum class Order { Before = 0, After = 1 };
struct MPTData

View File

@@ -2,7 +2,9 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
@@ -169,7 +171,7 @@ public:
static std::pair<TER, bool>
deleteAMMAccountIfEmpty(
Sandbox& sb,
SLE::pointer const ammSle,
AMMEntry<ApplyView>& ammSle,
STAmount const& lpTokenBalance,
Asset const& asset1,
Asset const& asset2,

View File

@@ -4,6 +4,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
@@ -41,34 +42,28 @@ public:
*/
static TER
defaultLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
LoanEntry<ApplyView>& loanSle,
LoanBrokerEntry<ApplyView>& brokerSle,
VaultEntry<ApplyView>& vaultSle,
Asset const& vaultAsset);
/**
* Helper function that might be needed by other transactors
*/
static TER
impairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
LoanEntry<ApplyView>& loanSle,
VaultEntry<ApplyView>& vaultSle,
Asset const& vaultAsset);
/**
* Helper function that might be needed by other transactors
*/
[[nodiscard]] static TER
unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
LoanEntry<ApplyView>& loanSle,
VaultEntry<ApplyView>& vaultSle,
Asset const& vaultAsset);
TER
doApply() override;

View File

@@ -3,7 +3,9 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
@@ -21,10 +23,10 @@ private:
pay(AccountID const& from, AccountID const& to, STAmount const& amount);
TER
acceptOffer(SLE::ref offer);
acceptOffer(NFTokenOfferEntry<ApplyView> const& offer);
TER
bridgeOffers(SLE::ref buy, SLE::ref sell);
bridgeOffers(NFTokenOfferEntry<ApplyView> const& buy, NFTokenOfferEntry<ApplyView> const& sell);
TER
transferNFToken(AccountID const& buyer, AccountID const& seller, uint256 const& nfTokenID);

View File

@@ -12,7 +12,6 @@
#include <array>
#include <cstdint>
#include <optional>
namespace xrpl {
@@ -40,9 +39,6 @@ public:
static NotTEC
checkSign(PreclaimContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
@@ -80,10 +76,6 @@ private:
// only be reached through Batch::checkSign.
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
// nullopt on overflow or oversized signer arrays.
static std::optional<XRPAmount>
calculateBaseFeeImpl(ReadView const& view, STTx const& tx);
};
} // namespace xrpl

View File

@@ -2,7 +2,9 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
@@ -48,8 +50,8 @@ public:
private:
std::expected<std::pair<STAmount, STAmount>, TER>
assetsToClawback(
SLE::ref vault,
SLE::const_ref sleShareIssuance,
VaultEntry<ApplyView>& vault,
MPTokenIssuanceEntry<ReadView> const& sleShareIssuance,
AccountID const& holder,
STAmount const& clawbackAmount);
};

View File

@@ -75,13 +75,9 @@ 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/ /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
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
# Tester: start from a clean BASE_IMAGE, install sanitizer runtime libraries,
# and run the compiled test binaries to verify they execute correctly.
@@ -98,18 +94,15 @@ 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/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
COPY nix/docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh
COPY --from=final /tmp/bins /tmp/bins
RUN <<EOF
if echo "${BASE_IMAGE}" | grep -qiE 'nixos'; then
echo "Skipping runnning binaries on NixOS."
else
/tmp/install-sanitizer-libs.sh
/tmp/test_files/cpp/run-binaries.sh /tmp/cpp-bins
/tmp/test_files/rust/run-binaries.sh /tmp/rust-bins
/tmp/run-test-binaries.sh /tmp/bins
fi
touch /tmp/tests-passed
EOF

View File

@@ -42,12 +42,9 @@ work without `ca-certificates` being installed in the base image.
- installs the dynamic linker if the base image lacks one (see
[How libc is handled](#how-libc-is-handled)),
- runs [`bin/check-tools.sh`](../../bin/check-tools.sh) to verify every
expected tool is present and runnable.
expected tool is present and runnable, and
- compiles the C++ test programs in
[`test_files/cpp/sources/`](./test_files/cpp/sources) with both `g++` and
`clang++`, and sanitizers, and
- compiles the Rust test programs in
[`test_files/rust/sources/`](./test_files/rust/sources) with `rustc`.
[`test_files/`](./test_files) with both `g++` and `clang++`, and sanitizers.
3. **`tester`** — Start again from a clean `BASE_IMAGE` (no Nix toolchain),
install only the sanitizer runtime libraries
([`install-sanitizer-libs.sh`](./install-sanitizer-libs.sh)), and run the
@@ -76,12 +73,11 @@ toolchain being present at runtime. Two pieces make that work:
[`loader-path.sh`](./loader-path.sh) reports the expected loader path for the
current architecture, so we can patch the binaries to use the correct loader.
The build then verifies all of this end to end: the C++ test programs in
`test_files/cpp/sources/` (a regular binary plus ASan/TSan/UBSan variants) and
the Rust test programs in `test_files/rust/sources/` (a hello binary plus panic
and overflow-check variants) are compiled in `final`, their `PT_INTERP` is
patched to the target loader, and they are run in the clean `tester` stage to
confirm each emits the expected diagnostic on a stock base image.
The build then verifies all of this end to end: the test programs in
`test_files/` (a regular binary plus ASan/TSan/UBSan variants) are compiled in
`final`, their `PT_INTERP` is patched to the target loader, and they are run in
the clean `tester` stage to confirm each emits the expected sanitizer
diagnostic on a stock base image.
## Files
@@ -89,7 +85,6 @@ confirm each emits the expected diagnostic on a stock base image.
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. |
| [`./test_files/rust/`](./test_files/rust) | Rust rustc smoke test: sources + compile/run scripts. |
| [`./test_files/`](./test_files) | C++ sources and scripts to compile and run the sanitizer smoke tests. |
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |

View File

@@ -1,45 +0,0 @@
#!/bin/bash
# Compile all Rust test binaries during the Docker image build.
# Each binary has the target system's ELF PT_INTERP (dynamic-linker path)
# baked in so it can run on the (potentially minimal) final BASE_IMAGE.
set -eo pipefail
src_dir="${1:?usage: $0 <src_dir> <dst_dir>}"
dst_dir="${2:?usage: $0 <src_dir> <dst_dir>}"
loader="$(/tmp/loader-path.sh)"
mkdir -p "${dst_dir}"
function compile() {
local name="${1}"
local extra_flags="${2:-}"
local src="${src_dir}/${name}.rs"
local binary="${dst_dir}/${name}"
echo "=== Compiling ${name} with rustc ==="
# -O optimizes (opt-level 2); Rust statically links its own std, so the
# only dynamic dependency left is the system glibc (+ libgcc_s), exactly
# like the C++ binaries.
local compile_cmd="rustc --edition 2021 -O -g ${extra_flags} \
${src} -o ${binary}"
echo "Compile cmd: ${compile_cmd}"
eval "${compile_cmd}"
echo "=== Patching ${binary} to use ${loader} as PT_INTERP ==="
local patch_cmd="patchelf --set-interpreter ${loader} --remove-rpath ${binary}"
echo "Patch cmd: ${patch_cmd}"
eval "${patch_cmd}"
}
# `-O` disables overflow checks by default, so `overflow` re-enables them
# explicitly to exercise the runtime overflow check.
compile hello
compile panic
compile overflow "-C overflow-checks=on"
echo "=== All binaries compiled ==="
ls -la "${dst_dir}"

View File

@@ -1,74 +0,0 @@
#!/bin/bash
# Run pre-compiled Rust binaries and confirm each emits its expected diagnostic.
# Binaries must already exist in <bins_dir> as <name> for name in
# {hello,panic,overflow}.
set -eo pipefail
bins_dir="${1:?usage: $0 <bins_dir>}"
failed_binaries=()
# Run a binary and verify its exit code and output.
# Usage: run <binary> <expected_output> <expected_rc>
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

View File

@@ -1,16 +0,0 @@
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");
}

View File

@@ -1,13 +0,0 @@
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}");
}

View File

@@ -1,5 +0,0 @@
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");
}

View File

@@ -62,15 +62,5 @@ in
runClangTidy
vim
zip
# Rust packages
cargo
cargo-audit
cargo-llvm-cov
cargo-nextest
clippy
corrosion
rust-analyzer
rustc
rustfmt
];
}

View File

@@ -1,8 +0,0 @@
# Rust toolchain pin for rustup-based CI runners and local development.
# rustup reads this file and installs the pinned toolchain (see the
# prepare-runner action in XRPLF/actions, which runs `rustup toolchain install`).
# NOTE: the Nix CI image and development shell ignore this file; its rustc comes from flake.lock.
[toolchain]
channel = "1.95"
components = ["rustfmt", "clippy"]
profile = "minimal"

View File

@@ -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. invalidFieldError(StaticString)),
# When Clang inlines the StaticString overloads (e.g. invalid_field_error(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.

View File

@@ -1,22 +0,0 @@
include(XrplAddBenchmark)
# Benchmark requirements.
find_package(benchmark REQUIRED)
# Custom target for all benchmarks defined in this file.
add_custom_target(xrpl.benchmarks)
# Common library dependencies for every benchmark module. `benchmark_main`
# supplies a `main()` that parses the standard Google Benchmark CLI flags
# (`--benchmark_filter`, `--benchmark_format`, ...), so no per-module main.cpp
# is needed.
add_library(xrpl.imports.bench INTERFACE)
target_link_libraries(
xrpl.imports.bench
INTERFACE benchmark::benchmark_main xrpl.libxrpl
)
# One benchmark executable for each module.
xrpl_add_benchmark(nodestore)
target_link_libraries(xrpl.bench.nodestore PRIVATE xrpl.imports.bench)
add_dependencies(xrpl.benchmarks xrpl.bench.nodestore)

View File

@@ -1,329 +0,0 @@
#include <xrpl/nodestore/Backend.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Types.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/nodestore/NodeStoreBench.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::NodeStore {
namespace {
constexpr std::size_t kPoolSizes[] = {1000, 10000, 100000};
constexpr int kThreadCounts[] = {1, 4, 8};
constexpr std::size_t kBatchSize = 256;
constexpr std::string_view kNamePrefix = "BM_Backend_";
constexpr std::string_view kNameSeparator = "/";
struct RunState
{
std::unique_ptr<BackendHarness> harness;
Batch present; // prefix-1 objects, eligible to be stored
Batch recent; // prefix-1 objects in the "future" key space
std::vector<uint256> missing; // prefix-2 keys that are never stored
std::vector<std::size_t> shuffle; // [0, poolSize) permutation for random-like access
std::size_t avgPayload = 0; // mean getData().size() over `present`
void
release()
{
harness.reset();
Batch{}.swap(present);
Batch{}.swap(recent);
std::vector<uint256>{}.swap(missing);
std::vector<std::size_t>{}.swap(shuffle);
}
};
struct SetupContext
{
RunState& rs;
Backend& backend;
std::size_t poolSize;
};
struct IterateContext
{
RunState& rs;
Backend& backend;
std::size_t index;
std::size_t poolSize;
};
struct Workload
{
std::string_view name;
std::function<void(SetupContext const&)> setup;
std::function<void(IterateContext const&)> iterate;
bool reportBytes = false; // SetBytesProcessed from rs.avgPayload
bool clobber = true; // ClobberMemory after the loop (false for pure stores)
bool pinToPool = false; // pin iterations to one pool sweep instead of autotuning
};
// One store() per iteration. Iterations are pinned to one pool sweep (per
// thread) so the index never wraps past the pool - otherwise NuDB::doInsert
// swallows key_exists and the workload degenerates into duplicate-detection
// no-ops.
Workload const kInsert{
.name = "Insert",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
backend.store(rs.present[index % poolSize]);
},
.reportBytes = true,
.clobber = false,
.pinToPool = true,
};
// One fetch() of a present key (a hit) per iteration.
Workload const kFetch{
.name = "Fetch",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
prepopulate(ctx.backend, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
backend.fetch(rs.present[index % poolSize]->getHash(), &result);
benchmark::DoNotOptimize(result);
},
.reportBytes = true,
};
// One fetch() of a never-stored key (a miss); the backend is left empty.
Workload const kMissing{
.name = "Missing",
.setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); },
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
backend.fetch(rs.missing[index % poolSize], &result);
benchmark::DoNotOptimize(result);
},
};
// 80% hits / 20% misses. The fetch index comes from a shuffle table so access
// is random-like without per-iteration RNG cost; sequential `index % poolSize`
// would be artificially cache-friendly to RocksDB's block cache.
Workload const kMixed{
.name = "Mixed",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.missing = makeMissingKeys(ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/1);
prepopulate(ctx.backend, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
auto const pick = rs.shuffle[index % poolSize];
if (index % 5 == 0)
{
backend.fetch(rs.missing[pick], &result);
}
else
{
backend.fetch(rs.present[pick]->getHash(), &result);
}
benchmark::DoNotOptimize(result);
},
};
// An xrpld-like cycle: a hit, a maybe-miss recent fetch, and a store. The
// recent fetch uses the shuffle table (not `slot`) so it doesn't fetch the item
// it's about to store this iteration - which would give an all-miss-then-hit
// step instead of a smooth ramp. The store walks sequentially so each recent
// object is stored once.
Workload const kWork{
.name = "Work",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.recent = makePool(1, ctx.poolSize, ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/2);
prepopulate(ctx.backend, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
auto const slot = index % poolSize;
auto const pick = rs.shuffle[slot];
std::shared_ptr<NodeObject> historical;
backend.fetch(rs.present[pick]->getHash(), &historical);
benchmark::DoNotOptimize(historical);
std::shared_ptr<NodeObject> recent;
backend.fetch(rs.recent[pick]->getHash(), &recent);
benchmark::DoNotOptimize(recent);
backend.store(rs.recent[slot]);
},
.clobber = true,
.pinToPool = true,
};
auto
makeRunner(Workload w, std::string cfg, std::shared_ptr<RunState> rs)
{
return [w = std::move(w), cfg = std::move(cfg), rs = std::move(rs)](benchmark::State& state) {
auto const poolSize = static_cast<std::size_t>(state.range(0));
if (state.thread_index() == 0)
{
rs->harness = std::make_unique<BackendHarness>(cfg);
w.setup(
SetupContext{.rs = *rs, .backend = *rs->harness->backend, .poolSize = poolSize});
}
std::size_t index = state.thread_index();
for (auto _ : state)
{
w.iterate(
IterateContext{
.rs = *rs,
.backend = *rs->harness->backend,
.index = index,
.poolSize = poolSize});
index += state.threads();
}
if (w.clobber)
benchmark::ClobberMemory();
state.SetItemsProcessed(state.iterations());
if (w.reportBytes)
state.SetBytesProcessed(static_cast<std::int64_t>(state.iterations() * rs->avgPayload));
if (state.thread_index() == 0)
rs->release();
};
}
// Register workload `w` against backend `bc`, choosing the registration shape
// from `w.pinToPool`.
void
registerWorkload(BackendConfig const& bc, Workload const& w)
{
std::string const cfg = bc.config;
std::string name{kNamePrefix};
name += w.name;
name += kNameSeparator;
name += bc.name;
if (!w.pinToPool)
{
auto rs = std::make_shared<RunState>();
auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs));
b->RangeMultiplier(10)->Range(kPoolSizes[0], kPoolSizes[std::size(kPoolSizes) - 1]);
b->Threads(1)->Threads(4)->Threads(8)->UseRealTime();
return;
}
for (auto const poolSize : kPoolSizes)
{
for (auto const threads : kThreadCounts)
{
if (poolSize % static_cast<std::size_t>(threads) != 0)
continue;
auto rs = std::make_shared<RunState>();
benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs))
->Arg(poolSize)
->Iterations(poolSize / static_cast<std::size_t>(threads))
->Threads(threads)
->UseRealTime();
}
}
}
// One storeBatch() of kBatchSize objects per iteration. Single-threaded:
// Backend::storeBatch must not run concurrently with itself or store().
// Iterations are pinned to the batch count so the index never wraps into
// key_exists no-ops. Kept separate from Workload: batch slicing and the
// per-batch item/byte accounting don't fit the thread-axis mold.
void
registerStoreBatch(BackendConfig const& bc)
{
std::string const cfg = bc.config;
std::string name{kNamePrefix};
name += "StoreBatch";
name += kNameSeparator;
name += bc.name;
for (auto const poolSize : kPoolSizes)
{
auto const numBatches = poolSize / kBatchSize;
if (numBatches == 0)
continue;
auto rs = std::make_shared<RunState>();
benchmark::RegisterBenchmark(
name,
[rs, cfg](benchmark::State& state) {
auto const poolSize = static_cast<std::size_t>(state.range(0));
rs->harness = std::make_unique<BackendHarness>(cfg);
rs->present = makePool(1, poolSize);
rs->avgPayload = averagePayload(rs->present);
std::vector<Batch> const batches = sliceBatches(rs->present, kBatchSize);
if (batches.empty())
{
state.SkipWithError("pool smaller than one batch");
return;
}
std::size_t index = 0;
for (auto _ : state)
{
rs->harness->backend->storeBatch(batches[index % batches.size()]);
++index;
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * kBatchSize));
state.SetBytesProcessed(
static_cast<std::int64_t>(state.iterations() * kBatchSize * rs->avgPayload));
rs->release();
})
->Arg(poolSize)
->Iterations(numBatches);
}
}
[[maybe_unused]] bool const kRegistered = [] {
auto const workloads = std::to_array({&kInsert, &kFetch, &kMissing, &kMixed, &kWork});
for (auto const& bc : backendConfigs())
{
for (auto const* w : workloads)
registerWorkload(bc, *w);
registerStoreBatch(bc);
}
return true;
}();
} // namespace
} // namespace xrpl::NodeStore

View File

@@ -1,243 +0,0 @@
#include <xrpl/nodestore/Database.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Types.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/nodestore/NodeStoreBench.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::NodeStore {
namespace {
// Number of distinct objects pre-generated per run.
constexpr std::size_t kDefaultPoolSize = 100000;
// Async read threads the Database spawns. Unused by the synchronous fetch path
// these benchmarks take; kept fixed so runs are comparable.
constexpr int kReadThreads = 4;
constexpr std::string_view kNamePrefix = "BM_Database_";
constexpr std::string_view kNameSeparator = "/";
struct RunState
{
std::unique_ptr<DatabaseHarness> harness;
Batch present; // prefix-1 objects, eligible to be stored
Batch recent; // prefix-1 objects in the "future" key space
std::vector<uint256> missing; // prefix-2 keys that are never stored
std::vector<std::size_t> shuffle; // [0, poolSize) permutation for random-like access
std::size_t avgPayload = 0; // mean getData().size() over `present`
};
struct SetupContext
{
RunState& rs;
Database& db;
std::size_t poolSize;
};
struct IterateContext
{
RunState& rs;
Database& db;
std::uint32_t seq;
std::size_t index;
std::size_t poolSize;
};
struct Workload
{
std::string_view name;
std::function<void(SetupContext const&)> setup;
std::function<void(IterateContext const&)> iterate;
bool reportBytes = false;
bool pinIterations = false;
};
void
prepopulate(Database& db, Batch const& objects)
{
auto const seq = db.earliestLedgerSeq();
for (auto const& obj : objects)
{
Blob data(obj->getData());
db.store(obj->getType(), std::move(data), obj->getHash(), seq);
}
db.sync();
}
// One store() per iteration; a fresh Blob copy is handed over each time.
Workload const kStore{
.name = "Store",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto const& obj = rs.present[index % poolSize];
Blob data(obj->getData());
db.store(obj->getType(), std::move(data), obj->getHash(), seq);
},
.reportBytes = true,
.pinIterations = true,
};
// One fetchNodeObject() of a stored key (a hit) per iteration.
Workload const kFetch{
.name = "Fetch",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.avgPayload = averagePayload(ctx.rs.present);
prepopulate(ctx.db, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto obj = db.fetchNodeObject(rs.present[index % poolSize]->getHash(), seq);
benchmark::DoNotOptimize(obj);
},
.reportBytes = true,
};
// One fetchNodeObject() of a never-stored key (a miss) per iteration.
Workload const kMissing{
.name = "Missing",
.setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); },
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto obj = db.fetchNodeObject(rs.missing[index % poolSize], seq);
benchmark::DoNotOptimize(obj);
},
};
// 80% hits / 20% misses. The fetch index comes from a shuffle table so access
// is random-like without per-iteration RNG cost; sequential `index % poolSize`
// would be artificially cache-friendly.
Workload const kMixed{
.name = "Mixed",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.missing = makeMissingKeys(ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/1);
prepopulate(ctx.db, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto const pick = rs.shuffle[index % poolSize];
std::shared_ptr<NodeObject> obj;
if (index % 5 == 0)
{
obj = db.fetchNodeObject(rs.missing[pick], seq);
}
else
{
obj = db.fetchNodeObject(rs.present[pick]->getHash(), seq);
}
benchmark::DoNotOptimize(obj);
},
};
// An xrpld-like cycle: a hit, a maybe-miss recent fetch, and a store. The
// recent fetch uses the shuffle table (not `slot`) so it doesn't fetch the item
// it's about to store this iteration - which would give an all-miss-then-hit
// step instead of a smooth ramp. The store walks sequentially so each recent
// object is stored once.
Workload const kWork{
.name = "Work",
.setup =
[](SetupContext const& ctx) {
ctx.rs.present = makePool(1, ctx.poolSize);
ctx.rs.recent = makePool(1, ctx.poolSize, ctx.poolSize);
ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/2);
prepopulate(ctx.db, ctx.rs.present);
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, db, seq, index, poolSize] = ctx;
auto const slot = index % poolSize;
auto const pick = rs.shuffle[slot];
auto historical = db.fetchNodeObject(rs.present[pick]->getHash(), seq);
benchmark::DoNotOptimize(historical);
auto recent = db.fetchNodeObject(rs.recent[pick]->getHash(), seq);
benchmark::DoNotOptimize(recent);
auto const& obj = rs.recent[slot];
Blob data(obj->getData());
db.store(obj->getType(), std::move(data), obj->getHash(), seq);
},
.pinIterations = true,
};
void
registerWorkload(BackendConfig const& bc, Workload const& w)
{
auto rs = std::make_shared<RunState>();
std::string const cfg = bc.config;
std::string name{kNamePrefix};
name += w.name;
name += kNameSeparator;
name += bc.name;
auto* b = benchmark::RegisterBenchmark(name, [rs, cfg, w](benchmark::State& state) {
auto const poolSize = static_cast<std::size_t>(state.range(0));
rs->harness = std::make_unique<DatabaseHarness>(cfg, kReadThreads);
auto& db = *rs->harness->db;
w.setup(SetupContext{.rs = *rs, .db = db, .poolSize = poolSize});
auto const seq = db.earliestLedgerSeq();
std::size_t index = 0;
for (auto _ : state)
{
w.iterate(
IterateContext{
.rs = *rs, .db = db, .seq = seq, .index = index, .poolSize = poolSize});
++index;
}
benchmark::ClobberMemory();
state.SetItemsProcessed(state.iterations());
if (w.reportBytes)
{
state.SetBytesProcessed(static_cast<std::int64_t>(state.iterations() * rs->avgPayload));
}
rs->harness.reset();
});
b->Arg(kDefaultPoolSize);
if (w.pinIterations)
b->Iterations(kDefaultPoolSize);
}
[[maybe_unused]] bool const kRegistered = [] {
auto const workloads = std::to_array({&kStore, &kFetch, &kMissing, &kMixed, &kWork});
for (auto const& bc : backendConfigs())
{
for (auto const* w : workloads)
registerWorkload(bc, *w);
}
return true;
}();
} // namespace
} // namespace xrpl::NodeStore

View File

@@ -1,318 +0,0 @@
#pragma once
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/nodestore/Backend.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <numeric>
#include <random>
#include <string>
#include <utility>
#include <vector>
// Shared helpers for the NodeStore benchmarks.
//
namespace xrpl::NodeStore {
// Fill `bytes` of memory at `buffer` with random bits drawn from `g`.
template <class Generator>
inline void
rngcpy(void* buffer, std::size_t bytes, Generator& g)
{
using result_type = typename Generator::result_type;
while (bytes >= sizeof(result_type))
{
auto const v = g();
std::memcpy(buffer, &v, sizeof(v));
buffer = reinterpret_cast<std::uint8_t*>(buffer) + sizeof(v);
bytes -= sizeof(v);
}
if (bytes > 0)
{
auto const v = g();
std::memcpy(buffer, &v, bytes);
}
}
/**
* @brief Deterministic generator of a reproducible sequence of random NodeObjects.
*
* Indexing is stable: `obj(n)` and `key(n)` always return the same value for a
* given `n`, regardless of call order, because the engine is reseeded from `n`
* on every call.
*
* Using different prefixes guarantees the two key spaces are disjoint for the fetch-miss
* workloads.
*/
class Sequence
{
private:
static constexpr auto kMinSize = 250;
static constexpr auto kMaxSize = 1250;
beast::xor_shift_engine gen_;
std::uint8_t prefix_;
std::discrete_distribution<std::uint32_t> dType_;
std::uniform_int_distribution<std::uint32_t> dSize_;
public:
explicit Sequence(std::uint8_t prefix)
: prefix_(prefix)
// uniform distribution over hotLEDGER - hotTRANSACTION_NODE
// but exclude hotTRANSACTION = 2 (removed)
, dType_({1, 1, 0, 1, 1})
, dSize_(kMinSize, kMaxSize)
{
}
// Returns the n-th key. Used to generate keys that are never stored.
// The layout mirrors obj()'s: prefix at byte 0, RNG over the rest, so the
// two key spaces stay disjoint by construction (not by coincidence).
uint256
key(std::size_t n)
{
gen_.seed(n + 1);
uint256 result;
auto const data = static_cast<std::uint8_t*>(&*result.begin());
*data = prefix_;
rngcpy(data + 1, result.size() - 1, gen_);
return result;
}
// Returns the n-th complete NodeObject.
std::shared_ptr<NodeObject>
obj(std::size_t n)
{
gen_.seed(n + 1);
uint256 key;
auto const data = static_cast<std::uint8_t*>(&*key.begin());
*data = prefix_;
rngcpy(data + 1, key.size() - 1, gen_);
Blob value(dSize_(gen_));
rngcpy(&value[0], value.size(), gen_);
return NodeObject::createObject(
safeCast<NodeObjectType>(dType_(gen_)), std::move(value), key);
}
// Fills `b` with `size` consecutive NodeObjects starting at index `n`.
void
batch(std::size_t n, Batch& b, std::size_t size)
{
b.clear();
b.reserve(size);
while ((size--) != 0u)
b.push_back(obj(n++));
}
};
// Parse a comma-separated "key=value,key=value" string into a config Section.
inline Section
parseConfig(std::string const& s)
{
Section section;
std::vector<std::string> values;
boost::split(values, s, boost::algorithm::is_any_of(","));
section.append(values);
return section;
}
// Pre-generate `count` distinct objects from key space `prefix`, starting at
// sequence index `start`.
inline Batch
makePool(std::uint8_t prefix, std::size_t count, std::size_t start = 0)
{
Sequence seq(prefix);
Batch pool;
pool.reserve(count);
for (std::size_t i = 0; i < count; ++i)
pool.push_back(seq.obj(start + i));
return pool;
}
// Pre-generate `count` keys disjoint from every `makePool(...)` object, for
// measuring fetches that miss.
inline std::vector<uint256>
makeMissingKeys(std::size_t count)
{
Sequence seq(2);
std::vector<uint256> keys;
keys.reserve(count);
for (std::size_t i = 0; i < count; ++i)
keys.push_back(seq.key(i));
return keys;
}
// Mean payload size across a pool, used for SetBytesProcessed throughput.
inline std::size_t
averagePayload(Batch const& pool)
{
if (pool.empty())
return 0;
std::size_t total = 0;
for (auto const& obj : pool)
total += obj->getData().size();
return total / pool.size();
}
// Store every object and flush, so a following fetch exercises the real read
// path rather than an in-memory write buffer.
//
// We chunk the write at kBatchWriteLimitSize because Types.h documents that as
// the maximum allowed batch size. NuDB happens to tolerate larger batches
// today, but the benchmark should not rely on that.
//
// sync() is a no-op for both NuDB and RocksDB at the moment (NuDB has a small
// internal burst buffer that the timed loop will warm up). That is a contract
// hint, not a guarantee; if either backend ever grows a real flush we get it
// here for free.
inline void
prepopulate(Backend& backend, Batch const& objects)
{
for (std::size_t i = 0; i < objects.size(); i += kBatchWriteLimitSize)
{
auto const end = std::min(i + kBatchWriteLimitSize, objects.size());
backend.storeBatch(Batch(objects.begin() + i, objects.begin() + end));
}
backend.sync();
}
// A deterministic permutation of [0, size). Lets the timed loop visit the
// pre-generated pool in a random-like order with zero RNG cost per iteration -
// the Timing_test workloads it replaces used uniform_int_distribution per
// fetch, and a shuffle table reproduces that access pattern without paying for
// the distribution inside the timed region.
inline std::vector<std::size_t>
makeShuffle(std::size_t size, std::uint64_t seed)
{
std::vector<std::size_t> v(size);
std::iota(v.begin(), v.end(), std::size_t{0});
beast::xor_shift_engine gen(seed);
std::shuffle(v.begin(), v.end(), gen);
return v;
}
// Partition a pool into fixed-size batches. Any trailing remainder shorter than
// `batchSize` is dropped, so every returned batch has exactly `batchSize`.
inline std::vector<Batch>
sliceBatches(Batch const& pool, std::size_t batchSize)
{
std::vector<Batch> batches;
if (batchSize == 0)
return batches;
batches.reserve(pool.size() / batchSize);
for (std::size_t i = 0; i + batchSize <= pool.size(); i += batchSize)
batches.emplace_back(pool.begin() + i, pool.begin() + i + batchSize);
return batches;
}
/**
* @brief RAII owner of a NodeStore Backend opened on a private temporary directory.
*
* Member declaration order matters: `tempDir` is declared first so it is
* destroyed last, after the backend has closed and released its files.
*/
struct BackendHarness
{
beast::TempDir tempDir;
DummyScheduler scheduler;
beast::Journal journal{beast::Journal::getNullSink()};
std::unique_ptr<Backend> backend;
explicit BackendHarness(std::string const& configString)
{
Section config = parseConfig(configString);
// A private, unique path per harness, so concurrent or repeated runs
// never share on-disk state.
config.set("path", tempDir.path());
backend =
Manager::instance().makeBackend(config, megabytes(std::size_t{4}), scheduler, journal);
backend->setDeletePath();
backend->open();
}
~BackendHarness()
{
if (backend)
backend->close();
}
};
/**
* RAII owner of a NodeStore Database - the application-facing wrapper around a
* Backend, which adds fetch/store accounting and the async read-thread pool.
*/
struct DatabaseHarness
{
beast::TempDir tempDir;
DummyScheduler scheduler;
beast::Journal journal{beast::Journal::getNullSink()};
std::unique_ptr<Database> db;
DatabaseHarness(std::string const& configString, int readThreads)
{
Section config = parseConfig(configString);
config.set("path", tempDir.path());
db = Manager::instance().makeDatabase(
megabytes(std::size_t{4}), scheduler, readThreads, config, journal);
}
~DatabaseHarness()
{
if (db)
db->stop();
}
};
// A NodeStore backend to benchmark, named for the --benchmark_filter CLI flag.
struct BackendConfig
{
char const* name; // short label, e.g. "nudb"
char const* config; // parseConfig() string, e.g. "type=nudb"
};
// The backends every workload is registered against.
//
// The in-memory backend is intentionally excluded. It keeps its table in a
// process-global map keyed by path, with no removal API, so building a fresh
// backend per run - as a microbenchmark must - would leak the whole dataset on
// every run. Timing_test, the suite this benchmark replaces, excluded it for
// the same reason. NuDB and RocksDB are the production backends worth timing.
//
// RocksDB is included only when it was compiled in (xrpl.libxrpl carries
// XRPL_ROCKSDB_AVAILABLE transitively).
inline std::vector<BackendConfig> const&
backendConfigs()
{
static std::vector<BackendConfig> const kConfigs = {
{.name = "nudb", .config = "type=nudb"},
#if XRPL_ROCKSDB_AVAILABLE
{.name = "rocksdb",
.config = "type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256,"
"file_size_mb=8,file_size_mult=2"},
#endif
};
return kConfigs;
}
} // namespace xrpl::NodeStore

View File

@@ -277,25 +277,6 @@ public:
void
doDropDigit(T& mantissa, int& exponent) noexcept;
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location);
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
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 <UnsignedMantissa T>
void
pushOverflow(T mantissa);
enum class Round {
// The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or
// higher.
@@ -308,22 +289,37 @@ private:
// 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)
// Enabled)
Up = 1,
};
// Indicate round direction. See Round enum above.
// 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]] Round
round() const noexcept;
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location);
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
doRoundDown(bool& negative, T& mantissa, int& exponent);
// Modify the result to the correctly rounded value
void
doRound(rep& drops, std::string location) const;
private:
void
doPush(unsigned d) noexcept;
template <UnsignedMantissa T>
void
bringIntoRange(bool& negative, T& mantissa, int& exponent) const;
bringIntoRange(bool& negative, T& mantissa, int& exponent);
};
inline void
@@ -353,7 +349,6 @@ 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;
@@ -401,69 +396,10 @@ Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexce
++exponent;
}
template <UnsignedMantissa T>
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<unsigned>((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:
// 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
// -1 if Guard is less than half
// 0 if Guard is exactly half
// 1 if Guard is greater than half
Number::Guard::Round
Number::Guard::round() const noexcept
{
@@ -509,23 +445,17 @@ Number::Guard::round() const noexcept
template <UnsignedMantissa T>
void
Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) const
Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent)
{
// Bring mantissa back into the minMantissa / maxMantissa range AFTER
// rounding.
if (mantissa < minMantissa &&
(cuspRoundingFix < MantissaRange::CuspRoundingFix::Enabled330 || mantissa != 0))
// rounding
if (mantissa < minMantissa)
{
mantissa *= 10;
--exponent;
}
// 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))
if (exponent < kMinExponent)
{
// 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_;
@@ -538,9 +468,7 @@ template <UnsignedMantissa T>
void
Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location)
{
pushOverflow(mantissa);
auto const r = round();
auto r = round();
if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1))
{
auto const safeToIncrement = [this](auto const& mantissa) {
@@ -557,29 +485,18 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string
}
else
{
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;
}
// 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
@@ -597,14 +514,6 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string
}
}
}
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::overflow_error>(std::string(location));
@@ -612,10 +521,8 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string
template <UnsignedMantissa T>
void
Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) const
Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent)
{
// Do not pushOverflow here.
auto r = round();
if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330)
{
@@ -650,8 +557,6 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) const
void
Number::Guard::doRound(rep& drops, std::string location) const
{
// Do not pushOverflow here.
auto r = round();
if (r == Round::Up || (r == Round::Even && (drops & 1) == 1))
{
@@ -668,8 +573,6 @@ Number::Guard::doRound(rep& drops, std::string location) const
}
++drops;
}
XRPL_ASSERT(drops >= 0, "xrpl::Number::Guard::doRound : positive magnitude");
if (isNegative())
drops = -drops;
}
@@ -719,9 +622,7 @@ doNormalize(
{
static constexpr auto kMinExponent = Number::kMinExponent;
static constexpr auto kMaxExponent = Number::kMaxExponent;
auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330
? Number::kMaxRepUp
: Number::kMaxRep;
static constexpr auto kMaxRep = Number::kMaxRep;
using Guard = Number::Guard;
@@ -771,17 +672,17 @@ 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 > repLimit)
if (m > kMaxRep)
{
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 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");
// 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");
mantissa = m;
g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2");
@@ -913,9 +814,6 @@ Number::operator+=(Number const& y)
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.
@@ -1000,7 +898,7 @@ Number::operator+=(Number const& y)
}
else
{
if (xm > maxMantissa || xm > repLimit)
if (xm > maxMantissa || xm > kMaxRep)
{
g.doDropDigit(xm, xe);
}
@@ -1044,7 +942,7 @@ Number::operator+=(Number const& y)
{
// 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)
while (xm < minMantissa && xm * 10 <= kMaxRep)
{
xm *= 10;
xm -= g.pop();
@@ -1118,10 +1016,8 @@ Number::operator*=(Number const& y)
g.setNegative();
auto const& maxMantissa = g.maxMantissa;
auto const repLimit =
g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep;
while (zm > maxMantissa || zm > repLimit)
while (zm > maxMantissa || zm > kMaxRep)
{
g.doDropDigit(zm, ze);
}
@@ -1386,11 +1282,8 @@ to_string(Number const& amount)
}
std::string ret = negative ? "-" : "";
ret.append(std::to_string(mantissa));
if (exponent != 0)
{
ret.append(1, 'e');
ret.append(std::to_string(exponent));
}
ret.append(1, 'e');
ret.append(std::to_string(exponent));
return ret;
}

View File

@@ -127,11 +127,15 @@ Workers::deleteWorkers(beast::LockFreeStack<Worker>& stack)
{
Worker const* const worker = stack.popFront();
if (worker == nullptr)
if (worker != nullptr)
{
// This call blocks until the thread orderly exits
delete worker;
}
else
{
break;
// This call blocks until the thread orderly exits
delete worker;
}
}
}

View File

@@ -314,7 +314,8 @@ Value::~Value()
case ValueType::Array:
case ValueType::Object:
delete value_.mapVal;
if (value_.mapVal != nullptr)
delete value_.mapVal;
break;
// LCOV_EXCL_START

View File

@@ -12,6 +12,8 @@
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SLEBase.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/AccountID.h>
@@ -555,7 +557,7 @@ ammLPHolds(
auto const currency = ammLPTCurrency(asset1, asset2);
STAmount amount;
auto const sle = view.read(keylet::trustLine(lpAccount, ammAccount, currency));
RippleStateEntry<ReadView> const sle{keylet::trustLine(lpAccount, ammAccount, currency), view};
if (!sle)
{
amount.clear(Issue{currency, ammAccount});
@@ -632,7 +634,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
// Get the actual AMM balance without factoring in the balance hook
return asset.visit(
[&](MPTIssue const& issue) {
if (auto const sle = view.read(keylet::mptoken(issue, ammAccountID));
if (MPTokenEntry<ReadView> const sle{keylet::mptoken(issue, ammAccountID), view};
sle && !isFrozen(view, ammAccountID, issue))
return STAmount{issue, (*sle)[sfMPTAmount]};
return STAmount{asset};
@@ -640,12 +642,12 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
[&](Issue const& issue) {
if (isXRP(issue))
{
if (auto const sle = view.read(keylet::account(ammAccountID)))
if (AccountRootEntry<ReadView> const sle{keylet::account(ammAccountID), view})
return (*sle)[sfBalance];
}
else if (
auto const sle =
view.read(keylet::trustLine(ammAccountID, issue.account, issue.currency));
RippleStateEntry<ReadView> const sle{
keylet::trustLine(ammAccountID, issue.account, issue.currency), view};
sle && !isFrozen(view, ammAccountID, issue.currency, issue.account))
{
STAmount amount = (*sle)[sfBalance];
@@ -746,7 +748,7 @@ deleteAMMMPTokens(Sandbox& sb, AccountID const& ammAccountID, beast::Journal j)
TER
deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Journal j)
{
auto ammSle = sb.peek(keylet::amm(asset, asset2));
AMMEntry<ApplyView> ammSle{keylet::amm(asset, asset2), sb};
if (!ammSle)
{
// LCOV_EXCL_START
@@ -756,7 +758,7 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
}
auto const ammAccountID = (*ammSle)[sfAccount];
auto sleAMMRoot = sb.peek(keylet::account(ammAccountID));
AccountRootEntry<ApplyView> sleAMMRoot{keylet::account(ammAccountID), sb};
if (!sleAMMRoot)
{
// LCOV_EXCL_START
@@ -776,25 +778,12 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
if (auto const ter = deleteAMMMPTokens(sb, ammAccountID, j); !isTesSuccess(ter))
return ter;
auto const ownerDirKeylet = keylet::ownerDir(ammAccountID);
if (!sb.dirRemove(ownerDirKeylet, (*ammSle)[sfOwnerNode], ammSle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: failed to remove dir link";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (sb.exists(ownerDirKeylet) && !sb.emptyDirDelete(ownerDirKeylet))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: cannot delete root dir node of "
<< toBase58(ammAccountID);
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Unlink the AMM from its pseudo-account's directory (collapsing the empty
// root) and erase it. See AMMEntry::destroy().
if (auto const ter = ammSle.destroy(); !isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
sb.erase(ammSle);
sb.erase(sleAMMRoot);
sleAMMRoot.erase();
return tesSUCCESS;
}
@@ -885,12 +874,12 @@ isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID c
// Iterate over AMM owner directory objects.
while (limit-- >= 1)
{
auto const ownerDir = view.read(currentIndex);
DirectoryNodeEntry<ReadView> const ownerDir{currentIndex, view};
if (!ownerDir)
return std::unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
for (auto const& key : ownerDir->getFieldV256(sfIndexes))
{
auto const sle = view.read(keylet::child(key));
ReadOnlySLE const sle{keylet::child(key), view};
if (!sle)
return std::unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
auto const entryType = sle->getFieldU16(sfLedgerEntryType);

View File

@@ -9,6 +9,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OwnerCounts.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -39,7 +40,7 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer)
{
if (isXRP(issuer))
return false;
if (auto const sle = view.read(keylet::account(issuer)))
if (AccountRootEntry<ReadView> const sle{keylet::account(issuer), view})
return sle->isFlag(lsfGlobalFreeze);
return false;
}
@@ -269,17 +270,17 @@ ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj)
XRPAmount
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j)
{
auto const sle = view.read(keylet::account(id));
if (sle == nullptr)
AccountRootEntry<ReadView> const sle{keylet::account(id), view};
if (!sle)
return beast::kZero;
// Return balance minus reserve
std::uint32_t const currentOwnerCount =
confineOwnerCount(view.ownerCountHook(id, OwnerCounts(sle)).count(), ownerCountAdj);
std::uint32_t const currentAccountCount = accountCountImpl(sle, 0, j);
confineOwnerCount(view.ownerCountHook(id, OwnerCounts(sle.sle())).count(), ownerCountAdj);
std::uint32_t const currentAccountCount = accountCountImpl(sle.sle(), 0, j);
// Pseudo-accounts have no reserve requirement
auto const reserve = isPseudoAccount(sle)
auto const reserve = isPseudoAccount(sle.sle())
? XRPAmount{0}
: view.fees().accountReserve(currentOwnerCount, currentAccountCount);
@@ -301,7 +302,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj,
Rate
transferRate(ReadView const& view, AccountID const& issuer)
{
auto const sle = view.read(keylet::account(issuer));
AccountRootEntry<ReadView> const sle{keylet::account(issuer), view};
if (sle && sle->isFieldPresent(sfTransferRate))
return Rate{sle->getFieldU32(sfTransferRate)};

View File

@@ -7,7 +7,7 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -53,13 +53,13 @@ removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j)
{
// Credentials already checked in preclaim. Look only for expired here.
auto const k = keylet::credential(h);
auto const sleCred = view.peek(k);
CredentialEntry<ApplyView> sleCred{k, view};
if (sleCred && checkExpired(*sleCred, closeTime))
{
JLOG(j.trace()) << "Credentials are expired. Cred: " << sleCred->getText();
// delete expired credentials even if the transaction failed
auto const err = deleteSLE(view, sleCred, j);
auto const err = deleteSLE(view, sleCred.mutableSle(), j);
if (view.rules().enabled(fixCleanup3_1_3) && !isTesSuccess(err))
return std::unexpected(err);
foundExpired = true;
@@ -75,52 +75,26 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j)
if (!sleCredential)
return tecNO_ENTRY;
auto delSLE = [&view, &sleCredential, j](
AccountID const& account, SField const& node, bool isOwner) -> TER {
auto const sleAccount = view.peek(keylet::account(account));
if (!sleAccount)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Internal error: can't retrieve Owner account.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Remove object from owner directory
std::uint64_t const page = sleCredential->getFieldU64(node);
if (!view.dirRemove(keylet::ownerDir(account), page, sleCredential->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Credential from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
if (isOwner)
decreaseOwnerCountForObject(view, sleAccount, sleCredential, 1, j);
return tesSUCCESS;
};
// Historically deleteSLE fetched both the issuer's and (for a third-party
// credential) the subject's account and failed if either was missing, even
// though only one of them is counted against a reserve. Preserve that
// stricter contract: a corrupted view missing one of these accounts must
// report tecINTERNAL rather than silently unlinking.
auto const issuer = sleCredential->getAccountID(sfIssuer);
auto const subject = sleCredential->getAccountID(sfSubject);
bool const accepted = sleCredential->isFlag(lsfAccepted);
auto err = delSLE(issuer, sfIssuerNode, !accepted || (subject == issuer));
if (!isTesSuccess(err))
return err;
if (subject != issuer)
if (!view.exists(keylet::account(issuer)) ||
(subject != issuer && !view.exists(keylet::account(subject))))
{
err = delSLE(subject, sfSubjectNode, accepted);
if (!isTesSuccess(err))
return err;
JLOG(j.fatal()) << "Internal error: can't retrieve Owner account.";
return tecINTERNAL;
}
// Remove object from ledger
view.erase(sleCredential);
return tesSUCCESS;
// Unlink the credential from the issuer's and subject's directories,
// decrementing whichever account currently owns it (the issuer until the
// subject accepts, the subject afterwards), and erase it. See
// CredentialEntry::ownerDirs().
CredentialEntry<ApplyView> cred{sleCredential, view, j};
return cred.destroy();
}
NotTEC
@@ -160,7 +134,7 @@ valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal
auto const& credIDs(tx.getFieldV256(sfCredentialIDs));
for (auto const& h : credIDs)
{
auto const sleCred = view.read(keylet::credential(h));
CredentialEntry<ReadView> const sleCred{keylet::credential(h), view};
if (!sleCred)
{
JLOG(j.trace()) << "Credential doesn't exist. Cred: " << h;
@@ -189,7 +163,7 @@ TER
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject)
{
// Note, permissioned domain objects can be deleted at any time
auto const slePD = view.read(keylet::permissionedDomain(domainID));
PermissionedDomainEntry<ReadView> const slePD{keylet::permissionedDomain(domainID), view};
if (!slePD)
return tecOBJECT_NOT_FOUND;
@@ -200,7 +174,7 @@ validDomain(ReadView const& view, uint256 domainID, AccountID const& subject)
auto const issuer = h.getAccountID(sfIssuer);
auto const type = h.getFieldVL(sfCredentialType);
auto const keyletCredential = keylet::credential(subject, issuer, makeSlice(type));
auto const sleCredential = view.read(keyletCredential);
CredentialEntry<ReadView> const sleCredential{keyletCredential, view};
// We cannot delete expired credentials, that would require ApplyView&
// However we can check if credentials are expired. Expected transaction
@@ -234,14 +208,14 @@ authorizedDepositPreauth(ReadView const& view, STVector256 const& credIDs, Accou
lifeExtender.reserve(credIDs.size());
for (auto const& h : credIDs)
{
auto sleCred = view.read(keylet::credential(h));
CredentialEntry<ReadView> const sleCred{keylet::credential(h), view};
if (!sleCred) // already checked in preclaim
return tefINTERNAL; // LCOV_EXCL_LINE
auto [it, ins] = sorted.emplace((*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]);
if (!ins)
return tefINTERNAL; // LCOV_EXCL_LINE
lifeExtender.push_back(std::move(sleCred));
lifeExtender.push_back(sleCred.sle());
}
if (!view.exists(keylet::depositPreauth(dst, sorted)))
@@ -312,7 +286,7 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j)
TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j)
{
auto const slePD = view.read(keylet::permissionedDomain(domainID));
PermissionedDomainEntry<ReadView> const slePD{keylet::permissionedDomain(domainID), view};
if (!slePD)
return tecOBJECT_NOT_FOUND;
@@ -334,7 +308,7 @@ verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, b
for (auto const& h : credentials)
{
auto sleCredential = view.read(keylet::credential(h));
CredentialEntry<ReadView> const sleCredential{keylet::credential(h), view};
if (!sleCredential)
continue; // expired, i.e. deleted in credentials::removeExpired

View File

@@ -9,6 +9,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/LedgerFormats.h>
@@ -32,13 +33,14 @@ namespace xrpl {
[[nodiscard]] TER
canApplyToBrokerCover(
ReadView const& view,
SLE::const_ref sleBroker,
LoanBrokerEntry<ReadView> const& sleBroker,
Asset const& vaultAsset,
STAmount const& amount,
beast::Journal j,
std::string_view logPrefix)
{
ReadView const& view = sleBroker.readView();
beast::Journal const j = sleBroker.journal();
XRPL_ASSERT(
sleBroker && sleBroker->getType() == ltLOAN_BROKER,
"xrpl::canApplyToBrokerCover : valid LoanBroker sle");
@@ -312,25 +314,6 @@ computeInterestAndFeeParts(
return std::make_pair(interest - fee, fee);
}
/* Rounds a raw (unrounded) interest amount to the loan's scale, then splits
* the rounded amount into net interest (to the vault) and management fee (to
* the broker).
*
* This is the common "round then split" step shared by late payment, full
* payment, and overpayment interest calculations.
*/
std::pair<Number, Number>
roundAndSplitInterest(
Asset const& asset,
Number const& rawInterest,
TenthBips16 managementFeeRate,
std::int32_t loanScale,
Number::RoundingMode mode = Number::getround())
{
auto const interest = roundToAsset(asset, rawInterest, loanScale, mode);
return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}
/* Calculates penalty interest accrued on overdue payments.
* Returns 0 if payment is not late.
*
@@ -406,18 +389,22 @@ loanAccruedInterest(
*
* This is the core function that updates the Loan ledger object fields based on
* a computed payment.
*/
LoanPaymentParts
doPayment(ExtendedPaymentComponents const& payment, SLE::ref loan)
{
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate);
auto nextDueDateProxy = loan->at(sfNextPaymentDueDate);
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
* The function is templated to work with both direct Number/uint32_t values
* (for testing/simulation) and ValueProxy types (for actual ledger updates).
*/
template <class NumberProxy, class UInt32Proxy, class UInt32OptionalProxy>
LoanPaymentParts
doPayment(
ExtendedPaymentComponents const& payment,
NumberProxy& totalValueOutstandingProxy,
NumberProxy& principalOutstandingProxy,
NumberProxy& managementFeeOutstandingProxy,
UInt32Proxy& paymentRemainingProxy,
UInt32Proxy& prevPaymentDateProxy,
UInt32OptionalProxy& nextDueDateProxy,
std::uint32_t paymentInterval)
{
XRPL_ASSERT_PARTS(nextDueDateProxy, "xrpl::detail::doPayment", "Next due date proxy set");
if (payment.specialCase == PaymentSpecialCase::Final)
@@ -485,12 +472,16 @@ doPayment(ExtendedPaymentComponents const& payment, SLE::ref loan)
// Principal can never exceed total value (principal is part of total value)
XRPL_ASSERT_PARTS(
// Use an explicit cast because the template parameter can be
// ValueProxy<Number> or Number
static_cast<Number>(principalOutstandingProxy) <=
static_cast<Number>(totalValueOutstandingProxy),
"xrpl::detail::doPayment",
"principal does not exceed total");
XRPL_ASSERT_PARTS(
// Use an explicit cast because the template parameter can be
// ValueProxy<Number> or Number
static_cast<Number>(managementFeeOutstandingProxy) >= beast::kZero,
"xrpl::detail::doPayment",
"fee outstanding stays valid");
@@ -728,23 +719,22 @@ tryOverpayment(
* overpayment would leave the loan in an invalid state, we can reject it
* gracefully without corrupting the ledger data.
*/
template <class NumberProxy>
std::expected<LoanPaymentParts, TER>
doOverpayment(
Rules const& rules,
Asset const& asset,
std::int32_t loanScale,
ExtendedPaymentComponents const& overpaymentComponents,
SLE::ref loan,
NumberProxy& totalValueOutstandingProxy,
NumberProxy& principalOutstandingProxy,
NumberProxy& managementFeeOutstandingProxy,
NumberProxy& periodicPaymentProxy,
Number const& periodicRate,
std::uint32_t const paymentRemaining,
TenthBips16 const managementFeeRate,
beast::Journal j)
{
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
auto const paymentsRemaining = loan->at(sfPaymentRemaining);
auto const loanState = constructLoanState(
totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy);
auto const periodicPayment = periodicPaymentProxy;
@@ -756,7 +746,7 @@ doOverpayment(
<< ", interestPart: " << overpaymentComponents.trackedInterestPart()
<< ", untrackedInterest: " << overpaymentComponents.untrackedInterest
<< ", totalDue: " << overpaymentComponents.totalDue
<< ", payments remaining :" << paymentsRemaining;
<< ", payments remaining :" << paymentRemaining;
// Attempt to re-amortize the loan with the overpayment applied.
// This modifies the temporary copies, leaving the proxies unchanged.
@@ -768,7 +758,7 @@ doOverpayment(
loanState,
periodicPayment,
periodicRate,
paymentsRemaining,
paymentRemaining,
managementFeeRate,
j);
if (!ret)
@@ -876,15 +866,16 @@ std::expected<ExtendedPaymentComponents, TER>
computeLatePayment(
Asset const& asset,
ApplyView const& view,
SLE::const_ref loan,
Number const& principalOutstanding,
std::int32_t nextDueDate,
ExtendedPaymentComponents const& periodic,
TenthBips32 lateInterestRate,
std::int32_t loanScale,
Number const& latePaymentFee,
STAmount const& amount,
TenthBips16 managementFeeRate,
beast::Journal j)
{
std::int32_t const nextDueDate = loan->at(sfNextPaymentDueDate);
std::int32_t const loanScale = loan->at(sfLoanScale);
// Check if the due date has passed. If not, reject the payment as
// being too soon
if (!hasExpired(view, nextDueDate))
@@ -892,15 +883,15 @@ computeLatePayment(
// Calculate the penalty interest based on how long the payment is overdue.
auto const latePaymentInterest = loanLatePaymentInterest(
loan->at(sfPrincipalOutstanding),
TenthBips32{loan->at(sfLateInterestRate)},
view.parentCloseTime(),
nextDueDate);
principalOutstanding, lateInterestRate, view.parentCloseTime(), nextDueDate);
// Round the late interest and split it between the vault (net interest)
// and the broker (management fee portion).
auto const [roundedLateInterest, roundedLateManagementFee] =
roundAndSplitInterest(asset, latePaymentInterest, managementFeeRate, loanScale);
// and the broker (management fee portion). This lambda ensures we
// round before splitting to maintain precision.
auto const [roundedLateInterest, roundedLateManagementFee] = [&]() {
auto const interest = roundToAsset(asset, latePaymentInterest, loanScale);
return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}();
XRPL_ASSERT(roundedLateInterest >= 0, "xrpl::detail::computeLatePayment : valid late interest");
XRPL_ASSERT_PARTS(
@@ -919,7 +910,7 @@ computeLatePayment(
// 1. Regular service fee (from periodic.untrackedManagementFee)
// 2. Late payment fee (fixed penalty)
// 3. Management fee portion of late interest
periodic.untrackedManagementFee + loan->at(sfLatePaymentFee) + roundedLateManagementFee,
periodic.untrackedManagementFee + latePaymentFee + roundedLateManagementFee,
// Untracked interest includes:
// 1. Any untracked interest from the regular payment (usually 0)
@@ -969,15 +960,22 @@ std::expected<ExtendedPaymentComponents, TER>
computeFullPayment(
Asset const& asset,
ApplyView& view,
SLE::const_ref loan,
Number const& principalOutstanding,
Number const& managementFeeOutstanding,
Number const& periodicPayment,
std::uint32_t paymentRemaining,
std::uint32_t prevPaymentDate,
std::uint32_t const startDate,
std::uint32_t const paymentInterval,
TenthBips32 const closeInterestRate,
std::int32_t loanScale,
Number const& totalInterestOutstanding,
Number const& periodicRate,
Number const& closePaymentFee,
STAmount const& amount,
TenthBips16 managementFeeRate,
beast::Journal j)
{
std::uint32_t const paymentRemaining = loan->at(sfPaymentRemaining);
std::int32_t const loanScale = loan->at(sfLoanScale);
// Full payment must be made before the final scheduled payment.
if (paymentRemaining <= 1)
{
@@ -990,7 +988,7 @@ computeFullPayment(
// This theoretical (unrounded) value is used to compute interest and
// penalties accurately.
Number const theoreticalPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
view.rules(), loan->at(sfPeriodicPayment), periodicRate, paymentRemaining);
view.rules(), periodicPayment, periodicRate, paymentRemaining);
// Full payment interest includes both accrued interest (time since last
// payment) and prepayment penalty (for closing early).
@@ -998,21 +996,18 @@ computeFullPayment(
theoreticalPrincipalOutstanding,
periodicRate,
view.parentCloseTime(),
loan->at(sfPaymentInterval),
loan->at(sfPreviousPaymentDueDate),
loan->at(sfStartDate),
TenthBips32{loan->at(sfCloseInterestRate)});
paymentInterval,
prevPaymentDate,
startDate,
closeInterestRate);
// Split the full payment interest into net interest (to vault) and management fee (to broker),
// applying proper rounding.
auto const [roundedFullInterest, roundedFullManagementFee] = roundAndSplitInterest(
asset, fullPaymentInterest, managementFeeRate, loanScale, Number::RoundingMode::Downward);
LoanState const loanState = constructLoanState(loan);
Number const principalOutstanding = loanState.principalOutstanding;
Number const managementFeeOutstanding = loanState.managementFeeDue;
Number const totalInterestOutstanding = loanState.interestDue;
Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
// Split the full payment interest into net interest (to vault) and
// management fee (to broker), applying proper rounding.
auto const [roundedFullInterest, roundedFullManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, fullPaymentInterest, loanScale, Number::RoundingMode::Downward);
return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}();
ExtendedPaymentComponents const full{
PaymentComponents{
@@ -1053,7 +1048,8 @@ computeFullPayment(
"xrpl::detail::computeFullPayment",
"total due is rounded");
JLOG(j.trace()) << "computeFullPayment result: periodicRate: " << periodicRate
JLOG(j.trace()) << "computeFullPayment result: periodicPayment: " << periodicPayment
<< ", periodicRate: " << periodicRate
<< ", paymentRemaining: " << paymentRemaining
<< ", theoreticalPrincipalOutstanding: " << theoreticalPrincipalOutstanding
<< ", fullPaymentInterest: " << fullPaymentInterest
@@ -1304,34 +1300,6 @@ computePaymentComponents(
};
}
/* Thin overload of computePaymentComponents() that unwraps the tracked
* fields directly from the Loan ledger object. `periodicRate` is derived
* rather than stored, and `managementFeeRate` comes from the LoanBroker, not
* the Loan, so both remain explicit parameters. Kept separate from the
* value-based overload above, which is exercised directly by unit tests
* against simulated (non-ledger) loan states.
*/
PaymentComponents
computePaymentComponents(
Rules const& rules,
Asset const& asset,
SLE::ref loan,
Number const& periodicRate,
TenthBips16 managementFeeRate)
{
return computePaymentComponents(
rules,
asset,
loan->at(sfLoanScale),
loan->at(sfTotalValueOutstanding),
loan->at(sfPrincipalOutstanding),
loan->at(sfManagementFeeOutstanding),
loan->at(sfPeriodicPayment),
periodicRate,
loan->at(sfPaymentRemaining),
managementFeeRate);
}
/* Computes payment components for an overpayment scenario.
*
* An overpayment occurs when a borrower pays more than the scheduled periodic
@@ -1376,12 +1344,11 @@ computeOverpaymentComponents(
// This interest doesn't follow the normal amortization schedule - it's
// a one-time charge for paying early.
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
roundAndSplitInterest(
asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate),
managementFeeRate,
loanScale);
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, tenthBipsOfValue(overpayment, overpaymentInterestRate), loanScale);
return detail::computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
}();
auto const result = detail::ExtendedPaymentComponents{
// Build the payment components, after fees and penalty
@@ -1408,265 +1375,6 @@ computeOverpaymentComponents(
return result;
}
/* Derives the two rate values every make*Payment() helper needs: the
* broker's management fee rate, and the loan's periodic (per-payment-period)
* interest rate.
*/
std::pair<TenthBips16, Number>
loanRatesFor(SLE::const_ref loan, SLE::const_ref brokerSle)
{
TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
TenthBips32 const interestRate{loan->at(sfInterestRate)};
Number const periodicRate = loanPeriodicRate(interestRate, loan->at(sfPaymentInterval));
XRPL_ASSERT(interestRate == 0 || periodicRate > 0, "xrpl::detail::loanRatesFor : valid rate");
return {managementFeeRate, periodicRate};
}
/* Handles a full (early payoff) payment. Implements the "full payment"
* branch of the make_payment function from the XLS-66 spec, Section
* 3.2.4.4.
*/
std::expected<LoanPaymentParts, TER>
makeFullPayment(
Asset const& asset,
ApplyView& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
beast::Journal j)
{
auto const [managementFeeRate, periodicRate] = loanRatesFor(loan, brokerSle);
auto const fullPaymentComponents =
computeFullPayment(asset, view, loan, periodicRate, amount, managementFeeRate, j);
// computeFullPayment only ever fails with a genuine error TER (never
// tesSUCCESS), so there is no separate "no-op" outcome to handle here.
if (fullPaymentComponents.has_value())
return doPayment(*fullPaymentComponents, loan);
return std::unexpected(fullPaymentComponents.error());
}
/* Handles a late payment (past due date, with the late-payment flag set).
* Implements the "late payment" branch of the make_payment function from
* the XLS-66 spec, Section 3.2.4.4.
*/
std::expected<LoanPaymentParts, TER>
makeLatePayment(
Asset const& asset,
ApplyView const& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
beast::Journal j)
{
auto const [managementFeeRate, periodicRate] = loanRatesFor(loan, brokerSle);
Number const serviceFee = loan->at(sfLoanServiceFee);
ExtendedPaymentComponents const periodic{
computePaymentComponents(view.rules(), asset, loan, periodicRate, managementFeeRate),
serviceFee};
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::detail::makeLatePayment",
"regular payment valid principal");
auto const latePaymentComponents =
computeLatePayment(asset, view, loan, periodic, amount, managementFeeRate, j);
// computeLatePayment only ever fails with a genuine error TER (never
// tesSUCCESS), so there is no separate "no-op" outcome to handle here.
if (latePaymentComponents.has_value())
return doPayment(*latePaymentComponents, loan);
return std::unexpected(latePaymentComponents.error());
}
/* Handles regular scheduled payments, including an optional overpayment tail.
* Implements the "regular" and "overpayment" branches of the make_payment
* function from the XLS-66 spec, Section 3.2.4.4.
*/
std::expected<LoanPaymentParts, TER>
makeRegularPayment(
Asset const& asset,
ApplyView const& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
LoanPaymentType const paymentType,
beast::Journal j)
{
using namespace Lending;
XRPL_ASSERT_PARTS(
paymentType == LoanPaymentType::Regular || paymentType == LoanPaymentType::Overpayment,
"xrpl::detail::makeRegularPayment",
"regular payment type");
auto const [managementFeeRate, periodicRate] = loanRatesFor(loan, brokerSle);
std::int32_t const loanScale = loan->at(sfLoanScale);
Number const serviceFee = loan->at(sfLoanServiceFee);
ExtendedPaymentComponents periodic{
computePaymentComponents(view.rules(), asset, loan, periodicRate, managementFeeRate),
serviceFee};
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::detail::makeRegularPayment",
"regular payment valid principal");
// Keep a running total of the actual parts paid
LoanPaymentParts totalParts;
Number totalPaid = kNumZero;
std::size_t numPayments = 0;
// Cached here (rather than re-looking up loan->at(sfPaymentRemaining) at each use) since it's
// read multiple times below. It's a write-through proxy, so it still reflects doPayment's
// mutations each iteration.
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
while ((amount >= (totalPaid + periodic.totalDue)) && paymentRemainingProxy > 0 &&
numPayments < kLoanMaximumPaymentsPerTransaction)
{
// Try to make more payments
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::detail::makeRegularPayment",
"payment pays non-negative principal");
totalPaid += periodic.totalDue;
totalParts += doPayment(periodic, loan);
++numPayments;
XRPL_ASSERT_PARTS(
(periodic.specialCase == PaymentSpecialCase::Final) == (paymentRemainingProxy == 0),
"xrpl::detail::makeRegularPayment",
"final payment is the final payment");
// Don't compute the next payment if this was the last payment
if (periodic.specialCase == PaymentSpecialCase::Final)
break;
periodic = ExtendedPaymentComponents{
computePaymentComponents(view.rules(), asset, loan, periodicRate, managementFeeRate),
serviceFee};
}
if (numPayments == 0)
{
JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: " << periodic.totalDue
<< ", paid: " << amount;
return std::unexpected(tecINSUFFICIENT_PAYMENT);
}
XRPL_ASSERT_PARTS(
totalParts.principalPaid + totalParts.interestPaid + totalParts.feePaid == totalPaid,
"xrpl::detail::makeRegularPayment",
"payment parts add up");
XRPL_ASSERT_PARTS(
totalParts.valueChange == 0, "xrpl::detail::makeRegularPayment", "no value change");
// -------------------------------------------------------------
// overpayment handling
//
// If the "fixCleanup3_1_3" amendment is enabled, truncate "amount",
// at the loan scale. If the raw value is used, the overpayment
// amount could be meaningless dust. Trying to process such a small
// amount will, at best, waste time when all the result values round
// to zero. At worst, it can cause logical errors with tiny amounts
// of interest that don't add up correctly.
auto const roundedAmount = view.rules().enabled(fixCleanup3_1_3)
? roundToAsset(asset, amount, loanScale, Number::RoundingMode::TowardsZero)
: amount;
bool const overpaymentSupported =
paymentType == LoanPaymentType::Overpayment && loan->isFlag(lsfLoanOverpayment);
bool const overpaymentAllowed = //
paymentRemainingProxy > 0 && //
totalPaid < roundedAmount && //
numPayments < kLoanMaximumPaymentsPerTransaction;
if (overpaymentSupported && overpaymentAllowed)
{
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)};
// It shouldn't be possible for the overpayment to be greater than
// totalValueOutstanding, because that would have been processed as
// another normal payment. But cap it just in case.
Number const overpaymentRaw =
std::min(roundedAmount - totalPaid, *loan->at(sfTotalValueOutstanding));
bool const fixEnabled = view.rules().enabled(fixCleanup3_2_0);
Number const overpayment = fixEnabled
? roundToAsset(asset, overpaymentRaw, loanScale, Number::RoundingMode::Downward)
: overpaymentRaw;
// Post-amendment, the rounded overpayment can be zero; pre-amendment
// it's always positive given the surrounding guards.
if (!fixEnabled || overpayment > 0)
{
ExtendedPaymentComponents const overpaymentComponents = computeOverpaymentComponents(
view.rules(),
asset,
loanScale,
overpayment,
overpaymentInterestRate,
overpaymentFeeRate,
managementFeeRate);
// Don't process an overpayment if the whole amount (or more!)
// gets eaten by fees and interest.
if (overpaymentComponents.trackedPrincipalDelta > 0)
{
XRPL_ASSERT_PARTS(
overpaymentComponents.untrackedInterest >= beast::kZero,
"xrpl::detail::makeRegularPayment",
"overpayment penalty did not reduce value of loan");
if (auto const overResult = doOverpayment(
view.rules(),
asset,
loanScale,
overpaymentComponents,
loan,
periodicRate,
managementFeeRate,
j))
{
totalParts += *overResult;
}
else if (overResult.error())
{
// error() will be the TER returned if a payment is not
// made. It will only evaluate to true if it's unsuccessful.
// Otherwise, tesSUCCESS means nothing was done, so
// continue.
return std::unexpected(overResult.error());
}
}
}
}
// Check the final results are rounded, to double-check that the
// intermediate steps were rounded.
XRPL_ASSERT(
isRounded(asset, totalParts.principalPaid, loanScale) &&
totalParts.principalPaid >= beast::kZero,
"xrpl::detail::makeRegularPayment : total principal paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.interestPaid, loanScale) &&
totalParts.interestPaid >= beast::kZero,
"xrpl::detail::makeRegularPayment : total interest paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.valueChange, loanScale),
"xrpl::detail::makeRegularPayment : loan value change is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.feePaid, loanScale) && totalParts.feePaid >= beast::kZero,
"xrpl::detail::makeRegularPayment : fee paid is valid");
return totalParts;
}
} // namespace detail
detail::LoanStateDeltas
@@ -1926,10 +1634,8 @@ constructLoanState(
}
LoanState
constructLoanState(SLE::const_ref loan)
constructRoundedLoanState(LoanEntry<ReadView> const& loan)
{
XRPL_ASSERT(loan && loan->getType() == ltLOAN, "xrpl::constructLoanState : valid loan SLE");
return constructLoanState(
loan->at(sfTotalValueOutstanding),
loan->at(sfPrincipalOutstanding),
@@ -2079,14 +1785,20 @@ computeLoanProperties(
std::expected<LoanPaymentParts, TER>
loanMakePayment(
Asset const& asset,
ApplyView& view,
SLE::ref loan,
SLE::const_ref brokerSle,
LoanEntry<ApplyView>& loan,
LoanBrokerEntry<ReadView> const& brokerSle,
STAmount const& amount,
LoanPaymentType const paymentType,
beast::Journal j)
LoanPaymentType const paymentType)
{
if (loan->at(sfPaymentRemaining) == 0 || loan->at(sfPrincipalOutstanding) == 0)
using namespace Lending;
ApplyView& view = loan.applyView();
beast::Journal const j = loan.journal();
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0)
{
// Loan complete this is already checked in LoanPay::preclaim()
// LCOV_EXCL_START
@@ -2095,6 +1807,9 @@ loanMakePayment(
// LCOV_EXCL_STOP
}
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
// Next payment due date must be set unless the loan is complete
auto nextDueDateProxy = loan->at(sfNextPaymentDueDate);
if (*nextDueDateProxy == 0)
@@ -2103,10 +1818,28 @@ loanMakePayment(
return std::unexpected(tecINTERNAL);
}
XRPL_ASSERT(
*loan->at(sfTotalValueOutstanding) > 0, "xrpl::loanMakePayment : valid total value");
std::int32_t const loanScale = loan->at(sfLoanScale);
view.update(loan);
TenthBips32 const interestRate{loan->at(sfInterestRate)};
Number const serviceFee = loan->at(sfLoanServiceFee);
TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
Number const periodicPayment = loan->at(sfPeriodicPayment);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate);
std::uint32_t const startDate = loan->at(sfStartDate);
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
// Compute the periodic rate that will be used for calculations
// throughout
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
XRPL_ASSERT(interestRate == 0 || periodicRate > 0, "xrpl::loanMakePayment : valid rate");
XRPL_ASSERT(*totalValueOutstandingProxy > 0, "xrpl::loanMakePayment : valid total value");
loan.update();
// -------------------------------------------------------------
// A late payment not flagged as late overrides all other options.
@@ -2114,29 +1847,311 @@ loanMakePayment(
{
// If the payment is late, and the late flag was not set, it's not
// valid
JLOG(j.warn()) << "Loan payment is overdue. Use the tfLoanLatePayment transaction flag to "
"make a late payment. Loan was created on "
<< loan->at(sfStartDate) << ", prev payment due date is "
<< loan->at(sfPreviousPaymentDueDate) << ", next payment due date is "
<< nextDueDateProxy << ", ledger time is "
JLOG(j.warn()) << "Loan payment is overdue. Use the tfLoanLatePayment "
"transaction "
"flag to make a late payment. Loan was created on "
<< startDate << ", prev payment due date is " << prevPaymentDateProxy
<< ", next payment due date is " << nextDueDateProxy << ", ledger time is "
<< view.parentCloseTime().time_since_epoch().count();
return std::unexpected(tecEXPIRED);
}
switch (paymentType)
// -------------------------------------------------------------
// full payment handling
if (paymentType == LoanPaymentType::Full)
{
case LoanPaymentType::Full:
return detail::makeFullPayment(asset, view, loan, brokerSle, amount, j);
case LoanPaymentType::Late:
return detail::makeLatePayment(asset, view, loan, brokerSle, amount, j);
case LoanPaymentType::Regular:
case LoanPaymentType::Overpayment:
return detail::makeRegularPayment(asset, view, loan, brokerSle, amount, paymentType, j);
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
LoanState const roundedLoanState = constructLoanState(
totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy);
auto const fullPaymentComponents = detail::computeFullPayment(
asset,
view,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPayment,
paymentRemainingProxy,
prevPaymentDateProxy,
startDate,
paymentInterval,
closeInterestRate,
loanScale,
roundedLoanState.interestDue,
periodicRate,
closePaymentFee,
amount,
managementFeeRate,
j);
if (fullPaymentComponents.has_value())
{
return doPayment(
*fullPaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
}
if (fullPaymentComponents.error())
{
// error() will be the TER returned if a payment is not made. It
// will only evaluate to true if it's unsuccessful. Otherwise,
// tesSUCCESS means nothing was done, so continue.
return std::unexpected(fullPaymentComponents.error());
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::loanMakePayment : invalid full payment result");
JLOG(j.error()) << "Full payment computation failed unexpectedly.";
return std::unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::loanMakePayment : invalid payment type");
return std::unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
// -------------------------------------------------------------
// compute the periodic payment info that will be needed whether the
// payment is late or regular
detail::ExtendedPaymentComponents periodic{
detail::computePaymentComponents(
view.rules(),
asset,
loanScale,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPayment,
periodicRate,
paymentRemainingProxy,
managementFeeRate),
serviceFee};
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::loanMakePayment",
"regular payment valid principal");
// -------------------------------------------------------------
// late payment handling
if (paymentType == LoanPaymentType::Late)
{
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
Number const latePaymentFee = loan->at(sfLatePaymentFee);
auto const latePaymentComponents = detail::computeLatePayment(
asset,
view,
principalOutstandingProxy,
nextDueDateProxy,
periodic,
lateInterestRate,
loanScale,
latePaymentFee,
amount,
managementFeeRate,
j);
if (latePaymentComponents.has_value())
{
return doPayment(
*latePaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
}
if (latePaymentComponents.error())
{
// error() will be the TER returned if a payment is not made. It
// will only evaluate to true if it's unsuccessful.
return std::unexpected(latePaymentComponents.error());
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::loanMakePayment : invalid late payment result");
JLOG(j.error()) << "Late payment computation failed unexpectedly.";
return std::unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
}
// -------------------------------------------------------------
// regular periodic payment handling
XRPL_ASSERT_PARTS(
paymentType == LoanPaymentType::Regular || paymentType == LoanPaymentType::Overpayment,
"xrpl::loanMakePayment",
"regular payment type");
// Keep a running total of the actual parts paid
LoanPaymentParts totalParts;
Number totalPaid;
std::size_t numPayments = 0;
while ((amount >= (totalPaid + periodic.totalDue)) && paymentRemainingProxy > 0 &&
numPayments < kLoanMaximumPaymentsPerTransaction)
{
// Try to make more payments
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"xrpl::loanMakePayment",
"payment pays non-negative principal");
totalPaid += periodic.totalDue;
totalParts += detail::doPayment(
periodic,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
++numPayments;
XRPL_ASSERT_PARTS(
(periodic.specialCase == detail::PaymentSpecialCase::Final) ==
(paymentRemainingProxy == 0),
"xrpl::loanMakePayment",
"final payment is the final payment");
// Don't compute the next payment if this was the last payment
if (periodic.specialCase == detail::PaymentSpecialCase::Final)
break;
periodic = detail::ExtendedPaymentComponents{
detail::computePaymentComponents(
view.rules(),
asset,
loanScale,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPayment,
periodicRate,
paymentRemainingProxy,
managementFeeRate),
serviceFee};
}
if (numPayments == 0)
{
JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: " << periodic.totalDue
<< ", paid: " << amount;
return std::unexpected(tecINSUFFICIENT_PAYMENT);
}
XRPL_ASSERT_PARTS(
totalParts.principalPaid + totalParts.interestPaid + totalParts.feePaid == totalPaid,
"xrpl::loanMakePayment",
"payment parts add up");
XRPL_ASSERT_PARTS(totalParts.valueChange == 0, "xrpl::loanMakePayment", "no value change");
// -------------------------------------------------------------
// overpayment handling
//
// If the "fixCleanup3_1_3" amendment is enabled, truncate "amount",
// at the loan scale. If the raw value is used, the overpayment
// amount could be meaningless dust. Trying to process such a small
// amount will, at best, waste time when all the result values round
// to zero. At worst, it can cause logical errors with tiny amounts
// of interest that don't add up correctly.
auto const roundedAmount = view.rules().enabled(fixCleanup3_1_3)
? roundToAsset(asset, amount, loanScale, Number::RoundingMode::TowardsZero)
: amount;
if (paymentType == LoanPaymentType::Overpayment && loan->isFlag(lsfLoanOverpayment) &&
paymentRemainingProxy > 0 && totalPaid < roundedAmount &&
numPayments < kLoanMaximumPaymentsPerTransaction)
{
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)};
// It shouldn't be possible for the overpayment to be greater than
// totalValueOutstanding, because that would have been processed as
// another normal payment. But cap it just in case.
Number const overpaymentRaw =
std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
bool const fixEnabled = view.rules().enabled(fixCleanup3_2_0);
Number const overpayment = fixEnabled
? roundToAsset(asset, overpaymentRaw, loanScale, Number::RoundingMode::Downward)
: overpaymentRaw;
// Post-amendment, the rounded overpayment can be zero; pre-amendment
// it's always positive given the surrounding guards.
if (!fixEnabled || overpayment > 0)
{
detail::ExtendedPaymentComponents const overpaymentComponents =
detail::computeOverpaymentComponents(
view.rules(),
asset,
loanScale,
overpayment,
overpaymentInterestRate,
overpaymentFeeRate,
managementFeeRate);
// Don't process an overpayment if the whole amount (or more!)
// gets eaten by fees and interest.
if (overpaymentComponents.trackedPrincipalDelta > 0)
{
XRPL_ASSERT_PARTS(
overpaymentComponents.untrackedInterest >= beast::kZero,
"xrpl::loanMakePayment",
"overpayment penalty did not reduce value of loan");
// Can't just use `periodicPayment` here, because it might
// change
auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
if (auto const overResult = detail::doOverpayment(
view.rules(),
asset,
loanScale,
overpaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPaymentProxy,
periodicRate,
paymentRemainingProxy,
managementFeeRate,
j))
{
totalParts += *overResult;
}
else if (overResult.error())
{
// error() will be the TER returned if a payment is not
// made. It will only evaluate to true if it's unsuccessful.
// Otherwise, tesSUCCESS means nothing was done, so
// continue.
return std::unexpected(overResult.error());
}
}
}
}
// Check the final results are rounded, to double-check that the
// intermediate steps were rounded.
XRPL_ASSERT(
isRounded(asset, totalParts.principalPaid, loanScale) &&
totalParts.principalPaid >= beast::kZero,
"xrpl::loanMakePayment : total principal paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.interestPaid, loanScale) &&
totalParts.interestPaid >= beast::kZero,
"xrpl::loanMakePayment : total interest paid is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.valueChange, loanScale),
"xrpl::loanMakePayment : loan value change is valid");
XRPL_ASSERT(
isRounded(asset, totalParts.feePaid, loanScale) && totalParts.feePaid >= beast::kZero,
"xrpl::loanMakePayment : fee paid is valid");
return totalParts;
}
} // namespace xrpl

View File

@@ -10,6 +10,8 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SLEBase.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -28,7 +30,6 @@
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
#include <cstdint>
#include <initializer_list>
#include <limits>
@@ -41,7 +42,8 @@ namespace xrpl {
bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue)
{
if (auto const sle = view.read(keylet::mptokenIssuance(mptIssue.getMptID())))
if (MPTokenIssuanceEntry<ReadView> const sle{
keylet::mptokenIssuance(mptIssue.getMptID()), view})
return sle->isFlag(lsfMPTLocked);
return false;
}
@@ -49,7 +51,7 @@ isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue)
bool
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue)
{
if (auto const sle = view.read(keylet::mptoken(mptIssue.getMptID(), account)))
if (MPTokenEntry<ReadView> const sle{keylet::mptoken(mptIssue.getMptID(), account), view})
return sle->isFlag(lsfMPTLocked);
return false;
}
@@ -81,9 +83,13 @@ isAnyFrozen(
return true;
}
return std::ranges::any_of(accounts, [&](auto const& account) {
return isVaultPseudoAccountFrozen(view, account, mptIssue, depth);
});
for (auto const& account : accounts)
{
if (isVaultPseudoAccountFrozen(view, account, mptIssue, depth))
return true;
}
return false;
}
Rate
@@ -92,7 +98,7 @@ transferRate(ReadView const& view, MPTID const& issuanceID)
// fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000
// For example, if transfer fee is 50% then 10,000 * 50,000 = 500,000
// which represents 50% of 1,000,000,000
if (auto const sle = view.read(keylet::mptokenIssuance(issuanceID));
if (MPTokenIssuanceEntry<ReadView> const sle{keylet::mptokenIssuance(issuanceID), view};
sle && sle->isFieldPresent(sfTransferFee))
{
auto const fee = sle->getFieldU16(sfTransferFee);
@@ -107,7 +113,7 @@ transferRate(ReadView const& view, MPTID const& issuanceID)
canAddHolding(ReadView const& view, MPTIssue const& mptIssue)
{
auto mptID = mptIssue.getMptID();
auto issuance = view.read(keylet::mptokenIssuance(mptID));
MPTokenIssuanceEntry<ReadView> const issuance{keylet::mptokenIssuance(mptID), view};
if (!issuance)
{
return tecOBJECT_NOT_FOUND;
@@ -129,7 +135,7 @@ addEmptyHolding(
beast::Journal journal)
{
auto const& mptID = mptIssue.getMptID();
auto const mpt = ctx.view.peek(keylet::mptokenIssuance(mptID));
MPTokenIssuanceEntry<ApplyView> const mpt{keylet::mptokenIssuance(mptID), ctx.view};
if (!mpt)
return tefINTERNAL; // LCOV_EXCL_LINE
if (mpt->isFlag(lsfMPTLocked))
@@ -152,7 +158,7 @@ authorizeMPToken(
std::uint32_t flags,
std::optional<AccountID> holderID)
{
auto const sleAcct = ctx.view.peek(keylet::account(account));
AccountRootEntry<ApplyView> const sleAcct{keylet::account(account), ctx.view};
if (!sleAcct)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -167,50 +173,28 @@ authorizeMPToken(
if ((flags & tfMPTUnauthorize) != 0u)
{
auto const mptokenKey = keylet::mptoken(mptIssuanceID, account);
auto const sleMpt = ctx.view.peek(mptokenKey);
MPTokenEntry<ApplyView> sleMpt{mptokenKey, ctx.view, journal};
if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0 ||
(ctx.view.rules().enabled(fixCleanup3_1_3) &&
(*sleMpt)[~sfLockedAmount].valueOr(0) != 0))
return tecINTERNAL; // LCOV_EXCL_LINE
if (!ctx.view.dirRemove(
keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false))
return tecINTERNAL; // LCOV_EXCL_LINE
decreaseOwnerCountForObject(ctx.view, sleAcct, sleMpt, 1, journal);
ctx.view.erase(sleMpt);
return tesSUCCESS;
// Unlink from the holder's owner directory, decrement its
// OwnerCount (refunding a reserve sponsor if present), and erase.
// See MPTokenEntry.
return sleMpt.destroy();
}
// A potential holder wants to authorize/hold a mpt, the ledger must:
// - 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.
// 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;
}
// A potential holder wants to authorize/hold a mpt. The trust-line-style
// free-tier reserve rule and reserve-sponsorship accounting are applied
// by MPTokenEntry::create() below.
// Defensive check before we attempt to create MPToken for the issuer
auto const mpt = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
MPTokenIssuanceEntry<ReadView> const mpt{keylet::mptokenIssuance(mptIssuanceID), ctx.view};
if (!mpt || mpt->getAccountID(sfIssuer) == account)
{
// LCOV_EXCL_START
@@ -221,23 +205,22 @@ authorizeMPToken(
}
auto const mptokenKey = keylet::mptoken(mptIssuanceID, account);
auto mptoken = std::make_shared<SLE>(mptokenKey);
if (auto ter = dirLink(ctx.view, account, mptoken))
return ter; // LCOV_EXCL_LINE
// Build with the ApplyViewContext so create() can apply reserve
// sponsorship (reserve check, OwnerCount, and sponsor stamp).
MPTokenEntry<ApplyView> mptoken{mptokenKey, ctx, journal};
mptoken.newSLE();
(*mptoken)[sfAccount] = account;
(*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID;
(*mptoken)[sfFlags] = 0;
ctx.view.insert(mptoken);
// Update owner count.
increaseOwnerCount(ctx.view, sleAcct, sponsorSle, 1, journal);
addSponsorToLedgerEntry(mptoken, sponsorSle);
return tesSUCCESS;
// Trust-line-style free-tier reserve check + link into the holder's
// owner directory + bump the holder's OwnerCount + stamp any reserve
// sponsor + insert. See MPTokenEntry::create().
return mptoken.create(priorBalance);
}
auto const sleMptIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
MPTokenIssuanceEntry<ReadView> const sleMptIssuance{
keylet::mptokenIssuance(mptIssuanceID), ctx.view};
if (!sleMptIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -247,7 +230,7 @@ authorizeMPToken(
if (account != (*sleMptIssuance)[sfIssuer])
return tecINTERNAL; // LCOV_EXCL_LINE
auto const sleMpt = ctx.view.peek(keylet::mptoken(mptIssuanceID, *holderID));
MPTokenEntry<ApplyView> sleMpt{keylet::mptoken(mptIssuanceID, *holderID), ctx.view};
if (!sleMpt)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -270,7 +253,7 @@ authorizeMPToken(
if (flagsIn != flagsOut)
sleMpt->setFieldU32(sfFlags, flagsOut);
ctx.view.update(sleMpt);
sleMpt.update();
return tesSUCCESS;
}
@@ -286,7 +269,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 = ctx.view.peek(keylet::mptoken(mptID, accountID));
MPTokenEntry<ApplyView> mptoken{keylet::mptoken(mptID, accountID), ctx.view};
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
@@ -337,7 +320,7 @@ requireAuth(
};
auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
auto const sleIssuance = view.read(mptID);
MPTokenIssuanceEntry<ReadView> const sleIssuance{mptID, view};
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
@@ -362,13 +345,14 @@ requireAuth(
}
// requireAuth is recursive if the issuer is a vault pseudo-account
auto const sleIssuer = view.read(keylet::account(mptIssuer));
AccountRootEntry<ReadView> const sleIssuer{keylet::account(mptIssuer), view};
if (!sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
if (sleIssuer->isFieldPresent(sfVaultID))
{
auto const sleVault = view.read(keylet::vault(sleIssuer->getFieldH256(sfVaultID)));
VaultEntry<ReadView> const sleVault{
keylet::vault(sleIssuer->getFieldH256(sfVaultID)), view};
if (!sleVault)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -384,7 +368,7 @@ requireAuth(
}
auto const mptokenID = keylet::mptoken(mptID.key, account);
auto const sleToken = view.read(mptokenID);
MPTokenEntry<ReadView> const sleToken{mptokenID, view};
// if account has no MPToken, fail
if (!sleToken && (authType == AuthType::StrongAuth || authType == AuthType::Legacy))
@@ -432,7 +416,8 @@ enforceMPTokenAuthorization(
XRPAmount const& priorBalance, // for MPToken authorization
beast::Journal j)
{
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
MPTokenIssuanceEntry<ReadView> const sleIssuance{
keylet::mptokenIssuance(mptIssuanceID), ctx.view};
if (!sleIssuance)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -444,7 +429,7 @@ enforceMPTokenAuthorization(
return tefINTERNAL; // LCOV_EXCL_LINE
auto const keylet = keylet::mptoken(mptIssuanceID, account);
auto const sleToken = ctx.view.read(keylet); // NOTE: might be null
MPTokenEntry<ReadView> const sleToken{keylet, ctx.view}; // NOTE: might be null
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
bool expired = false;
bool const authorizedByDomain = [&]() -> bool {
@@ -460,7 +445,7 @@ enforceMPTokenAuthorization(
return false;
}();
if (!authorizedByDomain && sleToken == nullptr)
if (!authorizedByDomain && !sleToken)
{
// Could not find MPToken and won't create one, could be either of:
//
@@ -483,14 +468,14 @@ enforceMPTokenAuthorization(
// We found an MPToken, but sfDomainID is not set, so this is a classic
// MPToken which requires authorization by the token issuer.
XRPL_ASSERT(
sleToken != nullptr && !maybeDomainID.has_value(),
sleToken && !maybeDomainID.has_value(),
"xrpl::enforceMPTokenAuthorization : found MPToken");
if (sleToken->isFlag(lsfMPTAuthorized))
return tesSUCCESS;
return tecNO_AUTH;
}
if (authorizedByDomain && sleToken != nullptr)
if (authorizedByDomain && sleToken)
{
// Found an MPToken, authorized by the domain. Ignore authorization flag
// lsfMPTAuthorized because it is meaningless. Return tesSUCCESS
@@ -504,7 +489,7 @@ enforceMPTokenAuthorization(
// Could not find MPToken but there should be one because we are
// authorized by domain. Proceed to create it, then return tesSUCCESS
XRPL_ASSERT(
maybeDomainID.has_value() && sleToken == nullptr,
maybeDomainID.has_value() && !sleToken,
"xrpl::enforceMPTokenAuthorization : new MPToken for domain");
if (auto const err = authorizeMPToken(
ctx,
@@ -557,7 +542,7 @@ canTransfer(
std::uint8_t depth)
{
auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
auto const sleIssuance = view.read(mptID);
MPTokenIssuanceEntry<ReadView> const sleIssuance{mptID, view};
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
@@ -587,8 +572,8 @@ canTransfer(
// LCOV_EXCL_STOP
}
auto const sleHolding =
view.read(keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)));
ReadOnlySLE const sleHolding{
keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)), view};
if (!sleHolding)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -610,7 +595,8 @@ canTrade(ReadView const& view, Asset const& asset, std::uint8_t depth)
return asset.visit(
[&](Issue const&) -> TER { return tesSUCCESS; },
[&](MPTIssue const& mptIssue) -> TER {
auto const sleIssuance = view.read(keylet::mptokenIssuance(mptIssue.getMptID()));
MPTokenIssuanceEntry<ReadView> const sleIssuance{
keylet::mptokenIssuance(mptIssue.getMptID()), view};
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanTrade))
@@ -632,8 +618,8 @@ canTrade(ReadView const& view, Asset const& asset, std::uint8_t depth)
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const sleHolding =
view.read(keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)));
ReadOnlySLE const sleHolding{
keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)), view};
if (!sleHolding)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -665,7 +651,7 @@ lockEscrowMPT(ApplyView& view, AccountID const& sender, STAmount const& amount,
{
auto const mptIssue = amount.get<MPTIssue>();
auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
auto sleIssuance = view.peek(mptID);
MPTokenIssuanceEntry<ApplyView> sleIssuance{mptID, view};
if (!sleIssuance)
{ // LCOV_EXCL_START
JLOG(j.error()) << "lockEscrowMPT: MPT issuance not found for " << mptIssue.getMptID();
@@ -682,7 +668,7 @@ lockEscrowMPT(ApplyView& view, AccountID const& sender, STAmount const& amount,
// 2. Increase the MPT Holder EscrowedAmount
{
auto const mptokenID = keylet::mptoken(mptID.key, sender);
auto sle = view.peek(mptokenID);
MPTokenEntry<ApplyView> sle{mptokenID, view};
if (!sle)
{ // LCOV_EXCL_START
JLOG(j.error()) << "lockEscrowMPT: MPToken not found for " << sender;
@@ -721,7 +707,7 @@ lockEscrowMPT(ApplyView& view, AccountID const& sender, STAmount const& amount,
sle->setFieldU64(sfLockedAmount, pay);
}
view.update(sle);
sle.update();
}
// 1. Increase the Issuance EscrowedAmount
@@ -748,7 +734,7 @@ lockEscrowMPT(ApplyView& view, AccountID const& sender, STAmount const& amount,
sleIssuance->setFieldU64(sfLockedAmount, pay);
}
view.update(sleIssuance);
sleIssuance.update();
}
return tesSUCCESS;
}
@@ -770,7 +756,7 @@ unlockEscrowMPT(
auto const& issuer = netAmount.getIssuer();
auto const& mptIssue = netAmount.get<MPTIssue>();
auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
auto sleIssuance = view.peek(mptID);
MPTokenIssuanceEntry<ApplyView> sleIssuance{mptID, view};
if (!sleIssuance)
{ // LCOV_EXCL_START
JLOG(j.error()) << "unlockEscrowMPT: MPT issuance not found for " << mptIssue.getMptID();
@@ -806,14 +792,14 @@ unlockEscrowMPT(
{
sleIssuance->setFieldU64(sfLockedAmount, newLocked);
}
view.update(sleIssuance);
sleIssuance.update();
}
if (issuer != receiver)
{
// Increase the MPT Holder MPTAmount
auto const mptokenID = keylet::mptoken(mptID.key, receiver);
auto sle = view.peek(mptokenID);
MPTokenEntry<ApplyView> sle{mptokenID, view};
if (!sle)
{ // LCOV_EXCL_START
JLOG(j.error()) << "unlockEscrowMPT: MPToken not found for " << receiver;
@@ -832,7 +818,7 @@ unlockEscrowMPT(
} // LCOV_EXCL_STOP
(*sle)[sfMPTAmount] += delta;
view.update(sle);
sle.update();
}
else
{
@@ -849,7 +835,7 @@ unlockEscrowMPT(
} // LCOV_EXCL_STOP
sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem);
view.update(sleIssuance);
sleIssuance.update();
}
if (issuer == sender)
@@ -860,7 +846,7 @@ unlockEscrowMPT(
} // LCOV_EXCL_STOP
// Decrease the MPT Holder EscrowedAmount
auto const mptokenID = keylet::mptoken(mptID.key, sender);
auto sle = view.peek(mptokenID);
MPTokenEntry<ApplyView> sle{mptokenID, view};
if (!sle)
{ // LCOV_EXCL_START
JLOG(j.error()) << "unlockEscrowMPT: MPToken not found for " << sender;
@@ -893,7 +879,7 @@ unlockEscrowMPT(
{
sle->setFieldU64(sfLockedAmount, newLocked);
}
view.update(sle);
sle.update();
// Note: The gross amount is the amount that was locked, the net
// amount is the amount that is being unlocked. The difference is the fee
@@ -912,7 +898,7 @@ unlockEscrowMPT(
} // LCOV_EXCL_STOP
sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - diff);
view.update(sleIssuance);
sleIssuance.update();
}
return tesSUCCESS;
}
@@ -933,15 +919,16 @@ createMPToken(
if (!ownerNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
auto mptoken = std::make_shared<SLE>(mptokenKey);
MPTokenEntry<ApplyView> mptoken{mptokenKey, view};
mptoken.newSLE();
(*mptoken)[sfAccount] = account;
(*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID;
(*mptoken)[sfFlags] = flags;
(*mptoken)[sfOwnerNode] = *ownerNode;
addSponsorToLedgerEntry(mptoken, sponsorSle);
addSponsorToLedgerEntry(mptoken.mutableSle(), sponsorSle);
view.insert(mptoken);
mptoken.insert();
return tesSUCCESS;
}
@@ -966,13 +953,12 @@ checkCreateMPT(
{
return err;
}
auto const sleAcct = view.peek(keylet::account(holder));
AccountRootEntry<ApplyView> const sleAcct{keylet::account(holder), view};
if (!sleAcct)
{
return tecINTERNAL;
}
increaseOwnerCount(view, sleAcct, sponsorSle, 1, j);
increaseOwnerCount(view, sleAcct.mutableSle(), sponsorSle, 1, j);
}
return tesSUCCESS;
}
@@ -994,7 +980,7 @@ availableMPTAmount(SLE const& sleIssuance)
std::int64_t
availableMPTAmount(ReadView const& view, MPTID const& mptID)
{
auto const sle = view.read(keylet::mptokenIssuance(mptID));
MPTokenIssuanceEntry<ReadView> const sle{keylet::mptokenIssuance(mptID), view};
if (!sle)
Throw<std::runtime_error>(transHuman(tecINTERNAL));
return availableMPTAmount(*sle);
@@ -1018,7 +1004,7 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue)
{
STAmount amount{issue};
auto const sle = view.read(keylet::mptokenIssuance(issue));
MPTokenIssuanceEntry<ReadView> const sle{keylet::mptokenIssuance(issue), view};
if (!sle)
return amount;
auto const available = availableMPTAmount(*sle);

View File

@@ -8,8 +8,8 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -182,7 +182,8 @@ getPageForToken(
? narr[kDirMaxTokensPerPage - 1].getFieldH256(sfNFTokenID).next()
: carr[0].getFieldH256(sfNFTokenID);
auto np = std::make_shared<SLE>(keylet::nftokenPage(base, tokenIDForNewPage));
NFTokenPageEntry<ApplyView> np{base, tokenIDForNewPage, view};
np.newSLE();
XRPL_ASSERT(np->key() > base.key, "xrpl::nft::getPageForToken : valid NFT page index");
np->setFieldArray(sfNFTokens, narr);
np->setFieldH256(sfNextPageMin, cp->key());
@@ -191,14 +192,14 @@ getPageForToken(
{
np->setFieldH256(sfPreviousPageMin, *ppm);
if (auto p3 = view.peek(Keylet(ltNFTOKEN_PAGE, *ppm)))
if (NFTokenPageEntry<ApplyView> p3{Keylet(ltNFTOKEN_PAGE, *ppm), view})
{
p3->setFieldH256(sfNextPageMin, np->key());
view.update(p3);
p3.update();
}
}
view.insert(np);
np.insert();
cp->setFieldArray(sfNFTokens, carr);
cp->setFieldH256(sfPreviousPageMin, np->key());
@@ -206,7 +207,7 @@ getPageForToken(
createCallback(view, owner);
return (first.key < np->key()) ? np : cp;
return (first.key < np->key()) ? np.mutableSle() : cp;
}
bool
@@ -230,7 +231,7 @@ changeTokenURI(
uint256 const& nftokenID,
std::optional<xrpl::Slice> const& uri)
{
SLE::pointer const page = locatePage(view, owner, nftokenID);
NFTokenPageEntry<ApplyView> page{locatePage(view, owner, nftokenID), view};
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
@@ -254,7 +255,7 @@ changeTokenURI(
nftIter->makeFieldAbsent(sfURI);
}
view.update(page);
page.update();
return tesSUCCESS;
}
@@ -269,10 +270,16 @@ insertToken(ApplyView& view, AccountID owner, STObject&& nft)
// First, we need to locate the page the NFT belongs to, creating it
// if necessary. This operation may fail if it is impossible to insert
// the NFT.
SLE::pointer const page =
getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) {
increaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
});
NFTokenPageEntry<ApplyView> page{
getPageForToken(
view,
owner,
nft[sfNFTokenID],
[](ApplyView& view, AccountID const& owner) {
increaseOwnerCount(
view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
}),
view};
if (!page)
return tecNO_SUITABLE_NFTOKEN_PAGE;
@@ -288,7 +295,7 @@ insertToken(ApplyView& view, AccountID owner, STObject&& nft)
page->setFieldArray(sfNFTokens, arr);
}
view.update(page);
page.update();
return tesSUCCESS;
}
@@ -332,13 +339,13 @@ mergePages(ApplyView& view, SLE::ref p1, SLE::ref p2)
if (auto const ppm = (*p1)[~sfPreviousPageMin])
{
auto p0 = view.peek(Keylet(ltNFTOKEN_PAGE, *ppm));
NFTokenPageEntry<ApplyView> p0{Keylet(ltNFTOKEN_PAGE, *ppm), view};
if (!p0)
Throw<std::runtime_error>("mergePages: p0 can't be located!");
p0->setFieldH256(sfNextPageMin, p2->key());
view.update(p0);
p0.update();
p2->setFieldH256(sfPreviousPageMin, *ppm);
}
@@ -355,13 +362,13 @@ mergePages(ApplyView& view, SLE::ref p1, SLE::ref p2)
TER
removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID)
{
SLE::pointer const page = locatePage(view, owner, nftokenID);
NFTokenPageEntry<ApplyView> const page{locatePage(view, owner, nftokenID), view};
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
return tecNO_ENTRY;
return removeToken(view, owner, nftokenID, page);
return removeToken(view, owner, nftokenID, page.mutableSle());
}
/**
@@ -402,8 +409,8 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
return page2;
};
auto const prev = loadPage(curr, sfPreviousPageMin);
auto const next = loadPage(curr, sfNextPageMin);
NFTokenPageEntry<ApplyView> prev{loadPage(curr, sfPreviousPageMin), view};
NFTokenPageEntry<ApplyView> next{loadPage(curr, sfNextPageMin), view};
if (!arr.empty())
{
@@ -415,10 +422,10 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
std::uint32_t cnt = 0;
if (prev && mergePages(view, prev, curr))
if (prev && mergePages(view, prev.mutableSle(), curr))
++cnt;
if (next && mergePages(view, curr, next))
if (next && mergePages(view, curr, next.mutableSle()))
++cnt;
if (cnt != 0)
@@ -449,9 +456,9 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
curr->at(sfPreviousPageMin) = *prevLink;
// Also fix up the NextPageMin link in the new Previous.
auto const newPrev = loadPage(curr, sfPreviousPageMin);
NFTokenPageEntry<ApplyView> newPrev{loadPage(curr, sfPreviousPageMin), view};
newPrev->at(sfNextPageMin) = curr->key();
view.update(newPrev);
newPrev.update();
}
else
{
@@ -461,7 +468,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
view.update(curr);
view.erase(prev);
prev.erase();
return tesSUCCESS;
}
@@ -476,7 +483,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
prev->makeFieldAbsent(sfNextPageMin);
}
view.update(prev);
prev.update();
}
if (next)
@@ -491,7 +498,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
next->makeFieldAbsent(sfPreviousPageMin);
}
view.update(next);
next.update();
}
view.erase(curr);
@@ -521,7 +528,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
std::optional<STObject>
findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID)
{
SLE::const_pointer const page = locatePage(view, owner, nftokenID);
NFTokenPageEntry<ReadView> const page{locatePage(view, owner, nftokenID), view};
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
@@ -569,7 +576,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
do
{
auto const page = view.peek(keylet::page(directory, *pageIndex));
DirectoryNodeEntry<ApplyView> const page{keylet::page(directory, *pageIndex), view};
if (!page)
break;
@@ -587,9 +594,10 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
// deleting during iteration.
for (int i = offerIndexes.size() - 1; i >= 0; --i)
{
if (auto const offer = view.peek(keylet::nftokenOffer(offerIndexes[i])))
if (NFTokenOfferEntry<ApplyView> const offer{
keylet::nftokenOffer(offerIndexes[i]), view})
{
if (deleteTokenOffer(view, offer))
if (deleteTokenOffer(view, offer.mutableSle()))
{
++deletedOffersCount;
}
@@ -614,25 +622,11 @@ deleteTokenOffer(ApplyView& view, SLE::ref offer)
if (offer->getType() != ltNFTOKEN_OFFER)
return false;
auto const owner = (*offer)[sfOwner];
if (!view.dirRemove(keylet::ownerDir(owner), (*offer)[sfOwnerNode], offer->key(), false))
return false;
auto const nftokenID = (*offer)[sfNFTokenID];
if (!view.dirRemove(
offer->isFlag(lsfSellNFToken) ? keylet::nftSells(nftokenID)
: keylet::nftBuys(nftokenID),
(*offer)[sfNFTokenOfferNode],
offer->key(),
false))
return false;
decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
view.erase(offer);
return true;
// Unlink from the owner directory and the token's buy/sell offer directory,
// decrement the owner's OwnerCount, and erase. See
// NFTokenOfferEntry::destroy().
NFTokenOfferEntry<ApplyView> offerEntry{offer, view};
return isTesSuccess(offerEntry.destroy());
}
bool
@@ -729,7 +723,7 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
nextPage->at(sfPreviousPageMin) = *prevLink;
// Also fix up the NextPageMin link in the new Previous.
auto const newPrev = view.peek(Keylet(ltNFTOKEN_PAGE, *prevLink));
NFTokenPageEntry<ApplyView> newPrev{Keylet(ltNFTOKEN_PAGE, *prevLink), view};
if (!newPrev)
{
// LCOV_EXCL_START
@@ -739,7 +733,7 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
// LCOV_EXCL_STOP
}
newPrev->at(sfNextPageMin) = nextPage->key();
view.update(newPrev);
newPrev.update();
}
view.erase(page);
view.insert(nextPage);
@@ -844,7 +838,7 @@ tokenOfferCreatePreclaim(
if (nftIssuer != acctID && ((nftFlags & nft::kFlagTransferable) == 0))
{
auto const root = view.read(keylet::account(nftIssuer));
AccountRootEntry<ReadView> const root{keylet::account(nftIssuer), view};
XRPL_ASSERT(root, "xrpl::nft::tokenOfferCreatePreclaim : non-null account");
if (auto minter = (*root)[~sfNFTokenMinter]; minter != acctID)
@@ -869,7 +863,7 @@ tokenOfferCreatePreclaim(
{
// If a destination is specified, the destination must already be in
// the ledger.
auto const sleDst = view.read(keylet::account(*dest));
AccountRootEntry<ReadView> const sleDst{keylet::account(*dest), view};
if (!sleDst)
return tecNO_DST;
@@ -881,7 +875,7 @@ tokenOfferCreatePreclaim(
if (owner)
{
auto const sleOwner = view.read(keylet::account(*owner));
AccountRootEntry<ReadView> const sleOwner{keylet::account(*owner), view};
// defensively check
// it should not be possible to specify owner that doesn't exist
@@ -920,63 +914,27 @@ tokenOfferCreateApply(
beast::Journal j,
std::uint32_t txFlags)
{
Keylet const acctKeylet = keylet::account(acctID);
if (auto const acct = view.read(acctKeylet);
priorBalance < accountReserve(view, acct, j, {.ownerCountDelta = 1}))
return tecINSUFFICIENT_RESERVE;
auto const offerID = keylet::nftokenOffer(acctID, seqProxy.value());
// Create the offer:
{
// Token offers are always added to the owner's owner directory:
auto const ownerNode =
view.dirInsert(keylet::ownerDir(acctID), offerID, describeOwnerDir(acctID));
bool const isSellOffer = (txFlags & tfSellNFToken) != 0u;
if (!ownerNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
NFTokenOfferEntry<ApplyView> offer{offerID, view, j};
offer.newSLE();
(*offer)[sfOwner] = acctID;
(*offer)[sfNFTokenID] = nftokenID;
(*offer)[sfAmount] = amount;
(*offer)[sfFlags] = isSellOffer ? lsfSellNFToken : 0u;
bool const isSellOffer = (txFlags & tfSellNFToken) != 0u;
if (expiration)
(*offer)[sfExpiration] = *expiration;
// Token offers are also added to the token's buy or sell offer
// directory
auto const offerNode = view.dirInsert(
isSellOffer ? keylet::nftSells(nftokenID) : keylet::nftBuys(nftokenID),
offerID,
[&nftokenID, isSellOffer](SLE::ref sle) {
(*sle)[sfFlags] = isSellOffer ? lsfNFTokenSellOffers : lsfNFTokenBuyOffers;
(*sle)[sfNFTokenID] = nftokenID;
});
if (dest)
(*offer)[sfDestination] = *dest;
if (!offerNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
std::uint32_t sleFlags = 0;
if (isSellOffer)
sleFlags |= lsfSellNFToken;
auto offer = std::make_shared<SLE>(offerID);
(*offer)[sfOwner] = acctID;
(*offer)[sfNFTokenID] = nftokenID;
(*offer)[sfAmount] = amount;
(*offer)[sfFlags] = sleFlags;
(*offer)[sfOwnerNode] = *ownerNode;
(*offer)[sfNFTokenOfferNode] = *offerNode;
if (expiration)
(*offer)[sfExpiration] = *expiration;
if (dest)
(*offer)[sfDestination] = *dest;
view.insert(offer);
}
// Update owner count.
increaseOwnerCount(view, acctID, {}, 1, j);
return tesSUCCESS;
// Reserve check + link into the owner directory and the token's buy/sell
// offer directory + bump the owner's OwnerCount + insert. See
// NFTokenOfferEntry::create().
return offer.create(priorBalance);
}
TER
@@ -991,7 +949,7 @@ checkTrustlineAuthorized(
if (view.rules().enabled(fixEnforceNFTokenTrustlineV2))
{
auto const issuerAccount = view.read(keylet::account(issue.account));
AccountRootEntry<ReadView> const issuerAccount{keylet::account(issue.account), view};
if (!issuerAccount)
{
JLOG(j.debug()) << "xrpl::nft::checkTrustlineAuthorized: can't "
@@ -1011,7 +969,7 @@ checkTrustlineAuthorized(
if (issuerAccount->isFlag(lsfRequireAuth))
{
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
RippleStateEntry<ReadView> const trustLine{id, issue.account, issue.currency, view};
if (!trustLine)
{
@@ -1043,7 +1001,7 @@ checkTrustlineDeepFrozen(
if (view.rules().enabled(featureDeepFreeze))
{
auto const issuerAccount = view.read(keylet::account(issue.account));
AccountRootEntry<ReadView> const issuerAccount{keylet::account(issue.account), view};
if (!issuerAccount)
{
JLOG(j.debug()) << "xrpl::nft::checkTrustlineDeepFrozen: can't "
@@ -1061,7 +1019,7 @@ checkTrustlineDeepFrozen(
return tesSUCCESS;
}
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
RippleStateEntry<ReadView> const trustLine{id, issue.account, issue.currency, view};
if (!trustLine)
{

View File

@@ -1,14 +1,10 @@
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h> // IWYU pragma: keep
#include <xrpl/protocol/STArray.h> // IWYU pragma: keep
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
@@ -19,47 +15,13 @@ offerDelete(ApplyView& view, SLE::ref sle, beast::Journal j)
{
if (!sle)
return tesSUCCESS;
auto offerIndex = sle->key();
auto owner = sle->getAccountID(sfAccount);
// Detect legacy directories.
uint256 const uDirectory = sle->getFieldH256(sfBookDirectory);
if (!view.dirRemove(keylet::ownerDir(owner), sle->getFieldU64(sfOwnerNode), offerIndex, false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
if (!view.dirRemove(keylet::page(uDirectory), sle->getFieldU64(sfBookNode), offerIndex, false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
if (sle->isFieldPresent(sfAdditionalBooks))
{
XRPL_ASSERT(
sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID),
"xrpl::offerDelete : should be a hybrid domain offer");
auto const& additionalBookDirs = sle->getFieldArray(sfAdditionalBooks);
for (auto const& bookDir : additionalBookDirs)
{
auto const& dirIndex = bookDir.getFieldH256(sfBookDirectory);
auto const& dirNode = bookDir.getFieldU64(sfBookNode);
if (!view.dirRemove(keylet::page(dirIndex), dirNode, offerIndex, false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
}
}
decreaseOwnerCountForObject(view, owner, sle, 1, j);
view.erase(sle);
return tesSUCCESS;
// Unlink the offer from its owner directory and every order-book page it
// sits in (including a hybrid offer's additional books), decrement the
// owner's OwnerCount (refunding a reserve sponsor when present), and erase
// it. See OfferEntry::destroy().
OfferEntry<ApplyView> offer{sle, view, j};
return offer.destroy();
}
} // namespace xrpl

View File

@@ -1,15 +1,13 @@
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
@@ -22,48 +20,28 @@
namespace xrpl {
TER
closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j)
closeChannel(
PayChannelEntry<ApplyView>& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j)
{
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!view.dirRemove(keylet::ownerDir(src), page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from src owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove PayChan from recipient's owner directory, if present.
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from dst owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
// Transfer any remaining balance back to the owner.
AccountRootEntry<ApplyView> sle{src, view};
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
decreaseOwnerCountForObject(view, sle, slep, 1, j);
view.update(sle);
sle.update();
// Remove PayChan from ledger
view.erase(slep);
return tesSUCCESS;
// Unlink the channel from the owner and destination directories, decrement
// the owner's OwnerCount (refunding any reserve sponsor), and erase it. See
// PayChannelEntry::ownerDirs().
return slep.destroy();
}
uint32_t

View File

@@ -7,6 +7,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -31,7 +32,7 @@ accountInDomain(ReadView const& view, AccountID const& account, Domain const& do
// LCOV_EXCL_STOP
}
auto const sleDomain = view.read(keylet::permissionedDomain(domainID));
PermissionedDomainEntry<ReadView> const sleDomain{keylet::permissionedDomain(domainID), view};
if (!sleDomain)
return false;
@@ -42,8 +43,8 @@ accountInDomain(ReadView const& view, AccountID const& account, Domain const& do
auto const& credentials = sleDomain->getFieldArray(sfAcceptedCredentials);
bool const inDomain = std::ranges::any_of(credentials, [&](auto const& credential) {
auto const sleCred = view.read(
keylet::credential(account, credential[sfIssuer], credential[sfCredentialType]));
CredentialEntry<ReadView> const sleCred{
keylet::credential(account, credential[sfIssuer], credential[sfCredentialType]), view};
if (!sleCred || !sleCred->isFlag(lsfAccepted))
return false;
@@ -60,7 +61,7 @@ offerInDomain(
Domain const& domainID,
beast::Journal j)
{
auto const sleOffer = view.read(keylet::offer(offerID));
OfferEntry<ReadView> const sleOffer{keylet::offer(offerID), view};
// The following are defensive checks that should never happen, since this
// function is used to check against the order book offers, which should not

View File

@@ -28,7 +28,6 @@
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
#include <cstdint>
#include <initializer_list>
#include <limits>
@@ -106,9 +105,12 @@ isAnyFrozen(
std::initializer_list<AccountID> const& accounts,
Issue const& issue)
{
return std::ranges::any_of(accounts, [&](auto const& account) {
return isFrozen(view, account, issue.currency, issue.account);
});
for (auto const& account : accounts)
{
if (isFrozen(view, account, issue.currency, issue.account))
return true;
}
return false;
}
bool

View File

@@ -3,6 +3,7 @@
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
@@ -17,7 +18,10 @@
namespace xrpl {
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& assets)
assetsToSharesDeposit(
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& assets)
{
XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesDeposit : non-negative assets");
XRPL_ASSERT(
@@ -41,7 +45,10 @@ assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
}
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares)
sharesToAssetsDeposit(
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& shares)
{
XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsDeposit : non-negative shares");
XRPL_ASSERT(
@@ -65,8 +72,8 @@ sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
[[nodiscard]] std::optional<STAmount>
assetsToSharesWithdraw(
SLE::const_ref vault,
SLE::const_ref issuance,
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& assets,
TruncateShares truncate,
WaiveUnrealizedLoss waive)
@@ -94,8 +101,8 @@ assetsToSharesWithdraw(
[[nodiscard]] std::optional<STAmount>
sharesToAssetsWithdraw(
SLE::const_ref vault,
SLE::const_ref issuance,
VaultEntry<ReadView> const& vault,
MPTokenIssuanceEntry<ReadView> const& issuance,
STAmount const& shares,
WaiveUnrealizedLoss waive)
{
@@ -118,7 +125,10 @@ sharesToAssetsWithdraw(
}
[[nodiscard]] bool
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance)
isSoleShareholder(
ReadView const& view,
AccountID const& account,
MPTokenIssuanceEntry<ReadView> const& issuance)
{
XRPL_ASSERT(
issuance && issuance->getType() == ltMPTOKEN_ISSUANCE,
@@ -130,7 +140,7 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref
auto const shareMPTID =
makeMptID(issuance->getFieldU32(sfSequence), issuance->getAccountID(sfIssuer));
auto const sleToken = view.read(keylet::mptoken(shareMPTID, account));
MPTokenEntry<ReadView> const sleToken{keylet::mptoken(shareMPTID, account), view};
if (!sleToken)
return false; // LCOV_EXCL_LINE

View File

@@ -13,7 +13,6 @@
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <atomic>
#include <cstdint>
#include <exception>
#include <functional>
@@ -53,7 +52,6 @@ DatabaseRotatingImp::rotate(
// callback finishes. Only then will the archive directory be
// deleted.
std::shared_ptr<NodeStore::Backend> oldArchiveBackend;
std::uint64_t copyForwards = 0;
{
std::scoped_lock const lock(mutex_);
@@ -64,28 +62,11 @@ 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
{
@@ -196,18 +177,9 @@ DatabaseRotatingImp::fetchNodeObject(
writable = writableBackend_;
}
// 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);
// Update writable backend with data from the archive backend
if (duplicate)
writable->store(nodeObject);
}
}
}

View File

@@ -23,7 +23,7 @@ namespace {
//------------------------------------------------------------------------------
// clang-format off
// NOLINTNEXTLINE(readability-identifier-naming)
char const* const versionString = "3.3.0-rc1"
char const* const versionString = "3.3.0-b1"
// clang-format on
;

View File

@@ -45,7 +45,7 @@ setCurrentTransactionRules(std::optional<Rules> r)
// 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.
// If enableLargeNumbers is true, then useRulesGuard 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(

View File

@@ -255,47 +255,8 @@ numberFromJson(SField const& field, json::Value const& value)
Throw<std::runtime_error>("not a number");
}
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<std::runtime_error>("number cannot be represented");
}
return STNumber{field, num};
return STNumber{
field, Number{parts.negative, parts.mantissa, parts.exponent, Number::Normalized{}}};
}
} // namespace xrpl

View File

@@ -633,6 +633,20 @@ STObject::getAccountID(SField const& field) const
return getFieldByValue<STAccount>(field);
}
AccountID
STObject::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);
}
Blob
STObject::getFieldVL(SField const& field) const
{
@@ -696,7 +710,7 @@ STObject::getFieldNumber(SField const& field) const
void
STObject::set(std::unique_ptr<STBase> v)
{
set(std::move(*v));
set(std::move(*v.get()));
}
void

View File

@@ -13,7 +13,6 @@
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <cstddef>
#include <stdexcept>
#include <utility>
@@ -160,10 +159,13 @@ STPathSet::isDefault() const
bool
STPath::hasSeen(AccountID const& account, PathAsset const& asset, AccountID const& issuer) const
{
return std::ranges::any_of(path_, [&](auto& p) {
return p.getAccountID() == account && p.getPathAsset() == asset &&
p.getIssuerID() == issuer;
});
for (auto& p : path_)
{
if (p.getAccountID() == account && p.getPathAsset() == asset && p.getIssuerID() == issuer)
return true;
}
return false;
}
json::Value

View File

@@ -72,7 +72,7 @@ STTx::STTx(STObject&& object)
{
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // may throw
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxns();
buildBatchTxnIds();
}
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
@@ -89,7 +89,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // May throw
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxns();
buildBatchTxnIds();
}
STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfTransaction)
@@ -110,7 +110,7 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfT
logicError("Transaction type was mutated during assembly");
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxns();
buildBatchTxnIds();
}
STBase*
@@ -279,9 +279,12 @@ STTx::checkSign(Rules const& rules) const
return std::unexpected("Sponsor: " + ret.error());
}
// Verify batch signer signatures here so they are cached with the rest
// of signature checking.
if (isFieldPresent(sfBatchSigners))
// 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 (auto const ret = checkBatchSign(rules); !ret)
return ret;
@@ -304,28 +307,11 @@ 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, txIds)
: checkBatchSingleSign(signer, txIds);
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules)
: checkBatchSingleSign(signer);
if (!result)
return result;
@@ -455,11 +441,12 @@ STTx::checkSingleSign(STObject const& sigObject) const
}
std::expected<void, std::string>
STTx::checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const
STTx::checkBatchSingleSign(STObject const& batchSigner) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction");
Serializer msg;
serializeBatch(msg, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
serializeBatch(
msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
return singleSignHelper(batchSigner, msg.slice());
}
@@ -542,10 +529,7 @@ multiSignHelper(
}
std::expected<void, std::string>
STTx::checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const
STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction");
// We can ease the computational load inside the loop a bit by
@@ -553,7 +537,8 @@ STTx::checkBatchMultiSign(
// with the stuff that stays constant from signature to signature.
auto const batchSignerAccount = batchSigner.getAccountID(sfAccount);
Serializer dataStart;
serializeBatch(dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
serializeBatch(
dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
dataStart.addBitString(batchSignerAccount);
return multiSignHelper(
batchSigner,
@@ -592,79 +577,38 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
}
void
STTx::buildBatchTxns()
STTx::buildBatchTxnIds()
{
// 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::buildBatchTxns : template applied");
if (getTxnType() != ttBATCH)
XRPL_ASSERT(!isFree(), "STTx::buildBatchTxnIds : template applied");
if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions))
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<std::runtime_error>("Batch has no RawTransactions.");
// LCOV_EXCL_STOP
}
auto const& raw = getFieldArray(sfRawTransactions);
if (raw.size() > kMaxBatchTxCount)
Throw<std::runtime_error>("Batch has too many inner transactions.");
// 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());
// 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());
for (STObject const& rb : raw)
{
if (rb.getFieldU16(sfTransactionType) == ttBATCH)
Throw<std::runtime_error>("Batch inner transaction cannot be a Batch.");
txns.push_back(std::make_shared<STTx const>(STObject{rb}));
}
ids.push_back(rb.getHash(HashPrefix::TransactionId));
}
std::vector<uint256>
std::vector<uint256> const&
STTx::getBatchTransactionIDs() const
{
auto const& txns = getBatchTransactions();
std::vector<uint256> ids;
ids.reserve(txns.size());
for (auto const& stx : txns)
ids.push_back(stx->getTransactionID());
return ids;
}
std::vector<std::shared_ptr<STTx const>> const&
STTx::getBatchTransactions() const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactions : batch transaction");
XRPL_ASSERT(batchTxns_.has_value(), "STTx::getBatchTransactions : batch transactions built");
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : batch transaction");
XRPL_ASSERT(
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);
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_;
}
AccountID
@@ -810,62 +754,86 @@ invalidMPTAmountInTx(STObject const& tx)
}
static bool
isBatchRawTransactionOkay(STTx const& tx, std::string& reason)
isBatchRawTransactionOkay(STObject const& st, std::string& reason)
{
if (!tx.isFieldPresent(sfRawTransactions))
if (!st.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 (tx.getTxnType() != ttBATCH)
if (st.getFieldU16(sfTransactionType) != ttBATCH)
{
reason = "Only Batch transactions may contain raw transactions.";
return false;
}
if (tx.isFieldPresent(sfBatchSigners) &&
tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
if (st.isFieldPresent(sfBatchSigners) &&
st.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
{
reason = "BatchSigners array exceeds max entries.";
reason = "Batch Signers array exceeds max entries.";
return false;
}
// 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())
auto const& rawTxns = st.getFieldArray(sfRawTransactions);
if (rawTxns.size() > kMaxBatchTxCount)
{
if (!passesLocalChecks(*inner, reason))
reason = "Raw Transactions array exceeds max entries.";
return false;
}
for (STObject raw : rawTxns)
{
try
{
auto const tt = safeCast<TxType>(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();
return false;
}
}
return true;
}
bool
passesLocalChecks(STTx const& tx, std::string& reason)
passesLocalChecks(STObject const& st, std::string& reason)
{
if (!isMemoOkay(tx, reason))
if (!isMemoOkay(st, reason))
return false;
if (!isAccountFieldOkay(tx))
if (!isAccountFieldOkay(st))
{
reason = "An account field is invalid.";
return false;
}
if (isPseudoTx(tx))
if (isPseudoTx(st))
{
reason = "Cannot submit pseudo transactions.";
return false;
}
if (invalidMPTAmountInTx(tx))
if (invalidMPTAmountInTx(st))
{
reason = "Amount can not be MPT.";
return false;
}
if (!isBatchRawTransactionOkay(tx, reason))
if (!isBatchRawTransactionOkay(st, reason))
return false;
return true;

View File

@@ -64,7 +64,7 @@ to_string(Currency const& currency)
bool
toCurrency(Currency& currency, std::string const& code)
{
if (code.empty() || code == systemCurrencyCode())
if (code.empty() || (code.compare(systemCurrencyCode()) == 0))
{
currency = beast::kZero;
return true;

View File

@@ -17,7 +17,7 @@
#include <string>
#include <utility>
#include <vector>
#ifdef __clang__
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
#endif
@@ -342,6 +342,6 @@ makeCheckpointer(
} // namespace xrpl
#ifdef __clang__
#if defined(__clang__)
#pragma clang diagnostic pop
#endif

View File

@@ -177,9 +177,9 @@ applyBatchTransactions(
int applied = 0;
for (auto const& stx : batchTxn.getBatchTransactions())
for (STObject rb : batchTxn.getFieldArray(sfRawTransactions))
{
auto const result = applyOneTransaction(*stx);
auto const result = applyOneTransaction(STTx{std::move(rb)});
XRPL_ASSERT(
result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)),
"Outer Batch failure, inner transaction should not be applied");

View File

@@ -5,7 +5,6 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -13,7 +12,6 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
#include <memory>
namespace xrpl {
@@ -90,15 +88,17 @@ ValidBookDirectory::finalize(
return false;
}
return std::ranges::all_of(rootIndexes_, [&](auto const& rootIndex) {
for (auto const& rootIndex : rootIndexes_)
{
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

View File

@@ -16,7 +16,6 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <algorithm>
#include <utility>
namespace xrpl {
@@ -74,8 +73,8 @@ TransfersNotFrozen::finalize(
*/
[[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze);
return std::ranges::all_of(balanceChanges_, [&](auto const& entry) {
auto const& [issue, changes] = entry;
for (auto const& [issue, changes] : balanceChanges_)
{
auto const issuerSle = findIssuer(issue.account, view);
// It should be impossible for the issuer to not be found, but check
// just in case so xrpld doesn't crash in release.
@@ -87,11 +86,20 @@ TransfersNotFrozen::finalize(
enforce,
"xrpl::TransfersNotFrozen::finalize : enforce "
"invariant.");
return !enforce;
if (enforce)
{
return false;
}
continue;
}
return validateIssuerChanges(issuerSle, changes, tx, j, enforce);
});
if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce))
{
return false;
}
}
return true;
}
bool

View File

@@ -162,37 +162,33 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a
}
}
if (!after)
if (after)
{
// 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;
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;
}
}
}
@@ -489,7 +485,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(featureSponsor) || view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureLendingProtocol);
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
@@ -803,49 +799,14 @@ ValidNewAccountRoot::finalize(
//------------------------------------------------------------------------------
static std::optional<STAmount>
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<Issue>().account = issuer;
return balance;
}
void
ValidClawback::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
ValidClawback::visitEntry(bool, SLE::const_ref before, SLE::const_ref)
{
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
@@ -874,109 +835,31 @@ 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);
return amount.asset().visit(
AccountID const& holder = amount.getIssuer();
STAmount const holderBalance = amount.asset().visit(
[&](Issue const& issue) {
AccountID const issuer = tx.getAccountID(sfAccount);
AccountID const& holder = amount.getIssuer();
STAmount const holderBalance = accountHolds(
return 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<Issue>().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) {
auto const holder = tx[~sfHolder];
if (!holder)
{
JLOG(j.fatal()) << "Invariant failed: MPT clawback missing holder";
return !mptV2Enabled;
}
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;
return accountHolds(
view,
holder,
issue,
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
});
if (holderBalance.signum() < 0)
{
JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative";
return false;
}
}
}
else

View File

@@ -15,8 +15,6 @@
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
namespace xrpl {
void
@@ -129,8 +127,8 @@ ValidLoanBroker::finalize(
}
}
return std::ranges::all_of(brokers_, [&](auto const& entry) {
auto const& [brokerID, broker] = entry;
for (auto const& [brokerID, broker] : brokers_)
{
auto const& after =
broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanBroker(brokerID));
@@ -206,8 +204,8 @@ ValidLoanBroker::finalize(
return false;
}
}
return true;
});
}
return true;
}
} // namespace xrpl

View File

@@ -403,7 +403,7 @@ ValidMPTIssuance::finalize(
}
void
ValidMPTBalanceChanges::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
ValidMPTPayment::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
{
if (overflow_)
return;
@@ -465,7 +465,7 @@ ValidMPTBalanceChanges::visitEntry(bool, SLE::const_ref before, SLE::const_ref a
}
bool
ValidMPTBalanceChanges::finalize(
ValidMPTPayment::finalize(
STTx const& tx,
TER const result,
XRPAmount const,

View File

@@ -13,6 +13,8 @@
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/ledger/helpers/SLEBase.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -224,7 +226,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
AccountID const account{ctx.tx[sfAccount]};
AccountID const dst{ctx.tx[sfDestination]};
auto sleDst = ctx.view.read(keylet::account(dst));
AccountRootEntry<ReadView> const sleDst{keylet::account(dst), ctx.view};
if (!sleDst)
return tecNO_DST;
@@ -248,7 +250,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
}
}
auto sleAccount = ctx.view.read(keylet::account(account));
AccountRootEntry<ReadView> const sleAccount{keylet::account(account), ctx.view};
XRPL_ASSERT(sleAccount, "xrpl::AccountDelete::preclaim : non-null account");
if (!sleAccount)
return terNO_ACCOUNT;
@@ -262,8 +264,9 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
Keylet const first = keylet::nftokenPageMin(account);
Keylet const last = keylet::nftokenPageMax(account);
auto const cp = ctx.view.read(
Keylet(ltNFTOKEN_PAGE, ctx.view.succ(first.key, last.key.next()).value_or(last.key)));
NFTokenPageEntry<ReadView> const cp{
Keylet(ltNFTOKEN_PAGE, ctx.view.succ(first.key, last.key.next()).value_or(last.key)),
ctx.view};
if (cp)
return tecHAS_OBLIGATIONS;
@@ -322,7 +325,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
{
// Make sure any directory node types that we find are the kind
// we can delete.
auto sleItem = ctx.view.read(keylet::child(dirEntry));
ReadOnlySLE const sleItem{keylet::child(dirEntry), ctx.view};
if (!sleItem)
{
// Directory node has an invalid index. Bail out.
@@ -351,11 +354,11 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
TER
AccountDelete::doApply()
{
auto src = view().peek(keylet::account(accountID_));
AccountRootEntry<ApplyView> src{keylet::account(accountID_), view()};
XRPL_ASSERT(src, "xrpl::AccountDelete::doApply : non-null source account");
auto const dstID = ctx_.tx[sfDestination];
auto dst = view().peek(keylet::account(dstID));
AccountRootEntry<ApplyView> dst{keylet::account(dstID), view()};
XRPL_ASSERT(dst, "xrpl::AccountDelete::doApply : non-null destination account");
if (!src || !dst)
@@ -363,8 +366,8 @@ AccountDelete::doApply()
if (ctx_.tx.isFieldPresent(sfCredentialIDs))
{
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, dstID, dst, ctx_.journal);
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), accountID_, dstID, dst.sle(), ctx_.journal);
!isTesSuccess(err))
return err;
}
@@ -446,8 +449,8 @@ AccountDelete::doApply()
if (remainingBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
dst->clearFlag(lsfPasswordSpent);
view().update(dst);
view().erase(src);
dst.update();
src.erase();
return tesSUCCESS;
}

Some files were not shown because too many files have changed in this diff Show More