Compare commits

...

36 Commits

Author SHA1 Message Date
Bart
6c0195ad8c test: Cover the two throws a childless inner node reaches
A childless inner node cannot be built in process, since serializing one asserts
it has a branch, but it can be parsed from a blob. `makeFromPrefix` passes
`hashValid = true` and `makeFullInner` then adopts the hash the node was fetched
under rather than recomputing it, so sixteen zero child hashes give a node whose
own hash is whatever its parent claims. Every check on the way in passes: it
carries no key, so `belongsAt` waves it through, and `canonicalize` and
`canonicalizeChild` both compare against the hash it adopted.

The map that results is well formed by every position rule and still cannot be
walked, which is the one case where `belowHelper` returns nullptr for a node it
has just pushed. Two entry points reach it, and both must refuse rather than
report an absence: an iterator increment would otherwise end the walk early and
drop the rest of the map, and `upperBound` would answer `end()`, which is the
positive claim that no greater key exists. Each throws today, and neither
statement had a test.
2026-09-23 22:25:25 +02:00
Bart
c617b7cf4f refactor: Derive a node's position from its path, not a stored ID
Responds to review feedback on PR 7942, which asked whether the stored ID could
go away entirely. `NodePathStack` stored a `SHAMapNodeID` beside every node, so
the path carried two answers to "where does this node sit" that could disagree.
The path already answers it: a SHAMap has no path compression, so every nibble
down to a leaf has an inner node of its own and entry `i` sits at depth `i`. The
stack now holds `std::vector<SHAMapTreeNodePtr>` and offers `topDepth()`.

No consumer wanted the ID. Each one wanted a depth and read the nibbles it cared
about from a key it already held, so `selectBranch` gains a depth overload and
the ID-taking one becomes a thin wrapper for the sync paths that hold real IDs.
`unshareNode` takes a depth, since it only ever asked `isRoot()`.
`walkTowardsKey`'s no-stack mode counts a depth rather than advancing an ID.

What a path cannot derive is whether the caller descended the branches it says it
did, so that much is still recorded: one `uint256` of nibbles for the whole path
rather than an ID per entry, and a leaf is still judged against every branch
recorded above it. Judging only the last nibble would have accepted a whole
subtree hung under the wrong branch, since one wrong child pointer leaves every
leaf below it agreeing at its own final nibble.
2026-09-23 21:48:41 +02:00
Bart
a9a0dd1f69 test: Cover visitDifferences node skipping
`visitDifferences` had no test coverage: a file-static `populateFetchPack` in
`LedgerMaster.cpp` is its only caller, and nothing exercised that path, so the
private `hasLeafNode` and `hasInnerNode` it consults were untested too. Codecov
flagged the gap against an unrelated refactor that happened to touch a line
inside `hasLeafNode`. Four cases follow: a map missing one item reports only
the nodes on that item's path, an identical map reports nothing, a null
comparison map reports every node the plain `visitNodes` walk sees, and a
callback returning false stops the walk where `populateFetchPack` would stop
once a pack is full.

The first case uses 64 shared items rather than a couple, so the shared leaves
build inner nodes of their own and the walk has whole matching subtrees to skip.
Verified by mutation: with only two shared items, breaking `hasInnerNode` to
always claim a match left every test passing, since the tree was too shallow for
that branch to be taken. The case also asserts that some inner nodes are reported
but not all, which is what distinguishes skipping a subtree from walking it.
Breaking either helper in either direction now fails it.

A new `finalize` helper seals each map before it is compared, since
`visitDifferences` returns early while the root hash is still zero and every
assertion would then hold vacuously. `getHash` is the call that does the work
there, because it unshares a map whose root hash is zero. Each of its six call
sites is wrapped in `ASSERT_NO_FATAL_FAILURE`, since a gtest `ASSERT_` aborts
only the function holding it, so a failure inside the helper would otherwise
leave the calling test running on a map whose root hash is still zero.
2026-09-23 21:48:41 +02:00
Bart
c2b1c5a551 refactor: Move entries off NodePathStack instead of copying them
Five sites copied the top entry out and then popped it. `SHAMapTreeNodePtr` is
refcounted, so each copy bumped the pointee's atomic strong count and the
original's destructor then released it. `releaseNode()` moves the pointer out
instead, a plain swap with no atomic at all. `dirtyUp` and `delItem` walk up to
64 levels per insert or delete on the ledger write path, so this removes up to 64
increments and 64 release sequences per call. The sites that also want the ID
read `top().second` first, which costs the same either way, and the two that read
without popping now bind a reference.

`staticPointerCast` and `dynamicPointerCast` had only a `TT const&` overload, so
no caller could move into them. Each gains an rvalue overload, tied to
`SharedIntrusive<TT>&&` rather than a bare `TT&&` so it cannot bind to an lvalue
in preference to the const-ref one, and the sites that own a discarded pointer
now pass `std::move`. `SharedIntrusive`'s move constructors also become
`noexcept`, so a `std::vector` of them relocates by moving; without that,
`move_if_noexcept` copies every element, since the type is copy constructible.

Three of the casts become static, and a fourth that already was gains the same
live type test, so no traversal path is left paying for a `dynamic_cast`.
`dirtyUp` and `delItem`'s loop rest on every remaining entry being inner, which
holds but was only an `XRPL_ASSERT`, a no-op under `NDEBUG`, so both report
`UNREACHABLE` and throw rather than writing through a misread node.
`updateGiveItem` and `delItem`'s leaf cast need the test for a different reason:
an absent tag leaves an inner node on top, which the public API permits, so they
return false rather than aborting an instrumented build. A test pins that.
2026-09-23 21:48:41 +02:00
Bart
19d8ff8ff5 fix: Reject a misplaced leaf at entry, fail closed if one slips past
`NodePathStack`'s position and depth checks were `XRPL_ASSERT_IF`s, which are
stripped under `NDEBUG`, so a release build walked on with a node sitting where
it did not belong. Each is now a live test that refuses the push and lets the
caller stop, and each reports `SOMETIMES` rather than `UNREACHABLE`: a node
resolved from the local store reaches a walk through `descend(parent, branch)`,
which fetches by the parent's recorded child hash and judges neither position nor
type, so external data can reach either case and neither may abort a build.

Every path that does know the position now judges a node before hooking it: the
two filter descents and the deferred-read hook, each marking the map invalid the
way `addKnownNode` already did. `getMissingNodes` no longer calls
`clearSynching()` on a map it has condemned, at either of its two returns, since
that would move the state to `Modifying` and erase the verdict.
`gmnProcessDeferredReads` became non-static so it can record one.

`boundHelper` now throws where it used to answer `end()`. An empty map still
leaves its root on the path, so an empty path means only that a node was refused,
while `end()` is the positive claim that no key lies on the requested side of the
key asked for. New `SHAMapMisplacedLeaf` tests build a tree whose hashes agree
but whose leaf sits under the wrong branch, drive it in through both acquisition
routes, and check that iteration and both bounds refuse it.
2026-09-23 21:46:45 +02:00
Bart
66ead1a7e0 refactor: Unify upperBound and lowerBound into boundHelper
The two functions were near duplicates: walk to the key, then look for the
nearest leaf on one side. Only the scan direction, the comparison deciding a
leaf qualifies, and whether to take the first or last leaf below the subtree
differed, exactly the distinction `BelowDirection` already draws for
`belowHelper`, so the pair collapse into one parameterized walk. Also drops
the stale `// TODO: what to return here?` above `lowerBound`'s `return end()`:
no predecessor is the correct answer for the smallest key, and the tests pin
it.

Adds the two map shapes the existing tests never built. Every test map held at
least three items, so the root was always an inner node with a populated
branch on either side of a probe. An empty map exercises the childless root,
where the scan finds nothing on any branch and the walk falls through to
`end()`. A single-item map exercises a leaf directly under the root, where
that one leaf decides the outcome.

