Compare commits

...

30 Commits

Author SHA1 Message Date
Nik Bougalis
3c6279b735 Restructure the slab allocator
- Define size classes as template parameters, validated at compile
  time, allowing allocators to be constinit-constructed
- Move slab metadata out of the data buffer into a cache-line-aligned
  global pool
- Replace per-block mutexes with spinlocks, and add lock-free
  "parking" slots so most alloc/dealloc pairs avoid the lock entirely
- Release a slab's buffer when it fully drains; cache one spare
  buffer to avoid thrashing around slab boundaries
- Make the fallback path a compile-time policy (heap or none)
- Add sized deallocate() overloads that route directly to the
  owning size class
2026-10-05 19:42:53 -07:00
Nik Bougalis
55b265a8c2 Improve BaseUInt:
This commit reworks the BaseUInt class, which was originally taken from
Bitcoin and has since been heavily modified.

The internal limb type now widens from 32 to 64 bits when the requested
width is a multiple of 64. This halves the iteration count when working
on limbs; coupled with the use of add-with-carry intrinsics, the result
is better code generation and optimized use of processor resources.

The constructors have been reshuffled, constraining how an instance can
be initialized. As a result, many common initialization errors will now
fail at compile time instead of run time.

A `BaseUInt` can now be initialized with:

 - A byte sequence of fixed size, i.e an `std::array`, a C array, or a
   fixed-extent `std::span` of `unsigned char` or `std::byte`.
 - A string literal, formatted as a hexadecimal string with no leading
   0x. Malformed or improperly sized inputs are a compile-time error.
 - An unsigned integer. This constructor is mostly used for tests, and
   must execute at compile time.
 - A byte sequence whose length is not known at compile time, via the
   fromRaw() function which returns a seated `std::optional` only if
   the input is byte sequence is properly sized.

The runtime raw-pointer and `std::uint64_t` constructors are gone, as
are the container assignment operator and the the `fromVoid` and
`fromVoidChecked` functions.

Other changes:

 - The SFINAE-based container trait is replaced by the ByteCopySource
   and FixedByteRange concepts.
 - operator== and operator<=> are now hidden friends. The workaround
   in the old operator<=> is gone.
 - Efficient tag conversion is now possible.
 - `BaseUInt` is now `noexcept` and (almost) fully `constexpr`: every
   operation except byte access, hashing and text output is usable in
   constant evaluation.

Cleanups:

 - Most invocations of `stringIsUInt256Sized` in PeerImp are now gone
   and leverage `fromRaw`.
2026-10-05 19:42:52 -07:00
Nik Bougalis
02e40e7a00 Restructure the spinlock code:
- Add function-based spinlock API
- Improve comments
- Reduce bouncing with contested locks
2026-10-05 19:42:50 -07:00
Nik Bougalis
f14395427e Modernize CountedObject infrastructure:
- Replace the lazy singleton with a constinit registry of per-type
  static counters. The `getInstance` method is removed.
- A concurrency bug that would corrupt the list of counters during
  insertion has been fixed.
- Debug asserts can detect incorrect accounting that can result in
  counter under- or overflow.
- `CountedObject` can only be used as a CRTP base for its own type
  parameter.
- Track the current and maximum values of counter. This results in
  a change in the output of `get_counts`, with individual counters
  now reported as sub-objects of a `counters` object.
2026-10-05 19:42:50 -07:00
Nik Bougalis
b707bac74f Improve AccountID base58 conversion cache:
The previous cache guarded a configurably-sized vector with 64
packed spinlocks. Two aspects of that setup were not worth the
cost:

- Callers were forced to perform atomic RMW operations on a
  single word which was shared by all 64 locks; this caused
  the lock line to bounce between cores on every lookup.

- The configurable size and runtime initialization resulted
  in complexity that did not yield a meaningful improvement
  in performance.

This commit ditches the 64 packed spinlocks, replacing them
with per-entry sequence locks, allowing readers on the fast
path to avoid performing any stores at all.

Cache entries are now carefully sized and aligned to fit into
a typical cache line, which helps avoid false sharing and any
unnecessary coherence traffic.

The cache is now a fixed array of 65,536 entries. The size was
chosen to balance capacity against memory overhead and to keep
the indexing operation fast and simple: since AccountID values
are uniformly distributed, two bytes can serve directly as the
index without requiring additional hashing.

Note: The reader path will perform unsynchronized reads against
      concurrent writes; the sequence lock protocol detects and
      discards any data observed mid-update, so these races are
      benign. A sanitizer like TSAN can still flag them and the
      warnings are expected. A compile-time knob to disable the
      cache is available, if necessary.
2026-10-05 19:42:50 -07:00
Nik Bougalis
da2b5d5eaf Remove beast::typeName
Replace Beast's demangling wrapper (originally by @HowardHinnant)
with calls to `boost::core::type_name`, eliminating the low-level
dependency and manual memory management from the codebase.
2026-10-05 19:42:50 -07:00
Nik Bougalis
4de4902200 Rework and clean up the safe_cast framework:
The SafeToCast concept transposed its signedness clause: it was written in
<Dest, Src> order but it declared <Src, Dest>. This introduced what can be
best described as a polarity bug. As a result:

* Signed-to-wider-unsigned casts were wrongly deemed safe.
* Unsigned-to-wider-signed casts were wrongly deemed unsafe.

The root cause was drift caused by the safety condition being repeated in
the concept and again as a static_assert in safeCast. The checking is now
done only in the concept and is expressed in terms of range coverage, and
not via a sizeof/signedness proxy. This change also improves the handling
of bool (which previously compiled despite truncating) and eliminates the
platform-dependent accept/reject behavior for same-size types.

Additional fixes:

* The pointer form of safeDowncast now performs the same static_cast
  in all builds. The dynamic_cast check is now entirely contained in
  the XRPL_ASSERT.
* Extended integer types wider than intmax_t (e.g. __int128) are now
  excluded from SafeToCast instead of silently misevaluating through
  wrapped bounds; they remain expressible via unsafeCast.

Cleanups:

* Remove the single-parameter enum overload of safeCast: it had an
  explicitly-specified template argument binding to Src, where all
  sibling overload bound to Dest, silently yielding the underlying
  type from expressions that read as casts *to* an enum.
* Add checkedCast for conversions guarded by runtime bounds checks
  where safety varies per instantiation.
* Constrain safeDowncast to polymorphic sources and genuine public
  unambiguous downcasts, rejecting upcasts and unrelated types.
* Route enum conversions through std::to_underlying; clarify the
  unsafeCast/checkedCast documentation split.
2026-10-05 19:42:49 -07:00
Nik Bougalis
2ea1f09dcb Modernize beast::Zero for C++20
- Replace the twelve comparison operators with two
  `constexpr`-capable replacements: `==` and `<=>`
  and allow the compiler to synthesize the rest.
- Constrain `==` and `<=>` using concepts and mark
  them as conditionally noexcept).
- Remove the detail::zero_helper indirection which
  did not do what its comment claimed.
2026-10-05 19:30:38 -07:00
Nik Bougalis
68b4b894de Make ApplyFlags a scoped enumeration:
Convert ApplyFlags to a scoped enum, drop the now-redundant `Tap`
prefix from its enumerators and replace the hand-written bitwise
operators by opting in to enum_bitops.

No functional change.
2026-10-05 19:30:38 -07:00
Nik Bougalis
defd83e5a6 Make STPathElement::Type a scoped enumeration:
`STPathElement` stored its type as an `unsigned int`, returned it
as a `std::uint32_t`, while the flags themselves were an unscoped
`enum` (i.e. an `int`). The type field has always been serialized
as a single byte.

This commit turns `STPathElement::Type` into a scoped `enum` with
a fixed sized that matches the serialized width, and opts it into
the bitwise operators from `enum_bitops.h`:

- Store the type as `Type`, and use `Type` for the constructor
  parameter and the return type of `getNodeType()`.
- Keep the existing spellings such as `STPathElement::TypeAccount`
  valid through `using enum`.
- Replace raw mask tests in the serializer, JSON output, strand
  construction and pathfinder with the existing predicates.
- Validate the type byte once when deserializing, before any field
  is read, and restructure the loop around that.
- Make `operator==` a hidden friend.
- Mark the trivial accessors `noexcept`.

No functional changes are intended. The one observable difference
is the rejection error generated for a malformed path element: if
the currency and MPT bits are both set and the input is truncated
within the account field, the invalid bit combination is detected
first; previously the short read would be detected. Such elements
are rejected either way.
2026-10-05 19:30:38 -07:00
Nik Bougalis
452e31a05f Introduce opt-in bitwise operators for scoped enumerations:
Scoped enumerations do not support bitwise operators, so using an
`enum class` as a set of flags requires extensive casting at call
sites or a hand-written set of operators for each type.

Add `enum_bitops.h`, which provides `&`, `|`, `^`, `~` along with
the compound assignment forms for any scoped enumeration that has
an unsigned underlying type and is opted in:

    enum class Flags : std::uint8_t { a = 1, b = 2 };

    // Opt into bitwise operations
    template <>
    struct enum_bitops::OptIn<Flags> : std::true_type {};

