Compare commits

..

30 Commits

Author SHA1 Message Date
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
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 Mankawde
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 <noreply@anthropic.com>
2026-03-25 15:44:19 +00:00
198 changed files with 10588 additions and 12666 deletions

View File

@@ -256,6 +256,8 @@ words:
- replayer
- repodata
- repomd
- repost
- reposts
- rerandomize
- rerandomization
- rerandomized
@@ -303,6 +305,7 @@ words:
- sponsees
- SRPMS
- sslws
- stackful
- statsd
- STATSDCOLLECTOR
- stissue
@@ -366,7 +369,6 @@ words:
- venv
- vfalco
- vinnie
- vkeylet
- wasmi
- wextra
- wptr

View File

@@ -1,39 +0,0 @@
name: Use cargo artifacts cache
description: >
Cache the cargo build artifacts with rust-cache. Never caches ~/.cargo/bin:
when saving the cache, rust-cache deletes all binaries that were already
present there, which on persistent self-hosted runners wipes the tools
installed by prepare-runner. Harmless on ephemeral runners, but kept
consistent everywhere.
inputs:
workspaces:
description: "Workspaces to cache, as 'workspace -> target' lines."
required: false
default: crates
key:
description: "Additional part of the cache key."
required: false
default: ""
cache-directories:
description: "Additional non-workspace directories to cache."
required: false
default: ""
save-if:
description: >
Condition for saving the cache after the job. Defaults to save only from develop branch
required: false
default: ${{ github.ref == 'refs/heads/develop' }}
runs:
using: composite
steps:
- name: Use cargo artifacts cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
cache-bin: "false"
cache-directories: ${{ inputs.cache-directories }}
key: ${{ inputs.key }}
save-if: ${{ inputs.save-if }}
workspaces: ${{ inputs.workspaces }}

View File

@@ -4,7 +4,6 @@ updates:
directories:
- /
- .github/actions/build-deps/
- .github/actions/cargo-cache/
- .github/actions/release-info/
- .github/actions/set-compiler-env/
- .github/actions/setup-conan/

View File

@@ -58,7 +58,7 @@ jobs:
base_image: debian:bookworm
- name: rhel
base_image: registry.access.redhat.com/ubi9/ubi:latest
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
with:
image_name: xrpld/nix-${{ matrix.distro.name }}
dockerfile: nix/docker/Dockerfile

View File

@@ -39,7 +39,7 @@ jobs:
# AlmaLinux rather than UBI9, which does not ship rpm-sign.
- name: rhel
base_image: almalinux:9
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
with:
image_name: xrpld/packaging-${{ matrix.distro.name }}
dockerfile: package/Dockerfile

View File

@@ -30,7 +30,7 @@ jobs:
permissions:
contents: read
packages: write
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
with:
image_name: xrpld/pre-commit
dockerfile: bin/pre-commit/Dockerfile

View File

@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: false

View File

@@ -14,7 +14,7 @@ on:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@f1952595d212e86169935135efc66294b4574131
uses: XRPLF/actions/.github/workflows/pre-commit.yml@3ba08d6ddf114092891d48491fc2e26c3ba15552
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-f56b79f" }'

View File

@@ -47,7 +47,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: false

View File

@@ -129,7 +129,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: ${{ inputs.ccache_enabled }}
@@ -163,10 +163,11 @@ jobs:
compiler: ${{ inputs.compiler }}
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
cache-directories: ${{ env.BUILD_DIR }}/corrosion
key: ${{ inputs.config_name }}
save-if: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release') }}
# two workspaces here because build artifacts are located in 2 places:
# - crates/target when cargo is called directly
# - build/cargo when cargo is called by cmake

View File

@@ -27,7 +27,7 @@ jobs:
determine-files:
permissions:
contents: read
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@70145243b905dc3e040a61d39c00e178cfb96f71
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@d041ac9f1fa9f07a4ba335eb4c1c82233fb3fef6
run-clang-tidy:
name: Run clang tidy
@@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: false
@@ -60,9 +60,10 @@ jobs:
compiler: ${{ env.COMPILER }}
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
cache-directories: ${{ env.BUILD_DIR }}/corrosion
save-if: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release') }}
workspaces: crates -> ../${{ env.BUILD_DIR }}/cargo
- name: Setup Conan

View File

@@ -33,7 +33,9 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: crates
- name: Run clippy
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
@@ -46,7 +48,9 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: crates
- name: Generate coverage report
run: cargo llvm-cov nextest --workspace --all-features --locked --no-tests=warn --lcov --output-path lcov.info
@@ -72,7 +76,9 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: crates
- name: Build documentation
env:

View File

@@ -68,7 +68,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f
with:
enable_ccache: false

View File