The single-item test's comment records why `root_` is an inner node even
there: a map built through `addItem` keeps the root it was constructed with,
and only a single-item map synced from a peer (`addRootNode`) replaces `root_`
with a leaf directly. The comment also notes that the leaf entry settles a
probe only when the leaf qualifies against it; for every other probe the leaf
is popped and `root_`'s own inner-node scan is what reaches `end()`.
2026-09-23 19:57:54 +02:00
Bart
00606bec1a fix: Derive traversal node IDs from the branch actually descended (#7942)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-09-22 14:02:11 +00:00
Mayukha Vadari
2bc17c3e73 refactor: Add initial wrapper classes for all SLEs (#7886) 2026-09-21 19:47:23 +00:00
dependabot[bot]
0229c294a9 ci: [DEPENDABOT] bump codecov/codecov-action from 7.0.0 to 7.1.1 in the github-actions group across 1 directory (#8251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-21 12:09:57 +00:00
Bart
184fe173fb refactor: Remove a dead unwrap from the WebSocket success path (#8252)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-09-21 11:23:31 +00:00
Bart
16f7b263fd docs: Correct three comments about null-terminated views (#8253)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-09-21 11:12:09 +00:00
Bart
f6b51f0b8b ci: Say when a package publish is a dry run (#8247)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-09-18 18:31:07 +00:00
Denis Angell
07b36871c7 ci: Publish packages through packages-upload.xrplf.org (#8241) 2026-09-17 18:05:51 +00:00
Bart
638ae08eeb chore: Bump version to 3.5.0-b0 2026-09-17 13:33:08 +02:00
yinyiqian1
24be48ee84 fix: Reject PaymentBurn payments that cross zero balance 2026-09-17 13:33:08 +02:00
Pratik Mankawde
e20f448a71 fix: Reject variable-length prefixes the encoder cannot write 2026-09-17 13:33:07 +02:00
Vito Tumas
f6c80fef68 fix: Relax MPT authorize cap for LoanSet and VaultWithdraw 2026-09-17 13:33:07 +02:00
Gregory Tsipenyuk
a0c12420b5 fix: Skip CheckCash limit waiver for the issuer 2026-09-17 13:33:06 +02:00
Ayaz Salikhov
1245254cac build: Add missing script to conan package 2026-09-17 13:33:06 +02:00
Jingchen
b60636169a fix: Make calculateBaseFee exception-safe 2026-09-17 13:33:06 +02:00
Ayaz Salikhov
d6022fbc4d build: Fix test installation on debian:11 due to EOL 2026-09-17 13:33:06 +02:00
Ayaz Salikhov
295b74da1c build: Add assert-enabled builds and packages 2026-09-17 13:33:05 +02:00
Timothy Banks
faa2bf583f fix: Cap TMTransactions list size and charge fee for undeserializable transactions 2026-09-17 13:33:04 +02:00
Vito Tumas
da260fa42b fix: Relax Loan Invariants to allow zero-principal LoanPay transaction 2026-09-17 13:33:04 +02:00
Timothy Banks
53788b193d test: Add ProtocolMessage harness for testing TMPing 2026-09-17 13:33:03 +02:00
Timothy Banks
227f1b4d9c fix: Unbounded Database Seek via TMGetLedger 2026-09-17 13:33:03 +02:00
Ed Hennis
26b66957ec fix: Trim unknown fields when parsing incoming peer protobuf messages 2026-09-17 13:33:02 +02:00
Mayukha Vadari
54e62a621f fix: Prevent simulate from updating the orderbook db 2026-09-17 13:33:02 +02:00
Timothy Banks
53628b70c0 fix: Use a hardened hash on the STPathElement 2026-09-17 13:33:01 +02:00
Vito Tumas
6fec2c11bf refactor: Rename vault accrual accounting to instant interest recognition (#8237) 2026-09-17 10:27:44 +00:00
Bart
04108a030c refactor: Build the RPC dispatch and command-line tables at compile time (#8006)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 10:01:28 +00:00
Bart
551a19b10d refactor: Add Cluster::isMember, a membership query that copies nothing (#8221)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-09-15 20:40:29 +00:00
Peter Chen
b0a940a383 refactor: Extract common tx-building helpers for ConfidentialMPT in MPTTester (#8135) 2026-09-15 17:13:44 +00:00
Bart
e302e4eeed fix: Set the peer limit total when per-direction limits are configured (#8220)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-09-15 13:14:12 +00:00
Mayukha Vadari
1a4a40ebb8 fix: Update noripple_check to exclude transactions field on error responses (#6303)
Co-authored-by: Timur Yalymov <36795566+tyalymov@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-09-15 00:21:57 +00:00
yinyiqian1
7f55dd390c feat: Support mirror key epochs in confidential MPT transactions for Key Rotation amendment (#8210) 2026-09-14 18:23:38 +00:00
206 changed files with 11956 additions and 2562 deletions

View File

@@ -64,6 +64,7 @@ words:
- blindings
- bookdir
- Bougalis
- bthomee
- Britto
- Btrfs
- Buildx
@@ -141,6 +142,7 @@ words:
- hwrap
- ifndef
- inequation
- Injectivity
- insuf
- insuff
- invasively

View File

@@ -82,7 +82,6 @@ test.app > xrpl.tx
test.basics > test.jtx
test.basics > xrpl.basics
test.basics > xrpl.core
test.basics > xrpld.rpc
test.basics > xrpl.json
test.basics > xrpl.protocol
test.beast > xrpl.basics
@@ -286,10 +285,10 @@ xrpld.perflog > xrpl.basics
xrpld.perflog > xrpl.config
xrpld.perflog > xrpl.core
xrpld.perflog > xrpld.app
xrpld.perflog > xrpld.rpc
xrpld.perflog > xrpl.json
xrpld.perflog > xrpl.nodestore
xrpld.perflog > xrpl.protocol
xrpld.perflog > xrpl.server
xrpld.rpc > xrpl.basics
xrpld.rpc > xrpl.config
xrpld.rpc > xrpl.core

View File

@@ -15,6 +15,14 @@ _BASE_CMAKE_ARGS = [
"-Drust=ON",
]
# The package formats a config can be packaged as, each with its own
# install-test job in reusable-package.yml.
PACKAGE_TYPES = ("deb", "rpm")
# The package name a variant suffixes, as build_pkg.py's BASE_NAME spells it:
# the two have to agree, or the artifact globs miss what was built.
BASE_NAME = "xrpld"
# Maps sanitizer names (as used in cmake) to short config-name suffixes.
_SANITIZER_SUFFIX: dict[str, str] = {
"address": "asan",
@@ -62,10 +70,20 @@ def get_cmake_args(build_type: str, extra_args: str) -> str:
class PackageConfig:
"""The 'package' map of a config whose binaries are also packaged."""
type: str # "deb" or "rpm"; has to match what the image provides
type: str # has to match what the image provides
# The packaging container image: a vanilla distro image, not the nix image
# the config itself builds in.
image: str
# A flavour of the package, named xrpld-<variant>, for a config whose
# binaries are not the plain release build. A variant needs no counterpart
# in the other format.
variant: str = ""
def __post_init__(self) -> None:
assert self.type in PACKAGE_TYPES, (
f"unsupported package type {self.type!r}: "
f"use one of {', '.join(PACKAGE_TYPES)}."
)
@dataclasses.dataclass
@@ -178,6 +196,8 @@ class PackagingEntry:
validator_keys_artifact_name: str
image: str
package_type: str # "deb" or "rpm"; drives the format-specific steps
package_variant: str # passed to build_pkg.py --variant; empty for xrpld
package_name: str # the name it builds under, which the artifact globs use
# ---------------------------------------------------------------------------
@@ -267,12 +287,32 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
validator_keys_artifact_name=f"validator-keys-{name}",
image=cfg.package.image,
package_type=cfg.package.type,
package_variant=cfg.package.variant,
package_name=(
f"{BASE_NAME}-{cfg.package.variant}"
if cfg.package.variant
else BASE_NAME
),
)
)
return entries
def package_names_by_type(entries: list[PackagingEntry]) -> dict[str, list[str]]:
"""The names of the packages in 'entries', keyed by format.
Derived from the packaging matrix rather than listed again, so the packages
the install-test jobs look for are the packages that were built.
"""
return {
package_type: sorted(
{e.package_name for e in entries if e.package_type == package_type}
)
for package_type in PACKAGE_TYPES
}
def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry]:
"""Expand a PlatformFile (macOS or Windows) into matrix entries.
@@ -341,6 +381,10 @@ if __name__ == "__main__":
if args.packaging:
matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json"))
# One list per format, so each install-test job installs the packages its
# own format produced.
for package_type, names in package_names_by_type(matrix).items():
print(f"{package_type}_package_names={json.dumps(names)}")
else:
if args.config in ("linux", None):
matrix += expand_linux_matrix(

View File

@@ -76,6 +76,19 @@
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10"
}
},
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"suffix": "assert",
"extra_cmake_args": "-Dvalidator_keys=ON -Dassert=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10",
"variant": "assert"
}
}
],

View File

@@ -85,6 +85,7 @@ jobs:
.github/workflows/reusable-build-test.yml
.github/workflows/reusable-check-autogen.yml
.github/workflows/reusable-clang-tidy.yml
.github/workflows/reusable-package-test-install.yml
.github/workflows/reusable-package.yml
.github/workflows/reusable-rust.yml
.github/workflows/reusable-strategy-matrix.yml
@@ -189,6 +190,12 @@ jobs:
# matrix (i.e. not yet labeled "Ready to merge" or "Full CI build").
if: ${{ needs.should-run.outputs.go == 'true' && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'Ready to merge') || contains(github.event.pull_request.labels.*.name, 'Full CI build')) }}
uses: ./.github/workflows/reusable-package.yml
with:
# A pull request builds packages to prove they still build, and publishes
# nothing. Stated rather than left to the input's default, so that changing
# that default cannot start publishing from pull requests. No secrets are
# passed either, which is the second reason a publish here cannot succeed.
publish: false
upload-recipe:
needs:

View File

@@ -23,6 +23,7 @@ on:
- ".github/workflows/reusable-build-test.yml"
- ".github/workflows/reusable-check-autogen.yml"
- ".github/workflows/reusable-clang-tidy.yml"
- ".github/workflows/reusable-package-test-install.yml"
- ".github/workflows/reusable-package.yml"
- ".github/workflows/reusable-rust.yml"
- ".github/workflows/reusable-strategy-matrix.yml"

View File

@@ -439,7 +439,7 @@ jobs:
- name: Upload coverage report
if: ${{ github.repository_owner == 'XRPLF' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
disable_search: true
disable_telem: true

View File

@@ -0,0 +1,120 @@
# Install one package format on every distro family it targets, one job per
# package name and image, and run the binaries there. Called once per format by
# reusable-package.yml, which owns the names and the image lists.
name: Install packages
on:
workflow_call:
inputs:
package_type:
description: 'The package format to install ("deb" or "rpm").'
required: true
type: string
package_names:
description: "JSON array of package names built for this format."
required: true
type: string
images:
description: "JSON array of container images to install in."
required: true
type: string
defaults:
run:
shell: bash
env:
PACKAGE_DIR: packages
jobs:
install:
strategy:
fail-fast: false
matrix:
package_name: ${{ fromJson(inputs.package_names) }}
image: ${{ fromJson(inputs.images) }}
name: "${{ matrix.package_name }} on ${{ matrix.image }}"
permissions:
contents: read
runs-on: ubuntu-latest
container: ${{ matrix.image }}
timeout-minutes: 5
steps:
# Every package lands in one directory; the step below picks its own,
# which keeps this independent of the artifact names.
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "*-pkg"
merge-multiple: true
path: ${{ env.PACKAGE_DIR }}
- name: Find the package
id: find
env:
PACKAGE_NAME: ${{ matrix.package_name }}
PACKAGE_TYPE: ${{ inputs.package_type }}
run: |
# The version follows the name, separated by '_' in a DEB and '-' in an
# RPM. Requiring a digit after it is what keeps 'xrpld' from picking up
# another package, such as 'xrpld-assert'.
pattern="${PACKAGE_NAME}[_-][0-9]*.${PACKAGE_TYPE}"
package="$(find "${PACKAGE_DIR}" -type f -name "${pattern}" -print -quit)"
test -n "${package}" || {
echo "no ${pattern} found in ${PACKAGE_DIR}" >&2
exit 1
}
echo "package=${package}" >>"${GITHUB_OUTPUT}"
# Debian 11 went end-of-life on 2026-08-31
# (https://www.debian.org/News/2026/20260831) and its packages are
# already partly gone from deb.debian.org, so switch to the
# snapshot.debian.org entries the image ships commented out in its
# sources.list: they are pinned to the snapshot the image was built
# from, so they serve every version it needs and never go away.
# Snapshots keep their original, long-passed Valid-Until, hence the
# disabled check; the retries absorb snapshot.debian.org's throttling.
- name: Switch Debian 11 to snapshot.debian.org
if: ${{ matrix.image == 'debian:11' }}
run: |
sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list
printf '%s\n' \
'Acquire::Check-Valid-Until "false";' \
'Acquire::Retries "3";' \
>/etc/apt/apt.conf.d/99snapshot
- name: Install the DEB
if: ${{ inputs.package_type == 'deb' }}
env:
DEBIAN_FRONTEND: noninteractive
PACKAGE: ${{ steps.find.outputs.package }}
run: |
# Stock Debian and Ubuntu images carry no package lists, so apt has
# nothing to resolve the systemd dependency from until it fetches them.
apt-get update -qq
apt-get install -y "./${PACKAGE}"
- name: Install the RPM
if: ${{ inputs.package_type == 'rpm' }}
env:
PACKAGE: ${{ steps.find.outputs.package }}
run: dnf install -y "./${PACKAGE}"
- name: Run xrpld
run: xrpld --version
- name: Run validator-keys
run: validator-keys --version
- name: Run rippled, the legacy compatibility symlink
run: rippled --version
- name: Check the service account
run: id xrpld
- name: Check the state directory
run: test -d /var/lib/xrpld
- name: Check the log directory
run: test -d /var/log/xrpld

View File

@@ -3,8 +3,10 @@
#
# - 'package' builds and signs one format per config that carries a "package"
# map in linux.json; that map names the container image and the format
# - 'test-install' installs what was built on a range of distros and runs the
# binaries there, so a package that cannot be installed never reaches Nexus
# - 'test-install-deb' and 'test-install-rpm' call
# reusable-package-test-install.yml to install what was built on a range of
# distros and run the binaries there, so a package that cannot be installed
# never reaches Nexus
# - 'publish' uploads with the image's publish_pkg.py, doing a --dry-run
# unless 'publish: true'
#
@@ -23,7 +25,7 @@ on:
description: "The base URL of the Nexus instance hosting the deb and rpm repositories."
required: false
type: string
default: https://packages.xrplf.org
default: https://packages-upload.xrplf.org
secrets:
remote_username:
@@ -49,6 +51,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
deb_package_names: ${{ steps.generate.outputs.deb_package_names }}
rpm_package_names: ${{ steps.generate.outputs.rpm_package_names }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -107,6 +111,7 @@ jobs:
- name: Build package
env:
PACKAGE_TYPE: ${{ matrix.package_type }}
PACKAGE_VARIANT: ${{ matrix.package_variant }}
PKG_RELEASE: ${{ steps.release_info.outputs.pkg_release }}
CHANNEL: ${{ steps.release_info.outputs.channel }}
run: |
@@ -114,6 +119,7 @@ jobs:
--package-type "${PACKAGE_TYPE}" \
--build-dir "${BUILD_DIR}" \
--pkg-release "${PKG_RELEASE}" \
--variant "${PACKAGE_VARIANT}" \
--channel "${CHANNEL}"
# Before the upload, so the artifact, the tested package and the published
@@ -125,14 +131,17 @@ jobs:
run: ./package/sign_rpm.py --package-dir "${BUILD_DIR}"
# Split from the debug symbols, which are an order of magnitude larger, so
# that test-install downloads only what it installs.
# that test-install downloads only what it installs. In the globs below the
# version follows the name, separated by '_' in a DEB and '-' in an RPM. A
# version starts with a digit and a longer name does not, so that one digit
# is what tells 'xrpld-3.4.1-...' from 'xrpld-assert-3.4.1-...'.
- name: Upload package artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.xrpld_artifact_name }}-pkg
path: |
${{ env.BUILD_DIR }}/debbuild/xrpld_*.deb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/xrpld-[0-9]*.rpm
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}_[0-9]*.deb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/${{ matrix.package_name }}-[0-9]*.rpm
if-no-files-found: error
- name: Upload debug symbol artifact
@@ -140,133 +149,59 @@ jobs:
with:
name: ${{ matrix.xrpld_artifact_name }}-pkg-debug
path: |
${{ env.BUILD_DIR }}/debbuild/xrpld-dbgsym_*.deb
${{ env.BUILD_DIR }}/debbuild/xrpld-dbgsym_*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/xrpld-debuginfo-*.rpm
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}-dbgsym_[0-9]*.deb
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}-dbgsym_[0-9]*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/${{ matrix.package_name }}-debuginfo-[0-9]*.rpm
if-no-files-found: error
# Every distro family the packages target, oldest release first, so both ends
# of the dependency range they declare are exercised.
test-install:
needs: [package]
strategy:
fail-fast: false
matrix:
include:
- package_type: deb
image: debian:11
- package_type: deb
image: debian:12
- package_type: deb
image: debian:13
- package_type: deb
image: ubuntu:20.04
- package_type: deb
image: ubuntu:22.04
- package_type: deb
image: ubuntu:24.04
- package_type: deb
image: ubuntu:26.04
# One call per format, so a variant packaged for one format is installed for
# that format alone. The images are every distro family that format targets,
# oldest release first, so both ends of the dependency range the packages
# declare are exercised.
test-install-deb:
needs: [generate-matrix, package]
name: install deb
uses: ./.github/workflows/reusable-package-test-install.yml
with:
package_type: deb
package_names: ${{ needs.generate-matrix.outputs.deb_package_names }}
images: |
[
"debian:11",
"debian:12",
"debian:13",
"ubuntu:20.04",
"ubuntu:22.04",
"ubuntu:24.04",
"ubuntu:26.04"
]
- package_type: rpm
image: almalinux:9
- package_type: rpm
image: almalinux:10
- package_type: rpm
image: rockylinux/rockylinux:9
- package_type: rpm
image: rockylinux/rockylinux:10
- package_type: rpm
image: registry.access.redhat.com/ubi9/ubi
- package_type: rpm
image: registry.access.redhat.com/ubi10/ubi
name: "install ${{ matrix.package_type }} on ${{ matrix.image }}"
permissions:
contents: read
runs-on: ubuntu-latest
container: ${{ matrix.image }}
timeout-minutes: 5
steps:
# Both formats land in one directory; the step below picks its own by
# extension, so this stays independent of the artifact names.
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "*-pkg"
merge-multiple: true
path: ${{ env.PACKAGE_DIR }}
- name: Find the package
id: find
env:
PACKAGE_TYPE: ${{ matrix.package_type }}
run: |
package="$(find "${PACKAGE_DIR}" -type f -name "*.${PACKAGE_TYPE}" -print -quit)"
test -n "${package}" || {
echo "no .${PACKAGE_TYPE} found in ${PACKAGE_DIR}" >&2
exit 1
}
echo "package=${package}" >>"${GITHUB_OUTPUT}"
# Debian 11 went end-of-life on 2026-08-31
# (https://www.debian.org/News/2026/20260831) and its packages are
# already partly gone from deb.debian.org, so switch to the
# snapshot.debian.org entries the image ships commented out in its
# sources.list: they are pinned to the snapshot the image was built
# from, so they serve every version it needs and never go away.
# Snapshots keep their original, long-passed Valid-Until, hence the
# disabled check; the retries absorb snapshot.debian.org's throttling.
- name: Switch Debian 11 to snapshot.debian.org
if: ${{ matrix.image == 'debian:11' }}
run: |
sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list
printf '%s\n' \
'Acquire::Check-Valid-Until "false";' \
'Acquire::Retries "3";' \
>/etc/apt/apt.conf.d/99snapshot
- name: Install the DEB
if: ${{ matrix.package_type == 'deb' }}
env:
DEBIAN_FRONTEND: noninteractive
PACKAGE: ${{ steps.find.outputs.package }}
run: |
# Stock Debian and Ubuntu images carry no package lists, so apt has
# nothing to resolve the systemd dependency from until it fetches them.
apt-get update -qq
apt-get install -y "./${PACKAGE}"
- name: Install the RPM
if: ${{ matrix.package_type == 'rpm' }}
env:
PACKAGE: ${{ steps.find.outputs.package }}
run: dnf install -y "./${PACKAGE}"
- name: Run xrpld
run: xrpld --version
- name: Run validator-keys
run: validator-keys --version
- name: Run rippled, the legacy compatibility symlink
run: rippled --version
- name: Check the service account
run: id xrpld
- name: Check the state directory
run: test -d /var/lib/xrpld
- name: Check the log directory
run: test -d /var/log/xrpld
test-install-rpm:
needs: [generate-matrix, package]
name: install rpm
uses: ./.github/workflows/reusable-package-test-install.yml
with:
package_type: rpm
package_names: ${{ needs.generate-matrix.outputs.rpm_package_names }}
images: |
[
"almalinux:9",
"almalinux:10",
"rockylinux/rockylinux:9",
"rockylinux/rockylinux:10",
"registry.access.redhat.com/ubi9/ubi",
"registry.access.redhat.com/ubi10/ubi"
]
publish:
needs: [generate-matrix, package, test-install]
needs: [generate-matrix, package, test-install-deb, test-install-rpm]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
name: "publish ${{ matrix.xrpld_artifact_name }}"
# The name says which of the two this is, because the job runs either way:
# with publish false it passes --dry-run and uploads nothing, and a job
# called "publish ..." succeeding on a pull request reads like a release.
name: "publish ${{ matrix.xrpld_artifact_name }}${{ !inputs.publish && ' (dry run)' || '' }}"
permissions:
contents: read
runs-on: ["self-hosted", "Linux", "X64", "heavy"]

View File

@@ -57,7 +57,7 @@ jobs:
- name: Upload coverage report
if: ${{ github.repository == 'XRPLF/rippled' }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
disable_search: true
disable_telem: true

View File

@@ -41,6 +41,7 @@ Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
- `noripple_check`: The `transactions` field is no longer included in error responses; it is still returned (possibly as an empty array) whenever `transactions` is `true` and the request succeeds. A malformed `account` is now rejected before the ledger is looked up, so that error response no longer carries the `ledger_hash`, `ledger_index`, and `validated` fields ([#6303](https://github.com/XRPLF/rippled/pull/6303)).
## XRP Ledger server version 3.3.0

View File

@@ -44,12 +44,18 @@ else()
set(pkg_type rpm)
endif()
# Unquoted below, so an empty value adds no argument at all.
set(pkg_variant_option "")
if(assert)
set(pkg_variant_option --variant=assert)
endif()
add_custom_target(
package
COMMAND
${CMAKE_SOURCE_DIR}/package/build_pkg.py --package-type=${pkg_type}
--build-dir=${CMAKE_BINARY_DIR} --pkg-release=${pkg_release}
--channel=UNRELEASED
${pkg_variant_option} --channel=UNRELEASED
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld validator-keys
COMMENT "Building Linux ${pkg_type} package"

View File

@@ -149,6 +149,7 @@ class Xrpl(ConanFile):
self.requires("xxhash/0.8.3", transitive_headers=True)
exports_sources = (
"bin/default-loader-path.sh",
"CMakeLists.txt",
"cfg/*",
"cmake/*",

View File

@@ -6,7 +6,8 @@
`xrpld` is published as DEB and RPM packages for 64-bit x86 Linux.
Use APT on Debian-based distributions such as Debian and Ubuntu,
and YUM on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux.
and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
where `yum` is a symlink to `dnf`.
To build from source instead, see [BUILD.md](../BUILD.md).
## Release channels
@@ -81,7 +82,7 @@ wherever it appears in the repository configuration.
sudo apt -y install xrpld
```
### With the YUM package manager
### With the DNF package manager
1. Add the XRPL Foundation package-signing key:
@@ -109,9 +110,23 @@ wherever it appears in the repository configuration.
3. Install the `xrpld` package:
```bash
sudo yum install -y xrpld
sudo dnf install -y xrpld
```
### Optional: the assert-enabled build
Every channel also carries `xrpld-assert` as a DEB, the same build with assertions
enabled, for diagnosing a problem on a non-production server.
It installs the same files as `xrpld` and replaces it, so install one or the other:
```bash
sudo apt -y install xrpld-assert # APT removes xrpld itself
```
Switching stops the service, since it is a removal and an installation rather than an upgrade,
and APT starts it again.
Install `xrpld` the same way to switch back.
## The xrpld service
Both package managers install a systemd unit and enable it, so `xrpld` starts on boot.
@@ -121,7 +136,7 @@ Check whether it is already running:
systemctl status xrpld.service
```
The APT packages start it immediately as well; the YUM packages do not, so start it yourself:
The DEB packages start it immediately as well; the RPM packages do not, so start it yourself:
```bash
sudo systemctl start xrpld.service

View File

@@ -86,12 +86,16 @@ public:
requires std::convertible_to<TT*, T*>
SharedIntrusive(SharedIntrusive<TT> const& rhs);
SharedIntrusive(SharedIntrusive&& rhs);
// noexcept so that a std::vector of these relocates by moving. Without it, move_if_noexcept
// copies each element instead, since this type is also copy constructible, and every copy is an
// atomic increment on the pointee's refcount followed by a release on the original. The body is
// a std::exchange on a raw pointer, so it provably cannot throw.
SharedIntrusive(SharedIntrusive&& rhs) noexcept;
template <class TT>
requires std::convertible_to<TT*, T*>
SharedIntrusive(
SharedIntrusive<TT>&& rhs); // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
SharedIntrusive(SharedIntrusive<TT>&& rhs) noexcept;
SharedIntrusive&
operator=(SharedIntrusive const& rhs);
@@ -529,11 +533,48 @@ staticPointerCast(TT const& v)
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, v);
}
/**
* Statically cast an intrusive pointer the caller is giving up, moving out of
* it.
*
* The parameter names the wrapped type rather than taking a bare `TT&&`. A
* bare one would be a forwarding reference, so it would also bind to lvalues
* in preference to the `const&` overload above and move out of a caller's live
* variable on what looks like a copy call.
*
* @param v the pointer to cast, left empty afterwards.
* @return a pointer of the requested type to the same object.
*/
template <class T, class TT>
SharedPtr<T>
staticPointerCast(SharedIntrusive<TT>&& v)
{
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, std::move(v));
}
template <class T, class TT>
SharedPtr<T>
dynamicPointerCast(TT const& v)
{
return SharedPtr<T>(DynamicCastTagSharedIntrusive{}, v);
}
/**
* Dynamically cast an intrusive pointer the caller is giving up, moving out of
* it.
*
* Tied to `SharedIntrusive<TT>&&` for the reason given above.
*
* @param v the pointer to cast, left empty afterwards if the cast succeeds and
* left owning the object if it does not.
* @return a pointer of the requested type, or an empty one if the object is
* not of that type.
*/
template <class T, class TT>
SharedPtr<T>
dynamicPointerCast(SharedIntrusive<TT>&& v)
{
return SharedPtr<T>(DynamicCastTagSharedIntrusive{}, std::move(v));
}
} // namespace intr_ptr
} // namespace xrpl

View File

@@ -43,7 +43,7 @@ SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT> const& rhs)
}
template <class T>
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs)
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs) noexcept
: ptr_{std::move(rhs).unsafeExchange(nullptr)}
{
}
@@ -51,7 +51,7 @@ SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs)
template <class T>
template <class TT>
requires std::convertible_to<TT*, T*>
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT>&& rhs)
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT>&& rhs) noexcept
: ptr_{std::move(rhs).unsafeExchange(nullptr)}
{
}

View File

@@ -162,4 +162,89 @@ toUInt64(std::string const& s);
bool
isProperlyFormedTomlDomain(std::string_view domain);
/**
* Whether a view can be passed on as a C string.
*
* A reader given only data() stops at the first null, so the view must reach the
* terminating null. The test rebuilds the view from data() and compares: a view
* that stops earlier rebuilds longer, and so compares unequal.
*
* consteval because reading the byte after the view is only defined when @p str
* points into storage holding a null at or after its end, such as a string
* literal. An unterminated view is then a compile error, not an out-of-bounds
* read.
*
* @param str The view to test.
* @return Whether @p str is null-terminated. A view with no data is not.
*/
consteval bool
isNullTerminated(std::string_view str)
{
if (str.data() == nullptr)
return false;
// Reading past the view is the point, so the usual data() warning does not
// apply.
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
return std::string_view{str.data()} == str;
}
/**
* A string that is known to reach its terminating null.
*
* Converts to std::string_view, so it compares and hashes as one. Unlike a
* view, asCString() may be handed to a reader that expects a C string, such
* as json::StaticString.
*
* The only constructor is consteval and rejects a view that stops before the
* null, so the property holds by construction and no caller asserts it.
*/
class NullTerminatedView
{
public:
/**
* Build a view from one that reaches its terminating null.
*
* Explicit, so that a plain view cannot become a proof of termination by
* accident. The conversion the other way stays implicit.
*
* @param view The string to hold. Rejected at compile time if it stops
* before its terminating null, or has no data.
*/
explicit consteval NullTerminatedView(std::string_view view)
: data_(view.data()), size_(view.size())
{
if (!isNullTerminated(view))
throw "xrpl::NullTerminatedView : view does not reach a null";
}
constexpr
operator std::string_view() const noexcept
{
return view();
}
/**
* @return The string as a view.
*/
[[nodiscard]] constexpr std::string_view
view() const noexcept
{
return {data_, size_};
}
/**
* @return The string as a C string. Never null.
*/
[[nodiscard]] constexpr char const*
asCString() const noexcept
{
return data_;
}
private:
char const* data_;
std::size_t size_;
};
} // namespace xrpl

View File

@@ -518,7 +518,7 @@ public:
* The input must be precisely `2 * bytes` hexadecimal characters
* long, with one exception: the value '0'.
*
* @param sv A null-terminated string of hexadecimal characters
* @param sv A string of hexadecimal characters
* @return true if the input was parsed properly; false otherwise.
*/
[[nodiscard]] constexpr bool

View File

@@ -1,6 +1,7 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/core/Job.h>
#include <xrpl/json/json_value.h>
@@ -9,7 +10,8 @@
#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <span>
#include <string_view>
namespace beast {
class Journal;
@@ -67,7 +69,7 @@ public:
* @param requestId Unique identifier to track command
*/
virtual void
rpcStart(std::string const& method, std::uint64_t requestId) = 0;
rpcStart(std::string_view method, std::uint64_t requestId) = 0;
/**
* Log successful finish of RPC call
@@ -76,7 +78,7 @@ public:
* @param requestId Unique identifier to track command
*/
virtual void
rpcFinish(std::string const& method, std::uint64_t requestId) = 0;
rpcFinish(std::string_view method, std::uint64_t requestId) = 0;
/**
* Log errored RPC call
@@ -85,7 +87,7 @@ public:
* @param requestId Unique identifier to track command
*/
virtual void
rpcError(std::string const& method, std::uint64_t requestId) = 0;
rpcError(std::string_view method, std::uint64_t requestId) = 0;
/**
* Log queued job
@@ -150,10 +152,20 @@ public:
PerfLog::Setup
setupPerfLog(Section const& section, std::filesystem::path const& configDir);
/**
* @param methodNames The RPC methods to count, one counter per name. Reported
* as JSON keys that borrow each name and read it as a C string, which is
* why the parameter type requires one that reaches its terminating null.
* The names must outlive the returned object, which holds views of them.
* The range itself need not: it is copied.
* Passed in rather than looked up here, so that this layer needs no
* knowledge of the dispatch table.
*/
std::unique_ptr<PerfLog>
makePerfLog(
PerfLog::Setup const& setup,
Application& app,
std::span<NullTerminatedView const> methodNames,
beast::Journal journal,
std::function<void()>&& signalStop);
@@ -161,7 +173,7 @@ template <typename Func, class Rep, class Period>
auto
measureDurationAndLog(
Func&& func,
std::string const& actionDescription,
std::string_view actionDescription,
std::chrono::duration<Rep, Period> maxDelay,
beast::Journal const& journal)
{

View File

@@ -0,0 +1,45 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class AMMEntry : public SLEBase<ViewT, ltAMM>
{
public:
using Base = SLEBase<ViewT, ltAMM>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit AMMEntry(
Asset const& issue1,
Asset const& issue2,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::amm(issue1, issue2), view, j)
{
}
explicit AMMEntry(
uint256 const& ammID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::amm(ammID), view, j)
{
}
};
using AMMEntryR = AMMEntry<ReadView>;
using AMMEntryW = AMMEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,35 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class AccountRootEntry : public SLEBase<ViewT, ltACCOUNT_ROOT>
{
public:
using Base = SLEBase<ViewT, ltACCOUNT_ROOT>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit AccountRootEntry(
AccountID const& id,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::account(id), view, j)
{
}
};
using AccountRootEntryR = AccountRootEntry<ReadView>;
using AccountRootEntryW = AccountRootEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,33 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class AmendmentsEntry : public SLEBase<ViewT, ltAMENDMENTS>
{
public:
using Base = SLEBase<ViewT, ltAMENDMENTS>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit AmendmentsEntry(
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::amendments(), view, j)
{
}
};
using AmendmentsEntryR = AmendmentsEntry<ReadView>;
using AmendmentsEntryW = AmendmentsEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,36 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STXChainBridge.h>
namespace xrpl {
template <typename ViewT>
class BridgeEntry : public SLEBase<ViewT, ltBRIDGE>
{
public:
using Base = SLEBase<ViewT, ltBRIDGE>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit BridgeEntry(
STXChainBridge const& bridge,
STXChainBridge::ChainType chainType,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::bridge(bridge, chainType), view, j)
{
}
};
using BridgeEntryR = BridgeEntry<ReadView>;
using BridgeEntryW = BridgeEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class CheckEntry : public SLEBase<ViewT, ltCHECK>
{
public:
using Base = SLEBase<ViewT, ltCHECK>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit CheckEntry(
AccountID const& id,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::check(id, seq), view, j)
{
}
explicit CheckEntry(
uint256 const& checkID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::check(checkID), view, j)
{
}
};
using CheckEntryR = CheckEntry<ReadView>;
using CheckEntryW = CheckEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,47 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class CredentialEntry : public SLEBase<ViewT, ltCREDENTIAL>
{
public:
using Base = SLEBase<ViewT, ltCREDENTIAL>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit CredentialEntry(
AccountID const& subject,
AccountID const& issuer,
Slice const& credType,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::credential(subject, issuer, credType), view, j)
{
}
explicit CredentialEntry(
uint256 const& credentialID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::credential(credentialID), view, j)
{
}
};
using CredentialEntryR = CredentialEntry<ReadView>;
using CredentialEntryW = CredentialEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,35 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class DIDEntry : public SLEBase<ViewT, ltDID>
{
public:
using Base = SLEBase<ViewT, ltDID>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit DIDEntry(
AccountID const& account,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::did(account), view, j)
{
}
};
using DIDEntryR = DIDEntry<ReadView>;
using DIDEntryW = DIDEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,36 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class DelegateEntry : public SLEBase<ViewT, ltDELEGATE>
{
public:
using Base = SLEBase<ViewT, ltDELEGATE>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit DelegateEntry(
AccountID const& account,
AccountID const& authorizedAccount,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::delegate(account, authorizedAccount), view, j)
{
}
};
using DelegateEntryR = DelegateEntry<ReadView>;
using DelegateEntryW = DelegateEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,58 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <set>
#include <utility>
namespace xrpl {
template <typename ViewT>
class DepositPreauthEntry : public SLEBase<ViewT, ltDEPOSIT_PREAUTH>
{
public:
using Base = SLEBase<ViewT, ltDEPOSIT_PREAUTH>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit DepositPreauthEntry(
AccountID const& owner,
AccountID const& preauthorized,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::depositPreauth(owner, preauthorized), view, j)
{
}
explicit DepositPreauthEntry(
AccountID const& owner,
std::set<std::pair<AccountID, Slice>> const& authCreds,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::depositPreauth(owner, authCreds), view, j)
{
}
explicit DepositPreauthEntry(
uint256 const& preauthID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::depositPreauth(preauthID), view, j)
{
}
};
using DepositPreauthEntryR = DepositPreauthEntry<ReadView>;
using DepositPreauthEntryW = DepositPreauthEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,50 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <cstdint>
namespace xrpl {
template <typename ViewT>
class DirectoryNodeEntry : public SLEBase<ViewT, ltDIR_NODE>
{
public:
using Base = SLEBase<ViewT, ltDIR_NODE>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit DirectoryNodeEntry(
AccountID const& id,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::ownerDir(id), view, j)
{
}
/**
* Resolve a specific page of the directory rooted at @p root.
*/
explicit DirectoryNodeEntry(
uint256 const& root,
std::uint64_t index,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::page(root, index), view, j)
{
}
};
using DirectoryNodeEntryR = DirectoryNodeEntry<ReadView>;
using DirectoryNodeEntryW = DirectoryNodeEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,37 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class EscrowEntry : public SLEBase<ViewT, ltESCROW>
{
public:
using Base = SLEBase<ViewT, ltESCROW>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit EscrowEntry(
AccountID const& src,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::escrow(src, seq), view, j)
{
}
};
using EscrowEntryR = EscrowEntry<ReadView>;
using EscrowEntryW = EscrowEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,33 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class FeeSettingsEntry : public SLEBase<ViewT, ltFEE_SETTINGS>
{
public:
using Base = SLEBase<ViewT, ltFEE_SETTINGS>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit FeeSettingsEntry(
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::feeSettings(), view, j)
{
}
};
using FeeSettingsEntryR = FeeSettingsEntry<ReadView>;
using FeeSettingsEntryW = FeeSettingsEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,33 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class LedgerHashesEntry : public SLEBase<ViewT, ltLEDGER_HASHES>
{
public:
using Base = SLEBase<ViewT, ltLEDGER_HASHES>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit LedgerHashesEntry(
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::skip(), view, j)
{
}
};
using LedgerHashesEntryR = LedgerHashesEntry<ReadView>;
using LedgerHashesEntryW = LedgerHashesEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class LoanBrokerEntry : public SLEBase<ViewT, ltLOAN_BROKER>
{
public:
using Base = SLEBase<ViewT, ltLOAN_BROKER>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit LoanBrokerEntry(
AccountID const& owner,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::loanBroker(owner, seq), view, j)
{
}
explicit LoanBrokerEntry(
uint256 const& loanBrokerID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::loanBroker(loanBrokerID), view, j)
{
}
};
using LoanBrokerEntryR = LoanBrokerEntry<ReadView>;
using LoanBrokerEntryW = LoanBrokerEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,45 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class LoanEntry : public SLEBase<ViewT, ltLOAN>
{
public:
using Base = SLEBase<ViewT, ltLOAN>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit LoanEntry(
uint256 const& loanBrokerID,
SeqProxy const& loanSeq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::loan(loanBrokerID, loanSeq), view, j)
{
}
explicit LoanEntry(
uint256 const& loanID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::loan(loanID), view, j)
{
}
};
using LoanEntryR = LoanEntry<ReadView>;
using LoanEntryW = LoanEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,55 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/UintTypes.h>
namespace xrpl {
template <typename ViewT>
class MPTokenEntry : public SLEBase<ViewT, ltMPTOKEN>
{
public:
using Base = SLEBase<ViewT, ltMPTOKEN>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit MPTokenEntry(
MPTID const& issuanceID,
AccountID const& holder,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::mptoken(issuanceID, holder), view, j)
{
}
explicit MPTokenEntry(
uint256 const& issuanceKey,
AccountID const& holder,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::mptoken(issuanceKey, holder), view, j)
{
}
explicit MPTokenEntry(
uint256 const& mptokenKey,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::mptoken(mptokenKey), view, j)
{
}
};
using MPTokenEntryR = MPTokenEntry<ReadView>;
using MPTokenEntryW = MPTokenEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,56 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
namespace xrpl {
template <typename ViewT>
class MPTokenIssuanceEntry : public SLEBase<ViewT, ltMPTOKEN_ISSUANCE>
{
public:
using Base = SLEBase<ViewT, ltMPTOKEN_ISSUANCE>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit MPTokenIssuanceEntry(
std::uint32_t seq,
AccountID const& issuer,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::mptokenIssuance(makeMptID(seq, issuer)), view, j)
{
}
explicit MPTokenIssuanceEntry(
MPTID const& issuanceID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::mptokenIssuance(issuanceID), view, j)
{
}
explicit MPTokenIssuanceEntry(
uint256 const& issuanceKey,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::mptokenIssuance(issuanceKey), view, j)
{
}
};
using MPTokenIssuanceEntryR = MPTokenIssuanceEntry<ReadView>;
using MPTokenIssuanceEntryW = MPTokenIssuanceEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class NFTokenOfferEntry : public SLEBase<ViewT, ltNFTOKEN_OFFER>
{
public:
using Base = SLEBase<ViewT, ltNFTOKEN_OFFER>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit NFTokenOfferEntry(
AccountID const& owner,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::nftokenOffer(owner, seq), view, j)
{
}
explicit NFTokenOfferEntry(
uint256 const& offerID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::nftokenOffer(offerID), view, j)
{
}
};
using NFTokenOfferEntryR = NFTokenOfferEntry<ReadView>;
using NFTokenOfferEntryW = NFTokenOfferEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,37 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class NFTokenPageEntry : public SLEBase<ViewT, ltNFTOKEN_PAGE>
{
public:
using Base = SLEBase<ViewT, ltNFTOKEN_PAGE>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit NFTokenPageEntry(
Keylet const& page,
uint256 const& token,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::nftokenPage(page, token), view, j)
{
}
};
using NFTokenPageEntryR = NFTokenPageEntry<ReadView>;
using NFTokenPageEntryW = NFTokenPageEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,33 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class NegativeUNLEntry : public SLEBase<ViewT, ltNEGATIVE_UNL>
{
public:
using Base = SLEBase<ViewT, ltNEGATIVE_UNL>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit NegativeUNLEntry(
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::negativeUNL(), view, j)
{
}
};
using NegativeUNLEntryR = NegativeUNLEntry<ReadView>;
using NegativeUNLEntryW = NegativeUNLEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class OfferEntry : public SLEBase<ViewT, ltOFFER>
{
public:
using Base = SLEBase<ViewT, ltOFFER>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit OfferEntry(
AccountID const& id,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::offer(id, seq), view, j)
{
}
explicit OfferEntry(
uint256 const& offerID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::offer(offerID), view, j)
{
}
};
using OfferEntryR = OfferEntry<ReadView>;
using OfferEntryW = OfferEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,38 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <cstdint>
namespace xrpl {
template <typename ViewT>
class OracleEntry : public SLEBase<ViewT, ltORACLE>
{
public:
using Base = SLEBase<ViewT, ltORACLE>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit OracleEntry(
AccountID const& account,
std::uint32_t documentID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::oracle(account, documentID), view, j)
{
}
};
using OracleEntryR = OracleEntry<ReadView>;
using OracleEntryW = OracleEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,38 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class PayChannelEntry : public SLEBase<ViewT, ltPAYCHAN>
{
public:
using Base = SLEBase<ViewT, ltPAYCHAN>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit PayChannelEntry(
AccountID const& src,
AccountID const& dst,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::payChannel(src, dst, seq), view, j)
{
}
};
using PayChannelEntryR = PayChannelEntry<ReadView>;
using PayChannelEntryW = PayChannelEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class PermissionedDomainEntry : public SLEBase<ViewT, ltPERMISSIONED_DOMAIN>
{
public:
using Base = SLEBase<ViewT, ltPERMISSIONED_DOMAIN>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit PermissionedDomainEntry(
AccountID const& account,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::permissionedDomain(account, seq), view, j)
{
}
explicit PermissionedDomainEntry(
uint256 const& domainID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::permissionedDomain(domainID), view, j)
{
}
};
using PermissionedDomainEntryR = PermissionedDomainEntry<ReadView>;
using PermissionedDomainEntryW = PermissionedDomainEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,48 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/UintTypes.h>
namespace xrpl {
template <typename ViewT>
class RippleStateEntry : public SLEBase<ViewT, ltRIPPLE_STATE>
{
public:
using Base = SLEBase<ViewT, ltRIPPLE_STATE>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit RippleStateEntry(
AccountID const& id0,
AccountID const& id1,
Currency const& currency,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::trustLine(id0, id1, currency), view, j)
{
}
explicit RippleStateEntry(
AccountID const& id,
Issue const& issue,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::trustLine(id, issue), view, j)
{
}
};
using RippleStateEntryR = RippleStateEntry<ReadView>;
using RippleStateEntryW = RippleStateEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,503 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <concepts>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
namespace xrpl {
// Concept to distinguish read-only vs writable view types
template <typename V>
concept IsWritableView = std::derived_from<V, ApplyView>;
namespace detail {
/**
* Resolves a keylet for a read-only entry.
*
* ReadView::read() on an ApplyView returns the underlying ledger's entry
* whenever the view is not already tracking one, while peek() installs the
* view's own copy and returns that. A read-only entry built with read()
* would therefore hold an SLE that goes stale the moment anything peeks the
* same key and modifies it. Resolve through peek() whenever the view really is
* an ApplyView, so every entry over that view shares one SLE.
*
* @note The const_cast is what makes reaching ApplyView::peek() possible, and
* it is defined behavior only when the view really is a non-const
* object that the caller merely observes through a const reference.
* That holds for every production view today, but it is not a
* guarantee the codebase makes: the unit tests already build
* genuinely const ApplyView-derived objects (`Sandbox const` in
* Directory_test.cpp and View_test.cpp, `PaymentSandbox const` in
* TheoreticalQuality_test.cpp and View_test.cpp). Constructing a
* read-only entry over one of those would be undefined behavior, so
* do not, until #8069 removes the cast -- by giving ApplyView a
* const-qualified peek(), which needs no amendment because
* Action::Cache is invisible to apply(), visit() and metadata.
*
* @note Consequently a "read-only" entry over an ApplyView is not free of
* side effects: peek() installs an Action::Cache entry in the apply
* state table. That is benign for transaction metadata -- Cache entries
* are skipped in ApplyStateTable::apply(), ::visit() and in metadata
* generation -- but it does cost one deep SLE copy on first touch.
*/
inline SLE::const_pointer
resolveEntry(ReadView const& view, Keylet const& key)
{
// Safe only for a view that is not itself a const object -- see the
// note above. The entry holds a const reference because it does not
// modify the view, not because the view is const.
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
if (auto const applyView = dynamic_cast<ApplyView*>(const_cast<ReadView*>(&view)))
return applyView->peek(key);
return view.read(key);
}
} // namespace detail
/**
* View-parameterized base class for all ledger entries.
*
* 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 entries cannot mutate state.
*
* @tparam EntryType the ledger entry type this entry is statically bound to.
* Derived per-type entries pass their own type (e.g. ltACCOUNT_ROOT); the
* generic ReadOnlySLE / WritableSLE aliases leave it at ltANY, which opts out
* of the static type check. Binding the type here is what keeps an entry for
* one entry type from being constructed or converted from another -- see the
* converting constructor below.
*
* Derived classes should provide domain-specific accessors that hide
* implementation details of the underlying ledger entry format.
*/
template <typename ViewT, LedgerEntryType EntryType = ltANY>
class SLEBase
{
public:
static constexpr bool kIsWritable = IsWritableView<ViewT>;
// The ledger entry type this entry is bound to, and whether that binding
// is meaningful (ltANY means "any type", i.e. no static check).
static constexpr LedgerEntryType kEntryType = EntryType;
static constexpr bool kIsTyped = (EntryType != ltANY);
// SLE pointer type: mutable for writable views, const for read-only
using SlePtrType = std::conditional_t<kIsWritable, SLE::pointer, SLE::const_pointer>;
// View reference type: ApplyView& for writable, ReadView const& for
// read-only
using ViewRefType = std::conditional_t<kIsWritable, ApplyView&, ReadView const&>;
// Non-virtual by design: these entries are parameterized on the view and
// entry type, never used polymorphically through a base pointer. A vptr
// would be 8 bytes of pure overhead on a type meant to be as cheap as the
// shared_ptr it wraps. See the static_assert below the class.
//
// The destructor is public because the ReadOnlySLE / WritableSLE aliases
// name this class directly and are used as value types. Since it is not
// virtual, never delete a derived entry through an SLEBase*.
~SLEBase() = default;
SLEBase(SLEBase const&)
requires(!kIsWritable)
= default;
SLEBase(SLEBase&&) = default;
SLEBase&
operator=(SLEBase const&) = delete;
SLEBase&
operator=(SLEBase&&) = delete;
SLEBase() = delete;
// --- Constructors that adopt/resolve an SLE (public so the ReadOnlySLE /
// WritableSLE aliases and the per-type entries can be built directly
// from a keylet, or -- read-only only -- from an already-fetched
// SLE). ---
/**
* Constructor for read-only context (adopt an already-fetched SLE).
*
* There is deliberately no writable equivalent: a writable entry needs
* a Keylet so that newSLE() can still build an entry when none exists,
* and that cannot be recovered from a null SLE.
*/
explicit SLEBase(
SLE::const_pointer sle,
ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires(!kIsWritable)
: view_(view), sle_(std::move(sle)), j_(j)
{
XRPL_ASSERT(
!kIsTyped || !sle_ || sle_->getType() == kEntryType,
"xrpl::SLEBase::SLEBase : adopted SLE matches bound entry type");
}
/**
* Constructor for read-only context (read from view by keylet)
*/
explicit SLEBase(
Keylet const& key,
ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires(!kIsWritable)
: view_(view), sle_(detail::resolveEntry(view, key)), j_(j)
{
XRPL_ASSERT(
!kIsTyped || key.type == kEntryType,
"xrpl::SLEBase::SLEBase : keylet matches bound entry type");
}
/**
* Converting constructor: writable → read-only.
*
* Enables implicit conversion from SLEBase<ApplyView> to
* SLEBase<ReadView>, so functions taking ReadOnlySLE const& can accept
* WritableSLE.
*
* Constrained to the same entry type (or to a ltANY target, i.e. widening
* a typed entry to a generic ReadOnlySLE). The constraint is load-bearing:
* this constructor is inherited into every per-type entry, and unconstrained
* it would bind any writable entry that slices to SLEBase, so an OfferEntryW
* would convert to an AccountRootEntryR with no cast at the call site.
*/
template <typename OtherViewT, LedgerEntryType OtherType>
SLEBase(SLEBase<OtherViewT, OtherType> const& other)
requires(!kIsWritable && IsWritableView<OtherViewT> &&
(OtherType == EntryType || EntryType == ltANY))
: view_(other.readView()), sle_(other.rawSle()), j_(other.journal())
{
}
/**
* 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)
{
XRPL_ASSERT(
!kIsTyped || key.type == kEntryType,
"xrpl::SLEBase::SLEBase : keylet matches bound entry type");
}
/**
* Constructor for writable context, for call sites that hold an
* ApplyViewContext (peek from ctx.view by keylet).
*
* ctx.tx is not retained: this exists purely so transactors can pass the
* context they already have instead of spelling out ctx.view. If an entry
* ever needs the applying transaction, store it here rather than adding
* another overload.
*/
explicit SLEBase(
Keylet const& key,
ApplyViewContext const& ctx,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
requires kIsWritable
: SLEBase(key, ctx.view, j)
{
}
// --- 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.
*
* Prefer operator-> / operator* for field access; this is for the call
* sites that need the shared_ptr itself.
*/
[[nodiscard]] SLE::const_pointer
rawSle() const
{
return sle_;
}
/**
* Returns the ledger entry type of this entry.
*
* For a per-type entry this is kEntryType, known at compile time and
* valid whether or not the entry exists. Only the generic ReadOnlySLE /
* WritableSLE aliases have to read it back out of the SLE.
*
* @throws std::logic_error for a generic (ltANY) entry if exists() is
* false.
*/
[[nodiscard]] LedgerEntryType
type() const
{
if constexpr (kIsTyped)
{
return kEntryType;
}
else
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::type : entry does not exist");
return sle_->getType();
}
}
/**
* Returns the keylet identifying this entry.
*
* Writable entries keep the keylet they were built from, so it is valid
* even before newSLE(). Read-only entries derive it from the SLE, which
* must therefore exist.
*
* @throws std::logic_error for a read-only entry if exists() is false.
*/
[[nodiscard]] Keylet
keylet() const
{
if constexpr (kIsWritable)
{
return key_;
}
else
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::keylet : entry does not exist");
// Take the type from the SLE, not from kEntryType: the adopt-SLE
// constructor's type check is assert-only, so a Release build can
// be holding an SLE whose type disagrees with the binding, and the
// SLE is the one telling the truth.
return Keylet(sle_->getType(), sle_->key());
}
}
/**
* Returns the ledger key of this entry.
*
* @throws std::logic_error same as keylet(): for read-only entries,
* if exists() is false.
*/
[[nodiscard]] uint256
key() const
{
return keylet().key;
}
/**
* Returns the read view (always available; ApplyView inherits ReadView)
*/
[[nodiscard]] ReadView const&
readView() const
{
return view_;
}
/**
* Const dereference operators (always available)
*
* @throws std::logic_error if exists() is false.
*/
STLedgerEntry const*
operator->() const
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::operator-> : entry does not exist");
return sle_.get();
}
STLedgerEntry const&
operator*() const
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::operator* : entry does not exist");
return *sle_;
}
// --- Writable interface (compile-time gated) ---
//
// Everything that hands out mutable access (or mutates) is non-const, so
// that a `FooEntryW const&` is as inert as a `FooEntryR`. Use readView()
// when a const entry only needs to inspect the view.
/**
* Returns the underlying SLE for write access.
*
* Prefer operator-> / operator* for field access; this is for the call
* sites that need the shared_ptr itself.
*/
[[nodiscard]] SlePtrType const&
mutableRawSle()
requires kIsWritable
{
return sle_;
}
/**
* Returns the apply view for write operations
*/
[[nodiscard]] ApplyView&
applyView()
requires kIsWritable
{
return view_;
}
/**
* Mutable dereference operators
*
* @throws std::logic_error if exists() is false.
*/
STLedgerEntry*
operator->()
requires kIsWritable
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::operator-> : entry does not exist");
return sle_.get();
}
STLedgerEntry&
operator*()
requires kIsWritable
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::operator* : entry does not exist");
return *sle_;
}
/**
* Inserts the entry into the view.
*
* @throws std::logic_error if exists() is false.
*/
void
insert()
requires kIsWritable
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::insert : entry does not exist");
view_.insert(sle_);
}
/**
* Erases the entry from the view.
*
* Drops the SLE afterwards, so the entry reports !exists() and any
* further use throws here rather than either throwing from deep inside
* ApplyStateTable or -- worse -- silently succeeding. For an
* entry that already existed, ApplyStateTable::erase keeps holding this
* exact SLE and builds the DeletedNode's FinalFields from it, so a write
* through the entry after erase() would land in transaction metadata
* with no diagnostic at all.
*
* @throws std::logic_error if exists() is false.
*/
void
erase()
requires kIsWritable
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::erase : entry does not exist");
view_.erase(sle_);
sle_ = nullptr;
}
/**
* @throws std::logic_error if exists() is false.
*/
void
update()
requires kIsWritable
{
if (!exists())
Throw<std::logic_error>("xrpl::SLEBase::update : entry does not exist");
view_.update(sle_);
}
/**
* @throws std::logic_error if exists() is true: newSLE() would otherwise
* silently discard the SLE already held.
*/
void
newSLE()
requires kIsWritable
{
if (exists())
Throw<std::logic_error>("xrpl::SLEBase::newSLE : entry already exists");
sle_ = std::make_shared<SLE>(key_);
}
[[nodiscard]] beast::Journal
journal() const
{
return j_;
}
protected:
ViewRefType view_;
// Keylet is only meaningful for writable views, which need it to build an
// SLE that does not exist yet; read-only entries derive it from the SLE.
struct Empty
{
};
// No default member initializer: Keylet is not default-constructible, so
// every writable constructor must initialize key_ explicitly.
[[no_unique_address]]
std::conditional_t<kIsWritable, Keylet, Empty> key_;
SlePtrType sle_{};
beast::Journal j_;
};
/**
* Generic (any-entry-type) SLE entries.
*
* Use these when the concrete ledger entry type is not known at a given site;
* otherwise prefer the per-type entries (e.g. AccountRootEntry.h), which
* additionally enforce the entry type at compile time.
*
* SLE::const_pointer / SLE::const_ref -> ReadOnlySLE
* SLE::pointer / SLE::ref -> WritableSLE
*/
using ReadOnlySLE = SLEBase<ReadView>;
using WritableSLE = SLEBase<ApplyView>;
static_assert(
!std::is_polymorphic_v<ReadOnlySLE> && !std::is_polymorphic_v<WritableSLE>,
"SLEBase must stay a thin value type; it must not acquire a vtable");
} // namespace xrpl

View File

@@ -0,0 +1,35 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class SignerListEntry : public SLEBase<ViewT, ltSIGNER_LIST>
{
public:
using Base = SLEBase<ViewT, ltSIGNER_LIST>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit SignerListEntry(
AccountID const& account,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::signerList(account), view, j)
{
}
};
using SignerListEntryR = SignerListEntry<ReadView>;
using SignerListEntryW = SignerListEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,36 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
namespace xrpl {
template <typename ViewT>
class SponsorshipEntry : public SLEBase<ViewT, ltSPONSORSHIP>
{
public:
using Base = SLEBase<ViewT, ltSPONSORSHIP>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit SponsorshipEntry(
AccountID const& sponsor,
AccountID const& sponsee,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::sponsorship(sponsor, sponsee), view, j)
{
}
};
using SponsorshipEntryR = SponsorshipEntry<ReadView>;
using SponsorshipEntryW = SponsorshipEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class TicketEntry : public SLEBase<ViewT, ltTICKET>
{
public:
using Base = SLEBase<ViewT, ltTICKET>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit TicketEntry(
AccountID const& id,
SeqProxy const& ticketSeq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::ticket(id, ticketSeq), view, j)
{
}
explicit TicketEntry(
uint256 const& ticketID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::ticket(ticketID), view, j)
{
}
};
using TicketEntryR = TicketEntry<ReadView>;
using TicketEntryW = TicketEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SeqProxy.h>
namespace xrpl {
template <typename ViewT>
class VaultEntry : public SLEBase<ViewT, ltVAULT>
{
public:
using Base = SLEBase<ViewT, ltVAULT>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit VaultEntry(
AccountID const& owner,
SeqProxy const& seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::vault(owner, seq), view, j)
{
}
explicit VaultEntry(
uint256 const& vaultID,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::vault(vaultID), view, j)
{
}
};
using VaultEntryR = VaultEntry<ReadView>;
using VaultEntryW = VaultEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,38 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <cstdint>
namespace xrpl {
template <typename ViewT>
class XChainOwnedClaimIDEntry : public SLEBase<ViewT, ltXCHAIN_OWNED_CLAIM_ID>
{
public:
using Base = SLEBase<ViewT, ltXCHAIN_OWNED_CLAIM_ID>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit XChainOwnedClaimIDEntry(
STXChainBridge const& bridge,
std::uint64_t seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::xChainClaimID(bridge, seq), view, j)
{
}
};
using XChainOwnedClaimIDEntryR = XChainOwnedClaimIDEntry<ReadView>;
using XChainOwnedClaimIDEntryW = XChainOwnedClaimIDEntry<ApplyView>;
} // namespace xrpl

View File

@@ -0,0 +1,39 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <cstdint>
namespace xrpl {
template <typename ViewT>
class XChainOwnedCreateAccountClaimIDEntry
: public SLEBase<ViewT, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID>
{
public:
using Base = SLEBase<ViewT, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit XChainOwnedCreateAccountClaimIDEntry(
STXChainBridge const& bridge,
std::uint64_t seq,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::xChainCreateAccountClaimID(bridge, seq), view, j)
{
}
};
using XChainOwnedCreateAccountClaimIDEntryR = XChainOwnedCreateAccountClaimIDEntry<ReadView>;
using XChainOwnedCreateAccountClaimIDEntryW = XChainOwnedCreateAccountClaimIDEntry<ApplyView>;
} // namespace xrpl

View File

@@ -338,9 +338,9 @@ struct AccountingDeltas
Number debtTotalDelta;
};
// Whole-life (pre-LendingProtocolV1_1) recognition model: interest is
// recognized into AssetsTotal/DebtTotal up front, at origination.
namespace accrual {
// Instant interest recognition (pre-LendingProtocolV1_1): interest is
// recognized into AssetsTotal/DebtTotal immediately, at origination.
namespace instant_recognition {
// LoanSet origination: what's added to Vault.AssetsTotal and LoanBroker.DebtTotal
AccountingDeltas
@@ -362,7 +362,7 @@ loanVaultExposure(SLE::const_ref loanSle);
AccountingDeltas
loanPaymentDeltas(LoanPaymentParts const& parts);
} // namespace accrual
} // namespace instant_recognition
// Cash-basis (LendingProtocolV1_1) recognition model: AssetsTotal/DebtTotal
// are principal-only, interest is recognized only as it's actually paid.
@@ -381,7 +381,7 @@ loanPaymentDeltas(LoanPaymentParts const& parts);
// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is
// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is
// VaultVersion::CashBasis, else accrual::. These are the only entry points
// VaultVersion::CashBasis, else instant_recognition::. These are the only entry points
// transactors call.
AccountingDeltas
loanOriginationDeltas(

View File

@@ -172,8 +172,8 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref
/**
* Resolves a Vault's LEVersion, the single point every accounting touch
* point should call to determine which recognition model (accrual vs.
* cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
* point should call to determine which recognition model (instant interest
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
* activated never have sfLEVersion set, which resolves here to
* VaultVersion::Legacy.
*

View File

@@ -92,7 +92,7 @@ public:
void
importDatabase(Database& source) override
{
importInternal(*backend_.get(), source);
importInternal(*backend_, source);
}
void

View File

@@ -26,7 +26,8 @@ struct Config
/**
* The largest number of public peer slots to allow.
* This includes both inbound and outbound, but does not include
* fixed peers.
* fixed peers. A configuration built by `makeConfig` always holds
* `maxPeers == inPeers + outPeers`.
*/
std::size_t maxPeers{tuning::kDefaultMaxPeers};

View File

@@ -6,6 +6,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STInteger.h> // IWYU pragma: keep
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
@@ -301,6 +302,63 @@ verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 co
NotTEC
checkEncryptedAmountFormat(STObject const& object);
/**
* @brief Checks whether a holder's issuer mirror is encrypted under the
* issuance's currently registered issuer key.
*
* Verifies that the holder's issuer mirror epoch matches the active issuer key
* epoch on the issuance. An absent mirror epoch defaults to epoch 0. A holder without an issuer
* mirror is considered stale, as there is no key anchor for future re-encryptions.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger object.
* @return true if the MPToken's issuer mirror is current. false if stale.
*/
[[nodiscard]] bool
isIssuerMirrorCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Checks whether a holder's auditor mirror is encrypted under the
* issuance's currently registered auditor key.
*
* Verifies that the holder's auditor mirror epoch matches the active auditor key
* epoch on the issuance. An absent mirror epoch defaults to epoch 0. An issuance
* without an auditor key requires no auditor mirror and is considered current.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger object.
* @return true if the auditor mirror is current or not required.
*/
[[nodiscard]] bool
isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Checks whether each mirror a holder is required to have is encrypted
* under the issuance's currently registered ElGamal keys.
*
* Verifies that both the issuer mirror and the auditor mirror (if required)
* are current. This serves as a combined check, ensuring all necessary
* holder mirror epochs match the active key epochs on the issuance.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger object.
* @return true if the required mirrors are current.
*/
[[nodiscard]] bool
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
*
* Call this after writing mirror ciphertexts under the issuance's currently
* registered keys, so that the mirrors read as current afterwards.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger entry to update.
*/
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
/**
* @brief Verifies revealed amount encryptions for all recipients.
*

View File

@@ -5,9 +5,11 @@
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
#include <ostream>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <variant>
namespace xrpl {
@@ -121,9 +123,32 @@ operator==(PathAsset const& lhs, PathAsset const& rhs)
template <typename Hasher>
void
hash_append(Hasher& h, PathAsset const& pathAsset)
hash_append(Hasher& h, PathAsset const& pathAsset) noexcept
{
std::visit([&]<ValidPathAsset T>(T const& e) { hash_append(h, e); }, pathAsset.value());
using beast::hash_append;
using Variant = std::remove_cvref_t<decltype(pathAsset.value())>;
static_assert(
std::variant_size_v<Variant> < 0xFFu,
"PathAsset's discriminant must fit in a byte, leaving 0xFF reserved.");
// std::visit is not noexcept: it throws bad_variant_access when the variant
// is valueless_by_exception.
if (pathAsset.value().valueless_by_exception()) [[unlikely]]
{
hash_append(h, static_cast<std::uint8_t>(0xFFu));
return;
}
hash_append(h, static_cast<std::uint8_t>(pathAsset.value().index()));
std::visit(
[&]<ValidPathAsset T>(T const& e) noexcept {
static_assert(
noexcept(hash_append(h, e)),
"Every PathAsset alternative must be nothrow-hashable.");
hash_append(h, e);
},
pathAsset.value());
}
inline bool

View File

@@ -322,7 +322,7 @@ constexpr std::uint8_t kVaultMaximumIouScale = 18;
* Vault ledger-entry schema versions. Assigned to newly created
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
* activation are left without LEVersion (implicit legacy version 0,
* accrual-basis accounting).
* instant interest recognition).
*/
enum class VaultVersion : uint8_t {
Legacy = 0,

View File

@@ -12,6 +12,8 @@
#include <xrpl/protocol/UintTypes.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
@@ -65,7 +67,7 @@ public:
PathAsset const& asset,
AccountID const& issuer);
[[nodiscard]] auto
[[nodiscard]] std::uint32_t
getNodeType() const;
[[nodiscard]] bool
@@ -109,9 +111,6 @@ public:
[[nodiscard]] bool
isType(Type const& pe) const;
[[nodiscard]] size_t
getHash() const;
bool
operator==(STPathElement const& t) const;
@@ -120,6 +119,17 @@ private:
getHash(STPathElement const& element);
};
template <class Hasher>
void
hash_append(Hasher& h, STPathElement const& e) noexcept
{
using beast::hash_append;
hash_append(h, (e.getNodeType() & STPathElement::TypeAccount) != 0u);
hash_append(h, e.getAccountID());
hash_append(h, e.getPathAsset());
hash_append(h, e.getIssuerID());
}
class STPath final : public CountedObject<STPath>
{
std::vector<STPathElement> path_;
@@ -176,9 +186,10 @@ template <class Hasher>
void
hash_append(Hasher& h, STPath const& p) noexcept
{
using beast::hash_append;
for (auto const& e : p)
{
beast::hash_append(h, e.getHash());
hash_append(h, e);
}
}
@@ -188,13 +199,39 @@ hash_append(Hasher& h, STPath const& p) noexcept
class STPathSet final : public STBase, public CountedObject<STPathSet>
{
std::vector<STPath> value_;
xrpl::hardened_hash_set<STPath> seenHashes_;
/**
* Deduplication index over `value_`, for pathfinding.
* The use of a std::unique_ptr is intentional as it
* only requires 8 additional bytes of storage for the pointer
* as opposed to 64 bytes with an optional. This keeps the size
* of the STPathSet to within the `STVar::kMaxSize` limit of 72 bytes.
*/
std::unique_ptr<hardened_hash_set<STPath>> seen_;
public:
struct DeduplicationTag
{
};
STPathSet() = default;
/**
* Deduplication tagged constructor.
* Use when you want to ensure that the STPathSet does not contain duplicate paths.
*/
explicit STPathSet(DeduplicationTag);
STPathSet(SField const& n);
STPathSet(SerialIter& sit, SField const& name);
STPathSet(STPathSet const& other);
STPathSet(STPathSet&&) = default;
STPathSet&
operator=(STPathSet const& other);
STPathSet&
operator=(STPathSet&&) = default;
~STPathSet() override = default;
void
add(Serializer& s) const override;
@@ -204,6 +241,16 @@ public:
[[nodiscard]] SerializedTypeID
getSType() const override;
/**
* @brief assembleAdd adds a path to the set by combining a base path and a tail element.
*
* @param base The base path.
* @param tail The tail element.
* @return true if the path was added, false if it was a duplicate and not added.
* @remarks Requires the STPathSet to be constructed with the DeduplicationTag. The return value
* indicates whether the combined path was inserted (true) or rejected as a duplicate (false).
* It is fine for callers to ignore the return value.
*/
bool
assembleAdd(STPath const& base, STPathElement const& tail);
@@ -229,22 +276,61 @@ public:
[[nodiscard]] bool
empty() const;
void
/**
* @brief pushBack adds a path to the set.
*
* @param e The path to add.
* @return true if the path was added, false if it was a duplicate and not added.
* @remarks If the STPathSet was constructed with the DeduplicationTag, then this method will
* check for duplicates and only add the path if it is not already present in the
* set. If the STPathSet was constructed without the DeduplicationTag,
* then this method will always add the path to the set, regardless of duplicates.
* It is fine for callers to ignore the return value.
*/
bool
pushBack(STPath const& e);
/**
* @brief emplaceBack adds a path to the set.
*
* @param args The arguments to construct the path with.
* @return true if the path was added, false if it was a duplicate and not added.
* @remarks If the STPathSet was constructed with the DeduplicationTag, then this method will
* check for duplicates and only add the path if it is not already present in the
* set. If the STPathSet was constructed without the DeduplicationTag,
* then this method will always add the path to the set, regardless of duplicates.
* It is fine for callers to ignore the return value.
* @note The path is constructed before the duplicate check, so on a false
* return the constructed path is discarded and any argument
* forwarded as an rvalue is left in a moved-from state. Use
* pushBack when the caller needs to keep its path on rejection.
*/
template <typename... Args>
void
bool
emplaceBack(Args&&... args);
[[nodiscard]] bool
contains(STPath const& path) const;
private:
STBase*
copy(std::size_t n, void* buf) const override;
STBase*
move(std::size_t n, void* buf) override;
/**
* @brief Append a path via `append`, then register it in the deduplication index.
*
* @param append Invoked with `value_`; must append exactly one path to it.
* @return true if the path was kept, false if it was a duplicate and was rolled back.
* @remarks Appends to the vector before touching the index, so that a failed allocation
* there leaves both containers untouched rather than leaving the index holding
* a path the vector does not. If the index insert reports a duplicate, or
* throws, the append is rolled back so the two containers stay consistent; in
* the throwing case the exception propagates. With no index (constructed
* without the DeduplicationTag) the append is unconditional.
*/
template <typename Append>
bool
appendUnique(Append&& append);
friend class detail::STVar;
};
@@ -336,7 +422,7 @@ inline STPathElement::STPathElement(
hashValue_ = getHash(*this);
}
inline auto
inline std::uint32_t
STPathElement::getNodeType() const
{
return type_;
@@ -545,25 +631,50 @@ STPathSet::empty() const
return value_.empty();
}
inline void
STPathSet::pushBack(STPath const& e)
template <typename Append>
inline bool
STPathSet::appendUnique(Append&& append)
{
value_.push_back(e);
seenHashes_.emplace(value_.back());
}
// Append to the vector first, so that a failed allocation there leaves both
// containers untouched rather than leaving the index holding a path the
// vector does not.
append(value_);
template <typename... Args>
inline void
STPathSet::emplaceBack(Args&&... args)
{
value_.emplace_back(std::forward<Args>(args)...);
seenHashes_.emplace(value_.back());
if (seen_ == nullptr)
{
return true;
}
try
{
if (!seen_->insert(value_.back()).second)
{
// Already present: roll back the append.
value_.pop_back();
return false;
}
}
catch (...)
{
// The index insert failed, so roll back the append to keep the vector
// and the index consistent.
value_.pop_back();
throw;
}
return true;
}
inline bool
STPathSet::contains(STPath const& path) const
STPathSet::pushBack(STPath const& e)
{
return seenHashes_.contains(path);
return appendUnique([&](auto& value) { value.push_back(e); });
}
template <typename... Args>
inline bool
STPathSet::emplaceBack(Args&&... args)
{
return appendUnique([&](auto& value) { value.emplace_back(std::forward<Args>(args)...); });
}
} // namespace xrpl

View File

@@ -124,6 +124,13 @@ public:
[[nodiscard]] NodeID const&
getNodeID() const noexcept;
/**
* Whether this validation carries a good signature.
*
* Reports false if the signature cannot be checked at all, so a caller
* cannot tell that apart from a bad signature. Either way the validation is
* unusable, and the reason is logged. Only a computed answer is remembered.
*/
[[nodiscard]] bool
isValid() const noexcept;

View File

@@ -10,6 +10,7 @@
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/SField.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <stdexcept>
@@ -25,6 +26,101 @@ private:
Blob data_;
public:
/**
* A header is never longer than this. The encoder fills a buffer of this
* size and writes only the bytes it used.
*/
static constexpr int kMaxNumberOfBytesInHeader = 3;
// A field whose size varies is stored as a header holding its length, then
// the field data. The header is 1, 2 or 3 bytes long. Nothing outside it says
// which, so the decoder reads the first byte and its value says how long the
// header is:
//
// 0 ... 192 kMin/kMaxValueOfFirstByteFor1ByteHeader
// 193 ... 240 kMin/kMaxValueOfFirstByteFor2ByteHeader
// 241 ... 254 kMin/kMaxValueOfFirstByteFor3ByteHeader
// 255 belongs to no header
//
// Each range starts one past the end of the range before it.
static constexpr int kMinValueOfFirstByteFor1ByteHeader = 0;
static constexpr int kMaxValueOfFirstByteFor1ByteHeader = 192;
static constexpr int kMinValueOfFirstByteFor2ByteHeader =
kMaxValueOfFirstByteFor1ByteHeader + 1;
static constexpr int kMaxValueOfFirstByteFor2ByteHeader = 240;
static constexpr int kMinValueOfFirstByteFor3ByteHeader =
kMaxValueOfFirstByteFor2ByteHeader + 1;
static constexpr int kMaxValueOfFirstByteFor3ByteHeader = 254;
// A length x too big for one byte is split across the header. For 2 bytes:
//
// first byte = 193 + (x - 193) / 256
// second byte = (x - 193) % 256
//
// so 300 is stored as 193, 107. For 3 bytes it is the same, from 241, with
// the remainder split across two bytes: 20,000 is stored as 241, 29, 95.
static constexpr int kNumberOfValuesInOneByte = 256;
static constexpr int kNumberOfValuesInTwoBytes =
kNumberOfValuesInOneByte * kNumberOfValuesInOneByte;
// Each header length therefore covers a range of field lengths:
//
// 0 ... 192 kMin/kMaxValueOfLengthFor1ByteHeader
// 193 ... 12,480 kMin/kMaxValueOfLengthFor2ByteHeader
// 12,481 ... 918,744 kMin/kMaxValueOfLengthFor3ByteHeader
//
// The encoder always uses the shortest header that fits.
/**
* A 1 byte header holds the length in the byte itself, so both ends of
* this range are the same numbers as the first byte's own range.
*/
static constexpr int kMinValueOfLengthFor1ByteHeader = kMinValueOfFirstByteFor1ByteHeader;
static constexpr int kMaxValueOfLengthFor1ByteHeader = kMaxValueOfFirstByteFor1ByteHeader;
static constexpr int kMinValueOfLengthFor2ByteHeader = kMaxValueOfLengthFor1ByteHeader + 1;
/**
* 48 values of the first byte mean a 2 byte header, and each of them covers
* 256 lengths. The 48 is worked out from the two range ends above, so it
* stays right if either of them changes.
*/
static constexpr int kMaxValueOfLengthFor2ByteHeader = kMinValueOfLengthFor2ByteHeader +
((kMaxValueOfFirstByteFor2ByteHeader - kMaxValueOfFirstByteFor1ByteHeader) *
kNumberOfValuesInOneByte) -
1;
static constexpr int kMinValueOfLengthFor3ByteHeader = kMaxValueOfLengthFor2ByteHeader + 1;
/**
* 14 values of the first byte mean a 3 byte header, and each of them covers
* 65,536 lengths. Counted the same way, that gives the largest length any
* header can state.
*
* Nothing is accepted or rejected against this. The assertion below uses it
* to check that every length the encoder writes is one a header can state.
*/
static constexpr int kMaxRepresentableLength = kMinValueOfLengthFor3ByteHeader +
((kMaxValueOfFirstByteFor3ByteHeader - kMaxValueOfFirstByteFor2ByteHeader) *
kNumberOfValuesInTwoBytes) -
1;
/**
* The largest length the encoder will write. This is the one number here
* that is picked rather than worked out. The decoder accepts nothing above
* it, so both sides agree on the same set of lengths.
*/
static constexpr int kMaxValueOfLengthFor3ByteHeader = 918744;
static_assert(
kMaxValueOfLengthFor3ByteHeader <= kMaxRepresentableLength,
"a length the encoder writes must be one a header can state");
explicit Serializer(int n = 256)
{
data_.reserve(n);
@@ -61,7 +157,7 @@ public:
// assemble functions
int
add8(unsigned char i);
add8(unsigned char byteValue);
int
add16(std::uint16_t i);
@@ -270,18 +366,90 @@ public:
return v.data_ == data_;
}
/**
* Works out how long a header is, from its first byte.
*
* Each overload of decodeVLLength below reads one header length, so call
* this first to learn which of them to call.
*
* @param firstByte First byte of the header, as read from the stream.
* @return How many bytes the whole header takes, counting firstByte: 1, 2
* or 3.
* @throws std::overflow_error if firstByte is the one value that starts no
* header.
*/
static int
decodeLengthLength(int b1);
decodeLengthLength(std::byte firstByte);
/**
* Reads the field length out of a 1 byte header.
*
* @param firstByte The single header byte, which is the length itself.
* @return Field length in bytes, from kMinValueOfLengthFor1ByteHeader to
* kMaxValueOfLengthFor1ByteHeader.
* @throws std::overflow_error if firstByte is big enough to mean a longer
* header, in which case it is not a length by itself.
*/
static int
decodeVLLength(int b1);
decodeVLLength(std::byte firstByte);
/**
* Reads the field length out of a 2 byte header.
*
* @param firstByte First header byte. Its value means a 2 byte header, and
* how far it sits into that range gives the top part of the length.
* @param secondByte Second header byte, holding the rest of the length.
* @return Field length in bytes, from kMinValueOfLengthFor2ByteHeader to
* kMaxValueOfLengthFor2ByteHeader.
* @throws std::overflow_error if firstByte is outside the range that means
* a 2 byte header.
*/
static int
decodeVLLength(int b1, int b2);
decodeVLLength(std::byte firstByte, std::byte secondByte);
/**
* Reads the field length out of a 3 byte header.
*
* @param firstByte First header byte. Its value means a 3 byte header, and
* how far it sits into that range gives the top part of the length.
* @param secondByte Second header byte, holding the middle part of the
* length.
* @param thirdByte Third header byte, holding the low part.
* @return Field length in bytes, from kMinValueOfLengthFor3ByteHeader to
* kMaxValueOfLengthFor3ByteHeader.
* @throws std::overflow_error if firstByte is outside the range that means
* a 3 byte header, or if the three bytes together state a length above
* kMaxValueOfLengthFor3ByteHeader, which the encoder would not write back.
*/
static int
decodeVLLength(int b1, int b2, int b3);
decodeVLLength(std::byte firstByte, std::byte secondByte, std::byte thirdByte);
private:
/**
* Works out how many bytes the header needs for the given length.
*
* This deliberately repeats the width choice addEncoded makes, so that
* addVL's assertion can compare the two. It has no other caller; do not
* reach for it as a utility.
*
* @param length Field length in bytes.
* @return How many header bytes it needs: 1, 2 or 3.
* @throws std::overflow_error if length is negative, or above
* kMaxValueOfLengthFor3ByteHeader.
*/
static int
encodeLengthLength(int length); // length to encode length
encodeLengthLength(int length);
/**
* Appends the length header for a field of the given length.
*
* The field's own data is not written; the caller appends it next.
*
* @param length Field length in bytes.
* @return Offset within this Serializer at which the header was written.
* @throws std::overflow_error if length is negative, or above
* kMaxValueOfLengthFor3ByteHeader.
*/
int
addEncoded(int length);
};
@@ -390,9 +558,15 @@ public:
void
getFieldID(int& type, int& name);
// Returns the size of the VL if the
// next object is a VL. Advances the iterator
// to the beginning of the VL.
/**
* Reads the length header at the read position and steps past it.
*
* @return Field length in bytes. The iterator is left on the first byte of
* the field data.
* @throws std::overflow_error if the header states a length the encoder could
* not have written.
* @throws std::runtime_error if the data runs out before the header does.
*/
int
getVLDataLength();

View File

@@ -34,10 +34,11 @@ concept ValidConstructSTArgs =
// and includes a small-object allocation optimization.
class STVar
{
private:
public:
// The largest "small object" we can accommodate
static constexpr std::size_t kMaxSize = 72;
private:
alignas(std::max_align_t) std::byte d_[kMaxSize] = {};
STBase* p_ = nullptr;

View File

@@ -436,6 +436,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfConfidentialBalanceVersion, SoeDefault},
{sfIssuerEncryptedBalance, SoeOptional},
{sfAuditorEncryptedBalance, SoeOptional},
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
}))

View File

@@ -278,6 +278,7 @@ JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind
JSS(fullbelow_size); // out: GetCounts
JSS(gateway); // in: noripple_check
JSS(git); // out: server_info
JSS(good); // out: RPCVersion
JSS(hash); // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field
@@ -481,6 +482,7 @@ JSS(ports); // out: NetworkOPs
JSS(previous); // out: Reservations
JSS(previous_ledger); // out: LedgerPropose
JSS(price); // out: amm_info, AuctionSlot
JSS(problems); // out: noripple_check
JSS(proof); // in: BookOffers
JSS(propose_seq); // out: LedgerPropose
JSS(proposers); // out: NetworkOPs, LedgerConsensus
@@ -660,6 +662,7 @@ JSS(url); // in/out: Subscribe, Unsubscribe
JSS(url_password); // in: Subscribe
JSS(url_username); // in: Subscribe
JSS(urlgravatar); //
JSS(user); // in: noripple_check
JSS(username); // in: Subscribe
JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*, Tx
JSS(validator_list_expires); // out: NetworkOps, ValidatorList

View File

@@ -268,6 +268,54 @@ public:
return this->sle_->isFieldPresent(sfAuditorEncryptedBalance);
}
/**
* @brief Get sfIssuerKeyMirrorEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getIssuerKeyMirrorEpoch() const
{
if (hasIssuerKeyMirrorEpoch())
return this->sle_->at(sfIssuerKeyMirrorEpoch);
return std::nullopt;
}
/**
* @brief Check if sfIssuerKeyMirrorEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerKeyMirrorEpoch() const
{
return this->sle_->isFieldPresent(sfIssuerKeyMirrorEpoch);
}
/**
* @brief Get sfAuditorKeyMirrorEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getAuditorKeyMirrorEpoch() const
{
if (hasAuditorKeyMirrorEpoch())
return this->sle_->at(sfAuditorKeyMirrorEpoch);
return std::nullopt;
}
/**
* @brief Check if sfAuditorKeyMirrorEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorKeyMirrorEpoch() const
{
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
@@ -471,6 +519,28 @@ public:
return *this;
}
/**
* @brief Set sfIssuerKeyMirrorEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setIssuerKeyMirrorEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfIssuerKeyMirrorEpoch] = value;
return *this;
}
/**
* @brief Set sfAuditorKeyMirrorEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setAuditorKeyMirrorEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfAuditorKeyMirrorEpoch] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.

View File

@@ -14,6 +14,7 @@
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapLeafNode.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <condition_variable>
@@ -34,7 +35,6 @@
namespace xrpl {
class SHAMapNodeID;
class SHAMapSyncFilter;
/**
@@ -71,20 +71,31 @@ enum class SHAMapState {
};
/**
* A SHAMap is both a radix tree with a fan-out of 16 and a Merkle tree.
* A SHAMap is both a trie with a fan-out of 16 and a Merkle tree.
*
* A radix tree is a tree with two properties:
* A trie keeps a key in the position of its nodes rather than in the nodes
* themselves: the path from the root down to a node spells out the prefix
* every key below it shares (the "prefix property"). A SHAMap spends one
* nibble of the 256-bit key per level, so each inner node has at most 16
* children, which is the fan-out, and a leaf sits at depth 64 at the deepest.
* A leaf also carries its own full key, which is what lets a reader check
* that it was reached through the branches that key names.
*
* 1. The key for a node is represented by the node's position in the tree
* (the "prefix property").
* 2. A node with only one child is merged with that child
* (the "merge property")
* A radix tree adds a second property: a node with only one child is merged
* with that child (the "merge property"), which is what makes it a compressed
* trie. A SHAMap does not maintain that, so it is a trie and not a radix
* tree. Adding an item creates an inner node at every nibble the two keys
* share, however long that run is, and never merges one away. Deleting does
* merge: a chain that reduces to a single leaf collapses, pulling that leaf
* up to one nibble below the nearest ancestor still holding two branches.
* Either way no edge spans more than one nibble, so two keys agreeing on
* their first 63 nibbles give 63 single-child inner nodes, an inner node at
* depth 63 holding both branches, and the two leaves at depth 64: one entry
* per level with no gaps, and 65 entries at the most. Traversal relies on
* that, since it is what makes a path's length name each node's depth.
*
* These properties result in a significantly smaller memory footprint for
* a radix tree.
*
* A fan-out of 16 means that each node in the tree has at most 16
* children. See https://en.wikipedia.org/wiki/Radix_tree
* See https://en.wikipedia.org/wiki/Trie and
* https://en.wikipedia.org/wiki/Radix_tree
*
* A Merkle tree is a tree where each non-leaf node is labelled with the hash
* of the combined labels of its children nodes.
@@ -133,8 +144,7 @@ private:
public:
/**
* Number of children each non-leaf node has (the 'radix tree' part of the
* map)
* Number of children each non-leaf node has, which is the trie's fan-out
*/
static constexpr unsigned int kBranchFactor = SHAMapInnerNode::kBranchFactor;
@@ -420,7 +430,327 @@ public:
invariants() const;
private:
using SharedPtrNodeStack = std::stack<std::pair<SHAMapTreeNodePtr, SHAMapNodeID>>;
/**
* Whether placing `node` one level below `parentDepth` leaves it no room.
*
* Only a leaf may sit at kLeafDepth, since an inner node there would have
* no branch left to select. Both of the places that bound a descent call
* this, so a walk with a caller-supplied path and one without cannot drift
* apart and refuse at different nodes.
*
* The depth is tested before the node's type so the virtual call runs only
* where the bound can bite, which is the last level of a 65-level walk.
*
* @param parentDepth the depth of the node being descended from.
* @param node the node about to be placed one level below it.
* @return whether that placement is past the deepest level this kind of
* node may occupy.
*/
[[nodiscard]] static bool
pastLeafDepth(unsigned int parentDepth, SHAMapTreeNode const& node)
{
return parentDepth + 1u >= kLeafDepth && (node.isInner() || parentDepth >= kLeafDepth);
}
/**
* A root-down path of nodes through the map.
*
* The path itself names each node's position: entry `i` sits at depth
* `i`, since a SHAMap does not merge a single-child node away (see the
* class docstring above), so every nibble down to a leaf has an inner
* node of its own. Nothing is therefore stored per entry but the node.
* No consumer needs a whole SHAMapNodeID: every one of them wants a
* depth, and reads the nibbles it cares about from the key it already
* holds.
*
* Storing an ID alongside each node would add a second answer to "where
* does this node sit", which could then disagree with the first.
* Deriving it cannot.
*/
class NodePathStack
{
public:
/**
* @return whether the path holds no node at all.
*/
[[nodiscard]] bool
empty() const
{
return path_.empty();
}
/**
* @return how many nodes the path holds, which is one more than its
* last node's depth.
*/
[[nodiscard]] std::size_t
size() const
{
return path_.size();
}
/**
* The node at the end of the path.
*
* Reading an empty path would be undefined, and the assert alone is
* stripped in release, so an empty path yields a null node the caller
* can test instead.
*
* @return a reference into the path, which a later push may
* invalidate by reallocating. Callers that push and then want
* the node again ask for it again; the node itself does not
* move, only the slot holding the pointer to it.
*/
[[nodiscard]] SHAMapTreeNodePtr const&
top() const
{
if (path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::top : empty stack");
static SHAMapTreeNodePtr const kEmpty;
return kEmpty;
// LCOV_EXCL_STOP
}
return path_.back();
}
/**
* The depth of the node at the end of the path.
*
* @return the depth, which is the entry's own index; zero on an empty
* path, which a caller must not read but which must not be an
* out-of-range subtraction either.
*/
[[nodiscard]] unsigned int
topDepth() const
{
if (path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::topDepth : empty stack");
return 0;
// LCOV_EXCL_STOP
}
return static_cast<unsigned int>(path_.size() - 1);
}
/**
* Shorten the path by one node.
*
* Popping an empty path would be undefined, and the assert alone is
* stripped in release, so an empty path is left alone instead.
*/
void
pop()
{
if (path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::pop : empty stack");
return;
// LCOV_EXCL_STOP
}
path_.pop_back();
}
/**
* Discard the whole path.
*
* For a walk that pushed a node it then found unusable: the node never
* became a meaningful path entry, so it must not be mistaken for one
* by whatever the caller does next with an empty-vs-nonempty check.
*/
void
clear()
{
path_.clear();
pathKey_ = uint256{};
}
/**
* Shorten the path by one node and hand that node to the caller.
*
* Reading a node out and then popping copies it, which costs an atomic
* increment on its refcount. Moving it out does not.
*
* @return the node that was at the end of the path, or an empty
* pointer if there was none.
*/
[[nodiscard]] SHAMapTreeNodePtr
releaseNode()
{
if (path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::releaseNode : empty stack");
return {};
// LCOV_EXCL_STOP
}
auto node = std::move(path_.back());
path_.pop_back();
return node;
}
/**
* Start a path at the root of the map, which sits at depth zero by
* definition.
*
* @return false, leaving the path unchanged, if a path was already
* started. A malformed call must not abort a release build,
* so callers stop rather than overwrite it.
*/
[[nodiscard]] bool
pushRoot(SHAMapTreeNodePtr node)
{
if (!path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::pushRoot : non-empty stack");
return false;
// LCOV_EXCL_STOP
}
path_.push_back(std::move(node));
return true;
}
/**
* Extend the path to the child of the node at its end reached by
* `branch`.
*
* The branch is not stored. It is only used to judge the node offered,
* since the child's position is this path one level longer whichever
* branch reached it.
*
* Only a leaf may sit at kLeafDepth, since an inner node there would
* have no branch left to select.
*
* @param node the child to append.
* @param branch the branch of the current node that `node` was
* reached through.
* @return false if there is no node to descend from, no node to push,
* no branch of that number, no room left below for the kind
* of node offered, or a leaf whose own key does not select
* `branch`. A malformed call or a malformed map must not abort
* a release build, so callers stop walking instead. The path
* keeps its nodes, though the recorded branch chain may
* already name `branch`, which no later read reaches.
*/
[[nodiscard]] bool
pushChild(SHAMapTreeNodePtr node, unsigned int branch)
{
if (path_.empty() || !node || branch >= kBranchFactor)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::pushChild : no child to push");
return false;
// LCOV_EXCL_STOP
}
// Only a leaf may sit at kLeafDepth, so an inner child must land one level short of
// it, tighter than the plain depth bound a leaf child needs.
//
// Reachable, for the same reason the misplaced-leaf case below is: a node resolved from
// the local store has had neither its position nor its type judged. The two-argument
// SHAMap::descend fetches by the parent's recorded child hash and hooks what comes
// back, and a parsed node adopts that hash rather than recomputing it, so an inner node
// can arrive one level too deep. So this refuses rather than aborting a build.
//
auto const parentDepth = topDepth();
bool const tooDeep = pastLeafDepth(parentDepth, *node);
SOMETIMES(tooDeep, "xrpl::SHAMap::NodePathStack::pushChild : child past leaf depth");
if (tooDeep)
{
return false;
}
// Record the branch, then judge a leaf against every branch recorded so far. Testing
// only this step's nibble would accept a whole subtree hung under the wrong branch:
// one wrong child pointer in one inner node leaves every leaf below it agreeing at its
// own final nibble, because the subtree is internally well formed, and disagreeing only
// at the level where the pointer is wrong.
//
// This is the one thing a path cannot derive. Its length gives every depth, but whether
// the caller descended the branches it says it did is only visible against a real key.
//
// Reachable for the same reason, and by a wider route: a node arriving through a sync
// filter is judged by hash, and a hash says nothing about position. The paths that hook
// a node reject a misplaced one first (see SHAMap::descend and gmnProcessNodes), but a
// map read lazily from the local store never passes through them.
setNibble(parentDepth, branch);
bool const misplaced = node->isLeaf() &&
!SHAMapNodeID::createID(parentDepth + 1u, pathKey_).isPrefixOf(leafKey(*node));
SOMETIMES(
misplaced, "xrpl::SHAMap::NodePathStack::pushChild : leaf key outside branch");
if (misplaced)
{
return false;
}
path_.push_back(std::move(node));
return true;
}
/**
* Extend the path by one node lying on the way to `target`, starting
* it if it is empty.
*
* For nodes not reached by descending a known branch: the walk tracks
* only the key it is heading for, or the node is newly created. Either
* way `target` names the branch.
*
* @param node the node to append.
* @param target the key the walk is heading for.
* @return whatever pushRoot or pushChild returned.
*/
[[nodiscard]] bool
pushNode(SHAMapTreeNodePtr node, uint256 const& target)
{
if (path_.empty())
{
return pushRoot(std::move(node));
}
return pushChild(std::move(node), selectBranch(topDepth(), target));
}
private:
/**
* Write `branch` as the nibble at `depth` of the recorded branch chain.
*
* @param depth the nibble index to write, which is the depth the
* branch was taken from.
* @param branch the branch taken, which the caller has already bounded.
*/
void
setNibble(unsigned int depth, unsigned int branch)
{
auto& byte = *(pathKey_.begin() + (depth / 2));
if ((depth & 1) != 0u)
{
byte = static_cast<unsigned char>((byte & 0xF0u) | branch);
}
else
{
byte = static_cast<unsigned char>((byte & 0x0Fu) | (branch << 4));
}
}
// path_[i] holds the node at depth i, by construction: pushRoot starts at depth 0 and
// pushChild only ever appends one level.
std::vector<SHAMapTreeNodePtr> path_;
// The branches descended, one nibble per level: nibble i is the branch taken from depth i.
// One record for the whole path rather than an ID per entry, so path_.size() stays the only
// answer to where a node sits and this is only the claim being checked against it.
//
// A pop leaves the nibbles above the path's end as they were, because no read can reach
// them: createID masks this at parentDepth + 1, so a check reads nibbles 0 through
// parentDepth only, and those are always the current path's. Level j + 1 exists only if a
// push at depth j wrote nibble j, and re-descending at j overwrites it.
uint256 pathKey_;
};
using DeltaRef =
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
@@ -447,15 +777,21 @@ private:
* Update hashes up to the root
*/
void
dirtyUp(SharedPtrNodeStack& stack, uint256 const& target, SHAMapTreeNodePtr terminal);
dirtyUp(NodePathStack& stack, uint256 const& target, SHAMapTreeNodePtr terminal);
/**
* Walk towards the specified id, returning the node. Caller must check
* if the return is nullptr, and if not, if the node->peekItem()->key() ==
* id
* Walk towards the specified id, returning the node.
*
* @param id the key to walk towards, which need not be in the map.
* @param stack records the path walked, or nullptr to skip recording it.
* Lookups that only want the leaf (see findKey) omit it to
* avoid building a path they would immediately discard.
* @return the leaf the walk ended on, or nullptr if it ended on an inner
* node or was refused. A returned leaf need not hold `id`, so
* callers compare its key themselves.
*/
SHAMapLeafNode*
walkTowardsKey(uint256 const& id, SharedPtrNodeStack* stack = nullptr) const;
walkTowardsKey(uint256 const& id, NodePathStack* stack = nullptr) const;
/**
* Return nullptr if key not found
*/
@@ -464,10 +800,17 @@ private:
/**
* Unshare the node, allowing it to be modified
*
* @param node the node to unshare.
* @param depth the depth the node sits at, which says whether it is the
* root. A clone of the root has to be adopted as the new root; a
* clone of any other node is hooked up by the caller walking back
* up the path.
* @return the node, cloned if it was shared.
*/
template <class Node>
intr_ptr::SharedPtr<Node>
unshareNode(intr_ptr::SharedPtr<Node>, SHAMapNodeID const& nodeID);
unshareNode(intr_ptr::SharedPtr<Node> node, unsigned int depth);
/**
* prepare a node to be modified before flushing
@@ -482,27 +825,33 @@ private:
SHAMapTreeNodePtr
writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const;
// returns the first item at or below this node
SHAMapLeafNode*
firstBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, unsigned int branch = 0u) const;
// returns the last item at or below this node
SHAMapLeafNode*
lastBelow(
SHAMapTreeNodePtr node,
SharedPtrNodeStack& stack,
unsigned int branch = kBranchFactor) const;
// direction in which belowHelper scans an inner node's branches
// direction in which a scan walks an inner node's branches
enum class BelowDirection { First, Last };
// helper function for firstBelow and lastBelow
/**
* Returns the first or last item at or below the node already on top of `stack`, extending
* `stack` with the path walked to reach it.
*
* @param stack the path to extend, whose last node the search starts from.
* @param direction whether to take the lowest or the highest branch at
* each level.
* @return the leaf found, or nullptr if no leaf lies below that node.
*/
SHAMapLeafNode*
belowHelper(
SHAMapTreeNodePtr node,
SharedPtrNodeStack& stack,
unsigned int branch,
BelowDirection direction) const;
belowHelper(NodePathStack& stack, BelowDirection direction) const;
/**
* The nearest item on one side of `id`, which upperBound and lowerBound
* both answer.
*
* @param id the key to search around, which need not be in the map.
* @param direction First for the nearest key greater than `id`, Last for
* the nearest lesser.
* @return an iterator at that item, or end() if the map holds no key on
* that side.
*/
[[nodiscard]] ConstIterator
boundHelper(uint256 const& id, BelowDirection direction) const;
// Simple descent
// Get a child of the specified node
@@ -550,9 +899,9 @@ private:
hasLeafNode(uint256 const& tag, SHAMapHash const& hash) const;
SHAMapLeafNode const*
peekFirstItem(SharedPtrNodeStack& stack) const;
peekFirstItem(NodePathStack& stack) const;
SHAMapLeafNode const*
peekNextItem(uint256 const& id, SharedPtrNodeStack& stack) const;
peekNextItem(uint256 const& id, NodePathStack& stack) const;
bool
walkBranch(
SHAMapTreeNode* node,
@@ -626,10 +975,30 @@ private:
};
// getMissingNodes helper functions
/**
* Examine the remaining branches of one inner node, recording or
* requesting what is missing.
*
* @param mn the walk's shared state, which collects the missing nodes.
* @param node the walk's current position, updated to the node to process
* next.
*/
void
gmnProcessNodes(MissingNodes&, MissingNodes::StackEntry& node);
static void
gmnProcessDeferredReads(MissingNodes&);
gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& node);
/**
* Wait for every read this pass posted, then hook up or record what each
* one resolved.
*
* Drains all of them even after judging the map, since an outstanding
* read holds a pointer to `mn` and this is the only thing that waits for
* it.
*
* @param mn the walk's shared state, holding the posted reads.
*/
void
gmnProcessDeferredReads(MissingNodes& mn);
// fetch from DB helper function
SHAMapTreeNodePtr
@@ -697,7 +1066,7 @@ public:
using pointer = value_type const*;
private:
SharedPtrNodeStack stack_;
NodePathStack stack_;
SHAMap const* map_ = nullptr;
pointer item_ = nullptr;
@@ -723,7 +1092,7 @@ public:
private:
explicit ConstIterator(SHAMap const* map);
ConstIterator(SHAMap const* map, std::nullptr_t);
ConstIterator(SHAMap const* map, pointer item, SharedPtrNodeStack&& stack);
ConstIterator(SHAMap const* map, pointer item, NodePathStack&& stack);
friend bool
operator==(ConstIterator const& x, ConstIterator const& y);
@@ -742,10 +1111,7 @@ inline SHAMap::ConstIterator::ConstIterator(SHAMap const* map, std::nullptr_t) :
{
}
inline SHAMap::ConstIterator::ConstIterator(
SHAMap const* map,
pointer item,
SharedPtrNodeStack&& stack)
inline SHAMap::ConstIterator::ConstIterator(SHAMap const* map, pointer item, NodePathStack&& stack)
: stack_(std::move(stack)), map_(map), item_(item)
{
}

View File

@@ -19,7 +19,8 @@ class SHAMapInnerNode final : public SHAMapTreeNode, public CountedObject<SHAMap
{
public:
/**
* Each inner node has 16 children (the 'radix tree' part of the map)
* Each inner node has 16 children, which is the fan-out of the trie: one
* branch per value of the key nibble that level selects on.
*/
static constexpr unsigned int kBranchFactor = 16;

View File

@@ -75,4 +75,22 @@ leafKey(SHAMapTreeNode const& node)
return safeDowncast<SHAMapLeafNode const&>(node).peekItem()->key();
}
/**
* Whether a node may occupy a position in a SHAMap.
*
* A leaf's own key names its position, so an ID that is not a prefix of that
* key names a different subtree than the one the leaf belongs to. An inner
* node carries no key, so every position is consistent with it and the
* caller's own depth rules are what bound it.
*
* @param nodeID the position the node is claimed to occupy.
* @param node the node to judge.
* @return whether the node's own key agrees with that position.
*/
[[nodiscard]] inline bool
belongsAt(SHAMapNodeID const& nodeID, SHAMapTreeNode const& node)
{
return !node.isLeaf() || nodeID.isPrefixOf(leafKey(node));
}
} // namespace xrpl

View File

@@ -144,9 +144,31 @@ deserializeSHAMapNodeID(std::string_view s)
/** @} */
/**
* Returns the branch that would contain the given hash
* Returns the branch at the given depth that would contain the given hash
*
* Only the depth of a position matters here, since the nibble selected is read
* from `hash`. Callers holding a depth rather than a whole ID use this one.
*
* @param depth the depth of the node whose branch to select.
* @param hash the key whose nibble at that depth names the branch.
* @return the branch containing the hash.
*/
[[nodiscard]] unsigned int
selectBranch(SHAMapNodeID const& id, uint256 const& hash);
selectBranch(unsigned int depth, uint256 const& hash);
/**
* Returns the branch that would contain the given hash
*
* Reads only the depth of `id`, never its own key bits.
*
* @param id the node whose depth to read.
* @param hash the key whose nibble at that depth names the branch.
* @return the branch containing the hash.
*/
[[nodiscard]] inline unsigned int
selectBranch(SHAMapNodeID const& id, uint256 const& hash)
{
return selectBranch(id.getDepth(), hash);
}
} // namespace xrpl

View File

@@ -8,6 +8,7 @@
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/RawView.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
@@ -129,6 +130,14 @@ public:
view_->rawDestroyXRP(fee);
}
/**
* Registers a newly-created order book directory with the shared,
* process-wide OrderBookDB, unless this transaction is being applied
* under TapDryRun.
*/
void
addOrderBook(Book const& book);
ApplyViewContext
getApplyViewContext()
{

View File

@@ -12,6 +12,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <expected>
#include <optional>
#include <utility>
@@ -393,16 +394,21 @@ preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, Open
*
* No validation is done or implied by this function.
*
* Caller is responsible for handling any exceptions.
* Since none should be thrown, that will usually
* mean terminating.
*
* Callers do not expect this function to throw; exceptions from a transactor's
* `calculateBaseFee` are caught and reported as an error instead.
* @param view The current open ledger.
* @param tx The transaction to be checked.
*
* @return The base fee.
* @return The base fee on success. Returns `std::unexpected(temUNKNOWN)` if the transaction
* type is not recognized, and `std::unexpected(tefEXCEPTION)` if the transactor's
* `calculateBaseFee` threw.
*
* @note Failure is reported as an error rather than a fee of zero because a
* zero (or default) fee would pass checkFee and let the transaction be
* applied for less than it owes. Callers that only need a fee hint may fall
* back to a default; callers deciding whether to apply should reject.
*/
XRPAmount
[[nodiscard]] std::expected<XRPAmount, TER>
calculateBaseFee(ReadView const& view, STTx const& tx);
/**

View File

@@ -38,9 +38,11 @@ namespace xrpl {
* f. A Loan must reference a live `ltLOAN_BROKER`, and that broker must
* reference a live `ltVAULT`.
* g. Post-conditions for the Loan paid down by a successful `ttLOAN_PAY`:
* `PaymentRemaining > 0` after: `PrincipalOutstanding` and
* `PaymentRemaining` strictly decrease; `NextPaymentDueDate`
* advances by N * `PaymentInterval`, N > 0.
* `PaymentRemaining > 0` after: neither `PrincipalOutstanding` nor
* `TotalValueOutstanding` increases, and at least one of them
* strictly decreases;
* `PaymentRemaining` strictly decreases;
* `NextPaymentDueDate` advances by N * `PaymentInterval`, N > 0.
* `PaymentRemaining == 0` after: pinned by checks 1 and 5b.
*
*/

View File

@@ -15,7 +15,8 @@ package/
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
rpm/
xrpld.spec RPM spec
debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, xrpld.lintian-overrides, source/format)
debian/ Debian control files (control.in, lintian-overrides.in, rules, copyright, docs, links, source/format).
The `.in` files are templates rendered by `build_pkg.py`; `docs` and `links` are staged under the package name
shared/
xrpld.service systemd unit file (used by both RPM and DEB)
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
@@ -32,20 +33,74 @@ packaging job cannot drift apart. Today only `linux/amd64` is emitted. The map
pins the full container image in `image` — edit that field to move to a new
image and both CI and local builds pick it up — and names the format that image
builds in `type`, which CI passes to `build_pkg.py` as `--package-type`; the two
have to stay in step.
have to stay in step. An optional `variant` names a flavour of the package (see
[Package variants](#package-variants)), and CI passes it as `--variant`.
| Package type | Image (`configs.<distro>[].package.image` in `linux.json`) | Tools required |
| ------------ | ---------------------------------------------------------- | -------------------------------------------------------------- |
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild`, `rpmsign` |
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha>` | `dpkg-buildpackage`, debhelper with compat level 13, `lintian` |
To print the full packaging matrix (artifact names and images) for the current
`linux.json`:
To print the full packaging matrix (artifact names, images and package names)
for the current `linux.json`:
```bash
./.github/scripts/strategy-matrix/generate.py --packaging
```
## Package variants
A config whose binaries are not the plain release build cannot be packaged as
`xrpld`: both would carry the same name and version, so whichever published last
would win. It is packaged as a **variant** instead — `variant: "assert"` in its
`package` map, which CI passes to `build_pkg.py` as `--variant assert`,
producing `xrpld-assert`. What the build option itself does is a build concern,
not a packaging one; see the options table in [`BUILD.md`](../BUILD.md).
A variant ships the same paths as `xrpld``/usr/bin/xrpld`, `/etc/xrpld`,
`xrpld.service`, `/etc/logrotate.d/xrpld` — differing only in the per-package
documentation directory, so it declares itself a stand-in for the plain package
rather than something installable next to it: `Conflicts`, `Replaces` and a
versioned `Provides: xrpld` on Debian, `Conflicts` and `Provides` on RPM.
Neither format declares `Obsoletes`, so `apt upgrade` and `dnf upgrade` keep an
installed flavour on its own flavour, and switching is always explicit:
```bash
apt-get install xrpld-assert # apt removes the plain package itself
dnf swap xrpld xrpld-VARIANT # 'dnf install' alone stops at the conflict
```
Only the DEB packages carry a variant today — `xrpld-assert` comes from the
`debian` config alone, there being no call for an assert build on RHEL-based
distributions — but the RPM side works the same way if one is added.
A switch is a removal plus an installation rather than an upgrade, so unlike a
version upgrade it stops the service: Debian's scriptlets start it again, while
on RPM the operator runs `systemctl start xrpld`. Configuration survives either
way, being conffiles on Debian and `%config(noreplace)` on RPM.
`dnf` installs the replacement before erasing the old flavour, whose `%preun`
would leave `xrpld.service` disabled, so `%postun` re-applies the preset when
the unit file outlives the erase — which, since rpm keeps a file another
installed package owns, happens only during a swap. The cost is that a
deliberate `systemctl disable` is not carried across an RPM switch.
The alternative is an `xrpld-common` package owning the unit, the sysusers and
tmpfiles snippets and the configuration, required by both flavours at an exact
version: nothing is erased mid-swap, so no scriptlet has to detect one. It is
not worth it for a single variant — it moves files out of the production
package, and a sanitizer flavour would likely need its own unit anyway, putting
the lifecycle back where it is now.
Adding a variant is the flavour in `VARIANTS` in `build_pkg.py`, which is the
list `--variant` accepts, plus a config in `linux.json` with the CMake arguments
and a `package` map naming it, for one format or for both: `generate.py
--packaging` emits the package names per format, and the `test-install-deb` and
`test-install-rpm` jobs install what their own format produced.
Operators switch between the flavours as described in
[`docs/install.md`](../docs/install.md#optional-the-assert-enabled-build).
## Building packages
### Via CI
@@ -56,9 +111,11 @@ Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
1. `package` fans out one job per config carrying a `package` map, building and
signing in that config's container, and uploading `<config>-pkg` alongside
`<config>-pkg-debug` for the much larger debug symbols.
2. `test-install` installs `<config>-pkg` in the container of every distro the
packages target and runs the binaries there, so one that cannot be installed
never reaches Nexus.
2. `test-install-deb` and `test-install-rpm` call
[`reusable-package-test-install.yml`](../.github/workflows/reusable-package-test-install.yml)
with their format's package names and distro images, installing each package
in the container of every distro that format targets and running the binaries
there, so one that cannot be installed never reaches Nexus.
3. `publish` uploads both artifacts, or lists what it would upload.
The packaging script derives the package version from the downloaded binary's
@@ -104,6 +161,9 @@ docker run --rm \
# build/rpmbuild/RPMS/x86_64/*.rpm
```
Add `--variant assert` to package binaries built with `-Dassert=ON`; the package
is then named `xrpld-assert`.
### Via CMake (host-side target)
If you run CMake configure on a host that has `rpmbuild` or `dpkg-buildpackage`
@@ -133,11 +193,17 @@ The package version is not a CMake input on this path: `build_pkg.py` derives it
from the just-built `xrpld` binary's `xrpld --version` output. The package
release defaults to 1 and is overridable with `-Dpkg_release=N`.
`-Dassert=ON` passes `--variant assert`, so such a build packages as
`xrpld-assert` without anything else being asked for.
## Publishing packages
Packages are published to the XRPLF repositories on Sonatype Nexus at
`https://packages.xrplf.org`. The `release-info` action decides the channel from
the event, and `publish_pkg.py` maps that channel to its repositories:
Packages are published to the XRPLF repositories on Sonatype Nexus through
`https://packages-upload.xrplf.org`. Reads go through
`https://packages.xrplf.org`, which Cloudflare proxies to cache them and which
rejects request bodies over 100 MB, so uploads use the DNS-only host instead.
The `release-info` action decides the channel from the event, and
`publish_pkg.py` maps that channel to its repositories:
| Event | Version | Channel | DEB repository | RPM upload repository |
| ------------------------ | ----------------- | --------- | -------------- | --------------------- |
@@ -147,6 +213,9 @@ the event, and `publish_pkg.py` maps that channel to its repositories:
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
A variant is published to the same channel under its own name, so
`xrpld-assert` never overwrites `xrpld`.
Only a tag names a channel — do not extend that to `develop`, where
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
version during a release cycle, which would send develop builds into `stable`.
@@ -160,7 +229,7 @@ the last, and the date and hash say which commit a package on
`packages.xrplf.org` came from. Both reach the packaging scripts as arguments,
so neither script derives anything itself.
Publishing is its own job, gated behind `test-install`, uploading from the same
Publishing is its own job, gated behind the install tests, uploading from the same
image that built the packages with the `publish_pkg.py` shipped in it — the
same copy other repositories run. Without `publish: true` the job is a
`--dry-run`, listing the uploads it would make without needing credentials, so
@@ -175,7 +244,7 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP
signing keypair configured in Nexus, which rejects one created without a
keypair. Nexus signs the apt metadata with it, never the packages.
- Hosted yum repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
- yum-hosted repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
repository sits behind a `rpm-<channel>` yum group repository whose metadata
Nexus signs. Uploads go to the hosted repository; clients point at the group
and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs
@@ -244,6 +313,19 @@ pre-release ordering convention, so RPM filenames/NVRs begin with forms like
`xrpld-3.2.0~b1-...` and `xrpld-3.2.0~rc1-...` instead of encoding
pre-releases with an older `0.<release>.<suffix>` RPM `Release` value.
`--variant` is the flavour of the package, empty by default and accepting only
the flavours in `VARIANTS`; see [Package variants](#package-variants). The RPM
path passes it to the spec as the `pkg_variant` macro, which suffixes `Name` and
adds the `Conflicts`/`Provides` pair. Debian control files have no conditionals, so the DEB path renders
`debian/control.in` and `debian/lintian-overrides.in` instead, substituting
`@PKG@` with the package name and `@VARIANT_FIELDS@` with the
`Conflicts`/`Replaces`/`Provides` block, empty for the plain package; a token
with no value fails the build rather than reaching dpkg. The files debhelper
keys by package name (`docs`, `links`, and the units) are staged under that same
name. The paths inside the package are unchanged either way, so `debian/rules`
reads its package name from `dh_listpackages` and names the unit, sysusers,
tmpfiles and logrotate files with `--name xrpld`.
The package format is `--package-type`, either `deb` or `rpm`. It is required,
so a job never silently builds the wrong format for the image it runs in; the
matching build tool still has to be on PATH.
@@ -286,8 +368,13 @@ service restart.
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and
`validator-keys-LICENSE`.
3. Copies `package/debian/` control files into `debbuild/source/debian/`.
4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` where `dh_installsystemd`, `dh_installsysusers`, `dh_installtmpfiles` and `dh_installlogrotate` pick them up automatically.
3. Stages `package/debian/` into `debbuild/source/debian/`: the `.in` templates
are rendered, and the files debhelper keys by package name (`docs`, `links`,
`lintian-overrides`) are staged under the name being built.
4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` as
`<package>.xrpld.*`, which `dh_installsystemd`, `dh_installsysusers`,
`dh_installtmpfiles` and `dh_installlogrotate` read because `debian/rules`
passes them `--name xrpld`.
5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`,
where `pkg_version` is derived from the binary-reported `xrpld` version.
6. Runs `dpkg-buildpackage -b --no-sign -d` (`-d` skips the build-dependency check, since the binary is already built). `debian/rules` uses manual `install` commands.

View File

@@ -21,6 +21,14 @@ SRC_DIR = Path(__file__).resolve().parents[1]
PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)(\+.*)?$")
# The package name a variant suffixes, and the name every variant keeps for its
# on-disk paths (/usr/bin/xrpld, /etc/xrpld, xrpld.service).
BASE_NAME = "xrpld"
# The flavours that can be built, '' being the plain xrpld package. A variant
# needs a config in linux.json to be built by CI; see package/README.md.
VARIANTS = ("", "assert")
# Files both packaging systems consume, staged under the same names.
STAGED_FROM_BUILD = ("xrpld", "validator-keys", "validator-keys-LICENSE")
STAGED_FROM_SRC = {
@@ -31,6 +39,18 @@ STAGED_FROM_SRC = {
}
STAGED_UNITS = ("xrpld.service", "xrpld.sysusers", "xrpld.tmpfiles", "xrpld.logrotate")
# debian/ files debhelper keys by package name, staged as '<package>.<name>'.
DEBIAN_PKG_FILES = ("docs", "links")
# Debian control files have no conditionals, so what makes a variant replace the
# plain package is rendered into control.in rather than written there.
DEB_VARIANT_FIELDS = """\
Conflicts: xrpld
Replaces: xrpld
Provides: xrpld (= ${binary:Version})"""
TOKEN = re.compile(r"@[A-Z_]+@")
def run(*command: object, cwd: Path | None = None) -> None:
"""Echo a command and run it."""
@@ -75,6 +95,28 @@ def package_version(reported: str) -> str:
return version
def render(template: Path, dest: Path, values: dict[str, str]) -> None:
"""Write template to dest with its @TOKEN@ placeholders substituted.
A token left without a value fails the build rather than reaching dpkg.
"""
text = template.read_text()
for token, value in values.items():
text = text.replace(f"@{token}@", value)
missing = sorted(set(TOKEN.findall(text)))
assert not missing, f"{template}: no value for {', '.join(missing)}"
# An empty value at the end of a stanza would otherwise leave a blank line,
# which is what ends a stanza.
dest.write_text(text.rstrip("\n") + "\n")
def package_name(variant: str) -> str:
"""The binary package name for a variant: '' -> xrpld, 'assert' -> xrpld-assert."""
return f"{BASE_NAME}-{variant}" if variant else BASE_NAME
def read_version(xrpld: Path) -> str:
"""Read the version from the binary that is about to be packaged."""
fields = capture(xrpld, "--version").partition("\n")[0].split()
@@ -135,17 +177,18 @@ def stage_common(build_dir: Path, dest: Path) -> None:
shutil.copy2(SRC_DIR / source, dest / name)
def stage_units(dest: Path) -> None:
def stage_units(dest: Path, *, prefix: str = "") -> None:
"""Copy the systemd, sysusers, tmpfiles and logrotate files into dest.
Each format wants them somewhere else: rpmbuild reads them from SOURCES,
debhelper from debian/.
Each format wants them somewhere else: rpmbuild reads them from SOURCES by
path, debhelper from debian/ by package name -- hence 'prefix', which makes
the copies 'xrpld-assert.xrpld.service' and so on.
"""
for name in STAGED_UNITS:
shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / name)
shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / f"{prefix}{name}")
def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
def build_rpm(build_dir: Path, *, version: str, pkg_release: str, variant: str) -> None:
"""Stage the spec and its sources, then build the binary RPMs."""
topdir = build_dir / "rpmbuild"
for name in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"):
@@ -156,6 +199,9 @@ def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
stage_common(build_dir, topdir / "SOURCES")
stage_units(topdir / "SOURCES")
# The spec defaults it to nothing, so a plain build is unchanged.
variant_defines = ["--define", f"pkg_variant {variant}"] if variant else []
run(
"rpmbuild",
"-bb",
@@ -168,10 +214,29 @@ def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
# The image tracks the newest distro, but the packages target el9.
"--define",
"dist .el9",
*variant_defines,
spec,
)
def stage_debian(dest: Path, name: str) -> None:
"""Stage the debian directory for the package name being built."""
source = SRC_DIR / "package" / "debian"
shutil.copytree(
source, dest, ignore=shutil.ignore_patterns("*.in", *DEBIAN_PKG_FILES)
)
values = {
"PKG": name,
"VARIANT_FIELDS": "" if name == BASE_NAME else DEB_VARIANT_FIELDS,
}
render(source / "control.in", dest / "control", values)
render(source / "lintian-overrides.in", dest / f"{name}.lintian-overrides", values)
for suffix in DEBIAN_PKG_FILES:
shutil.copy2(source / suffix, dest / f"{name}.{suffix}")
def build_deb(
build_dir: Path,
*,
@@ -180,21 +245,23 @@ def build_deb(
pkg_release: str,
channel: str,
epoch: int,
name: str,
) -> None:
"""Stage the debian directory and its sources, then build the binary DEBs."""
staging = build_dir / "debbuild" / "source"
stage_common(build_dir, staging)
shutil.copytree(SRC_DIR / "package" / "debian", staging / "debian")
stage_debian(staging / "debian", name)
# debhelper picks these up from debian/ automatically.
stage_units(staging / "debian")
# Prefixed whether it is a variant's name or not: debian/rules names them
# explicitly either way.
stage_units(staging / "debian", prefix=f"{name}.")
date = datetime.fromtimestamp(epoch, timezone.utc).strftime(
"%a, %d %b %Y %H:%M:%S %z"
)
# The leading spaces are significant to dpkg.
changelog = textwrap.dedent(f"""\
xrpld ({version}-{pkg_release}) {channel}; urgency=medium
{name} ({version}-{pkg_release}) {channel}; urgency=medium
* Release {reported}.
-- XRPL Foundation <contact@xrplf.org> {date}
@@ -223,6 +290,14 @@ def main() -> None:
default="1",
help="package release iteration (default: %(default)s)",
)
parser.add_argument(
"--variant",
default="",
choices=VARIANTS,
help="the flavour of the package to build: 'assert' produces "
"xrpld-assert, which ships the same paths as xrpld and replaces it "
"(default: the plain xrpld package)",
)
parser.add_argument(
"--channel",
required=True,
@@ -234,6 +309,8 @@ def main() -> None:
build_dir: Path = args.build_dir.resolve()
pkg_release: str = args.pkg_release
channel: str = args.channel
variant: str = args.variant
name = package_name(variant)
assert build_dir.is_dir(), (
f"build directory not found: {build_dir}. Build the binaries before "
@@ -253,6 +330,8 @@ def main() -> None:
for tree in ("debbuild", "rpmbuild"):
shutil.rmtree(build_dir / tree, ignore_errors=True)
print(f"Building {package_type} {name} {version}-{pkg_release}", flush=True)
if package_type == "deb":
build_deb(
build_dir,
@@ -261,9 +340,10 @@ def main() -> None:
pkg_release=pkg_release,
channel=channel,
epoch=epoch,
name=name,
)
else:
build_rpm(build_dir, version=version, pkg_release=pkg_release)
build_rpm(build_dir, version=version, pkg_release=pkg_release, variant=variant)
if __name__ == "__main__":

View File

@@ -1,4 +1,4 @@
Source: xrpld
Source: @PKG@
Section: net
Priority: optional
Maintainer: XRPL Foundation <contact@xrplf.org>
@@ -11,7 +11,7 @@ Homepage: https://github.com/XRPLF/rippled
Vcs-Git: https://github.com/XRPLF/rippled.git
Vcs-Browser: https://github.com/XRPLF/rippled
Package: xrpld
Package: @PKG@
Architecture: any
Depends:
${shlibs:Depends},
@@ -22,3 +22,4 @@ Description: XRP Ledger daemon
transactions, and maintains the ledger database.
This package also includes the validator-keys tool for validator key
management.
@VARIANT_FIELDS@

View File

@@ -0,0 +1,6 @@
# The /usr/local/bin/rippled symlink is deliberate compatibility for pre-FHS
# layouts, so the Policy 9.1.2 tags it raises are expected.
# TODO: remove alongside debian/links after rippled fully deprecated.
@PKG@: dir-in-usr-local [usr/local/bin/]
@PKG@: file-in-usr-local [usr/local/bin/rippled]
@PKG@: file-in-unusual-dir [usr/local/bin/rippled]

View File

@@ -8,33 +8,58 @@ export DH_VERBOSE = 1
# the binaries actually run on.
LIBC_MIN = 2.31
# The binary package's name, which a variant build changes to e.g. xrpld-assert,
# and the directory debhelper expects its files staged in.
PKG := $(firstword $(shell dh_listpackages))
PKG_DIR = debian/$(PKG)
# The base name, which every package ships under whatever it is called itself.
BASE_NAME = xrpld
# What build_pkg.py stages beside this directory, each installed under its own
# name. The binaries are also the ones checked against LIBC_MIN below.
BINARIES = $(BASE_NAME) validator-keys
CONFIGS = $(BASE_NAME).cfg validators.txt
%:
dh $@
override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
@:
# The unit, sysusers, tmpfiles and logrotate files are named after the daemon
# rather than after the package, so a variant still ships xrpld.service and
# /etc/logrotate.d/xrpld. debhelper only reads debian/$(PKG).$(BASE_NAME).* when told
# the name.
override_dh_installsystemd:
dh_installsystemd --no-stop-on-upgrade xrpld.service
dh_installsystemd --no-stop-on-upgrade --name $(BASE_NAME)
# The tmpfiles snippet sets ownership to the xrpld user, so the sysusers snippet
# has to be emitted first: run it early and make its own sequence slot a no-op.
execute_before_dh_installtmpfiles:
dh_installsysusers
dh_installsysusers --name $(BASE_NAME)
override_dh_installsysusers:
override_dh_installtmpfiles:
dh_installtmpfiles --name $(BASE_NAME)
override_dh_installlogrotate:
dh_installlogrotate --name $(BASE_NAME)
override_dh_install:
install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld
install -D -m 0755 validator-keys debian/xrpld/usr/bin/validator-keys
install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg
install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt
for binary in $(BINARIES); do \
install -D -m 0755 "$$binary" "$(PKG_DIR)/usr/bin/$$binary"; \
done
for config in $(CONFIGS); do \
install -D -m 0644 "$$config" "$(PKG_DIR)/etc/$(BASE_NAME)/$$config"; \
done
override_dh_shlibdeps:
dh_shlibdeps
# Guards against the toolchain moving past LIBC_MIN and the packages then
# claiming a floor they do not meet.
for binary in xrpld validator-keys; do \
for binary in $(BINARIES); do \
needed=$$(readelf --dyn-syms --wide $$binary \
| grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -uV | tail -1); \
if [ -z "$$needed" ]; then \
@@ -46,7 +71,7 @@ override_dh_shlibdeps:
exit 1; \
fi; \
done
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/xrpld.substvars
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/$(PKG).substvars
override_dh_dwz:
@:

View File

@@ -1,6 +0,0 @@
# The /usr/local/bin/rippled symlink is deliberate compatibility for pre-FHS
# layouts, so the Policy 9.1.2 tags it raises are expected.
# TODO: remove alongside debian/xrpld.links after rippled fully deprecated.
xrpld: dir-in-usr-local [usr/local/bin/]
xrpld: file-in-usr-local [usr/local/bin/rippled]
xrpld: file-in-unusual-dir [usr/local/bin/rippled]

View File

@@ -98,7 +98,7 @@ def main() -> None:
)
parser.add_argument(
"--nexus-url",
default="https://packages.xrplf.org",
default="https://packages-upload.xrplf.org",
help="the Nexus instance to publish to (default: %(default)s)",
)
parser.add_argument(

View File

@@ -6,10 +6,14 @@
%{error:pkg_release must be defined}
%endif
Name: xrpld
# The base name, which every package ships under. A variant build
# (build_pkg.py --variant) only suffixes the package name, e.g. xrpld-assert.
%global base_name xrpld
Name: %{base_name}%{?pkg_variant:-%{pkg_variant}}
Version: %{pkg_version}
Release: %{pkg_release}%{?dist}
Summary: XRP Ledger daemon
Summary: XRP Ledger daemon%{?pkg_variant: (%{pkg_variant} build)}
License: ISC
URL: https://github.com/XRPLF/rippled
@@ -17,6 +21,12 @@ URL: https://github.com/XRPLF/rippled
ExclusiveArch: x86_64 aarch64
BuildRequires: systemd-rpm-macros
# A variant owns the same paths, so it stands in for the plain package.
%if "%{?pkg_variant}" != ""
Conflicts: %{base_name}
Provides: %{base_name} = %{version}-%{release}
%endif
# These have to precede %%debug_package: it opens the debuginfo subpackage, and
# any tag after it is silently dropped from the main package.
%{?systemd_requires}
@@ -52,22 +62,22 @@ management.
:
%install
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{name}
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{base_name}
install -Dm0755 %{_sourcedir}/validator-keys %{buildroot}%{_bindir}/validator-keys
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{name}/xrpld.cfg
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{name}/validators.txt
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{base_name}/xrpld.cfg
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{base_name}/validators.txt
# systemd units, sysusers, tmpfiles, preset
install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
install -Dm0644 %{_sourcedir}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
install -Dm0644 %{_sourcedir}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
install -d %{buildroot}%{_presetdir}
cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
cat >%{buildroot}%{_presetdir}/50-%{base_name}.preset <<'EOF'
enable xrpld.service
EOF
# Logrotate config
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name}
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{base_name}
# Docs
install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md
@@ -78,13 +88,13 @@ install -Dm0644 %{_sourcedir}/validator-keys-LICENSE %{buildroot}%{_docdir}/%{na
# Legacy compatibility for pre-FHS package layouts.
# TODO: remove after rippled fully deprecated.
install -d %{buildroot}/usr/local/bin
ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
ln -s %{_bindir}/%{base_name} %{buildroot}/usr/local/bin/rippled
%pre
%sysusers_create_package %{name} %{_sourcedir}/xrpld.sysusers
%sysusers_create_package %{base_name} %{_sourcedir}/xrpld.sysusers
%post
%tmpfiles_create_package %{name} %{_sourcedir}/xrpld.tmpfiles
%tmpfiles_create_package %{base_name} %{_sourcedir}/xrpld.tmpfiles
%systemd_post xrpld.service
%preun
@@ -92,6 +102,13 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
%postun
%systemd_postun xrpld.service
# A flavour swap installs the replacement before erasing this package, so the
# %%preun above has just disabled a unit the replacement still owns. rpm keeps a
# file that another installed package owns, so the unit outliving our own erase
# means exactly that; a plain erase takes it with us and re-presets nothing.
if [ $1 -eq 0 ] && [ -f %{_unitdir}/xrpld.service ]; then
systemctl preset xrpld.service >/dev/null 2>&1 || :
fi
%files
%attr(0755,root,root) %dir %{_docdir}/%{name}
@@ -99,18 +116,18 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
%license %{_docdir}/%{name}/validator-keys-LICENSE
%doc %{_docdir}/%{name}/README.md
%attr(0755,root,root) %dir %{_sysconfdir}/%{name}
%attr(0755,root,root) %dir %{_sysconfdir}/%{base_name}
%{_bindir}/%{name}
%{_bindir}/%{base_name}
%{_bindir}/validator-keys
%config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt
%config(noreplace) %{_sysconfdir}/logrotate.d/%{name}
%config(noreplace) %{_sysconfdir}/%{base_name}/xrpld.cfg
%config(noreplace) %{_sysconfdir}/%{base_name}/validators.txt
%config(noreplace) %{_sysconfdir}/logrotate.d/%{base_name}
%{_unitdir}/xrpld.service
%attr(0644,root,root) %{_presetdir}/50-xrpld.preset
%attr(0644,root,root) %{_presetdir}/50-%{base_name}.preset
%{_sysusersdir}/xrpld.conf
%{_tmpfilesdir}/xrpld.conf
%ghost %dir /var/lib/xrpld

View File

@@ -179,7 +179,7 @@ isPaymentLate(ReadView const& view, SLE::const_ref loanSle)
: ExpiryComparison::Inclusive);
}
namespace accrual {
namespace instant_recognition {
AccountingDeltas
loanOriginationDeltas(Number const& principalRequested, Number const& interestDue)
@@ -217,7 +217,7 @@ loanPaymentDeltas(LoanPaymentParts const& parts)
.debtTotalDelta = (parts.principalPaid + parts.interestPaid) - parts.valueChange};
}
} // namespace accrual
} // namespace instant_recognition
namespace cash_basis {
@@ -250,8 +250,8 @@ namespace {
// Cash-basis accounting applies only when featureLendingProtocolV1_1 is
// enabled AND the specific Vault was created under it (LEVersion ==
// VaultVersion::CashBasis). Vaults created before activation keep accrual-basis
// accounting forever, even after the amendment later turns on.
// VaultVersion::CashBasis). Vaults created before activation keep instant
// interest recognition forever, even after the amendment later turns on.
bool
cashBasisEnabled(SLE::const_ref vaultSle)
{
@@ -268,7 +268,7 @@ loanOriginationDeltas(
{
return cashBasisEnabled(vaultSle)
? cash_basis::loanOriginationDeltas(principalRequested)
: accrual::loanOriginationDeltas(principalRequested, interestDue);
: instant_recognition::loanOriginationDeltas(principalRequested, interestDue);
}
bool
@@ -283,21 +283,22 @@ loanOriginationExceedsVaultMaximum(
return false;
auto const vaultMaximum = vaultSle->at(sfAssetsMaximum);
return accrual::loanOriginationExceedsVaultMaximum(vaultMaximum, vaultTotal, interestDue);
return instant_recognition::loanOriginationExceedsVaultMaximum(
vaultMaximum, vaultTotal, interestDue);
}
Number
loanVaultExposure(SLE::const_ref vaultSle, SLE::const_ref loanSle)
{
return cashBasisEnabled(vaultSle) ? cash_basis::loanVaultExposure(loanSle)
: accrual::loanVaultExposure(loanSle);
: instant_recognition::loanVaultExposure(loanSle);
}
AccountingDeltas
loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const& parts)
{
return cashBasisEnabled(vaultSle) ? cash_basis::loanPaymentDeltas(parts)
: accrual::loanPaymentDeltas(parts);
: instant_recognition::loanPaymentDeltas(parts);
}
namespace detail {

View File

@@ -107,8 +107,12 @@ Config::makeConfig(
else
{
config.outPeers = *limits.outPeers;
config.inPeers = *limits.inPeers;
config.maxPeers = 0;
// Inbound slots only exist if we accept incoming connections, and
// `maxPeers` is the total across both directions. The legacy branch
// above upholds the same two invariants.
config.inPeers = config.wantIncoming ? *limits.inPeers : 0;
config.maxPeers = config.inPeers + config.outPeers;
}
// This will cause servers configured as validators to request that

View File

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

View File

@@ -6,9 +6,11 @@
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBlob.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
@@ -397,6 +399,59 @@ checkEncryptedAmountFormat(STObject const& object)
return tesSUCCESS;
}
bool
isIssuerMirrorCurrent(SLE const& issuance, SLE const& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isIssuerMirrorCurrent : issuance MPTokenIssuance object");
XRPL_ASSERT(
mptoken.getType() == ltMPTOKEN, "xrpl::isIssuerMirrorCurrent : mptoken MPToken object");
return mptoken.isFieldPresent(sfIssuerEncryptedBalance) &&
mptoken[~sfIssuerKeyMirrorEpoch].value_or(0) == issuance[~sfIssuerKeyEpoch].value_or(0);
}
bool
isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isAuditorMirrorCurrent : issuance MPTokenIssuance object");
XRPL_ASSERT(
mptoken.getType() == ltMPTOKEN, "xrpl::isAuditorMirrorCurrent : mptoken MPToken object");
if (!issuance.isFieldPresent(sfAuditorEncryptionKey))
return true;
return mptoken.isFieldPresent(sfAuditorEncryptedBalance) &&
mptoken[~sfAuditorKeyMirrorEpoch].value_or(0) == issuance[~sfAuditorKeyEpoch].value_or(0);
}
bool
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
{
return isIssuerMirrorCurrent(issuance, mptoken) && isAuditorMirrorCurrent(issuance, mptoken);
}
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::setMirrorEpochs : issuance MPTokenIssuance object");
XRPL_ASSERT(mptoken.getType() == ltMPTOKEN, "xrpl::setMirrorEpochs : mptoken MPToken object");
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
if (mptoken.isFieldPresent(sfAuditorEncryptedBalance))
{
if (auto const epoch = issuance[~sfAuditorKeyEpoch].value_or(0); epoch != 0)
mptoken[sfAuditorKeyMirrorEpoch] = epoch;
}
}
TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
{

View File

@@ -1,6 +1,8 @@
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/hash/uhash.h>
@@ -11,10 +13,12 @@
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/detail/STVar.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <cstddef>
#include <memory>
#include <stdexcept>
#include <utility>
#include <vector>
@@ -31,6 +35,11 @@ STPathElement::getHash(STPathElement const& element)
// NIKB NOTE: This doesn't have to be a secure hash as speed is more
// important. We don't even really need to fully hash the whole
// base_uint here, as a few bytes would do for our use.
//
// The note above is only true because the result of this function reaches
// nothing but STPathElement::operator==, where it is a fast-reject
// prefilter ahead of the field comparisons that decide the answer. Do not
// use it to key a container.
for (auto const x : element.getAccountID())
hashAccount += (hashAccount * 257) ^ x;
@@ -51,10 +60,49 @@ STPathElement::getHash(STPathElement const& element)
return (hashAccount ^ hashCurrency ^ hashIssuer);
}
[[nodiscard]] size_t
STPathElement::getHash() const
// For guidance on deciding which option to pursue:
// 1. Try to decrease the size of the STPathSet first. For instance, if a std::optional was
// injected into the type, could you get the same functionality using a std::unique_ptr instead?
// 2. If the size of the STPathSet is already as small as it can be, then consider what the cost
// of increasing STVar::kMaxSize would be on all the other STVar types. Each of those types
// will carry the additional cost of accommodating the larger STPathSet in their SBO.
// 3. If the cost of increasing STVar::kMaxSize is too high, then heap allocate the STPathSet and
// remove this static_assert.
static_assert(
sizeof(STPathSet) <= detail::STVar::kMaxSize,
"STPathSet is too large to fit in STVar's small object optimization. Please verify if it "
"should, if the kMaxSize should be increased, or if STPathSet should be stored on the heap "
"instead of in STVar.");
STPathSet::STPathSet(DeduplicationTag) : seen_{std::make_unique<hardened_hash_set<STPath>>()}
{
return STPathElement::getHash(*this);
}
STPathSet::STPathSet(STPathSet const& other)
: STBase{other}
, CountedObject<STPathSet>{other}
, value_{other.value_}
, seen_{
other.seen_ != nullptr ? std::make_unique<hardened_hash_set<STPath>>(*other.seen_)
: nullptr}
{
}
STPathSet&
STPathSet::operator=(STPathSet const& other)
{
if (this == &other)
{
return *this;
}
auto newSeen = other.seen_ != nullptr
? std::make_unique<hardened_hash_set<STPath>>(*other.seen_)
: nullptr;
STBase::operator=(other);
CountedObject<STPathSet>::operator=(other);
value_ = other.value_;
seen_ = std::move(newSeen);
return *this;
}
STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
@@ -72,7 +120,8 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
Throw<std::runtime_error>("empty path");
}
pushBack(path);
// Move rather than converting the vector to an STPath by copy.
value_.emplace_back(std::move(path));
path.clear();
if (iType == STPathElement::TypeNone)
@@ -132,16 +181,10 @@ STPathSet::move(std::size_t n, void* buf)
bool
STPathSet::assembleAdd(STPath const& base, STPathElement const& tail)
{ // assemble base+tail and add it to the set if it's not a duplicate
XRPL_ASSERT(seen_ != nullptr, "xrpl::STPathSet::assembleAdd : DeduplicationTag");
STPath combined = base;
combined.pushBack(tail);
if (!seenHashes_.insert(combined).second)
{
return false;
}
value_.push_back(std::move(combined));
return true;
return appendUnique([&](auto& value) { value.push_back(std::move(combined)); });
}
bool

View File

@@ -1,6 +1,7 @@
#include <xrpl/protocol/STValidation.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
@@ -15,6 +16,7 @@
#include <xrpl/protocol/Serializer.h>
#include <cstddef>
#include <exception>
#include <utility>
namespace xrpl {
@@ -108,11 +110,42 @@ STValidation::isValid() const noexcept
publicKeyType(getSignerPublic()) == KeyType::Secp256k1,
"xrpl::STValidation::isValid : valid key type");
valid_ = verifyDigest(
getSignerPublic(),
getSigningHash(),
makeSlice(getFieldVL(sfSignature)),
(getFlags() & kVfFullyCanonicalSig) != 0u);
// Log that the signature was never checked, so an operator does not
// read this as a bad key. The log is guarded because it can throw too.
auto reportUncheckable = [this](char const* reason) noexcept {
try
{
JLOG(debugLog().error())
<< "Cannot check the signature of the validation for ledger " << getLedgerHash()
<< ": " << reason;
}
catch (...) // NOLINT(bugprone-empty-catch)
{
// Nothing can be reported when reporting is what failed.
}
};
// The signing hash re-serializes the fields, which can fail. This
// function is noexcept, so report the validation as invalid instead of
// throwing. valid_ stays unset, so a later call checks again.
try
{
valid_ = verifyDigest(
getSignerPublic(),
getSigningHash(),
makeSlice(getFieldVL(sfSignature)),
(getFlags() & kVfFullyCanonicalSig) != 0u);
}
catch (std::exception const& e)
{
reportUncheckable(e.what());
return false;
}
catch (...)
{
reportUncheckable("unknown exception");
return false;
}
}
return valid_.value();

View File

@@ -143,10 +143,10 @@ Serializer::addFieldID(int type, int name)
}
int
Serializer::add8(unsigned char byte)
Serializer::add8(unsigned char byteValue)
{
int const ret = data_.size();
data_.push_back(byte);
data_.push_back(byteValue);
return ret;
}
@@ -210,109 +210,138 @@ Serializer::addVL(void const* ptr, int len)
int
Serializer::addEncoded(int length)
{
std::array<std::uint8_t, 4> bytes{};
// Without this, a negative length would fall into the 1 byte case below and
// be cast to a first byte no header uses. A size too big for int arrives
// here negative as well, since callers pass sizes through this parameter.
if (length < kMinValueOfLengthFor1ByteHeader)
Throw<std::overflow_error>("addEncoded: length is negative or did not fit in an int");
std::array<std::byte, kMaxNumberOfBytesInHeader> bytes{};
int numBytes = 0;
if (length <= 192)
if (length <= kMaxValueOfLengthFor1ByteHeader)
{
bytes[0] = static_cast<unsigned char>(length);
bytes[0] = static_cast<std::byte>(length);
numBytes = 1;
}
else if (length <= 12480)
else if (length <= kMaxValueOfLengthFor2ByteHeader)
{
length -= 193;
bytes[0] = 193 + static_cast<unsigned char>(length >> 8);
bytes[1] = static_cast<unsigned char>(length & 0xff);
// Count from the smallest length a 2 byte header covers.
int const offset = length - kMinValueOfLengthFor2ByteHeader;
bytes[0] = static_cast<std::byte>(
kMinValueOfFirstByteFor2ByteHeader + (offset / kNumberOfValuesInOneByte));
bytes[1] = static_cast<std::byte>(offset % kNumberOfValuesInOneByte);
numBytes = 2;
}
else if (length <= 918744)
else if (length <= kMaxValueOfLengthFor3ByteHeader)
{
length -= 12481;
bytes[0] = 241 + static_cast<unsigned char>(length >> 16);
bytes[1] = static_cast<unsigned char>((length >> 8) & 0xff);
bytes[2] = static_cast<unsigned char>(length & 0xff);
int const offset = length - kMinValueOfLengthFor3ByteHeader;
bytes[0] = static_cast<std::byte>(
kMinValueOfFirstByteFor3ByteHeader + (offset / kNumberOfValuesInTwoBytes));
bytes[1] =
static_cast<std::byte>((offset / kNumberOfValuesInOneByte) % kNumberOfValuesInOneByte);
bytes[2] = static_cast<std::byte>(offset % kNumberOfValuesInOneByte);
numBytes = 3;
}
else
{
Throw<std::overflow_error>("lenlen");
Throw<std::overflow_error>("addEncoded: length is too large to encode");
}
return addRaw(&bytes[0], numBytes);
return addRaw(bytes.data(), numBytes);
}
int
Serializer::encodeLengthLength(int length)
{
if (length < 0)
Throw<std::overflow_error>("len<0");
if (length < kMinValueOfLengthFor1ByteHeader)
{
Throw<std::overflow_error>(
"encodeLengthLength: length is negative or did not fit in an int");
}
if (length <= 192)
if (length <= kMaxValueOfLengthFor1ByteHeader)
return 1;
if (length <= 12480)
if (length <= kMaxValueOfLengthFor2ByteHeader)
return 2;
if (length <= 918744)
if (length <= kMaxValueOfLengthFor3ByteHeader)
return 3;
Throw<std::overflow_error>("len>918744");
return 0; // Silence compiler warning.
Throw<std::overflow_error>("encodeLengthLength: length is too large to encode");
}
int
Serializer::decodeLengthLength(int b1)
Serializer::decodeLengthLength(std::byte firstByte)
{
if (b1 < 0)
Throw<std::overflow_error>("b1<0");
int const firstByteValue = std::to_integer<int>(firstByte);
if (b1 <= 192)
if (firstByteValue <= kMaxValueOfFirstByteFor1ByteHeader)
return 1;
if (b1 <= 240)
if (firstByteValue <= kMaxValueOfFirstByteFor2ByteHeader)
return 2;
if (b1 <= 254)
if (firstByteValue <= kMaxValueOfFirstByteFor3ByteHeader)
return 3;
Throw<std::overflow_error>("b1>254");
return 0; // Silence compiler warning.
Throw<std::overflow_error>("decodeLengthLength: first byte does not start any header");
}
int
Serializer::decodeVLLength(int b1)
Serializer::decodeVLLength(std::byte firstByte)
{
if (b1 < 0)
Throw<std::overflow_error>("b1<0");
int const length = std::to_integer<int>(firstByte);
if (b1 > 254)
Throw<std::overflow_error>("b1>254");
// A bigger value means a longer header, so it is not a length by itself.
if (length > kMaxValueOfLengthFor1ByteHeader)
Throw<std::overflow_error>("decodeVLLength 1 byte: first byte is not a length");
return b1;
return length;
}
int
Serializer::decodeVLLength(int b1, int b2)
Serializer::decodeVLLength(std::byte firstByte, std::byte secondByte)
{
if (b1 < 193)
Throw<std::overflow_error>("b1<193");
int const firstByteValue = std::to_integer<int>(firstByte);
if (b1 > 240)
Throw<std::overflow_error>("b1>240");
if (firstByteValue < kMinValueOfFirstByteFor2ByteHeader)
Throw<std::overflow_error>("decodeVLLength 2 byte: first byte is below the range");
return 193 + ((b1 - 193) * 256) + b2;
if (firstByteValue > kMaxValueOfFirstByteFor2ByteHeader)
Throw<std::overflow_error>("decodeVLLength 2 byte: first byte is above the range");
// Both bytes are bounded by their own type, and the first one is bounded to
// the 2 byte range above, so this cannot leave the range the header covers.
return kMinValueOfLengthFor2ByteHeader +
((firstByteValue - kMinValueOfFirstByteFor2ByteHeader) * kNumberOfValuesInOneByte) +
std::to_integer<int>(secondByte);
}
int
Serializer::decodeVLLength(int b1, int b2, int b3)
Serializer::decodeVLLength(std::byte firstByte, std::byte secondByte, std::byte thirdByte)
{
if (b1 < 241)
Throw<std::overflow_error>("b1<241");
int const firstByteValue = std::to_integer<int>(firstByte);
if (b1 > 254)
Throw<std::overflow_error>("b1>254");
if (firstByteValue < kMinValueOfFirstByteFor3ByteHeader)
Throw<std::overflow_error>("decodeVLLength 3 byte: first byte is below the range");
return 12481 + ((b1 - 241) * 65536) + (b2 * 256) + b3;
if (firstByteValue > kMaxValueOfFirstByteFor3ByteHeader)
Throw<std::overflow_error>("decodeVLLength 3 byte: first byte is above the range");
int const length = kMinValueOfLengthFor3ByteHeader +
((firstByteValue - kMinValueOfFirstByteFor3ByteHeader) * kNumberOfValuesInTwoBytes) +
(std::to_integer<int>(secondByte) * kNumberOfValuesInOneByte) +
std::to_integer<int>(thirdByte);
// A 3 byte header reaches further than kMaxValueOfLengthFor3ByteHeader, which
// is as far as the encoder goes. Refuse the rest, so every length accepted
// here is one that can be written back.
if (length > kMaxValueOfLengthFor3ByteHeader)
Throw<std::overflow_error>("decodeVLLength 3 byte: length is too large to re-encode");
return length;
}
//------------------------------------------------------------------------------
@@ -471,24 +500,24 @@ SerialIter::getRaw(int size)
int
SerialIter::getVLDataLength()
{
int const b1 = get8();
std::byte const firstByte{get8()};
int datLen = 0;
int const lenLen = Serializer::decodeLengthLength(b1);
int const lenLen = Serializer::decodeLengthLength(firstByte);
if (lenLen == 1)
{
datLen = Serializer::decodeVLLength(b1);
datLen = Serializer::decodeVLLength(firstByte);
}
else if (lenLen == 2)
{
int const b2 = get8();
datLen = Serializer::decodeVLLength(b1, b2);
std::byte const secondByte{get8()};
datLen = Serializer::decodeVLLength(firstByte, secondByte);
}
else
{
XRPL_ASSERT(lenLen == 3, "xrpl::SerialIter::getVLDataLength : lenLen is 3");
int const b2 = get8();
int const b3 = get8();
datLen = Serializer::decodeVLLength(b1, b2, b3);
std::byte const secondByte{get8()};
std::byte const thirdByte{get8()};
datLen = Serializer::decodeVLLength(firstByte, secondByte, thirdByte);
}
return datLen;
}

View File

@@ -97,7 +97,7 @@ SHAMap::snapShot(bool isMutable) const
}
void
SHAMap::dirtyUp(SharedPtrNodeStack& stack, uint256 const& target, SHAMapTreeNodePtr child)
SHAMap::dirtyUp(NodePathStack& stack, uint256 const& target, SHAMapTreeNodePtr child)
{
// walk the tree up from through the inner nodes to the root_
// update hashes and links
@@ -111,14 +111,20 @@ SHAMap::dirtyUp(SharedPtrNodeStack& stack, uint256 const& target, SHAMapTreeNode
while (!stack.empty())
{
auto node = intr_ptr::dynamicPointerCast<SHAMapInnerNode>(stack.top().first);
SHAMapNodeID const nodeID = stack.top().second;
stack.pop();
XRPL_ASSERT(node, "xrpl::SHAMap::dirtyUp : non-null node");
auto const depth = stack.topDepth();
auto top = stack.releaseNode();
if (!top->isInner())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::dirtyUp : node is not inner");
Throw<SHAMapMissingNode>(type_, target);
// LCOV_EXCL_STOP
}
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(std::move(top));
auto const branch = selectBranch(nodeID, target);
auto const branch = selectBranch(depth, target);
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
node->setChild(branch, std::move(child));
child = std::move(node);
@@ -126,29 +132,71 @@ SHAMap::dirtyUp(SharedPtrNodeStack& stack, uint256 const& target, SHAMapTreeNode
}
SHAMapLeafNode*
SHAMap::walkTowardsKey(uint256 const& id, SharedPtrNodeStack* stack) const
SHAMap::walkTowardsKey(uint256 const& id, NodePathStack* stack) const
{
XRPL_ASSERT(
stack == nullptr || stack->empty(), "xrpl::SHAMap::walkTowardsKey : empty stack input");
if (stack != nullptr && !stack->empty())
{
// A plain XRPL_ASSERT here is a no-op under NDEBUG; without this guard a non-empty stack
// would be appended to below, leaving the caller with a path that starts mid-walk instead
// of at the root.
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::walkTowardsKey : non-empty stack input");
stack->clear();
return nullptr;
// LCOV_EXCL_STOP
}
auto inNode = root_;
SHAMapNodeID nodeID;
unsigned int noStackDepth = 0;
// Without a caller-supplied stack, `noStackDepth` is the only record of position, so it is
// counted directly here instead of read back from a push. A push fails when the map is
// malformed, by holding a leaf outside the branch it was reached through or a node with no room
// left below it, not because `id` is merely absent; the stack is cleared rather than left
// holding a node that never became a real path entry. Callers tell the two apart by the path,
// which is empty only in the first case.
auto pushCurrent = [&]() -> bool {
if (stack == nullptr || stack->pushNode(inNode, id))
{
return true;
}
stack->clear();
return false;
};
while (inNode->isInner())
{
if (stack != nullptr)
stack->emplace(inNode, nodeID);
if (!pushCurrent())
{
return nullptr;
}
auto const inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(inNode);
auto const branch = selectBranch(nodeID, id);
if (inner->isEmptyBranch(branch))
auto& inner = safeDowncast<SHAMapInnerNode&>(*inNode);
auto const depth = stack != nullptr ? stack->topDepth() : noStackDepth;
auto const branch = selectBranch(depth, id);
if (inner.isEmptyBranch(branch))
return nullptr;
inNode = descendThrow(*inner, branch);
nodeID = nodeID.getChildNodeID(branch);
inNode = descendThrow(inner, branch);
if (stack == nullptr)
{
// Shares pastLeafDepth with pushChild, so this mode and the one with a
// caller-supplied path refuse at the same node. Reachable for the reason that helper
// gives, so it refuses rather than aborts.
bool const tooDeep = pastLeafDepth(depth, *inNode);
SOMETIMES(tooDeep, "xrpl::SHAMap::walkTowardsKey : child too deep");
if (tooDeep)
{
return nullptr;
}
++noStackDepth;
}
}
if (stack != nullptr)
stack->emplace(inNode, nodeID);
if (!pushCurrent())
{
return nullptr;
}
return safeDowncast<SHAMapLeafNode*>(inNode.get());
}
@@ -352,12 +400,29 @@ SHAMap::descend(
!parent->isEmptyBranch(branch), "xrpl::SHAMap::descend : parent branch is non-empty");
SHAMapTreeNode* child = parent->getChildPointer(branch); // NOLINT(misc-const-correctness)
auto childID = parentID.getChildNodeID(branch);
if (child == nullptr)
{
auto const& childHash = parent->getChildHash(branch);
SHAMapTreeNodePtr childNode = fetchNodeNT(childHash, filter);
if (childNode && !belongsAt(childID, *childNode))
{
// A node arriving through the filter is judged by hash, and a hash covers a node's
// contents rather than its position, so this is where a leaf that belongs elsewhere
// enters the map. Judged before canonicalizeChild, after which every later walk would
// see it as part of the tree.
//
// The map is the verdict rather than the node, because refusing one node would only
// make the walk fetch the same thing again: the filter answers from a local cache, so
// the next attempt resolves the same blob to the same place.
JLOG(journal_.warn()) << "Leaf " << childHash << " does not belong at " << childID
<< ", map is invalid";
state_ = SHAMapState::Invalid;
return std::make_pair(nullptr, std::move(childID));
}
if (childNode)
{
childNode = parent->canonicalizeChild(branch, std::move(childNode));
@@ -365,7 +430,7 @@ SHAMap::descend(
}
}
return std::make_pair(child, parentID.getChildNodeID(branch));
return std::make_pair(child, std::move(childID));
}
SHAMapTreeNode*
@@ -412,7 +477,7 @@ SHAMap::descendAsync(
template <class Node>
intr_ptr::SharedPtr<Node>
SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, unsigned int depth)
{
// make sure the node is suitable for the intended operation (copy on write)
XRPL_ASSERT(node->cowid() <= cowid_, "xrpl::SHAMap::unshareNode : node valid for cowid");
@@ -421,72 +486,72 @@ SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
// have a CoW
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
node = intr_ptr::staticPointerCast<Node>(node->clone(cowid_));
if (nodeID.isRoot())
if (depth == 0)
root_ = node;
}
return node;
}
SHAMapLeafNode*
SHAMap::belowHelper(
SHAMapTreeNodePtr node,
SharedPtrNodeStack& stack,
unsigned int branch,
BelowDirection direction) const
SHAMap::belowHelper(NodePathStack& stack, BelowDirection direction) const
{
if (node->isLeaf())
{
auto n = intr_ptr::staticPointerCast<SHAMapLeafNode>(node);
stack.push({node, {kLeafDepth, n->peekItem()->key()}});
return n.get();
}
auto inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::belowHelper : non-empty stack input");
if (stack.empty())
{
stack.emplace(inner, SHAMapNodeID{});
// LCOV_EXCL_START
return nullptr;
// LCOV_EXCL_STOP
}
else
{
stack.emplace(inner, stack.top().second.getChildNodeID(branch));
}
// `scanned` counts how many branches of `inner` we have examined; the branch we look at is
// derived from it, so no index ever goes out of range.
if (auto const& top = stack.top(); top->isLeaf())
return safeDowncast<SHAMapLeafNode*>(top.get());
// The path names each node's position, so descending is only ever "push the node we reached".
// `scanned` counts how many branches of the current node we have examined; the branch we look
// at is derived from it, so no index ever goes out of range. `inner` tracks the node on top of
// the stack, which keeps it alive, so it only needs recomputing after a push.
auto* inner = safeDowncast<SHAMapInnerNode*>(stack.top().get());
for (auto scanned = 0u; scanned < kBranchFactor;)
{
auto const childBranch =
(direction == BelowDirection::Last) ? (kBranchFactor - 1u - scanned) : scanned;
if (!inner->isEmptyBranch(childBranch))
{
node.adopt(descendThrow(inner.get(), childBranch));
XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::belowHelper : non-empty stack");
if (node->isLeaf())
{
auto n = intr_ptr::staticPointerCast<SHAMapLeafNode>(node);
stack.push({n, {kLeafDepth, n->peekItem()->key()}});
return n.get();
}
inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
stack.emplace(inner, stack.top().second.getChildNodeID(branch));
scanned = 0u; // descend and restart the scan on the new node
}
else
if (inner->isEmptyBranch(childBranch))
{
++scanned; // scan next branch
continue;
}
auto const parentDepth = stack.topDepth();
auto descended = descendThrow(*inner, childBranch);
if (!stack.pushChild(std::move(descended), childBranch))
{
// A refused push means the map holds a node that cannot be walked, which is not the
// same as a subtree with no leaf below it. Throwing keeps nullptr meaning only the
// latter, so begin() cannot report such a map as empty while an iterator increment
// throws on the same condition. SHAMapMissingNode describes a resident node poorly,
// but descendThrow above throws it too, so every caller already handles it.
//
// The map is deliberately NOT condemned here. Every caller of belowHelper is a const
// read on an immutable snapshot, called from several RPC threads at once, and no
// reader checks isValid(); the callers that do are on the acquisition path. So the
// write would buy nothing, would race those readers, and would make a later compare()
// trip its own isValid() assertion. A map from peer data is judged where it is
// assembled (see SHAMap::descend and gmnProcessNodes).
JLOG(journal_.warn()) << "Cannot walk below depth " << parentDepth << " at branch "
<< childBranch;
Throw<SHAMapMissingNode>(type_, inner->getChildHash(childBranch));
}
auto const& child = stack.top();
if (child->isLeaf())
return safeDowncast<SHAMapLeafNode*>(child.get());
inner = safeDowncast<SHAMapInnerNode*>(child.get());
scanned = 0u; // descend and restart the scan on the new node
}
return nullptr;
}
SHAMapLeafNode*
SHAMap::lastBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, unsigned int branch) const
{
return belowHelper(node, stack, branch, BelowDirection::Last);
}
SHAMapLeafNode*
SHAMap::firstBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, unsigned int branch) const
{
return belowHelper(node, stack, branch, BelowDirection::First);
}
static boost::intrusive_ptr<SHAMapItem const> const kNoItem;
boost::intrusive_ptr<SHAMapItem const> const&
@@ -529,36 +594,53 @@ SHAMap::onlyBelow(SHAMapTreeNode* node) const
}
SHAMapLeafNode const*
SHAMap::peekFirstItem(SharedPtrNodeStack& stack) const
SHAMap::peekFirstItem(NodePathStack& stack) const
{
XRPL_ASSERT(stack.empty(), "xrpl::SHAMap::peekFirstItem : empty stack input");
SHAMapLeafNode const* node = firstBelow(root_, stack);
if (!stack.pushRoot(root_))
{
// LCOV_EXCL_START
return nullptr;
// LCOV_EXCL_STOP
}
SHAMapLeafNode const* node = belowHelper(stack, BelowDirection::First);
if (node == nullptr)
{
while (!stack.empty())
stack.pop();
// Whether the map was empty or belowHelper's walk otherwise failed to find a leaf, the
// stack is cleared rather than left holding a partial path the caller cannot use.
stack.clear();
return nullptr;
}
return node;
}
SHAMapLeafNode const*
SHAMap::peekNextItem(uint256 const& id, SharedPtrNodeStack& stack) const
SHAMap::peekNextItem(uint256 const& id, NodePathStack& stack) const
{
XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::peekNextItem : non-empty stack input");
XRPL_ASSERT(stack.top().first->isLeaf(), "xrpl::SHAMap::peekNextItem : stack starts with leaf");
if (stack.empty())
{
// LCOV_EXCL_START
return nullptr;
// LCOV_EXCL_STOP
}
XRPL_ASSERT(stack.top()->isLeaf(), "xrpl::SHAMap::peekNextItem : stack starts with leaf");
stack.pop();
while (!stack.empty())
{
auto [node, nodeID] = stack.top();
auto const& node = stack.top();
XRPL_ASSERT(!node->isLeaf(), "xrpl::SHAMap::peekNextItem : another node is not leaf");
auto inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
for (auto i = selectBranch(nodeID, id) + 1; i < kBranchFactor; ++i)
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
for (auto i = selectBranch(stack.topDepth(), id) + 1; i < kBranchFactor; ++i)
{
if (!inner->isEmptyBranch(i))
if (!inner.isEmptyBranch(i))
{
node = descendThrow(*inner, i);
auto leaf = firstBelow(node, stack, i);
auto child = descendThrow(inner, i);
if (!stack.pushChild(std::move(child), i))
{
Throw<SHAMapMissingNode>(type_, id);
}
auto leaf = belowHelper(stack, BelowDirection::First);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
XRPL_ASSERT(leaf->isLeaf(), "xrpl::SHAMap::peekNextItem : leaf is valid");
@@ -595,72 +677,72 @@ SHAMap::peekItem(uint256 const& id, SHAMapHash& hash) const
}
SHAMap::ConstIterator
SHAMap::upperBound(uint256 const& id) const
SHAMap::boundHelper(uint256 const& id, BelowDirection direction) const
{
SharedPtrNodeStack stack;
// Walk back up the path to `id` looking for the nearest leaf on the requested side. At each
// inner node the candidates are the branches on that side of the one `id` takes: the higher
// ones searching forward, the lower ones searching back. The nearest non-empty candidate holds
// the answer, which is its lowest leaf searching forward and its highest searching back.
auto const searchingForward = direction == BelowDirection::First;
NodePathStack stack;
walkTowardsKey(id, &stack);
// An empty path means the walk refused a node, not that the map is empty: an empty map still
// leaves its root on the path. end() is the positive claim that no key lies on the requested
// side of `id`, so it must not stand in for "cannot answer", which is what every other entry
// point reports by throwing.
if (stack.empty())
Throw<SHAMapMissingNode>(type_, id);
while (!stack.empty())
{
auto [node, nodeID] = stack.top();
auto const& node = stack.top();
if (node->isLeaf())
{
auto leaf = safeDowncast<SHAMapLeafNode*>(node.get());
if (leaf->peekItem()->key() > id)
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
auto const& item = safeDowncast<SHAMapLeafNode const&>(*node).peekItem();
if (searchingForward ? (item->key() > id) : (item->key() < id))
return ConstIterator(this, item.get(), std::move(stack));
}
else
{
auto inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
for (auto branch = selectBranch(nodeID, id) + 1; branch < kBranchFactor; ++branch)
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
auto const taken = selectBranch(stack.topDepth(), id);
auto const remaining = searchingForward ? (kBranchFactor - 1u - taken) : taken;
for (auto scanned = 0u; scanned < remaining; ++scanned)
{
if (!inner->isEmptyBranch(branch))
auto const branch =
searchingForward ? (taken + 1u + scanned) : (taken - 1u - scanned);
if (inner.isEmptyBranch(branch))
continue;
auto child = descendThrow(inner, branch);
if (!stack.pushChild(std::move(child), branch))
{
node = descendThrow(*inner, branch);
auto leaf = firstBelow(node, stack, branch);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
Throw<SHAMapMissingNode>(type_, id);
}
auto const leaf = belowHelper(stack, direction);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
}
}
stack.pop();
}
return end();
}
SHAMap::ConstIterator
SHAMap::upperBound(uint256 const& id) const
{
return boundHelper(id, BelowDirection::First);
}
SHAMap::ConstIterator
SHAMap::lowerBound(uint256 const& id) const
{
SharedPtrNodeStack stack;
walkTowardsKey(id, &stack);
while (!stack.empty())
{
auto [node, nodeID] = stack.top();
if (node->isLeaf())
{
auto leaf = safeDowncast<SHAMapLeafNode*>(node.get());
if (leaf->peekItem()->key() < id)
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
}
else
{
auto inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
for (auto branch = selectBranch(nodeID, id); branch > 0u;)
{
--branch;
if (!inner->isEmptyBranch(branch))
{
node = descendThrow(*inner, branch);
auto leaf = lastBelow(node, stack, branch);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
}
}
}
stack.pop();
}
// TODO: what to return here?
return end();
return boundHelper(id, BelowDirection::Last);
}
bool
@@ -675,16 +757,21 @@ SHAMap::delItem(uint256 const& id)
// delete the item with this ID
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
SharedPtrNodeStack stack;
NodePathStack stack;
walkTowardsKey(id, &stack);
if (stack.empty())
Throw<SHAMapMissingNode>(type_, id);
auto leaf = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
stack.pop();
// An absent id leaves an inner node on top rather than a leaf, which is a "not found" answer
// and not a fault, so it is tested rather than cast through. Matches the three sibling sites in
// this commit, so no traversal path is left paying for a dynamic_cast.
auto top = stack.releaseNode();
if (!top->isLeaf())
return false;
auto leaf = intr_ptr::staticPointerCast<SHAMapLeafNode>(std::move(top));
if (!leaf || (leaf->peekItem()->key() != id))
if (leaf->peekItem()->key() != id)
return false;
SHAMapNodeType const type = leaf->getType();
@@ -694,19 +781,26 @@ SHAMap::delItem(uint256 const& id)
while (!stack.empty())
{
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(stack.top().first);
SHAMapNodeID const nodeID = stack.top().second;
stack.pop();
auto const depth = stack.topDepth();
auto top = stack.releaseNode();
if (!top->isInner())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::delItem : node is not inner");
Throw<SHAMapMissingNode>(type_, id);
// LCOV_EXCL_STOP
}
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(std::move(top));
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
node->setChild(
selectBranch(nodeID, id), std::move(prevNode)); // NOLINT(bugprone-use-after-move)
selectBranch(depth, id), std::move(prevNode)); // NOLINT(bugprone-use-after-move)
XRPL_ASSERT(
not prevNode, // NOLINT(bugprone-use-after-move)
"xrpl::SHAMap::delItem : prevNode should be nullptr after std::move");
if (!nodeID.isRoot())
if (depth != 0)
{
// we may have made this a node with 1 or 0 children
// And, if so, we need to remove this branch
@@ -761,14 +855,14 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
// add the specified item, does not update
uint256 const tag = item->key();
SharedPtrNodeStack stack;
NodePathStack stack;
walkTowardsKey(tag, &stack);
if (stack.empty())
Throw<SHAMapMissingNode>(type_, tag);
auto [node, nodeID] = stack.top();
stack.pop();
auto depth = stack.topDepth();
auto node = stack.releaseNode();
if (node->isLeaf())
{
@@ -776,12 +870,12 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
if (leaf->peekItem()->key() == tag)
return false;
}
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
if (node->isInner())
{
// easy case, we end on an inner node
auto inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
auto const branch = selectBranch(nodeID, tag);
auto const branch = selectBranch(depth, tag);
XRPL_ASSERT(
inner->isEmptyBranch(branch), "xrpl::SHAMap::addGiveItem : inner branch is empty");
inner->setChild(branch, makeTypedLeaf(type, std::move(item), cowid_));
@@ -799,13 +893,22 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
auto b1 = 0u, b2 = 0u;
while ((b1 = selectBranch(nodeID, tag)) == (b2 = selectBranch(nodeID, otherItem->key())))
while ((b1 = selectBranch(depth, tag)) == (b2 = selectBranch(depth, otherItem->key())))
{
stack.emplace(node, nodeID);
if (!stack.pushNode(node, tag))
{
// The node pushed here is freshly made and inner, so only the depth bound could
// refuse it, and the loop cannot reach that bound: it advances only while the two
// keys agree at the current nibble, and keys agreeing at all 64 nibbles are equal,
// which the caller already returned false for.
// LCOV_EXCL_START
Throw<SHAMapMissingNode>(type_, tag);
// LCOV_EXCL_STOP
}
// we need a new inner node, since both go on same branch at this
// level
nodeID = nodeID.getChildNodeID(b1);
++depth;
node = intr_ptr::makeShared<SHAMapInnerNode>(cowid_);
}
@@ -848,22 +951,32 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
SharedPtrNodeStack stack;
NodePathStack stack;
walkTowardsKey(tag, &stack);
if (stack.empty())
Throw<SHAMapMissingNode>(type_, tag);
auto node = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
auto nodeID = stack.top().second;
stack.pop();
auto const depth = stack.topDepth();
auto top = stack.releaseNode();
if (!node || (node->peekItem()->key() != tag))
// walkTowardsKey pushes an inner node's own entry before testing whether the branch it needs
// is empty, so a tag absent from the map leaves that inner node on top rather than a leaf.
// No in-tree caller reaches this, since each checks the item exists first, but the API is
// public and permits the call, which is why it returns false rather than reporting UNREACHABLE.
// The static cast below is also safe only once this is confirmed.
if (!top->isLeaf())
{
return false;
}
auto node = intr_ptr::staticPointerCast<SHAMapLeafNode>(std::move(top));
// The other shape an absent tag takes: the walk ends on the leaf it reached, whose key need not
// be `tag`, which is why findKey discards such a leaf. Both shapes mean the same thing to a
// caller, so both answer false rather than reporting UNREACHABLE.
if (node->peekItem()->key() != tag)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::updateGiveItem : invalid node");
return false;
// LCOV_EXCL_STOP
}
if (node->getType() != type)
@@ -872,7 +985,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
return false;
}
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
if (node->setItem(item))
dirtyUp(stack, tag, node);
@@ -1170,7 +1283,7 @@ SHAMap::invariants() const
auto node = root_.get();
XRPL_ASSERT(node, "xrpl::SHAMap::invariants : non-null root node");
XRPL_ASSERT(!node->isLeaf(), "xrpl::SHAMap::invariants : root node is not leaf");
SharedPtrNodeStack stack;
NodePathStack stack;
for (auto leaf = peekFirstItem(stack); leaf != nullptr;
leaf = peekNextItem(leaf->peekItem()->key(), stack))
;

View File

@@ -144,16 +144,18 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
}
[[nodiscard]] unsigned int
selectBranch(SHAMapNodeID const& id, uint256 const& hash)
selectBranch(unsigned int depth, uint256 const& hash)
{
XRPL_ASSERT(id.getDepth() < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
XRPL_ASSERT(depth < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
// A depth-64 ID has no nibble left to select. Callers must not ask, but clamp anyway to keep
// the read below the end of the 32-byte key.
auto const depth = std::min(id.getDepth(), SHAMap::kLeafDepth - 1u);
auto branch = static_cast<unsigned int>(*(hash.begin() + (depth / 2)));
// A depth-64 position has no nibble left to select. Callers must not ask, but clamp anyway to
// keep the read below the end of the 32-byte key.
auto const clamped = std::min(depth, SHAMap::kLeafDepth - 1u);
auto branch = static_cast<unsigned int>(*(hash.begin() + (clamped / 2)));
if ((depth & 1) != 0u)
// Both reads take the clamped depth. Taking the byte from one and the nibble from the other
// would select the high nibble at depth 64 where depth 63 selects the low one.
if ((clamped & 1) != 0u)
{
branch &= 0xf;
}

View File

@@ -238,6 +238,28 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
if (--mn.max <= 0)
return;
}
// Only a leaf has a position of its own to judge, so the type is tested first: that
// also keeps getChildNodeID, which builds a SHAMapNodeID, off every inner child on the
// walk. The depth is tested next so the ID is only asked for a child that can exist.
else if (
d->isLeaf() && nodeID.getDepth() < kLeafDepth &&
!belongsAt(nodeID.getChildNodeID(branch), *d))
{
// The same judgment SHAMap::descend makes, for the path that consults the filter
// through descendAsync instead. descendAsync hooks what it resolves, so the node is
// already part of the tree and refusing it here would not remove it.
//
// `fullBelow` is cleared first, as on the missing-node path above. It is a
// reference into the caller's stack entry, and this node is left on that stack, so
// a later pass over its remaining branches would otherwise reach the full-below
// test with it still set and record this subtree's hash as complete in the
// family-wide cache, where another map would trust it.
JLOG(journal_.warn()) << "Leaf " << childHash << " does not belong below " << nodeID
<< " at branch " << branch << ", map is invalid";
fullBelow = false;
state_ = SHAMapState::Invalid;
return;
}
else if (d->isInner() && !safeDowncast<SHAMapInnerNode*>(d)->isFullBelow(mn.generation))
{
mn.stack.push(se);
@@ -291,6 +313,29 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
auto nodePtr = std::get<3>(deferredNode);
auto const& nodeHash = parent->getChildHash(branch);
// Guarded on depth for the same reason as the sibling test in gmnProcessNodes: a deferred
// entry carries the position the walk held when it posted the read, and the `pending`
// branch there records that position without building a child ID from it. So a child ID is
// asked for here only where the tree has room for one, which is the bound getChildNodeID
// keeps for itself.
if (nodePtr && nodePtr->isLeaf() && parentID.getDepth() < kLeafDepth &&
!belongsAt(parentID.getChildNodeID(branch), *nodePtr))
{
// The same judgment the two synchronous paths make (see SHAMap::descend and the
// descendAsync case in gmnProcessNodes), for a node an async read resolved. Every site
// that knows the position a node is about to take judges it here, which is what lets
// the traversal treat a misplaced leaf as a rarity rather than a routine case.
//
// Skips this node rather than returning: the reads still outstanding hold a pointer to
// `mn`, which lives in getMissingNodes' frame, and this loop is the only thing that
// waits for them. Returning early would let that frame go while a read was still due
// to write through it.
JLOG(journal_.warn()) << "Leaf " << nodeHash << " does not belong below " << parentID
<< " at branch " << branch << ", map is invalid";
state_ = SHAMapState::Invalid;
continue;
}
if (nodePtr)
{ // Got the node
nodePtr = parent->canonicalizeChild(branch, std::move(nodePtr));
@@ -328,10 +373,15 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
512, // number of async reads per pass
f_.getFullBelowCache()->getGeneration());
// Guarded with isValid() for the same reason the late return below is: clearSynching() moves
// the state to Modifying, which would erase a verdict an earlier walk already reached. No path
// to that was found, since every site that condemns the map also clears the fullBelow flag this
// return reads, but the rule holds either way and one conjunct is what it costs.
if (!root_->isInner() ||
intr_ptr::staticPointerCast<SHAMapInnerNode>(root_)->isFullBelow(mn.generation))
{
clearSynching();
if (isValid())
clearSynching();
return std::move(mn.missingNodes);
}
@@ -416,7 +466,11 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
} while (node != nullptr);
if (mn.missingNodes.empty())
// An empty result does not mean the map is complete when the walk judged it impossible on the
// way down: clearSynching() moves the state to Modifying, which would erase that verdict and
// report the map as satisfied. Asking nothing is the only part this has to get right, since
// clearSynching() is what a later walk would read.
if (mn.missingNodes.empty() && isValid())
clearSynching();
return std::move(mn.missingNodes);
@@ -569,10 +623,6 @@ SHAMap::addKnownNode(
{
XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node");
XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node");
XRPL_ASSERT_IF(
treeNode->isLeaf(),
nodeID.isPrefixOf(leafKey(*treeNode)),
"xrpl::SHAMap::addKnownNode : leaf position consistent with node ID");
if (!isSynching())
{
@@ -606,6 +656,17 @@ SHAMap::addKnownNode(
auto prevNode = inner;
std::tie(currNode, currNodeID) = descend(inner, currNodeID, branch, filter);
if (!isValid())
{
// descend judged a node on the way down and condemned the map. Stops here rather than
// falling through, for two reasons: `childHash` was read before that descent, so the
// hash comparison below would report a corrupt node against a sender that sent nothing
// wrong, and if the node descend refused is the one offered here, that comparison would
// instead succeed and hook it after all.
JLOG(journal_.warn()) << "Node " << nodeID << " cannot be hooked into an invalid map";
return SHAMapAddNode::invalid();
}
if (currNode != nullptr)
continue;
@@ -637,6 +698,19 @@ SHAMap::addKnownNode(
return SHAMapAddNode::useful();
}
// A leaf's own key names its position, so a leaf offered for this slot has to agree with
// the ID it was offered under. The hash test above already proves the parent records this
// exact leaf here, so a disagreement is a property of the map rather than of the sender.
// This was an entry assertion, which is stripped under NDEBUG, and the node is hooked
// immediately below.
if (!belongsAt(nodeID, *treeNode))
{
JLOG(journal_.warn()) << "Leaf " << treeNode->getHash() << " does not belong at "
<< nodeID << ", map is invalid";
state_ = SHAMapState::Invalid;
return SHAMapAddNode::invalid();
}
if (backed_)
canonicalize(childHash, treeNode);
@@ -793,7 +867,7 @@ SHAMap::hasLeafNode(uint256 const& tag, SHAMapHash const& targetNodeHash) const
std::optional<std::vector<Blob>>
SHAMap::getProofPath(uint256 const& key) const
{
SharedPtrNodeStack stack;
NodePathStack stack;
walkTowardsKey(key, &stack);
if (stack.empty())
@@ -802,7 +876,7 @@ SHAMap::getProofPath(uint256 const& key) const
return {};
}
if (auto const& node = stack.top().first; !node || node->isInner() ||
if (auto const& node = stack.top(); !node || node->isInner() ||
intr_ptr::staticPointerCast<SHAMapLeafNode>(node)->peekItem()->key() != key)
{
JLOG(journal_.debug()) << "no path to " << key;
@@ -814,7 +888,7 @@ SHAMap::getProofPath(uint256 const& key) const
while (!stack.empty())
{
Serializer s;
stack.top().first->serializeForWire(s);
stack.top()->serializeForWire(s);
path.emplace_back(std::move(s.modData()));
stack.pop();
}

View File

@@ -6,6 +6,8 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/OrderBookDB.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxMeta.h>
@@ -54,6 +56,13 @@ ApplyContext::apply(TER ter)
return view_->apply(base_, tx, ter, parentBatchId_, (flags_ & TapDryRun) != 0u, journal);
}
void
ApplyContext::addOrderBook(Book const& book)
{
if ((flags_ & TapDryRun) == TapNone)
registry.get().getOrderBookDB().addOrderBook(book);
}
std::size_t
ApplyContext::size()
{

View File

@@ -17,6 +17,7 @@
#include <cstdint>
#include <exception>
#include <expected>
#include <memory>
#include <optional>
#include <utility>
@@ -195,7 +196,12 @@ invokePreclaim(PreclaimContext const& ctx)
}())
return preSigResult;
if (TER const result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx)))
// We can't check the fee if we can't compute it, so reject.
auto const baseFee = calculateBaseFee(ctx.view, ctx.tx);
if (!baseFee)
return baseFee.error();
if (TER const result = T::checkFee(ctx, *baseFee))
return result;
}
@@ -223,13 +229,12 @@ invokePreclaim(PreclaimContext const& ctx)
*
* @param view The ledger view to use for fee calculation.
* @param tx The transaction for which the base fee is to be calculated.
* @return The calculated base fee as an XRPAmount.
* @return The calculated base fee. Returns `std::unexpected(temUNKNOWN)` if the transaction
* type is not recognized, and `std::unexpected(tefEXCEPTION)` if the transactor's
* `calculateBaseFee` threw.
*
* @throws std::exception If an error occurs during fee calculation, including
* but not limited to unknown transaction types or internal errors, the function
* logs an error and returns an XRPAmount of zero.
*/
static XRPAmount
static std::expected<XRPAmount, TER>
invokeCalculateBaseFee(ReadView const& view, STTx const& tx)
{
try
@@ -238,13 +243,25 @@ invokeCalculateBaseFee(ReadView const& view, STTx const& tx)
return T::calculateBaseFee(view, tx);
});
}
catch (UnknownTxnType const& e)
catch (UnknownTxnType const&)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::invoke_calculateBaseFee : unknown transaction type");
return XRPAmount{0};
return std::unexpected(temUNKNOWN);
// LCOV_EXCL_STOP
}
catch (std::exception const& e)
{
JLOG(debugLog().error()) << "calculateBaseFee: " << tx.getTransactionID()
<< " threw an exception: " << e.what();
return std::unexpected(tefEXCEPTION);
}
catch (...)
{
JLOG(debugLog().error()) << "calculateBaseFee: " << tx.getTransactionID()
<< " threw an unknown exception";
return std::unexpected(tefEXCEPTION);
}
}
TxConsequences::TxConsequences(NotTEC pfResult)
@@ -416,7 +433,7 @@ preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, Open
}
}
XRPAmount
std::expected<XRPAmount, TER>
calculateBaseFee(ReadView const& view, STTx const& tx)
{
return invokeCalculateBaseFee(view, tx);
@@ -441,13 +458,26 @@ doApply(PreclaimResult const& preclaimResult, ServiceRegistry& registry, OpenVie
{
if (!preclaimResult.likelyToClaimFee)
return {preclaimResult.ter, false};
// For any tx with a real account, preclaim already computed this fee
// successfully against this same view.
auto const baseFee = calculateBaseFee(view, preclaimResult.tx);
if (!baseFee)
{
// LCOV_EXCL_START
JLOG(preclaimResult.j.error())
<< "apply: could not compute base fee: " << transToken(baseFee.error());
return {tefINTERNAL, false};
// LCOV_EXCL_STOP
}
ApplyContext ctx(
registry,
view,
preclaimResult.parentBatchId,
preclaimResult.tx,
preclaimResult.ter,
calculateBaseFee(view, preclaimResult.tx),
*baseFee,
preclaimResult.flags,
preclaimResult.j);
return invokeApply(ctx);

View File

@@ -231,17 +231,38 @@ ValidLoan::finalize(
// must show that payment in its balance and schedule. A payment that clears
// the loan outright instead drives PaymentRemaining to zero, which the
// fully-paid-off and zero due-date checks above pin.
//
// PrincipalOutstanding may stay put on a non-final pay: at integer
// scale, fixCleanup3_2_0 rounds principal up so a fractional
// amortization step does not reduce it. Interest (TVO) still falls.
// Neither balance may grow: a payment never adds to what is owed,
// since late-payment penalties are charged in the same transaction
// rather than tracked in TotalValueOutstanding.
if (isTesSuccess(result) && txType == ttLOAN_PAY)
{
if (before && after->at(sfPaymentRemaining) != 0)
{
if (!(after->at(sfPrincipalOutstanding) < before->at(sfPrincipalOutstanding)))
if (after->at(sfPrincipalOutstanding) > before->at(sfPrincipalOutstanding))
{
JLOG(j.fatal()) << "Invariant failed: loan pay must strictly decrease "
JLOG(j.fatal()) << "Invariant failed: loan pay must not increase "
"PrincipalOutstanding on a non-full-repayment";
return false;
}
if (!(after->at(sfPaymentRemaining) < before->at(sfPaymentRemaining)))
if (after->at(sfTotalValueOutstanding) > before->at(sfTotalValueOutstanding))
{
JLOG(j.fatal()) << "Invariant failed: loan pay must not increase "
"TotalValueOutstanding on a non-full-repayment";
return false;
}
if (after->at(sfPrincipalOutstanding) == before->at(sfPrincipalOutstanding) &&
after->at(sfTotalValueOutstanding) == before->at(sfTotalValueOutstanding))
{
JLOG(j.fatal()) << "Invariant failed: loan pay must decrease "
"PrincipalOutstanding or TotalValueOutstanding "
"on a non-full-repayment";
return false;
}
if (after->at(sfPaymentRemaining) >= before->at(sfPaymentRemaining))
{
JLOG(j.fatal()) << "Invariant failed: loan pay must decrease "
"PaymentRemaining on a non-full-repayment";

View File

@@ -299,27 +299,46 @@ ValidMPTIssuance::finalize(
return false;
}
}
else if (lendingProtocolEnabled && (mptokensCreated_ + mptokensDeleted_) > 1)
else
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
"but created/deleted bad number mptokens";
return false;
}
else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
"succeeded but created/deleted mptokens";
return false;
}
else if (
!submittedByIssuer && hasPrivilege(tx, Privilege::MustAuthorizeMpt) &&
(mptokensCreated_ + mptokensDeleted_ != 1))
{
// if the holder submitted this tx, then a mptoken must be
// either created or deleted.
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder "
"succeeded but created/deleted bad number of mptokens";
return false;
// Cap on MPToken creates and deletes while featureLendingProtocol is enabled.
// - LoanSet: at most two creates and no deletes.
// - VaultWithdraw: at most one create and one delete.
// - Other MayAuthorizeMpt types: created + deleted <= 1.
// - MustAuthorizeMpt still requires exactly one create or delete below.
auto const mptokensExceedAuthorizeCap = [&] {
if (!lendingProtocolEnabled)
return false;
if (rules.enabled(fixCleanup3_4_0))
{
if (txnType == ttLOAN_SET)
return mptokensDeleted_ != 0 || mptokensCreated_ > 2;
if (txnType == ttVAULT_WITHDRAW)
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
}
return (mptokensCreated_ + mptokensDeleted_) > 1;
};
if (mptokensExceedAuthorizeCap())
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
"but created/deleted bad number mptokens";
return false;
}
if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
"succeeded but created/deleted mptokens";
return false;
}
if (!submittedByIssuer && hasPrivilege(tx, Privilege::MustAuthorizeMpt) &&
(mptokensCreated_ + mptokensDeleted_ != 1))
{
// if the holder submitted this tx, then a mptoken must be
// either created or deleted.
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder "
"succeeded but created/deleted bad number of mptokens";
return false;
}
}
return true;

View File

@@ -439,6 +439,12 @@ CheckCash::doApply()
AccountID const& deliverIssuer = flowDeliver.getIssuer();
auto const err = flowDeliver.asset().visit(
[&](Issue const& issue) -> std::optional<TER> {
// An issuer needs no holder-limit waiver to receive its own currency.
if (deliverIssuer == accountID_ && ctx_.view().rules().enabled(fixCleanup3_4_0))
{
return std::nullopt;
}
// If a trust line does not exist yet create one.
Issue const& trustLineIssue = issue;
AccountID const truster = deliverIssuer == accountID_ ? srcId : accountID_;

View File

@@ -2,8 +2,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/OrderBookDB.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
@@ -397,7 +395,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
Book const book{assetIn, assetOut, std::nullopt};
auto const dir = keylet::quality(keylet::book(book), uRate);
if (auto const bookExisted = static_cast<bool>(sb.read(dir)); !bookExisted)
ctx.registry.get().getOrderBookDB().addOrderBook(book);
ctx.addOrderBook(book);
};
addOrderBook(amount.asset(), amount2.asset(), getRate(amount2, amount));
addOrderBook(amount2.asset(), amount.asset(), getRate(amount, amount2));

View File

@@ -6,7 +6,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OrderBookDB.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
@@ -634,7 +633,7 @@ OfferCreate::applyHybrid(
bookArr.pushBack(std::move(bookInfo));
if (!bookExists)
ctx_.registry.get().getOrderBookDB().addOrderBook(book);
ctx_.addOrderBook(book);
sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
return tesSUCCESS;
@@ -1014,7 +1013,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
sb.insert(sleOffer);
if (!bookExisted)
ctx_.registry.get().getOrderBookDB().addOrderBook(book);
ctx_.addOrderBook(book);
JLOG(j_.debug()) << "final result: success";

View File

@@ -36,6 +36,15 @@
namespace xrpl {
namespace {
// Returns true if the transaction's payment amount is malformed. A loan
// payment must be strictly positive: zero would move nothing, and a negative
// amount is not a payment at all.
bool
isPaymentAmountInvalid(STAmount const& amount)
{
return amount <= beast::kZero;
}
// Returns the account's true, unclamped balance in `asset`, for use only in
// fund-conservation checks. accountHolds(..., SpendableHandling::FullBalance)
// cannot be used for this: for XRP it always defers to xrpLiquid, which
@@ -81,7 +90,7 @@ LoanPay::preflight(PreflightContext const& ctx)
if (ctx.tx[sfLoanID] == beast::kZero)
return temINVALID;
if (ctx.tx[sfAmount] <= beast::kZero)
if (isPaymentAmountInvalid(ctx.tx[sfAmount]))
return temBAD_AMOUNT;
// The loan payment flags are all mutually exclusive. If more than one is
@@ -103,10 +112,19 @@ LoanPay::preflight(PreflightContext const& ctx)
XRPAmount
LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto fixEnabled313 = view.rules().enabled(fixCleanup3_1_3);
auto fixEnabled340 = view.rules().enabled(fixCleanup3_4_0);
using namespace lending;
auto const normalCost = Transactor::calculateBaseFee(view, tx);
if (fixEnabled340 && isPaymentAmountInvalid(tx[sfAmount]))
{
// Let preflight worry about the error for this
return normalCost;
}
if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment))
{
// The loan will be making one set of calculations for one full or late
@@ -179,8 +197,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
static constexpr std::int64_t kMaxFeeIncrements =
kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
if (view.rules().enabled(fixCleanup3_1_3) &&
amount >= regularPayment * kLoanMaximumPaymentsPerTransaction)
if (fixEnabled313 && amount >= regularPayment * kLoanMaximumPaymentsPerTransaction)
{
// The payment handler will never process more than
// loanMaximumPaymentsPerTransaction payments (including overpayments),

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