All synthesized operators are constexpr and noexcept, and preserve
the enumeration type.
2026-10-05 19:30:37 -07:00
Mayukha Vadari
1798262d7a refactor: Use LedgerHashesEntry everywhere (#8369) 2026-10-05 18:15:02 +00:00
Mayukha Vadari
cd633b9ac9 refactor: Use EscrowEntry everywhere (#8354) 2026-10-05 17:51:18 +00:00
Ayaz Salikhov
5c5f7d315a chore: Update nix flake file (#8479) 2026-10-05 16:58:41 +00:00
Mayukha Vadari
0f7493ce50 refactor: Use FeeSettingsEntry everywhere (#8370) 2026-10-05 16:56:41 +00:00
Harshit Gupta
40f61f828a fix: Add string type validation for channel_id and signature (#7582) 2026-10-05 16:55:14 +00:00
Mayukha Vadari
108277f4b7 test: Migrate three beast suites from beast::unit_test to gtest (#7993) 2026-10-05 15:28:05 +00:00
Bart
651bb207b4 refactor: Build Throw messages with std::format (#8474)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-10-03 16:45:34 +00:00
Peter Chen
c45363fd8b feat: Implement Confidential mpt holder key update (#8266) 2026-10-02 22:54:38 +00:00
Chenna Keshava B S
f1744cb76e fix: Do not block MPToken deletion on unrelated confidential balances (#8209) 2026-10-02 22:54:31 +00:00
Gregory Tsipenyuk
bcbaa4df07 fix: Enable the large Number mantissa with MPTokensV2 (#8330) 2026-10-02 21:42:14 +00:00
Alex Kremer
3cd357949c chore: Add ignore revs for recent style changes (#8463) 2026-10-02 21:21:09 +00:00
Ayaz Salikhov
a9027bb997 ci: Make release always go into stable channel (#8469) 2026-10-02 19:02:46 +00:00
Bart
0a6da4de74 test: Restore config test environment variables with a guard (#8333)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-02 18:24:41 +00:00
Ayaz Salikhov
6a05339c6c ci: Add more guardrails for merging releases back to develop (#8466) 2026-10-02 15:43:18 +00:00
Shawn Xie
0c41a87604 feat: Add MPT transaction stream to subscribe RPC (#5671) 2026-10-02 14:55:23 +00:00
Ayaz Salikhov
f5938e4097 docs: Document new release process (#8467) 2026-10-02 14:55:23 +00:00
Pratik Mankawde
c7d10c1b60 docs: Document the LedgerMaster class (#8160)
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
2026-10-02 14:50:09 +00:00
Kassaking7
84e2a155b9 fix: Paginate account_lines/offers/channels past new owner-dir types (#8274) 2026-10-02 14:48:48 +00:00
Félix
cbade49976 feat: Add lean toolchain to nix (#8186) 2026-10-02 14:48:17 +00:00
290 changed files with 9247 additions and 4958 deletions

View File

@@ -255,6 +255,7 @@ words:
- queuable
- Raphson
- rcflags
- reencrypted
- replayer
- repodata
- repomd

4
.envrc
View File

@@ -8,3 +8,7 @@ watch_file rust-toolchain.toml
watch_dir conan
use flake
# Optional, untracked local overrides. To use a different shell, put e.g.
# `use flake .#formal-verification` in .envrc.local.
source_env_if_exists .envrc.local

View File

@@ -5,6 +5,18 @@
# This file is sorted in reverse chronological order, with the most recent commits at the top.
# The commits listed here are ignored by git blame, which is useful for formatting-only commits that would otherwise obscure the history of changes to a file.
# chore: CamelCase for typedef/using in `clang-tidy` (#8177)
97a1824537d20d82d25bf151a37a7a4532ebf085
# chore: Rename CamelCase namespaces to snake_case (#7933)
06488c1318d96f56d0536251bee08ac85fa7fdd3
# style: Unify style for all Doxygen comments (#7776)
73b6852a122854140336e6e6bc30a3a4b41aa5fd
# style: More clang-tidy identifier renaming (#7290)
a830ab10efed8d3e59ef2fc15d66efdf9c6bb0d8
# refactor: Rename static constants (#7120)
5b6e8b6f93b19c1e3f6a3467a25639031d9d9a53
# chore: More fixes for bad renames (#7092)
7afdd71a54d562b32a50b29a5aa00bb997dc9053
# refactor: Enable clang-tidy `readability-identifier-naming` check (#6571)
8995564ed6b9e453e144bb663303072a3c1ba305
# refactor: Enable remaining clang-tidy `cppcoreguidelines` checks (#6538)

View File

@@ -36,4 +36,4 @@ runs:
- name: Determine release channel and package release
id: release_info
uses: XRPLF/actions/release-info@ebcf6cea14eee258697308a51fea55cad777581b
uses: XRPLF/actions/release-info@a9f2eeca6fb3980ba3a84cf68566f1c69ad30674

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
# This script fails if <head> merges a release back into <base>,
# but also adds commits that aren't on a release or staging branch, see RELEASING.md.
# Merge commits are allowed.
# Usage: .github/scripts/releasing/check-merge-back-commits.sh <base> <head>
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <base> <head>"
exit 1
fi
BASE=$1
HEAD=$2
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
# shellcheck source=.github/scripts/releasing/common.sh
source "${SCRIPT_DIR}/common.sh"
load_release_branches
# A PR is a merge-back if some of its commits are on a release or staging branch.
PR_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}")
NEW_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}" --not "${BRANCHES[@]}")
if ((NEW_COUNT == PR_COUNT)); then
echo "This PR doesn't merge a release back."
exit 0
fi
if ((NEW_COUNT == 0)); then
echo "This merge-back adds no commits of its own."
exit 0
fi
echo "This PR merges a release back, but also adds commits that aren't on a release or staging branch:"
git log --no-merges --format=' %h %s' "${BASE}..${HEAD}" --not "${BRANCHES[@]}"
echo
echo "Make these changes in a separate PR, see RELEASING.md."
exit 1

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
@@ -21,11 +21,10 @@ patch_ids() {
git log --no-merges --patch --no-color --no-ext-diff "$@" | git patch-id --stable | sort
}
mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
if [ "${#BRANCHES[@]}" -eq 0 ]; then
echo "Error: No release or staging branches found."
exit 1
fi
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
# shellcheck source=.github/scripts/releasing/common.sh
source "${SCRIPT_DIR}/common.sh"
load_release_branches
# Each line is "<patch-id> <commit>".
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
# This script fails if <head> contains release or staging commits that <base> does not,
# i.e. if <head> merges a release back into <base>.
# Used in the merge queue, which squashes PRs and would drop the merge commit, see RELEASING.md.
# Usage: .github/scripts/releasing/check-no-merge-back.sh <base> <head>
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <base> <head>"
exit 1
fi
BASE=$1
HEAD=$2
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
# shellcheck source=.github/scripts/releasing/common.sh
source "${SCRIPT_DIR}/common.sh"
load_release_branches
RELEASE_COMMITS=$(git rev-list "${BRANCHES[@]}" --not "${BASE}")
HEAD_COMMITS=$(git rev-list "${BASE}..${HEAD}")
# The release commits in <head>, newest first.
MERGED=$(grep -xF -f <(echo "${RELEASE_COMMITS}") <<<"${HEAD_COMMITS}" || true)
if [ -z "${MERGED}" ]; then
echo "No release commits are merged back."
exit 0
fi
echo "This PR merges $(wc -l <<<"${MERGED}" | tr -d ' ') release commits back, e.g.:"
head -5 <<<"${MERGED}" | xargs git log --no-walk --format=' %h %s'
echo
echo "Merge-backs must not go through the merge queue, which squashes them."
echo "Fast-forward develop to the PR branch instead, see RELEASING.md."
exit 1

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail

12
.github/scripts/releasing/common.sh vendored Normal file
View File

@@ -0,0 +1,12 @@
# shellcheck shell=bash
# Helpers shared by the release checks in this directory, see RELEASING.md.
# Sets BRANCHES to all release and staging branches, and fails if there are none.
load_release_branches() {
mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
if [ "${#BRANCHES[@]}" -eq 0 ]; then
echo "Error: No release or staging branches found."
exit 1
fi
}

View File

@@ -166,10 +166,31 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Check for copied release commits
if: ${{ github.event_name == 'pull_request' }}
env:
BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: .github/scripts/releasing/check-no-copied-release-commits.sh "${BASE}" "${HEAD}"
# Runs even if the previous check fails, so that both problems are reported at once.
- name: Check merge-back has no new commits
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: .github/scripts/releasing/check-merge-back-commits.sh "${BASE}" "${HEAD}"
# The queue squashes PRs, so check the PR's own branch, named in the queue branch.
- name: Check the merge queue doesn't merge a release back
if: ${{ github.event_name == 'merge_group' }}
env:
BASE: ${{ github.event.merge_group.base_sha }}
HEAD_REF: ${{ github.event.merge_group.head_ref }}
run: |
if ! [[ "${HEAD_REF}" =~ /pr-([0-9]+)-[0-9a-f]+$ ]]; then
echo "Error: Can't find the PR number in '${HEAD_REF}'."
exit 1
fi
git fetch --no-tags origin "refs/pull/${BASH_REMATCH[1]}/head"
.github/scripts/releasing/check-no-merge-back.sh "${BASE}" FETCH_HEAD
clang-tidy:
needs: should-run

View File

@@ -269,14 +269,23 @@ jobs:
env:
CONTEXT: image-context
IMAGE: ${{ matrix.target == 'voidstar' && 'xrpld-voidstar' || 'xrplf/xrpld' }}:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
# Docker Hub is public, so a private build never reaches it;
# the Antithesis registry is not.
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public') }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Determine release info
id: release_info
uses: ./.github/actions/release-info
# Docker Hub is public, so it only gets builds whose packages are public:
# those of a public codebase, and stable releases.
# The Antithesis registry is private, so it gets every build.
- name: Decide whether to push
env:
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public' || steps.release_info.outputs.channel == 'stable') }}
run: echo "PUSH=${PUSH}" | tee -a "${GITHUB_ENV}"
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:

3
.gitignore vendored
View File

@@ -92,6 +92,9 @@ target/
# Direnv's directory
/.direnv
# Direnv's local, per-developer overrides
/.envrc.local
# clangd cache
/.cache

View File

@@ -22,6 +22,19 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
## XRP Ledger server version 3.5.0
Version 3.5.0 is not yet released.
### Additions in 3.5.0
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
### Bugfixes in 3.5.0
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
## XRP Ledger server version 3.4.0
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.

View File

@@ -41,15 +41,13 @@ branch.
git checkout develop
```
For a release candidate, choose the relevant release branch, e.g.
`release/3.2.x`.
For a release or release candidate, check out its [tag](https://github.com/XRPLF/rippled/releases), e.g.:
```bash
git checkout release/3.2.x
git checkout 3.4.0
```
For a stable release, choose one of the [tagged
releases](https://github.com/XRPLF/rippled/releases).
See [RELEASING.md](./RELEASING.md) for how branches and releases are organized.
A build reports `0.0.0-dev` with its short commit hash as build metadata, e.g.
`0.0.0-dev+0123abc`. Only builds of a release, from a tag in CI or a versioned

View File

@@ -14,9 +14,13 @@ The following branches exist in the main project repository:
- `develop`: The latest set of unreleased features, and the most common
starting point for contributions.
- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line,
holding the latest release candidate, or stable release for that line.
Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
- `staging/*` (e.g. `staging/3.4.x`): Staging branches, one per release line,
where fixes for that line are developed.
- `release/*` (e.g. `release/3.4.x`): Release branches, one per release line,
holding the latest public release candidate or release for that line.
Releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
See [RELEASING.md](./RELEASING.md) for how these branches are used.
The tip of each branch must be signed. In order for GitHub to sign a
squashed commit that it builds from your pull request, GitHub must know
@@ -145,8 +149,8 @@ tl;dr
In general, pull requests use `develop` as the base branch.
The exceptions are fixes, improvements, and hotfixes for an existing release,
which use that release's branch (e.g. `release/3.2.x`) as the base.
The exceptions are fixes for an existing release line,
which use that line's staging branch (e.g. `staging/3.4.x`) as the base.
If your changes are not quite ready, but you want to make it easily available
for preliminary examination or review, you can create a "Draft" pull request.
@@ -591,15 +595,16 @@ the suggested commit message, or modify it as needed.
#### Slightly more complicated pull requests
Some pull requests need to be pushed to `develop` as more than one
commit. A PR author may _request_ to merge as separate commits. They
Some pull requests need to be pushed to their base branch (usually `develop`)
as more than one commit.
A PR author may _request_ to merge as separate commits. They
must _justify_ why separate commits are needed, and _specify_ how they
would like the commits to be merged. If you disagree with the author,
discuss it with them directly.
If the process is reasonable, follow it. The simplest option is to do a
fast forward only merge (`--ff-only`) on the command line and push to
`develop`.
fast forward only merge (`--ff-only`) on the command line
and push to the base branch.
Some examples of when separate commits are worthwhile are:
@@ -612,9 +617,10 @@ Some examples of when separate commits are worthwhile are:
Either way, check that:
- The commits are based on the current tip of `develop`.
- The commits are clean: No merge commits (except when reverse
merging), no "[FOLD]" or "fixup!" messages.
- The commits are based on the current tip of the base branch.
- The commits are clean:
No merge commits (except when merging a release, see [RELEASING.md](./RELEASING.md)),
no "[FOLD]" or "fixup!" messages.
- All commits are signed. If the commits are not signed by the author, use
`git commit --amend -S` to sign them yourself.
- At least one (but preferably all) of the commits has the PR number
@@ -626,578 +632,8 @@ use them!**
### Releases
All releases, including release candidates and betas, are handled
differently from typical PRs. Most importantly, never use
the Github UI to merge a release.
Xrpld uses a linear workflow model that can be summarized as:
1. In between releases, developers work against the `develop` branch.
2. Periodically, a maintainer will build and tag a beta version from
`develop`, which is pushed to `release`.
- Betas are usually released every two to three weeks, though that
schedule can vary depending on progress, availability, and other
factors.
3. When the changes in `develop` are considered stable and mature enough
to be ready to release, a release candidate (RC) is built and tagged
from `develop`, and merged to `release`.
- Further development for that release (primarily fixes) then
continues against `release`, while other development continues on
`develop`. Effectively, `release` is forked from `develop`. Changes
to `release` must be reverse merged to `develop`.
4. When the candidate has passed testing and is ready for release, the
final release is merged to `master`.
5. If any issues are found post-release, a hotfix / point release may be
created, which is merged to `master`, and then reverse merged to
`develop`.
#### Betas, and the first release candidate
##### Preparing the `develop` branch
1. Optimally, the `develop` branch will be ready to go, with all
relevant PRs already merged.
2. If there are any PRs pending, merge them **BEFORE** preparing the beta.
1. If only one or two PRs need to be merged, merge those PRs [as
normal](#when-and-how-to-merge-pull-requests), updating the second
one, and waiting for CI to finish in between.
2. If there are several pending PRs, do not use the Github UI,
because the delays waiting for CI in between each merge will be
unnecessarily onerous. (Incidentally, this process can also be
used to merge if the Github UI has issues.) Merge each PR branch
directly to a `release-next` on your local machine and create a single
PR, then push your branch to `develop`.
1. Squash the changes from each PR, one commit each (unless more
are needed), being sure to sign each commit and update the
commit message to include the PR number. You may be able to use
a fast-forward merge for the first PR.
2. Push your branch.
3. Continue to [Making the release](#making-the-release) to update
the version number, etc.
The workflow may look something like:
```
git fetch --multiple upstreams user1 user2 user3 [...]
git checkout -B release-next --no-track upstream/develop
# Only do an ff-only merge if pr-branch1 is either already
# squashed, or needs to be merged with separate commits,
# and has no merge commits.
# Use -S on the ff-only merge if pr-branch1 isn't signed.
git merge [-S] --ff-only user1/pr-branch1
git merge --squash user2/pr-branch2
git commit -S # Use the commit message provided on the PR
git merge --squash user3/pr-branch3
git commit -S # Use the commit message provided on the PR
[...]
# Make sure the commits look right
git log --show-signature "upstream/develop..HEAD"
git push --set-upstream origin
# Continue to "Making the release" to update the version number, so
# everything can be done in one PR.
```
You can also use the [squash-branches] script.
You may also need to manually close the open PRs after the changes are
merged to `develop`. Be sure to include the commit ID.
##### Making the release
This includes, betas, and the first release candidate (RC).
1. If you didn't create one [preparing the `develop`
branch](#preparing-the-develop-branch), Ensure there is no old
`release-next` branch hanging around. Then make a `release-next`
branch that only changes the version number. e.g.
```
git fetch upstreams
git checkout --no-track -B release-next upstream/develop
v="A.B.C-bD"
build=$( find -name BuildInfo.cpp )
sed 's/\(^.*versionString =\).*$/\1 "'${v}'"/' ${build} > version.cpp && mv -vi version.cpp ${build}
git diff
git add ${build}
git commit -S -m "Set version to ${v}"
# You could use your "origin" repo, but some CI tests work better on upstream.
git push upstream-push
git fetch upstreams
git branch --set-upstream-to=upstream/release-next
```
You can also use the [update-version] script. 2. Create a Pull Request for `release-next` with **`develop`** as
the base branch.
1. Use the title "[TRIVIAL] Set version to X.X.X-bX".
2. Instead of the default description template, use the following:
```
## High Level Overview of Change
This PR only changes the version number. It will be merged as
soon as Github CI actions successfully complete.
```
3. Wait for CI to successfully complete, and get someone to approve
the PR. (It is safe to ignore known CI issues.)
4. Push the updated `develop` branch using your `release-next`
branch. **Do not use the Github UI. It's important to preserve
commit IDs.**
```
git push upstream-push release-next:develop
```
5. In the unlikely event that the push fails because someone has merged
something else in the meantime, rebase your branch onto the updated
`develop` branch, push again, and go back to step 3.
6. Ensure that your PR against `develop` is closed. Github should do it
automatically.
7. Once this is done, forward progress on `develop` can continue
(other PRs may be merged).
8. Now create a Pull Request for `release-next` with **`release`** as
the base branch. Instead of the default template, reuse and update
the message from the previous release. Include the following verbiage
somewhere in the description:
```
The base branch is `release`. [All releases (including
betas)](https://github.com/XRPLF/rippled/blob/develop/CONTRIBUTING.md#before-you-start)
go in `release`. This PR branch will be pushed directly to `release` (not
squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, simply abandon the
release. It's easy to start a new release, it should be easy to
abandon one. **DO NOT REUSE THE VERSION NUMBER.** e.g. If you
abandon 2.4.0-b1, the next attempt will be 2.4.0-b2.
8. Once everything is ready to go, push to `release`.
```
git fetch upstreams
# Just to be safe, do a dry run first:
git push --dry-run upstream-push release-next:release
# If everything looks right, push the branch
git push upstream-push release-next:release
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/release-next, upstream/release,
# upstream/develop) Set version to 2.4.0-b1
# Note that upstream/develop may not be on this commit, but
# upstream/release must be.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
10. Delete the `release-next` branch on the repo. Use the Github UI or:
```
git push --delete upstream-push release-next
```
11. Finally [create a new release on
Github](https://github.com/XRPLF/rippled/releases).
#### Release candidates after the first
Once the first release candidate is [merged into
release](#making-the-release), then `release` and `develop` _are allowed
to diverge_.
If a bug or issue is discovered in a version that has a release
candidate being tested, any fix and new version will need to be applied
against `release`, then reverse-merged to `develop`. This helps keep git
history as linear as possible.
A `release-next` branch will be created from `release`, and any further
work for that release must be based on `release-next`. Specifically,
PRs must use `release-next` as the base, and those PRs will be merged
directly to `release-next` when approved. Changes should be restricted
to bug fixes, but other changes may be necessary from time to time.
1. Open any PRs for the pending release using `release-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`release-next` can be thrown away and recreated if necessary.
2. Once a new release candidate is ready, create a version commit as in
step 1 [above](#making-the-release) on `release-next`. You can use
the [update-version] script for this, too.
3. Jump to step 8 ("Now create a Pull Request for `release-next` with
**`release`** as the base") from the process
[above](#making-the-release) to merge `release-next` into `release`.
##### Follow up: reverse merge
Once the RC is merged and tagged, it needs to be reverse merged into
`develop` as soon as possible.
1. Create a branch, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNNrcN".
E.g. For release A.B.C-rcD, use `mergeABCrcD`.
```
git fetch upstreams
git checkout --no-track -b mergeABCrcD upstream/develop
```
2. Merge `release` into your branch.
```
# I like the "--edit --log --verbose" parameters, but they are
# not required.
git merge upstream/release
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo (or `upstream` if you have permission),
and open a normal PR against `develop`. The "High level overview" can
simply indicate that this is a merge of the RC. The "Context" should
summarize the changes from the RC. Include the following text
prominently:
```
This PR must be merged manually using a push. Do not use the Github UI.
```
5. Depending on the complexity of the changes, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
# OR
git reset --hard upstream/develop
git merge upstream/release
```
7. When the PR is ready, push it to `develop`.
```
git fetch upstreams
# Make sure the commits look right
git log --show-signature "upstream/develop^..HEAD"
git push upstream-push mergeABCrcD:develop
git fetch upstreams
```
Development on `develop` can proceed as normal.
#### Final releases
A final release is any release that is not a beta or RC, such as 2.2.0.
Only code that has already been tested and vetted across all three
platforms should be included in a final release. Most of the time, that
means that the commit immediately preceding the commit setting the
version number will be an RC. Occasionally, there may be last-minute bug
fixes included as well. If so, those bug fixes must have been tested
internally as if they were RCs (at minimum, ensuring unit tests pass,
and the app starts, syncs, and stops cleanly across all three
platforms.)
_If in doubt, make an RC first._
The process for building a final release is very similar to [the process
for building a beta](#making-the-release), except the code will be
moving from `release` to `master` instead of from `develop` to
`release`, and both branches will be pushed at the same time.
1. Ensure there is no old `master-next` branch hanging around.
Then make a `master-next` branch that only changes the version
number. As above, or using the
[update-version] script.
2. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
`release` and `master` (not squashed or rebased, and not using the
GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, close the PR, delete
`master-next`, and move development back to `release`, [issuing
more RCs as necessary](#release-candidates-after-the-first)
8. Once everything is ready to go, push to `release` and `master`.
```
git fetch upstreams
# Just to be safe, do dry runs first:
git push --dry-run upstream-push master-next:release
git push --dry-run upstream-push master-next:master
# If everything looks right, push the branch
git push upstream-push master-next:release
git push upstream-push master-next:master
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/master-next, upstream/master,
# upstream/release) Set version to A.B.0
# Note that both upstream/release and upstream/master must be on this
# commit.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
10. Delete the `master-next` branch on the repo. Use the Github UI or:
```
git push --delete upstream-push master-next
```
11. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
12. Open a PR to update the [API-CHANGELOG](API-CHANGELOG.md) and `API-VERSION-[n].md` with the changes for this release (if any are missing).
13. Finally, [reverse merge the release into `develop`](#follow-up-reverse-merge).
#### Special cases: point releases, hotfixes, etc.
On occasion, a bug or issue is discovered in a version that already
had a final release. Most of the time, development will have started
on the next version, and will usually have changes in `develop`
and often in `release`.
Because git history is kept as linear as possible, any fix and new
version will need to be applied against `master`.
The process for building a hotfix release is very similar to [the
process for building release candidates after the
first](#release-candidates-after-the-first) and [for building a final
release](#final-releases), except the changes will be done against
`master` instead of `release`.
If there is only a single issue for the hotfix, the work can be done in
any branch. When it's ready to merge, jump to step 3 using your branch
instead of `master-next`.
1. Create a `master-next` branch from `master`.
```
git checkout --no-track -b master-next upstream/master
git push upstream-push
git fetch upstreams
```
2. Open any PRs for the pending hotfix using `master-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`master-next` can be thrown away and recreated if necessary.
3. Once the hotfix is ready, create a version commit using the same
steps as above, or use the
[update-version] script.
4. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
`master` (not squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, update `master-next` as
needed, but ensure that the changes are properly squashed, and the
version setting commit remains last
8. Once everything is ready to go, push to `master` **only**.
```
git fetch upstreams
# Just to be safe, do a dry run first:
git push --dry-run upstream-push master-next:master
# If everything looks right, push the branch
git push upstream-push master-next:master
# Check that all of the branches are updated
git fetch upstreams
git log -1 --oneline
# The output should look like:
# 0123456789 (HEAD -> upstream/master-next, upstream/master) Set version
# to 2.4.1
# Note that upstream/master must be on this commit. upstream/release and
# upstream/develop should not.
# Other branches, including some from upstream-push, may also be
# present.
```
9. Tag the release, too.
```
git tag <version number>
git push upstream-push <version number>
```
9. Delete the `master-next` branch on the repo.
```
git push --delete upstream-push master-next
```
10. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
Once the hotfix is released, it needs to be reverse merged into
`develop` as soon as possible. It may also need to be merged into
`release` if a release candidate is under development.
1. Create a branch in your own repo, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNN".
E.g. For release 2.2.3, use `merge223`.
```
git fetch upstreams
git checkout --no-track -b merge223 upstream/develop
```
2. Merge master into your branch.
```
# I like the "--edit --log --verbose" parameters, but they are
# not required.
git merge upstream/master
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo, and open a normal PR against
`develop`. The "High level overview" can simply indicate that this
is a merge of the hotfix version. The "Context" should summarize
the changes from the hotfix. Include the following text
prominently:
```
This PR must be merged manually using a --ff-only merge. Do not use the Github UI.
```
5. Depending on the complexity of the hotfix, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
# OR
git reset --hard upstream/develop
git merge upstream/master
```
7. When the PR is ready, push it to `develop`.
```
git fetch upstreams
# Make sure the commits look right
git log --show-signature "upstream/develop..HEAD"
git push upstream-push HEAD:develop
```
Development on `develop` can proceed as normal. It is recommended to
create a beta (or RC) immediately to ensure that everything worked as
expected.
##### An even rarer scenario: A hotfix on an old release
Historically, once a final release is tagged and packages are released,
versions older than the latest final release are no longer supported.
However, there is a possibility that a very high severity bug may occur
in a non-amendment blocked version that is still being run by
a significant fraction of users, which would necessitate a hotfix / point
release to that version as well as any later versions.
This scenario would follow the same basic procedure as above,
except that _none_ of `develop`, `release`, or `master`
would be touched during the release process.
In this example, consider if version 2.1.1 needed to be patched.
1. Create two branches in the main (`upstream`) repo.
```
git fetch upstreams
# Create a base branch off the tag
git checkout --no-track -b master-2.1.2 2.1.1
git push upstream-push
# Create a working branch
git checkout --no-track -b master212-next master-2.1.2
git push upstream-push
git fetch upstreams
```
2. Work continues as above, except using `master-2.1.2`as
the base branch for any merging, packaging, etc.
3. After the release is tagged and packages are built, you could
potentially delete both branches, e.g. `master-2.1.2` and
`master212-next`. However, it may be useful to keep `master-2.1.2`
around indefinitely for reference.
4. Assuming that a hotfix is also released for the latest
version in parallel with this one, or if the issue is
already fixed in the latest version, do no do any
reverse merges. However, if it is not, it probably makes
sense to reverse merge `master-2.1.2` into `master`,
release a hotfix for _that_ version, then reverse merge
from `master` to `develop`. (Please don't do this unless absolutely
necessary.)
Releases, release branches, and merging releases back into `develop`
are described in [RELEASING.md](./RELEASING.md).
[contrib]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
[squash]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits
@@ -1205,5 +641,3 @@ git fetch upstreams
[xrpld]: https://github.com/XRPLF/rippled
[signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification
[setup-upstreams]: ./bin/git/setup-upstreams.sh
[squash-branches]: ./bin/git/squash-branches.sh
[update-version]: ./bin/git/update-version.sh

152
RELEASING.md Normal file
View File

@@ -0,0 +1,152 @@
# Branching and Release Management
This document describes how we branch, release, and merge releases back into `develop`.
It does not define version naming
(e.g., what constitutes a major-minor, patch, or beta release).
Examples use `X.Y` for a release line:
`X` and `Y` are placeholders, while the trailing `x` is literal,
e.g., `release/X.Y.x` is `release/3.4.x` for the `3.4` line.
## Branches
| Branch | Purpose |
| :-------------- | :---------------------------------------------------------------------------------------- |
| `develop` | Main development branch. Betas and the first RC of a major-minor release are tagged here. |
| `staging/X.Y.x` | Where fixes for the `X.Y` line are developed and RCs are prepared. |
| `release/X.Y.x` | The last public RC or release of the `X.Y` line. |
A release line is named `X.Y.x`
because one branch serves every patch release of that line (`X.Y.0`, `X.Y.1`, `X.Y.2`, ...).
The `/` groups branches hierarchically,
so tools can filter them and protection rules can target `release/*` and `staging/*`.
## Principles
- **Releases are merged back into `develop`, never cherry-picked or rebased onto it.**
Cherry-picked and rebased commits get new hashes,
so Git can't tell that `develop` already has them.
Future merges then replay them and produce artificial conflicts,
and it's hard to verify that every fix actually reached `develop`.
Merging keeps a single history:
Git knows exactly which release commits `develop` contains,
and no fix is left behind.
- **Branches only move forward.**
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
- **Cherry-picking only goes from `develop` to a staging branch**,
for fixes that must get into a release after the code freeze
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
- **Security fixes are prepared privately and published with the release that contains them**,
so vulnerabilities are not disclosed prematurely.
## Release Lifecycle
This diagram shows a major-minor release and its first patch release.
The staging and release branches are drawn as one line.
```text
develop staging/X.Y.x & release/X.Y.x
│
├── Tag: X.Y.0-b1
├── Tag: X.Y.0-bN
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
│ │
│ (development continues) ├── Fixes
│ ├── Tag: X.Y.0-rcN
│ ├── Tag: X.Y.0 (final)
◀──── (merge) ────────────────────┤
│ ├── Fixes
│ ├── Tag: X.Y.1-rcN
│ ├── Tag: X.Y.1 (final)
◀──── (merge) ────────────────────┤
│
▼
```
### Betas, First RC & Branching
> [!NOTE]
> This phase applies only to a new major-minor release (e.g., `X.Y.0`).
> Patch releases work on the existing branches of the line.
1. **Betas:** All beta versions (e.g., `X.Y.0-b1`) are built and tagged directly on `develop`.
2. **First RC:** We release the first RC (`X.Y.0-rc1`)
once everything that should be included in the release has been merged.
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
at the commit tagged `X.Y.0-rc1`.
The first RC also kicks off the QE process.
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
into `staging/X.Y.x` or `release/X.Y.x`.
Only critical stabilization fixes go into the line.
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
are not merged into `develop`,
so that fixes on the line and the merge back don't run into conflicts.
### Release Candidates
1. Fixes are developed against `staging/X.Y.x`.
2. When ready, a new RC is created on `staging/X.Y.x`,
and `release/X.Y.x` is fast-forwarded to it.
RCs that contain unpublished security fixes are not published,
and don't touch the public branches.
RCs are not merged back into `develop`:
`X.Y.0-rc1` is tagged on `develop` itself,
and all later changes reach `develop` with the [final release](#final-release).
### Final Release
Security fixes become public as soon as they reach the public repo,
so these steps happen only once the release is ready to be published,
one right after the other.
1. Unpublished security fixes, if any, are merged into `staging/X.Y.x`.
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
and the release is tagged on it.
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
For a major-minor release, this lifts the freeze on large changes in `develop`.
### Merging Back Into `develop`
The merge back is a regular PR into `develop`
whose branch contains a real merge commit of the release tag:
1. Create a branch from `develop`, run `git merge --no-ff <tag>`, and resolve any conflicts.
2. Once the PR is approved, `develop` is fast-forwarded to the PR branch,
so the merge commit lands as it is.
Never squash or rebase it.
Never add it to the merge queue either: the queue squashes PRs, which would drop the merge commit.
3. If `develop` moves while the PR is open, redo the merge on top of the new `develop`.
After the merge, `git log develop..<tag>` must be empty.
## Special Cases
### Emergency Fixes From `develop`
If a commit was merged to `develop`
and needs to be included in a release after the code freeze:
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
2. Leave `develop` as it is, with no reverts.
3. Follow the [release candidates](#release-candidates) process as usual.
When the release is later merged back into `develop`,
both sides already contain the same change,
so the merge usually resolves it cleanly or with a trivial conflict.
From Git's perspective, the cherry-picked commit then becomes part of `develop` too.
### Several Supported Lines
When a fix must ship in more than one supported line (e.g., `X.Y` and `X.(Y+1)`),
the lines are merged upwards rather than cherry-picked between:
1. The fix goes into the oldest line first.
2. After that line's release,
its `release/X.Y.x` is merged into the staging branch of the next newer line,
and so on up to the newest line.
3. The newest line is merged back into `develop` as usual.
This way every newer line, and eventually `develop`, contains the history of the older lines.

View File

@@ -24,9 +24,13 @@
# `g++-15`, ...) are probed under both names: a suffixed name can break while
# the plain one still works (see mkVersionedToolLinks in nix/packages.nix).
#
# Tools scoped to a single dev shell rather than to commonPackages are checked
# only in that shell, keyed off XRPL_DEVSHELL.
#
# Environment variables:
# CI if set, skip the tools above when on macOS.
# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check.
# XRPL_DEVSHELL active dev shell; selects shell-specific tools.
set -uo pipefail
@@ -163,6 +167,14 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
check rustfmt
fi
# Lean4 is in the formal-verification shell only, not in commonPackages.
if [ "${XRPL_DEVSHELL:-}" = "formal-verification" ]; then
echo
echo "Formal verification toolchain:"
check lean
check lake
fi
# GCC is the default compiler on Linux. macOS uses the system Apple Clang
# instead, so GCC/g++/gcov are not expected there.
if [ "${os}" = "linux" ]; then

View File

@@ -31,7 +31,7 @@ namespace xrpl::ledger_entries {
// builder's STObject and the wrapper's SLE.
TEST(${name}Tests, BuilderSettersRoundTrip)
{
UInt256 const index{1u};
constexpr UInt256 index{1};
% for field in fields:
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
@@ -85,7 +85,7 @@ TEST(${name}Tests, BuilderSettersRoundTrip)
// from that SLE, build a new wrapper, and verify all fields (and validate()).
TEST(${name}Tests, BuilderFromSleRoundTrip)
{
UInt256 const index{2u};
constexpr UInt256 index{2};
% for field in fields:
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
@@ -146,7 +146,7 @@ TEST(${name}Tests, BuilderFromSleRoundTrip)
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(${name}Tests, WrapperThrowsOnWrongEntryType)
{
UInt256 const index{3u};
constexpr UInt256 index{3};
// Build a valid ledger entry of a different type
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
@@ -177,7 +177,7 @@ TEST(${name}Tests, WrapperThrowsOnWrongEntryType)
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(${name}Tests, BuilderThrowsOnWrongEntryType)
{
UInt256 const index{4u};
constexpr UInt256 index{4};
// Build a valid ledger entry of a different type
% if wrong_le_include == "Ticket":
@@ -207,7 +207,7 @@ TEST(${name}Tests, BuilderThrowsOnWrongEntryType)
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(${name}Tests, OptionalFieldsReturnNullopt)
{
UInt256 const index{3u};
constexpr UInt256 index{3};
% for field in required_fields:
auto const ${field["paramName"]}Value = ${canonical_expr(field)};

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1781173989,
"narHash": "sha256-fnzKKPvS+oieI/pTzotA5tkoM47EB1NpaBcgk4R97hE=",
"lastModified": 1791130267,
"narHash": "sha256-1sjwcQMcgAbmiip/VivBSiK8p2bTkQGOvCuO7vnwfx4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8c91a71d13451abc40eb9dae8910f972f979852f",
"rev": "9013764fcc0ea99fa16cf7aa7decf8a5c3889dd2",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
]
},
"locked": {
"lastModified": 1784611586,
"narHash": "sha256-OfqgY+0hp/zseZB7uyH0U8kIDPS4scZZCyAurEplvG0=",
"lastModified": 1791189933,
"narHash": "sha256-Y8eN7ki0Urx4Ojf4w1xSpWg8uk/3lJqK+E0Oth77rH0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "14f58845249f3552a89b07772626b8d3c632fa86",
"rev": "e60029353d0c48d216bc4b065168ccd4079c166f",
"type": "github"
},
"original": {

View File

@@ -1,29 +1,31 @@
#pragma once
#include <xrpl/beast/type_name.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/core/type_name.hpp>
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl {
/**
* Manages all counted object types.
*
* Counters register themselves on a lock-free intrusive list maintained
* by this object when constructed. Because counters are never destroyed
* or removed, the ABA problem does not apply.
*
* The registry is iterable as a forward range.
*/
class CountedObjects
{
public:
static CountedObjects&
getInstance() noexcept;
using Entry = std::pair<std::string, int>;
using List = std::vector<Entry>;
[[nodiscard]] List
getCounts(int minimumThreshold) const;
public:
/**
* Implementation for @ref CountedObject.
@@ -33,68 +35,147 @@ public:
class Counter
{
public:
Counter(std::string name) noexcept : name_(std::move(name)), count_(0)
{
// Insert ourselves at the front of the lock-free linked list
CountedObjects& instance = CountedObjects::getInstance();
Counter* head = nullptr;
Counter(std::string name) noexcept;
do
{
head = instance.head_.load();
next_ = head;
} while (instance.head_.exchange(this) != head);
// Counters are intrusive list nodes whose addresses are published
// in the registry; they must never be copied or moved. The atomic
// members already force this, but we make it explicit.
Counter(Counter const&) = delete;
Counter&
operator=(Counter const&) = delete;
Counter(Counter&&) = delete;
Counter&
operator=(Counter&&) = delete;
++instance.count_;
}
~Counter() noexcept = default;
int
std::uint32_t
increment() noexcept
{
return ++count_;
auto const newCount = count_.fetch_add(1, std::memory_order::relaxed) + 1;
XRPL_ASSERT(newCount != 0, "xrpl::CountedObjects::Counter::increment : no overflow");
auto maxCount = maxCount_.load(std::memory_order::relaxed);
while (newCount > maxCount &&
!maxCount_.compare_exchange_weak(maxCount, newCount, std::memory_order::relaxed))
{
}
return newCount;
}
int
std::uint32_t
decrement() noexcept
{
return --count_;
auto const prev = count_.fetch_sub(1, std::memory_order::relaxed);
XRPL_ASSERT(prev != 0, "xrpl::CountedObjects::Counter::decrement : no underflow");
return prev - 1;
}
[[nodiscard]] int
getCount() const noexcept
[[nodiscard]] std::uint32_t
count() const noexcept
{
return count_.load();
return count_.load(std::memory_order::relaxed);
}
[[nodiscard]] Counter*
getNext() const noexcept
[[nodiscard]] std::uint32_t
max() const noexcept
{
return next_;
return std::max(
count_.load(std::memory_order::relaxed),
maxCount_.load(std::memory_order::relaxed));
}
[[nodiscard]] std::string const&
getName() const noexcept
name() const noexcept
{
return name_;
}
private:
friend class CountedObjects;
Counter* next_ = nullptr;
std::atomic<std::uint32_t> count_ = 0;
std::atomic<std::uint32_t> maxCount_ = 0;
std::string const name_;
std::atomic<int> count_;
Counter* next_;
};
private:
CountedObjects() noexcept;
~CountedObjects() noexcept = default;
class Iterator
{
public:
using value_type = Counter const;
using reference = value_type&;
using pointer = value_type*;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
explicit Iterator(Counter* c = nullptr) noexcept : current_(c)
{
}
reference
operator*() const noexcept
{
return *current_;
}
pointer
operator->() const noexcept
{
return current_;
}
Iterator&
operator++() noexcept
{
current_ = current_->next_;
return *this;
}
Iterator
operator++(int) noexcept
{
auto tmp = *this;
++*this;
return tmp;
}
bool
operator==(Iterator const&) const noexcept = default;
private:
Counter* current_;
};
constexpr CountedObjects() noexcept = default;
[[nodiscard]] auto
begin() const noexcept
{
return Iterator{head_.load(std::memory_order::acquire)};
}
[[nodiscard]] auto
end() const noexcept
{
return Iterator{};
}
private:
std::atomic<int> count_;
std::atomic<Counter*> head_;
std::atomic<Counter*> head_ = nullptr;
};
/** The global counted object registry. */
inline constinit CountedObjects gCountedObjects;
inline CountedObjects::Counter::Counter(std::string name) noexcept
: next_(gCountedObjects.head_.load(std::memory_order::relaxed)), name_(std::move(name))
{
while (!gCountedObjects.head_.compare_exchange_weak(
next_, this, std::memory_order::release, std::memory_order::relaxed))
;
}
//------------------------------------------------------------------------------
/**
@@ -103,27 +184,37 @@ private:
* Derived classes have their instances counted automatically. This is used
* for reporting purposes.
*
* The constructors are private and `Object` is befriended so that the
* CRTP parameter must be the deriving class itself: a copy-paste error
* like `class B : public CountedObject<A>` fails to compile instead of
* silently polluting A's count.
*
* @note This class has no move operations by design: a derived class's
* move constructor falls back to the copy constructor for this
* base, so the newly created instance is counted. This keeps the
* invariant that count is the number of outstanding subobjects.
*
* @warning Counted objects constructed during dynamic initialization of
* other translation units may have their increments discarded when
* counter itself is dynamically initialized. Do not create counted
* objects before main() begins.
*
* @ingroup basics
*/
template <class Object>
requires std::is_class_v<Object>
class CountedObject
{
private:
static auto&
getCounter() noexcept
{
static CountedObjects::Counter kC{beast::typeName<Object>()};
return kC;
}
static inline CountedObjects::Counter counter{boost::core::type_name<Object>()};
CountedObject() noexcept
{
getCounter().increment();
counter.increment();
}
CountedObject(CountedObject const&) noexcept
{
getCounter().increment();
counter.increment();
}
CountedObject&
@@ -132,7 +223,7 @@ private:
public:
~CountedObject() noexcept
{
getCounter().decrement();
counter.decrement();
}
friend Object;

View File

@@ -110,10 +110,10 @@ static_assert(
*
* However, it does not have sufficient precision to represent the full integer
* range of int64_t values (-2^63 to 2^63-1), which are needed for XRP and MPT
* values. The implementation of SingleAssetVault, and LendingProtocol need to
* represent those integer values accurately and precisely, both for the
* STNumber field type, and for internal calculations. That necessitated the
* "large" scale.
* values. The implementation of SingleAssetVault, LendingProtocol, and
* MPTokensV2 need to represent those integer values accurately and precisely,
* both for the STNumber field type, and for internal calculations. That
* necessitated the "large" scale.
*
* The "Large" scales are intended to represent all values that can be represented
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
@@ -134,8 +134,8 @@ struct MantissaRange final
// NOLINTBEGIN(readability-enum-initial-value)
// The values don't matter, except for Large
enum class MantissaScale {
// Small can be removed when either featureSingleAssetVault or featureLendingProtocol are
// retired
// Small can be removed when any of featureSingleAssetVault, featureLendingProtocol, or
// featureMPTokensV2 are retired
Small,
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
LargeLegacy,
@@ -311,10 +311,10 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
*
* The mantissa range may be changed at runtime via setMantissaScale(). The
* default mantissa range is "large". The range is updated whenever transaction
* processing begins, based on whether SingleAssetVault or LendingProtocol are
* enabled. If either is enabled, the mantissa range is set to "large". If not,
* it is set to "small", preserving backward compatibility and correct
* "amendment-gating".
* processing begins, based on whether SingleAssetVault, LendingProtocol, or
* MPTokensV2 are enabled. If any is enabled, the mantissa range is set to
* "large". If not, it is set to "small", preserving backward compatibility and
* correct "amendment-gating".
*
* It is extremely unlikely that any more calls to setMantissaScale() will be
* needed outside of unit tests.
@@ -344,8 +344,8 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
* set/getMantissaScale() functions may be most appropriate. However, if the
* test has anything to do with transaction processing, it should enable or
* disable the amendments that control the mantissa range choice
* (SingleAssetVault and LendingProtocol), and/or check if either of those
* amendments are enabled to determine which result to expect.
* (SingleAssetVault, LendingProtocol, and MPTokensV2), and/or check if any of
* those amendments are enabled to determine which result to expect.
*/
class Number final
{

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
#pragma once
#include <xrpl/basics/sanitizers.h>
#include <xrpl/beast/type_name.h>
#include <boost/core/type_name.hpp>
#include <exception>
#include <string>
@@ -55,7 +56,7 @@ Throw(Args&&... args)
std::is_convertible_v<E*, std::exception*>, "Exception must derive from std::exception.");
E e(std::forward<Args>(args)...);
logThrow(std::string("Throwing exception of type " + beast::typeName<E>() + ": ") + e.what());
logThrow("Throwing exception of type " + boost::core::type_name<E>() + ": " + e.what());
throw std::move(e);
}

View File

@@ -0,0 +1,200 @@
#pragma once
#include <concepts>
#include <type_traits>
#include <utility>
namespace xrpl {
/**
* Opt-in bitwise operators for scoped enumerations.
*
* Provides `&`, `|`, `^`, `~` and the corresponding compound assignment forms
* for any scoped enumeration with an unsigned underlying type that opts in by
* specializing @ref OptIn:
*
* @code
* namespace xrpl {
*
* enum class MyFlags : std::uint32_t { a = 1, b = 2, c = 4 };
*
* template <>
* struct enum_bitops::OptIn<MyFlags> : std::true_type {};
*
* } // namespace xrpl
* @endcode
*
* The operators are constexpr and noexcept, and return the enumeration type.
*
* @par Where the specialization may be declared
* In namespace xrpl, or at global scope with full qualification, and after
* the enumeration is defined but before the operators are first used. This
* cannot be done in a nested namespace or at class scope; for enumerations
* nested in a class this means after the class definition.
*
* @par Where the operators are found
* The operators are declared in namespace xrpl, so argument-dependent lookup
* will find them only for enumerations declared in xrpl or nested in a class
* declared directly in xrpl. Enumerations in a nested namespace will only be
* found from code inside xrpl, and only if no enclosing scope declares an
* operator of the same name; elsewhere they require using-declarations.
*/
namespace enum_bitops {
/**
* Types that may be opted in to the bitwise operators.
*
* Satisfied by a scoped enumeration whose underlying type is an unsigned
* integer type.
*
* @tparam T The type to test.
*/
template <typename T>
concept Eligible = std::is_scoped_enum_v<T> && std::unsigned_integral<std::underlying_type_t<T>>;
/**
* Opt-in switch for the bitwise operators.
*
* The primary template derives from std::false_type. Specialize it to derive
* from std::true_type to enable the operators for an @ref Eligible enumeration.
*
* @tparam T The scoped enumeration to opt in.
*/
template <Eligible T>
struct OptIn : std::false_type
{
};
/**
* Enumerations for which the bitwise operators are enabled.
*
* Satisfied when @p T satisfies @ref Eligible and @ref OptIn has been
* specialized for it to derive from std::true_type.
*
* @tparam T The type to test.
*/
template <typename T>
concept Candidate = Eligible<T> && OptIn<T>::value;
} // namespace enum_bitops
// These are declared in xrpl, not in enum_bitops, so that argument-dependent
// lookup finds them for enumerations whose associated namespace is xrpl.
/**
* @name Bitwise operators for opted-in scoped enumerations
*
* Each operator applies the corresponding built-in operator to the
* underlying values and converts the result back to the enumeration type.
* Both operands must have the same enumeration type; there is no implicit
* conversion to or from the underlying type.
*/
/** @{ */
/**
* Bitwise AND.
*
* @param lhs The left operand.
* @param rhs The right operand.
* @return The bits set in both @p lhs and @p rhs.
*/
template <enum_bitops::Candidate T>
constexpr T
operator&(T lhs, T rhs) noexcept
{
return static_cast<T>(std::to_underlying(lhs) & std::to_underlying(rhs));
}
/**
* Bitwise OR.
*
* @param lhs The left operand.
* @param rhs The right operand.
* @return The bits set in @p lhs, in @p rhs, or in both.
*/
template <enum_bitops::Candidate T>
constexpr T
operator|(T lhs, T rhs) noexcept
{
return static_cast<T>(std::to_underlying(lhs) | std::to_underlying(rhs));
}
/**
* Bitwise exclusive OR.
*
* @param lhs The left operand.
* @param rhs The right operand.
* @return The bits set in exactly one of @p lhs and @p rhs.
*/
template <enum_bitops::Candidate T>
constexpr T
operator^(T lhs, T rhs) noexcept
{
return static_cast<T>(std::to_underlying(lhs) ^ std::to_underlying(rhs));
}
/**
* Bitwise complement.
*
* The result has every bit of the underlying type that is clear in
* @p val, including bits that no enumerator names. It is intended for
* clearing flags, as in `flags & ~flag`.
*
* @param val The operand.
* @return The complement of @p val.
*/
template <enum_bitops::Candidate T>
constexpr T
operator~(T val) noexcept
{
return static_cast<T>(~std::to_underlying(val));
}
/**
* Bitwise AND assignment.
*
* @param lhs The value to modify.
* @param rhs The right operand.
* @return A reference to @p lhs.
*/
template <enum_bitops::Candidate T>
constexpr T&
operator&=(T& lhs, T rhs) noexcept
{
lhs = lhs & rhs;
return lhs;
}
/**
* Bitwise OR assignment.
*
* @param lhs The value to modify.
* @param rhs The right operand.
* @return A reference to @p lhs.
*/
template <enum_bitops::Candidate T>
constexpr T&
operator|=(T& lhs, T rhs) noexcept
{
lhs = lhs | rhs;
return lhs;
}
/**
* Bitwise exclusive OR assignment.
*
* @param lhs The value to modify.
* @param rhs The right operand.
* @return A reference to @p lhs.
*/
template <enum_bitops::Candidate T>
constexpr T&
operator^=(T& lhs, T rhs) noexcept
{
lhs = lhs ^ rhs;
return lhs;
}
/** @} */
} // namespace xrpl

View File

@@ -2,107 +2,212 @@
#include <xrpl/beast/utility/instrumentation.h> // IWYU pragma: keep
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <type_traits>
#include <utility>
namespace xrpl {
// safe_cast adds compile-time checks to a static_cast to ensure that
// the destination can hold all values of the source. This is particularly
// handy when the source or destination is an enumeration type.
/** Every value of @p Src can be represented by @p Dest.
template <class Src, class Dest>
concept SafeToCast = (std::is_integral_v<Src> && std::is_integral_v<Dest>) &&
(std::is_signed_v<Src> || std::is_unsigned_v<Dest>) &&
(std::is_signed_v<Src> != std::is_signed_v<Dest> ? sizeof(Dest) > sizeof(Src)
: sizeof(Dest) >= sizeof(Src));
Given two integral types, the cast is safe when the destination can
hold every possible value of the source.
template <class Dest, class Src>
Comparing the bounds requires care: we use @c std::cmp_less_equal and
@c std::cmp_greater_equal; the plain relational operators would apply
arithmetic conversions, resulting in incorrect results when comparing
across signedness.
Because @c std::cmp_* requires standard signed or unsigned integer
arguments, which excludes character types and bool, we first widen
all type bounds to the maximum-width integer type while preserving
signedness.
@note Extended integer types, like __int128 on gcc, cannot be safely
widened to a standard integer type, so the concept will reject
them.
*/
template <typename Src, typename Dest>
concept SafeToCast = std::is_integral_v<Src> && std::is_integral_v<Dest> && []() consteval {
using WideSrc = std::conditional_t<std::is_signed_v<Src>, std::intmax_t, std::uintmax_t>;
using WideDest = std::conditional_t<std::is_signed_v<Dest>, std::intmax_t, std::uintmax_t>;
// Note: this guard must be an evaluated branch, and not a
// static_assert. The lambda body is outside the immediate
// context, so a substitution-time failure here would be a
// hard error rather than leaving the concept unsatisfied.
if constexpr (sizeof(Src) > sizeof(WideSrc) || sizeof(Dest) > sizeof(WideDest))
{
return false;
}
else
{
return std::cmp_less_equal(
static_cast<WideDest>(std::numeric_limits<Dest>::min()),
static_cast<WideSrc>(std::numeric_limits<Src>::min())) &&
std::cmp_greater_equal(
static_cast<WideDest>(std::numeric_limits<Dest>::max()),
static_cast<WideSrc>(std::numeric_limits<Src>::max()));
}
}();
/** Compile-time-checked static_cast that rejects non-value preserving casts.
@note There is deliberately no enum-to-enum overload, and no overload
returning the underlying type of an enum. For the latter, use
@c std::to_underlying.
*/
/** @{ */
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
safeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
{
static_assert(
std::is_signed_v<Dest> || std::is_unsigned_v<Src>, "Cannot cast signed to unsigned");
constexpr unsigned kNotSame = std::is_signed_v<Dest> != std::is_signed_v<Src>;
static_assert(
sizeof(Dest) >= sizeof(Src) + kNotSame,
"Destination is too small to hold all values of source");
SafeToCast<Src, Dest>, "This cast is not value-preserving. Please use unsafeCast instead.");
return static_cast<Dest>(s);
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
safeCast(Src s) noexcept
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
{
return static_cast<Dest>(safeCast<std::underlying_type_t<Dest>>(s));
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
constexpr Dest
safeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
{
return safeCast<Dest>(static_cast<std::underlying_type_t<Src>>(s));
return safeCast<Dest>(std::to_underlying(s));
}
/** @} */
// unsafe_cast explicitly flags a static_cast as not necessarily able to hold
// all values of the source. It includes a compile-time check so that if
// underlying types become safe, it can be converted to a safe_cast.
/** Integral-to-integral cast that is known to be narrowing or sign-erasing.
template <class Dest, class Src>
This explicitly flags a conversion that can lose information for some
values of the source type, where the call site accepts that loss (or
truncation is the intended behavior).
The compile-time check ensures the cast remains "unsafe": if the types
involved later change such that the conversion becomes inherently
value-preserving, the static assertion fires with instructions to
migrate the call site to @ref safeCast.
If the conversion's safety depends on a runtime precondition rather
than on the types, or varies across instantiations of generic code,
use @ref checkedCast instead.
*/
/** @{ */
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
unsafeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
{
static_assert(
!SafeToCast<Src, Dest>,
"Only unsafe if casting signed to unsigned or "
"destination is too small");
!SafeToCast<Src, Dest>, "This cast is value-preserving. Please use safeCast instead.");
return static_cast<Dest>(s);
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
unsafeCast(Src s) noexcept
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
{
return static_cast<Dest>(unsafeCast<std::underlying_type_t<Dest>>(s));
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
constexpr Dest
unsafeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
{
return unsafeCast<Dest>(static_cast<std::underlying_type_t<Src>>(s));
return unsafeCast<Dest>(std::to_underlying(s));
}
/** @} */
/** Integral-to-integral cast when the caller has performed a bounds check.
This documents that a runtime precondition or external invariant, which
is not necessarily visible to the compiler, guarantees that the requested
conversion is value-preserving for the values that can actually occur.
This is primarily meant for generic code, where the same expression may
be value-preserving for one instantiation but not for another, making
both @ref safeCast and @ref unsafeCast unusable.
Unlike @ref safeCast and @ref unsafeCast, this imposes no static check
on the type relationship: the caller's claim is about runtime values,
not about types.
*/
/** @{ */
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
checkedCast(Src s) noexcept
{
return static_cast<Dest>(s);
}
template <typename Dest, typename Src>
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
checkedCast(Src s) noexcept
{
return static_cast<Dest>(checkedCast<std::underlying_type_t<Dest>>(s));
}
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
constexpr Dest
checkedCast(Src s) noexcept
{
return checkedCast<Dest>(std::to_underlying(s));
}
/** @} */
/** Downcast within a class hierarchy, verified in debug builds.
Performs a static_cast down a hierarchy, but in debug builds verifies
via dynamic_cast that the object's dynamic type actually permits the
downcast. Both build modes execute the same conversion; debug builds
merely add the check.
The pointer form passes null through unchanged, as dynamic_cast does.
@note The check requires @p Src to be polymorphic; in release builds
an invalid downcast is undefined behavior on use, exactly as
with a bare static_cast.
*/
/** @{ */
template <class Dest, class Src>
requires std::is_pointer_v<Dest>
requires(
std::is_pointer_v<Dest> && std::is_polymorphic_v<Src> &&
std::derived_from<std::remove_pointer_t<Dest>, Src> &&
(std::is_const_v<std::remove_pointer_t<Dest>> || !std::is_const_v<Src>) &&
(std::is_volatile_v<std::remove_pointer_t<Dest>> || !std::is_volatile_v<Src>))
inline Dest
safeDowncast(Src* s) noexcept
{
#ifdef NDEBUG
XRPL_ASSERT(
s == nullptr || dynamic_cast<Dest>(s) != nullptr, "xrpl::safeDowncast : valid downcast");
return static_cast<Dest>(s); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
#else
auto* result = dynamic_cast<Dest>(s);
XRPL_ASSERT(result != nullptr, "xrpl::safeDowncast : pointer downcast is valid");
return result;
#endif
}
template <class Dest, class Src>
requires std::is_lvalue_reference_v<Dest>
requires(
std::is_lvalue_reference_v<Dest> && std::is_polymorphic_v<Src> &&
std::derived_from<std::remove_reference_t<Dest>, Src>)
inline Dest
safeDowncast(Src& s) noexcept
{
#ifndef NDEBUG
XRPL_ASSERT(
dynamic_cast<std::add_pointer_t<std::remove_reference_t<Dest>>>(&s) != nullptr,
"xrpl::safeDowncast : reference downcast is valid");
#endif
return static_cast<Dest>(s); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
return *safeDowncast<std::add_pointer_t<std::remove_reference_t<Dest>>>(std::addressof(s));
}
/** @} */
} // namespace xrpl

View File

@@ -4,88 +4,215 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/predef/architecture.h>
#include <atomic>
#include <concepts>
#include <limits>
#include <type_traits>
#ifndef __aarch64__
#if BOOST_ARCH_X86
#include <immintrin.h>
#endif
namespace xrpl {
/** An unsigned integral type suitable for use as a spinlock.
The type must be always lock-free when wrapped in std::atomic, so
that lock operations cannot themselves take a (library-level) lock.
*/
template <typename T>
concept SpinlockValueType = std::is_unsigned_v<T> && std::atomic<T>::is_always_lock_free;
/** A spinlock value type that additionally supports the atomic bitwise
operations required to pack multiple locks into a single integer.
*/
template <typename T>
concept PackedSpinlockValueType = SpinlockValueType<T> && requires(std::atomic<T>& a, T v) {
{ a.fetch_or(v) } -> std::same_as<T>;
{ a.fetch_and(v) } -> std::same_as<T>;
};
namespace detail {
/**
* Inform the processor that we are in a tight spin-wait loop.
*
* Spinlocks caught in tight loops can result in the processor's pipeline
* filling up with comparison operations, resulting in a misprediction at
* the time the lock is finally acquired, necessitating pipeline flushing
* which is ridiculously expensive and results in very high latency.
*
* This function instructs the processor to "pause" for some architecture
* specific amount of time, to prevent this.
/** Inform the processor that we are in a tight spin-wait loop.
Spinlocks caught in tight loops can result in the processor's pipeline
filling up with comparison operations, resulting in a misprediction at
the time the lock is finally acquired, necessitating pipeline flushing
which is ridiculously expensive and results in very high latency.
This function instructs the processor to "pause" for some architecture
specific amount of time, to prevent this.
*/
inline void
spinPause() noexcept
{
#ifdef __aarch64__
asm volatile("yield");
#else
#if BOOST_ARCH_X86
_mm_pause();
#elif BOOST_ARCH_ARM
asm volatile("yield" ::: "memory");
#else
#error No implementation available for spinPause to use
#endif
}
} // namespace detail
/** @{ */
/**
* Classes to handle arrays of spinlocks packed into a single atomic integer:
*
* Packed spinlocks allow for tremendously space-efficient lock-sharding
* but they come at a cost.
*
* First, the implementation is necessarily low-level and uses advanced
* features like memory ordering and highly platform-specific tricks to
* maximize performance. This imposes a significant and ongoing cost to
* developers.
*
* Second, and perhaps most important, is that the packing of multiple
* locks into a single integer which, albeit space-efficient, also has
* performance implications stemming from data dependencies, increased
* cache-coherency traffic between processors and heavier loads on the
* processor's load/store units.
*
* To be sure, these locks can have advantages but they are definitely
* not general purpose locks and should not be thought of or used that
* way. The use cases for them are likely few and far between; without
* a compelling reason to use them, backed by profiling data, it might
* be best to use one of the standard locking primitives instead. Note
* that in most common platforms, `std::mutex` is so heavily optimized
* that it can, usually, outperform spinlocks.
*
* @tparam T An unsigned integral type (e.g. std::uint16_t)
*/
//------------------------------------------------------------------------------
/**
* A class that grabs a single packed spinlock from an atomic integer.
*
* This class meets the requirements of Lockable:
* https://en.cppreference.com/w/cpp/named_req/Lockable
/** Attempt to acquire a spinlock without blocking.
@note This interface is primarily intended for one-shot attempts to
acquire the lock. Avoid calling this function directly from a
loop and use @ref spinLock instead.
@tparam T An unsigned integral type.
@param lock The atomic variable used as the lock.
@return true if the lock was acquired, false if it was already held.
*/
template <class T>
template <SpinlockValueType T>
[[nodiscard]] bool
spinTryLock(std::atomic<T>& lock) noexcept
{
// A compare-exchange is required here, not an unconditional exchange:
// a failed attempt must not modify the lock word, in case the atomic
// is shared with PackedSpinlock).
T expected = 0;
return lock.compare_exchange_strong(
expected,
std::numeric_limits<T>::max(),
std::memory_order::acquire,
std::memory_order::relaxed);
;
}
/** Acquire a spinlock, blocking until available.
Uses a TTAS (test-and-test-and-set) pattern, so waiters share the cache
line read-only, helping to avoid unnecessary coherency traffic.
@tparam T An unsigned integral type.
@param lock The atomic variable used as the lock.
*/
template <SpinlockValueType T>
void
spinLock(std::atomic<T>& lock) noexcept
{
do
{
// Relaxed ordering is sufficient for the spin: this load is only
// a filter. The acquire on the successful exchange in spinTryLock
// is what synchronizes the critical section.
while (lock.load(std::memory_order::relaxed) != 0)
detail::spinPause();
} while (!spinTryLock(lock));
}
/** Release a spinlock.
@tparam T An unsigned integral type.
@param lock The atomic variable used as the lock.
*/
template <SpinlockValueType T>
void
spinUnlock(std::atomic<T>& lock) noexcept
{
lock.store(0, std::memory_order::release);
}
//------------------------------------------------------------------------------
/** A Lockable interface to a spinlock implemented on top of an atomic.
@tparam T An unsigned integral type.
@note Using `PackedSpinlock` and `Spinlock` against the same underlying
atomic integer is possible but can result in `Spinlock` not being
able to acquire the lock during periods of high contention due to
the way the two locks operate: `Spinlock` spins and tries to grab
all the bits at once, whereas any given `PackedSpinlock` instance
only tries to grab one bit at a time. Caveat emptor.
This class meets the requirements of Lockable:
https://en.cppreference.com/w/cpp/named_req/Lockable
*/
template <SpinlockValueType T>
class Spinlock
{
std::atomic<T>& lock_;
public:
Spinlock(Spinlock const&) = delete;
Spinlock&
operator=(Spinlock const&) = delete;
/** Construct a spinlock handle.
@param lock The atomic integer to spin against.
@note For performance reasons, you should strive to have `lock` be
on a cacheline by itself.
*/
explicit Spinlock(std::atomic<T>& lock) noexcept : lock_(lock)
{
}
[[nodiscard]] bool
try_lock() noexcept // NOLINT(readability-identifier-naming)
{
return spinTryLock(lock_);
}
void
lock() noexcept
{
spinLock(lock_);
}
void
unlock() noexcept
{
spinUnlock(lock_);
}
};
//------------------------------------------------------------------------------
/** A Lockable interface to a packed spinlock implemented on top of an atomic.
Packed spinlocks offer tremendous space-efficient lock-sharding but
they come at a cost.
First, the implementation is necessarily low-level and uses advanced
features like memory ordering and highly platform-specific tricks to
maximize performance. This imposes a significant and ongoing cost to
developers.
Second, and perhaps most important, is that the packing of multiple
locks into a single integer which, albeit space-efficient, also has
performance implications stemming from data dependencies, increased
cache-coherency traffic between processors and heavier loads on the
processor's load/store units.
To be sure, these locks can have advantages but they are definitely
not general purpose locks and should not be thought of or used that
way. The use cases for them are likely few and far between; without
a compelling reason to use them, backed by profiling data, it might
be best to use one of the standard locking primitives instead. Note
that in most common platforms, `std::mutex` is so heavily optimized
that it can, usually, outperform spinlocks.
@tparam T An unsigned integral type (e.g. std::uint16_t)
This class meets the requirements of Lockable:
https://en.cppreference.com/w/cpp/named_req/Lockable
*/
template <PackedSpinlockValueType T>
class PackedSpinlock
{
// clang-format off
static_assert(std::is_unsigned_v<T>);
static_assert(std::atomic<T>::is_always_lock_free);
static_assert(
std::is_same_v<decltype(std::declval<std::atomic<T>&>().fetch_or(0)), T> &&
std::is_same_v<decltype(std::declval<std::atomic<T>&>().fetch_and(0)), T>,
"std::atomic<T>::fetch_and(T) and std::atomic<T>::fetch_and(T) are required by packed_spinlock");
// clang-format on
private:
std::atomic<T>& bits_;
T const mask_;
@@ -94,120 +221,49 @@ public:
PackedSpinlock&
operator=(PackedSpinlock const&) = delete;
/**
* A single spinlock packed inside the specified atomic
*
* @param lock The atomic integer inside which the spinlock is packed.
* @param index The index of the spinlock this object acquires.
*
* @note For performance reasons, you should strive to have `lock` be
* on a cacheline by itself.
/** Construct a packed spinlock handle for a single bit.
@param lock The atomic integer inside which the spinlock is packed.
@param index The index of the spinlock this object acquires.
@note For performance reasons, you should strive to have `lock` be
on a cacheline by itself.
*/
PackedSpinlock(std::atomic<T>& lock, int index) : bits_(lock), mask_(static_cast<T>(1) << index)
{
XRPL_ASSERT(
index >= 0 && (mask_ != 0),
"xrpl::PackedSpinlock::PackedSpinlock : valid index and mask");
}
[[nodiscard]] bool
try_lock() // NOLINT(readability-identifier-naming)
{
return (bits_.fetch_or(mask_, std::memory_order_acquire) & mask_) == 0;
}
void
lock()
{
while (!try_lock())
{
// The use of relaxed memory ordering here is intentional and
// serves to help reduce cache coherency traffic during times
// of contention by avoiding writes that would definitely not
// result in the lock being acquired.
while ((bits_.load(std::memory_order_relaxed) & mask_) != 0)
detail::spinPause();
}
}
void
unlock()
{
bits_.fetch_and(~mask_, std::memory_order_release);
}
};
/**
* A spinlock implemented on top of an atomic integer.
*
* @note Using `packed_spinlock` and `spinlock` against the same underlying
* atomic integer can result in `spinlock` not being able to actually
* acquire the lock during periods of high contention, because of how
* the two locks operate: `spinlock` will spin trying to grab all the
* bits at once, whereas any given `packed_spinlock` will only try to
* grab one bit at a time. Caveat emptor.
*
* This class meets the requirements of Lockable:
* https://en.cppreference.com/w/cpp/named_req/Lockable
*/
template <class T>
class Spinlock
{
static_assert(std::is_unsigned_v<T>);
static_assert(std::atomic<T>::is_always_lock_free);
private:
std::atomic<T>& lock_;
public:
Spinlock(Spinlock const&) = delete;
Spinlock&
operator=(Spinlock const&) = delete;
/**
* Grabs the
*
* @param lock The atomic integer to spin against.
*
* @note For performance reasons, you should strive to have `lock` be
* on a cacheline by itself.
*/
Spinlock(std::atomic<T>& lock) : lock_(lock)
PackedSpinlock(std::atomic<T>& lock, int index) noexcept
: bits_(lock), mask_([index]() {
XRPL_ASSERT(
index >= 0 && index < std::numeric_limits<T>::digits,
"xrpl::PackedSpinlock::PackedSpinlock : valid index");
return static_cast<T>(T{1} << index);
}())
{
}
[[nodiscard]] bool
try_lock() // NOLINT(readability-identifier-naming)
try_lock() noexcept // NOLINT(readability-identifier-naming)
{
T expected = 0;
return lock_.compare_exchange_weak(
expected,
std::numeric_limits<T>::max(),
std::memory_order_acquire,
std::memory_order_relaxed);
return (bits_.fetch_or(mask_, std::memory_order::acquire) & mask_) == 0;
}
void
lock()
lock() noexcept
{
while (!try_lock())
do
{
// The use of relaxed memory ordering here is intentional and
// serves to help reduce cache coherency traffic during times
// of contention by avoiding writes that would definitely not
// result in the lock being acquired.
while (lock_.load(std::memory_order_relaxed) != 0)
// of contention by avoiding writes that are unlikely to grab
// the requested lock.
while ((bits_.load(std::memory_order::relaxed) & mask_) != 0)
detail::spinPause();
}
} while (!try_lock());
}
void
unlock()
unlock() noexcept
{
lock_.store(0, std::memory_order_release);
bits_.fetch_and(~mask_, std::memory_order::release);
}
};
/** @} */
} // namespace xrpl

View File

@@ -1,47 +0,0 @@
#pragma once
#include <cstdlib>
#include <string>
#include <type_traits>
#include <typeinfo>
#ifndef _MSC_VER
#include <cxxabi.h>
#endif
namespace beast {
template <typename T>
std::string
typeName()
{
using TR = std::remove_reference_t<T>;
std::string name = typeid(TR).name();
#ifndef _MSC_VER
if (auto s = abi::__cxa_demangle(name.c_str(), nullptr, nullptr, nullptr))
{
name = s;
// NOLINTNEXTLINE(cppcoreguidelines-no-malloc)
std::free(s);
}
#endif
if (std::is_const_v<TR>)
name += " const";
if (std::is_volatile_v<TR>)
name += " volatile";
if (std::is_lvalue_reference_v<T>)
{
name += "&";
}
else if (std::is_rvalue_reference_v<T>)
{
name += "&&";
}
return name;
}
} // namespace beast

View File

@@ -2,26 +2,24 @@
#pragma once
#include <compare>
#include <concepts>
namespace beast {
/**
* Zero allows classes to offer efficient comparisons to zero.
*
* Zero is a struct to allow classes to efficiently compare with zero without
* requiring an rvalue construction.
*
* It's often the case that we have classes which combine a number and a unit.
* In such cases, comparisons like t > 0 or t != 0 make sense, but comparisons
* like t > 1 or t != 1 do not.
* like t > 1 or t != 1 do not. Comparing against kZero expresses exactly that,
* without constructing a T.
*
* The class Zero allows such comparisons to be easily made.
*
* The comparing class T either needs to have a method called signum() which
* returns a positive number, 0, or a negative; or there needs to be a signum
* function which resolves in the namespace which takes an instance of T and
* returns a positive, zero or negative number.
* A type T participates if either `t.signum()` or an unqualified `signum(t)`
* found by argument-dependent lookup returns an integer that is negative,
* zero, or positive according to the sign of t. Both `t == kZero` and
* `kZero == t` work, as do all six relational operators in either order.
*/
struct Zero
{
explicit Zero() = default;
@@ -30,115 +28,44 @@ struct Zero
inline constexpr Zero kZero{};
/**
* Default implementation of signum calls the method on the class.
* Default implementation of signum: call the member function.
*/
template <typename T>
auto
signum(T const& t)
template <class T>
requires requires(T const& t) {
{ t.signum() } -> std::integral;
}
[[nodiscard]] constexpr auto
signum(T const& t) noexcept(noexcept(t.signum()))
{
return t.signum();
}
namespace detail::zero_helper {
namespace detail {
// For argument dependent lookup to function properly, calls to signum must
// be made from a namespace that does not include overloads of the function..
/**
* A type with a usable signum: either the member-based default above, or a
* `signum(t)` overload in T's own namespace, found by ADL. A user overload
* that is a better match than the template wins, as usual.
*/
template <class T>
auto
callSignum(T const& t)
concept HasSignum = requires(T const& t) {
{ signum(t) } -> std::integral;
};
} // namespace detail
template <detail::HasSignum T>
[[nodiscard]] constexpr bool
operator==(T const& t, Zero) noexcept(noexcept(signum(t)))
{
return signum(t);
return signum(t) == 0;
}
} // namespace detail::zero_helper
// Handle operators where T is on the left side using signum.
template <typename T>
bool
operator==(T const& t, Zero)
template <detail::HasSignum T>
[[nodiscard]] constexpr std::strong_ordering
operator<=>(T const& t, Zero) noexcept(noexcept(signum(t)))
{
return detail::zero_helper::callSignum(t) == 0;
}
template <typename T>
bool
operator!=(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) != 0;
}
template <typename T>
bool
operator<(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) < 0;
}
template <typename T>
bool
operator>(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) > 0;
}
template <typename T>
bool
operator>=(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) >= 0;
}
template <typename T>
bool
operator<=(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) <= 0;
}
// Handle operators where T is on the right side by
// reversing the operation, so that T is on the left side.
template <typename T>
bool
operator==(Zero, T const& t)
{
return t == kZero;
}
template <typename T>
bool
operator!=(Zero, T const& t)
{
return t != kZero;
}
template <typename T>
bool
operator<(Zero, T const& t)
{
return t > kZero;
}
template <typename T>
bool
operator>(Zero, T const& t)
{
return t < kZero;
}
template <typename T>
bool
operator>=(Zero, T const& t)
{
return t <= kZero;
}
template <typename T>
bool
operator<=(Zero, T const& t)
{
return t >= kZero;
return signum(t) <=> 0;
}
} // namespace beast

View File

@@ -1,7 +1,7 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/basics/enum_bitops.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/OwnerCounts.h>
#include <xrpl/ledger/ReadView.h>
@@ -22,80 +22,39 @@
namespace xrpl {
// Bitwise flag enum with existing operator overloads
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum ApplyFlags : std::uint32_t {
TapNone = 0x00,
enum class ApplyFlags : std::uint32_t {
None = 0x00,
// This is a local transaction with the
// fail_hard flag set.
TapFailHard = 0x10,
FailHard = 0x10,
// This is not the transaction's last pass
// Transaction can be retried, soft failures allowed
TapRetry = 0x20,
Retry = 0x20,
// Transaction came from a privileged source
TapUnlimited = 0x400,
Unlimited = 0x400,
// Transaction is executing as part of a batch
TapBatch = 0x800,
Batch = 0x800,
// Transaction shouldn't be applied
// Signatures shouldn't be checked
TapDryRun = 0x1000,
DryRun = 0x1000,
// Transaction is being preflighted as the payload of a
// TransactionProposalCreate. Its signatures are collected on-ledger
// afterward, so signature-presence checks (e.g. Batch signer matching)
// are skipped at proposal-creation time (On-Chain Cosigner spec
// §5.3.1.2).
TapProposal = 0x2000
Proposal = 0x2000
};
constexpr ApplyFlags
operator|(ApplyFlags const& lhs, ApplyFlags const& rhs)
template <>
struct enum_bitops::OptIn<ApplyFlags> : std::true_type
{
return safeCast<ApplyFlags>(
safeCast<std::underlying_type_t<ApplyFlags>>(lhs) |
safeCast<std::underlying_type_t<ApplyFlags>>(rhs));
}
static_assert((TapFailHard | TapRetry) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
static_assert((TapRetry | TapFailHard) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
constexpr ApplyFlags
operator&(ApplyFlags const& lhs, ApplyFlags const& rhs)
{
return safeCast<ApplyFlags>(
safeCast<std::underlying_type_t<ApplyFlags>>(lhs) &
safeCast<std::underlying_type_t<ApplyFlags>>(rhs));
}
static_assert((TapFailHard & TapRetry) == TapNone, "ApplyFlags operator &");
static_assert((TapRetry & TapFailHard) == TapNone, "ApplyFlags operator &");
constexpr ApplyFlags
operator~(ApplyFlags const& flags)
{
return safeCast<ApplyFlags>(~safeCast<std::underlying_type_t<ApplyFlags>>(flags));
}
static_assert(~TapRetry == safeCast<ApplyFlags>(0xFFFFFFDFu), "ApplyFlags operator ~");
inline ApplyFlags
operator|=(ApplyFlags& lhs, ApplyFlags const& rhs)
{
lhs = lhs | rhs;
return lhs;
}
inline ApplyFlags
operator&=(ApplyFlags& lhs, ApplyFlags const& rhs)
{
lhs = lhs & rhs;
return lhs;
}
};
//------------------------------------------------------------------------------

View File

@@ -1,11 +1,17 @@
#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/SField.h>
#include <xrpl/protocol/STVector256.h>
#include <cstddef>
#include <optional>
namespace xrpl {
@@ -25,6 +31,31 @@ public:
: Base(keylet::skip(), view, j)
{
}
/**
* Looks up a hash `diff` slots back from the most recent entry in the
* sfHashes vector (diff == 0 is the most recent entry).
*
* Callers decide which skip list entry to read and how to translate a
* target ledger sequence into `diff`; this only does the bounds check
* and vector indexing shared by both the recent (stride 1) and distant
* (stride 256) skip lists.
*
* @param diff how many slots back from the most recent hash to look up;
* 0 is the most recent hash.
* @return the hash at that slot, or std::nullopt if the entry does not
* exist or `diff` is out of range.
*/
[[nodiscard]] std::optional<UInt256>
hashAt(std::size_t diff) const
{
if (!this->exists())
return std::nullopt;
STVector256 const vec = (*this)->getFieldV256(sfHashes);
if (vec.size() > diff)
return vec[vec.size() - diff - 1];
return std::nullopt;
}
};
using LedgerHashesEntryR = LedgerHashesEntry<ReadView>;

View File

@@ -1,8 +1,10 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/nodestore/NodeObject.h>
#include <memory>
#include <optional>
namespace xrpl::node_store {
@@ -23,7 +25,7 @@ public:
/**
* Construct the decoded blob from raw data.
*/
DecodedBlob(void const* key, void const* value, int valueBytes);
DecodedBlob(std::optional<uint256> key, void const* value, int valueBytes);
/**
* Determine if the decoding was successful.
@@ -43,10 +45,10 @@ public:
private:
bool success_{false};
void const* key_;
uint256 key_;
NodeObjectType objectType_{NodeObjectType::Unknown};
unsigned char const* objectData_{nullptr};
int dataBytes_;
int dataBytes_ = 0;
};
} // namespace xrpl::node_store

View File

@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/nodestore/NodeObject.h>
@@ -37,11 +38,6 @@ namespace xrpl::node_store {
class EncodedBlob
{
/**
* The 32-byte key of the serialized object.
*/
std::array<std::uint8_t, 32> key_{};
/**
* A pre-allocated buffer for the serialized object.
*
@@ -65,6 +61,11 @@ class EncodedBlob
*/
std::uint8_t* const ptr_;
/**
* The 32-byte key of the serialized object.
*/
uint256 key_;
public:
explicit EncodedBlob(std::shared_ptr<NodeObject> const& obj)
: size_([&obj]() {
@@ -76,11 +77,12 @@ public:
return obj->getData().size() + 9;
}())
, ptr_((size_ <= payload_.size()) ? payload_.data() : new std::uint8_t[size_])
, key_(obj->getHash())
{
std::fill_n(ptr_, 8, std::uint8_t{0});
ptr_[8] = static_cast<std::uint8_t>(obj->getType());
std::copy_n(obj->getData().data(), obj->getData().size(), ptr_ + 9);
std::copy_n(obj->getHash().data(), obj->getHash().size(), key_.data());
}
~EncodedBlob()
@@ -95,10 +97,10 @@ public:
delete[] ptr_;
}
[[nodiscard]] void const*
[[nodiscard]] uint256 const&
getKey() const noexcept
{
return static_cast<void const*>(key_.data());
return key_;
}
[[nodiscard]] std::size_t

View File

@@ -61,14 +61,22 @@ parseBase58(std::string const& s);
/**
* A special account that's used as the "issuer" for XRP.
*/
AccountID const&
xrpAccount();
constexpr inline AccountID const&
xrpAccount() noexcept
{
static constexpr AccountID kAccount(beast::kZero);
return kAccount;
}
/**
* A placeholder for empty accounts.
*/
AccountID const&
noAccount();
constexpr inline AccountID const&
noAccount() noexcept
{
static constexpr AccountID kAccount = xrpAccount().next();
return kAccount;
}
/**
* Convert hex or base58 string to AccountID.
@@ -80,10 +88,10 @@ bool
toIssuer(AccountID&, std::string const&);
// DEPRECATED Should be checking the currency or native flag
inline bool
isXRP(AccountID const& c)
constexpr inline bool
isXRP(AccountID const& c) noexcept
{
return c == beast::kZero;
return c == xrpAccount();
}
// DEPRECATED
@@ -101,22 +109,6 @@ operator<<(std::ostream& os, AccountID const& x)
return os;
}
/**
* Initialize the global cache used to map AccountID to base58 conversions.
*
* The cache is optional and need not be initialized. But because conversion
* is expensive (it requires a SHA-256 operation) in most cases the overhead
* of the cache is worth the benefit.
*
* @param count The number of entries the cache should accommodate. Zero will
* disable the cache, releasing any memory associated with it.
*
* @note The function will only initialize the cache the first time it is
* invoked. Subsequent invocations do nothing.
*/
void
initAccountIdCache(std::size_t count);
} // namespace xrpl
//------------------------------------------------------------------------------

View File

@@ -1,10 +1,10 @@
#pragma once
#include <xrpl/basics/contract.h>
#include <xrpl/beast/type_name.h>
#include <xrpl/protocol/SOTemplate.h>
#include <boost/container/flat_map.hpp>
#include <boost/core/type_name.hpp>
#include <algorithm>
#include <cstddef>
@@ -84,7 +84,7 @@ public:
* Derived classes will load the object with all the known formats.
*/
private:
KnownFormats() : name_(beast::typeName<Derived>())
KnownFormats() : name_(boost::core::type_name<Derived>())
{
}

View File

@@ -33,7 +33,20 @@ public:
STBitString(SField const& n);
STBitString(value_type const& v);
template <typename Tag>
requires(!std::is_void_v<Tag>)
STBitString(BaseUInt<Bits, Tag> const& v) : value_(v)
{
}
STBitString(SField const& n, value_type const& v);
template <typename Tag>
requires(!std::is_void_v<Tag>)
STBitString(SField const& n, BaseUInt<Bits, Tag> const& v) : STBase(n), value_(v)
{
}
STBitString(SerialIter& sit, SField const& name);
[[nodiscard]] SerializedTypeID
@@ -166,7 +179,7 @@ template <typename Tag>
void
STBitString<Bits>::setValue(BaseUInt<Bits, Tag> const& v)
{
value_ = v;
value_ = value_type{v};
}
template <int Bits>

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/enum_bitops.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
@@ -15,6 +16,7 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>
@@ -22,18 +24,8 @@ namespace xrpl {
class STPathElement final : public CountedObject<STPathElement>
{
unsigned int type_;
AccountID accountID_;
PathAsset assetID_;
AccountID issuerID_;
bool isOffer_;
std::size_t hashValue_;
public:
// Bitwise values (typeCurrency | typeMPT)
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Type {
enum class Type : std::uint8_t {
TypeNone = 0x00,
TypeAccount = 0x01, // Rippling through an account (vs taking an offer).
TypeCurrency = 0x10, // Currency follows.
@@ -45,6 +37,8 @@ public:
// Combination of all types.
};
using enum Type;
STPathElement();
STPathElement(STPathElement const&) = default;
STPathElement&
@@ -62,42 +56,42 @@ public:
bool forceAsset = false);
STPathElement(
unsigned int uType,
Type type,
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer);
[[nodiscard]] std::uint32_t
getNodeType() const;
[[nodiscard]] Type
getNodeType() const noexcept;
[[nodiscard]] bool
isOffer() const;
isOffer() const noexcept;
[[nodiscard]] bool
isAccount() const;
isAccount() const noexcept;
[[nodiscard]] bool
hasIssuer() const;
hasIssuer() const noexcept;
[[nodiscard]] bool
hasCurrency() const;
hasCurrency() const noexcept;
[[nodiscard]] bool
hasMPT() const;
hasMPT() const noexcept;
[[nodiscard]] bool
hasAsset() const;
hasAsset() const noexcept;
[[nodiscard]] bool
isNone() const;
isNone() const noexcept;
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
// class of offers.
[[nodiscard]] AccountID const&
getAccountID() const;
getAccountID() const noexcept;
[[nodiscard]] PathAsset const&
getPathAsset() const;
getPathAsset() const noexcept;
[[nodiscard]] Currency const&
getCurrency() const;
@@ -106,17 +100,35 @@ public:
getMPTID() const;
[[nodiscard]] AccountID const&
getIssuerID() const;
getIssuerID() const noexcept;
[[nodiscard]] bool
isType(Type const& pe) const;
isType(Type pe) const noexcept;
bool
operator==(STPathElement const& t) const;
friend bool
operator==(STPathElement const& lhs, STPathElement const& rhs) noexcept
{
return lhs.isType(TypeAccount) == rhs.isType(TypeAccount) &&
lhs.hashValue_ == rhs.hashValue_ && lhs.accountID_ == rhs.accountID_ &&
lhs.assetID_ == rhs.assetID_ && lhs.issuerID_ == rhs.issuerID_;
}
private:
static std::size_t
getHash(STPathElement const& element);
Type type_;
AccountID accountID_;
PathAsset assetID_;
AccountID issuerID_;
bool isOffer_;
std::size_t hashValue_;
};
template <>
struct enum_bitops::OptIn<STPathElement::Type> : std::true_type
{
};
template <class Hasher>
@@ -124,7 +136,7 @@ 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.isType(STPathElement::TypeAccount));
hash_append(h, e.getAccountID());
hash_append(h, e.getPathAsset());
hash_append(h, e.getIssuerID());
@@ -404,11 +416,11 @@ inline STPathElement::STPathElement(
}
inline STPathElement::STPathElement(
unsigned int uType,
Type type,
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer)
: type_(uType)
: type_(type)
, accountID_(account)
, assetID_(asset)
, issuerID_(issuer)
@@ -423,56 +435,56 @@ inline STPathElement::STPathElement(
hashValue_ = getHash(*this);
}
inline std::uint32_t
STPathElement::getNodeType() const
inline STPathElement::Type
STPathElement::getNodeType() const noexcept
{
return type_;
}
inline bool
STPathElement::isOffer() const
STPathElement::isOffer() const noexcept
{
return isOffer_;
}
inline bool
STPathElement::isAccount() const
STPathElement::isAccount() const noexcept
{
return !isOffer();
}
inline bool
STPathElement::isType(Type const& pe) const
STPathElement::isType(Type pe) const noexcept
{
return (type_ & pe) != 0u;
return (type_ & pe) != STPathElement::TypeNone;
}
inline bool
STPathElement::hasIssuer() const
STPathElement::hasIssuer() const noexcept
{
return isType(STPathElement::TypeIssuer);
}
inline bool
STPathElement::hasCurrency() const
STPathElement::hasCurrency() const noexcept
{
return isType(STPathElement::TypeCurrency);
}
inline bool
STPathElement::hasMPT() const
STPathElement::hasMPT() const noexcept
{
return isType(STPathElement::TypeMpt);
}
inline bool
STPathElement::hasAsset() const
STPathElement::hasAsset() const noexcept
{
return isType(STPathElement::TypeAsset);
}
inline bool
STPathElement::isNone() const
STPathElement::isNone() const noexcept
{
return getNodeType() == STPathElement::TypeNone;
}
@@ -480,13 +492,13 @@ STPathElement::isNone() const
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
// class of offers.
inline AccountID const&
STPathElement::getAccountID() const
STPathElement::getAccountID() const noexcept
{
return accountID_;
}
inline PathAsset const&
STPathElement::getPathAsset() const
STPathElement::getPathAsset() const noexcept
{
return assetID_;
}
@@ -504,18 +516,11 @@ STPathElement::getMPTID() const
}
inline AccountID const&
STPathElement::getIssuerID() const
STPathElement::getIssuerID() const noexcept
{
return issuerID_;
}
inline bool
STPathElement::operator==(STPathElement const& t) const
{
return (type_ & TypeAccount) == (t.type_ & TypeAccount) && hashValue_ == t.hashValue_ &&
accountID_ == t.accountID_ && assetID_ == t.assetID_ && issuerID_ == t.issuerID_;
}
// ------------ STPath ------------
inline STPath::STPath(std::vector<STPathElement> p) : path_(std::move(p))

View File

@@ -596,7 +596,7 @@ template <std::size_t Bits, class Tag>
BaseUInt<Bits, Tag>
SerialIter::getBitString()
{
auto const n = Bits / 8;
constexpr auto n = BaseUInt<Bits, Tag>::size();
if (remain_ < n)
Throw<std::runtime_error>("invalid SerialIter getBitString");
@@ -607,7 +607,7 @@ SerialIter::getBitString()
used_ += n;
remain_ -= n;
return BaseUInt<Bits, Tag>::fromVoid(x);
return BaseUInt<Bits, Tag>{std::span<std::uint8_t const, n>{x, n}};
}
} // namespace xrpl

View File

@@ -14,7 +14,7 @@ namespace xrpl {
// Various protocol and system specific constant globals.
/* The name of the system. */
static inline std::string const&
inline std::string const&
systemName()
{
static std::string const kName = "xrpld";
@@ -52,11 +52,10 @@ isLegalAmountSigned(XRPAmount const& amount)
}
/* The currency code for the native currency. */
static inline std::string const&
inline std::string
systemCurrencyCode()
{
static std::string const kCode = "XRP";
return kCode;
return "XRP";
}
/**

View File

@@ -238,6 +238,12 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
MASK_ADJ(0)) \
\
TRANSACTION(ConfidentialMPTHolderKeyUpdate, \
TF_FLAG(tfHolderKeyRotation, 0x00010000) \
TF_FLAG(tfHolderKeyRecovery, 0x00020000) \
TF_FLAG(tfCancelRecovery, 0x00040000), \
MASK_ADJ(0))
// clang-format on

View File

@@ -12,10 +12,12 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <boost/container/flat_set.hpp>
#include <cstdint>
#include <flat_set>
#include <optional>
namespace xrpl {
@@ -115,6 +117,9 @@ public:
parentBatchID_ = id;
}
[[nodiscard]] std::flat_set<MPTID>
getAffectedMPTs() const;
private:
UInt256 transactionID_;
std::uint32_t ledgerSeq_;

View File

@@ -61,26 +61,40 @@ using Domain = BaseUInt<256>;
/**
* XRP currency.
*/
Currency const&
xrpCurrency();
constexpr inline Currency const&
xrpCurrency() noexcept
{
static constexpr Currency const kCurrency(beast::kZero);
return kCurrency;
}
/**
* A placeholder for empty currencies.
*/
Currency const&
noCurrency();
constexpr inline Currency const&
noCurrency() noexcept
{
static constexpr Currency const kCurrency(xrpCurrency().next());
return kCurrency;
}
/**
* We deliberately disallow the currency that looks like "XRP" because too
* many people were using it instead of the correct XRP currency.
*
* Note that this doesn't catch "xRP" or "xrp" or other case variations.
*/
Currency const&
badCurrency();
inline bool
isXRP(Currency const& c)
constexpr inline Currency const&
badCurrency() noexcept
{
return c == beast::kZero;
static constexpr Currency kCurrency{"0000000000000000000000005852500000000000"};
return kCurrency;
}
constexpr inline bool
isXRP(Currency const& c) noexcept
{
return c == xrpCurrency();
}
/**
@@ -98,7 +112,7 @@ to_string(Currency const& c);
* to rewrite some unit test code.
*/
bool
toCurrency(Currency&, std::string const&);
toCurrency(Currency&, std::string_view);
/**
* Tries to convert a string to a Currency, returns noCurrency() on failure.
@@ -107,8 +121,7 @@ toCurrency(Currency&, std::string const&);
* unfortunate; changing this will require very careful checking
* everywhere and may mean having to rewrite some unit test code.
*/
Currency
toCurrency(std::string const&);
Currency toCurrency(std::string_view);
inline std::ostream&
operator<<(std::ostream& os, Currency const& x)

View File

@@ -439,6 +439,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRecoveryKey, SoeOptional},
}))
/** A ledger object which tracks Oracle

View File

@@ -330,6 +330,7 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
TYPED_SFIELD(sfAmountCommitment, VL, 45)
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
TYPED_SFIELD(sfBytecode, VL, 47)
TYPED_SFIELD(sfRecoveryKey, VL, 48)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -1147,12 +1147,27 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
{sfZKProof, SoeRequired},
}))
/** This transaction rotates or recovers a confidential MPT holder's ElGamal encryption key,
or cancels a recovery. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, 93, ConfidentialMPTHolderKeyUpdate,
({.amendment = featureConfidentialMPTKeyRotation}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfHolderEncryptionKey, SoeOptional},
{sfConfidentialBalanceSpending, SoeOptional},
{sfConfidentialBalanceInbox, SoeOptional},
{sfZKProof, SoeOptional},
}))
/** This transaction posts an unsigned transaction on-ledger as a
TransactionProposal, pending multi-signature collection. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
({.amendment = featureCosign}),
({
{sfProposedTransaction, SoeRequired},

View File

@@ -8,6 +8,7 @@
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <type_traits>
namespace xrpl {
@@ -182,7 +183,7 @@ public:
operator result_type() noexcept
{
auto const digest = Sha512Hasher::result_type(h_);
return result_type::fromVoid(digest.data());
return result_type{std::span{digest}.first<result_type::size()>()};
}
private:

View File

@@ -392,6 +392,7 @@ JSS(max_ledger); // in/out: LedgerCleaner
JSS(max_queue_size); // out: TxQ
JSS(max_spend_drops); // out: AccountInfo
JSS(max_spend_drops_total); // out: AccountInfo
JSS(maximum);
JSS(mean); // out: get_aggregate_price
JSS(median); // out: get_aggregate_price
JSS(median_fee); // out: TxQ
@@ -411,6 +412,7 @@ JSS(minimum_level); // out: TxQ
JSS(missingCommand); // error
JSS(mpt_amount); // out: mpt_holders
JSS(mpt_issuance_id); // in: Payment, mpt_holders
JSS(mpt_issuances); // in: Subscribe, Unsubscribe
JSS(mptoken_index); // out: mpt_holders
JSS(mpt_issuance_id_a); // out: BookChanges
JSS(mpt_issuance_id_b); // out: BookChanges

View File

@@ -8,6 +8,7 @@
#include <cstdint>
#include <cstring>
#include <span>
namespace xrpl::nft {
@@ -98,7 +99,8 @@ getTaxon(UInt256 const& id)
inline AccountID
getIssuer(UInt256 const& id)
{
return AccountID::fromVoid(id.data() + 4);
return AccountID{
std::span<unsigned char const, uint256::size()>{id}.subspan<4, AccountID::size()>()};
}
} // namespace xrpl::nft

View File

@@ -2,13 +2,10 @@
#include <xrpl/basics/base_uint.h>
#include <string_view>
namespace xrpl::nft {
// NFT directory pages order their contents based only on the low 96 bits of
// the NFToken value. This mask provides easy access to the necessary mask.
constexpr UInt256 kPageMask(
std::string_view("0000000000000000000000000000000000000000ffffffffffffffffffffffff"));
constexpr UInt256 kPageMask{"0000000000000000000000000000000000000000ffffffffffffffffffffffff"};
} // namespace xrpl::nft

View File

@@ -339,6 +339,30 @@ public:
{
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfRecoveryKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getRecoveryKey() const
{
if (hasRecoveryKey())
return this->sle_->at(sfRecoveryKey);
return std::nullopt;
}
/**
* @brief Check if sfRecoveryKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasRecoveryKey() const
{
return this->sle_->isFieldPresent(sfRecoveryKey);
}
};
/**
@@ -552,6 +576,17 @@ public:
return *this;
}
/**
* @brief Set sfRecoveryKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setRecoveryKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfRecoveryKey] = value;
return *this;
}
/**
* @brief Build and return the completed MPToken wrapper.
* @param index The ledger entry index.

View File

@@ -0,0 +1,279 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTHolderKeyUpdateBuilder;
/**
* @brief Transaction: ConfidentialMPTHolderKeyUpdate
*
* Type: ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureConfidentialMPTKeyRotation
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTHolderKeyUpdateBuilder to construct new transactions.
*/
class ConfidentialMPTHolderKeyUpdate : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE;
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdate transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTHolderKeyUpdate(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdate");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getHolderEncryptionKey() const
{
if (hasHolderEncryptionKey())
{
return this->tx_->at(sfHolderEncryptionKey);
}
return std::nullopt;
}
/**
* @brief Check if sfHolderEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasHolderEncryptionKey() const
{
return this->tx_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfConfidentialBalanceSpending (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceSpending() const
{
if (hasConfidentialBalanceSpending())
{
return this->tx_->at(sfConfidentialBalanceSpending);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceSpending is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceSpending() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceSpending);
}
/**
* @brief Get sfConfidentialBalanceInbox (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceInbox() const
{
if (hasConfidentialBalanceInbox())
{
return this->tx_->at(sfConfidentialBalanceInbox);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceInbox is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceInbox() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceInbox);
}
/**
* @brief Get sfZKProof (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getZKProof() const
{
if (hasZKProof())
{
return this->tx_->at(sfZKProof);
}
return std::nullopt;
}
/**
* @brief Check if sfZKProof is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasZKProof() const
{
return this->tx_->isFieldPresent(sfZKProof);
}
};
/**
* @brief Builder for ConfidentialMPTHolderKeyUpdate transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTHolderKeyUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTHolderKeyUpdateBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTHolderKeyUpdateBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
}
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdateBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTHolderKeyUpdateBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdateBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceSpending] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceInbox] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTHolderKeyUpdate wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTHolderKeyUpdate
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTHolderKeyUpdate{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
/**
* @brief Transaction: TransactionProposalCreate
*
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::NoPriv

View File

@@ -7,6 +7,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/server/Manifest.h>
@@ -26,11 +27,11 @@ namespace xrpl {
/**
* Maximum number of subscriptions a single client connection may hold at once.
*
* Applies to the account, real-time account, and account-history subscriptions
* tracked on one InfoSub (the sets counted by totalSubscriptionCount), bounding
* the disconnect-time cleanup of those sets. Book subscriptions are tracked
* separately (OrderBookDB) and are not counted here. Generous enough for
* legitimate power users such as block explorers.
* Applies to the account, real-time account, account-history, and MPT issuance
* subscriptions tracked on one InfoSub (the sets counted by
* totalSubscriptionCount), bounding the disconnect-time cleanup of those sets.
* Book subscriptions are tracked separately (OrderBookDB) and are not counted
* here. Generous enough for legitimate power users such as block explorers.
*/
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
@@ -151,6 +152,21 @@ public:
AccountID const& account,
bool historyOnly) = 0;
/**
* Remove an MPT issuance subscription during InfoSub teardown.
*
* Removes only the server-side entry from subMPT_. Does NOT touch
* InfoSub::mptSubscriptions_ because the InfoSub is being destroyed.
* Called by ~InfoSub() for each issuance in mptSubscriptions_.
*
* @param uListener The sequence number of the subscriber being torn down.
* @param mptID The MPT issuance to remove.
*
* @note Thread-safety: acquires mptLock_ internally.
*/
virtual void
unsubMPTInternal(std::uint64_t uListener, MPTID const& mptID) = 0;
/**
* Schedule the server-side teardown of a disconnecting connection's
* account subscriptions off the destructor thread.
@@ -270,6 +286,11 @@ public:
virtual bool
unsubConsensus(std::uint64_t uListener) = 0;
virtual void
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
virtual void
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
// VFALCO TODO Remove
// This was added for one particular partner, it
// "pushes" subscription data to a particular URL.
@@ -309,9 +330,9 @@ public:
* Return the number of subscriptions currently tracked on this
* connection.
*
* The combined size of the per-connection account, real-time account, and
* account-history subscription sets. `doSubscribe` reads this to enforce
* the per-connection subscription cap before admitting more.
* The combined size of the per-connection account, real-time account,
* account-history, and MPT issuance subscription sets. `doSubscribe` reads
* this to enforce the per-connection subscription cap before admitting more.
*
* @return The total tracked subscription count for this connection.
*
@@ -342,6 +363,25 @@ public:
HashSet<AccountID> const& normalAccounts,
std::size_t cap);
/**
* Enforce the cap and reserve a request's net-new MPT issuances, atomically.
*
* The MPT analogue of tryReserveAccountSubscriptions: under one hold of
* `lock_`, count the net-new issuances, check the total against @p cap, and
* insert them only if it fits. All-or-nothing, so a rejected request records
* nothing. Doing check and insert together stops two concurrent requests
* sharing an InfoSub (the admin subscribe-by-url path) from both passing the
* check before either records its issuances. The server-side map is
* populated afterwards by subMPT, whose re-insert is a no-op.
*
* @param mptIDs The MPT issuance ids to reserve.
* @param cap The effective per-connection cap.
* @return true if reserved; false if the request must be rejected.
* @note Thread-safe: takes `lock_`.
*/
[[nodiscard]] bool
tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap);
/**
* Whether this connection already tracks an account-history for @p account.
*
@@ -413,12 +453,29 @@ public:
[[nodiscard]] unsigned int
getApiVersion() const noexcept;
void
insertSubMPTInfo(MPTID const& mptID);
void
deleteSubMPTInfo(MPTID const& mptID);
protected:
// Mutable so the read-only totalSubscriptionCount() accessor can lock it
// from a const method; locking semantics are otherwise unchanged.
mutable std::mutex lock_;
private:
// The lock type guarding this instance's subscription sets.
using ScopedLock = std::scoped_lock<decltype(lock_)>;
/**
* The combined tally the per-connection cap is enforced against.
*
* @param lock Proof that `lock_` is held; unused otherwise.
*/
[[nodiscard]] std::size_t
subscriptionCount(ScopedLock const& lock) const;
Consumer consumer_;
Source& source_;
HashSet<AccountID> realTimeSubscriptions_;
@@ -427,6 +484,7 @@ private:
std::uint64_t seq_;
HashSet<AccountID> accountHistorySubscriptions_;
HashSet<Book> bookSubscriptions_;
HashSet<MPTID> mptSubscriptions_;
unsigned int apiVersion_ = 0;
static int

View File

@@ -1,6 +1,5 @@
#pragma once
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/SlabAllocator.h>
#include <xrpl/basics/Slice.h>
@@ -15,12 +14,13 @@
#include <cstdint>
#include <cstring>
#include <memory>
#include <new>
#include <type_traits>
namespace xrpl {
// an item stored in a SHAMap
class SHAMapItem : public CountedObject<SHAMapItem>
class alignas(8) SHAMapItem : public CountedObject<SHAMapItem>
{
// These are used to support boost::intrusive_ptr reference counting
// These functions are used internally by boost::intrusive_ptr to handle
@@ -101,15 +101,15 @@ namespace detail {
// The slab cutoffs and the number of megabytes per allocation are customized
// based on the number of objects of each size we expect to need at any point
// in time and with an eye to minimize the number of slack bytes in a block.
inline SlabAllocatorSet<SHAMapItem> gSlabber({
{ 128, megabytes(std::size_t(60)) },
{ 192, megabytes(std::size_t(46)) },
{ 272, megabytes(std::size_t(60)) },
{ 384, megabytes(std::size_t(56)) },
{ 564, megabytes(std::size_t(40)) },
{ 772, megabytes(std::size_t(46)) },
{ 1052, megabytes(std::size_t(60)) },
});
inline constinit slab::Allocator<SHAMapItem, slab::HeapFallback,
slab::Config<1000000, 128>,
slab::Config<1000000, 296>,
slab::Config<125000, 392>,
slab::Config<125000, 520>,
slab::Config<62500, 760>,
slab::Config<62500, 856>,
slab::Config<31250, 1048>
> gSlabber;
// clang-format on
} // namespace detail
@@ -150,23 +150,13 @@ makeShamapitem(UInt256 const& tag, Slice data)
XRPL_ASSERT(
data.size() <= megabytes<std::size_t>(16), "xrpl::makeShamapitem : maximum input size");
// NOLINTNEXTLINE(misc-const-correctness)
std::uint8_t* raw = detail::gSlabber.allocate(data.size());
// If we can't grab memory from the slab allocators, we fall back to
// the standard library and try to grab a precisely-sized memory block:
if (raw == nullptr)
raw = new std::uint8_t[sizeof(SHAMapItem) + data.size()];
// We do not increment the reference count here on purpose: the
// constructor of SHAMapItem explicitly sets it to 1. We use the fact
// that the refcount can never be zero before incrementing as an
// invariant.
return {new (raw) SHAMapItem{tag, data}, false};
}
// constructor of SHAMapItem explicitly sets it to 1.
if (auto raw = detail::gSlabber.allocate(data.size())) [[likely]]
return {new (raw) SHAMapItem{tag, data}, false};
static_assert(alignof(SHAMapItem) != 40);
static_assert(alignof(SHAMapItem) == 8 || alignof(SHAMapItem) == 4);
Throw<std::bad_alloc>();
}
inline boost::intrusive_ptr<SHAMapItem>
makeShamapitem(SHAMapItem const& other)

View File

@@ -51,7 +51,8 @@ public:
beast::Journal journal = beast::Journal{beast::Journal::getNullSink()})
: ApplyContext(registry, base, std::nullopt, tx, preclaimResult, baseFee, flags, journal)
{
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
}
std::reference_wrapper<ServiceRegistry> registry;

View File

@@ -59,10 +59,11 @@ public:
, parentBatchId(parentBatchId)
, j(j)
{
XRPL_ASSERT((flags & TapBatch) == TapBatch, "Batch apply flag should be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::Batch, "Batch apply flag should be set");
XRPL_ASSERT_IF(
(flags & TapProposal) != TapNone,
(flags & TapDryRun) != TapNone,
(flags & ApplyFlags::Proposal) != ApplyFlags::None,
(flags & ApplyFlags::DryRun) != ApplyFlags::None,
"xrpl::PreflightContext : proposal preflight implies dry run");
}
@@ -74,10 +75,11 @@ public:
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: registry(registry), tx(tx), rules(std::move(rules)), flags(flags), j(j)
{
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
XRPL_ASSERT_IF(
(flags & TapProposal) != TapNone,
(flags & TapDryRun) != TapNone,
(flags & ApplyFlags::Proposal) != ApplyFlags::None,
(flags & ApplyFlags::DryRun) != ApplyFlags::None,
"xrpl::PreflightContext : proposal preflight implies dry run");
}
@@ -116,7 +118,7 @@ public:
, j(j)
{
XRPL_ASSERT(
parentBatchId.has_value() == ((flags & TapBatch) == TapBatch),
parentBatchId.has_value() == ((flags & ApplyFlags::Batch) == ApplyFlags::Batch),
"Parent Batch ID should be set if batch apply flag is set");
}
@@ -129,7 +131,8 @@ public:
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: PreclaimContext(registry, view, preflightResult, tx, flags, std::nullopt, j)
{
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
}
PreclaimContext&

View File

@@ -41,7 +41,7 @@ struct ApplyResult
inline bool
isTecClaimHardFail(TER ter, ApplyFlags flags)
{
return isTecClaim(ter) && ((flags & TapRetry) == 0u);
return isTecClaim(ter) && ((flags & ApplyFlags::Retry) == ApplyFlags::None);
}
/**

View File

@@ -46,6 +46,9 @@ class ValidMPTIssuance
* MPTokens and RippleStates deleted during apply. finalize() checks each
* holder's AccountRoot to detect vault pseudo-account holdings deleted
* outside VaultDelete. All these checks are gated on fixCleanup3_2_0.
*
* Under fixCleanup3_5_0, finalize() also rejects any MPToken erased with
* a non-zero sfMPTAmount.
*/
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
@@ -138,8 +141,9 @@ public:
* - Convert/ConvertBack symmetry:
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
* - Cannot delete MPToken with non-zero confidential state:
* Cannot delete if sfIssuerEncryptedBalance exists
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
* Cannot delete if any of sfConfidentialBalanceSpending, sfConfidentialBalanceInbox,
* sfIssuerEncryptedBalance or sfAuditorEncryptedBalance is present, and the issuance's
* sfConfidentialOutstandingAmount is non-zero. Mirrors MPTokenAuthorize::preclaim.
* - Privacy flag consistency:
* MPToken confidential balance fields can only be created or changed if
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
@@ -162,6 +166,8 @@ class ValidConfidentialMPToken
std::int64_t outstandingDelta = 0;
SLE::const_pointer issuance;
bool deletedWithEncrypted = false;
// True when an erased MPToken had a non-zero pre-tx public balance.
bool deletedWithBalanceBefore = false;
bool badConsistency = false;
bool badCOA = false;
bool changesConfidentialFields = false;

View File

@@ -0,0 +1,84 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
/**
* @brief Allows a confidential MPT holder to rotate or recover their ElGamal
* encryption key, or to cancel a pending recovery.
*
* Submitted by the holder. Exactly one of three modes must be selected via
* the transaction flags:
*
* - Rotation (tfHolderKeyRotation): the holder still has their current
* ElGamal private key. They provide a new public key along with their
* current spending/inbox balances re-encrypted under that new key. The
* holder's encryption key and confidential balances are updated
* immediately.
*
* - Recovery (tfHolderKeyRecovery): the holder has lost their current
* private key. They provide a new public key but cannot provide
* re-encrypted balances. The new key is recorded as a pending
* sfRecoveryKey; the confidential balances are left untouched. Completing
* the recovery (rewriting the balances) is done separately by the issuer
* via ConfidentialMPTRecoverBalance.
*
* - Cancel (tfCancelRecovery): the holder revokes a pending recovery
* authorization created by a prior Recovery-mode transaction. No key,
* balances, or proof are carried; authorization is via the holder's
* ordinary XRPL signature. sfRecoveryKey is removed and everything else
* is left untouched. Fails if no recovery is pending.
*
* @note Zero-knowledge proof verification for Rotation and Recovery is
* deferred to a follow-up once the mpt-crypto constructions are finalized.
*/
class ConfidentialMPTHolderKeyUpdate : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTHolderKeyUpdate(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl

View File

@@ -26,6 +26,10 @@ The store paths carry their derivation hash, so they change whenever a tool is
rebuilt — a `flake.lock` update generally rewrites most of them even when no
version moves. That is deliberate: it makes tooling changes visible in review.
Tools scoped to a single dev shell rather than to `commonPackages` do not appear
here: `check-tools.sh` keys those off `XRPL_DEVSHELL`, and none of the three
environments above is such a shell. Adding to one needs no snapshot update.
## Regenerating
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible

View File

@@ -2,144 +2,144 @@ Detected OS: macos (Darwin arm64)
Core build tools:
✅ cmake
cmake version 4.1.2
/nix/store/gvabsb4yqb5xsqzqph54rijnn4zpihnp-cmake-4.1.2/bin/cmake
cmake version 4.4.3
/nix/store/q85csxf4s4shx89zif097h1ql4ax1rrz-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.28.1
/nix/store/9jiyxmkpwmn6dcqs0765s83riw3l5ail-conan-2.28.1/bin/conan
Conan version 2.32.0
/nix/store/921jqsgbilixmr3xchj95si0jl8pi13z-conan-2.32.0/bin/conan
✅ git
git version 2.54.0
/nix/store/a14yxcqvv9x2l9mllgpirzhvz93pgprg-git-2.54.0/bin/git
git version 2.55.0
/nix/store/gw7c7m0dwca5lg4152x9lpmxpj171bzw-git-2.55.0/bin/git
✅ python3
Python 3.13.13
/nix/store/ygxqin6ydzjfawywqpp5pal8wv6sf5bh-python3-3.13.13/bin/python3.13
Python 3.14.7
/nix/store/0kcws5blwbdyxqxjd1w8hjipz61xny3k-python3-3.14.7/bin/python3.14
Development tooling:
✅ ccache
ccache version 4.13.6
/nix/store/57davyvs6p6dkrl3svzwg1ph18wsy4cz-ccache-4.13.6/bin/ccache
/nix/store/ydr5nlzxp1djb5256y0zzqa87vz55gsk-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.7
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang
clang version 22.1.8
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/rbap7zqq7mw00fyqa02p5rj7gqjp4w5i-clang-22/bin/clang-22
clang version 22.1.8
/nix/store/w826sqb98nnym6kzackg829664iqdlxj-clang-22/bin/clang-22
✅ clang++
clang version 22.1.7
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang++
clang version 22.1.8
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/v9haf787f7bcz0mq1sad4bpyx21pj6li-clang++-22/bin/clang++-22
clang version 22.1.8
/nix/store/wnwyw3yfqrhpxij65bl3pa1x65q2zg3m-clang++-22/bin/clang++-22
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/4l50ds9fa2mkvh7wg8qzrlbmjs12sb8l-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
/nix/store/c7jjnw29ra78jdlzjww2m374izhy05n8-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
✅ curl
curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
/nix/store/kclq0czaxvsgh4ym9ld7b6iwy50l1snk-curl-8.20.0-bin/bin/curl
curl 8.22.0 (aarch64-apple-darwin25.6.0) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
/nix/store/pq1h17abqy6q5y6k5dn9qj0fwb89flba-curl-8.22.0-bin/bin/curl
✅ file
file-5.47
/nix/store/dax63li7wwcbqxxkkgzc4g2rx7d4w86x-file-5.47/bin/file
file-5.48
/nix/store/y44vpziwbzfm8020dzlqqbybyyy7lb6g-file-5.48/bin/file
✅ less
less 692 (PCRE2 regular expressions)
/nix/store/lvr16y75r1pdxpdv0aph5ak2yd0hkvqm-less-692/bin/less
less 710 (PCRE2 regular expressions)
/nix/store/3yn3dx5fwg5i0inqqzwnxq7f6lwhpjqa-less-710/bin/less
✅ make
GNU Make 4.4.1
/nix/store/8wwiw8pwyhrkzyq28hqzxfl4z84lks81-gnumake-4.4.1/bin/make
/nix/store/8l4wgxmhdxfvc9g18y76bnxl1ghgcp7n-gnumake-4.4.1/bin/make
✅ netstat
present
/nix/store/qsd1kzqb0ahrk433vmyl245gp623j19s-network_cmds-730.80.3/bin/netstat
/nix/store/57fqvgdav8lrmzndkwm7qrgksl01yj23-network_cmds-741.100.2/bin/netstat
✅ ninja
1.13.2
/nix/store/bqykhrblarkj4fl0hz2mf8ngwfv6x6bz-ninja-1.13.2/bin/ninja
/nix/store/infm4vlx71q83bqz0k559w1gn4b0rq4i-ninja-1.13.2/bin/ninja
✅ perl
v5.42.0
/nix/store/js13ri9fvm0ajk1fpd3acigys2a9whdv-perl-5.42.0/bin/perl
v5.42.3
/nix/store/7rvr6qjfzk2ybzn1imjw6c6qq1n1a9cr-perl-5.42.3/bin/perl
✅ pkg-config
0.29.2
/nix/store/lzrwr375jqhhbca116kja96xf1md83l8-pkg-config-wrapper-0.29.2/bin/pkg-config
/nix/store/nhgylvj98klqggy54h7gbc3b2ia4a00y-pkg-config-wrapper-0.29.2/bin/pkg-config
✅ vim
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
/nix/store/6vbkykg92w603c0sw3mkk7p7mfaawbns-vim-9.2.0389/bin/vim
/nix/store/akaa9kqm3b8rlkv61kvsqr2l2j70rvxb-vim-9.2.1001/bin/vim
✅ zip
Zip 3.0
/nix/store/z6ph729vcakbvz3wh8ln1wk6mi06w487-zip-3.0/bin/zip
/nix/store/dvawr36npnad4xa6dxi2ss0fw44aa82r-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-apply-replacements
clang-apply-replacements version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.7
/nix/store/m3ii69rca4077lf4wlk7m3jcag1fs577-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 22.1.8
/nix/store/w4bz034k4l0w719rrmbwnqcgyjbswy81-clang-apply-replacements-22/bin/clang-apply-replacements-22
✅ clang-format
clang-format version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-format
clang-format version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-format
✅ clang-format-22
clang-format version 22.1.7
/nix/store/4fawqy6ngqcsqd2ygyyzm93q0xy3f5gs-clang-format-22/bin/clang-format-22
clang-format version 22.1.8
/nix/store/w7z346l0r8y36b6lc5i7jzj3w0581l2y-clang-format-22/bin/clang-format-22
✅ clang-tidy
LLVM version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-tidy
LLVM version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/jqw4280saixaxxihwdba9ldm2fsm6dr3-clang-tidy-22/bin/clang-tidy-22
LLVM version 22.1.8
/nix/store/0lv25qd3ddgjm3ndn70q61jfr6krlkl7-clang-tidy-22/bin/clang-tidy-22
✅ dot
dot - graphviz version 12.2.1 (0)
/nix/store/ijb4fbnqa6wzlpqnhb6q9knqpf7qqn5z-graphviz-12.2.1/bin/dot
dot - graphviz version 15.1.1 (0)
/nix/store/mc99a6bpbk2waym2cnw6ifmxli4ndb4x-graphviz-15.1.1/bin/dot
✅ doxygen
1.16.1
/nix/store/kbryjdpq9jizjb0ws0nzbf2h2ymbdiwm-doxygen-1.16.1/bin/doxygen
1.17.0
/nix/store/kz43yaj0wg6fx6cbrcmi2442z1ramy0c-doxygen-1.17.0/bin/doxygen
✅ gcovr
gcovr 8.4
/nix/store/wn8jiyh9p0bybs96s4163qp3k8vfmczx-python3.13-gcovr-8.4/bin/gcovr
/nix/store/czj1vxsizrbba280gg22cys40qxkqjgr-python3.14-gcovr-8.4/bin/gcovr
✅ gh
gh version 2.94.0 (nixpkgs)
/nix/store/fhnpw0hs0gjms1ha6ap02jq7rx13gkbp-gh-2.94.0/bin/gh
gh version 2.102.0 (2026-09-30)
/nix/store/s8bahl6zssvry93ihivfmk1l6c58byrr-gh-2.102.0/bin/gh
✅ git-cliff
git-cliff 2.13.1
/nix/store/cy0wwhgxa7yvrz97zydbq6sqmixc90fq-git-cliff-2.13.1/bin/git-cliff
git-cliff 2.14.2
/nix/store/k4m5yk0qb7hfslwsq1zjajz1d94j12bj-git-cliff-2.14.2/bin/git-cliff
✅ git-lfs
git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3)
/nix/store/k9r7zjfjplqa4d5s71cqvf2iv73jd9mc-git-lfs-3.7.1/bin/git-lfs
git-lfs/3.8.0 (3.8.0; darwin arm64; go 1.26.8)
/nix/store/wzixjr3yb40a13m2q654hm3bdjn6sal3-git-lfs-3.8.0/bin/git-lfs
✅ gpg
gpg (GnuPG) 2.4.9
/nix/store/cgh6iwzz5jgx9z5whka4vgj210i6npc6-gnupg-2.4.9/bin/gpg
/nix/store/0ldfwnwx385i5kh7cyvxx4mkvgjhs5rh-gnupg-2.4.9/bin/gpg
✅ pre-commit
pre-commit 4.5.1
/nix/store/z3cca68620w0w10f090szgzdnmh1waf2-pre-commit-4.5.1/bin/pre-commit
pre-commit 4.6.2
/nix/store/ivh54xypg9qvd3ha9lis0i8p4nkf8n7a-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/4x28x911z2f9y7adqlh3qspp4a16dig7-run-clang-tidy/bin/run-clang-tidy
/nix/store/3xss1mm3mh1hzcmg182na6ki1p5rnxf1-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/x8iymrh76sk5q91ryg5pa7i32s6gfh34-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/w68r9z07hcq4fwbfyq3yvc7qxm8aqfbl-run-clang-tidy-22/bin/run-clang-tidy-22
Rust toolchain:
✅ cargo
cargo 1.97.1 (c980f4866 2026-06-30)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/cargo
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/cargo
✅ cargo-audit
cargo-audit-audit 0.22.1
/nix/store/snwkga2f5gyf404h7mmp9wriwxb8v65f-cargo-audit-0.22.1/bin/cargo-audit
cargo-audit-audit 0.22.2
/nix/store/7jgrwj557l91940aj0ag4i924ima1cqn-cargo-audit-0.22.2/bin/cargo-audit
✅ cargo-llvm-cov
cargo-llvm-cov 0.8.5
/nix/store/fpiqdh91gwyxalqp409ynm0s0g086w7w-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
cargo-llvm-cov 0.9.0
/nix/store/5q1h285z8zzkw2qxig9a0b1avcsk1cf0-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
✅ cargo-nextest
cargo-nextest 0.9.137
/nix/store/ylz7m947mhkgsp6i7611id3s3gcd58nq-cargo-nextest-0.9.137/bin/cargo-nextest
cargo-nextest 0.9.146
/nix/store/69v3hx3k597hcxd2zy7yad7fg0aw5yxd-cargo-nextest-0.9.146/bin/cargo-nextest
✅ clippy-driver
clippy 0.1.97 (8bab26f4f6 2026-07-14)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/clippy-driver
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/clippy-driver
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f4 2026-07-14)
/nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
/nix/store/30rbyk8gx7v53fxqhs4pp0fvxfjycspr-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly
rustc 1.101.0-nightly (282215592 2026-10-04)
/nix/store/a9552rf3pylcnkijc1z0hf1lcnv50djq-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/rustc
✅ rustfmt
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
/nix/store/5ymwgr9jqjz7zzbmj0j5vqbwcd3kp0vm-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
/nix/store/0jmdipyn8266j9kqx2xxm2qkxlrlrjfc-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).

View File

@@ -21,7 +21,8 @@ let
)
);
toolchain = if pkgs.stdenv.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
toolchain =
if pkgs.stdenv.hostPlatform.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
in
{
default = pkgs.buildEnv {

View File

@@ -20,15 +20,16 @@ let
# Custom-glibc stdenvs, matching the CI environment. darwin has no custom
# glibc, so there they fall back to the plain nixpkgs stdenvs.
customGccStdenv = if pkgs.stdenv.isLinux then linux.gccStdenv else plainGccStdenv;
customClangStdenv = if pkgs.stdenv.isLinux then linux.clangStdenv else plainClangStdenv;
customGccStdenv = if pkgs.stdenv.hostPlatform.isLinux then linux.gccStdenv else plainGccStdenv;
customClangStdenv =
if pkgs.stdenv.hostPlatform.isLinux then linux.clangStdenv else plainClangStdenv;
# gcov matching each gcc shell, so `-Dcoverage=ON` builds work in the shell.
plainGcov = mkGcov {
name = "plain";
cc = gccPackage.cc;
};
customGccGcov = if pkgs.stdenv.isLinux then linux.gcov else plainGcov;
customGccGcov = if pkgs.stdenv.hostPlatform.isLinux then linux.gcov else plainGcov;
# Whole directory: init.sh locates the profiles relative to itself.
conanDir = ../conan;
@@ -51,7 +52,7 @@ let
# Not sdkEnv: a shell's stdenv already sets that up. Prepended so the stub
# beats the nixpkgs libresolv this shell's tooling drags in.
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.isDarwin (
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isDarwin (
pkgs.lib.concatLines (
pkgs.lib.mapAttrsToList (
name: value: ''export ${name}="${value} ''${${name}:-}"''
@@ -126,7 +127,7 @@ let
in
rec {
# macOS: Nix Clang. Linux: Nix GCC.
default = if pkgs.stdenv.isDarwin then clang else gcc;
default = if pkgs.stdenv.hostPlatform.isDarwin then clang else gcc;
# gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no
# custom glibc, so they fall back to the plain nixpkgs toolchain.
@@ -147,6 +148,19 @@ rec {
versionedTools = clangVersionedTools;
};
# The gcc shell plus the Lean4 formal verification toolchain
formal-verification = makeShell {
shellName = "formal-verification";
stdenv = customGccStdenv;
compilerName = "gcc";
version = gccVersion;
versionedTools = gccVersionedTools;
extraPackages = [
customGccGcov
pkgs.lean4
];
};
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
no-compiler = makeShell {
shellName = "no-compiler";
@@ -158,7 +172,7 @@ rec {
# The *-plain shells (stock nixpkgs toolchain) exist only on Linux: on darwin
# gcc/clang are already plain, so these would be redundant and are omitted, which
# makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc.
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
// pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
gcc-plain = makeShell {
shellName = "gcc-plain";
stdenv = plainGccStdenv;

View File

@@ -217,12 +217,13 @@ The `release-info` action decides the channel from the event, and
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` |
| push to `develop` | `0.0.0-dev+<hash>` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
| tag, non-public codebase | _any but `X.Y.Z`_ | `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 picks a release channel. Versions sort in row order, so moving to a
Only a tag picks a release channel. A final release, `X.Y.Z`, goes to `stable`
even from a non-public codebase. Versions sort in row order, so moving to a
more mature channel never downgrades. A tag matching none of the release
patterns, such as `X.Y.Z-hotfix1`, publishes to `custom`, which sits outside
that order.
@@ -284,8 +285,9 @@ checks that the server starts in each image. A tag's images are tagged with the
tag name, a develop image as `develop`. With `publish: true`:
- `xrpld` is pushed to `xrplf/xrpld` on Docker Hub using the `DOCKERHUB_TOKEN`
secret, an organization access token for `xrplf`. Private builds are never
pushed there.
secret, an organization access token for `xrplf`. Builds of a non-public
codebase are pushed there only for `stable` releases, whose packages are
public too.
- `voidstar` replaces `/usr/bin/xrpld` with the binary of the `voidstar` build
config, adds `libvoidstar.so` and links the binary into `/symbols`, as
Antithesis expects. It is pushed as `xrpld-voidstar` to the Antithesis

View File

@@ -1,39 +0,0 @@
#include <xrpl/basics/CountedObject.h>
#include <algorithm>
namespace xrpl {
CountedObjects&
CountedObjects::getInstance() noexcept
{
static CountedObjects kInstance;
return kInstance;
}
CountedObjects::CountedObjects() noexcept : count_(0), head_(nullptr)
{
}
CountedObjects::List
CountedObjects::getCounts(int minimumThreshold) const
{
List counts;
// When other operations are concurrent, the count
// might be temporarily less than the actual count.
counts.reserve(count_.load());
for (auto* ctr = head_.load(); ctr != nullptr; ctr = ctr->getNext())
{
if (ctr->getCount() >= minimumThreshold)
counts.emplace_back(ctr->getName(), ctr->getCount());
}
std::ranges::sort(counts);
return counts;
}
} // namespace xrpl

View File

@@ -5,6 +5,7 @@
#include <cerrno>
#include <cstddef>
#include <filesystem>
#include <format>
#include <fstream>
#include <iomanip>
#include <ios>
@@ -99,12 +100,13 @@ uniqueRandomPath(
if (ec)
{
Throw<std::runtime_error>(
"Unable to check path '" + candidate.string() + "': " + ec.message());
std::format("Unable to check path '{}': {}", candidate.string(), ec.message()));
}
if (!exists)
return candidate;
}
Throw<std::runtime_error>("Unable to generate a unique path under '" + base.string() + "'");
Throw<std::runtime_error>(
std::format("Unable to generate a unique path under '{}'", base.string()));
}
TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))

View File

@@ -80,10 +80,10 @@ BookDirs::const_iterator::operator++()
XRPL_ASSERT(index_ != kZero, "xrpl::BookDirs::const_iterator::operator++ : nonzero index");
if (!cdirNext(*view_, curKey_, sle_, entry_, index_))
{
if (index_ == 0)
if (index_ == kZero)
curKey_ = view_->succ(++curKey_, nextQuality_).value_or(kZero);
if (index_ != 0 || curKey_ == kZero)
if (index_ != kZero || curKey_ == kZero)
{
curKey_ = key_;
entry_ = 0;

View File

@@ -11,6 +11,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/LedgerTiming.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
@@ -261,7 +262,7 @@ Ledger::Ledger(Ledger const& prevLedger, NetClock::time_point closeTime)
{
header_.seq = prevLedger.header_.seq + 1;
header_.parentCloseTime = prevLedger.header_.closeTime;
header_.hash = prevLedger.header().hash + UInt256(1);
header_.hash = prevLedger.header().hash.next();
header_.drops = prevLedger.header().drops;
header_.closeTimeResolution = prevLedger.header_.closeTimeResolution;
header_.parentHash = prevLedger.header().hash;
@@ -560,7 +561,7 @@ Ledger::setup()
try
{
if (auto const sle = read(keylet::feeSettings()))
if (auto const sle = FeeSettingsEntryR(*this))
{
bool oldFees = false;
bool newFees = false;

View File

@@ -16,6 +16,7 @@
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
#include <cstddef>
#include <format>
#include <memory>
#include <optional>
#include <stdexcept>
@@ -264,7 +265,7 @@ OpenView::rawTxInsert(
auto const result = txs_.emplace(
std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData));
if (!result.second)
Throw<std::logic_error>("rawTxInsert: duplicate TX id: " + to_string(key));
Throw<std::logic_error>(std::format("rawTxInsert: duplicate TX id: {}", to_string(key)));
}
} // namespace xrpl

View File

@@ -9,6 +9,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/LedgerHashesEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
@@ -361,17 +362,16 @@ hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal)
if (int const diff = ledger.seq() - seq; diff <= 256)
{
// Within 256...
auto const hashIndex = ledger.read(keylet::skip());
LedgerHashesEntryR const hashIndex(ledger, journal);
if (hashIndex)
{
XRPL_ASSERT(
hashIndex->getFieldU32(sfLastLedgerSequence) == (ledger.seq() - 1),
"xrpl::hashOfSeq : matching ledger sequence");
STVector256 vec = hashIndex->getFieldV256(sfHashes);
if (vec.size() >= diff)
return vec[vec.size() - diff];
if (auto const hash = hashIndex.hashAt(diff - 1))
return hash;
JLOG(journal.warn()) << "Ledger " << ledger.seq() << " missing hash for " << seq << " ("
<< vec.size() << "," << diff << ")";
<< hashIndex->getFieldV256(sfHashes).size() << "," << diff << ")";
}
else
{
@@ -387,16 +387,14 @@ hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal)
}
// in skiplist
auto const hashIndex = ledger.read(keylet::skip(seq));
LedgerHashesEntryR const hashIndex(keylet::skip(seq), ledger, journal);
if (hashIndex)
{
auto const lastSeq = hashIndex->getFieldU32(sfLastLedgerSequence);
XRPL_ASSERT(lastSeq >= seq, "xrpl::hashOfSeq : minimum last ledger");
XRPL_ASSERT((lastSeq & 0xff) == 0, "xrpl::hashOfSeq : valid last ledger");
auto const diff = (lastSeq - seq) >> 8;
STVector256 vec = hashIndex->getFieldV256(sfHashes);
if (vec.size() > diff)
return vec[vec.size() - diff - 1];
if (auto const hash = hashIndex.hashAt((lastSeq - seq) >> 8))
return hash;
}
JLOG(journal.warn()) << "Can't get seq " << seq << " from " << ledger.seq() << " error";
return std::nullopt;

View File

@@ -506,7 +506,7 @@ pseudoAccountAddress(ReadView const& view, UInt256 const& pseudoOwnerKey)
RipeshaHasher rsh;
auto const hash = sha512Half(i, view.header().parentHash, pseudoOwnerKey);
rsh(hash.data(), hash.size());
AccountID const ret = AccountID::fromRaw(static_cast<RipeshaHasher::result_type>(rsh));
AccountID const ret{static_cast<RipeshaHasher::result_type>(rsh)};
if (!view.read(keylet::account(ret)))
return ret;
}

View File

@@ -33,6 +33,7 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <format>
#include <functional>
#include <iterator>
#include <memory>
@@ -394,9 +395,11 @@ removeToken(ApplyView& view, AccountID const& owner, UInt256 const& nftokenID, S
if (!page2)
{
Throw<std::runtime_error>(
"page " + to_string(page1->key()) + " has a broken " + field.getName() +
" field pointing to " + to_string(*id));
Throw<std::runtime_error>(std::format(
"page {} has a broken {} field pointing to {}",
to_string(page1->key()),
field.getName(),
to_string(*id)));
}
}
@@ -597,7 +600,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
else
{
Throw<std::runtime_error>(
"Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
std::format("Offer {} cannot be deleted!", to_string(offerIndexes[i])));
}
}
@@ -734,9 +737,9 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
if (!newPrev)
{
// LCOV_EXCL_START
Throw<std::runtime_error>(
"NFTokenPage directory for " + to_string(owner) +
" cannot be repaired. Unexpected link problem.");
Throw<std::runtime_error>(std::format(
"NFTokenPage directory for {} cannot be repaired. Unexpected link problem.",
to_string(owner)));
// LCOV_EXCL_STOP
}
newPrev->at(sfNextPageMin) = nextPage->key();

View File

@@ -1,7 +1,6 @@
#include <xrpl/nodestore/detail/DecodedBlob.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/nodestore/NodeObject.h>
@@ -12,8 +11,13 @@
namespace xrpl::node_store {
DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) : key_(key)
DecodedBlob::DecodedBlob(std::optional<UInt256> key, void const* value, int valueBytes)
{
if (!key)
return;
key_ = key.value();
/* Data format:
Bytes
@@ -63,7 +67,7 @@ DecodedBlob::createObject()
{
Blob data(objectData_, objectData_ + dataBytes_);
object = NodeObject::createObject(objectType_, std::move(data), UInt256::fromVoid(key_));
object = NodeObject::createObject(objectType_, std::move(data), key_);
}
return object;

View File

@@ -38,6 +38,7 @@
#include <functional>
#include <memory>
#include <optional>
#include <span>
#include <sstream>
#include <stdexcept>
#include <string>
@@ -216,7 +217,7 @@ public:
[&hash, pno, &status](void const* data, std::size_t size) {
nudb::detail::buffer bf;
auto const result = nodeobjectDecompress(data, size, bf);
DecodedBlob decoded(hash.data(), result.first, result.second);
DecodedBlob decoded(hash, result.first, result.second);
if (!decoded.wasOk())
{
status = Status::DataCorrupt;
@@ -240,7 +241,7 @@ public:
nudb::error_code ec;
nudb::detail::buffer bf;
auto const result = nodeobjectCompress(e.getData(), e.getSize(), bf);
db.insert(e.getKey(), result.first, result.second, ec);
db.insert(e.getKey().data(), result.first, result.second, ec);
if (ec && ec != nudb::error::key_exists)
Throw<nudb::system_error>(ec);
}
@@ -288,14 +289,25 @@ public:
Throw<nudb::system_error>(ec);
nudb::visit(
dp,
[&](void const* key,
[&](void const* keyData,
std::size_t keyBytes,
void const* data,
std::size_t size,
nudb::error_code&) {
auto key = uint256::fromRaw(
std::span{static_cast<unsigned char const*>(keyData), keyBytes});
if (!key) [[unlikely]]
{
ec = make_error_code(nudb::error::invalid_key_size);
return;
}
nudb::detail::buffer bf;
auto const result = nodeobjectDecompress(data, size, bf);
DecodedBlob decoded(key, result.first, result.second);
if (!decoded.wasOk())
{
ec = make_error_code(nudb::error::missing_value);

View File

@@ -35,8 +35,10 @@
#include <atomic>
#include <cstddef>
#include <filesystem>
#include <format>
#include <functional>
#include <memory>
#include <span>
#include <stdexcept>
#include <string>
@@ -110,6 +112,9 @@ public:
if (!getIfExists(keyValues, Keys::kPath, name))
Throw<std::runtime_error>("Missing path in RocksDBFactory backend");
if (keyBytes != uint256::size())
Throw<std::runtime_error>("Incorrect key size: expected 32 bytes");
rocksdb::BlockBasedTableOptions tableOptions;
options.env = env;
@@ -193,7 +198,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::string("Unable to set RocksDB bbt_options: ") + s.ToString());
std::format("Unable to set RocksDB bbt_options: {}", s.ToString()));
}
}
@@ -206,7 +211,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::string("Unable to set RocksDB options: ") + s.ToString());
std::format("Unable to set RocksDB options: {}", s.ToString()));
}
}
@@ -241,7 +246,7 @@ public:
if (!status.ok() || (localDb == nullptr))
{
Throw<std::runtime_error>(
std::string("Unable to open/create RocksDB: ") + status.ToString());
std::format("Unable to open/create RocksDB: {}", status.ToString()));
}
db.reset(localDb);
}
@@ -291,7 +296,7 @@ public:
if (getStatus.ok())
{
DecodedBlob decoded(hash.data(), string.data(), string.size());
DecodedBlob decoded(hash, string.data(), string.size());
if (decoded.wasOk())
{
@@ -316,8 +321,8 @@ public:
}
else
{
status = static_cast<Status>(
static_cast<int>(Status::CustomCode) + unsafeCast<int>(getStatus.code()));
status = checkedCast<Status>(
safeCast<int>(Status::CustomCode) + safeCast<int>(getStatus.code()));
JLOG(journal.error()) << getStatus.ToString();
}
@@ -346,7 +351,7 @@ public:
EncodedBlob const encoded(e);
wb.Put(
rocksdb::Slice(reinterpret_cast<char const*>(encoded.getKey()), keyBytes),
rocksdb::Slice(reinterpret_cast<char const*>(encoded.getKey().data()), keyBytes),
rocksdb::Slice(
reinterpret_cast<char const*>(encoded.getData()), encoded.getSize()));
}
@@ -356,7 +361,7 @@ public:
auto ret = db->Write(options, &wb);
if (!ret.ok())
Throw<std::runtime_error>("storeBatch failed: " + ret.ToString());
Throw<std::runtime_error>(std::format("storeBatch failed: {}", ret.ToString()));
}
void
@@ -374,9 +379,12 @@ public:
for (it->SeekToFirst(); it->Valid(); it->Next())
{
if (it->key().size() == keyBytes)
if (it->key().size() == keyBytes && it->key().size() == uint256::size())
{
DecodedBlob decoded(it->key().data(), it->value().data(), it->value().size());
DecodedBlob decoded(
uint256::fromRaw(std::span{it->key().data(), it->key().size()}),
it->value().data(),
it->value().size());
if (decoded.wasOk())
{

View File

@@ -8,6 +8,8 @@
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/tokens.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
#include <cstring>
@@ -19,95 +21,143 @@
namespace xrpl {
namespace detail {
namespace {
/**
* Caches the base58 representations of AccountIDs
/** If true, we implement a cache for account ID lookups.
This is generally a good idea and can improve performance, especially
when serving client requests.
The cache may generate warnings under some sanitizers; if you want to
disable caching, set this variable to false.
*/
class AccountIdCache
constexpr bool cacheAccountIDs = true;
/** Represents a cached entry for the given AccountID.
We keep the size and alignment of the structure at 64 bytes to
make sure it fits within a cache line and avoid false sharing.
*/
struct alignas(64) CachedAccountID
{
private:
struct CachedAccountID
{
AccountID id;
char encoding[40] = {0};
};
/** The sequence lock for the entry.
// The actual cache
std::vector<CachedAccountID> cache_;
An even value means that the data is stable, whereas an odd
value indicates the data is being actively modified.
// We use a hash function designed to resist algorithmic complexity attacks
HardenedHash<> hasher_;
We use a 32-bit value to ensure that the counter will never
return to an observed value during a read window.
*/
std::atomic<std::uint32_t> seq{0};
// 64 spinlocks, packed into a single 64-bit value
std::atomic<std::uint64_t> locks_ = 0;
/** The account id. */
AccountID id{};
public:
AccountIdCache(std::size_t count) : cache_(count)
{
// This is non-binding, but we try to avoid wasting memory that
// is caused by overallocation.
cache_.shrink_to_fit();
}
/** The length of the cached encoding.
std::string
toBase58(AccountID const& id)
{
auto const index = hasher_(id) % cache_.size();
This field disambiguates collisions between ACCOUNT_ZERO and
never-written-to slots, since they would otherwise share the
same encoded bits.
*/
std::uint8_t len = 0;
PackedSpinlock sl(locks_, index % 64);
{
std::scoped_lock const lock(sl);
// The check against the first character of the encoding ensures
// that we don't mishandle the case of the all-zero account:
if (cache_[index].encoding[0] != 0 && cache_[index].id == id)
return cache_[index].encoding;
}
auto ret = encodeBase58Token(TokenType::AccountID, id.data(), id.size());
XRPL_ASSERT(ret.size() <= 38, "xrpl::detail::AccountIdCache : maximum result size");
{
std::scoped_lock const lock(sl);
cache_[index].id = id;
std::strcpy(cache_[index].encoding, ret.c_str());
}
return ret;
}
/** The actual string encoding. */
char encoding[39] = {0};
};
} // namespace detail
static_assert(sizeof(CachedAccountID) == 64 && alignof(CachedAccountID) == 64);
static std::unique_ptr<detail::AccountIdCache> gAccountIdCache;
// The actual cache to use: a simple, direct-mapped, best-effort construct.
//
// We do not use a hardened hash for this because an AccountID is already
// just cryptographically generated (i.e. uniformly distributed) bits and
// there is no amplification, since collisions do not carry an additional
// cost.
//
// The size is a "nice" power of 2 because it makes the calculation of an
// index easier.
constinit std::array<CachedAccountID, 65536> gAccountIdCache{};
void
initAccountIdCache(std::size_t count)
{
if (!gAccountIdCache && count != 0)
gAccountIdCache = std::make_unique<detail::AccountIdCache>(count);
}
} // namespace
std::string
toBase58(AccountID const& v)
{
if (gAccountIdCache)
return gAccountIdCache->toBase58(v);
// Since the input bits are generated by a cryptographically secure
// hash construct, we can just pick any subset of them, no muss, no
// fuss.
auto const index = [&v]() -> std::uint16_t {
auto const data = v.data();
return encodeBase58Token(TokenType::AccountID, v.data(), v.size());
return static_cast<std::uint16_t>(data[0]) | (static_cast<std::uint16_t>(data[1]) << 8);
}();
if constexpr (cacheAccountIDs)
{
auto& slot = gAccountIdCache[index];
// This is the optimistic, store-free read path: a collision with a
// concurrent writer will be detected because the seq recheck fails
// and we fall through to the slow path.
//
// These unsynchronized reads of the slot fields are, formally, data
// races and, technically speaking, result in undefined behavior and
// a tool like TSAN might flag them. These should not have an impact
// on the correctness of the returned data: the sequence lock checks
// ensure we discard any data that was in flux.
if (auto const s0 = slot.seq.load(std::memory_order::acquire);
(s0 & 1) == 0 && slot.len != 0 && slot.id == v)
{
char buf[38];
auto const len = std::min<std::size_t>(slot.len, sizeof(buf));
std::memcpy(buf, slot.encoding, len);
// Prevents the loads above from sinking below the recheck; the
// acquire on s0 constrains later operations, not earlier ones.
std::atomic_thread_fence(std::memory_order::acquire);
// Everything read from the slot — the gates above and the bytes in
// buf — precedes this recheck and is validated by it. No slot access
// may follow it.
if (slot.seq.load(std::memory_order::relaxed) == s0)
return std::string(buf, len);
}
}
auto ret = encodeBase58Token(TokenType::AccountID, v.data(), v.size());
XRPL_ASSERT(!ret.empty() && ret.size() <= 38, "xrpl::toBase58 : maximum result size");
if constexpr (cacheAccountIDs)
{
auto& slot = gAccountIdCache[index];
// The CAS to an odd value excludes other writers; if it fails, another
// thread wrote or is writing into this slot. That is not a problem: we
// just don't store our result into the cache this time.
if (auto s0 = slot.seq.load(std::memory_order::relaxed); (s0 & 1) == 0 &&
slot.seq.compare_exchange_strong(s0, s0 + 1, std::memory_order::acquire))
{
slot.id = v;
slot.len = static_cast<std::uint8_t>(ret.size());
std::copy_n(ret.data(), ret.size(), slot.encoding);
// The acquire on the CAS above keeps the writes from becoming visible
// before the seqlock transitions to an odd value, which means readers
// can detect an in-progress write. This release publishes them before
// the seqlock transitions to an even value.
slot.seq.store(s0 + 2, std::memory_order::release);
}
}
return ret;
}
template <>
std::optional<AccountID>
parseBase58(std::string const& s)
{
auto const result = decodeBase58Token(s, TokenType::AccountID);
if (result.size() != AccountID::kBytes)
return std::nullopt;
return AccountID::fromRaw(result);
return AccountID::fromRaw(decodeBase58Token(s, TokenType::AccountID));
}
//------------------------------------------------------------------------------
@@ -148,25 +198,9 @@ parseBase58(std::string const& s)
AccountID
calcAccountID(PublicKey const& pk)
{
static_assert(AccountID::kBytes == sizeof(RipeshaHasher::result_type));
RipeshaHasher rsh;
rsh(pk.data(), pk.size());
return AccountID::fromRaw(static_cast<RipeshaHasher::result_type>(rsh));
}
AccountID const&
xrpAccount()
{
static AccountID const kAccount(beast::kZero);
return kAccount;
}
AccountID const&
noAccount()
{
static AccountID const kAccount(1);
return kAccount;
return AccountID{static_cast<RipeshaHasher::result_type>(rsh)};
}
bool

View File

@@ -185,8 +185,9 @@ getBookBase(Book const& book)
UInt256
getQualityNext(UInt256 const& uBase)
{
static constexpr UInt256 kNextQuality(
"0000000000000000000000000000000000000000000000010000000000000000");
static constexpr UInt256 kNextQuality{
"0000000000000000000000000000000000000000000000010000000000000000"};
return uBase + kNextQuality;
}
@@ -429,9 +430,9 @@ payChannel(AccountID const& src, AccountID const& dst, SeqProxy const& seq) noex
Keylet
nftokenPageMin(AccountID const& owner)
{
std::array<std::uint8_t, 32> buf{};
std::memcpy(buf.data(), owner.data(), owner.size());
return {ltNFTOKEN_PAGE, UInt256::fromRaw(buf)};
std::array<std::uint8_t, UInt256::size()> buf{};
std::ranges::copy(owner, buf.begin());
return {ltNFTOKEN_PAGE, UInt256{buf}};
}
Keylet

View File

@@ -14,6 +14,7 @@
#include <algorithm>
#include <cstdint>
#include <format>
#include <functional>
#include <limits>
#include <optional>
@@ -105,8 +106,8 @@ Permission::Permission()
if (type <= UINT16_MAX)
{
// LCOV_EXCL_START
Throw<std::logic_error>(
"Granular permission value must exceed the maximum uint16_t value: " + name);
Throw<std::logic_error>(std::format(
"Granular permission value must exceed the maximum uint16_t value: {}", name));
// LCOV_EXCL_STOP
}
}

View File

@@ -295,11 +295,9 @@ verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept
NodeID
calcNodeID(PublicKey const& pk)
{
static_assert(NodeID::kBytes == sizeof(RipeshaHasher::result_type));
RipeshaHasher h;
h(pk.data(), pk.size());
return NodeID::fromRaw(static_cast<RipeshaHasher::result_type>(h));
return NodeID{static_cast<RipeshaHasher::result_type>(h)};
}
} // namespace xrpl

View File

@@ -43,8 +43,9 @@ setCurrentTransactionRules(std::optional<Rules> r)
auto const range = [&r]() {
// If any new conditions with new amendments are added to "enableLargeNumbers", those
// amendments must also be added to useRulesGuards.
bool const enableLargeNumbers =
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
bool const enableLargeNumbers = !r ||
(r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol) ||
r->enabled(featureMPTokensV2));
// If enableLargeNumbers is true, then useRulesGuards must also return true.
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
// even though large numbers are _not_ used.
@@ -84,7 +85,8 @@ useRulesGuards(Rules const& rules)
// with createGuards, and any other callers, and the first set of guards can be created directly
// at the call site, without using optional.
return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) ||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0);
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0) ||
rules.enabled(featureMPTokensV2);
}
void

View File

@@ -37,6 +37,7 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <format>
#include <iterator>
#include <limits>
#include <memory>
@@ -147,12 +148,12 @@ STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name)
}
Issue issue;
issue.currency = sit.get160();
issue.currency = Currency{sit.get160()};
if (isXRP(issue.currency))
Throw<std::runtime_error>("invalid native currency");
issue.account = sit.get160();
issue.account = AccountID{sit.get160()};
if (isXRP(issue.account))
Throw<std::runtime_error>("invalid native account");
@@ -1193,8 +1194,7 @@ muldiv(std::uint64_t multiplier, std::uint64_t multiplicand, std::uint64_t divis
if (ret > std::numeric_limits<std::uint64_t>::max())
{
Throw<std::overflow_error>(
"overflow: (" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
") / " + std::to_string(divisor));
std::format("overflow: ({} * {}) / {}", multiplier, multiplicand, divisor));
}
return static_cast<uint64_t>(ret);
@@ -1215,9 +1215,8 @@ muldivRound(
if (ret > std::numeric_limits<std::uint64_t>::max())
{
Throw<std::overflow_error>(
"overflow: ((" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
") + " + std::to_string(rounding) + ") / " + std::to_string(divisor));
Throw<std::overflow_error>(std::format(
"overflow: (({} * {}) + {}) / {}", multiplier, multiplicand, rounding, divisor));
}
return static_cast<uint64_t>(ret);

View File

@@ -19,16 +19,16 @@ STCurrency::STCurrency(SField const& name) : STBase{name}
{
}
STCurrency::STCurrency(SerialIter& sit, SField const& name) : STBase{name}
{
currency_ = sit.get160();
}
STCurrency::STCurrency(SField const& name, Currency const& currency)
: STBase{name}, currency_{currency}
{
}
STCurrency::STCurrency(SerialIter& sit, SField const& name)
: STCurrency{name, Currency{sit.get160()}}
{
}
SerializedTypeID
STCurrency::getSType() const
{

View File

@@ -28,48 +28,38 @@ STIssue::STIssue(SField const& name) : STBase{name}
STIssue::STIssue(SerialIter& sit, SField const& name) : STBase{name}
{
// Either the currency of an Issue or the issuer of an MPT; the
// following 160 bits disambiguate.
auto const currencyOrAccount = sit.get160();
if (isXRP(Currency::fromRaw(currencyOrAccount)))
if (isXRP(Currency{currencyOrAccount}))
{
asset_ = xrpIssue();
return;
}
// Check if MPT
else
// MPT is serialized as:
// - 160 bits MPT issuer account
// - 160 bits black hole account
// - 32 bits sequence
AccountID const account{sit.get160()};
// MPT
if (noAccount() == account)
{
// MPT is serialized as:
// - 160 bits MPT issuer account
// - 160 bits black hole account
// - 32 bits sequence
AccountID const account = AccountID::fromRaw(sit.get160());
// MPT
if (noAccount() == account)
{
MPTID mptID;
std::uint32_t sequence = sit.get32();
// MPTID stores the sequence in canonical big-endian bytes. STIssue
// ledger bytes are the legacy LE-host encoding, so convert the
// native get32() value to LE bytes before copying into the MPTID.
sequence = boost::endian::native_to_little(sequence);
static_assert(MPTID::size() == sizeof(sequence) + sizeof(currencyOrAccount));
memcpy(mptID.data(), &sequence, sizeof(sequence));
memcpy(
mptID.data() + sizeof(sequence),
currencyOrAccount.data(),
sizeof(currencyOrAccount));
MPTIssue const issue{mptID};
asset_ = issue;
}
else
{
Issue issue;
issue.currency = currencyOrAccount;
issue.account = account;
if (!isConsistent(issue))
Throw<std::runtime_error>("invalid issue: currency and account native mismatch");
asset_ = issue;
}
// MPT: issuer (160), noAccount() (160), sequence (32). The sequence
// is stored in little-endian and get32() reads big-endian, so
// reverse it before building the canonical ID.
asset_ = MPTIssue{std::byteswap(sit.get32()), AccountID{currencyOrAccount}};
return;
}
Issue const issue{Currency{currencyOrAccount}, account};
if (!isConsistent(issue))
Throw<std::runtime_error>("invalid issue: currency and account native mismatch");
asset_ = issue;
}
SerializedTypeID

View File

@@ -35,9 +35,8 @@ STLedgerEntry::STLedgerEntry(Keylet const& k) : STObject(sfLedgerEntry), key_(k.
if (format == nullptr)
{
Throw<std::runtime_error>(
"Attempt to create a SLE of unknown type " +
std::to_string(safeCast<std::uint16_t>(k.type)));
Throw<std::runtime_error>(std::format(
"Attempt to create a SLE of unknown type {}", safeCast<std::uint16_t>(k.type)));
}
set(format->getSOTemplate());

View File

@@ -19,6 +19,7 @@
#include <cstddef>
#include <cstdint>
#include <format>
#include <limits>
#include <ostream>
#include <stdexcept>
@@ -170,7 +171,7 @@ partsFromString(std::string const& number)
boost::smatch match;
if (!boost::regex_match(number, match, kReNumber))
Throw<std::runtime_error>("'" + number + "' is not a number");
Throw<std::runtime_error>(std::format("'{}' is not a number", number));
// Match fields:
// 0 = whole input

View File

@@ -5,6 +5,7 @@
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
@@ -17,9 +18,13 @@
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
@@ -36,10 +41,10 @@ STPathElement::getHash(STPathElement const& element)
// 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.
// The note above is only true because the result of this function is only
// used by the STPathElement equality operator as a fast-reject prefilter,
// ahead of the field comparisons that then decide the answer. Do not use
// it to key a container.
for (auto const x : element.getAccountID())
hashAccount += (hashAccount * 257) ^ x;
@@ -106,12 +111,38 @@ STPathSet::operator=(STPathSet const& other)
STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
{
std::vector<STPathElement> path;
for (;;)
{
int const iType = sit.get8();
using enum STPathElement::Type;
if (iType == STPathElement::TypeNone || iType == STPathElement::TypeBoundary)
auto parsePathElementType = [](std::underlying_type_t<STPathElement::Type> byte) noexcept
-> std::optional<STPathElement::Type> {
if (byte == std::to_underlying(TypeBoundary))
return TypeBoundary;
constexpr auto knownBits = static_cast<std::underlying_type_t<STPathElement::Type>>(
std::to_underlying(TypeAccount) | std::to_underlying(TypeCurrency) |
std::to_underlying(TypeIssuer) | std::to_underlying(TypeMpt));
if ((byte & ~knownBits) != 0)
return std::nullopt;
return static_cast<STPathElement::Type>(byte);
};
std::vector<STPathElement> path;
std::optional<STPathElement::Type> type;
do
{
type = parsePathElementType(sit.get8());
if (!type)
{
JLOG(debugLog().error()) << "Bad path element in pathset";
Throw<std::runtime_error>("bad path element");
}
if (type == TypeNone || type == TypeBoundary)
{
if (path.empty())
{
@@ -122,28 +153,11 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
// Move rather than converting the vector to an STPath by copy.
value_.emplace_back(std::move(path));
path.clear();
if (iType == STPathElement::TypeNone)
return;
}
else if ((iType & ~STPathElement::TypeAll) != 0)
{
JLOG(debugLog().error()) << "Bad path element " << iType << " in pathset";
Throw<std::runtime_error>("bad path element");
}
else
{
auto const hasAccount = (iType & STPathElement::TypeAccount) != 0u;
auto const hasCurrency = (iType & STPathElement::TypeCurrency) != 0u;
auto const hasIssuer = (iType & STPathElement::TypeIssuer) != 0u;
auto const hasMPT = (iType & STPathElement::TypeMpt) != 0u;
AccountID account;
PathAsset asset;
AccountID issuer;
if (hasAccount)
account = sit.get160();
bool const hasCurrency = ((*type & TypeCurrency) == TypeCurrency);
bool const hasMPT = ((*type & TypeMpt) == TypeMpt);
if (hasCurrency && hasMPT)
{
@@ -151,18 +165,26 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
Throw<std::runtime_error>("bad path element: MPT and Currency");
}
AccountID account;
AccountID issuer;
if ((*type & TypeAccount) == TypeAccount)
account = AccountID{sit.get160()};
PathAsset asset;
if (hasCurrency)
asset = Currency::fromRaw(sit.get160());
asset = Currency{sit.get160()};
if (hasMPT)
asset = sit.get192();
if (hasIssuer)
issuer = sit.get160();
if ((*type & TypeIssuer) == TypeIssuer)
issuer = AccountID{sit.get160()};
path.emplace_back(account, asset, issuer, hasCurrency || hasMPT);
}
}
} while (type != TypeNone);
}
STBase*
@@ -216,24 +238,22 @@ STPath::getJson(JsonOptions) const
for (auto const& it : path_)
{
json::Value elem(json::ValueType::Object);
auto const iType = it.getNodeType();
elem[jss::type] = iType;
elem[jss::type] = safeCast<std::uint32_t>(it.getNodeType());
if ((iType & STPathElement::TypeAccount) != 0u)
if (it.isType(STPathElement::TypeAccount))
elem[jss::account] = to_string(it.getAccountID());
XRPL_ASSERT(
((iType & STPathElement::TypeCurrency) == 0u) ||
((iType & STPathElement::TypeMpt) == 0u),
!(it.hasCurrency() && it.hasMPT()),
"xrpl::STPath::getJson : not type Currency and MPT");
if ((iType & STPathElement::TypeCurrency) != 0u)
if (it.hasCurrency())
elem[jss::currency] = to_string(it.getCurrency());
if ((iType & STPathElement::TypeMpt) != 0u)
if (it.hasMPT())
elem[jss::mpt_issuance_id] = to_string(it.getMPTID());
if ((iType & STPathElement::TypeIssuer) != 0u)
if (it.hasIssuer())
elem[jss::issuer] = to_string(it.getIssuerID());
ret.append(elem);
@@ -265,34 +285,42 @@ STPathSet::add(Serializer& s) const
XRPL_ASSERT(getFName().fieldType == STI_PATHSET, "xrpl::STPathSet::add : valid field type");
bool first = true;
// Workaround, since `Serializer` does not accept strongly-typed enums
// yet. When support is added, the static_assert will trigger, and the
// lambda can be removed entirely.
auto const toByte = [](std::same_as<STPathElement::Type> auto t) noexcept {
static_assert(
!requires(Serializer& ser) { ser.add8(t); },
"Serializer::add8 now accepts STPathElement::Type directly; remove toByte.");
return std::to_underlying(t);
};
for (auto const& spPath : value_)
{
if (!first)
s.add8(STPathElement::TypeBoundary);
s.add8(toByte(STPathElement::TypeBoundary));
for (auto const& speElement : spPath)
{
int const iType = speElement.getNodeType();
s.add8(toByte(speElement.getNodeType()));
s.add8(iType);
if ((iType & STPathElement::TypeAccount) != 0u)
if (speElement.isType(STPathElement::TypeAccount))
s.addBitString(speElement.getAccountID());
if ((iType & STPathElement::TypeMpt) != 0u)
if (speElement.hasMPT())
s.addBitString(speElement.getMPTID());
if ((iType & STPathElement::TypeCurrency) != 0u)
if (speElement.hasCurrency())
s.addBitString(speElement.getCurrency());
if ((iType & STPathElement::TypeIssuer) != 0u)
if (speElement.hasIssuer())
s.addBitString(speElement.getIssuerID());
}
first = false;
}
s.add8(STPathElement::TypeNone);
s.add8(toByte(STPathElement::TypeNone));
}
} // namespace xrpl

View File

@@ -59,9 +59,8 @@ getTxFormat(TxType type)
if (format == nullptr)
{
Throw<std::runtime_error>(
"Invalid transaction type " +
std::to_string(safeCast<std::underlying_type_t<TxType>>(type)));
Throw<std::runtime_error>(std::format(
"Invalid transaction type {}", safeCast<std::underlying_type_t<TxType>>(type)));
}
return format;

View File

@@ -9,28 +9,32 @@
#include <xrpl/protocol/Serializer.h>
#include <cstddef>
#include <cstdint>
#include <format>
#include <span>
#include <stdexcept>
#include <string>
#include <utility>
namespace xrpl {
STVector256::STVector256(SerialIter& sit, SField const& name) : STBase(name)
{
auto const slice = sit.getSlice(sit.getVLDataLength());
auto slice = sit.getSlice(sit.getVLDataLength());
if (slice.size() % UInt256::size() != 0)
{
Throw<std::runtime_error>(
"Bad serialization for STVector256: " + std::to_string(slice.size()));
std::format("Bad serialization for STVector256: {}", slice.size()));
}
auto const cnt = slice.size() / UInt256::size();
value_.reserve(slice.size() / UInt256::size());
value_.reserve(cnt);
for (std::size_t i = 0; i != cnt; ++i)
value_.push_back(UInt256::fromRaw(slice.substr(i * UInt256::size(), UInt256::size())));
while (!slice.empty())
{
value_.emplace_back(
std::span<std::uint8_t const, UInt256::size()>{slice.data(), UInt256::size()});
slice += UInt256::size();
}
}
STBase*

View File

@@ -70,7 +70,8 @@ STXChainBridge::STXChainBridge(SField const& name, json::Value const& v) : STBas
std::string const name = it.memberName();
if (!kBridgeJson.isMember(name))
{
Throw<std::runtime_error>("STXChainBridge extra field detected: " + name);
Throw<std::runtime_error>(
std::format("STXChainBridge extra field detected: {}", name));
}
}
return true;

View File

@@ -104,8 +104,9 @@ parseGenericSeed(std::string const& str, bool rfc1751)
std::string key;
if (RFC1751::getKeyFromEnglish(key, str) == 1)
{
Blob const blob(key.rbegin(), key.rend());
return Seed{UInt128::fromRaw(blob)};
std::array<std::uint8_t, UInt128::size()> bytes{};
std::ranges::reverse_copy(key, bytes.begin());
return Seed{UInt128{bytes}};
}
}

View File

@@ -15,8 +15,8 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <format>
#include <stdexcept>
#include <string>
#include <type_traits>
namespace xrpl {
@@ -456,7 +456,7 @@ SerialIter::getFieldID(int& type, int& name)
// uncommon type
type = get8();
if (type < 16)
Throw<std::runtime_error>("gFID: uncommon type out of range " + std::to_string(type));
Throw<std::runtime_error>(std::format("gFID: uncommon type out of range {}", type));
}
if (name == 0)
@@ -464,7 +464,7 @@ SerialIter::getFieldID(int& type, int& name)
// uncommon name
name = get8();
if (name < 16)
Throw<std::runtime_error>("gFID: uncommon name out of range " + std::to_string(name));
Throw<std::runtime_error>(std::format("gFID: uncommon name out of range {}", name));
}
}

View File

@@ -6,6 +6,8 @@
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/STAmount.h>
@@ -13,10 +15,12 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <boost/container/flat_set.hpp>
#include <cstdint>
#include <flat_set>
#include <limits>
#include <stdexcept>
@@ -145,6 +149,52 @@ TxMeta::getAffectedAccounts() const
return list;
}
std::flat_set<MPTID>
TxMeta::getAffectedMPTs() const
{
std::flat_set<MPTID> list;
for (auto const& it : nodes_)
{
int const index =
it.getFieldIndex((it.getFName() == sfCreatedNode) ? sfNewFields : sfFinalFields);
if (index != -1)
{
auto inner = dynamic_cast<STObject const*>(&it.peekAtIndex(index));
XRPL_ASSERT(inner, "xrpl::getAffectedMPTs : STObject type cast succeeded");
if (inner != nullptr)
{
// An MPTokenIssuance entry does not store its own issuance id;
// the id is derived from the issuer and the sequence that
// created the issuance.
if (it.getFieldU16(sfLedgerEntryType) == ltMPTOKEN_ISSUANCE)
{
list.insert(
makeMptID(inner->getFieldU32(sfSequence), inner->getAccountID(sfIssuer)));
}
for (auto const& field : *inner)
{
if (auto mptID = dynamic_cast<STBitString<192> const*>(&field);
field.getFName() == sfMPTokenIssuanceID && (mptID != nullptr))
{
list.insert(mptID->value());
}
else if (
auto amount = dynamic_cast<STAmount const*>(&field);
(amount != nullptr) && amount->holds<MPTIssue>())
{
list.insert(amount->get<MPTIssue>().getMptID());
}
}
}
}
}
return list;
}
STObject&
TxMeta::getAffectedNode(SLE::Ref node, SField const& type)
{

View File

@@ -35,7 +35,7 @@ constexpr std::size_t kIsoCodeLength = 3;
std::string
to_string(Currency const& currency)
{
if (currency == beast::kZero)
if (currency == xrpCurrency())
return systemCurrencyCode();
if (currency == noCurrency())
@@ -62,7 +62,7 @@ to_string(Currency const& currency)
}
bool
toCurrency(Currency& currency, std::string const& code)
toCurrency(Currency& currency, std::string_view code)
{
if (code.empty() || code == systemCurrencyCode())
{
@@ -87,7 +87,7 @@ toCurrency(Currency& currency, std::string const& code)
}
Currency
toCurrency(std::string const& code)
toCurrency(std::string_view code)
{
Currency currency;
if (!toCurrency(currency, code))
@@ -95,25 +95,4 @@ toCurrency(std::string const& code)
return currency;
}
Currency const&
xrpCurrency()
{
static Currency const kCurrency(beast::kZero);
return kCurrency;
}
Currency const&
noCurrency()
{
static Currency const kCurrency(1);
return kCurrency;
}
Currency const&
badCurrency()
{
static Currency const kCurrency(0x5852500000000000);
return kCurrency;
}
} // namespace xrpl

View File

@@ -27,6 +27,7 @@
#include <soci/sqlite3/soci-sqlite3.h> // IWYU pragma: keep
#include <format>
#include <memory>
namespace xrpl {
@@ -40,8 +41,8 @@ getSociSqliteInit(std::string const& name, std::string const& dir, std::string c
{
if (name.empty())
{
Throw<std::runtime_error>(
"Sqlite databases must specify a dir and a name. Name: " + name + " Dir: " + dir);
Throw<std::runtime_error>(std::format(
"Sqlite databases must specify a dir and a name. Name: {} Dir: {}", name, dir));
}
std::filesystem::path file(dir);
if (std::filesystem::is_directory(file))
@@ -56,7 +57,7 @@ getSociInit(BasicConfig const& config, std::string const& dbName)
auto const backendName = get(section, Keys::kBackend, "sqlite");
if (backendName != "sqlite")
Throw<std::runtime_error>("Unsupported soci backend: " + backendName);
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", backendName));
auto const path = config.legacy(Sections::kDatabasePath);
auto const ext = dbName == "validators" || dbName == "peerfinder" ? ".sqlite" : ".db";
@@ -101,7 +102,7 @@ open(soci::session& s, std::string const& beName, std::string const& connectionS
}
else
{
Throw<std::runtime_error>("Unsupported soci backend: " + beName);
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", beName));
}
}

View File

@@ -1,10 +1,12 @@
#include <xrpl/server/InfoSub.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/resource/Consumer.h>
#include <cstddef>
@@ -42,6 +44,22 @@ safeUnsub(std::uint64_t seq, F&& f, beast::Journal j) noexcept
}
}
// Number of requested entries not already tracked in `existing`. Only these are
// charged against the cap, so re-subscribing entries a connection already holds
// is free.
template <typename T>
[[nodiscard]] std::size_t
countNew(HashSet<T> const& requested, HashSet<T> const& existing)
{
std::size_t fresh = 0;
for (auto const& entry : requested)
{
if (!existing.contains(entry))
++fresh;
}
return fresh;
}
} // namespace
// This is the primary interface into the "client" portion of the program.
@@ -84,6 +102,16 @@ InfoSub::~InfoSub()
safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j);
// MPT subscriptions are torn down inline here, keyed on seq_, like books
// below. The set is capped; each unsubMPTInternal takes mptLock_ for a
// single O(1) erase and releases it, so a competing MPT publish can
// interleave between erases. Use the internal variant so it does not write
// back to mptSubscriptions_ on this partially-destroyed object.
for (auto const& mptID : mptSubscriptions_)
{
safeUnsub(seq_, [&] { source_.unsubMPTInternal(seq_, mptID); }, j);
}
// Book subscriptions are torn down inline here, keyed on seq_, rather than
// through the chunked account cleanup below. The book set is not capped, so
// it can be large; but each unsubBookInternal takes bookLock_ for a single
@@ -145,16 +173,21 @@ InfoSub::onSendEmpty()
{
}
std::size_t
InfoSub::subscriptionCount(ScopedLock const&) const
{
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
accountHistorySubscriptions_.size() + mptSubscriptions_.size();
}
std::size_t
InfoSub::totalSubscriptionCount() const
{
// Hold lock_ for the whole read so the three sets cannot be mutated
// Hold lock_ for the whole read so the counted sets cannot be mutated
// mid-count by a concurrent (un)subscribe on this connection.
std::scoped_lock const sl(lock_);
// Combined tally the per-connection cap is enforced against.
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
accountHistorySubscriptions_.size();
return subscriptionCount(sl);
}
bool
@@ -166,25 +199,10 @@ InfoSub::tryReserveAccountSubscriptions(
// One lock hold covers the count, the check and the insert.
std::scoped_lock const sl(lock_);
// Entries not already tracked; re-subscribing held accounts is not charged.
auto const countNew = [](HashSet<AccountID> const& requested,
HashSet<AccountID> const& existing) {
std::size_t fresh = 0;
for (auto const& account : requested)
{
if (!existing.contains(account))
++fresh;
}
return fresh;
};
std::size_t const additional = countNew(proposedAccounts, realTimeSubscriptions_) +
countNew(normalAccounts, normalSubscriptions_);
std::size_t const current = normalSubscriptions_.size() + realTimeSubscriptions_.size() +
accountHistorySubscriptions_.size();
if (exceedsSubscriptionCap(current, additional, cap))
if (exceedsSubscriptionCap(subscriptionCount(sl), additional, cap))
return false;
realTimeSubscriptions_.insert(proposedAccounts.begin(), proposedAccounts.end());
@@ -192,6 +210,19 @@ InfoSub::tryReserveAccountSubscriptions(
return true;
}
bool
InfoSub::tryReserveMPTSubscriptions(HashSet<MPTID> const& mptIDs, std::size_t cap)
{
// One lock hold covers the count, the check and the insert.
std::scoped_lock const sl(lock_);
if (exceedsSubscriptionCap(subscriptionCount(sl), countNew(mptIDs, mptSubscriptions_), cap))
return false;
mptSubscriptions_.insert(mptIDs.begin(), mptIDs.end());
return true;
}
void
InfoSub::insertSubAccountInfo(AccountID const& account, bool rt)
{
@@ -288,4 +319,20 @@ InfoSub::getApiVersion() const noexcept
return apiVersion_;
}
void
InfoSub::insertSubMPTInfo(MPTID const& mptID)
{
std::scoped_lock const sl(lock_);
mptSubscriptions_.insert(mptID);
}
void
InfoSub::deleteSubMPTInfo(MPTID const& mptID)
{
std::scoped_lock const sl(lock_);
mptSubscriptions_.erase(mptID);
}
} // namespace xrpl

View File

@@ -26,6 +26,7 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <format>
#include <functional>
#include <limits>
#include <mutex>
@@ -48,7 +49,7 @@ to_string(Manifest const& m)
return "Revocation Manifest " + mk;
if (!m.signingKey)
Throw<std::runtime_error>("No SigningKey in manifest " + mk);
Throw<std::runtime_error>(std::format("No SigningKey in manifest {}", mk));
return "Manifest " + mk + " (" + std::to_string(m.sequence) + ": " +
toBase58(TokenType::NodePublic, *m.signingKey) + ")";

View File

@@ -8,7 +8,9 @@
#include <algorithm>
#include <cstddef>
#include <format>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
@@ -107,10 +109,10 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
depth_ <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::getChildNodeID : maximum leaf depth");
if (depth_ >= SHAMap::kLeafDepth)
Throw<std::logic_error>("Request for child node ID of " + to_string(*this));
Throw<std::logic_error>(std::format("Request for child node ID of {}", to_string(*this)));
if (!isPrefixOf(id_))
Throw<std::logic_error>("Incorrect mask for " + to_string(*this));
Throw<std::logic_error>(std::format("Incorrect mask for {}", to_string(*this)));
SHAMapNodeID node{depth_ + 1, id_};
node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? branch : (branch << 4);
@@ -128,14 +130,15 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
{
std::optional<SHAMapNodeID> ret;
if (size == 33)
if (size == UInt256::size() + 1)
{
unsigned int const depth = *(static_cast<unsigned char const*>(data) + 32);
if (depth <= SHAMap::kLeafDepth)
std::span const bytes{static_cast<unsigned char const*>(data), size};
if (unsigned int const depth = bytes[uint256::size()]; depth <= SHAMap::kLeafDepth)
{
// Reject a serialized ID carrying bits below its own depth. Checked before
// constructing, since the constructor asserts that same property.
if (auto const id = UInt256::fromVoid(data); isPrefixOfAtDepth(id, depth, id))
// constructing the node, since the constructor asserts that same property.
if (uint256 const id{bytes.first<uint256::size()>()}; isPrefixOfAtDepth(id, depth, id))
ret.emplace(depth, id);
}
}

View File

@@ -18,6 +18,7 @@
#include <xrpl/shamap/SHAMapTxPlusMetaLeafNode.h>
#include <cstdint>
#include <format>
#include <stdexcept>
#include <string>
#include <type_traits>
@@ -30,9 +31,8 @@ SHAMapTreeNode::makeTransaction(Slice data, SHAMapHash const& hash, bool hashVal
{
if (data.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(
"Short TXN node: " + std::to_string(data.size()) + " bytes (minimum " +
std::to_string(kMinShaMapItemBytes) + " required)");
Throw<std::runtime_error>(std::format(
"Short TXN node: {} bytes (minimum {} required)", data.size(), kMinShaMapItemBytes));
}
auto item = makeShamapitem(sha512Half(HashPrefix::TransactionId, data), data);
@@ -52,27 +52,25 @@ SHAMapTreeNode::makeTransactionWithMeta(Slice data, SHAMapHash const& hash, bool
if (s.size() < tag.kBytes)
{
Throw<std::runtime_error>(
"Short TXN+MD node: " + std::to_string(s.size()) + " bytes (minimum " +
std::to_string(tag.kBytes) + " required for tag)");
Throw<std::runtime_error>(std::format(
"Short TXN+MD node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
}
// FIXME: improve this interface so that the above check isn't needed
if (!s.getBitString(tag, s.size() - tag.kBytes))
{
Throw<std::out_of_range>(
"Short TXN+MD node: failed to read tag at offset " +
std::to_string(s.size() - tag.kBytes));
Throw<std::out_of_range>(std::format(
"Short TXN+MD node: failed to read tag at offset {}", s.size() - tag.kBytes));
}
s.chop(tag.kBytes);
if (s.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(
"Short TXN+MD node: " + std::to_string(s.size()) +
" bytes after tag removal (minimum " + std::to_string(kMinShaMapItemBytes) +
" required)");
Throw<std::runtime_error>(std::format(
"Short TXN+MD node: {} bytes after tag removal (minimum {} required)",
s.size(),
kMinShaMapItemBytes));
}
auto item = makeShamapitem(tag, s.slice());
@@ -92,16 +90,15 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
if (s.size() < tag.kBytes)
{
Throw<std::runtime_error>(
"Short AS node: " + std::to_string(s.size()) + " bytes (minimum " +
std::to_string(tag.kBytes) + " required for tag)");
Throw<std::runtime_error>(std::format(
"Short AS node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
}
// FIXME: improve this interface so that the above check isn't needed
if (!s.getBitString(tag, s.size() - tag.kBytes))
{
Throw<std::out_of_range>(
"Short AS node: failed to read tag at offset " + std::to_string(s.size() - tag.kBytes));
std::format("Short AS node: failed to read tag at offset {}", s.size() - tag.kBytes));
}
s.chop(tag.kBytes);
@@ -111,9 +108,10 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
if (s.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(
"Short AS node: " + std::to_string(s.size()) + " bytes after tag removal (minimum " +
std::to_string(kMinShaMapItemBytes) + " required)");
Throw<std::runtime_error>(std::format(
"Short AS node: {} bytes after tag removal (minimum {} required)",
s.size(),
kMinShaMapItemBytes));
}
auto item = makeShamapitem(tag, s.slice());
@@ -152,7 +150,7 @@ SHAMapTreeNode::makeFromWire(Slice rawNode)
if (type == kWireTypeTransactionWithMeta)
return makeTransactionWithMeta(rawNode, hash, hashValid);
Throw<std::runtime_error>("wire: Unknown type (" + std::to_string(type) + ")");
Throw<std::runtime_error>(std::format("wire: Unknown type ({})", type));
}
SHAMapTreeNodePtr
@@ -183,9 +181,8 @@ SHAMapTreeNode::makeFromPrefix(Slice rawNode, SHAMapHash const& hash)
if (type == HashPrefix::TxNode)
return makeTransactionWithMeta(rawNode, hash, hashValid);
Throw<std::runtime_error>(
"prefix: unknown type (" +
std::to_string(safeCast<std::underlying_type_t<HashPrefix>>(type)) + ")");
Throw<std::runtime_error>(std::format(
"prefix: unknown type ({})", safeCast<std::underlying_type_t<HashPrefix>>(type)));
}
std::string

View File

@@ -38,7 +38,7 @@ ApplyContext::ApplyContext(
, parentBatchId_(parentBatchId)
{
XRPL_ASSERT(
parentBatchId.has_value() == ((flags_ & TapBatch) == TapBatch),
parentBatchId.has_value() == ((flags_ & ApplyFlags::Batch) == ApplyFlags::Batch),
"Parent Batch ID should be set if batch apply flag is set");
view_.emplace(&base_, flags_);
}
@@ -53,13 +53,14 @@ std::optional<TxMeta>
ApplyContext::apply(TER ter)
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) view_ emplaced in constructor
return view_->apply(base_, tx, ter, parentBatchId_, (flags_ & TapDryRun) != 0u, journal);
return view_->apply(
base_, tx, ter, parentBatchId_, (flags_ & ApplyFlags::DryRun) != ApplyFlags::None, journal);
}
void
ApplyContext::addOrderBook(Book const& book)
{
if ((flags_ & TapDryRun) == TapNone)
if ((flags_ & ApplyFlags::DryRun) == ApplyFlags::None)
registry.get().getOrderBookDB().addOrderBook(book);
}

View File

@@ -134,7 +134,7 @@ preflightCheckSigningKey(STObject const& sigObject, beast::Journal j)
std::optional<NotTEC>
preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::Journal j)
{
if ((flags & TapDryRun) != 0u) // simulation
if ((flags & ApplyFlags::DryRun) != ApplyFlags::None) // simulation
{
std::optional<Slice> const signature = sigObject[~sfTxnSignature];
if (signature && !signature->empty())
@@ -510,7 +510,8 @@ Transactor::minimumFee(
Fees const& fees,
ApplyFlags flags)
{
return scaleFeeLoad(baseFee, registry.getFeeTrack(), fees, (flags & TapUnlimited) != 0u);
return scaleFeeLoad(
baseFee, registry.getFeeTrack(), fees, (flags & ApplyFlags::Unlimited) != ApplyFlags::None);
}
TER
@@ -521,7 +522,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
auto const feePaid = ctx.tx[sfFee].xrp();
if ((ctx.flags & TapBatch) != 0u)
if ((ctx.flags & ApplyFlags::Batch) != ApplyFlags::None)
{
if (feePaid == beast::kZero)
return tesSUCCESS;
@@ -951,7 +952,8 @@ Transactor::checkSign(
return tesSUCCESS;
}
if (((flags & TapDryRun) != 0u) && pkSigner.empty() && !sigObject.isFieldPresent(sfSigners))
if (((flags & ApplyFlags::DryRun) != ApplyFlags::None) && pkSigner.empty() &&
!sigObject.isFieldPresent(sfSigners))
{
// simulate: skip signature validation when neither SigningPubKey nor
// Signers are provided
@@ -1122,7 +1124,7 @@ Transactor::checkMultiSign(
}
XRPL_ASSERT(
(flags & TapDryRun) || !spk.empty(),
(flags & ApplyFlags::DryRun) != ApplyFlags::None || !spk.empty(),
"xrpl::Transactor::checkMultiSign : non-empty signer or "
"simulation");
AccountID const signingAcctIDFromPubKey =
@@ -1604,7 +1606,7 @@ Transactor::operator()()
if (ctx_.size() > kOversizeMetaDataCap)
result = tecOVERSIZE;
if (isTecClaim(result) && ((view().flags() & TapFailHard) != 0u))
if (isTecClaim(result) && ((view().flags() & ApplyFlags::FailHard) != ApplyFlags::None))
{
// If the TapFailHard flag is set, a tec result
// must not do anything
@@ -1684,7 +1686,7 @@ Transactor::operator()()
// Once we call apply, we will no longer be able to look at view()
metadata = ctx_.apply(result);
if ((ctx_.flags() & TapDryRun) != 0u)
if ((ctx_.flags() & ApplyFlags::DryRun) != ApplyFlags::None)
return logger(result, false, std::move(metadata));
return logger(result, canApply, std::move(metadata));

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