@@ -54,9 +54,6 @@ This section contains changes targeting a future version.
- `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529)
- `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529)
- Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529)
- `vault_info`: Errors now identify what the request got wrong instead of reporting every failure as the unregistered token `malformedRequest`, and the `error`, `error_code` and `error_message` fields now agree with each other. An invalid `vault_id` or `seq` returns `invalidParams`, an invalid `owner` returns `actMalformed`, and a request that mixes `vault_id` with `owner`/`seq` or supplies neither returns `invalidParams` with a message naming the accepted combinations. [#8015](https://github.com/XRPLF/rippled/pull/8015)
- `vault_info`: A well-formed all-zero `vault_id` now returns `entryNotFound` instead of being rejected as malformed, and `entryNotFound` responses now include `error_code` and `error_message`. Clients that request `ripplerpc` 3.0 or above therefore receive HTTP 400 with that error rather than HTTP 200. [#8015](https://github.com/XRPLF/rippled/pull/8015)
- `vault_info`: `vault_id` and `owner` must now be strings, matching how `ledger_entry` reads the same fields. An object or an array in either field previously produced an internal error, and a number was silently converted to its decimal text; `vault_id` now returns `invalidParams` and `owner` returns `actMalformed`. [#8015](https://github.com/XRPLF/rippled/pull/8015)
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)

View File

@@ -1094,8 +1094,8 @@
# Default is 100.
#
# back_off_milliseconds
# Number of milliseconds to wait between online_delete
# SQL deletion batches to allow other functions
# Number of milliseconds to wait between
# online_delete batches to allow other functions
# to catch up.
# Default is 100.
#
@@ -1109,22 +1109,10 @@
# The online delete process checks periodically
# that xrpld is still in sync with the network,
# and that the validated ledger is less than
# 'age_threshold_seconds' old, and that all
# recent ledgers are available. If not, then continue
# 'age_threshold_seconds' old. If not, then continue
# sleeping for this number of seconds and
# checking until healthy.
# Default is 2.
#
# max_waiting_ledgers
# The maximum number of ledgers that may be validated
# while online deletion is waiting for the node to get
# fully synced with the rest of the network. If more than
# this number of ledgers are validated while waiting, then
# online deletion gives up on the current ledger and tries
# again later. Note this only affects situations that cause
# rotation to wait, such as going out of sync, or missing
# ledgers. Forward progress is not penalized. Minimum is 64.
# Default is the online_delete value.
# Default is 5.
#
# Notes:
# The 'node_db' entry configures the primary, persistent storage.

View File

@@ -120,10 +120,7 @@ if(MSVC)
_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:Debug>>:_CRTDBG_MAP_ALLOC>
)
target_link_libraries(
common
INTERFACE -errorreport:none -machine:X64 -ignore:4099
)
target_link_libraries(common INTERFACE -errorreport:none -machine:X64)
else()
target_compile_options(
common

View File

@@ -8,7 +8,6 @@ Uses pcpp to preprocess the macro file and pyparsing to parse the DSL.
import io
import argparse
import re
from pathlib import Path
import pyparsing as pp
@@ -54,89 +53,28 @@ def create_transaction_parser():
return macro_parser
# Defaults for xrpl::TxSettings members, mirroring
# include/xrpl/protocol/TxSettings.h. A transaction's settings blob only names
# the members that differ from these.
SETTING_DEFAULTS = {
"delegable": "Delegation::NotDelegable",
"amendment": "uint256{}",
"privileges": "Privilege::NoPriv",
}
def parse_settings(settings_str):
"""Parse a TxSettings blob into a dict, filling in defaults.
Args:
settings_str: A string like '({.delegable = Delegation::NotDelegable,
.privileges = Privilege::CreateAcct})', or '({})'.
Returns:
A dict with a value for every key in SETTING_DEFAULTS.
"""
body = settings_str.strip()
if not (body.startswith("(") and body.endswith(")")):
raise ValueError(
f"Malformed settings blob, expected '({{...}})': {settings_str!r}"
)
body = body[1:-1].strip()
if not (body.startswith("{") and body.endswith("}")):
raise ValueError(
f"Malformed settings blob, expected '({{...}})': {settings_str!r}"
)
body = body[1:-1]
# Strip comments, which may be interleaved with the designated initializers.
body = re.sub(r"//[^\n]*", "", body)
settings = dict(SETTING_DEFAULTS)
seen = set()
# Each entry runs from '.key =' up to the next '.key =' or the end.
for key, value in re.findall(
r"\.(\w+)\s*=\s*(.*?)(?=,\s*\.\w+\s*=|,?\s*$)", body, re.S
):
if key not in SETTING_DEFAULTS:
raise ValueError(f"Unknown TxSettings member '.{key}' in {settings_str!r}")
settings[key] = " ".join(value.split()).rstrip(",")
seen.add(key)
# Catch a typo'd or unparsed initializer rather than silently defaulting it.
# Every '.member' in the blob must have been consumed above.
if len(re.findall(r"\.\w+", body)) != len(seen):
raise ValueError(f"Could not parse every setting in {settings_str!r}")
# A blob with content but no designated initializer is positional, which
# would otherwise be read as "all defaults" and silently generate the
# wrong output.
if body.strip() and not seen:
raise ValueError(
"TxSettings requires designated initializers (.member = value), "
f"got {settings_str!r}"
)
return settings
def parse_transaction_args(args_list):
"""Parse the arguments of a TRANSACTION macro call.
Args:
args_list: A list of parsed arguments from pyparsing, e.g.,
['ttPAYMENT', '0', 'Payment',
'({.privileges = Privilege::CreateAcct})', '({...})']
['ttPAYMENT', '0', 'Payment', 'Delegation::delegable',
'uint256{}', 'createAcct', '({...})']
Returns:
A dict with parsed transaction information.
"""
if len(args_list) < 5:
if len(args_list) < 7:
raise ValueError(
f"Expected at least 5 parts in TRANSACTION, got {len(args_list)}: {args_list}"
f"Expected at least 7 parts in TRANSACTION, got {len(args_list)}: {args_list}"
)
tag = args_list[0]
value = args_list[1]
name = args_list[2]
settings = parse_settings(args_list[3])
delegable = args_list[3]
amendments = args_list[4]
privileges = args_list[5]
fields_str = args_list[-1]
# Parse fields: ({field1, field2, ...})
@@ -146,9 +84,9 @@ def parse_transaction_args(args_list):
"tag": tag,
"value": value,
"name": name,
"delegable": settings["delegable"],
"amendments": settings["amendment"],
"privileges": settings["privileges"],
"delegable": delegable,
"amendments": amendments,
"privileges": privileges,
"fields": fields,
}

20
crates/Cargo.lock generated
View File

@@ -57,9 +57,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824894a4a85dca76d4c95c2b9098c036f5a29f627b30c12780774f6654e60974"
checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291"
dependencies = [
"cc",
"cxx-build",
@@ -72,9 +72,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1ae0b651ea5b0000b19513aef5a03f194d7e3486f2d9258b658da8677fe9036"
checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2"
dependencies = [
"cc",
"codespan-reporting",
@@ -87,9 +87,9 @@ dependencies = [
[[package]]
name = "cxxbridge-cmd"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb05f91d3fb8435d9bab6ac5ce6ac1868be774325fb7fb2a91be39393b21388e"
checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d"
dependencies = [
"clap",
"codespan-reporting",
@@ -101,15 +101,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf293202e0e3e98495785745389e8d0755b217e66f19194a5c695c25e03282ef"
checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89"
[[package]]
name = "cxxbridge-macro"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca001d746947c7249ed9d332a10f7a59daedbafeb0ec68c5c18a7db7a93f6ccc"
checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c"
dependencies = [
"indexmap",
"proc-macro2",

View File

@@ -65,7 +65,7 @@ wherever it appears in the repository configuration.
4. Add the repository, using the channel you picked in [Release channels](#release-channels):
```bash
echo "deb [signed-by=/etc/apt/keyrings/xrplf.asc] https://packages.xrplf.org/repository/deb-stable any main" | \
echo "deb [signed-by=/etc/apt/keyrings/xrplf.asc] https://packages.xrplf.org/repository/deb-stable focal main" | \
sudo tee /etc/apt/sources.list.d/xrplf.list
```
@@ -92,19 +92,19 @@ wherever it appears in the repository configuration.
2. Add the repository, using the channel you picked in [Release channels](#release-channels):
```bash
cat << 'REPOFILE' | sudo tee /etc/yum.repos.d/xrplf.repo
cat << REPOFILE | sudo tee /etc/yum.repos.d/xrplf.repo
[xrplf-stable]
name=XRP Ledger Packages
enabled=1
baseurl=https://packages.xrplf.org/repository/rpm-stable/$basearch/
baseurl=https://packages.xrplf.org/repository/rpm-stable/
gpgcheck=1
repo_gpgcheck=1
repo_gpgcheck=0
gpgkey=https://packages.xrplf.org/xrplf.asc
REPOFILE
```
`gpgcheck=1` verifies each package against the key above.
`repo_gpgcheck=1` verifies the repository metadata, which the server signs with the same key.
`repo_gpgcheck` is off because the repository metadata is generated by the server and is not signed.
3. Install the `xrpld` package:

View File

@@ -529,30 +529,11 @@ staticPointerCast(TT const& v)
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, v);
}
// A bare `TT&&` here would be a forwarding reference, since TT is deduced directly from this
// parameter, and would then also bind to lvalues in preference to the `const&` overload above
// (binding to a plain reference beats binding to a const one), silently moving out of a caller's
// live variable on what looks like a copy call. Naming the wrapped type keeps TT nested inside
// SharedIntrusive<TT>, so this only binds to an actual rvalue of that type.
template <class T, class TT>
SharedPtr<T>
staticPointerCast(SharedIntrusive<TT>&& v)
{
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, std::move(v));
}
template <class T, class TT>
SharedPtr<T>
dynamicPointerCast(TT const& v)
{
return SharedPtr<T>(DynamicCastTagSharedIntrusive{}, v);
}
template <class T, class TT>
SharedPtr<T>
dynamicPointerCast(SharedIntrusive<TT>&& v)
{
return SharedPtr<T>(DynamicCastTagSharedIntrusive{}, std::move(v));
}
} // namespace intr_ptr
} // namespace xrpl

View File

@@ -125,7 +125,6 @@ struct Keys
static constexpr auto kMaximumTxnInLedger = "maximum_txn_in_ledger";
static constexpr auto kMaximumTxnPerAccount = "maximum_txn_per_account";
static constexpr auto kMemoryLevel = "memory_level";
static constexpr auto kMaxWaitingLedgers = "max_waiting_ledgers";
static constexpr auto kMinLedgersToComputeSizeLimit = "min_ledgers_to_compute_size_limit";
static constexpr auto kMinimumEscalationMultiplier = "minimum_escalation_multiplier";
static constexpr auto kMinimumLastLedgerBuffer = "minimum_last_ledger_buffer";

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

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

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

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

View File

@@ -24,7 +24,6 @@
#include <optional>
#include <set>
#include <utility>
#include <vector>
namespace xrpl {
@@ -199,10 +198,7 @@ dirLink(
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it (account-based or credential-based).
* - Expects any credentials passed in to already exist in the ledger, and
* returns an internal error otherwise. Validate them beforehand with
* credentials::valid().
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
*/
@@ -213,8 +209,7 @@ canWithdraw(
AccountID const& to,
SLE::const_ref toSle,
STAmount const& amount,
bool hasDestinationTag,
std::optional<std::vector<uint256>> const& credentialIDs = std::nullopt);
bool hasDestinationTag);
/**
* Checks that can withdraw funds from an object to itself or a destination.
@@ -227,10 +222,7 @@ canWithdraw(
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it (account-based or credential-based).
* - Expects any credentials passed in to already exist in the ledger, and
* returns an internal error otherwise. Validate them beforehand with
* credentials::valid().
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
*/
@@ -240,25 +232,20 @@ canWithdraw(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
bool hasDestinationTag,
std::optional<std::vector<uint256>> const& credentialIDs = std::nullopt);
bool hasDestinationTag);
/**
* Checks that can withdraw funds from an object to itself or a destination.
*
* The receiver may be either the submitting account (sfAccount) or a different
* destination account (sfDestination). Credentials, if any, are taken from the
* transaction's sfCredentialIDs field.
* destination account (sfDestination).
*
* - Checks that the receiver account exists.
* - If the receiver requires a destination tag, check that one exists, even
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it (account-based or credential-based).
* - Expects any credentials in sfCredentialIDs to already exist in the
* ledger, and returns an internal error otherwise. Validate them
* beforehand with credentials::valid().
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
*/

View File

@@ -14,7 +14,6 @@
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
#include <memory>
#include <set>
#include <utility>
@@ -34,32 +33,6 @@ checkExpired(SLE const& sleCredential, NetClock::time_point const& closed);
[[nodiscard]] TER
deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j);
/**
* @brief Remove credentials pinned to a pseudo-account's owner directory.
*
* Cleans up credentials that were linked to a pseudo-account (Vault, LoanBroker,
* AMM), which such an account can neither accept nor delete. Only credentials
* are removed; every other object is left in place. The walk visits at most
* @p maxNodesToDelete directory entries and charges the ones it leaves alone
* against that budget too, so a directory holding other objects yields fewer
* than @p maxNodesToDelete deletions. On reaching the bound the result is
* `tecINCOMPLETE` and the caller must propagate it so a later transaction
* resumes.
*
* @param view Mutable ledger view.
* @param pseudoAcct The pseudo-account whose directory is cleaned.
* @param maxNodesToDelete Upper bound on directory entries processed in one call.
* @param j Journal for diagnostics.
* @return tesSUCCESS once no credentials remain, tecINCOMPLETE if the bound was
* reached, or a deletion error.
*/
[[nodiscard]] TER
deletePseudoAccountCredentials(
ApplyView& view,
AccountID const& pseudoAcct,
std::uint16_t maxNodesToDelete,
beast::Journal j);
// Amendment and parameters checks for sfCredentialIDs field
NotTEC
checkFields(STTx const& tx, Rules const& rules, beast::Journal j);

View File

@@ -7,7 +7,6 @@
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
#include <optional>
@@ -239,40 +238,4 @@ getVaultPhase(
std::optional<std::uint32_t> subscriptionDate,
std::optional<std::uint32_t> redemptionDate);
/**
* Controls whether checkVaultDomain reports an expired credential as an
* error. A caller that deletes expired credentials later, in doApply, passes
* Yes and treats the subject as authorized; a caller with no such cleanup
* step must keep the error.
*/
enum class SuppressExpired : bool { No = false, Yes = true };
/**
* Checks that subject belongs to the permissioned domain governing a vault's
* shares.
*
* The domain is read from the share issuance rather than from the vault. Vault
* shares are issued by the vault's pseudo-account, which cannot grant an
* authorization explicitly, so domain membership is the only route to being
* authorized: a vault with no domain set has no authorized participants at
* all, and every subject fails with tecNO_AUTH.
*
* Which accounts to check, and whether to check at all, is left to the caller.
* This says nothing about vault privacy or about the roles of the accounts.
*
* @param view The ledger view.
* @param issuance The MPTokenIssuance SLE for the vault's shares.
* @param subject The account whose domain membership is checked.
* @param suppressExpired Whether an expired credential counts as authorized.
*
* @return tesSUCCESS if the subject is a domain member, otherwise the reason
* it is not.
*/
[[nodiscard]] TER
checkVaultDomain(
ReadView const& view,
SLE::const_ref issuance,
AccountID const& subject,
SuppressExpired suppressExpired);
} // namespace xrpl

View File

@@ -91,17 +91,6 @@ getFee(std::uint16_t tfee)
return Number{tfee} / kAuctionSlotFeeScaleFactor;
}
/**
* Minimum auction slot price: LPTokens * TradingFee / kAuctionSlotMinFeeFraction
* @param lptAMMBalance AMM LP token balance
* @param tradingFee trading fee in {0, 1000}
*/
inline Number
ammAuctionMinSlotPrice(Number const& lptAMMBalance, std::uint16_t tradingFee)
{
return lptAMMBalance * getFee(tradingFee) / kAuctionSlotMinFeeFraction;
}
/**
* Get fee multiplier (1 - tfee)
* @tfee trading fee in basis points

View File

@@ -4,7 +4,6 @@
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxSettings.h>
#include <cstdint>
#include <functional>
@@ -39,6 +38,11 @@ enum GranularPermissionType : std::uint32_t {
#pragma pop_macro("GRANULAR_PERMISSION")
};
// Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor
// tricks in tests and macro-generated code; enum class would break that.
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Delegation { Delegable, NotDelegable };
class Permission
{
private:
@@ -61,7 +65,7 @@ private:
struct TxDelegationEntry
{
uint256 amendment;
Delegation delegable{Delegation::NotDelegable};
Delegation delegable{NotDelegable};
};
std::unordered_set<TxType> granularTxTypes_;

View File

@@ -396,16 +396,6 @@ using TxID = uint256;
*/
constexpr std::uint16_t kMaxDeletableAmmTrustLines = 512;
/**
* The maximum number of owner-directory entries to walk when clearing
* credentials pinned to a pseudo-account, in a single transaction.
*
* The walk stops after this many entries whether or not each one turns out to
* be a credential, so a directory that also holds other objects yields fewer
* deletions per transaction.
*/
constexpr std::uint16_t kMaxDeletablePseudoAccountCredentials = 512;
/**
* The maximum length of a URI inside an Oracle
*/

