mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-18 04:30:21 +00:00
Compare commits
10 Commits
ximinez/pa
...
mvadari/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c0f8f0a7 | ||
|
|
30066af4ac | ||
|
|
d4cb62e78d | ||
|
|
cb2ab92927 | ||
|
|
16e7f81110 | ||
|
|
42c93e333e | ||
|
|
b8b9da92ee | ||
|
|
9d11ba0ca7 | ||
|
|
fb9b10fc03 | ||
|
|
628272a11e |
17
.clang-tidy
17
.clang-tidy
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,9 +11,6 @@ endfunction()
|
||||
function(create_symbolic_link target link)
|
||||
endfunction()
|
||||
|
||||
function(xrpl_add_benchmark name)
|
||||
endfunction()
|
||||
|
||||
macro(exclude_from_default target_)
|
||||
endmacro()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
62
.github/scripts/strategy-matrix/generate.py
vendored
62
.github/scripts/strategy-matrix/generate.py
vendored
@@ -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]})}")
|
||||
|
||||
33
.github/scripts/strategy-matrix/linux.json
vendored
33
.github/scripts/strategy-matrix/linux.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
5
.github/scripts/strategy-matrix/macos.json
vendored
5
.github/scripts/strategy-matrix/macos.json
vendored
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
4
.github/scripts/strategy-matrix/windows.json
vendored
4
.github/scripts/strategy-matrix/windows.json
vendored
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
.github/workflows/conflicting-pr.yml
vendored
1
.github/workflows/conflicting-pr.yml
vendored
@@ -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
|
||||
|
||||
44
.github/workflows/on-pr.yml
vendored
44
.github/workflows/on-pr.yml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -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
|
||||
|
||||
|
||||
21
.github/workflows/reusable-build-test-config.yml
vendored
21
.github/workflows/reusable-build-test-config.yml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/reusable-clang-tidy.yml
vendored
4
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
2
.github/workflows/reusable-upload-recipe.yml
vendored
2
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
11
conan.lock
11
conan.lock
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
599
include/xrpl/ledger/helpers/SLEBase.h
Normal file
599
include/xrpl/ledger/helpers/SLEBase.h
Normal 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
|
||||
1054
include/xrpl/ledger/helpers/SLEWrappers.h
Normal file
1054
include/xrpl/ledger/helpers/SLEWrappers.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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&
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -62,15 +62,5 @@ in
|
||||
runClangTidy
|
||||
vim
|
||||
zip
|
||||
# Rust packages
|
||||
cargo
|
||||
cargo-audit
|
||||
cargo-llvm-cov
|
||||
cargo-nextest
|
||||
clippy
|
||||
corrosion
|
||||
rust-analyzer
|
||||
rustc
|
||||
rustfmt
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user