Compare commits

..
Author SHA1 Message Date
Pratik Mankawde 4034c866a3 Merge the entry-point migration branch
Bring in Stdcoroutine-2, which now carries the migration plan and the
latest develop.
2026-10-02 17:49:17 +01:00
Pratik Mankawde 4935cbaeb3 style: Remove unused Zero.h include from PayChan_test
misc-include-cleaner flags it now that this branch edits the file.
2026-10-02 17:47:30 +01:00
Pratik Mankawde 302a486c5f Merge the coroutine primitives branch
Bring in Stdcoroutine-1, which now carries the migration plan and the
latest develop.
2026-10-02 17:40:57 +01:00
Pratik Mankawde 754e0b90b7 Merge the coroutine migration plan and develop
Merge Stdcoroutine-0 (the migration plan docs), which now includes the
latest develop. Keep both sides' cspell words and drop the duplicate
stackful entry.
2026-10-02 17:33:42 +01:00
Pratik Mankawde 0ec6648c5f Merge branch 'develop'
Resolve the cspell word list conflict after develop renamed the file to
.cspell.config.yaml.
2026-10-02 16:34:27 +01:00
Pratik Mankawde bec82ee704 Merge branch 'pratik/std-coro/migrate-entry-points' into pratik/std-coro/migrate-test-code 2026-08-19 17:47:41 +01:00
Pratik Mankawde c6224c48ed Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points
Bring in develop via the upstream branch. Two classes of collision needed
manual resolution:

Namespace case refactor (develop #7933) renamed RPC -> rpc and
xrpl::RPC::Tuning -> xrpl::rpc::tuning. Conflicts were resolved by taking
develop's naming together with this stack's coroutine API (postCoroTask,
CoroTask<void>, no Context::coro):
  - src/test/app/Path_test.cpp
  - src/test/app/PathMPT_test.cpp
  - src/test/jtx/impl/TestHelpers.cpp
  - src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp (no textual
    conflict; stale spellings carried forward on stack-authored lines)

New old-API caller: develop added postCoro and Context::coro uses in
src/test/app/PayChan_test.cpp, which this stack does not otherwise touch,
so it merged without conflict but would not compile once Context::coro is
removed here. Both call sites are migrated to postCoroTask and the
.coro = {} designated initialisers dropped. The kFeeHeavyBurdenRpc
assertions that motivated develop's change are preserved unchanged.
2026-08-19 17:47:30 +01:00
Pratik Mankawde 5fb1429e3a Merge remote-tracking branch 'origin/develop' into pratik/std-coro/add-coroutine-primitives
# Conflicts:
#	.cspell.config.yaml
2026-08-19 17:41:56 +01:00
Pratik Mankawde b6526522fe Merge branch 'pratik/std-coro/migrate-entry-points' into pratik/std-coro/migrate-test-code 2026-07-27 13:48:08 +01:00
Pratik Mankawde 61041155d2 Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points 2026-07-27 13:48:08 +01:00
Pratik Mankawde 5ea72b45f1 Merge remote-tracking branch 'origin/develop' into pratik/std-coro/add-coroutine-primitives 2026-07-27 13:47:54 +01:00
Pratik Mankawde 7385004952 Move coroutine-capture NOLINTNEXTLINE comments next to the lambdas
clang-format placed the suppression comments two lines above the lambda
expressions (before the postCoroTask call), so NOLINTNEXTLINE suppressed
the wrong line and clang-tidy still reported
cppcoreguidelines-avoid-capturing-lambda-coroutines at the lambda. Move
the comments inside the argument list, immediately before each lambda,
matching the pattern already used in threadSpecificStorage.
2026-07-27 13:06:13 +01:00
Pratik Mankawde a6fea1227f Suppress clang-tidy coroutine-capture warnings in core tests
The JobQueue_test and Coroutine_test coroutine lambdas capture pointers
to test-scope locals, but each test drives the coroutine to completion
(or, for the stopped-queue case, guarantees it never starts) before the
locals go out of scope. Add NOLINTNEXTLINE for
cppcoreguidelines-avoid-capturing-lambda-coroutines with comments
explaining the lifetime guarantee, and drop the unused <memory> include
flagged by misc-include-cleaner.
2026-07-27 12:36:10 +01:00
Pratik Mankawde 968f9e1bf3 Merge branch 'pratik/std-coro/migrate-entry-points' into pratik/std-coro/migrate-test-code 2026-07-27 12:34:16 +01:00
Pratik Mankawde 70813b336a Suppress clang-tidy coroutine-capture warnings in pathfinding tests
The Path_test and TestHelpers coroutine lambdas capture locals by
reference, but the caller blocks on Gate::waitFor() until the coroutine
signals completion, so the captures cannot dangle. Add NOLINTNEXTLINE
for cppcoreguidelines-avoid-capturing-lambda-coroutines with a comment
explaining the lifetime guarantee.
2026-07-27 12:33:49 +01:00
Pratik Mankawde ab8350b558 Merge branch 'pratik/std-coro/migrate-entry-points' into pratik/std-coro/migrate-test-code 2026-07-27 12:06:41 +01:00
Pratik Mankawde 072e11ecd1 Address clang-tidy findings in entry-point migration
- Suppress cppcoreguidelines-avoid-capturing-lambda-coroutines with
  NOLINT where lifetime is guaranteed: GRPCServer (thisShared keeps
  CallData alive), ServerHandler RPC/WS clients (captures by value,
  handler outlives JobQueue jobs), and PathMPT_test (test blocks on
  Gate::waitFor until the coroutine completes).
- Change ServerHandler::processRequest to take Output const& to fix
  cppcoreguidelines-rvalue-reference-param-not-moved.
- Replace std::lock_guard with std::scoped_lock in RipplePathFind.
- Remove redundant includes and default member initializer in
  Context.h, GRPCServer.h, RipplePathFind.cpp.
2026-07-27 12:06:25 +01:00
Pratik Mankawde f669b70451 Merge branch 'pratik/std-coro/migrate-entry-points' into pratik/std-coro/migrate-test-code 2026-07-27 11:30:34 +01:00
Pratik Mankawde 62839b1531 Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points 2026-07-27 11:30:34 +01:00
Pratik Mankawde 9bddc54722 Fix clang-tidy violations in coroutine primitives
- NOLINT the compiler-mandated coroutine protocol names (promise_type,
  await_ready, ...) that conflict with readability-identifier-naming and
  readability-convert-member-functions-to-static
- Add [[nodiscard]] to handle(), done(), await_ready()
- Replace std::lock_guard with std::scoped_lock const
- Pass CoroTaskRunner name by value and std::move it; default-init
  runCount_ in-class
- Drop unused <coroutine> include from JobQueue.h; include
  instrumentation.h directly in JobQueueAwaiter.h
- CoroTask_test: kN constant naming, const locals, file-level NOLINT for
  cppcoreguidelines-avoid-capturing-lambda-coroutines (lifetimes are
  gated and joined)
2026-07-27 11:30:16 +01:00
Pratik Mankawde 25b9b4d27d Fix JobQueue_test resume() contract violation and Coroutine_test timeout hazards
- PostCoroTest2 called runner->resume() directly from the test thread,
  violating the documented precondition on CoroTaskRunner::resume() (a
  post() must precede every resume) and driving runCount_ negative.
  Rewrite the loop as post()+join(); the non-atomic yieldCount now also
  verifies the happens-before edge join() provides via mutexRun_.
- PostCoroTest3 wrote false into an already-false flag, so it could
  not detect the coroutine running after stop(). Write true and assert
  the flag stays false.
- Coroutine_test: early-return when a Gate waitFor() times out instead
  of falling through to c->join()/a[i]->join(), which would deref a
  null shared_ptr (correctOrder, threadSpecificStorage first loop) or
  block indefinitely (second loop).
2026-07-27 10:29:57 +01:00
Pratik Mankawde 22ed85c3ad Merge branch 'pratik/std-coro/migrate-entry-points' into pratik/std-coro/migrate-test-code 2026-07-27 10:26:00 +01:00
Pratik Mankawde 8c7a27d721 Name the ripple_path_find wait timeout and fire completion on exception
- Replace the magic 30s in doRipplePathFind with
  RPC::Tuning::kPathfindCompletionTimeout.
- In PathRequestManager::updateAll, invoke updateComplete() via a
  ScopeExit guard on the one-shot (hasCompletion) path so the blocked
  RPC handler is released immediately even if doUpdate throws, instead
  of waiting out the full timeout.
2026-07-25 15:05:08 +01:00
Pratik Mankawde 9266c23ef6 Bound concurrent blocking ripple_path_find calls to prevent worker starvation
The no-ledger branch of doRipplePathFind parks its JobQueue worker on a
condition_variable for up to 30s, waiting for a completion that is fired
from a JtUpdatePf job -- which itself needs a free worker to run. With
unbounded JtClientRpc concurrency, enough simultaneous ripple_path_find
calls could park every worker, stalling the entire JobQueue until the
30s timeouts expired. The old Boost.Coroutine implementation suspended
and released the worker, so it did not have this failure mode.

Reuse the RPC::LegacyPathFind guard (already applied on the
ledger-specified branch) to admit at most kMaxPathfindsInProgress
non-admin blocking requests, returning rpcTOO_BUSY beyond that. The
guard stays in scope across the wait.
2026-07-25 12:39:04 +01:00
Pratik Mankawde 5e9ce16de6 Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points 2026-07-25 12:37:14 +01:00
Pratik Mankawde 8d4ea00453 Strengthen CoroTask_test assertions and timeout handling
- testExceptionPropagation now co_awaits an inner CoroTask<void> that
  throws and asserts the rethrown message, covering the
  CoroTask<void>::await_resume rethrow path; the old version could not
  distinguish a throw from a normal return.
- testJobQueueAwaiter now actually awaits the JobQueueAwaiter struct
  (single use per coroutine, per the GCC-12 note) and asserts the full
  ordered step sequence instead of only the terminal value.
- testValueException asserts the caught exception's message.
- All waitFor() timeouts early-return on failure instead of falling
  through to join()/state reads (null-deref and TSAN-race hazards on
  timeout, plus the nSuspend_ assert in ~Env).
- Remove dead shared_ptr locals in testCorrectOrder/testMultipleYields.
- Add missing includes (<array>, <stdexcept>, <string>, <vector>,
  LocalValue.h, CoroTask.h).
2026-07-25 12:36:30 +01:00
Pratik Mankawde bdcf094171 Guard resume() against completed coroutines and log unhandled exceptions
- resume() now skips the handle resume when the task is null or done
  (duplicate external post() after completion), matching the old
  Coro::resume() 'if (coro_)' guard instead of invoking UB in release
  builds. The runCount_ bookkeeping still runs to balance post().
- Exceptions escaping a top-level coroutine body were captured by
  unhandled_exception() and destroyed unobserved with the frame; they
  are now logged at error level before the frame is released.
- Document that join() may return via the finished_ disjunct while the
  final resume() is still completing its bookkeeping.
2026-07-25 12:33:51 +01:00
Pratik Mankawde a42e8174d9 Fix check-rename CI failure
- Remove BoostToStdCoroutineSwitchPlan.md and BoostToStdCoroutineTaskList.md
  working documents from the repo root; the rename script rewrites their
  'rippled' references and the job fails on the resulting dirty tree.
- Reword two CoroTask.h comments to use 'xrpld'.
- Drop cspell words (cppcoro, gantt, Pratik, Mankawde) that existed only
  for the removed documents.
2026-07-25 12:30:38 +01:00
Pratik Mankawde 91004209e9 Merge branch 'pratik/std-coro/migrate-entry-points' into pratik/std-coro/migrate-test-code
Forward-merge the entry-point migration (carrying the latest develop) into
the test-code migration.

Conflict resolutions in Coroutine_test.cpp and JobQueue_test.cpp: keep the
postCoroTask / CoroTaskRunner form from this branch, with develop's renames
(JtClient, Gate::waitFor, kN) and develop's initialization of the
unprotected flag.
2026-07-24 20:57:01 +01:00
Pratik Mankawde 4c18dd867c Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points
Forward-merge the updated coroutine primitives (which themselves carry the
latest develop) into the entry-point migration.

Conflict resolutions:
- .cspell.config.yaml: keep develop's dotfile name, merge word lists.
- Context.h: drop the coro member; infoSub gets a default initializer.
- Application.cpp, ServerHandler.cpp, TestHelpers.cpp, Path_test.cpp,
  PathMPT_test.cpp: adopt develop's designated-initializer JsonContext and
  renamed identifiers (JtClient, kApiVersionIfUnspecified, kMaxSrcCur, Gate).
- GRPCServer: process(coro) becomes processRequest(), posted with
  postCoroTask.
- RipplePathFind.cpp: follow develop's move to handlers/orderbook/ and
  PathRequestManager; the completion state shared with the path-finding
  continuation is now heap-allocated so it outlives an early return.
- AMMTest.cpp: develop moved find_paths_request into TestHelpers.cpp, so the
  local copy is dropped and the migration applied there instead.
2026-07-24 20:52:28 +01:00
Pratik Mankawde fb6ece56f2 Merge remote-tracking branch 'origin/develop' into pratik/std-coro/add-coroutine-primitives
Resolves the develop rename of cspell.config.yaml and the JobQueue.h
conflicts, and aligns the new coroutine primitives with develop naming
(CreateT, mutex_/mutexRun_, JtClient, forceMultiThread,
beast::unit_test::Suite).

Also addresses two review findings:

- postCoroTask now holds a jobCounter_ reservation for the whole
  function. JobQueue::stop() joins jobCounter_ before asserting
  nSuspend_ == 0, so the reservation closes the window between the
  ++nSuspend_ and the balancing post()/expectEarlyExit(), and doubles
  as the shutdown check.

- YieldPostAwaiter::await_suspend returns a coroutine_handle<>
  (symmetric transfer) instead of resuming inline. This keeps a
  yield loop against a stopping JobQueue from growing the stack
  without bound and avoids touching a frame that resume() may have
  already destroyed.
2026-07-24 20:39:41 +01:00
Pratik Mankawde c85cd77b2f cspell update
Signed-off-by: Pratik Mankawde <[email protected]>
2026-04-22 14:14:16 +01:00
Pratik MankawdeandClaude Opus 4.6 cb5259d7c5 docs: Add Boost to C++20 coroutine migration plan
Comprehensive migration plan documenting the switch from
Boost.Coroutine2 to C++20 standard coroutines in rippled, including
research analysis, implementation phases, risk assessment, and
testing strategy.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-04-22 13:18:04 +01:00
Pratik Mankawde 42cced50fb feat: Migrate coroutine tests from Boost.Coroutine to C++20 coroutines
Migrate Coroutine_test and JobQueue_test from Boost.Coroutine to
C++20 std::coroutine using CoroTask/CoroTaskRunner:

- Coroutine_test: Replace Coro-based coroutine tests with CoroTask
  equivalents using co_await runner->yieldAndPost().
- JobQueue_test: Replace Coro suspend/resume patterns with CoroTask
  equivalents, use pointer-by-value captures in coroutine lambdas
  to avoid dangling reference issues.
2026-03-25 15:48:17 +00:00
Pratik Mankawde 0e815aa1ac feat: Migrate production entry points from Boost.Coroutine to C++20 coroutines
Migrate all production coroutine entry points from Boost.Coroutine
to C++20 std::coroutine using the CoroTask/CoroTaskRunner primitives:

- RipplePathFind: Replace Coro suspend/resume with co_await pattern,
  add cv timeout for graceful shutdown.
- ServerHandler: Replace Coro-based processRequest with CoroTask,
  simplify coroutine lifecycle management.
- GRPCServer: Replace Coro with CoroTask for streaming RPC handlers.
- Remove Coro usage from Context.h aggregate initialization.
- Add exception handling in coroutine bodies to prevent unhandled
  exceptions from escaping the coroutine frame.
2026-03-25 15:48:10 +00:00
Pratik Mankawde 21149a81e3 feat: Add C++20 coroutine primitives: CoroTask, CoroTaskRunner, JobQueueAwaiter
Add C++20 std::coroutine based task primitives for the JobQueue:

- CoroTask<T>: A coroutine return type with RAII ownership semantics
  and symmetric transfer for efficient resumption.
- CoroTaskRunner: Manages coroutine lifecycle on the JobQueue with
  suspend/resume tracking, LocalValue preservation, and graceful
  shutdown support.
- JobQueueAwaiter: External awaiter combining yield+post atomically.
- yieldAndPost(): Inline awaiter workaround for GCC-12 codegen bug
  where external awaiters at multiple co_await points corrupt the
  coroutine state machine resume index.
- CoroTask_test: Comprehensive test suite covering task lifecycle,
  suspend/resume, shutdown, and value-returning coroutines.
- BoostToStdCoroutineSwitchPlan.md: Migration plan documentation.
2026-03-25 15:46:44 +00:00
Pratik MankawdeandClaude Opus 4.6 b78202a99a docs: Add Boost to C++20 coroutine migration plan
Comprehensive migration plan documenting the switch from
Boost.Coroutine2 to C++20 standard coroutines in rippled, including
research analysis, implementation phases, risk assessment, and
testing strategy.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-25 15:44:19 +00:00
133 changed files with 6002 additions and 5140 deletions

No files matched your search

+7 -1
View File
@@ -83,6 +83,7 @@ words:
- coeffs
- coldwallet
- compr
- cppcoro
- conanfile
- conanrun
- confs
@@ -129,6 +130,7 @@ words:
- fsanitize
- funclets
- Gamal
- gantt
- gcov
- gcovr
- ghead
@@ -178,6 +180,7 @@ words:
- ltype
- mathbunnyru
- mcmodel
- Mankawde
- MEMORYSTATUSEX
- MPTAMM
- MPTDEX
@@ -247,6 +250,7 @@ words:
- protobuf
- protos
- ptrs
- Pratik
- pushd
- pyenv
- pyparsing
@@ -255,10 +259,11 @@ words:
- queuable
- Raphson
- rcflags
- reencrypted
- replayer
- repodata
- repomd
- repost
- reposts
- rerandomize
- rerandomization
- rerandomized
@@ -306,6 +311,7 @@ words:
- sponsees
- SRPMS
- sslws
- stackful
- statsd
- STATSDCOLLECTOR
- stissue
-4
View File
@@ -8,7 +8,3 @@ 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
-12
View File
@@ -5,18 +5,6 @@
# 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)
+1 -1
View File
@@ -36,4 +36,4 @@ runs:
- name: Determine release channel and package release
id: release_info
uses: XRPLF/actions/release-info@a9f2eeca6fb3980ba3a84cf68566f1c69ad30674
uses: XRPLF/actions/release-info@ebcf6cea14eee258697308a51fea55cad777581b
@@ -1,40 +0,0 @@
#!/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
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
# Exit the script as soon as an error occurs.
set -euo pipefail
@@ -21,10 +21,11 @@ patch_ids() {
git log --no-merges --patch --no-color --no-ext-diff "$@" | git patch-id --stable | sort
}
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
# shellcheck source=.github/scripts/releasing/common.sh
source "${SCRIPT_DIR}/common.sh"
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
# Each line is "<patch-id> <commit>".
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
@@ -1,38 +0,0 @@
#!/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
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
# Exit the script as soon as an error occurs.
set -euo pipefail
-12
View File
@@ -1,12 +0,0 @@
# 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
}
+2 -23
View File
@@ -166,31 +166,10 @@ 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 }}
HEAD: ${{ github.event.pull_request.head.sha }}
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 }}
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
+3 -12
View File
@@ -269,23 +269,14 @@ 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
View File
@@ -92,9 +92,6 @@ target/
# Direnv's directory
/.direnv
# Direnv's local, per-developer overrides
/.envrc.local
# clangd cache
/.cache
-8
View File
@@ -22,14 +22,6 @@ 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))
## 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.
+5 -3
View File
@@ -41,13 +41,15 @@ branch.
git checkout develop
```
For a release or release candidate, check out its [tag](https://github.com/XRPLF/rippled/releases), e.g.:
For a release candidate, choose the relevant release branch, e.g.
`release/3.2.x`.
```bash
git checkout 3.4.0
git checkout release/3.2.x
```
See [RELEASING.md](./RELEASING.md) for how branches and releases are organized.
For a stable release, choose one of the [tagged
releases](https://github.com/XRPLF/rippled/releases).
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
File diff suppressed because it is too large. Load diff
+142
View File
@@ -0,0 +1,142 @@
# Boost.Coroutine to C++20 Migration — Task List
> Parent document: [BoostToStdCoroutineSwitchPlan.md](BoostToStdCoroutineSwitchPlan.md)
---
## Milestone 1: New Coroutine Primitives
- [ ] **1.1** Design `CoroTask<T>` class with `promise_type`
- Define `promise_type` with `initial_suspend`, `final_suspend`, `unhandled_exception`, `return_value`/`return_void`
- Implement `FinalAwaiter` for continuation support
- Implement move-only RAII handle wrapper
- Support both `CoroTask<T>` and `CoroTask<void>`
- [ ] **1.2** Design and implement `JobQueueAwaiter`
- `await_suspend()` calls `jq_.addJob(type, name, [h]{ h.resume(); })`
- Handle `addJob()` failure (shutdown) — resume with error flag or throw
- Integrate `nSuspend_` counter increment/decrement
- [ ] **1.3** Implement `LocalValues` swap in new coroutine resume path
- Before `handle.resume()`: save thread-local, install coroutine-local
- After `handle.resume()` returns: restore thread-local
- Ensure this works when coroutine migrates between threads
- [ ] **1.4** Add `postCoroTask()` template to `JobQueue`
- Accept callable returning `CoroTask<void>`
- Schedule initial execution on JobQueue (mirror `postCoro()` behavior)
- Return a handle/shared_ptr for join/cancel
- [ ] **1.5** Write unit tests (`src/test/core/CoroTask_test.cpp`)
- Test `CoroTask<void>` runs to completion
- Test `CoroTask<int>` returns value
- Test exception propagation across co_await
- Test coroutine destruction before completion
- Test `JobQueueAwaiter` schedules on correct thread
- Test `LocalValue` isolation across 4+ coroutines
- Test shutdown rejection (addJob returns false)
- Test `correct_order` equivalent (yield → join → post → complete)
- Test `incorrect_order` equivalent (post → yield → complete)
- Test multiple sequential co_await points
- [ ] **1.6** Verify build on GCC 12+, Clang 16+
- [ ] **1.7** Run ASAN + TSAN on new tests
- [ ] **1.8** Run full `--unittest` suite (no regressions)
- [ ] **1.9** Self-review and create PR #1
---
## Milestone 2: Entry Point Migration
- [ ] **2.1** Migrate `ServerHandler::onRequest()` (`ServerHandler.cpp:287`)
- Replace `m_jobQueue.postCoro(jtCLIENT_RPC, ...)` with `postCoroTask()`
- Update lambda to return `CoroTask<void>` (add `co_return`)
- Update `processSession` to accept new coroutine type
- [ ] **2.2** Migrate `ServerHandler::onWSMessage()` (`ServerHandler.cpp:325`)
- Replace `m_jobQueue.postCoro(jtCLIENT_WEBSOCKET, ...)` with `postCoroTask()`
- Update lambda signature
- [ ] **2.3** Migrate `GRPCServer::CallData::process()` (`GRPCServer.cpp:102`)
- Replace `app_.getJobQueue().postCoro(JobType::jtRPC, ...)` with `postCoroTask()`
- Update `process(shared_ptr<Coro> coro)` overload signature
- [ ] **2.4** Update `RPC::Context` (`Context.h:27`)
- Replace `std::shared_ptr<JobQueue::Coro> coro{}` with new coroutine wrapper type
- Ensure all code that accesses `context.coro` compiles
- [ ] **2.5** Update `ServerHandler.h` signatures
- `processSession()` and `processRequest()` parameter types
- [ ] **2.6** Update `GRPCServer.h` signatures
- `process()` method parameter types
- [ ] **2.7** Run full `--unittest` suite
- [ ] **2.8** Manual smoke test: HTTP + WS + gRPC RPC requests
- [ ] **2.9** Run ASAN + TSAN
- [ ] **2.10** Self-review and create PR #2
---
## Milestone 3: Handler Migration
- [ ] **3.1** Migrate `doRipplePathFind()` (`RipplePathFind.cpp`)
- Replace `context.coro->yield()` with `co_await PathFindAwaiter{...}`
- Replace continuation lambda's `coro->post()` / `coro->resume()` with awaiter scheduling
- Handle shutdown case (post failure) in awaiter
- [ ] **3.2** Create `PathFindAwaiter` (or use generic `JobQueueAwaiter`)
- Encapsulate the continuation + yield pattern from `RipplePathFind.cpp` lines 108-132
- [ ] **3.3** Update `Path_test.cpp`
- Replace `postCoro` usage with `postCoroTask`
- Ensure `context.coro` usage matches new type
- [ ] **3.4** Update `AMMTest.cpp`
- Replace `postCoro` usage with `postCoroTask`
- [ ] **3.5** Rewrite `Coroutine_test.cpp` for new API
- `correct_order`: postCoroTask → co_await → join → resume → complete
- `incorrect_order`: post before yield equivalent
- `thread_specific_storage`: 4 coroutines with LocalValue isolation
- [ ] **3.6** Update `JobQueue_test.cpp` `testPostCoro`
- Migrate to `postCoroTask` API
- [ ] **3.7** Verify `ripple_path_find` works end-to-end with new coroutines
- [ ] **3.8** Test shutdown-during-pathfind scenario
- [ ] **3.9** Run full `--unittest` suite
- [ ] **3.10** Run ASAN + TSAN
- [ ] **3.11** Self-review and create PR #3
---
## Milestone 4: Cleanup & Validation
- [ ] **4.1** Delete `include/xrpl/core/Coro.ipp`
- [ ] **4.2** Remove from `JobQueue.h`:
- `#include <boost/coroutine2/all.hpp>`
- `struct Coro_create_t`
- `class Coro` (entire class)
- `postCoro()` template
- Comment block (lines 322-377) describing old race condition
- [ ] **4.3** Update `cmake/deps/Boost.cmake`:
- Remove `coroutine` from `find_package(Boost REQUIRED COMPONENTS ...)`
- Remove `Boost::coroutine` from `target_link_libraries`
- [ ] **4.4** Update `cmake/XrplInterface.cmake`:
- Remove `BOOST_COROUTINES2_NO_DEPRECATION_WARNING`
- [ ] **4.5** Run memory benchmark
- Create N=1000 coroutines, compare RSS: before vs after
- Document results
- [ ] **4.6** Run context switch benchmark
- 100K yield/resume cycles, compare latency: before vs after
- Document results
- [ ] **4.7** Run RPC throughput benchmark
- Concurrent `ripple_path_find` requests, compare throughput
- Document results
- [ ] **4.8** Run full `--unittest` suite
- [ ] **4.9** Run ASAN, TSAN, UBSan
- Confirm `__asan_handle_no_return` warnings are gone
- [ ] **4.10** Verify build on all supported compilers
- [ ] **4.11** Self-review and create PR #4
- [ ] **4.12** Document final benchmark results in PR description
+586 -20
View File
@@ -14,13 +14,9 @@ The following branches exist in the main project repository:
- `develop`: The latest set of unreleased features, and the most common
starting point for contributions.
- `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.
- `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).
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
@@ -149,8 +145,8 @@ tl;dr
In general, pull requests use `develop` as the base branch.
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.
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.
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.
@@ -595,16 +591,15 @@ the suggested commit message, or modify it as needed.
#### Slightly more complicated pull requests
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
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
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 the base branch.
fast forward only merge (`--ff-only`) on the command line and push to
`develop`.
Some examples of when separate commits are worthwhile are:
@@ -617,10 +612,9 @@ Some examples of when separate commits are worthwhile are:
Either way, check that:
- 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.
- 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.
- 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
@@ -632,8 +626,578 @@ use them!**
### Releases
Releases, release branches, and merging releases back into `develop`
are described in [RELEASING.md](./RELEASING.md).
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.)
[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
@@ -641,3 +1205,5 @@ are described in [RELEASING.md](./RELEASING.md).
[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
View File
@@ -1,152 +0,0 @@
# 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.
-12
View File
@@ -24,13 +24,9 @@
# `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
@@ -167,14 +163,6 @@ 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
+12 -12
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, 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.
* 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.
*
* 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 any of featureSingleAssetVault, featureLendingProtocol, or
// featureMPTokensV2 are retired
// Small can be removed when either featureSingleAssetVault or featureLendingProtocol 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, 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".
* 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".
*
* 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, LendingProtocol, and MPTokensV2), and/or check if any of
* those amendments are enabled to determine which result to expect.
* (SingleAssetVault and LendingProtocol), and/or check if either of those
* amendments are enabled to determine which result to expect.
*/
class Number final
{
+713
View File
@@ -0,0 +1,713 @@
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
#include <coroutine>
#include <exception>
#include <type_traits>
#include <utility>
#include <variant>
namespace xrpl {
template <typename T = void>
class CoroTask;
/**
* CoroTask<void> -- coroutine return type for void-returning coroutines.
*
* Class / Dependency Diagram
* ==========================
*
* CoroTask<void>
* +-----------------------------------------------+
* | - handle_ : Handle (coroutine_handle<promise>) |
* +-----------------------------------------------+
* | + handle(), done() |
* | + await_ready/suspend/resume (Awaiter iface) |
* +-----------------------------------------------+
* | owns
* v
* promise_type
* +-----------------------------------------------+
* | - exception_ : std::exception_ptr |
* | - continuation_ : std::coroutine_handle<> |
* +-----------------------------------------------+
* | + get_return_object() -> CoroTask |
* | + initial_suspend() -> suspend_always (lazy) |
* | + final_suspend() -> FinalAwaiter |
* | + return_void() |
* | + unhandled_exception() |
* +-----------------------------------------------+
* | returns at final_suspend
* v
* FinalAwaiter
* +-----------------------------------------------+
* | await_suspend(h): |
* | if continuation_ set -> symmetric transfer |
* | else -> noop_coroutine |
* +-----------------------------------------------+
*
* Design Notes
* ------------
* - Lazy start: initial_suspend returns suspend_always, so the coroutine
* body does not execute until the handle is explicitly resumed.
* - Symmetric transfer: await_suspend returns a coroutine_handle instead
* of void/bool, allowing the scheduler to jump directly to the next
* coroutine without growing the call stack.
* - Continuation chaining: when one CoroTask is co_await-ed inside
* another, the caller's handle is stored as continuation_ so
* FinalAwaiter can resume it when this task finishes.
* - Move-only: the handle is exclusively owned; copy is deleted.
*
* Usage Examples
* ==============
*
* 1. Basic void coroutine (the most common case in xrpld):
*
* CoroTask<void> doWork(std::shared_ptr<CoroTaskRunner> runner) {
* // do something
* co_await runner->suspend(); // yield control
* // resumed later via runner->post() or runner->resume()
* co_return;
* }
*
* 2. co_await-ing one CoroTask<void> from another (chaining):
*
* CoroTask<void> inner() {
* // ...
* co_return;
* }
* CoroTask<void> outer() {
* co_await inner(); // continuation_ links outer -> inner
* co_return; // FinalAwaiter resumes outer
* }
*
* 3. Exceptions propagate through co_await:
*
* CoroTask<void> failing() {
* throw std::runtime_error("oops");
* co_return;
* }
* CoroTask<void> caller() {
* try { co_await failing(); }
* catch (std::runtime_error const&) { // caught here }
* }
*
* Caveats / Pitfalls
* ==================
*
* BUG-RISK: Dangling references in coroutine parameters.
* Coroutine parameters are copied into the frame, but references
* are NOT -- they are stored as-is. If the referent goes out of scope
* before the coroutine finishes, you get use-after-free.
*
* // BROKEN -- local dies before coroutine runs:
* CoroTask<void> bad(int& ref) { co_return; }
* void launch() {
* int local = 42;
* auto task = bad(local); // frame stores &local
* } // local destroyed; frame holds dangling ref
*
* // FIX -- pass by value, or ensure lifetime via shared_ptr.
*
* BUG-RISK: GCC 14 corrupts reference captures in coroutine lambdas.
* When a lambda that returns CoroTask captures by reference ([&]),
* GCC 14 may generate a corrupted coroutine frame. Always capture
* by explicit pointer-to-value instead:
*
* // BROKEN on GCC 14:
* jq.postCoroTask(t, n, [&](auto) -> CoroTask<void> { ... });
*
* // FIX -- capture pointers explicitly:
* jq.postCoroTask(t, n, [ptr = &val](auto) -> CoroTask<void> { ... });
*
* BUG-RISK: Resuming a destroyed or completed CoroTask.
* Calling handle().resume() after the coroutine has already run to
* completion (done() == true) is undefined behavior. The CoroTaskRunner
* guards against this with an XRPL_ASSERT, but standalone usage of
* CoroTask must check done() before resuming.
*
* BUG-RISK: Moving a CoroTask that is being awaited.
* If task A is co_await-ed by task B (so A.continuation_ == B), moving
* or destroying A will invalidate the continuation link. Never move
* or reassign a CoroTask while it is mid-execution or being awaited.
*
* LIMITATION: CoroTask is fire-and-forget for the top-level owner.
* There is no built-in notification when the coroutine finishes.
* The caller must use external synchronization (e.g. CoroTaskRunner::join
* or a gate/condition_variable) to know when it is done.
*
* LIMITATION: No cancellation token.
* There is no way to cancel a suspended CoroTask from outside. The
* coroutine body must cooperatively check a flag (e.g. jq_.isStopping())
* after each co_await and co_return early if needed.
*
* LIMITATION: Stackless -- cannot suspend from nested non-coroutine calls.
* If a coroutine calls a regular function that wants to "yield", it
* cannot. Only the immediate coroutine body can use co_await.
* This is acceptable for xrpld because all yield() sites are shallow.
*/
template <>
class CoroTask<void>
{
public:
// The C++ coroutine protocol mandates these names (promise_type,
// initial_suspend, await_ready, ...) and instance-callable awaiter
// methods, which conflict with the project naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct promise_type;
using Handle = std::coroutine_handle<promise_type>;
/**
* Coroutine promise. Compiler uses this to manage coroutine state.
* Stores the exception (if any) and the continuation handle for
* symmetric transfer back to the awaiting coroutine.
*/
struct promise_type
{
// Captured exception from the coroutine body, rethrown in
// await_resume() when this task is co_await-ed by a caller.
std::exception_ptr exception_;
// Handle to the coroutine that is co_await-ing this task.
// Set by await_suspend(). FinalAwaiter uses it for symmetric
// transfer back to the caller. Null if this is a top-level task.
std::coroutine_handle<> continuation_;
/**
* Create the CoroTask return object.
* Called by the compiler at coroutine creation.
*/
CoroTask
get_return_object()
{
return CoroTask{Handle::from_promise(*this)};
}
/**
* Lazy start. The coroutine body does not execute until the
* handle is explicitly resumed (e.g. by CoroTaskRunner::resume).
*/
std::suspend_always
initial_suspend() noexcept
{
return {};
}
/**
* Awaiter returned by final_suspend(). Uses symmetric transfer:
* if a continuation exists, transfers control directly to it
* (tail-call, no stack growth). Otherwise returns noop_coroutine
* so the coroutine frame stays alive for the owner to destroy.
*/
struct FinalAwaiter
{
/**
* Always false. We need await_suspend to run for
* symmetric transfer.
*/
bool
await_ready() noexcept
{
return false;
}
/**
* Symmetric transfer: returns the continuation handle so
* the compiler emits a tail-call instead of a nested resume.
* If no continuation is set, returns noop_coroutine to
* suspend at final_suspend without destroying the frame.
*
* @param h Handle to this completing coroutine
*
* @return Continuation handle, or noop_coroutine
*/
std::coroutine_handle<>
await_suspend(Handle h) noexcept
{
if (auto cont = h.promise().continuation_)
return cont;
return std::noop_coroutine();
}
void
await_resume() noexcept
{
}
};
/**
* Returns FinalAwaiter for symmetric transfer at coroutine end.
*/
FinalAwaiter
final_suspend() noexcept
{
return {};
}
/**
* Called by the compiler for `co_return;` (void coroutine).
*/
void
return_void()
{
}
/**
* Called by the compiler when an exception escapes the coroutine
* body. Captures it for later rethrowing in await_resume().
*/
void
unhandled_exception()
{
exception_ = std::current_exception();
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
/**
* Default constructor. Creates an empty (null handle) task.
*/
CoroTask() = default;
/**
* Takes ownership of a compiler-generated coroutine handle.
*
* @param h Coroutine handle to own
*/
explicit CoroTask(Handle h) : handle_(h)
{
}
/**
* Destroys the coroutine frame if this task owns one.
*/
~CoroTask()
{
if (handle_)
handle_.destroy();
}
/**
* Move constructor. Transfers handle ownership, leaves other empty.
*/
CoroTask(CoroTask&& other) noexcept : handle_(std::exchange(other.handle_, {}))
{
}
/**
* Move assignment. Destroys current frame (if any), takes other's.
*/
CoroTask&
operator=(CoroTask&& other) noexcept
{
if (this != &other)
{
if (handle_)
handle_.destroy();
handle_ = std::exchange(other.handle_, {});
}
return *this;
}
CoroTask(CoroTask const&) = delete;
CoroTask&
operator=(CoroTask const&) = delete;
/**
* @return The underlying coroutine_handle
*/
[[nodiscard]] Handle
handle() const
{
return handle_;
}
/**
* @return true if the coroutine has run to completion (or thrown)
*/
[[nodiscard]] bool
done() const
{
return handle_ && handle_.done();
}
// -- Awaiter interface: allows `co_await someCoroTask;` --
/**
* Always false. This task is lazy, so co_await always suspends
* the caller to set up the continuation link.
*/
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Stores the caller's handle as our continuation, then returns
* our handle for symmetric transfer (caller suspends, we resume).
*
* @param caller Handle of the coroutine doing co_await on us
*
* @return Our handle for symmetric transfer
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> caller) noexcept
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<void>::await_suspend : handle is valid");
handle_.promise().continuation_ = caller;
return handle_; // Symmetric transfer
}
/**
* Called in the awaiting coroutine's context after this task
* completes. Rethrows any exception captured by
* unhandled_exception().
*/
void
await_resume()
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<void>::await_resume : handle is valid");
if (auto& ep = handle_.promise().exception_)
std::rethrow_exception(ep);
}
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
private:
// Exclusively-owned coroutine handle. Null after move or default
// construction. Destroyed in the destructor.
Handle handle_;
};
/**
* CoroTask<T> -- coroutine return type for value-returning coroutines.
*
* Class / Dependency Diagram
* ==========================
*
* CoroTask<T>
* +-----------------------------------------------+
* | - handle_ : Handle (coroutine_handle<promise>) |
* +-----------------------------------------------+
* | + handle(), done() |
* | + await_ready/suspend/resume (Awaiter iface) |
* +-----------------------------------------------+
* | owns
* v
* promise_type
* +-----------------------------------------------+
* | - result_ : variant<monostate, T, |
* | exception_ptr> |
* | - continuation_ : std::coroutine_handle<> |
* +-----------------------------------------------+
* | + get_return_object() -> CoroTask |
* | + initial_suspend() -> suspend_always (lazy) |
* | + final_suspend() -> FinalAwaiter |
* | + return_value(T) -> stores in result_[1] |
* | + unhandled_exception -> stores in result_[2] |
* +-----------------------------------------------+
* | returns at final_suspend
* v
* FinalAwaiter (same symmetric-transfer pattern as CoroTask<void>)
*
* Value Extraction
* ----------------
* await_resume() inspects the variant:
* - index 2 (exception_ptr) -> rethrow
* - index 1 (T) -> return value via move
*
* Usage Examples
* ==============
*
* 1. Simple value return:
*
* CoroTask<int> computeAnswer() { co_return 42; }
*
* CoroTask<void> caller() {
* int v = co_await computeAnswer(); // v == 42
* }
*
* 2. Chaining value-returning coroutines:
*
* CoroTask<int> add(int a, int b) { co_return a + b; }
* CoroTask<int> doubleSum(int a, int b) {
* int s = co_await add(a, b);
* co_return s * 2;
* }
*
* 3. Exception propagation from inner to outer:
*
* CoroTask<int> failing() {
* throw std::runtime_error("bad");
* co_return 0; // never reached
* }
* CoroTask<void> caller() {
* try {
* int v = co_await failing(); // throws here
* } catch (std::runtime_error const& e) {
* // e.what() == "bad"
* }
* }
*
* Caveats / Pitfalls (in addition to CoroTask<void> caveats above)
* ================================================================
*
* BUG-RISK: await_resume() moves the value out of the variant.
* Calling co_await on the same CoroTask<T> instance twice is undefined
* behavior -- the second call will see a moved-from T. CoroTask is
* single-shot: one co_return, one co_await.
*
* BUG-RISK: T must be move-constructible.
* return_value(T) takes by value and moves into the variant.
* Types that are not movable cannot be used as T.
*
* LIMITATION: No co_yield support.
* CoroTask<T> only supports a single co_return. It does not implement
* yield_value(), so using co_yield inside a CoroTask<T> coroutine is a
* compile error. For streaming values, a different return type
* (e.g. Generator<T>) would be needed.
*
* LIMITATION: Result is only accessible via co_await.
* There is no .get() or .result() method. The value can only be
* extracted by co_await-ing the CoroTask<T> from inside another
* coroutine. For extracting results in non-coroutine code, pass a
* pointer to the caller and write through it (as the tests do).
*/
template <typename T>
class CoroTask
{
static_assert(
std::is_move_constructible_v<T>,
"CoroTask<T> requires T to be move-constructible");
public:
// The C++ coroutine protocol mandates these names (promise_type,
// initial_suspend, await_ready, ...) and instance-callable awaiter
// methods, which conflict with the project naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct promise_type;
using Handle = std::coroutine_handle<promise_type>;
/**
* Coroutine promise for value-returning coroutines.
* Stores the result as a variant: monostate (not yet set),
* T (co_return value), or exception_ptr (unhandled exception).
*/
struct promise_type
{
// Tri-state result:
// index 0 (monostate) -- coroutine has not yet completed
// index 1 (T) -- co_return value stored here
// index 2 (exception) -- unhandled exception captured here
std::variant<std::monostate, T, std::exception_ptr> result_;
// Handle to the coroutine co_await-ing this task. Used by
// FinalAwaiter for symmetric transfer. Null for top-level tasks.
std::coroutine_handle<> continuation_;
/**
* Create the CoroTask return object.
* Called by the compiler at coroutine creation.
*/
CoroTask
get_return_object()
{
return CoroTask{Handle::from_promise(*this)};
}
/**
* Lazy start. Coroutine body does not run until explicitly resumed.
*/
std::suspend_always
initial_suspend() noexcept
{
return {};
}
/**
* Symmetric-transfer awaiter at coroutine completion.
* Same pattern as CoroTask<void>::FinalAwaiter.
*/
struct FinalAwaiter
{
bool
await_ready() noexcept
{
return false;
}
/**
* Returns continuation for symmetric transfer, or
* noop_coroutine if this is a top-level task.
*
* @param h Handle to this completing coroutine
*
* @return Continuation handle, or noop_coroutine
*/
std::coroutine_handle<>
await_suspend(Handle h) noexcept
{
if (auto cont = h.promise().continuation_)
return cont;
return std::noop_coroutine();
}
void
await_resume() noexcept
{
}
};
FinalAwaiter
final_suspend() noexcept
{
return {};
}
/**
* Called by the compiler for `co_return value;`.
* Moves the value into result_ at index 1.
*
* @param value The value to store
*/
void
return_value(T value)
{
result_.template emplace<1>(std::move(value));
}
/**
* Captures unhandled exceptions at index 2 of result_.
* Rethrown later in await_resume().
*/
void
unhandled_exception()
{
result_.template emplace<2>(std::current_exception());
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
/**
* Default constructor. Creates an empty (null handle) task.
*/
CoroTask() = default;
/**
* Takes ownership of a compiler-generated coroutine handle.
*
* @param h Coroutine handle to own
*/
explicit CoroTask(Handle h) : handle_(h)
{
}
/**
* Destroys the coroutine frame if this task owns one.
*/
~CoroTask()
{
if (handle_)
handle_.destroy();
}
/**
* Move constructor. Transfers handle ownership, leaves other empty.
*/
CoroTask(CoroTask&& other) noexcept : handle_(std::exchange(other.handle_, {}))
{
}
/**
* Move assignment. Destroys current frame (if any), takes other's.
*/
CoroTask&
operator=(CoroTask&& other) noexcept
{
if (this != &other)
{
if (handle_)
handle_.destroy();
handle_ = std::exchange(other.handle_, {});
}
return *this;
}
CoroTask(CoroTask const&) = delete;
CoroTask&
operator=(CoroTask const&) = delete;
/**
* @return The underlying coroutine_handle
*/
[[nodiscard]] Handle
handle() const
{
return handle_;
}
/**
* @return true if the coroutine has run to completion (or thrown)
*/
[[nodiscard]] bool
done() const
{
return handle_ && handle_.done();
}
// -- Awaiter interface: allows `T val = co_await someCoroTask;` --
/**
* Always false. co_await always suspends to set up continuation.
*/
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Stores caller as continuation, returns our handle for
* symmetric transfer.
*
* @param caller Handle of the coroutine doing co_await on us
*
* @return Our handle for symmetric transfer
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> caller) noexcept
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<T>::await_suspend : handle is valid");
handle_.promise().continuation_ = caller;
return handle_;
}
/**
* Extracts the result: rethrows if exception, otherwise moves
* the T value out of the variant. Single-shot: calling twice
* on the same task is undefined (moved-from T).
*
* @return The co_return-ed value
*/
T
await_resume()
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<T>::await_resume : handle is valid");
auto& result = handle_.promise().result_;
if (auto* ep = std::get_if<2>(&result))
std::rethrow_exception(*ep);
return std::get<1>(std::move(result));
}
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
private:
// Exclusively-owned coroutine handle. Null after move or default
// construction. Destroyed in the destructor.
Handle handle_;
};
} // namespace xrpl
+448
View File
@@ -0,0 +1,448 @@
#pragma once
/**
* @file CoroTaskRunner.ipp
*
* CoroTaskRunner inline implementation.
*
* This file contains the business logic for managing C++20 coroutines
* on the JobQueue. It is included at the bottom of JobQueue.h.
*
* Data Flow: suspend / post / resume cycle
* =========================================
*
* coroutine body CoroTaskRunner JobQueue
* -------------- -------------- --------
* |
* co_await runner->suspend()
* |
* +--- await_suspend ------> onSuspend()
* | ++nSuspend_ ------------> nSuspend_
* | [coroutine is now suspended]
* |
* . (externally or by yieldAndPost())
* .
* +--- (caller calls) -----> post()
* | ++runCount_
* | addJob(resume) ----------> job enqueued
* | |
* | [worker picks up]
* | |
* +--- <----- resume() <-----------------------------------+
* | --nSuspend_ ------> nSuspend_
* | swap in LocalValues (lvs_)
* | task_.handle().resume()
* | |
* | [coroutine body continues here]
* | |
* | swap out LocalValues
* | --runCount_
* | cv_.notify_all()
* v
*
* Thread Safety
* =============
* - mutex_ : guards task_.handle().resume() so that post()-before-suspend
* races cannot resume the coroutine while it is still running.
* (See the race condition discussion in JobQueue.h)
* - mutexRun_ : guards runCount_ counter; used by join() to wait until
* all in-flight resume operations complete.
* - jq_.mutex_: guards nSuspend_ increments/decrements.
*
* Common Mistakes When Modifying This File
* =========================================
*
* 1. Changing lock ordering.
* resume() acquires locks sequentially (never held simultaneously):
* jq_.mutex_ (released immediately), then mutex_ (held across resume),
* then mutexRun_ (released after decrement). post() acquires only
* mutexRun_. Any new code path must follow the same order.
*
* 2. Removing the shared_from_this() capture in post().
* The lambda passed to addJob captures [this, sp = shared_from_this()].
* If you remove sp, 'this' can be destroyed before the job runs,
* causing use-after-free. The sp capture is load-bearing.
*
* 3. Forgetting to decrement nSuspend_ on a new code path.
* Every ++nSuspend_ must have a matching --nSuspend_. If you add a new
* suspension path (e.g. a new awaiter) and forget to decrement on resume
* or on failure, JobQueue::stop() will hang.
*
* 4. Calling task_.handle().resume() without holding mutex_.
* This allows a race where the coroutine runs on two threads
* simultaneously. Always hold mutex_ around resume().
*
* 5. Swapping LocalValues outside of the mutex_ critical section.
* The swap-in and swap-out of LocalValues must bracket the resume()
* call. If you move the swap-out before the lock_guard(mutex_) is
* released, you break LocalValue isolation for any code that runs
* after the coroutine suspends but before the lock is dropped.
*/
namespace xrpl {
/**
* Construct a CoroTaskRunner. Sets runCount_ to 0; does not
* create the coroutine. Call init() afterwards.
*
* @param jq The JobQueue this coroutine will run on
* @param type Job type for scheduling priority
* @param name Human-readable name for logging
*/
inline JobQueue::CoroTaskRunner::CoroTaskRunner(
CreateT,
JobQueue& jq,
JobType type,
std::string name)
: jq_(jq), type_(type), name_(std::move(name))
{
}
/**
* Initialize with a coroutine-returning callable.
* Stores the callable on the heap (FuncStore) so it outlives the
* coroutine frame. Coroutine frames store a reference to the
* callable's implicit object parameter (the lambda). If the callable
* is a temporary, that reference dangles after the caller returns.
* Keeping the callable alive here ensures the coroutine's captures
* remain valid.
*
* @param f Callable: CoroTask<void>(shared_ptr<CoroTaskRunner>)
*/
template <class F>
void
JobQueue::CoroTaskRunner::init(F&& f)
{
using Fn = std::decay_t<F>;
auto store = std::make_unique<FuncStore<Fn>>(std::forward<F>(f));
task_ = store->func(shared_from_this());
storedFunc_ = std::move(store);
}
/**
* Destructor. Waits for any in-flight resume() to complete, then
* asserts (debug) that the coroutine has finished or
* expectEarlyExit() was called.
*
* The join() call is necessary because with async dispatch the
* coroutine runs on a worker thread. The gate signal (which wakes
* the test thread) can arrive before resume() has set finished_.
* join() synchronizes via mutexRun_, establishing a happens-before
* edge: finished_ = true -> unlock(mutexRun_) in resume() ->
* lock(mutexRun_) in join() -> read finished_.
*/
inline JobQueue::CoroTaskRunner::~CoroTaskRunner()
{
#ifndef NDEBUG
join();
XRPL_ASSERT(finished_, "xrpl::JobQueue::CoroTaskRunner::~CoroTaskRunner : is finished");
#endif
}
/**
* Increment the JobQueue's suspended-coroutine count (nSuspend_).
*/
inline void
JobQueue::CoroTaskRunner::onSuspend()
{
std::scoped_lock const lock(jq_.mutex_);
++jq_.nSuspend_;
}
/**
* Decrement nSuspend_ without resuming.
*/
inline void
JobQueue::CoroTaskRunner::onUndoSuspend()
{
std::scoped_lock const lock(jq_.mutex_);
--jq_.nSuspend_;
}
/**
* Return a SuspendAwaiter whose await_suspend() increments nSuspend_
* before the coroutine actually suspends. The caller must later call
* post() or resume() to continue execution.
*
* @return Awaiter for use with `co_await runner->suspend()`
*/
inline auto
JobQueue::CoroTaskRunner::suspend()
{
/**
* Custom awaiter for suspend(). Always suspends (await_ready
* returns false) and increments nSuspend_ in await_suspend().
*/
// The C++ coroutine protocol mandates these awaiter names and
// instance-callable methods, which conflict with the project
// naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct SuspendAwaiter
{
CoroTaskRunner& runner_; // The runner that owns this coroutine.
/**
* Always returns false so the coroutine suspends.
*/
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Called when the coroutine suspends. Increments nSuspend_
* so the JobQueue knows a coroutine is waiting.
*/
void
await_suspend(std::coroutine_handle<>) const
{
runner_.onSuspend();
}
void
await_resume() const noexcept
{
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
return SuspendAwaiter{*this};
}
/**
* Suspend and immediately repost on the JobQueue. Equivalent to
* `co_await JobQueueAwaiter{runner}` but uses an inline struct
* to work around a GCC-12 codegen bug (see declaration in JobQueue.h).
*
* If the JobQueue is stopping (post fails), the suspend count is
* undone and the coroutine continues immediately via symmetric
* transfer back to its own handle.
*
* @return An inline YieldPostAwaiter
*/
inline auto
JobQueue::CoroTaskRunner::yieldAndPost()
{
// The C++ coroutine protocol mandates these awaiter names and
// instance-callable methods, which conflict with the project
// naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct YieldPostAwaiter
{
CoroTaskRunner& runner_;
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Returns a coroutine_handle<> (symmetric transfer) rather than
* void + h.resume(). Two reasons:
*
* 1. h.resume() runs the coroutine nested inside this frame. A
* coroutine that yields in a loop against a stopping JobQueue
* fails post() every iteration, so the stack grows without
* bound. Symmetric transfer is a tail call and does not nest.
*
* 2. After h.resume() returns, the coroutine may have completed
* and destroyed its frame -- the frame this awaiter lives in.
* Returning from await_suspend would then touch freed memory.
*
* A bool return would also avoid nesting, but GCC-12 miscompiles
* bool-returning await_suspend (see JobQueueAwaiter.h).
*
* @return noop_coroutine() to stay suspended (job posted);
* the caller's handle to continue now (JQ stopping)
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> h)
{
runner_.onSuspend();
if (!runner_.post())
{
runner_.onUndoSuspend();
return h;
}
return std::noop_coroutine();
}
void
await_resume() const noexcept
{
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
return YieldPostAwaiter{*this};
}
/**
* Schedule coroutine resumption as a job on the JobQueue.
* A shared_ptr capture (sp) prevents this CoroTaskRunner from being
* destroyed while the job is queued but not yet executed.
*
* @return false if the JobQueue rejected the job (shutting down)
*/
inline bool
JobQueue::CoroTaskRunner::post()
{
{
std::scoped_lock const lk(mutexRun_);
++runCount_;
}
// sp prevents 'this' from being destroyed while the job is pending
if (jq_.addJob(type_, name_, [this, sp = shared_from_this()]() { resume(); }))
{
return true;
}
// The coroutine will not run. Undo the runCount_ increment.
std::scoped_lock const lk(mutexRun_);
--runCount_;
cv_.notify_all();
return false;
}
/**
* Resume the coroutine on the current thread.
*
* Steps:
* 1. Decrement nSuspend_ (under jq_.mutex_)
* 2. Swap in this coroutine's LocalValues for thread-local isolation
* 3. Resume the coroutine handle (under mutex_)
* 4. Swap out LocalValues, restoring the thread's previous state
* 5. Decrement runCount_ and notify join() waiters
*
* @pre post() must have been called before resume(). Direct calls
* without a prior post() will corrupt runCount_ and break join().
* Note: runCount_ is NOT incremented here — post() already did that.
* This ensures join() stays blocked for the entire post->resume lifetime.
*/
inline void
JobQueue::CoroTaskRunner::resume()
{
{
std::scoped_lock const lock(jq_.mutex_);
--jq_.nSuspend_;
}
auto saved = detail::getLocalValues().release();
detail::getLocalValues().reset(&lvs_);
std::scoped_lock const lock(mutex_);
XRPL_ASSERT(
task_.handle() && !task_.done(),
"xrpl::JobQueue::CoroTaskRunner::resume : task handle is valid and not done");
if (task_.handle() && !task_.done())
{
task_.handle().resume();
}
else
{
// A resume() with no coroutine to run (e.g. a duplicate external
// post() after completion). Resuming a null or finished handle is
// undefined behavior, so skip it -- this matches the old
// Coro::resume() `if (coro_)` guard. The bookkeeping below still
// runs to balance the ++runCount_ done by the post() that
// scheduled this call.
JLOG(jq_.journal_.warn())
<< "CoroTaskRunner::resume called for coroutine '" << name_
<< "' with no runnable coroutine (duplicate post or already completed)";
}
detail::getLocalValues().release();
detail::getLocalValues().reset(saved);
if (task_.done())
{
finished_ = true;
// An exception that escapes a top-level coroutine body is captured
// by promise_type::unhandled_exception() but has no awaiter to
// rethrow it, so it would vanish with the frame. Surface it in the
// log. (The old Boost path propagated it out of resume() instead.)
if (auto const& ep = task_.handle().promise().exception_)
{
try
{
std::rethrow_exception(ep);
}
catch (std::exception const& e)
{
JLOG(jq_.journal_.error())
<< "Unhandled exception in coroutine '" << name_ << "': " << e.what();
}
catch (...)
{
JLOG(jq_.journal_.error())
<< "Unhandled non-standard exception in coroutine '" << name_ << "'";
}
}
// Break the shared_ptr cycle: frame -> shared_ptr<runner> -> this.
// Use std::move (not task_ = {}) so task_.handle_ is null BEFORE the
// frame is destroyed. operator= would destroy the frame while handle_
// still holds the old value -- a re-entrancy hazard on GCC-12 if
// frame destruction triggers runner cleanup.
[[maybe_unused]] auto completed = std::move(task_);
}
std::scoped_lock const lk(mutexRun_);
--runCount_;
cv_.notify_all();
}
/**
* @return true if the coroutine has not yet run to completion
*/
inline bool
JobQueue::CoroTaskRunner::runnable() const
{
// After normal completion, task_ is reset to break the shared_ptr cycle
// (handle_ becomes null). A null handle means the coroutine is done.
return task_.handle() && !task_.done();
}
/**
* Handle early termination when the coroutine never ran (e.g. JobQueue
* is stopping). Decrements nSuspend_ and destroys the coroutine frame
* to break the shared_ptr cycle: frame -> lambda -> runner -> frame.
*/
inline void
JobQueue::CoroTaskRunner::expectEarlyExit()
{
if (!finished_)
{
std::scoped_lock const lock(jq_.mutex_);
--jq_.nSuspend_;
finished_ = true;
}
// Break the shared_ptr cycle: frame -> shared_ptr<runner> -> this.
// The coroutine is at initial_suspend and never ran user code, so
// destroying it is safe. Use std::move (not task_ = {}) so
// task_.handle_ is null before the frame is destroyed.
{
[[maybe_unused]] auto completed = std::move(task_);
}
storedFunc_.reset();
}
/**
* Block until all pending/active resume operations complete.
* Uses cv_ + mutexRun_ to wait until runCount_ reaches 0 or
* finished_ becomes true. The finished_ check handles the case
* where resume() is called directly (without post()), which
* decrements runCount_ below zero. In that scenario runCount_
* never returns to 0, but finished_ becoming true guarantees
* the coroutine is done and no more resumes will occur.
*
* Note: when join() returns via the finished_ disjunct, the final
* resume() call may still be executing its post-completion
* bookkeeping (the --runCount_ / notify after finished_ is set).
* That is safe -- the coroutine body has fully completed and the
* runner is kept alive by the resume job's shared_ptr -- but
* callers must not assume resume() itself has returned.
*/
inline void
JobQueue::CoroTaskRunner::join()
{
std::unique_lock<std::mutex> lk(mutexRun_);
cv_.wait(lk, [this]() { return runCount_ == 0 || finished_; });
}
} // namespace xrpl
+509 -1
View File
@@ -2,6 +2,7 @@
#include <xrpl/basics/LocalValue.h>
#include <xrpl/core/ClosureCounter.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/JobTypeData.h>
#include <xrpl/core/detail/Workers.h>
#include <xrpl/json/json_value.h>
@@ -152,6 +153,420 @@ public:
join();
};
/**
* C++20 coroutine lifecycle manager. Replaces Coro for new code.
*
* Class / Inheritance / Dependency Diagram
* =========================================
*
* std::enable_shared_from_this<CoroTaskRunner>
* ^
* | (public inheritance)
* |
* CoroTaskRunner
* +---------------------------------------------------+
* | - lvs_ : detail::LocalValues |
* | - jq_ : JobQueue& |
* | - type_ : JobType |
* | - name_ : std::string |
* | - runCount_ : int (in-flight resumes) |
* | - mutex_ : std::mutex (coroutine guard) |
* | - mutexRun_ : std::mutex (join guard) |
* | - cv_ : condition_variable |
* | - task_ : CoroTask<void> |
* | - storedFunc_ : unique_ptr<FuncBase> (type-erased)|
* +---------------------------------------------------+
* | + init(F&&) : set up coroutine callable |
* | + onSuspend() : ++jq_.nSuspend_ |
* | + onUndoSuspend() : --jq_.nSuspend_ |
* | + suspend() : returns SuspendAwaiter |
* | + post() : schedule resume on JobQueue |
* | + resume() : resume coroutine on caller |
* | + runnable() : !task_.done() |
* | + expectEarlyExit() : teardown for failed post |
* | + join() : block until not running |
* +---------------------------------------------------+
* | |
* | owns | references
* v v
* CoroTask<void> JobQueue
* (coroutine frame) (thread pool + nSuspend_)
*
* FuncBase / FuncStore<F> (type-erased heap storage
* for the coroutine lambda)
*
* Coroutine Lifecycle (Control Flow)
* ===================================
*
* Caller thread JobQueue worker thread
* ------------- ----------------------
* postCoroTask(f)
* |
* +-- reserve a jobCounter_ slot (reject if JQ shutting down)
* +-- ++nSuspend_ (lazy start counts as suspended)
* +-- make_shared<CoroTaskRunner>
* +-- init(f)
* | +-- store lambda on heap (FuncStore)
* | +-- task_ = f(shared_from_this())
* | [coroutine created, suspended at initial_suspend]
* +-- post()
* | +-- ++runCount_
* | +-- addJob(type_, [resume]{})
* | resume()
* | |
* | +-- --nSuspend_
* | +-- swap in LocalValues
* | +-- task_.handle().resume()
* | | [coroutine body runs]
* | | ...
* | | co_await suspend()
* | | +-- ++nSuspend_
* | | [coroutine suspends]
* | +-- swap out LocalValues
* | +-- --runCount_
* | +-- cv_.notify_all()
* |
* post() <-- called externally or by yieldAndPost()
* +-- ++runCount_
* +-- addJob(type_, [resume]{})
* resume()
* |
* +-- [coroutine body continues]
* +-- co_return
* +-- --runCount_
* +-- cv_.notify_all()
* join()
* +-- cv_.wait([]{runCount_ == 0})
* +-- [done]
*
* Usage Examples
* ==============
*
* 1. Fire-and-forget coroutine (most common pattern):
*
* jq.postCoroTask(JtClient, "MyWork",
* [](auto runner) -> CoroTask<void> {
* doSomeWork();
* co_await runner->suspend(); // yield to other jobs
* doMoreWork();
* co_return;
* });
*
* 2. Manually controlling suspend / resume (external trigger):
*
* auto runner = jq.postCoroTask(JtClient, "ExtTrigger",
* [&result](auto runner) -> CoroTask<void> {
* startAsyncOperation(callback);
* co_await runner->suspend();
* // callback called runner->post() to get here
* result = collectResult();
* co_return;
* });
* // ... later, from the callback:
* runner->post(); // reschedule the coroutine on the JobQueue
*
* 3. Using yieldAndPost() for automatic suspend + repost:
*
* jq.postCoroTask(JtClient, "AutoRepost",
* [](auto runner) -> CoroTask<void> {
* step1();
* co_await runner->yieldAndPost(); // yield + auto-repost
* step2();
* co_await runner->yieldAndPost();
* step3();
* co_return;
* });
*
* 4. Checking shutdown after co_await (cooperative cancellation):
*
* jq.postCoroTask(JtClient, "Cancellable",
* [&jq](auto runner) -> CoroTask<void> {
* while (moreWork()) {
* co_await runner->yieldAndPost();
* if (jq.isStopping())
* co_return; // bail out cleanly
* processNextItem();
* }
* co_return;
* });
*
* Caveats / Pitfalls
* ==================
*
* BUG-RISK: Calling suspend() without a matching post()/resume().
* After co_await runner->suspend(), the coroutine is parked and
* nSuspend_ is incremented. If nothing ever calls post() or
* resume(), the coroutine is leaked and JobQueue::stop() will
* hang forever waiting for nSuspend_ to reach zero.
*
* BUG-RISK: Calling post() on an already-running coroutine.
* post() schedules a resume() job. If the coroutine has not
* actually suspended yet (no co_await executed), the resume job
* will try to call handle().resume() while the coroutine is still
* running on another thread. This is UB. The mutex_ prevents
* data corruption but the logic is wrong — always co_await
* suspend() before calling post(). (The test incorrect_order()
* shows this works only because mutex_ serializes the calls.)
*
* BUG-RISK: Dropping the shared_ptr<CoroTaskRunner> before join().
* The CoroTaskRunner destructor asserts that finished_ is true
* (the coroutine completed). If you let the last shared_ptr die
* while the coroutine is still running or suspended, you get an
* assertion failure in debug and UB in release. Always call
* join() or expectEarlyExit() first.
*
* BUG-RISK: Lambda captures outliving the coroutine frame.
* The lambda passed to postCoroTask is heap-allocated (FuncStore)
* to prevent dangling. But objects captured by pointer still need
* their own lifetime management. If you capture a raw pointer to
* a stack variable, and the stack frame exits before the coroutine
* finishes, the pointer dangles. Use shared_ptr or ensure the
* pointed-to object outlives the coroutine.
*
* BUG-RISK: Forgetting co_return in a void coroutine.
* If the coroutine body falls off the end without co_return,
* the compiler may silently treat it as co_return (per standard),
* but some compilers warn. Always write explicit co_return.
*
* LIMITATION: CoroTaskRunner only supports CoroTask<void>.
* The task_ member is CoroTask<void>. To return values from
* the top-level coroutine, write through a captured pointer
* (as the tests demonstrate), or co_await inner CoroTask<T>
* coroutines that return values.
*
* LIMITATION: One coroutine per CoroTaskRunner.
* init() must be called exactly once. You cannot reuse a
* CoroTaskRunner to run a second coroutine. Create a new one
* via postCoroTask() instead.
*
* LIMITATION: No timeout on join().
* join() blocks indefinitely. If the coroutine is suspended
* and never posted, join() will deadlock. Use timed waits
* on the gate pattern (condition_variable + wait_for) in tests.
*/
class CoroTaskRunner : public std::enable_shared_from_this<CoroTaskRunner>
{
private:
// Per-coroutine thread-local storage. Swapped in before resume()
// and swapped out after, so each coroutine sees its own LocalValue
// state regardless of which worker thread executes it.
detail::LocalValues lvs_;
// Back-reference to the owning JobQueue. Used to post jobs,
// increment/decrement nSuspend_, and acquire jq_.mutex_.
JobQueue& jq_;
// Job type passed to addJob() when posting this coroutine.
JobType type_;
// Human-readable name for this coroutine job (for logging).
std::string name_;
// Number of in-flight resume operations (pending + active).
// Incremented by post(), decremented when resume() finishes.
// Guarded by mutexRun_. join() blocks until this reaches 0.
//
// A counter (not a bool) is needed because post() can be called
// from within the coroutine body (e.g. via yieldAndPost()),
// enqueuing a second resume while the first is still running.
// A bool would be clobbered: R2.post() sets true, then R1's
// cleanup sets false — losing the fact that R2 is still pending.
int runCount_ = 0;
// Serializes all coroutine resume() calls, preventing concurrent
// execution of the coroutine body on multiple threads. Handles the
// race where post() enqueues a resume before the coroutine has
// actually suspended (post-before-suspend pattern).
std::mutex mutex_;
// Guards runCount_. Used with cv_ for join() to wait
// until all pending/active resume operations complete.
std::mutex mutexRun_;
// Notified when runCount_ reaches zero, allowing
// join() waiters to wake up.
std::condition_variable cv_;
// The coroutine handle wrapper. Owns the coroutine frame.
// Set by init(). Reset to empty in resume() upon coroutine
// completion (to break the shared_ptr cycle) or in
// expectEarlyExit() on early termination.
CoroTask<void> task_;
/**
* Type-erased base for heap-stored callables.
* Prevents the coroutine lambda from being destroyed before
* the coroutine frame is done with it.
*
* @see FuncStore
*/
struct FuncBase
{
virtual ~FuncBase() = default;
};
/**
* Concrete type-erased storage for a callable of type F.
* The coroutine frame stores a reference to the lambda's implicit
* object parameter. If the lambda is a temporary, that reference
* dangles after the call returns. FuncStore keeps it alive on
* the heap for the lifetime of the CoroTaskRunner.
*/
template <class F>
struct FuncStore : FuncBase
{
F func; // The stored callable (coroutine lambda).
explicit FuncStore(F&& f) : func(std::move(f))
{
}
};
// Heap-allocated callable storage. Set by init(), ensures the
// lambda outlives the coroutine frame that references it.
std::unique_ptr<FuncBase> storedFunc_;
// True once the coroutine has completed or expectEarlyExit() was
// called. Asserted in the destructor (debug) to catch leaked
// runners. Available in all builds to guard expectEarlyExit()
// against double-decrementing nSuspend_.
bool finished_ = false;
public:
/**
* Tag type for private construction. Prevents external code
* from constructing CoroTaskRunner directly. Use postCoroTask().
*/
struct CreateT
{
explicit CreateT() = default;
};
/**
* Construct a CoroTaskRunner. Private by convention (CreateT tag).
*
* @param jq The JobQueue this coroutine will run on
* @param type Job type for scheduling priority
* @param name Human-readable name for logging
*/
CoroTaskRunner(CreateT, JobQueue&, JobType, std::string);
CoroTaskRunner(CoroTaskRunner const&) = delete;
CoroTaskRunner&
operator=(CoroTaskRunner const&) = delete;
/**
* Destructor. Asserts (debug) that the coroutine has finished
* or expectEarlyExit() was called.
*/
~CoroTaskRunner();
/**
* Initialize with a coroutine-returning callable.
* Must be called exactly once, after the object is managed by
* shared_ptr (because init uses shared_from_this internally).
* This is handled automatically by postCoroTask().
*
* @param f Callable: CoroTask<void>(shared_ptr<CoroTaskRunner>)
*/
template <class F>
void
init(F&& f);
/**
* Increment the JobQueue's suspended-coroutine count (nSuspend_).
* Called when the coroutine is about to suspend. Every call
* must be balanced by a corresponding decrement (via resume()
* or onUndoSuspend()), or JobQueue::stop() will hang.
*/
void
onSuspend();
/**
* Decrement nSuspend_ without resuming.
* Used to undo onSuspend() when a scheduled post() fails
* (e.g. JobQueue is stopping).
*/
void
onUndoSuspend();
/**
* Suspend the coroutine.
* The awaiter's await_suspend() increments nSuspend_ before the
* coroutine actually suspends. The caller must later call post()
* or resume() to continue execution.
*
* @return An awaiter for use with `co_await runner->suspend()`
*/
auto
suspend();
/**
* Suspend the coroutine and immediately repost it on the
* JobQueue. Combines suspend() + post() atomically inside
* await_suspend, so there is no window where an external
* event could race between the two.
*
* Equivalent to JobQueueAwaiter but defined as an inline
* awaiter returned from a member function. This avoids a
* GCC-12 coroutine codegen bug where an external awaiter
* struct (JobQueueAwaiter) used at multiple co_await points
* corrupts the coroutine state machine's resume index,
* causing the coroutine to hang on the third resumption.
*
* @return An awaiter for use with `co_await runner->yieldAndPost()`
*/
auto
yieldAndPost();
/**
* Schedule coroutine resumption as a job on the JobQueue.
* Captures shared_from_this() to prevent this runner from being
* destroyed while the job is queued.
*
* @return true if the job was accepted; false if the JobQueue
* is stopping (caller must handle cleanup)
*/
bool
post();
/**
* Resume the coroutine on the current thread.
* Decrements nSuspend_, swaps in LocalValues, resumes the
* coroutine handle, swaps out LocalValues, and notifies join()
* waiters. Lock ordering (sequential, non-overlapping):
* jq_.mutex_ -> mutex_ -> mutexRun_.
*
* @pre post() must have been called before resume(). Direct
* calls without a prior post() will corrupt runCount_
* and break join().
*/
void
resume();
/**
* @return true if the coroutine has not yet run to completion
*/
bool
runnable() const;
/**
* Handle early termination when the coroutine never ran.
* Decrements nSuspend_ and destroys the coroutine frame to
* break the shared_ptr cycle (frame -> lambda -> runner -> frame).
* Called by postCoroTask() when post() fails.
*/
void
expectEarlyExit();
/**
* Block until all pending/active resume operations complete.
* Uses cv_ + mutexRun_ to wait until runCount_ reaches 0.
* Warning: deadlocks if the coroutine is suspended and never posted.
*/
void
join();
};
using JobFunction = std::function<void()>;
JobQueue(
@@ -197,6 +612,20 @@ public:
std::shared_ptr<Coro>
postCoro(JobType t, std::string const& name, F&& f);
/**
* Creates a C++20 coroutine and adds a job to the queue to run it.
*
* @param t The type of job.
* @param name Name of the job.
* @param f Callable with signature
* CoroTask<void>(std::shared_ptr<CoroTaskRunner>).
*
* @return shared_ptr to posted CoroTaskRunner. nullptr if not successful.
*/
template <class F>
std::shared_ptr<CoroTaskRunner>
postCoroTask(JobType t, std::string const& name, F&& f);
/**
* Jobs waiting at this priority.
*/
@@ -419,7 +848,8 @@ private:
} // namespace xrpl
#include <xrpl/core/Coro.ipp> // IWYU pragma: keep
#include <xrpl/core/Coro.ipp> // IWYU pragma: keep
#include <xrpl/core/CoroTaskRunner.ipp> // IWYU pragma: keep
namespace xrpl {
@@ -442,4 +872,82 @@ JobQueue::postCoro(JobType t, std::string const& name, F&& f)
return coro;
}
// postCoroTask — entry point for launching a C++20 coroutine on the JobQueue.
//
// Control Flow
// ============
//
// postCoroTask(t, name, f)
// |
// +-- 1. Reserve a jobCounter_ slot — reject if JQ shutting down
// |
// +-- 2. ++nSuspend_ (mirrors Boost Coro ctor's implicit yield)
// | The coroutine is "suspended" from the JobQueue's perspective
// | even though it hasn't run yet — this keeps the JQ shutdown
// | logic correct (it waits for nSuspend_ to reach 0).
// |
// +-- 3. Create CoroTaskRunner (shared_ptr, ref-counted)
// |
// +-- 4. runner->init(f)
// | +-- Heap-allocate the lambda (FuncStore) to prevent
// | | dangling captures in the coroutine frame
// | +-- task_ = f(shared_from_this())
// | [coroutine created but NOT started — lazy initial_suspend]
// |
// +-- 5. runner->post()
// | +-- addJob(type_, [resume]{}) → resume on worker thread
// | +-- failure (JQ stopping):
// | +-- runner->expectEarlyExit()
// | | --nSuspend_, destroy coroutine frame
// | +-- return nullptr
// |
// +-- 6. Release the jobCounter_ slot (on return)
//
// Why reserve a jobCounter_ slot?
// ===============================
// JobQueue::stop() joins jobCounter_ before it acquires mutex_ and asserts
// nSuspend_ == 0. Without a reservation, stop() could run to completion in
// the window between the ++nSuspend_ in step 2 and the balancing post() or
// expectEarlyExit() in step 5, tripping that assert. Holding a slot blocks
// stop()'s join() for the whole function, closing the window. wrap() also
// returns nullopt once the counter is joined, so it doubles as the shutdown
// check — a plain stopping_ read cannot do this, because the read and the
// ++nSuspend_ are not a single atomic step.
//
// Why async post() instead of synchronous resume()?
// ==================================================
// The initial dispatch MUST use async post() so the coroutine body runs on
// a JobQueue worker thread, not the caller's thread. resume() swaps the
// caller's thread-local LocalValues with the coroutine's private copy.
// If the coroutine mutates LocalValues (e.g. thread_specific_storage test),
// those mutations bleed back into the caller's thread-local state after the
// swap-out, corrupting subsequent tests that share the same thread pool.
// Async post() avoids this by running the coroutine on a worker thread whose
// LocalValues are managed by the thread pool, not by the caller.
//
template <class F>
std::shared_ptr<JobQueue::CoroTaskRunner>
JobQueue::postCoroTask(JobType t, std::string const& name, F&& f)
{
// Held until this function returns. Null once jobCounter_ is joined,
// which is how a shutting-down JobQueue rejects new coroutines.
auto const shutdownGuard = jobCounter_.wrap([]() {});
if (!shutdownGuard)
return nullptr;
{
std::scoped_lock const lock(mutex_);
++nSuspend_;
}
auto runner = std::make_shared<CoroTaskRunner>(CoroTaskRunner::CreateT{}, *this, t, name);
runner->init(std::forward<F>(f));
if (!runner->post())
{
runner->expectEarlyExit();
runner.reset();
}
return runner;
}
} // namespace xrpl
+212
View File
@@ -0,0 +1,212 @@
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/JobQueue.h>
#include <coroutine>
#include <memory>
namespace xrpl {
/**
* Awaiter that suspends and immediately reschedules on the JobQueue.
* Equivalent to calling yield() followed by post() in the old Coro API.
*
* Usage:
* co_await JobQueueAwaiter{runner};
*
* What it waits for: The coroutine is re-queued as a job and resumes
* when a worker thread picks it up.
*
* Which thread resumes: A JobQueue worker thread.
*
* What await_resume() returns: void.
*
* Dependency Diagram
* ==================
*
* JobQueueAwaiter
* +----------------------------------------------+
* | + runner : shared_ptr<CoroTaskRunner> |
* +----------------------------------------------+
* | + await_ready() -> false (always suspend) |
* | + await_suspend() -> bool (suspend or cancel) |
* | + await_resume() -> void |
* +----------------------------------------------+
* | |
* | uses | uses
* v v
* CoroTaskRunner JobQueue
* .onSuspend() (via runner->post() -> addJob)
* .onUndoSuspend()
* .post()
*
* Control Flow (await_suspend)
* ============================
*
* co_await JobQueueAwaiter{runner}
* |
* +-- await_ready() -> false
* +-- await_suspend(handle)
* |
* +-- runner->onSuspend() // ++nSuspend_
* +-- runner->post() // addJob to JobQueue
* | |
* | +-- success? return noop_coroutine()
* | | // coroutine stays suspended;
* | | // worker thread will call resume()
* | +-- failure? (JQ stopping)
* | +-- runner->onUndoSuspend() // --nSuspend_
* | +-- return handle // symmetric transfer back
* | // coroutine continues immediately
* | // so it can clean up and co_return
*
* DEPRECATED — prefer `co_await runner->yieldAndPost()`
* =====================================================
*
* GCC-12 has a coroutine codegen bug where using this external awaiter
* struct at multiple co_await points in the same coroutine corrupts the
* state machine's resume index. After the second co_await, the third
* resumption enters handle().resume() but never reaches await_resume()
* or any subsequent user code — the coroutine hangs indefinitely.
*
* The fix is `co_await runner->yieldAndPost()`, which defines the
* awaiter as an inline struct inside a CoroTaskRunner member function.
* GCC-12 handles inline awaiters correctly at multiple co_await points.
*
* This struct is retained for single-use scenarios and documentation
* purposes. For any code that may use co_await in a loop or at
* multiple points, always use `runner->yieldAndPost()`.
*
* Usage Examples
* ==============
*
* 1. Yield and auto-repost (preferred — works on all compilers):
*
* CoroTask<void> handler(auto runner) {
* doPartA();
* co_await runner->yieldAndPost(); // yield + repost
* doPartB(); // runs on a worker thread
* co_return;
* }
*
* 2. Multiple yield points in a loop:
*
* CoroTask<void> batchProcessor(auto runner) {
* for (auto& item : items) {
* process(item);
* co_await runner->yieldAndPost(); // let other jobs run
* }
* co_return;
* }
*
* 3. Graceful shutdown — checking after resume:
*
* CoroTask<void> longTask(auto runner, JobQueue& jq) {
* while (hasWork()) {
* co_await runner->yieldAndPost();
* // If JQ is stopping, await_suspend resumes the coroutine
* // immediately without re-queuing. Always check
* // isStopping() to decide whether to proceed:
* if (jq.isStopping())
* co_return;
* doNextChunk();
* }
* co_return;
* }
*
* Caveats / Pitfalls
* ==================
*
* BUG-RISK: Using a stale or null runner.
* The runner shared_ptr must be valid and point to the CoroTaskRunner
* that owns the coroutine currently executing. Passing a runner from
* a different coroutine, or a default-constructed shared_ptr, is UB.
*
* BUG-RISK: Assuming resume happens on the same thread.
* After co_await, the coroutine resumes on whatever worker thread
* picks up the job. Do not rely on thread-local state unless it is
* managed through LocalValue (which CoroTaskRunner automatically
* swaps in/out).
*
* BUG-RISK: Ignoring the shutdown path.
* When the JobQueue is stopping, post() fails and await_suspend()
* resumes the coroutine immediately (symmetric transfer back to h).
* The coroutine body continues on the same thread. If your code
* after co_await assumes it was re-queued and is running on a worker
* thread, that assumption breaks during shutdown. Always handle the
* "JQ is stopping" case, either by checking jq.isStopping() or by
* letting the coroutine fall through to co_return naturally.
*
* DIFFERENCE from runner->suspend() + runner->post():
* Both JobQueueAwaiter and yieldAndPost() combine suspend + post
* in one atomic operation. With the manual suspend()/post() pattern,
* there is a window between the two calls where an external event
* could race. The atomic awaiters remove that window — onSuspend()
* and post() happen within the same await_suspend() call while the
* coroutine is guaranteed to be suspended. Use yieldAndPost() unless
* you need an external party to decide *when* to call post().
*/
struct JobQueueAwaiter
{
// The CoroTaskRunner that owns the currently executing coroutine.
std::shared_ptr<JobQueue::CoroTaskRunner> runner;
/**
* Always returns false so the coroutine suspends.
*/
// The C++ coroutine protocol mandates these awaiter names and
// instance-callable methods, which conflict with the project
// naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Increment nSuspend (equivalent to yield()) and schedule resume
* on the JobQueue (equivalent to post()). If the JobQueue is
* stopping, undoes the suspend count and transfers back to the
* coroutine so it can clean up and co_return.
*
* Returns a coroutine_handle<> (symmetric transfer) instead of
* bool to work around a GCC-12 codegen bug where bool-returning
* await_suspend leaves the coroutine in an invalid state —
* neither properly suspended nor resumed — causing a hang.
*
* WARNING: GCC-12 has an additional codegen bug where using this
* external awaiter struct at multiple co_await points in the same
* coroutine corrupts the state machine's resume index, causing the
* coroutine to hang on the third resumption. Prefer
* `co_await runner->yieldAndPost()` which uses an inline awaiter
* that GCC-12 handles correctly.
*
* @return noop_coroutine() to stay suspended (job posted);
* the caller's handle to resume immediately (JQ stopping)
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> h)
{
XRPL_ASSERT(runner, "xrpl::JobQueueAwaiter::await_suspend : runner is valid");
runner->onSuspend();
if (!runner->post())
{
// JobQueue is stopping. Undo the suspend count and
// transfer back to the coroutine so it can clean up
// and co_return.
runner->onUndoSuspend();
return h;
}
return std::noop_coroutine();
}
void
await_resume() const noexcept
{
}
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
};
} // namespace xrpl
@@ -8,8 +8,6 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/UintTypes.h>
namespace xrpl {
@@ -42,81 +40,6 @@ public:
: Base(keylet::trustLine(id, issue), view, j)
{
}
/**
* Returns the limit that @p account sets on this line for IOUs issued
* by @p issuer. The issuer of the result is @p account.
*
* @param account the account whose limit is returned.
* @param issuer the account on the other side of the line.
* @return the limit @p account sets on this line.
*/
[[nodiscard]] STAmount
creditLimit(AccountID const& account, AccountID const& issuer) const
{
STAmount result = (*this)->getFieldAmount(account < issuer ? sfLowLimit : sfHighLimit);
result.get<Issue>().account = account;
return result;
}
/**
* Returns the amount of IOUs issued by @p issuer that @p account holds
* on this line. The issuer of the result is @p account.
*
* @param account the account whose balance is returned.
* @param issuer the account on the other side of the line.
* @return the balance @p account holds on this line.
*/
[[nodiscard]] STAmount
creditBalance(AccountID const& account, AccountID const& issuer) const
{
STAmount result = (*this)->getFieldAmount(sfBalance);
if (account < issuer)
result.negate();
result.get<Issue>().account = account;
return result;
}
/**
* Returns true if @p issuer froze this line for @p account.
*
* @param account the account whose side of the line is checked.
* @param issuer the account on the other side of the line.
* @return true if @p issuer froze this line for @p account.
*/
[[nodiscard]] bool
isIndividualFrozen(AccountID const& account, AccountID const& issuer) const
{
return (*this)->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze);
}
/**
* Returns true if either side deep-froze this line.
*
* @return true if either side deep-froze this line.
*/
[[nodiscard]] bool
isDeepFrozen() const
{
return (*this)->isFlag(lsfHighDeepFreeze) || (*this)->isFlag(lsfLowDeepFreeze);
}
/**
* Returns true if @p issuer authorized @p account on this line.
*
* @note This checks only the line's auth flag. Whether the issuer
* requires authorization (lsfRequireAuth) is on the issuer AccountRoot;
* see requireAuth().
*
* @param account the account whose authorization is checked.
* @param issuer the account on the other side of the line.
* @return true if @p issuer authorized @p account on this line.
*/
[[nodiscard]] bool
isAuthorized(AccountID const& account, AccountID const& issuer) const
{
return (*this)->isFlag((account > issuer) ? lsfLowAuth : lsfHighAuth);
}
};
using RippleStateEntryR = RippleStateEntry<ReadView>;
+1 -2
View File
@@ -4,7 +4,6 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -150,7 +149,7 @@ escrowUnlockApplyHelper<Issue>(
// of the funds
if (!createAsset)
{
RippleStateEntryW const sleRippleState(trustLineKey, ctx.view, journal);
auto const sleRippleState = ctx.view.peek(trustLineKey);
if (!sleRippleState)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -4,7 +4,6 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/IOUAmount.h>
@@ -166,7 +165,7 @@ trustCreate(
[[nodiscard]] TER
trustDelete(
ApplyView& view,
RippleStateEntryW& sleRippleState,
SLE::Ref sleRippleState,
AccountID const& uLowAccountID,
AccountID const& uHighAccountID,
beast::Journal j);
@@ -271,7 +270,7 @@ removeEmptyHolding(
[[nodiscard]] TER
deleteAMMTrustLine(
ApplyView& view,
RippleStateEntryW& sleState,
SLE::pointer sleState,
std::optional<AccountID> const& ammAccountID,
beast::Journal j);
-6
View File
@@ -238,12 +238,6 @@ 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
-5
View File
@@ -12,12 +12,10 @@
#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 {
@@ -117,9 +115,6 @@ public:
parentBatchID_ = id;
}
[[nodiscard]] std::flat_set<MPTID>
getAffectedMPTs() const;
private:
UInt256 transactionID_;
std::uint32_t ledgerSeq_;
@@ -439,7 +439,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRecoveryKey, SoeOptional},
}))
/** A ledger object which tracks Oracle
@@ -330,7 +330,6 @@ 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)
@@ -1147,27 +1147,12 @@ 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, 95, TransactionProposalCreate,
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
({.amendment = featureCosign}),
({
{sfProposedTransaction, SoeRequired},
-1
View File
@@ -411,7 +411,6 @@ 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
@@ -339,30 +339,6 @@ 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);
}
};
/**
@@ -576,17 +552,6 @@ 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.
@@ -1,279 +0,0 @@
// 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
@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
/**
* @brief Transaction: TransactionProposalCreate
*
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::NoPriv
+8 -66
View File
@@ -7,7 +7,6 @@
#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>
@@ -27,11 +26,11 @@ namespace xrpl {
/**
* Maximum number of subscriptions a single client connection may hold at once.
*
* 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.
* 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.
*/
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
@@ -152,21 +151,6 @@ 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.
@@ -286,11 +270,6 @@ 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.
@@ -330,9 +309,9 @@ public:
* Return the number of subscriptions currently tracked on this
* connection.
*
* 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.
* 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.
*
* @return The total tracked subscription count for this connection.
*
@@ -363,25 +342,6 @@ 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.
*
@@ -453,29 +413,12 @@ 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_;
@@ -484,7 +427,6 @@ private:
std::uint64_t seq_;
HashSet<AccountID> accountHistorySubscriptions_;
HashSet<Book> bookSubscriptions_;
HashSet<MPTID> mptSubscriptions_;
unsigned int apiVersion_ = 0;
static int
+2 -8
View File
@@ -46,9 +46,6 @@ 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_;
@@ -141,9 +138,8 @@ 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 any of sfConfidentialBalanceSpending, sfConfidentialBalanceInbox,
* sfIssuerEncryptedBalance or sfAuditorEncryptedBalance is present, and the issuance's
* sfConfidentialOutstandingAmount is non-zero. Mirrors MPTokenAuthorize::preclaim.
* Cannot delete if sfIssuerEncryptedBalance exists
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
* - Privacy flag consistency:
* MPToken confidential balance fields can only be created or changed if
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
@@ -166,8 +162,6 @@ 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;
+3 -4
View File
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -34,7 +33,7 @@ checkFreeze(
}
}
if (RippleStateEntryR const sle(src, dst, currency, view); sle)
if (auto sle = view.read(keylet::trustLine(src, dst, currency)))
{
if (sle->isFlag((dst > src) ? lsfHighFreeze : lsfLowFreeze))
{
@@ -78,8 +77,8 @@ checkNoRipple(
beast::Journal j)
{
// fetch the ripple lines into and out of this node
RippleStateEntryR const sleIn(prev, cur, currency, view);
RippleStateEntryR const sleOut(cur, next, currency, view);
auto sleIn = view.read(keylet::trustLine(prev, cur, currency));
auto sleOut = view.read(keylet::trustLine(cur, next, currency));
if (!sleIn || !sleOut)
return terNO_LINE;
@@ -1,84 +0,0 @@
#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
-4
View File
@@ -26,10 +26,6 @@ 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
-13
View File
@@ -147,19 +147,6 @@ 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";
+4 -6
View File
@@ -217,13 +217,12 @@ 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 but `X.Y.Z`_ | `private` | `deb-private` | `rpm-private-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
A variant is published to the same channel under its own name, so
`xrpld-assert` never overwrites `xrpld`.
Only a tag 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
Only a tag picks a release channel. 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.
@@ -285,9 +284,8 @@ 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`. Builds of a non-public
codebase are pushed there only for `stable` releases, whose packages are
public too.
secret, an organization access token for `xrplf`. Private builds are never
pushed there.
- `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
+2 -4
View File
@@ -5,7 +5,6 @@
#include <cerrno>
#include <cstddef>
#include <filesystem>
#include <format>
#include <fstream>
#include <iomanip>
#include <ios>
@@ -100,13 +99,12 @@ uniqueRandomPath(
if (ec)
{
Throw<std::runtime_error>(
std::format("Unable to check path '{}': {}", candidate.string(), ec.message()));
"Unable to check path '" + candidate.string() + "': " + ec.message());
}
if (!exists)
return candidate;
}
Throw<std::runtime_error>(
std::format("Unable to generate a unique path under '{}'", base.string()));
Throw<std::runtime_error>("Unable to generate a unique path under '" + base.string() + "'");
}
TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))
+1 -2
View File
@@ -16,7 +16,6 @@
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
#include <cstddef>
#include <format>
#include <memory>
#include <optional>
#include <stdexcept>
@@ -265,7 +264,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>(std::format("rawTxInsert: duplicate TX id: {}", to_string(key)));
Throw<std::logic_error>("rawTxInsert: duplicate TX id: " + to_string(key));
}
} // namespace xrpl
+3 -6
View File
@@ -11,7 +11,6 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -22,7 +21,6 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
@@ -558,7 +556,7 @@ ammLPHolds(
auto const currency = ammLPTCurrency(asset1, asset2);
STAmount amount;
RippleStateEntryR const sle(lpAccount, ammAccount, currency, view);
auto const sle = view.read(keylet::trustLine(lpAccount, ammAccount, currency));
if (!sle)
{
amount.clear(Issue{currency, ammAccount});
@@ -648,7 +646,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
}
else if (
auto const sle =
RippleStateEntryR(ammAccountID, issue.account, issue.currency, view);
view.read(keylet::trustLine(ammAccountID, issue.account, issue.currency));
sle && !isFrozen(view, ammAccountID, issue.currency, issue.account))
{
STAmount amount = (*sle)[sfBalance];
@@ -690,8 +688,7 @@ deleteAMMTrustLines(
// LCOV_EXCL_STOP
}
RippleStateEntryW line(Keylet(ltRIPPLE_STATE, sleItem->key()), sb, j);
return {deleteAMMTrustLine(sb, line, ammAccountID, j), SkipEntry::No};
return {deleteAMMTrustLine(sb, sleItem, ammAccountID, j), SkipEntry::No};
}
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMObjects: deleting non-trustline or non-MPT " << nodeType;
+10 -14
View File
@@ -7,7 +7,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -34,7 +33,6 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <format>
#include <functional>
#include <iterator>
#include <memory>
@@ -396,11 +394,9 @@ removeToken(ApplyView& view, AccountID const& owner, UInt256 const& nftokenID, S
if (!page2)
{
Throw<std::runtime_error>(std::format(
"page {} has a broken {} field pointing to {}",
to_string(page1->key()),
field.getName(),
to_string(*id)));
Throw<std::runtime_error>(
"page " + to_string(page1->key()) + " has a broken " + field.getName() +
" field pointing to " + to_string(*id));
}
}
@@ -601,7 +597,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
else
{
Throw<std::runtime_error>(
std::format("Offer {} cannot be deleted!", to_string(offerIndexes[i])));
"Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
}
}
@@ -738,9 +734,9 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
if (!newPrev)
{
// LCOV_EXCL_START
Throw<std::runtime_error>(std::format(
"NFTokenPage directory for {} cannot be repaired. Unexpected link problem.",
to_string(owner)));
Throw<std::runtime_error>(
"NFTokenPage directory for " + to_string(owner) +
" cannot be repaired. Unexpected link problem.");
// LCOV_EXCL_STOP
}
newPrev->at(sfNextPageMin) = nextPage->key();
@@ -842,7 +838,7 @@ tokenOfferCreatePreclaim(
if (view.rules().enabled(featureNFTokenMintOffer))
{
if (nftIssuer != amount.getIssuer() &&
!RippleStateEntryR(nftIssuer, amount.get<Issue>(), view))
!view.read(keylet::trustLine(nftIssuer, amount.get<Issue>())))
return tecNO_LINE;
}
else if (!view.exists(keylet::trustLine(nftIssuer, amount.get<Issue>())))
@@ -1029,7 +1025,7 @@ checkTrustlineAuthorized(
if (issuerAccount->isFlag(lsfRequireAuth))
{
RippleStateEntryR const trustLine(id, issue.account, issue.currency, view);
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
if (!trustLine)
{
@@ -1079,7 +1075,7 @@ checkTrustlineDeepFrozen(
return tesSUCCESS;
}
RippleStateEntryR const trustLine(id, issue.account, issue.currency, view);
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
if (!trustLine)
{
@@ -7,7 +7,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
@@ -49,10 +48,13 @@ creditLimit(
{
STAmount result(Issue{currency, account});
RippleStateEntryR const sleRippleState(account, issuer, currency, view);
auto sleRippleState = view.read(keylet::trustLine(account, issuer, currency));
if (sleRippleState)
result = sleRippleState.creditLimit(account, issuer);
{
result = sleRippleState->getFieldAmount(account < issuer ? sfLowLimit : sfHighLimit);
result.get<Issue>().account = account;
}
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditLimit : result issuer match");
XRPL_ASSERT(
@@ -77,10 +79,15 @@ creditBalance(
{
STAmount result(Issue{currency, account});
RippleStateEntryR const sleRippleState(account, issuer, currency, view);
auto sleRippleState = view.read(keylet::trustLine(account, issuer, currency));
if (sleRippleState)
result = sleRippleState.creditBalance(account, issuer);
{
result = sleRippleState->getFieldAmount(sfBalance);
if (account < issuer)
result.negate();
result.get<Issue>().account = account;
}
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditBalance : result issuer match");
XRPL_ASSERT(
@@ -108,8 +115,8 @@ isIndividualFrozen(
if (issuer != account)
{
// Check if the issuer froze the line
RippleStateEntryR const sle(account, issuer, currency, view);
if (sle && sle.isIndividualFrozen(account, issuer))
auto const sle = view.read(keylet::trustLine(account, issuer, currency));
if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
return true;
}
return false;
@@ -132,8 +139,8 @@ isFrozen(
if (issuer != account)
{
// Check if the issuer froze the line
RippleStateEntryR const sleLine(account, issuer, currency, view);
if (sleLine && sleLine.isIndividualFrozen(account, issuer))
sle = view.read(keylet::trustLine(account, issuer, currency));
if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
return true;
}
return false;
@@ -156,13 +163,13 @@ isDeepFrozen(
return false;
}
RippleStateEntryR const sle(account, issuer, currency, view);
auto const sle = view.read(keylet::trustLine(account, issuer, currency));
if (!sle)
{
return false;
}
return sle.isDeepFrozen();
return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze);
}
//------------------------------------------------------------------------------
@@ -206,9 +213,8 @@ trustCreate(
// LCOV_EXCL_STOP
}
RippleStateEntryW sleRippleState(Keylet(ltRIPPLE_STATE, uIndex), view, j);
sleRippleState.newSLE();
sleRippleState.insert();
auto const sleRippleState = std::make_shared<SLE>(ltRIPPLE_STATE, uIndex);
view.insert(sleRippleState);
auto lowNode = view.dirInsert(
keylet::ownerDir(uLowAccountID), sleRippleState->key(), describeOwnerDir(uLowAccountID));
@@ -279,8 +285,7 @@ trustCreate(
sleRippleState->setFieldU32(sfFlags, uFlags);
increaseOwnerCount(view, sleAccount, sponsorSle, 1, j);
addSponsorToLedgerEntry(
sleRippleState.mutableRawSle(), sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor);
addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor);
// ONLY: Create ripple balance.
sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance);
@@ -293,7 +298,7 @@ trustCreate(
TER
trustDelete(
ApplyView& view,
RippleStateEntryW& sleRippleState,
SLE::Ref sleRippleState,
AccountID const& uLowAccountID,
AccountID const& uHighAccountID,
beast::Journal j)
@@ -316,11 +321,11 @@ trustDelete(
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfHighSponsor);
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfLowSponsor);
removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor);
removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor);
JLOG(j.trace()) << "trustDelete: Deleting ripple line: state";
sleRippleState.erase();
view.erase(sleRippleState);
return tesSUCCESS;
}
@@ -334,7 +339,7 @@ trustDelete(
static bool
updateTrustLine(
ApplyView& view,
RippleStateEntryW& state,
SLE::pointer state,
bool bSenderHigh,
AccountID const& sender,
STAmount const& before,
@@ -371,15 +376,14 @@ updateTrustLine(
{
// VFALCO Where is the line being deleted?
// Clear the reserve of the sender, possibly delete the line!
auto const currentSponsor = getLedgerEntryReserveSponsor(
view, state.rawSle(), bSenderHigh ? sfHighSponsor : sfLowSponsor);
auto const currentSponsor =
getLedgerEntryReserveSponsor(view, state, bSenderHigh ? sfHighSponsor : sfLowSponsor);
decreaseOwnerCount(view, sle, currentSponsor, 1, j);
// Clear reserve flag.
state->clearFlag(senderReserveFlag);
removeSponsorFromLedgerEntry(
state.mutableRawSle(), !bSenderHigh ? sfLowSponsor : sfHighSponsor);
removeSponsorFromLedgerEntry(state, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
// Balance is zero, receiver reserve is clear.
if (!after && !state->isFlag(receiverReserveFlag))
@@ -414,7 +418,7 @@ issueIOU(
auto const index = keylet::trustLine(issue.account, account, issue.currency);
if (RippleStateEntryW state(index, view, j); state)
if (auto state = view.peek(index))
{
STAmount finalBalance = state->getFieldAmount(sfBalance);
@@ -447,7 +451,7 @@ issueIOU(
j);
}
state.update();
view.update(state);
return tesSUCCESS;
}
@@ -507,7 +511,7 @@ redeemIOU(
bool const bSenderHigh = account > issue.account;
if (RippleStateEntryW state(account, issue.account, issue.currency, view, j); state)
if (auto state = view.peek(keylet::trustLine(account, issue.account, issue.currency)))
{
STAmount finalBalance = state->getFieldAmount(sfBalance);
@@ -541,7 +545,7 @@ redeemIOU(
j);
}
state.update();
view.update(state);
return tesSUCCESS;
}
@@ -568,7 +572,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
if (isXRP(issue) || issue.account == account)
return tesSUCCESS;
RippleStateEntryR const trustLine(account, issue.account, issue.currency, view);
auto const trustLine = view.read(keylet::trustLine(account, issue.account, issue.currency));
// If account has no line, and this is a strong check, fail
if (!trustLine && authType == AuthType::StrongAuth)
return tecNO_LINE;
@@ -580,7 +584,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
{
if (trustLine)
{
if (trustLine.isAuthorized(account, issue.account))
if (trustLine->isFlag((account > issue.account) ? lsfLowAuth : lsfHighAuth))
return tesSUCCESS;
// A pseudo-account cannot submit transactions and only stores assets for the object
@@ -612,7 +616,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
auto const isRippleDisabled = [&](AccountID account) -> bool {
// Line might not exist, but some transfers can create it. If this
// is the case, just check the default ripple on the issuer account.
RippleStateEntryR const line(account, issue, view);
auto const line = view.read(keylet::trustLine(account, issue));
if (line)
{
bool const issuerHigh = issuerId > account;
@@ -739,7 +743,7 @@ removeEmptyHolding(
// If the account is the issuer, then no line should exist. Check anyway.
// If a line does exist, it will get deleted. If not, return success.
bool const accountIsIssuer = accountID == issue.account;
RippleStateEntryW line(accountID, issue, ctx.view, journal);
auto const line = ctx.view.peek(keylet::trustLine(accountID, issue));
if (!line)
return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND;
if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::kZero)
@@ -753,15 +757,14 @@ removeEmptyHolding(
if (!sleLowAccount)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const currentLowSponsor =
getLedgerEntryReserveSponsor(ctx.view, line.rawSle(), sfLowSponsor);
auto const currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor);
decreaseOwnerCount(ctx.view, sleLowAccount, currentLowSponsor, 1, journal);
// It's not really necessary to clear the reserve flag, since the line
// is about to be deleted, but this will make the metadata reflect an
// accurate state at the time of deletion.
line->clearFlag(lsfLowReserve);
removeSponsorFromLedgerEntry(line.mutableRawSle(), sfLowSponsor);
removeSponsorFromLedgerEntry(line, sfLowSponsor);
}
if (line->isFlag(lsfHighReserve))
@@ -771,15 +774,14 @@ removeEmptyHolding(
if (!sleHighAccount)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const currentHighSponsor =
getLedgerEntryReserveSponsor(ctx.view, line.rawSle(), sfHighSponsor);
auto const currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor);
decreaseOwnerCount(ctx.view, sleHighAccount, currentHighSponsor, 1, journal);
// It's not really necessary to clear the reserve flag, since the line
// is about to be deleted, but this will make the metadata reflect an
// accurate state at the time of deletion.
line->clearFlag(lsfHighReserve);
removeSponsorFromLedgerEntry(line.mutableRawSle(), sfHighSponsor);
removeSponsorFromLedgerEntry(line, sfHighSponsor);
}
return trustDelete(
@@ -793,7 +795,7 @@ removeEmptyHolding(
TER
deleteAMMTrustLine(
ApplyView& view,
RippleStateEntryW& sleState,
SLE::pointer sleState,
std::optional<AccountID> const& ammAccountID,
beast::Journal j)
{
@@ -823,13 +825,8 @@ deleteAMMTrustLine(
if (ammAccountID && (low != *ammAccountID && high != *ammAccountID))
return terNO_AMM;
auto const sponsorSle = getLedgerEntryReserveSponsor(
view, sleState.rawSle(), !ammLow ? sfLowSponsor : sfHighSponsor);
// trustDelete() drops the entry's SLE, so read the reserve flag first.
// trustDelete() does not change the flags.
auto const uFlags = !ammLow ? lsfLowReserve : lsfHighReserve;
bool const hasReserve = sleState->isFlag(uFlags);
auto const sponsorSle =
getLedgerEntryReserveSponsor(view, sleState, !ammLow ? sfLowSponsor : sfHighSponsor);
if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter))
{
@@ -837,7 +834,8 @@ deleteAMMTrustLine(
return ter;
}
if (!hasReserve)
auto const uFlags = !ammLow ? lsfLowReserve : lsfHighReserve;
if (!sleState->isFlag(uFlags))
return tecINTERNAL; // LCOV_EXCL_LINE
decreaseOwnerCount(view, !ammLow ? sleLow : sleHigh, sponsorSle, 1, j);
+15 -17
View File
@@ -7,7 +7,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -33,7 +32,6 @@
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <optional>
#include <string>
#include <variant>
@@ -267,7 +265,7 @@ checkDepositFreeze(
//
//------------------------------------------------------------------------------
static std::optional<RippleStateEntryR>
static SLE::const_pointer
getLineIfUsable(
ReadView const& view,
AccountID const& account,
@@ -276,11 +274,11 @@ getLineIfUsable(
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
RippleStateEntryR sle(account, issuer, currency, view);
auto sle = view.read(keylet::trustLine(account, issuer, currency));
if (!sle)
{
return std::nullopt;
return nullptr;
}
if (zeroIfFrozen == FreezeHandling::ZeroIfFrozen)
@@ -288,7 +286,7 @@ getLineIfUsable(
if (isFrozen(view, account, currency, issuer) ||
isDeepFrozen(view, account, currency, issuer))
{
return std::nullopt;
return nullptr;
}
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
@@ -298,7 +296,7 @@ getLineIfUsable(
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
return std::nullopt; // LCOV_EXCL_LINE
return nullptr; // LCOV_EXCL_LINE
}
if (sleIssuer->isFieldPresent(sfAMMID))
{
@@ -307,7 +305,7 @@ getLineIfUsable(
if (!sleAmm ||
isLPTokenFrozen(view, account, (*sleAmm)[sfAsset], (*sleAmm)[sfAsset2]))
{
return std::nullopt;
return nullptr;
}
}
}
@@ -318,7 +316,7 @@ getLineIfUsable(
// gated by featureMPTokensV2.
if (!isTesSuccess(canTransferLPToken(view, account, account, issuer)))
{
return std::nullopt;
return nullptr;
}
}
@@ -328,7 +326,7 @@ getLineIfUsable(
static STAmount
getTrustLineBalance(
ReadView const& view,
std::optional<RippleStateEntryR> const& sle,
SLE::ConstRef sle,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
@@ -338,7 +336,7 @@ getTrustLineBalance(
STAmount amount;
if (sle)
{
amount = (*sle)->getFieldAmount(sfBalance);
amount = sle->getFieldAmount(sfBalance);
bool const accountHigh = account > issuer;
auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit;
if (accountHigh)
@@ -348,7 +346,7 @@ getTrustLineBalance(
}
if (includeOppositeLimit)
{
amount += (*sle)->getFieldAmount(oppositeField);
amount += sle->getFieldAmount(oppositeField);
}
amount.get<Issue>().account = issuer;
}
@@ -388,7 +386,7 @@ accountHolds(
}
// IOU: Return balance on trust line modulo freeze
std::optional<RippleStateEntryR> const sle =
SLE::const_pointer const sle =
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
return getTrustLineBalance(view, sle, account, currency, issuer, returnSpendable, j);
@@ -715,7 +713,7 @@ directSendNoFeeIOU(
"xrpl::directSendNoFeeIOU : receiver is not XRP");
// If the line exists, modify it accordingly.
if (RippleStateEntryW sleRippleState(index, view, j); sleRippleState)
if (auto const sleRippleState = view.peek(index))
{
STAmount saBalance = sleRippleState->getFieldAmount(sfBalance);
@@ -761,11 +759,11 @@ directSendNoFeeIOU(
{
// Clear the reserve of the sender, possibly delete the line!
auto const currentSponsor = getLedgerEntryReserveSponsor(
view, sleRippleState.rawSle(), !bSenderHigh ? sfLowSponsor : sfHighSponsor);
view, sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
decreaseOwnerCount(view, view.peek(keylet::account(uSenderID)), currentSponsor, 1, j);
removeSponsorFromLedgerEntry(
sleRippleState.mutableRawSle(), !bSenderHigh ? sfLowSponsor : sfHighSponsor);
sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
// Clear reserve flag.
sleRippleState->clearFlag(senderReserveFlag);
@@ -793,7 +791,7 @@ directSendNoFeeIOU(
j);
}
sleRippleState.update();
view.update(sleRippleState);
return tesSUCCESS;
}
@@ -35,7 +35,6 @@
#include <atomic>
#include <cstddef>
#include <filesystem>
#include <format>
#include <functional>
#include <memory>
#include <stdexcept>
@@ -194,7 +193,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::format("Unable to set RocksDB bbt_options: {}", s.ToString()));
std::string("Unable to set RocksDB bbt_options: ") + s.ToString());
}
}
@@ -207,7 +206,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::format("Unable to set RocksDB options: {}", s.ToString()));
std::string("Unable to set RocksDB options: ") + s.ToString());
}
}
@@ -242,7 +241,7 @@ public:
if (!status.ok() || (localDb == nullptr))
{
Throw<std::runtime_error>(
std::format("Unable to open/create RocksDB: {}", status.ToString()));
std::string("Unable to open/create RocksDB: ") + status.ToString());
}
db.reset(localDb);
}
@@ -357,7 +356,7 @@ public:
auto ret = db->Write(options, &wb);
if (!ret.ok())
Throw<std::runtime_error>(std::format("storeBatch failed: {}", ret.ToString()));
Throw<std::runtime_error>("storeBatch failed: " + ret.ToString());
}
void
+2 -3
View File
@@ -14,7 +14,6 @@
#include <algorithm>
#include <cstdint>
#include <format>
#include <functional>
#include <limits>
#include <optional>
@@ -106,8 +105,8 @@ Permission::Permission()
if (type <= UINT16_MAX)
{
// LCOV_EXCL_START
Throw<std::logic_error>(std::format(
"Granular permission value must exceed the maximum uint16_t value: {}", name));
Throw<std::logic_error>(
"Granular permission value must exceed the maximum uint16_t value: " + name);
// LCOV_EXCL_STOP
}
}
+3 -5
View File
@@ -43,9 +43,8 @@ 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) ||
r->enabled(featureMPTokensV2));
bool const enableLargeNumbers =
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
// If enableLargeNumbers is true, then useRulesGuards must also return true.
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
// even though large numbers are _not_ used.
@@ -85,8 +84,7 @@ 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(featureMPTokensV2);
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0);
}
void
+5 -4
View File
@@ -37,7 +37,6 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <format>
#include <iterator>
#include <limits>
#include <memory>
@@ -1194,7 +1193,8 @@ 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>(
std::format("overflow: ({} * {}) / {}", multiplier, multiplicand, divisor));
"overflow: (" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
") / " + std::to_string(divisor));
}
return static_cast<uint64_t>(ret);
@@ -1215,8 +1215,9 @@ muldivRound(
if (ret > std::numeric_limits<std::uint64_t>::max())
{
Throw<std::overflow_error>(std::format(
"overflow: (({} * {}) + {}) / {}", multiplier, multiplicand, rounding, divisor));
Throw<std::overflow_error>(
"overflow: ((" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
") + " + std::to_string(rounding) + ") / " + std::to_string(divisor));
}
return static_cast<uint64_t>(ret);
+3 -2
View File
@@ -35,8 +35,9 @@ STLedgerEntry::STLedgerEntry(Keylet const& k) : STObject(sfLedgerEntry), key_(k.
if (format == nullptr)
{
Throw<std::runtime_error>(std::format(
"Attempt to create a SLE of unknown type {}", safeCast<std::uint16_t>(k.type)));
Throw<std::runtime_error>(
"Attempt to create a SLE of unknown type " +
std::to_string(safeCast<std::uint16_t>(k.type)));
}
set(format->getSOTemplate());
+1 -2
View File
@@ -19,7 +19,6 @@
#include <cstddef>
#include <cstdint>
#include <format>
#include <limits>
#include <ostream>
#include <stdexcept>
@@ -171,7 +170,7 @@ partsFromString(std::string const& number)
boost::smatch match;
if (!boost::regex_match(number, match, kReNumber))
Throw<std::runtime_error>(std::format("'{}' is not a number", number));
Throw<std::runtime_error>("'" + number + "' is not a number");
// Match fields:
// 0 = whole input
+3 -2
View File
@@ -59,8 +59,9 @@ getTxFormat(TxType type)
if (format == nullptr)
{
Throw<std::runtime_error>(std::format(
"Invalid transaction type {}", safeCast<std::underlying_type_t<TxType>>(type)));
Throw<std::runtime_error>(
"Invalid transaction type " +
std::to_string(safeCast<std::underlying_type_t<TxType>>(type)));
}
return format;
+2 -2
View File
@@ -9,8 +9,8 @@
#include <xrpl/protocol/Serializer.h>
#include <cstddef>
#include <format>
#include <stdexcept>
#include <string>
#include <utility>
namespace xrpl {
@@ -22,7 +22,7 @@ STVector256::STVector256(SerialIter& sit, SField const& name) : STBase(name)
if (slice.size() % UInt256::size() != 0)
{
Throw<std::runtime_error>(
std::format("Bad serialization for STVector256: {}", slice.size()));
"Bad serialization for STVector256: " + std::to_string(slice.size()));
}
auto const cnt = slice.size() / UInt256::size();
+1 -2
View File
@@ -70,8 +70,7 @@ STXChainBridge::STXChainBridge(SField const& name, json::Value const& v) : STBas
std::string const name = it.memberName();
if (!kBridgeJson.isMember(name))
{
Throw<std::runtime_error>(
std::format("STXChainBridge extra field detected: {}", name));
Throw<std::runtime_error>("STXChainBridge extra field detected: " + name);
}
}
return true;
+3 -3
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>(std::format("gFID: uncommon type out of range {}", type));
Throw<std::runtime_error>("gFID: uncommon type out of range " + std::to_string(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>(std::format("gFID: uncommon name out of range {}", name));
Throw<std::runtime_error>("gFID: uncommon name out of range " + std::to_string(name));
}
}
-50
View File
@@ -6,8 +6,6 @@
#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>
@@ -15,12 +13,10 @@
#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>
@@ -149,52 +145,6 @@ 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)
{
+4 -5
View File
@@ -27,7 +27,6 @@
#include <soci/sqlite3/soci-sqlite3.h> // IWYU pragma: keep
#include <format>
#include <memory>
namespace xrpl {
@@ -41,8 +40,8 @@ getSociSqliteInit(std::string const& name, std::string const& dir, std::string c
{
if (name.empty())
{
Throw<std::runtime_error>(std::format(
"Sqlite databases must specify a dir and a name. Name: {} Dir: {}", name, dir));
Throw<std::runtime_error>(
"Sqlite databases must specify a dir and a name. Name: " + name + " Dir: " + dir);
}
std::filesystem::path file(dir);
if (std::filesystem::is_directory(file))
@@ -57,7 +56,7 @@ getSociInit(BasicConfig const& config, std::string const& dbName)
auto const backendName = get(section, Keys::kBackend, "sqlite");
if (backendName != "sqlite")
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", backendName));
Throw<std::runtime_error>("Unsupported soci backend: " + backendName);
auto const path = config.legacy(Sections::kDatabasePath);
auto const ext = dbName == "validators" || dbName == "peerfinder" ? ".sqlite" : ".db";
@@ -102,7 +101,7 @@ open(soci::session& s, std::string const& beName, std::string const& connectionS
}
else
{
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", beName));
Throw<std::runtime_error>("Unsupported soci backend: " + beName);
}
}
+20 -67
View File
@@ -1,12 +1,10 @@
#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>
@@ -44,22 +42,6 @@ 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.
@@ -102,16 +84,6 @@ 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
@@ -173,21 +145,16 @@ 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 counted sets cannot be mutated
// Hold lock_ for the whole read so the three sets cannot be mutated
// mid-count by a concurrent (un)subscribe on this connection.
std::scoped_lock const sl(lock_);
return subscriptionCount(sl);
// Combined tally the per-connection cap is enforced against.
return normalSubscriptions_.size() + realTimeSubscriptions_.size() +
accountHistorySubscriptions_.size();
}
bool
@@ -199,10 +166,25 @@ 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_);
if (exceedsSubscriptionCap(subscriptionCount(sl), additional, cap))
std::size_t const current = normalSubscriptions_.size() + realTimeSubscriptions_.size() +
accountHistorySubscriptions_.size();
if (exceedsSubscriptionCap(current, additional, cap))
return false;
realTimeSubscriptions_.insert(proposedAccounts.begin(), proposedAccounts.end());
@@ -210,19 +192,6 @@ 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)
{
@@ -319,20 +288,4 @@ 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
+1 -2
View File
@@ -26,7 +26,6 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <format>
#include <functional>
#include <limits>
#include <mutex>
@@ -49,7 +48,7 @@ to_string(Manifest const& m)
return "Revocation Manifest " + mk;
if (!m.signingKey)
Throw<std::runtime_error>(std::format("No SigningKey in manifest {}", mk));
Throw<std::runtime_error>("No SigningKey in manifest " + mk);
return "Manifest " + mk + " (" + std::to_string(m.sequence) + ": " +
toBase58(TokenType::NodePublic, *m.signingKey) + ")";
+2 -3
View File
@@ -8,7 +8,6 @@
#include <algorithm>
#include <cstddef>
#include <format>
#include <optional>
#include <stdexcept>
#include <string>
@@ -108,10 +107,10 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
depth_ <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::getChildNodeID : maximum leaf depth");
if (depth_ >= SHAMap::kLeafDepth)
Throw<std::logic_error>(std::format("Request for child node ID of {}", to_string(*this)));
Throw<std::logic_error>("Request for child node ID of " + to_string(*this));
if (!isPrefixOf(id_))
Throw<std::logic_error>(std::format("Incorrect mask for {}", to_string(*this)));
Throw<std::logic_error>("Incorrect mask for " + to_string(*this));
SHAMapNodeID node{depth_ + 1, id_};
node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? branch : (branch << 4);
+24 -21
View File
@@ -18,7 +18,6 @@
#include <xrpl/shamap/SHAMapTxPlusMetaLeafNode.h>
#include <cstdint>
#include <format>
#include <stdexcept>
#include <string>
#include <type_traits>
@@ -31,8 +30,9 @@ SHAMapTreeNode::makeTransaction(Slice data, SHAMapHash const& hash, bool hashVal
{
if (data.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(std::format(
"Short TXN node: {} bytes (minimum {} required)", data.size(), kMinShaMapItemBytes));
Throw<std::runtime_error>(
"Short TXN node: " + std::to_string(data.size()) + " bytes (minimum " +
std::to_string(kMinShaMapItemBytes) + " required)");
}
auto item = makeShamapitem(sha512Half(HashPrefix::TransactionId, data), data);
@@ -52,25 +52,27 @@ SHAMapTreeNode::makeTransactionWithMeta(Slice data, SHAMapHash const& hash, bool
if (s.size() < tag.kBytes)
{
Throw<std::runtime_error>(std::format(
"Short TXN+MD node: {} bytes (minimum {} required for tag)", 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)");
}
// 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>(std::format(
"Short TXN+MD node: failed to read tag at offset {}", 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));
}
s.chop(tag.kBytes);
if (s.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(std::format(
"Short TXN+MD node: {} bytes after tag removal (minimum {} required)",
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)");
}
auto item = makeShamapitem(tag, s.slice());
@@ -90,15 +92,16 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
if (s.size() < tag.kBytes)
{
Throw<std::runtime_error>(std::format(
"Short AS node: {} bytes (minimum {} required for tag)", 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)");
}
// 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>(
std::format("Short AS node: failed to read tag at offset {}", s.size() - tag.kBytes));
"Short AS node: failed to read tag at offset " + std::to_string(s.size() - tag.kBytes));
}
s.chop(tag.kBytes);
@@ -108,10 +111,9 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
if (s.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(std::format(
"Short AS node: {} bytes after tag removal (minimum {} required)",
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)");
}
auto item = makeShamapitem(tag, s.slice());
@@ -150,7 +152,7 @@ SHAMapTreeNode::makeFromWire(Slice rawNode)
if (type == kWireTypeTransactionWithMeta)
return makeTransactionWithMeta(rawNode, hash, hashValid);
Throw<std::runtime_error>(std::format("wire: Unknown type ({})", type));
Throw<std::runtime_error>("wire: Unknown type (" + std::to_string(type) + ")");
}
SHAMapTreeNodePtr
@@ -181,8 +183,9 @@ SHAMapTreeNode::makeFromPrefix(Slice rawNode, SHAMapHash const& hash)
if (type == HashPrefix::TxNode)
return makeTransactionWithMeta(rawNode, hash, hashValid);
Throw<std::runtime_error>(std::format(
"prefix: unknown type ({})", safeCast<std::underlying_type_t<HashPrefix>>(type)));
Throw<std::runtime_error>(
"prefix: unknown type (" +
std::to_string(safeCast<std::underlying_type_t<HashPrefix>>(type)) + ")");
}
std::string
+1 -2
View File
@@ -11,7 +11,6 @@
#include <xrpl/json/to_string.h> // IWYU pragma: keep
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
@@ -1278,7 +1277,7 @@ removeDeletedTrustLines(
for (auto const& index : trustLines)
{
if (RippleStateEntryW sleState(Keylet(ltRIPPLE_STATE, index), view, viewJ);
if (auto const sleState = view.peek({ltRIPPLE_STATE, index});
!isTesSuccess(deleteAMMTrustLine(view, sleState, std::nullopt, viewJ)))
{
JLOG(viewJ.error()) << "removeDeletedTrustLines: failed to delete AMM trustline";
+14 -56
View File
@@ -41,7 +41,6 @@ constexpr auto kConfidentialMptTxTypes = std::to_array<TxType>({
ttCONFIDENTIAL_MPT_MERGE_INBOX,
ttCONFIDENTIAL_MPT_CLAWBACK,
ttCONFIDENTIAL_MPT_MIRROR_UPDATE,
ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE,
});
// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant
@@ -74,7 +73,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
// only meaningful post-fixCleanup3_2_0 (the field is never set
// pre-amendment, and the holding-deletion rule does not apply).
// Skip both blocks when the amendment is off so we avoid wasted work
// on the hot path, except where noted for fixCleanup3_5_0 below.
// on the hot path.
bool const fix320Enabled = isFeatureEnabled(fixCleanup3_2_0);
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
@@ -109,10 +108,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
if (isDelete)
{
mptokensDeleted_++;
// deletedHoldings_ also feeds finalize()'s erase-time public
// balance check, gated on fixCleanup3_5_0 independently of
// fixCleanup3_2_0.
if (fix320Enabled || isFeatureEnabled(fixCleanup3_5_0))
if (fix320Enabled)
deletedHoldings_.push_back(after);
}
else if (!before)
@@ -199,19 +195,6 @@ ValidMPTIssuance::finalize(
return false;
}
// Deleting an MPToken with a non-zero MPTAmount is rejected.
if (rules.enabled(fixCleanup3_5_0))
{
for (auto const& sleHolding : deletedHoldings_)
{
if (sleHolding->getType() == ltMPTOKEN && sleHolding->getFieldU64(sfMPTAmount) > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPToken deleted with non-zero balance";
return false;
}
}
}
if (isTesSuccess(result) || (mptV2Enabled && result == tecINCOMPLETE))
{
[[maybe_unused]]
@@ -596,27 +579,16 @@ ValidConfidentialMPToken::visitEntry(
change.mptAmountDelta =
subtractMPTAmountDelta(change.mptAmountDelta, before->getFieldU64(sfMPTAmount));
// Cannot delete MPToken with non-zero confidential state.
// Cannot delete MPToken with non-zero confidential state or non-zero public amount
if (isDelete)
{
// changes_ is keyed by issuance, so sibling holders erased by the
// same transaction share this entry. Only ever set these flags,
// never clear them, or an empty sibling visited later would mask
// a funded MPToken.
// Retired by fixCleanup3_5_0, which moved the public balance
// check to ValidMPTIssuance::finalize. Kept pre-amendment for
// consensus safety: a non-zero public balance used to feed the
// confidential gate below, rejecting the erase whenever the
// issuance's COA was non-zero, and already-validated ledgers
// depend on that.
if (!isFeatureEnabled(fixCleanup3_5_0) && before->getFieldU64(sfMPTAmount) > 0)
changes_[id].deletedWithBalanceBefore = true;
if (before->isFieldPresent(sfConfidentialBalanceSpending) ||
bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
before->isFieldPresent(sfConfidentialBalanceInbox) ||
before->isFieldPresent(sfIssuerEncryptedBalance) ||
before->isFieldPresent(sfAuditorEncryptedBalance))
before->isFieldPresent(sfAuditorEncryptedBalance);
if (hasPublicBalance || hasEncryptedFields)
changes_[id].deletedWithEncrypted = true;
}
}
@@ -722,8 +694,6 @@ ValidConfidentialMPToken::finalize(
if (result != tesSUCCESS)
return true;
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
for (auto const& [id, checks] : changes_)
{
// Find the MPTokenIssuance
@@ -738,20 +708,8 @@ ValidConfidentialMPToken::finalize(
if (!issuance)
continue;
// Cannot delete MPToken with non-zero confidential state.
//
// Before fixCleanup3_5_0 this gate also absorbed the pre-transaction
// public balance, so any drain-then-erase of an MPToken -- an
// AMMWithdraw of the whole pool, a LoanBrokerDelete returning cover --
// was rejected whenever some unrelated holder of the same issuance
// held a confidential balance. The COA gate itself is correct for
// ciphertext and mirrors MPTokenAuthorize::preclaim; only the public
// balance leg was misplaced.
bool const deletedWithEncrypted = fix350Enabled
? checks.deletedWithEncrypted
: (checks.deletedWithEncrypted || checks.deletedWithBalanceBefore);
if (deletedWithEncrypted)
// Cannot delete MPToken with non-zero confidential state
if (checks.deletedWithEncrypted)
{
if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
{
@@ -826,10 +784,10 @@ ValidConfidentialMPToken::finalize(
return false;
}
// Reaching here means this confidential MPT transaction left coaDelta
// unmodified (e.g. ConfidentialMPTSend, ConfidentialMPTMergeInbox, or
// ConfidentialMPTHolderKeyUpdate/ConfidentialMPTMirrorUpdate, none of which touch
// sfConfidentialOutstandingAmount), so it must not modify sfOutstandingAmount either.
// Among confidential MPT transactions, only ConfidentialMPTSend and
// ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT
// transaction reaches here, it must be one of these two types, neither of which will
// modify sfOutstandingAmount
if (checks.outstandingDelta != 0)
{
JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed "
+1 -2
View File
@@ -7,7 +7,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -1450,7 +1449,7 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
auto const err = book_.in.visit(
[&](Issue const& issue) -> std::optional<TER> {
RippleStateEntryR const sle(*prev, cur, issue.currency, view);
auto sle = view.read(keylet::trustLine(*prev, cur, issue.currency));
if (!sle)
return terNO_LINE;
if (sle->isFlag((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple))
+2 -3
View File
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -346,7 +345,7 @@ DirectIPaymentStep::quality(ReadView const& sb, QualityDirection qDir) const
if (src_ == dst_)
return QUALITY_ONE;
RippleStateEntryR const sle(dst_, src_, currency_, sb);
auto const sle = sb.read(keylet::trustLine(dst_, src_, currency_));
if (!sle)
return QUALITY_ONE;
@@ -422,7 +421,7 @@ DirectIPaymentStep::check(StrandContext const& ctx, SLE::ConstRef sleSrc) const
// Since this is a payment a trust line must be present. Perform all
// trust line related checks.
{
RippleStateEntryR const sleLine(src_, dst_, currency_, ctx.view);
auto const sleLine = ctx.view.read(keylet::trustLine(src_, dst_, currency_));
if (!sleLine)
{
JLOG(j_.trace()) << "DirectStepI: No credit line. " << *this;
@@ -7,7 +7,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -199,7 +198,8 @@ CheckCash::preclaim(PreclaimContext const& ctx)
return value.asset().visit(
[&](Issue const& issue) -> TER {
Currency const currency{issue.currency};
RippleStateEntryR const sleTrustLine(dstId, issuerId, currency, ctx.view);
auto const sleTrustLine =
ctx.view.read(keylet::trustLine(dstId, issuerId, currency));
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
if (!sleIssuer)
@@ -506,7 +506,7 @@ CheckCash::doApply()
// would exceed the limit on their trust line. So we
// tweak the trust line limits before calling flow and
// then restore the trust line limits afterwards.
RippleStateEntryW sleTrustLine(*trustLineKey, psb, j_);
auto const sleTrustLine = psb.peek(*trustLineKey);
if (!sleTrustLine)
return tecNO_LINE;
@@ -552,7 +552,7 @@ CheckCash::doApply()
if (trustLineKey)
{
SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit;
if (RippleStateEntryW sleTrustLine(*trustLineKey, psb); sleTrustLine)
if (auto const sleTrustLine = psb.peek(*trustLineKey))
sleTrustLine->at(tweakedLimit) = savedLimit;
}
});
@@ -4,7 +4,6 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -127,7 +126,8 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
if (issuerId != srcId)
{
// Check if the issuer froze the line
RippleStateEntryR const sleTrust(srcId, issuerId, issue.currency, ctx.view);
auto const sleTrust =
ctx.view.read(keylet::trustLine(srcId, issuerId, issue.currency));
if (sleTrust &&
sleTrust->isFlag((issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze))
{
@@ -138,7 +138,8 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
if (issuerId != dstId)
{
// Check if dst froze the line.
RippleStateEntryR const sleTrust(issuerId, dstId, issue.currency, ctx.view);
auto const sleTrust =
ctx.view.read(keylet::trustLine(issuerId, dstId, issue.currency));
if (sleTrust &&
sleTrust->isFlag((dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze))
{
+4 -4
View File
@@ -5,7 +5,6 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -276,7 +275,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
// LP Token already exists. (should not happen)
auto const lptIss = ammLPTIssue(amount.asset(), amount2.asset(), accountId);
if (RippleStateEntryR(accountId, lptIss, sb))
if (sb.read(keylet::trustLine(accountId, lptIss)))
{
JLOG(j.error()) << "AMM Instance: LP Token already exists.";
return {tecDUPLICATE, false};
@@ -359,7 +358,8 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
// Set AMM flag on AMM trustline
if (!isXRP(amount))
{
RippleStateEntryW sleRippleState(accountId, issue, sb, j);
SLE::pointer const sleRippleState =
sb.peek(keylet::trustLine(accountId, issue));
if (!sleRippleState)
{
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -367,7 +367,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
auto const flags = sleRippleState->getFlags();
sleRippleState->setFieldU32(sfFlags, flags | lsfAMMNode);
sleRippleState.update();
sb.update(sleRippleState);
}
return tesSUCCESS;
});
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -234,7 +233,8 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
{
auto const lpIssue = (*ammSle)[sfLPTokenBalance].get<Issue>();
// Adjust the reserve if LP doesn't have LPToken trustline
RippleStateEntryR const sle(accountID, lpIssue.account, lpIssue.currency, ctx.view);
auto const sle =
ctx.view.read(keylet::trustLine(accountID, lpIssue.account, lpIssue.currency));
if (xrpLiquid(ctx.view, accountID, !sle, ctx.j) >= deposit)
return TER(tesSUCCESS);
if (sle)
@@ -584,7 +584,8 @@ AMMDeposit::deposit(
{
auto const& lpIssue = lpTokensDeposit.get<Issue>();
// Adjust the reserve if LP doesn't have LPToken trustline
RippleStateEntryR const sle(accountID_, lpIssue.account, lpIssue.currency, view);
auto const sle =
view.read(keylet::trustLine(accountID_, lpIssue.account, lpIssue.currency));
if (xrpLiquid(view, accountID_, !sle, j_) >= depositAmount)
return tesSUCCESS;
}
@@ -9,7 +9,6 @@
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
@@ -308,7 +307,7 @@ OfferCreate::checkAcceptAsset(
return asset.visit(
[&](Issue const& issue) -> TER {
auto const& issuer = issue.getIssuer();
RippleStateEntryR const trustLine(id, issuer, issue.currency, view);
auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency));
// Check if the issuer has lsfDisallowIncomingTrustline set.
// If so, the account must already have a trustline to receive tokens.
@@ -7,7 +7,6 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -215,7 +214,7 @@ escrowCreatePreclaimHelper<Issue>(
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
RippleStateEntryR const sleRippleState(account, issuer, issue.currency, ctx.view);
auto const sleRippleState = ctx.view.read(keylet::trustLine(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -318,7 +317,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) &&
(nft::getFlags(tokenID) & nft::kFlagCreateTrustLines) == 0 &&
nftMinter != amount.getIssuer() &&
!RippleStateEntryR(nftMinter, amount.get<Issue>(), ctx.view))
!ctx.view.read(keylet::trustLine(nftMinter, amount.get<Issue>())))
return tecNO_LINE;
// Check that the issuer is allowed to receive IOUs.
@@ -6,7 +6,6 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -330,7 +329,7 @@ Payment::checkGranularSemantics(
if (issue.getIssuer() != account && issue.getIssuer() != destination)
return terNO_DELEGATE_PERMISSION;
RippleStateEntryR const sle(account, destination, issue.currency, view);
auto const sle = view.read(keylet::trustLine(account, destination, issue.currency));
if (!sle)
return terNO_DELEGATE_PERMISSION;
@@ -2,7 +2,6 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -109,8 +108,8 @@ preclaimHelper<Issue>(
if (!sleIssuer.isFlag(lsfAllowTrustLineClawback) || sleIssuer.isFlag(lsfNoFreeze))
return tecNO_PERMISSION;
RippleStateEntryR const sleRippleState(
holder, issuer, clawAmount.get<Issue>().currency, ctx.view);
auto const sleRippleState =
ctx.view.read(keylet::trustLine(holder, issuer, clawAmount.get<Issue>().currency));
if (!sleRippleState)
return tecNO_LINE;
@@ -1,220 +0,0 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <bit>
#include <cstdint>
namespace xrpl {
bool
ConfidentialMPTHolderKeyUpdate::checkExtraFeatures(PreflightContext const& ctx)
{
// Holder key update is only meaningful when confidential transfers are enabled.
return ctx.rules.enabled(featureConfidentialTransfer);
}
std::uint32_t
ConfidentialMPTHolderKeyUpdate::getFlagsMask(PreflightContext const& ctx)
{
return tfConfidentialMPTHolderKeyUpdateMask;
}
NotTEC
ConfidentialMPTHolderKeyUpdate::preflight(PreflightContext const& ctx)
{
bool const rotation = ctx.tx.isFlag(tfHolderKeyRotation);
bool const recovery = ctx.tx.isFlag(tfHolderKeyRecovery);
bool const cancel = ctx.tx.isFlag(tfCancelRecovery);
// Exactly one of the three mode flags must be set.
static constexpr auto modeFlags = tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery;
if (std::popcount(ctx.tx.getFlags() & modeFlags) != 1)
return temINVALID_FLAG;
// The issuer cannot hold confidential balances.
if (ctx.tx[sfAccount] == MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
return temMALFORMED;
bool const hasHolderKey = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
bool const hasSpending = ctx.tx.isFieldPresent(sfConfidentialBalanceSpending);
bool const hasInbox = ctx.tx.isFieldPresent(sfConfidentialBalanceInbox);
bool const hasProof = ctx.tx.isFieldPresent(sfZKProof);
if (cancel)
{
// Cancel mode only revokes a pending recovery authorization; it
// carries no key material, balances, or proof.
if (hasHolderKey || hasSpending || hasInbox || hasProof)
return temMALFORMED;
return tesSUCCESS;
}
// Rotation and Recovery both require a holder key and a proof.
if (!hasHolderKey || !hasProof)
return temMALFORMED;
// Rotation mode requires re-encrypted balances; Recovery mode must not
// provide them since the holder cannot decrypt the current ones.
if (rotation && (!hasSpending || !hasInbox))
return temMALFORMED;
if (recovery && (hasSpending || hasInbox))
return temMALFORMED;
if (hasSpending &&
ctx.tx[sfConfidentialBalanceSpending].length() != kEcGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (hasInbox && ctx.tx[sfConfidentialBalanceInbox].length() != kEcGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
return temMALFORMED;
if (hasSpending && !isValidCiphertext(ctx.tx[sfConfidentialBalanceSpending]))
return temBAD_CIPHERTEXT;
if (hasInbox && !isValidCiphertext(ctx.tx[sfConfidentialBalanceInbox]))
return temBAD_CIPHERTEXT;
return tesSUCCESS;
}
XRPAmount
ConfidentialMPTHolderKeyUpdate::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
}
TER
ConfidentialMPTHolderKeyUpdate::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
return tecNO_PERMISSION;
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox))
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFlag(tfCancelRecovery))
{
// Nothing to cancel if no recovery is pending.
if (!sleMptoken->isFieldPresent(sfRecoveryKey))
return tecNO_PERMISSION;
return tesSUCCESS;
}
if (ctx.tx[sfHolderEncryptionKey] == (*sleMptoken)[sfHolderEncryptionKey])
return tecDUPLICATE;
// Recovery mode: reject if a recovery is already pending
if (ctx.tx.isFlag(tfHolderKeyRecovery) && sleMptoken->isFieldPresent(sfRecoveryKey))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
ConfidentialMPTHolderKeyUpdate::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
if (!sleMptoken)
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTHolderKeyUpdate::doApply : preclaim already validated the "
"MPToken exists");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (ctx_.tx.isFlag(tfCancelRecovery))
{
// The holder revokes their pending recovery authorization; the
// current key and balances are left untouched.
sleMptoken->makeFieldAbsent(sfRecoveryKey);
view().update(sleMptoken);
return tesSUCCESS;
}
auto const newPubKey = ctx_.tx[sfHolderEncryptionKey];
if (ctx_.tx.isFlag(tfHolderKeyRotation))
{
// Replace the key and balances in rotation mode. Rotation proves the
// holder still has the old key, so any pending recovery is cancelled.
(*sleMptoken)[sfHolderEncryptionKey] = newPubKey;
(*sleMptoken)[sfConfidentialBalanceSpending] = ctx_.tx[sfConfidentialBalanceSpending];
(*sleMptoken)[sfConfidentialBalanceInbox] = ctx_.tx[sfConfidentialBalanceInbox];
sleMptoken->makeFieldAbsent(sfRecoveryKey);
incrementConfidentialVersion(*sleMptoken);
}
else if (ctx_.tx.isFlag(tfHolderKeyRecovery))
{
// Recovery mode: the holder cannot decrypt their current balances,
// so only the pending recovery key is recorded. The balances are
// rewritten separately by the issuer via ConfidentialMPTRecoverBalance.
(*sleMptoken)[sfRecoveryKey] = newPubKey;
}
else
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::ConfidentialMPTHolderKeyUpdate::doApply : invalid mode");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
view().update(sleMptoken);
return tesSUCCESS;
}
void
ConfidentialMPTHolderKeyUpdate::visitInvariantEntry(bool, SLE::ConstRef, SLE::ConstRef)
{
}
bool
ConfidentialMPTHolderKeyUpdate::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl
+14 -13
View File
@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
@@ -130,8 +129,9 @@ TrustSet::checkGranularSemantics(
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
{
auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount);
RippleStateEntryR const sleRippleState(
tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get<Issue>().currency, view);
auto const sleRippleState = view.read(
keylet::trustLine(
tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get<Issue>().currency));
// granular permissions are not allowed to create a trustline
if (!sleRippleState)
@@ -258,7 +258,7 @@ TrustSet::preclaim(PreclaimContext const& ctx)
bool const bHigh = id > uDstAccountID;
// Fetching current state of trust line
RippleStateEntryR const sleRippleState(id, uDstAccountID, currency, ctx.view);
auto const sleRippleState = ctx.view.read(keylet::trustLine(id, uDstAccountID, currency));
std::uint32_t uFlags = sleRippleState ? sleRippleState->getFieldU32(sfFlags) : 0u;
// Computing expected trust line state
uFlags = computeFreezeFlags(
@@ -361,7 +361,8 @@ TrustSet::doApply()
STAmount saLimitAllow = saLimitAmount;
saLimitAllow.get<Issue>().account = accountID_;
RippleStateEntryW sleRippleState(accountID_, uDstAccountID, currency, view(), j_);
SLE::pointer const sleRippleState =
view().peek(keylet::trustLine(accountID_, uDstAccountID, currency));
if (sleRippleState)
{
@@ -518,9 +519,9 @@ TrustSet::doApply()
bool bReserveIncrease = false;
auto const currentHighSponsor =
getLedgerEntryReserveSponsor(view(), sleRippleState.rawSle(), sfHighSponsor);
getLedgerEntryReserveSponsor(view(), sleRippleState, sfHighSponsor);
auto const currentLowSponsor =
getLedgerEntryReserveSponsor(view(), sleRippleState.rawSle(), sfLowSponsor);
getLedgerEntryReserveSponsor(view(), sleRippleState, sfLowSponsor);
if (bSetAuth)
{
@@ -551,7 +552,7 @@ TrustSet::doApply()
increaseOwnerCount(view(), sleLowAccount, lowSponsor, 1, viewJ);
uFlagsOut |= lsfLowReserve;
addSponsorToLedgerEntry(sleRippleState.mutableRawSle(), lowSponsor, sfLowSponsor);
addSponsorToLedgerEntry(sleRippleState, lowSponsor, sfLowSponsor);
if (!bHigh)
bReserveIncrease = true;
@@ -563,7 +564,7 @@ TrustSet::doApply()
decreaseOwnerCount(view(), sleLowAccount, currentLowSponsor, 1, viewJ);
uFlagsOut &= ~lsfLowReserve;
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfLowSponsor);
removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor);
}
if (bHighReserveSet && !bHighReserved)
@@ -593,7 +594,7 @@ TrustSet::doApply()
increaseOwnerCount(view(), sleHighAccount, highSponsor, 1, viewJ);
uFlagsOut |= lsfHighReserve;
addSponsorToLedgerEntry(sleRippleState.mutableRawSle(), highSponsor, sfHighSponsor);
addSponsorToLedgerEntry(sleRippleState, highSponsor, sfHighSponsor);
if (bHigh)
bReserveIncrease = true;
@@ -605,7 +606,7 @@ TrustSet::doApply()
decreaseOwnerCount(view(), sleHighAccount, currentHighSponsor, 1, viewJ);
uFlagsOut &= ~lsfHighReserve;
removeSponsorFromLedgerEntry(sleRippleState.mutableRawSle(), sfHighSponsor);
removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor);
}
if (uFlagsIn != uFlagsOut)
@@ -641,7 +642,7 @@ TrustSet::doApply()
}
else
{
sleRippleState.update();
view().update(sleRippleState);
JLOG(j_.trace()) << "Modify ripple line";
}
@@ -667,7 +668,7 @@ TrustSet::doApply()
}
else
{
sleRippleState.update();
view().update(sleRippleState);
JLOG(j_.trace()) << "Modify ripple line";
}
+10 -15
View File
@@ -31,15 +31,6 @@
namespace xrpl::test {
class AMMClawbackMPT_test : public beast::unit_test::Suite
{
// SingleAssetVault, LendingProtocol and MPTokensV2 enable the large
// Number mantissa.
static bool
largeMantissa(FeatureBitset const& features)
{
return features[featureSingleAssetVault] || features[featureLendingProtocol] ||
features[featureMPTokensV2];
}
void
testInvalidRequest(FeatureBitset features)
{
@@ -1733,7 +1724,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
.asString();
auto const lpTokenBalance =
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
if (largeMantissa(features))
if (features[featureSingleAssetVault] || features[featureLendingProtocol])
{
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741");
}
@@ -1750,14 +1741,16 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
BEAST_EXPECT(!amm.ammExists());
}
else if (features[fixAMMv1_3] && largeMantissa(features))
else if (
features[fixAMMv1_3] &&
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
{
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
// Without the Rounding feature and with new Number a dust pool
// amount remains
BEAST_EXPECT(amm.ammExists());
}
else if (!largeMantissa(features))
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
{
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
BEAST_EXPECT(amm.ammExists());
@@ -1801,7 +1794,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
.asString();
auto const lpTokenBalance =
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
if (!largeMantissa(features))
if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
{
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.414213562374");
}
@@ -1818,14 +1811,16 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
BEAST_EXPECT(!amm.ammExists());
}
else if (features[fixAMMv1_3] && largeMantissa(features))
else if (
features[fixAMMv1_3] &&
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
{
// Without the Rounding feature and with new Number a dust pool
// amount remains
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
BEAST_EXPECT(amm.ammExists());
}
else if (!largeMantissa(features))
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
{
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
BEAST_EXPECT(amm.ammExists());
+2 -2
View File
@@ -2857,8 +2857,8 @@ class AMMClawback_test : public beast::unit_test::Suite
{
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
FeatureBitset const all = jtx::testableAmendments() - featureSingleAssetVault -
featureLendingProtocol - featureMPTokensV2;
FeatureBitset const all =
jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
testInvalidRequest(all);
testInvalidRequest(all - featureMPTokensV2);
+1 -1
View File
@@ -116,7 +116,7 @@ private:
Txflags(tfPartialPayment));
BEAST_EXPECT(ammCarol.expectBalances(
btc(1'001'000'000'374'814), eth(100'000'000'000'000'000), ammCarol.tokens()));
btc(1'001'000'000'374'816), eth(100'000'000'000'000'000), ammCarol.tokens()));
env.require(Balance(bob_, eth(200'100'000'000'000'000)));
BEAST_EXPECT(isOffer(env, carol_, btc(49'000'000'000'000), XRP(49)));
+35 -16
View File
@@ -73,8 +73,7 @@ class AMMExtended_test : public jtx::AMMTest
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
FeatureBitset const all_{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
featureMPTokensV2};
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
private:
void
@@ -268,20 +267,39 @@ private:
{features});
// tfPassive -- place the offer without crossing it.
testAMM(
[&](AMM& ammAlice, Env& env) {
// Carol creates a passive offer that could cross AMM.
// Carol's offer should stay in the ledger.
env(offer(carol_, XRP(100), USD(100), tfPassive));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
if (features[featureMPTokensV2])
{
Env env{*this, features};
fund(env, gw_, {alice_, carol_}, XRP(30'000'000), {USD(30'000'000)});
AMM const ammAlice(env, alice_, XRP(10'100'000), USD(10'000'000));
// Scale the exact-quality fixture up so the visual relationship
// stays clear: the passive CLOB offer has the same 1:1 quality as
// the generated AMM offer, so it should not cross.
env(offer(carol_, XRP(100'000), USD(100'000), tfPassive));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(XRP(10'100'000), USD(10'000'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), USD(100'000)}}}));
}
else
{
testAMM(
[&](AMM& ammAlice, Env& env) {
// Carol creates a passive offer that could cross AMM.
// Carol's offer should stay in the ledger.
env(offer(carol_, XRP(100), USD(100), tfPassive));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
}
// tfPassive -- cross only offers of better quality.
testAMM(
@@ -1432,6 +1450,7 @@ private:
testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3);
testEnforceNoRipple(all_);
testFillModes(all_);
testFillModes(all_ - featureMPTokensV2);
testOfferCrossWithXRP(all_);
testOfferCrossWithLimitOverride(all_);
testCurrencyConversionEntire(all_);
+23 -26
View File
@@ -1451,14 +1451,12 @@ private:
auto carolXRP = env.balance(carol_, XRP);
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
// The exact deposit is 201; rounding in the pool's favor at
// the large mantissa takes one more unit.
ammAlice.deposit(carol_, 100'000, MPT(ammAlice[1])(205));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), MPT(ammAlice[1])(10'202), IOUAmount{10'100'000, 0}));
XRP(10'000), MPT(ammAlice[1])(10'201), IOUAmount{10'100'000, 0}));
env.require(Balance(carol_, carolXRP - drops(baseFee)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(202)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(201)));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1469,13 +1467,11 @@ private:
auto carolXRP = env.balance(carol_, XRP);
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
// The exact deposit is 201 XRP; rounding in the pool's favor
// at the large mantissa takes one more drop.
ammAlice.deposit(carol_, 100'000, XRP(205));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{10'201'000'001}, MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
XRP(10'201), MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
env.require(Balance(carol_, carolXRP - XRPAmount{201'000'001} - drops(baseFee)));
env.require(Balance(carol_, carolXRP - XRP(201) - drops(baseFee)));
env.require(Balance(carol_, carolMPT));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1555,10 +1551,10 @@ private:
STAmount{ammAlice.lptIssue(), 2004, -6});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
env.require(Balance(carol_, carolXRP - drops(baseFee)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1577,10 +1573,10 @@ private:
STAmount{ammAlice.lptIssue(), 2004, -6});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
env.require(Balance(carol_, carolXRP - drops(baseFee)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1801,9 +1797,9 @@ private:
ammAlice.deposit(gw_, MPT(ammAlice[0])(1000));
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(1'011'000),
MPT(ammAlice[0])(1'010'999),
MPT(ammAlice[1])(1'010'000),
IOUAmount{1'010'499'876298854, -9}));
IOUAmount{1'010'499'376546071, -9}));
},
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
@@ -2216,7 +2212,7 @@ private:
if (env.enabled(fixAMMv1_3))
{
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(1), STAmount{USD, 1, -12}, IOUAmount{1, -9}));
MPT(ammAlice[0])(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
}
},
{{gAmmmpt(10'000'000'000), USD(10'000)}},
@@ -2545,12 +2541,11 @@ private:
// XRP amount to withdraw is 0
ammAlice.withdraw(
alice_, IOUAmount{1, -5}, std::nullopt, std::nullopt, Ter(tecAMM_FAILED));
// Calculated tokens to withdraw are 0: 1e-16 of 10,000 USD is
// below the large Number mantissa's precision
// Calculated tokens to withdraw are 0
ammAlice.withdraw(
alice_,
std::nullopt,
STAmount{USD, 1, -16},
STAmount{USD, 1, -11},
std::nullopt,
Ter(tecAMM_INVALID_TOKENS));
ammAlice.deposit(carol_, STAmount{USD, 1, -10});
@@ -2712,7 +2707,7 @@ private:
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(9000'000001),
MPT(ammAlice[1])(10'000),
IOUAmount{9'486'832'980505138, -9}));
IOUAmount{9'486'832'98050514, -8}));
},
{{XRP(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2720,7 +2715,7 @@ private:
// single withdraw MPT from XRP/MPT
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'980505138, -9}));
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'98050514, -8}));
},
{{XRP(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2728,7 +2723,9 @@ private:
// single withdraw IOU from IOU/MPT
ammAlice.withdraw(alice_, USD(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
USD(9'000), MPT(ammAlice[1])(10'000), IOUAmount{9486'832980505138, -12}));
STAmount{USD, UINT64_C(9000'000000000004), -12},
MPT(ammAlice[1])(10'000),
IOUAmount{9486'83298050514, -11}));
},
{{USD(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2736,7 +2733,7 @@ private:
// single withdraw MPT from IOU/MPT
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'832980505138, -12}));
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'83298050514, -11}));
},
{{USD(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2746,7 +2743,7 @@ private:
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(9001),
MPT(ammAlice[1])(10'000),
IOUAmount{9486'832980505138, -12}));
IOUAmount{9486'83298050514, -11}));
},
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
@@ -3027,7 +3024,7 @@ private:
ammAlice.deposit(carol_, 1'000);
ammAlice.withdrawAll(carol_, USD(0));
BEAST_EXPECT(ammAlice.expectBalances(
STAmount{USD, UINT64_C(9'090'909090909091), -12},
STAmount{USD, UINT64_C(9'090'909090909092), -12},
MPT(ammAlice[1])(11'000),
IOUAmount{10'000}));
},
@@ -3252,7 +3249,7 @@ private:
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(10'000'000'000),
STAmount{USD, 1, -11},
IOUAmount{316227766, -9}));
IOUAmount{316227765, -9}));
},
{{gAmmmpt(10'000'000'000), USD(10'000)}});
// XRP
@@ -5963,7 +5960,7 @@ private:
if (!features[fixAMMv1_3])
{
BEAST_EXPECT(tokensFee == IOUAmount(98'019'80198019, -8));
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933557, -8));
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933556, -8));
}
else
{
+69 -8
View File
@@ -84,8 +84,7 @@ private:
{
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
featureMPTokensV2;
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
}
// Seed from the local testableAmendments() which strips SAV and Lending.
@@ -3811,7 +3810,7 @@ private:
BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
}
else
else if (!features[featureMPTokensV2])
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
@@ -3826,6 +3825,30 @@ private:
env.balance(carol_, USD) ==
STAmount(USD, UINT64_C(29'949'94999999494), -11));
}
else
{
// Post-amendment the transfer fee is taken into account
// when calculating the limit out based on limitQuality.
// This increases the limitQuality and decreases
// the limit out. Consequently, AMM offer size is decreased,
// and the quality is increased, matching the overall
// quality.
// AMM offer ~50USD/91XRP
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'910),
STAmount{USD, UINT64_C(549'99999945), -8},
amm.tokens()));
// Offer ~91XRP/50USD
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
// Carol pays 0.1% fee on ~50USD =~ 0.05USD
BEAST_EXPECT(
env.balance(carol_, USD) ==
STAmount(USD, UINT64_C(29'949'95000060055), -11));
}
},
{{XRP(1'000), USD(500)}},
0,
@@ -6475,7 +6498,7 @@ private:
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
}
else
else if (!features[featureMPTokensV2])
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
@@ -6488,6 +6511,19 @@ private:
{{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'910),
STAmount{USD, UINT64_C(549'99999945), -8},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
}
}
// There is no blocking offer, the same AMM liquidity is consumed
@@ -6499,10 +6535,30 @@ private:
AMM const amm(env, alice_, XRP(1'000), USD(500));
env(offer(carol_, XRP(100), USD(55)));
env.close();
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
BEAST_EXPECT(expectOffers(
env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
if (!features[featureMPTokensV2])
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
STAmount{USD, UINT64_C(550'000000055), -9},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'910),
STAmount{USD, UINT64_C(549'99999945), -8},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
}
}
}
@@ -7392,6 +7448,8 @@ private:
.err = Ter(tecAMM_BALANCE)});
};
// Bound holds regardless of the deposit-side fix amendment.
test(all - featureMPTokensV2);
test(all);
}
@@ -7423,6 +7481,7 @@ private:
testFlags();
testRippling();
testAMMAndCLOB(all);
testAMMAndCLOB(all - featureMPTokensV2);
testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
testTradingFee(all);
testTradingFee(all - fixAMMv1_3);
@@ -7442,8 +7501,10 @@ private:
testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
testSwapRounding();
testFixChangeSpotPriceQuality(all);
testFixChangeSpotPriceQuality(all - featureMPTokensV2);
testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
testFixAMMOfferBlockedByLOB(all);
testFixAMMOfferBlockedByLOB(all - featureMPTokensV2);
testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
testLPTokenBalance(all);
testLPTokenBalance(all - fixAMMv1_3);
@@ -1,7 +1,6 @@
#include <test/jtx/Account.h>
#include <test/jtx/ConfidentialTransfer.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <xrpl/basics/Buffer.h>
@@ -21,7 +20,6 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
namespace xrpl {
@@ -2456,703 +2454,6 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
});
}
void
testConfidentialMPTHolderKeyUpdatePreflight(FeatureBitset features)
{
testcase("ConfidentialMPTHolderKeyUpdate preflight");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
// Both amendments are required: ConfidentialMPTKeyRotation and ConfidentialTransfer.
if (!features[featureConfidentialMPTKeyRotation] || !features[featureConfidentialTransfer])
{
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
mptAlice.holderKeyUpdate({
.account = bob,
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
.flags = tfHolderKeyRecovery,
.err = temDISABLED,
});
return;
}
ConfidentialEnv ct{env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
// The flag contains a value outside the recognized mode bits. This is
// rejected by the flags-mask check
for (auto const flags : {0x00080000u, 0x00080000u | tfHolderKeyRecovery, 0x00100000u})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.proof = gMakeZeroBuffer(1),
.flags = flags,
.err = temINVALID_FLAG,
});
}
// Exactly one of Rotation, Recovery, and Cancel must be set.
for (auto const flags :
{0u,
tfHolderKeyRotation | tfHolderKeyRecovery,
tfHolderKeyRotation | tfCancelRecovery,
tfHolderKeyRecovery | tfCancelRecovery,
tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.proof = gMakeZeroBuffer(1),
.flags = flags,
.err = temINVALID_FLAG,
});
}
// The issuer cannot rotate or recover a confidential balance it cannot hold.
Account const aliceNewKey("aliceNewKey");
ct.mpt.generateKeyPair(aliceNewKey);
ct.mpt.holderKeyUpdate({
.account = alice,
.holderPubKey = ct.mpt.getPubKey(aliceNewKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// HolderEncryptionKey one byte short of the required length.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength - 1),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// HolderEncryptionKey the correct length, but not a well-formed
// compressed secp256k1 point.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// HolderEncryptionKey is entirely absent (as opposed to present with
// the wrong length or format).
ct.mpt.holderKeyUpdate({
.account = bob,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
std::optional<Buffer> const cipher = getTrivialCiphertext();
std::optional<Buffer> const none;
// Rotation mode requires both the spending and inbox ciphertexts;
for (auto const& [spending, inbox] :
{std::pair{none, none}, std::pair{cipher, none}, std::pair{none, cipher}})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = spending,
.inboxCiphertext = inbox,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temMALFORMED,
});
}
// Recovery and Cancel modes must not include either.
for (auto const& [spending, inbox] :
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = spending,
.inboxCiphertext = inbox,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
}
for (auto const& [spending, inbox] :
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.spendingCiphertext = spending,
.inboxCiphertext = inbox,
.flags = tfCancelRecovery,
.err = temMALFORMED,
});
}
// Spending ciphertext has the wrong length.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
.inboxCiphertext = getTrivialCiphertext(),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Inbox ciphertext has the wrong length.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = getTrivialCiphertext(),
.inboxCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Spending ciphertext has the correct length, but is not a
// well-formed EC ElGamal ciphertext.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = getBadCiphertext(),
.inboxCiphertext = getTrivialCiphertext(),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Inbox ciphertext has the correct length, but is not a well-formed
// EC ElGamal ciphertext.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = getTrivialCiphertext(),
.inboxCiphertext = getBadCiphertext(),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Rotation/Recovery mode requires a ZKProof.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// Cancel mode must not include HolderEncryptionKey.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.flags = tfCancelRecovery,
.err = temMALFORMED,
});
// Cancel mode must not include a ZKProof.
ct.mpt.holderKeyUpdate({
.account = bob,
.proof = gMakeZeroBuffer(1),
.flags = tfCancelRecovery,
.err = temMALFORMED,
});
}
void
testConfidentialMPTHolderKeyUpdatePreclaim(FeatureBitset features)
{
testcase("ConfidentialMPTHolderKeyUpdate preclaim");
using namespace test::jtx;
// The issuance does not exist.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
// Destroy the issuance to test issuance not found.
mptAlice.destroy();
mptAlice.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
.err = tecOBJECT_NOT_FOUND,
});
}
// The issuance has not enabled confidential balances. The holder is
// authorized so this reaches the confidential-balance-flag check
// rather than failing earlier on a missing MPToken.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
.err = tecNO_PERMISSION,
});
}
// carol exists as an account but was never authorized to hold this
// issuance, so she has no MPToken for it at all.
{
Env env{*this, features};
Account const alice("alice");
Account const carol("carol");
MPTTester mptAlice(env, alice);
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
env.fund(XRP(1000), carol);
env.close();
Account const carolNewKey("carolNewKey");
mptAlice.generateKeyPair(carolNewKey);
mptAlice.holderKeyUpdate({
.account = carol,
.holderPubKey = mptAlice.getPubKey(carolNewKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecOBJECT_NOT_FOUND,
});
}
// The holder has an MPToken but no confidential state yet - never
// registered a key or converted anything.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 100);
Account const bobNewKey("bobNewKey");
mptAlice.generateKeyPair(bobNewKey);
mptAlice.holderKeyUpdate({
.account = bob,
.holderPubKey = mptAlice.getPubKey(bobNewKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecNO_PERMISSION,
});
}
// Submitting the holder's own current key as the "new" key is a no-op.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bob),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecDUPLICATE,
});
}
// A second Recovery-mode transaction must not silently overwrite an
// already-pending RecoveryKey.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
Account const bobRecoveryKey2("bobRecoveryKey2");
ct.mpt.generateKeyPair(bobRecoveryKey2);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey2),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecNO_PERMISSION,
});
}
// bob never submitted a Recovery-mode transaction, so there is no
// sfRecoveryKey to cancel.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
.err = tecNO_PERMISSION,
});
}
// Runs rotation, recovery, and cancel in sequence and expects each to
// succeed.
auto const allModesSucceed = [&](ConfidentialEnv& ct, Account const& bob) {
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
ct.mpt.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
});
};
// Individual (per-holder) lock.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock});
allModesSucceed(ct, bob);
}
// Global (issuance-wide) lock.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.set({.account = alice, .flags = tfMPTLock});
allModesSucceed(ct, bob);
}
// A stale mirror epoch does not block rotation, recovery, or cancel:
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
// Rotate the issuer's key without refreshing bob's mirror,
// leaving bob's mirror epoch stale relative to the issuance.
Account const aliceNewIssuerKey("aliceNewIssuerKey");
ct.mpt.generateKeyPair(aliceNewIssuerKey);
ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(aliceNewIssuerKey)});
BEAST_EXPECT(ct.mpt.checkKeyEpochs(1u, std::nullopt));
BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
allModesSucceed(ct, bob);
}
}
void
testConfidentialMPTHolderKeyUpdateDoApply(FeatureBitset features)
{
testcase("ConfidentialMPTHolderKeyUpdate doApply");
using namespace test::jtx;
// Rotation mode updates the key and re-encrypted balances, bumps the
// version, and clears any pending recovery.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
auto const newPubKey = ct.mpt.getPubKey(bobNewKey);
BEAST_EXPECT(
newPubKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*newPubKey));
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion + 1);
// The rotated balances must still decrypt to the same amounts,
// but now only under the NEW private key.
auto const spendingCt =
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
BEAST_EXPECT(spendingCt.has_value());
BEAST_EXPECT(inboxCt.has_value());
if (!spendingCt || !inboxCt)
return;
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
BEAST_EXPECT(inboxAmt && *inboxAmt == 0);
}
// Rotation mode re-encrypts both balances correctly when spending and
// inbox are both non-zero and differ from each other.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
ConfidentialEnv ct{
env,
alice,
{{.account = bob, .payAmount = 100, .convertAmount = 40},
{.account = carol, .payAmount = 100, .convertAmount = 50}}};
// carol sends 15 into bob's inbox, which is not merged into
// spending, leaving bob with spending=40, inbox=15.
ct.mpt.send({.account = carol, .dest = bob, .amt = 15});
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
auto const spendingCt =
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
BEAST_EXPECT(spendingCt.has_value());
BEAST_EXPECT(inboxCt.has_value());
if (!spendingCt || !inboxCt)
return;
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
BEAST_EXPECT(inboxAmt && *inboxAmt == 15);
}
// Recovery mode only records the pending recovery key; the current
// key, balances, and version are untouched.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
auto const prevSpending =
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
auto const prevKey = ct.mpt.getPubKey(bob);
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
auto const recoveryKey = ct.mpt.getPubKey(bobRecoveryKey);
BEAST_EXPECT(
sleMptoken->isFieldPresent(sfRecoveryKey) && recoveryKey &&
strHex((*sleMptoken)[sfRecoveryKey]) == strHex(*recoveryKey));
BEAST_EXPECT(
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
prevSpending);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
}
// Cancel mode clears the pending recovery key only; the current key,
// balances, and version are untouched.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
auto const prevSpending =
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
auto const prevKey = ct.mpt.getPubKey(bob);
ct.mpt.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
BEAST_EXPECT(
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
prevSpending);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
}
// Rotation mode clears a pending recovery key.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
{
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
BEAST_EXPECT(sleMptoken->isFieldPresent(sfRecoveryKey));
}
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
}
}
public:
void
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
@@ -3189,12 +2490,6 @@ public:
testConfidentialMPTMirrorUpdateDoApply(all);
testConfidentialMPTMirrorUpdateMultipleRotationsIssuerMode(all);
testConfidentialMPTMirrorUpdateMultipleRotationsHolderMode(all);
testConfidentialMPTHolderKeyUpdatePreflight(all);
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialMPTKeyRotation);
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialTransfer);
testConfidentialMPTHolderKeyUpdatePreclaim(all);
testConfidentialMPTHolderKeyUpdateDoApply(all);
}
};
@@ -30,7 +30,6 @@
#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
@@ -638,104 +637,6 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
}
}
// A full AMMWithdraw must succeed even when an unrelated third party
// holds a confidential balance. Pre-fixCleanup3_5_0 the erase of the
// AMM pseudo-account's MPToken was rejected in this case, permanently
// stranding the last LP's position.
void
testAMMWithdrawAllBlockedByUnrelatedCOA(FeatureBitset features)
{
testcase("Full AMMWithdraw with an unrelated holder's COA");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
for (bool const withFix : {true, false})
{
for (bool const carolConverts : {false, true})
{
Env env{*this, withFix ? features | fixCleanup3_5_0 : features - fixCleanup3_5_0};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.flags = kMptDexFlags | tfMPTCanHoldConfidentialBalance});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 1'000);
mptAlice.pay(alice, carol, 100);
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// bob is the sole LP, so withdrawing all of his LPTokens zeroes
// LPTokenBalance and deletes the AMM in that same transaction.
AMM amm(env, bob, XRP(100), mptAlice(100));
Account const ammHolder("amm", amm.ammAccount());
BEAST_EXPECT(amm.ammExists());
BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
// The AMM pseudo-account cannot initialize confidential state:
// confidential transactions must be signed by sfAccount and it
// has no signing key.
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedInbox));
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedSpending));
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::issuerEncryptedBalance));
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::auditorEncryptedBalance));
if (carolConverts)
{
mptAlice.generateKeyPair(carol);
mptAlice.convert(
{.account = carol, .amt = 1, .holderPubKey = mptAlice.getPubKey(carol)});
}
BEAST_EXPECT(mptAlice.getIssuanceConfidentialBalance() == (carolConverts ? 1 : 0));
if (carolConverts && !withFix)
{
amm.withdrawAll(bob, std::nullopt, Ter(tecINVARIANT_FAILED));
env.close();
// bob cannot close his position: the pool, his LPTokens and
// the AMM object all survive the failed withdrawal.
BEAST_EXPECT(amm.ammExists());
BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
// A partial withdrawal still succeeds, because it never
// erases the MPToken -- bob can drain the pool down to a
// residual amount but can never close his position. Withdraw
// half of his actual LPToken balance; a fixed token count
// would round the MPT side of the pool to zero and be
// rejected as a one-sided withdrawal.
auto const bobLPTokens = amm.getLPTokensBalance(bob.id());
amm.withdraw(
bob, IOUAmount{bobLPTokens.mantissa() / 2, bobLPTokens.exponent()});
env.close();
BEAST_EXPECT(amm.ammExists());
BEAST_EXPECT(mptAlice.getBalance(ammHolder) > 0);
}
else
{
// With the fix, carol's confidential balance is irrelevant to
// an AMM she has no stake in: bob closes his position and the
// AMM and its MPToken are erased.
amm.withdrawAll(bob);
env.close();
BEAST_EXPECT(!amm.ammExists());
BEAST_EXPECT(
env.le(keylet::mptoken(mptAlice.issuanceID(), amm.ammAccount())) ==
nullptr);
}
}
}
}
// Exercises every Confidential Transfer transaction type (MPTokenIssuanceSet,
// Convert, MergeInbox, Send, ConvertBack) using tickets instead of regular account
// sequence numbers.
@@ -2660,7 +2561,6 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
// AMM/pseudo-account interaction.
testAMMHolderCannotHaveConfidentialStateClawback(features);
testAMMWithdrawAllBlockedByUnrelatedCOA(features);
// Ticket interactions.
testWithTickets(features);
+5 -5
View File
@@ -573,8 +573,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
env(pay(gw, bob, usd(1)));
env.close();
bool const largeMantissa = features[featureSingleAssetVault] ||
features[featureLendingProtocol] || features[featureMPTokensV2];
bool const largeMantissa =
features[featureSingleAssetVault] || features[featureLendingProtocol];
// alice cannot create escrow for 1/10 iou - precision loss
env(escrow::create(alice, bob, usd(1)),
@@ -2294,8 +2294,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
env(pay(gw, bob, usd(1)));
env.close();
bool const largeMantissa = features[featureSingleAssetVault] ||
features[featureLendingProtocol] || features[featureMPTokensV2];
bool const largeMantissa =
features[featureSingleAssetVault] || features[featureLendingProtocol];
// alice cannot create escrow for 1/10 iou - precision loss
env(escrow::create(alice, bob, usd(1)),
@@ -4334,7 +4334,7 @@ public:
using namespace test::jtx;
FeatureBitset const all{testableAmendments()};
for (FeatureBitset const& feats :
{all - featureSingleAssetVault - featureLendingProtocol - featureMPTokensV2, all})
{all - featureSingleAssetVault - featureLendingProtocol, all})
{
testIOUWithFeats(feats);
testIOUWithFeats(feats - fixCleanup3_2_0);
+21 -9
View File
@@ -21,6 +21,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/json/json_value.h>
@@ -128,7 +129,6 @@ public:
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -140,22 +140,28 @@ public:
numSrc.reserve(rpc::tuning::kMaxSrcCur);
for (std::uint8_t i = 0; i < rpc::tuning::kMaxSrcCur; ++i)
numSrc.push_back(makeMptID(i, bob));
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_src_cur source currencies.
numSrc.push_back(makeMptID(rpc::tuning::kMaxSrcCur, bob));
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));
@@ -167,22 +173,28 @@ public:
auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}});
numSrc.push_back(curm.issuanceID());
}
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, {});
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_auto_src_cur source currencies.
auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}});
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, {});
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));
+26 -12
View File
@@ -27,6 +27,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/json/json_reader.h>
@@ -169,7 +170,6 @@ public:
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -198,11 +198,14 @@ public:
json::Value result;
Gate g;
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = std::move(params);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
using namespace std::chrono_literals;
@@ -285,7 +288,6 @@ public:
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -293,21 +295,27 @@ public:
json::Value result;
Gate g;
// Test rpc::tuning::max_src_cur source currencies.
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_src_cur source currencies.
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur + 1);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));
@@ -315,22 +323,28 @@ public:
// Test rpc::tuning::max_auto_src_cur source currencies.
for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i)
env.trust(Account("alice")[std::to_string(i + 100)](100), "bob");
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), 0);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_auto_src_cur source currencies.
env.trust(Account("alice")["AUD"](100), "bob");
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), 0);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));
+12 -7
View File
@@ -23,8 +23,9 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
@@ -1638,7 +1639,6 @@ struct PayChan_test : public beast::unit_test::Suite
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -1654,10 +1654,13 @@ struct PayChan_test : public beast::unit_test::Suite
BEAST_EXPECT(context.loadType == resource::kFeeReferenceRpc);
json::Value result;
Gate g;
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
context.coro = coro;
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
result = doChannelVerify(context);
g.signal();
co_return;
});
using namespace std::chrono_literals;
@@ -1705,7 +1708,6 @@ struct PayChan_test : public beast::unit_test::Suite
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::ADMIN, // channel_authorize requires ADMIN or canSign()
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -1720,10 +1722,13 @@ struct PayChan_test : public beast::unit_test::Suite
BEAST_EXPECT(context.loadType == resource::kFeeReferenceRpc);
json::Value result;
Gate g;
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
context.coro = coro;
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
result = doChannelAuthorize(context);
g.signal();
co_return;
});
using namespace std::chrono_literals;
+1 -4
View File
@@ -252,10 +252,7 @@ class TheoreticalQuality_test : public beast::unit_test::Suite
std::shared_ptr<ReadView const> closed,
std::optional<Quality> const& expectedQ = {})
{
// Not const: read-only ledger entries peek() through an ApplyView, which
// is undefined behavior on a const object (see SLEBase.h).
// NOLINTNEXTLINE(misc-const-correctness)
PaymentSandbox sb(closed.get(), TapNone);
PaymentSandbox const sb(closed.get(), TapNone);
AMMContext ammContext(rcp.srcAccount, false);
auto const sendMaxIssue = [&rcp]() -> std::optional<Asset> {
@@ -1632,121 +1632,12 @@ class InvariantsMPT_test : public InvariantsBase
precloseOrphan);
}
// deletedHoldings_ in ValidMPTIssuance captures every erased MPToken
// per-holder, so finalize()'s fixCleanup3_5_0 balance check must reject
// a funded MPToken erased alongside an empty sibling regardless of the
// order the two are visited in.
void
testDeleteWithBalanceTwoHolders()
{
using namespace test::jtx;
testcase << "MPToken deleted with non-zero balance, two holders";
MPTID mptID;
Account const carol{"carol"};
// Single-holder setup used by the baseline and the amendment-gate
// cases.
auto const setupSingle = [&](Account const& a1, Account const& a2, Env& env) -> bool {
MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
mpt.create({.flags = tfMPTCanTransfer});
mptID = mpt.issuanceID();
mpt.authorize({.account = a2});
mpt.pay(a1, a2, 100);
return true;
};
Precheck const eraseSingle = [&](Account const&, Account const& a2, ApplyContext& ac) {
auto sleA2 = ac.view().peek(keylet::mptoken(mptID, a2.id()));
if (!sleA2)
return false;
ac.view().erase(sleA2);
return true;
};
Precheck const eraseBoth = [&](Account const&, Account const& a2, ApplyContext& ac) {
auto sleA2 = ac.view().peek(keylet::mptoken(mptID, a2.id()));
auto sleCarol = ac.view().peek(keylet::mptoken(mptID, carol.id()));
if (!sleA2 || !sleCarol)
return false;
ac.view().erase(sleA2);
ac.view().erase(sleCarol);
return true;
};
// Cases below expecting `tecINVARIANT_FAILED` use ttACCOUNT_SET; the
// success case (fixCleanup3_5_0 disabled) uses ttMPTOKEN_AUTHORIZE
// to sidestep unrelated invariants that would otherwise mask the
// tesSUCCESS signal.
// Baseline: a single funded MPToken erased on its own.
doInvariantCheck(
{{"MPToken deleted with non-zero balance"}},
eraseSingle,
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
setupSingle);
// Two-holder erase: whichever key sorts last is visited last, so
// funding each holder in turn guarantees one run where the funded
// MPToken is visited first and an empty sibling follows it.
for (bool const fundCarol : {false, true})
{
auto const setupTwo = [&, fundCarol](
Account const& a1, Account const& a2, Env& env) -> bool {
env.fund(XRP(1'000), carol);
MPTTester mpt(env, a1, {.holders = {a2, carol}, .fund = false});
mpt.create({.flags = tfMPTCanTransfer});
mptID = mpt.issuanceID();
mpt.authorize({.account = a2});
mpt.authorize({.account = carol});
mpt.pay(a1, fundCarol ? carol : a2, 100);
return true;
};
doInvariantCheck(
{{"MPToken deleted with non-zero balance"}},
eraseBoth,
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
setupTwo);
}
// fixCleanup3_5_0 enabled, fixCleanup3_2_0 disabled: the two
// amendments are independent, so this is the only configuration in
// which the `|| isFeatureEnabled(fixCleanup3_5_0)` half of the
// deletedHoldings_ capture gate in ValidMPTIssuance::visitEntry is
// load-bearing.
doInvariantCheck(
makeEnv(all_ - fixCleanup3_2_0),
{{"MPToken deleted with non-zero balance"}},
eraseSingle,
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
setupSingle);
// fixCleanup3_5_0 disabled: erasing a funded MPToken must not trip
// the new check. This pins the fixCleanup3_5_0 gate in finalize().
doInvariantCheck(
makeEnv(all_ - fixCleanup3_5_0),
{},
eraseSingle,
XRPAmount{},
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
{tesSUCCESS, tesSUCCESS},
setupSingle);
}
public:
void
run() override
{
testConfidentialMPTTransfer();
testMPT();
testDeleteWithBalanceTwoHolders();
}
};
@@ -19,7 +19,6 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/RippleStateEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -381,7 +380,7 @@ class InvariantsMisc_test : public InvariantsBase
for (auto const& trustKeylet :
{keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
{
RippleStateEntryW line(trustKeylet, ac.view());
auto const line = ac.view().peek(trustKeylet);
if (!line)
{
return false;
-116
View File
@@ -1763,119 +1763,6 @@ class LoanBroker_test : public beast::unit_test::Suite
}
}
// LoanBrokerDelete should succeed even when an unrelated third party
// holds a confidential balance. The broker pseudo-account carries no
// confidential fields of its own, but pre-fixCleanup3_5_0 the erase
// was rejected.
void
testLoanBrokerDeleteConfidentialCOA(FeatureBitset features)
{
testcase << "LoanBrokerDelete - unrelated confidential holder";
using namespace jtx;
using namespace loan_broker;
auto const withFix = features[fixCleanup3_5_0];
Account const issuer("issuer");
Account const alice("alice");
// carol holds the same MPT but has no relationship to the vault, the
// broker, or alice.
Account const carol("carol");
for (bool const carolConverts : {false, true})
{
Env env(*this, features);
env.fund(XRP(100'000), issuer, alice, carol);
env.close();
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
PrettyAsset const mpt{mptt.issuanceID()};
mptt.authorize({.account = alice});
mptt.authorize({.account = carol});
env(pay(issuer, alice, mpt(100'000)));
env(pay(issuer, carol, mpt(100)));
env.close();
// The issuer must publish an encryption key before any holder can
// convert into confidential form.
mptt.generateKeyPair(issuer);
mptt.set({.account = issuer, .issuerPubKey = mptt.getPubKey(issuer)});
Vault const vault{env};
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
env(tx);
env.close();
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(10'000)}));
env.close();
auto const brokerKeylet =
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
env(set(alice, vaultKeylet.key));
env.close();
env(coverDeposit(alice, brokerKeylet.key, mpt(5'000).value()));
env.close();
auto const broker = env.le(brokerKeylet);
if (!BEAST_EXPECT(broker))
return;
BEAST_EXPECT(broker->at(sfCoverAvailable) > 0);
auto const brokerPseudoID = broker->at(sfAccount);
auto const pseudoMptKey = keylet::mptoken(mptt.issuanceID(), brokerPseudoID);
if (!BEAST_EXPECT(env.le(pseudoMptKey)))
return;
// The broker pseudo-account has no signing key, so it can never
// submit a confidential transaction and its MPToken holds none of
// the four ciphertext fields.
Account const brokerHolder("broker", brokerPseudoID);
BEAST_EXPECT(!mptt.getEncryptedBalance(brokerHolder, MPTTester::holderEncryptedInbox));
BEAST_EXPECT(
!mptt.getEncryptedBalance(brokerHolder, MPTTester::holderEncryptedSpending));
BEAST_EXPECT(
!mptt.getEncryptedBalance(brokerHolder, MPTTester::issuerEncryptedBalance));
BEAST_EXPECT(
!mptt.getEncryptedBalance(brokerHolder, MPTTester::auditorEncryptedBalance));
if (carolConverts)
{
mptt.generateKeyPair(carol);
mptt.convert({.account = carol, .amt = 1, .holderPubKey = mptt.getPubKey(carol)});
}
BEAST_EXPECT(mptt.getIssuanceConfidentialBalance() == (carolConverts ? 1 : 0));
if (carolConverts && !withFix)
{
// The cover is non-zero at the start of the transaction, so
// the erase is flagged and carol's COA rejects it.
env(del(alice, brokerKeylet.key), Ter(tecINVARIANT_FAILED));
env.close();
BEAST_EXPECT(env.le(brokerKeylet) != nullptr);
BEAST_EXPECT(env.le(pseudoMptKey) != nullptr);
// Draining the cover in an earlier transaction leaves the
// MPToken's pre-transaction balance at zero, which sidesteps
// the flag entirely.
env(coverWithdraw(alice, brokerKeylet.key, mpt(5'000).value()));
env.close();
}
// With the fix the balance is read at erase time, so returning
// the cover and deleting the broker in one transaction succeeds
// regardless of carol's confidential balance.
env(del(alice, brokerKeylet.key));
env.close();
BEAST_EXPECT(env.le(brokerKeylet) == nullptr);
BEAST_EXPECT(env.le(pseudoMptKey) == nullptr);
}
}
void
testLoanBrokerDeleteFrozenIOU(FeatureBitset features)
{
@@ -3117,9 +3004,6 @@ public:
testLoanBrokerDeleteFrozenIOU(all_);
testLoanBrokerDeleteFrozenIOU(all_ - fixCleanup3_2_0);
testLoanBrokerDeleteConfidentialCOA(all_);
testLoanBrokerDeleteConfidentialCOA(all_ - fixCleanup3_5_0);
// featureMPTokensV2 independently makes ValidMPTTransfer enforcing,
// but it's Supported::No (never enabled on real networks); exclude
// it here so fixCleanup3_4_0 alone is the deciding amendment, as it
+23 -100
View File
@@ -122,83 +122,6 @@ backend=sqlite
return std::format(kConfigContentsTemplate, dbPathSection, valFileSection);
}
// setenv and unsetenv are POSIX only, and MSVC does not have them.
#ifndef _MSC_VER
/**
* Read an environment variable into a value the caller owns.
*
* The value is copied, because POSIX allows a later `setenv` to release the
* string `getenv` returned.
*
* @param name Name of the environment variable to read.
* @return The value, or std::nullopt when the variable is not set.
*/
[[nodiscard]] std::optional<std::string>
envVar(char const* name)
{
if (char const* const value = std::getenv(name); value != nullptr)
return std::string{value};
return std::nullopt;
}
/**
* Set an environment variable and restore its previous value when done.
*/
class EnvVarGuard
{
private:
std::string const name_;
std::optional<std::string> const saved_;
/**
* Give an environment variable a value, or remove it.
*
* @param name Name of the environment variable to write.
* @param value Value to set, or std::nullopt to remove the variable.
*/
static void
apply(char const* name, std::optional<std::string> const& value) noexcept
{
if (value)
{
setenv(name, value->c_str(), 1);
}
else
{
unsetenv(name);
}
}
public:
/**
* Save the variable's current value, then write the given one.
*
* @param name Name of the environment variable to set.
* @param value Value to set, or std::nullopt to remove the variable.
*/
[[nodiscard]] EnvVarGuard(char const* name, std::optional<std::string> const& value)
: name_(name), saved_(envVar(name))
{
apply(name_.c_str(), value);
}
EnvVarGuard(EnvVarGuard const&) = delete;
EnvVarGuard&
operator=(EnvVarGuard const&) = delete;
/**
* Restore the value the variable held before construction.
*/
~EnvVarGuard()
{
apply(name_.c_str(), saved_);
}
};
#endif // _MSC_VER
/**
* Write an xrpld config file and remove when done.
*/
@@ -412,11 +335,7 @@ port_wss_admin
}
// Config file in HOME or XDG_CONFIG_HOME directory.
#ifndef _MSC_VER
// Save the values the guards below must put back.
auto const home = detail::envVar("HOME");
auto const xdgConfigHome = detail::envVar("XDG_CONFIG_HOME");
#if BOOST_OS_LINUX || BOOST_OS_MACOS
for (auto const& configFile : configFiles)
{
// Point the current working directory to a temporary directory, so
@@ -429,11 +348,13 @@ port_wss_admin
{
TempDir const tc;
// The HOME variable is not used when XDG_CONFIG_HOME is set,
// but must be set. Both guards are declared after tc, so they
// restore before it is removed.
detail::EnvVarGuard const homeGuard("HOME", tc.path());
detail::EnvVarGuard const xdgGuard("XDG_CONFIG_HOME", tc.path());
// Set the HOME and XDG_CONFIG_HOME environment variables. The
// HOME variable is not used when XDG_CONFIG_HOME is set, but
// must be set.
char const* h = getenv("HOME");
setenv("HOME", tc.path().c_str(), 1);
char const* x = getenv("XDG_CONFIG_HOME");
setenv("XDG_CONFIG_HOME", tc.path().c_str(), 1);
// Create the config file in '${XDG_CONFIG_HOME}/[systemName]'.
Path p = tc.file(systemName());
@@ -450,21 +371,22 @@ port_wss_admin
BEAST_EXPECT(
c.section(Sections::kDebugLogfile).values()[0] ==
"/Users/dummy/xrpld/config/log/debug.log");
}
// Both guards are gone: both variables must hold their old values.
BEAST_EXPECT(detail::envVar("HOME") == home);
BEAST_EXPECT(detail::envVar("XDG_CONFIG_HOME") == xdgConfigHome);
// Restore the environment variables.
(h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
(x != nullptr) ? setenv("XDG_CONFIG_HOME", x, 1) : unsetenv("XDG_CONFIG_HOME");
}
// The XDG config directory is not set: the config file must be in a
// subdirectory named .config followed by the system name.
{
TempDir const tc;
// Both guards are declared after tc, so they restore before it
// is removed.
detail::EnvVarGuard const homeGuard("HOME", tc.path());
detail::EnvVarGuard const xdgGuard("XDG_CONFIG_HOME", std::nullopt);
// Set only the HOME environment variable.
char const* h = getenv("HOME");
setenv("HOME", tc.path().c_str(), 1);
char const* x = getenv("XDG_CONFIG_HOME");
unsetenv("XDG_CONFIG_HOME");
// Create the config file in '${HOME}/.config/[systemName]'.
std::string s = ".config";
@@ -485,13 +407,14 @@ port_wss_admin
BEAST_EXPECT(
c.section(Sections::kDebugLogfile).values()[0] ==
"/Users/dummy/xrpld/config/log/debug.log");
}
// Both guards are gone: both variables must hold their old values.
BEAST_EXPECT(detail::envVar("HOME") == home);
BEAST_EXPECT(detail::envVar("XDG_CONFIG_HOME") == xdgConfigHome);
// Restore the environment variables.
(h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
if (x != nullptr)
setenv("XDG_CONFIG_HOME", x, 1);
}
}
#endif // _MSC_VER
#endif
// Restore the current working directory.
current_path(cwd);
+581
View File
@@ -0,0 +1,581 @@
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/LocalValue.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/core/JobQueueAwaiter.h>
#include <array>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
// Tests intentionally capture state in coroutine lambdas; lifetimes are
// controlled by Gate synchronization and join() before scope exit.
// NOLINTBEGIN(cppcoreguidelines-avoid-capturing-lambda-coroutines)
namespace xrpl::test {
/**
* Test suite for the C++20 coroutine primitives: CoroTask, CoroTaskRunner,
* and JobQueueAwaiter.
*
* Dependency Diagram
* ==================
*
* CoroTask_test
* +-------------------------------------------------+
* | + Gate (inner class) : condition_variable helper |
* +-------------------------------------------------+
* | uses
* v
* jtx::Env --> JobQueue::postCoroTask()
* |
* +-- CoroTaskRunner (suspend / post / resume)
* +-- CoroTask<void> / CoroTask<T>
* +-- JobQueueAwaiter
*
* Test Coverage Matrix
* ====================
*
* Test | Primitives exercised
* --------------------------+----------------------------------------------
* testVoidCompletion | CoroTask<void> basic lifecycle
* testCorrectOrder | suspend() -> join() -> post() -> complete
* testIncorrectOrder | post() before suspend() (race-safe path)
* testJobQueueAwaiter | JobQueueAwaiter + yieldAndPost suspend/repost
* testThreadSpecificStorage | LocalValue isolation across coroutines
* testExceptionPropagation | CoroTask<void> exception via co_await
* testMultipleYields | N sequential suspend/resume cycles
* testValueReturn | CoroTask<T> co_return value
* testValueException | CoroTask<T> exception via co_await
* testValueChaining | nested CoroTask<T> -> CoroTask<T>
* testShutdownRejection | postCoroTask returns nullptr when stopping
*/
class CoroTask_test : public beast::unit_test::Suite
{
public:
/**
* Simple one-shot gate for synchronizing between test thread
* and coroutine worker threads. signal() sets the flag;
* waitFor() blocks until signaled or timeout.
*/
class Gate
{
private:
std::condition_variable cv_;
std::mutex mutex_;
bool signaled_ = false;
public:
/**
* Block until signaled or timeout expires.
*
* @param relTime Maximum duration to wait
*
* @return true if signaled before timeout
*/
template <class Rep, class Period>
bool
waitFor(std::chrono::duration<Rep, Period> const& relTime)
{
std::unique_lock<std::mutex> lk(mutex_);
auto b = cv_.wait_for(lk, relTime, [this] { return signaled_; });
signaled_ = false;
return b;
}
/**
* Signal the gate, waking any waiting thread.
*/
void
signal()
{
std::scoped_lock const lk(mutex_);
signaled_ = true;
cv_.notify_all();
}
};
// NOTE: All coroutine lambdas passed to postCoroTask use explicit
// pointer-by-value captures instead of [&] to work around a GCC 14
// bug where reference captures in coroutine lambdas are corrupted
// in the coroutine frame.
/**
* CoroTask<void> runs to completion and runner becomes non-runnable.
*/
void
testVoidCompletion()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("void completion");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [gp = &g](auto) -> CoroTask<void> {
gp->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
runner->join();
BEAST_EXPECT(!runner->runnable());
}
/**
* Correct order: suspend, join, post, complete.
* Mirrors existing Coroutine_test::correct_order.
*/
void
testCorrectOrder()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("correct order");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g1, g2;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [g1p = &g1, g2p = &g2](auto runner) -> CoroTask<void> {
g1p->signal();
co_await runner->suspend();
g2p->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g1.waitFor(5s)))
return;
runner->join();
runner->post();
if (!BEAST_EXPECT(g2.waitFor(5s)))
return;
runner->join();
}
/**
* Incorrect order: post() before suspend(). Verifies the
* race-safe path. Mirrors Coroutine_test::incorrect_order.
*/
void
testIncorrectOrder()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("incorrect order");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [gp = &g](auto runner) -> CoroTask<void> {
runner->post();
co_await runner->suspend();
gp->signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
}
/**
* Suspend + auto-repost across multiple yield points, using the
* external JobQueueAwaiter struct for the first suspension and the
* inline yieldAndPost() awaiter for the second. JobQueueAwaiter is
* used at only one co_await point per coroutine (see the GCC-12
* multi-use warning in JobQueueAwaiter.h).
*/
void
testJobQueueAwaiter()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("JobQueueAwaiter");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
std::vector<int> steps;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [sp = &steps, gp = &g](auto runner) -> CoroTask<void> {
sp->push_back(1);
co_await JobQueueAwaiter{runner};
sp->push_back(2);
co_await runner->yieldAndPost();
sp->push_back(3);
gp->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
runner->join();
BEAST_EXPECT(steps == std::vector<int>({1, 2, 3}));
}
/**
* Per-coroutine LocalValue isolation. Each coroutine sees its own
* copy of thread-local state. Mirrors Coroutine_test::thread_specific_storage.
*/
void
testThreadSpecificStorage()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("thread specific storage");
Env env(*this);
auto& jq = env.app().getJobQueue();
static constexpr int kN = 4;
std::array<std::shared_ptr<JobQueue::CoroTaskRunner>, kN> a;
LocalValue<int> lv(-1);
BEAST_EXPECT(*lv == -1);
Gate g;
jq.addJob(JtClient, "LocalValTest", [&]() {
this->BEAST_EXPECT(*lv == -1);
*lv = -2;
this->BEAST_EXPECT(*lv == -2);
g.signal();
});
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
BEAST_EXPECT(*lv == -1);
for (int i = 0; i < kN; ++i)
{
jq.postCoroTask(
JtClient,
"CoroTaskTest",
[this, ap = &a, gp = &g, lvp = &lv, id = i](auto runner) -> CoroTask<void> {
(*ap)[id] = runner;
gp->signal();
co_await runner->suspend();
this->BEAST_EXPECT(**lvp == -1);
**lvp = id;
this->BEAST_EXPECT(**lvp == id);
gp->signal();
co_await runner->suspend();
this->BEAST_EXPECT(**lvp == id);
co_return;
});
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
a[i]->join();
}
for (auto const& r : a)
{
r->post();
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
r->join();
}
for (auto const& r : a)
{
r->post();
r->join();
}
jq.addJob(JtClient, "LocalValTest", [&]() {
this->BEAST_EXPECT(*lv == -2);
g.signal();
});
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
BEAST_EXPECT(*lv == -1);
}
/**
* An exception thrown in an awaited CoroTask<void> is rethrown into
* the awaiting coroutine by await_resume(), with the original
* message intact. (An exception escaping the top-level body has no
* awaiter to rethrow it; it is captured by unhandled_exception()
* and logged by CoroTaskRunner::resume().)
*/
void
testExceptionPropagation()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("exception propagation");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
std::string what;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [wp = &what, gp = &g](auto) -> CoroTask<void> {
auto inner = []() -> CoroTask<void> {
throw std::runtime_error("test exception");
co_return;
};
try
{
co_await inner();
}
catch (std::runtime_error const& e)
{
*wp = e.what();
}
gp->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
runner->join();
BEAST_EXPECT(what == "test exception");
BEAST_EXPECT(!runner->runnable());
}
/**
* Multiple sequential suspend/resume cycles via co_await.
*/
void
testMultipleYields()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("multiple yields");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
int counter = 0;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [cp = &counter, gp = &g](auto runner) -> CoroTask<void> {
++(*cp);
gp->signal();
co_await runner->suspend();
++(*cp);
gp->signal();
co_await runner->suspend();
++(*cp);
gp->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
BEAST_EXPECT(counter == 1);
runner->join();
runner->post();
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
BEAST_EXPECT(counter == 2);
runner->join();
runner->post();
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
BEAST_EXPECT(counter == 3);
runner->join();
BEAST_EXPECT(!runner->runnable());
}
/**
* CoroTask<T> returns a value via co_return. Outer coroutine
* extracts it with co_await.
*/
void
testValueReturn()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("value return");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
int result = 0;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [rp = &result, gp = &g](auto) -> CoroTask<void> {
auto inner = []() -> CoroTask<int> { co_return 42; };
*rp = co_await inner();
gp->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
runner->join();
BEAST_EXPECT(result == 42);
BEAST_EXPECT(!runner->runnable());
}
/**
* CoroTask<T> propagates exceptions from inner coroutines.
* Outer coroutine catches via try/catch around co_await.
*/
void
testValueException()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("value exception");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
std::string what;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [wp = &what, gp = &g](auto) -> CoroTask<void> {
auto inner = []() -> CoroTask<int> {
throw std::runtime_error("inner error");
co_return 0;
};
try
{
co_await inner();
}
catch (std::runtime_error const& e)
{
*wp = e.what();
}
gp->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
runner->join();
BEAST_EXPECT(what == "inner error");
BEAST_EXPECT(!runner->runnable());
}
/**
* CoroTask<T> chaining. Nested value-returning coroutines
* compose via co_await.
*/
void
testValueChaining()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("value chaining");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
Gate g;
int result = 0;
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [rp = &result, gp = &g](auto) -> CoroTask<void> {
auto add = [](int a, int b) -> CoroTask<int> { co_return a + b; };
auto mul = [add](int a, int b) -> CoroTask<int> {
int const sum = co_await add(a, b);
co_return sum * 2;
};
*rp = co_await mul(3, 4);
gp->signal();
co_return;
});
BEAST_EXPECT(runner);
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
runner->join();
BEAST_EXPECT(result == 14); // (3 + 4) * 2
BEAST_EXPECT(!runner->runnable());
}
/**
* postCoroTask returns nullptr when JobQueue is stopping.
*/
void
testShutdownRejection()
{
using namespace std::chrono_literals;
using namespace jtx;
testcase("shutdown rejection");
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->forceMultiThread = true;
return cfg;
}));
// Stop the JobQueue
env.app().getJobQueue().stop();
auto runner = env.app().getJobQueue().postCoroTask(
JtClient, "CoroTaskTest", [](auto) -> CoroTask<void> { co_return; });
BEAST_EXPECT(!runner);
}
void
run() override
{
testVoidCompletion();
testCorrectOrder();
testIncorrectOrder();
testJobQueueAwaiter();
testThreadSpecificStorage();
testExceptionPropagation();
testMultipleYields();
testValueReturn();
testValueException();
testValueChaining();
testShutdownRejection();
}
};
BEAST_DEFINE_TESTSUITE(CoroTask, core, xrpl);
} // namespace xrpl::test
// NOLINTEND(cppcoreguidelines-avoid-capturing-lambda-coroutines)
+62 -27
View File
@@ -6,6 +6,7 @@
#include <xrpl/basics/LocalValue.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
@@ -49,6 +50,11 @@ public:
}
};
// NOTE: All coroutine lambdas passed to postCoroTask use explicit
// pointer-by-value captures instead of [&] to work around a GCC 14
// bug where reference captures in coroutine lambdas are corrupted
// in the coroutine frame.
void
correctOrder()
{
@@ -63,14 +69,22 @@ public:
}));
Gate g1, g2;
std::shared_ptr<JobQueue::Coro> c;
env.app().getJobQueue().postCoro(JtClient, "CoroTest", [&](auto const& cr) {
c = cr;
g1.signal();
c->yield();
g2.signal();
});
BEAST_EXPECT(g1.waitFor(5s));
std::shared_ptr<JobQueue::CoroTaskRunner> c;
env.app().getJobQueue().postCoroTask(
JtClient,
"CoroTest",
// Safe capture: the test blocks on the Gates until the coroutine
// completes, so the captured pointers outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
[cp = &c, g1p = &g1, g2p = &g2](auto runner) -> CoroTask<void> {
*cp = runner;
g1p->signal();
co_await runner->suspend();
g2p->signal();
co_return;
});
if (!BEAST_EXPECT(g1.waitFor(5s)))
return;
c->join();
c->post();
BEAST_EXPECT(g2.waitFor(5s));
@@ -90,11 +104,22 @@ public:
}));
Gate g;
env.app().getJobQueue().postCoro(JtClient, "CoroTest", [&](auto const& c) {
c->post();
c->yield();
g.signal();
});
env.app().getJobQueue().postCoroTask(
JtClient,
"CoroTest",
// Safe capture: the test blocks on the Gate until the coroutine
// completes, so the captured pointer outlives the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
[gp = &g](auto runner) -> CoroTask<void> {
// Schedule a resume before suspending. The posted job
// cannot actually call resume() until the current resume()
// releases CoroTaskRunner::mutex_, which only happens after
// the coroutine suspends at co_await.
runner->post();
co_await runner->suspend();
gp->signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
}
@@ -110,7 +135,7 @@ public:
auto& jq = env.app().getJobQueue();
static int const kN = 4;
std::array<std::shared_ptr<JobQueue::Coro>, kN> a;
std::array<std::shared_ptr<JobQueue::CoroTaskRunner>, kN> a;
LocalValue<int> lv(-1);
BEAST_EXPECT(*lv == -1);
@@ -127,26 +152,36 @@ public:
for (int i = 0; i < kN; ++i)
{
jq.postCoro(JtClient, "CoroTest", [&, id = i](auto const& c) {
a[id] = c;
g.signal();
c->yield();
jq.postCoroTask(
JtClient,
"CoroTest",
// Safe capture: the test drives every coroutine to completion
// via the Gate/post()/join() sequences below, so the captured
// pointers outlive the coroutines.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
[this, ap = &a, gp = &g, lvp = &lv, id = i](auto runner) -> CoroTask<void> {
(*ap)[id] = runner;
gp->signal();
co_await runner->suspend();
this->BEAST_EXPECT(*lv == -1);
*lv = id;
this->BEAST_EXPECT(*lv == id);
g.signal();
c->yield();
this->BEAST_EXPECT(**lvp == -1);
**lvp = id;
this->BEAST_EXPECT(**lvp == id);
gp->signal();
co_await runner->suspend();
this->BEAST_EXPECT(*lv == id);
});
BEAST_EXPECT(g.waitFor(5s));
this->BEAST_EXPECT(**lvp == id);
co_return;
});
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
a[i]->join();
}
for (auto const& c : a)
{
c->post();
BEAST_EXPECT(g.waitFor(5s));
if (!BEAST_EXPECT(g.waitFor(5s)))
return;
c->join();
}
for (auto const& c : a)
+58 -31
View File
@@ -1,11 +1,11 @@
#include <test/jtx/Env.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <atomic>
#include <memory>
namespace xrpl::test {
@@ -46,87 +46,114 @@ class JobQueue_test : public beast::unit_test::Suite
}
}
// NOTE: All coroutine lambdas passed to postCoroTask use explicit
// pointer-by-value captures instead of [&] to work around a GCC 14
// bug where reference captures in coroutine lambdas are corrupted
// in the coroutine frame.
void
testPostCoro()
testPostCoroTask()
{
jtx::Env env{*this};
JobQueue& jQueue = env.app().getJobQueue();
{
// Test repeated post()s until the Coro completes.
// Test repeated post()s until the coroutine completes.
std::atomic<int> yieldCount{0};
auto const coro = jQueue.postCoro(
auto const runner = jQueue.postCoroTask(
JtClient,
"PostCoroTest1",
[&yieldCount](std::shared_ptr<JobQueue::Coro> const& coroCopy) {
while (++yieldCount < 4)
coroCopy->yield();
// Safe capture: the test blocks below until the coroutine
// completes, so the captured pointer outlives the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
[ycp = &yieldCount](auto runner) -> CoroTask<void> {
while (++(*ycp) < 4)
co_await runner->suspend();
co_return;
});
BEAST_EXPECT(coro != nullptr);
BEAST_EXPECT(runner != nullptr);
// Wait for the Job to run and yield.
while (yieldCount == 0)
;
// Now re-post until the Coro says it is done.
// Now re-post until the CoroTaskRunner says it is done.
int old = yieldCount;
while (coro->runnable())
while (runner->runnable())
{
BEAST_EXPECT(coro->post());
BEAST_EXPECT(runner->post());
while (old == yieldCount)
{
}
coro->join();
runner->join();
BEAST_EXPECT(++old == yieldCount);
}
BEAST_EXPECT(yieldCount == 4);
}
{
// Test repeated resume()s until the Coro completes.
// Test repeated post()+join()s until the coroutine completes.
int yieldCount{0};
auto const coro = jQueue.postCoro(
auto const runner = jQueue.postCoroTask(
JtClient,
"PostCoroTest2",
[&yieldCount](std::shared_ptr<JobQueue::Coro> const& coroCopy) {
while (++yieldCount < 4)
coroCopy->yield();
// Safe capture: the test blocks below until the coroutine
// completes, so the captured pointer outlives the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
[ycp = &yieldCount](auto runner) -> CoroTask<void> {
while (++(*ycp) < 4)
co_await runner->suspend();
co_return;
});
if (!coro)
if (!runner)
{
// There's no good reason we should not get a Coro, but we
// There's no good reason we should not get a runner, but we
// can't continue without one.
BEAST_EXPECT(false);
return;
}
// Wait for the Job to run and yield.
coro->join();
runner->join();
// Now resume until the Coro says it is done.
// Now post()+join() until the CoroTaskRunner says it is done.
// resume() requires a prior post() (see the precondition on
// CoroTaskRunner::resume()), so the posted job performs the
// resume and join() blocks until it completes. yieldCount is
// deliberately not atomic: the mutexRun_ handoff inside join()
// must provide the happens-before edge that makes the
// increment visible to this thread.
int old = yieldCount;
while (coro->runnable())
while (runner->runnable())
{
coro->resume(); // Resume runs synchronously on this thread.
BEAST_EXPECT(runner->post());
runner->join();
BEAST_EXPECT(++old == yieldCount);
}
BEAST_EXPECT(yieldCount == 4);
}
{
// If the JobQueue is stopped, we should no
// longer be able to add a Coro (and calling postCoro() should
// return false).
// longer be able to post a coroutine (and calling postCoroTask()
// should return nullptr).
using namespace std::chrono_literals;
jQueue.stop();
// The Coro should never run, so having the Coro access this
// The coroutine should never run, so having it access this
// unprotected variable on the stack should be completely safe.
// Not recommended for the faint of heart...
bool unprotected = false;
auto const coro = jQueue.postCoro(
JtClient, "PostCoroTest3", [&unprotected](std::shared_ptr<JobQueue::Coro> const&) {
unprotected = false;
auto const runner = jQueue.postCoroTask(
JtClient,
"PostCoroTest3",
// Safe capture: the JobQueue is stopped, so the coroutine is
// never started and the captured pointer is never dereferenced.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
[up = &unprotected](auto) -> CoroTask<void> {
*up = true;
co_return;
});
BEAST_EXPECT(coro == nullptr);
BEAST_EXPECT(runner == nullptr);
BEAST_EXPECT(unprotected == false);
}
}
@@ -135,7 +162,7 @@ public:
run() override
{
testAddJob();
testPostCoro();
testPostCoroTask();
}
};
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more