View File

@@ -19,7 +19,7 @@ namespace xrpl {
class Rules;
namespace test {
class InvariantsMisc_test;
class Invariants_test;
} // namespace test
class STLedgerEntry final : public STObject, public CountedObject<STLedgerEntry>
@@ -83,8 +83,8 @@ private:
void
setSLEType();
friend test::InvariantsMisc_test; // this test wants access to the
// private type_
friend test::Invariants_test; // this test wants access to the private
// type_
STBase*
copy(std::size_t n, void* buf) const override;

View File

@@ -1,96 +0,0 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/safe_cast.h>
#include <cstdint>
#include <type_traits>
namespace xrpl {
enum class Delegation { Delegable, NotDelegable };
/**
* Operations a transaction is permitted to perform, as a bitfield.
*
* These are declared per-transaction in transactions.macro (via
* TxSettings::privileges) and enforced in InvariantCheck.cpp.
*/
enum class Privilege : std::uint16_t {
NoPriv = 0x0000, // The transaction can not do any of the enumerated operations
CreateAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object.
CreatePseudoAcct = 0x0002, // The transaction can create a pseudo account,
// which implies createAcct
MustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object
MayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT
// object, but does not have to
OverrideFreeze = 0x0010, // The transaction can override some freeze rules
ChangeNftCounts = 0x0020, // The transaction can mint or burn an NFT
CreateMptIssuance = 0x0040, // The transaction can create a new MPT issuance
DestroyMptIssuance = 0x0080, // The transaction can destroy an MPT issuance
MustAuthorizeMpt = 0x0100, // The transaction MUST create or delete an MPT
// object (except by issuer)
MayAuthorizeMpt = 0x0200, // The transaction MAY create or delete an MPT
// object (except by issuer)
MayDeleteMpt = 0x0400, // The transaction MAY delete an MPT object. May not create.
MustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault
MayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault
MayCreateMpt = 0x2000, // The transaction MAY create an MPT object, except for issuer.
};
// The inner static_cast is not redundant: the underlying type is narrower than
// `int`, so the operands integer-promote and the result has to be narrowed back.
// safeCast rejects that narrowing, but every input bit is a Privilege bit by
// construction, so the result is always representable.
constexpr Privilege
operator|(Privilege lhs, Privilege rhs)
{
using Underlying = std::underlying_type_t<Privilege>;
return static_cast<Privilege>(
static_cast<Underlying>(safeCast<Underlying>(lhs) | safeCast<Underlying>(rhs)));
}
constexpr Privilege
operator&(Privilege lhs, Privilege rhs)
{
using Underlying = std::underlying_type_t<Privilege>;
return static_cast<Privilege>(
static_cast<Underlying>(safeCast<Underlying>(lhs) & safeCast<Underlying>(rhs)));
}
/**
* Per-transaction metadata declared in transactions.macro.
*
* Every member has a default, so a transaction only needs to name the settings
* that differ from the common case. See the documentation at the top of
* transactions.macro for the authoring syntax.
*
* This is deliberately not a constexpr-friendly type: amendment identifiers are
* runtime-initialized `extern uint256 const` globals (see Feature.h), so a
* TxSettings can only be built at runtime.
*/
struct TxSettings
{
/**
* Whether an account may delegate this transaction to another account.
*/
Delegation delegable{Delegation::NotDelegable};
/**
* The amendment gating this transaction, or uint256{} if always available.
*/
// The `{}` looks redundant, because BaseUInt's default constructor already
// zeroes the value. It is not: without a default member initializer here,
// every partial designated initializer in transactions.macro trips the
// missing-designated-field-initializers warning, which the build treats as
// an error.
// NOLINTNEXTLINE(readability-redundant-member-init)
uint256 amendment{};
/**
* Operations this transaction is permitted to perform.
*/
Privilege privileges{Privilege::NoPriv};
};
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ class AMMBidBuilder;
* Type: ttAMM_BID (39)
* Delegable: Delegation::Delegable
* Amendment: featureAMM
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMBidBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AMMClawbackBuilder;
* Type: ttAMM_CLAWBACK (31)
* Delegable: Delegation::Delegable
* Amendment: featureAMMClawback
* Privileges: Privilege::MayDeleteAcct | Privilege::OverrideFreeze | Privilege::MayAuthorizeMpt
* Privileges: MayDeleteAcct | OverrideFreeze | MayAuthorizeMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMClawbackBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AMMCreateBuilder;
* Type: ttAMM_CREATE (35)
* Delegable: Delegation::Delegable
* Amendment: featureAMM
* Privileges: Privilege::CreatePseudoAcct | Privilege::MayCreateMpt
* Privileges: CreatePseudoAcct | MayCreateMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMCreateBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AMMDeleteBuilder;
* Type: ttAMM_DELETE (40)
* Delegable: Delegation::Delegable
* Amendment: featureAMM
* Privileges: Privilege::MustDeleteAcct | Privilege::MayDeleteMpt
* Privileges: MustDeleteAcct | MayDeleteMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AMMDepositBuilder;
* Type: ttAMM_DEPOSIT (36)
* Delegable: Delegation::Delegable
* Amendment: featureAMM
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMDepositBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AMMVoteBuilder;
* Type: ttAMM_VOTE (38)
* Delegable: Delegation::Delegable
* Amendment: featureAMM
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMVoteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AMMWithdrawBuilder;
* Type: ttAMM_WITHDRAW (37)
* Delegable: Delegation::Delegable
* Amendment: featureAMM
* Privileges: Privilege::MayDeleteAcct | Privilege::MayAuthorizeMpt
* Privileges: MayDeleteAcct | MayAuthorizeMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMWithdrawBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AccountDeleteBuilder;
* Type: ttACCOUNT_DELETE (21)
* Delegable: Delegation::NotDelegable
* Amendment: uint256{}
* Privileges: Privilege::MustDeleteAcct
* Privileges: MustDeleteAcct
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AccountDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class AccountSetBuilder;
* Type: ttACCOUNT_SET (3)
* Delegable: Delegation::NotDelegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AccountSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class BatchBuilder;
* Type: ttBATCH (71)
* Delegable: Delegation::NotDelegable
* Amendment: featureBatchV1_1
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use BatchBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class CheckCancelBuilder;
* Type: ttCHECK_CANCEL (18)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use CheckCancelBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class CheckCashBuilder;
* Type: ttCHECK_CASH (17)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::MayCreateMpt
* Privileges: MayCreateMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use CheckCashBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class CheckCreateBuilder;
* Type: ttCHECK_CREATE (16)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use CheckCreateBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class ClawbackBuilder;
* Type: ttCLAWBACK (30)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ClawbackBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class ConfidentialMPTClawbackBuilder;
* Type: ttCONFIDENTIAL_MPT_CLAWBACK (89)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTClawbackBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class ConfidentialMPTConvertBuilder;
* Type: ttCONFIDENTIAL_MPT_CONVERT (85)
* Delegable: Delegation::NotDelegable
* Amendment: featureConfidentialTransfer
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTConvertBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class ConfidentialMPTConvertBackBuilder;
* Type: ttCONFIDENTIAL_MPT_CONVERT_BACK (87)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTConvertBackBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class ConfidentialMPTMergeInboxBuilder;
* Type: ttCONFIDENTIAL_MPT_MERGE_INBOX (86)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTMergeInboxBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class ConfidentialMPTSendBuilder;
* Type: ttCONFIDENTIAL_MPT_SEND (88)
* Delegable: Delegation::Delegable
* Amendment: featureConfidentialTransfer
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTSendBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class CredentialAcceptBuilder;
* Type: ttCREDENTIAL_ACCEPT (59)
* Delegable: Delegation::Delegable
* Amendment: featureCredentials
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use CredentialAcceptBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class CredentialCreateBuilder;
* Type: ttCREDENTIAL_CREATE (58)
* Delegable: Delegation::Delegable
* Amendment: featureCredentials
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use CredentialCreateBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class CredentialDeleteBuilder;
* Type: ttCREDENTIAL_DELETE (60)
* Delegable: Delegation::Delegable
* Amendment: featureCredentials
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use CredentialDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class DIDDeleteBuilder;
* Type: ttDID_DELETE (50)
* Delegable: Delegation::Delegable
* Amendment: featureDID
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use DIDDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class DIDSetBuilder;
* Type: ttDID_SET (49)
* Delegable: Delegation::Delegable
* Amendment: featureDID
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use DIDSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class DelegateSetBuilder;
* Type: ttDELEGATE_SET (64)
* Delegable: Delegation::NotDelegable
* Amendment: featurePermissionDelegationV1_1
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use DelegateSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class DepositPreauthBuilder;
* Type: ttDEPOSIT_PREAUTH (19)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use DepositPreauthBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class EnableAmendmentBuilder;
* Type: ttAMENDMENT (100)
* Delegable: Delegation::NotDelegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use EnableAmendmentBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class EscrowCancelBuilder;
* Type: ttESCROW_CANCEL (4)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use EscrowCancelBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class EscrowCreateBuilder;
* Type: ttESCROW_CREATE (1)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use EscrowCreateBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class EscrowFinishBuilder;
* Type: ttESCROW_FINISH (2)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use EscrowFinishBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LedgerStateFixBuilder;
* Type: ttLEDGER_STATE_FIX (53)
* Delegable: Delegation::Delegable
* Amendment: fixNFTokenPageLinks
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LedgerStateFixBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanBrokerCoverClawbackBuilder;
* Type: ttLOAN_BROKER_COVER_CLAWBACK (78)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanBrokerCoverClawbackBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanBrokerCoverDepositBuilder;
* Type: ttLOAN_BROKER_COVER_DEPOSIT (76)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanBrokerCoverDepositBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanBrokerCoverWithdrawBuilder;
* Type: ttLOAN_BROKER_COVER_WITHDRAW (77)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::MayAuthorizeMpt
* Privileges: MayAuthorizeMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanBrokerCoverWithdrawBuilder to construct new transactions.
@@ -121,32 +121,6 @@ public:
{
return this->tx_->isFieldPresent(sfDestinationTag);
}
/**
* @brief Get sfCredentialIDs (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VECTOR256::type::value_type>
getCredentialIDs() const
{
if (hasCredentialIDs())
{
return this->tx_->at(sfCredentialIDs);
}
return std::nullopt;
}
/**
* @brief Check if sfCredentialIDs is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasCredentialIDs() const
{
return this->tx_->isFieldPresent(sfCredentialIDs);
}
};
/**
@@ -240,17 +214,6 @@ public:
return *this;
}
/**
* @brief Set sfCredentialIDs (SoeOptional)
* @return Reference to this builder for method chaining.
*/
LoanBrokerCoverWithdrawBuilder&
setCredentialIDs(std::decay_t<typename SF_VECTOR256::type::value_type> const& value)
{
object_[sfCredentialIDs] = value;
return *this;
}
/**
* @brief Build and return the LoanBrokerCoverWithdraw wrapper.
* @param publicKey The public key for signing.

View File

@@ -21,7 +21,7 @@ class LoanBrokerDeleteBuilder;
* Type: ttLOAN_BROKER_DELETE (75)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::MustDeleteAcct | Privilege::MayAuthorizeMpt
* Privileges: MustDeleteAcct | MayAuthorizeMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanBrokerDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanBrokerSetBuilder;
* Type: ttLOAN_BROKER_SET (74)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::CreatePseudoAcct | Privilege::MayAuthorizeMpt
* Privileges: CreatePseudoAcct | MayAuthorizeMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanBrokerSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanDeleteBuilder;
* Type: ttLOAN_DELETE (81)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanManageBuilder;
* Type: ttLOAN_MANAGE (82)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::MayModifyVault
* Privileges: MayModifyVault
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanManageBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanPayBuilder;
* Type: ttLOAN_PAY (84)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::MayAuthorizeMpt | Privilege::MustModifyVault
* Privileges: MayAuthorizeMpt | MustModifyVault
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanPayBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class LoanSetBuilder;
* Type: ttLOAN_SET (80)
* Delegable: Delegation::NotDelegable
* Amendment: featureLendingProtocol
* Privileges: Privilege::MayAuthorizeMpt | Privilege::MustModifyVault
* Privileges: MayAuthorizeMpt | MustModifyVault
*
* Immutable wrapper around STTx providing type-safe field access.
* Use LoanSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class MPTokenAuthorizeBuilder;
* Type: ttMPTOKEN_AUTHORIZE (57)
* Delegable: Delegation::Delegable
* Amendment: featureMPTokensV1
* Privileges: Privilege::MustAuthorizeMpt
* Privileges: MustAuthorizeMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use MPTokenAuthorizeBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class MPTokenIssuanceCreateBuilder;
* Type: ttMPTOKEN_ISSUANCE_CREATE (54)
* Delegable: Delegation::Delegable
* Amendment: featureMPTokensV1
* Privileges: Privilege::CreateMptIssuance
* Privileges: CreateMptIssuance
*
* Immutable wrapper around STTx providing type-safe field access.
* Use MPTokenIssuanceCreateBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class MPTokenIssuanceDestroyBuilder;
* Type: ttMPTOKEN_ISSUANCE_DESTROY (55)
* Delegable: Delegation::Delegable
* Amendment: featureMPTokensV1
* Privileges: Privilege::DestroyMptIssuance
* Privileges: DestroyMptIssuance
*
* Immutable wrapper around STTx providing type-safe field access.
* Use MPTokenIssuanceDestroyBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class MPTokenIssuanceSetBuilder;
* Type: ttMPTOKEN_ISSUANCE_SET (56)
* Delegable: Delegation::Delegable
* Amendment: featureMPTokensV1
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use MPTokenIssuanceSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class NFTokenAcceptOfferBuilder;
* Type: ttNFTOKEN_ACCEPT_OFFER (29)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use NFTokenAcceptOfferBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class NFTokenBurnBuilder;
* Type: ttNFTOKEN_BURN (26)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::ChangeNftCounts
* Privileges: ChangeNftCounts
*
* Immutable wrapper around STTx providing type-safe field access.
* Use NFTokenBurnBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class NFTokenCancelOfferBuilder;
* Type: ttNFTOKEN_CANCEL_OFFER (28)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use NFTokenCancelOfferBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class NFTokenCreateOfferBuilder;
* Type: ttNFTOKEN_CREATE_OFFER (27)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use NFTokenCreateOfferBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class NFTokenMintBuilder;
* Type: ttNFTOKEN_MINT (25)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::ChangeNftCounts
* Privileges: ChangeNftCounts
*
* Immutable wrapper around STTx providing type-safe field access.
* Use NFTokenMintBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class NFTokenModifyBuilder;
* Type: ttNFTOKEN_MODIFY (61)
* Delegable: Delegation::Delegable
* Amendment: featureDynamicNFT
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use NFTokenModifyBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class OfferCancelBuilder;
* Type: ttOFFER_CANCEL (8)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use OfferCancelBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class OfferCreateBuilder;
* Type: ttOFFER_CREATE (7)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::MayCreateMpt
* Privileges: MayCreateMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use OfferCreateBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class OracleDeleteBuilder;
* Type: ttORACLE_DELETE (52)
* Delegable: Delegation::Delegable
* Amendment: featurePriceOracle
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use OracleDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class OracleSetBuilder;
* Type: ttORACLE_SET (51)
* Delegable: Delegation::Delegable
* Amendment: featurePriceOracle
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use OracleSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class PaymentBuilder;
* Type: ttPAYMENT (0)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::CreateAcct | Privilege::MayCreateMpt
* Privileges: CreateAcct | MayCreateMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use PaymentBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class PaymentChannelClaimBuilder;
* Type: ttPAYCHAN_CLAIM (15)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use PaymentChannelClaimBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class PaymentChannelCreateBuilder;
* Type: ttPAYCHAN_CREATE (13)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use PaymentChannelCreateBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class PaymentChannelFundBuilder;
* Type: ttPAYCHAN_FUND (14)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use PaymentChannelFundBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class PermissionedDomainDeleteBuilder;
* Type: ttPERMISSIONED_DOMAIN_DELETE (63)
* Delegable: Delegation::Delegable
* Amendment: featurePermissionedDomains
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use PermissionedDomainDeleteBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class PermissionedDomainSetBuilder;
* Type: ttPERMISSIONED_DOMAIN_SET (62)
* Delegable: Delegation::Delegable
* Amendment: featurePermissionedDomains
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use PermissionedDomainSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class SetFeeBuilder;
* Type: ttFEE (101)
* Delegable: Delegation::NotDelegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SetFeeBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class SetRegularKeyBuilder;
* Type: ttREGULAR_KEY_SET (5)
* Delegable: Delegation::NotDelegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SetRegularKeyBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class SignerListSetBuilder;
* Type: ttSIGNER_LIST_SET (12)
* Delegable: Delegation::NotDelegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SignerListSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class SponsorshipSetBuilder;
* Type: ttSPONSORSHIP_SET (91)
* Delegable: Delegation::Delegable
* Amendment: featureSponsor
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SponsorshipSetBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class SponsorshipTransferBuilder;
* Type: ttSPONSORSHIP_TRANSFER (90)
* Delegable: Delegation::NotDelegable
* Amendment: featureSponsor
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SponsorshipTransferBuilder to construct new transactions.

View File

@@ -21,7 +21,7 @@ class TicketCreateBuilder;
* Type: ttTICKET_CREATE (10)
* Delegable: Delegation::Delegable
* Amendment: uint256{}
* Privileges: Privilege::NoPriv
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use TicketCreateBuilder to construct new transactions.

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