Compare commits

..

42 Commits

Author SHA1 Message Date
Ed Hennis
81e06a9ba8 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop:
  fix: Update PermissionedDEX invariant domain tracking for valid offer replacement (7387)
  feat: Check default fields are not default when serializing (6267)
  test: Migrate csf and xrpld-consensus Beast non-JTx tests to GTest (7046)
  test: Modularize Peerfinder component and migrate Peerfinder tests from Beast to GTest and GMock (7054)
  test: Add an RAII class to manage the env.parseFailureExpected flag (7669)
  build: Use custom libc in a devshell by default (7852)
  feat: Add fixCleanup3_4_0 amendment (no functionality yet) (7854)
  ci: Add llvm-tools-preview to rust toolchain (7853)
  ci: Update CI image (7850)
  ci: Update XRPLF/actions (7849)
  chore: Clean up grammar in PR template (7846)
  build: Create versioned compiler/tooling symlinks in nix environments (7844)
  ci: Use rust-overlay to bring Rust into nix (7837)
2026-07-23 21:03:45 -04:00
Ed Hennis
b3141d91fa Merge branch 'develop' into ximinez/directory 2026-07-21 19:29:00 -04:00
Ed Hennis
1b1cd4c260 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop: (21 commits)
  fix: Re-store nodes missing from both backends during online_delete rotation (7763)
  fix: Add amendment sponsor for AccountRootsDeletedClean (7801)
  fix: Update base_uint and test changes released in 3.1.3 (7570)
  fix: Handle rounding just above kMaxRep more accurately (7389)
  fix: Document and assert "after" is never null in invariants (7354)
  ci: Run full matrix only on `Ready to merge` or `Full CI build` labeled PRs (7689)
  fix: Strengthen Clawback invariant checks for MPT balances (7285)
  test: Add unit tests for IP address related functions (7744)
  ci: Add Rust to Nix docker image (7571)
  docs: Add more information about pre-commit hooks and how to set them up (7802)
  test: Add JSON array size tests (7592)
  chore: Enable most readability checks (7772)
  ci: Do not run conflict checker when label is applied (7774)
  chore: Run clang_tidy_check with `pass_filenames: false` from pre-commit (7800)
  feat: Add delegate filter param for account_tx RPC (6126)
  refactor: Move `jss.h` `include` out of `Indexes.h` (7799)
  test: Add tests for check doxygen style (7795)
  style: Add pre-commit hook to check doxygen style (7794)
  style: Unify style for all Doxygen comments (7776)
  fix: Improve Number addition/subtraction rounding (7369)
  ...
2026-07-14 20:37:19 -04:00
Ed Hennis
a482d6aced Merge branch 'develop' into ximinez/directory 2026-07-09 19:50:00 -04:00
Ed Hennis
299627298f Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop:
  chore: Enable most cppcoreguidelines checks (7660)
  test: Migrate basics Beast tests to GTest (7136)
  test: Migrate resource, shamap Beast tests to GTest (7133)
  chore: Enable most misc checks (7663)
  chore: Fix unity build (7730)
  chore: Enable most modernize checks (7664)
  chore: Delete dead code (7718)
  chore: Enable modernize-use-constraints (7715)
  chore: Enable modernize-avoid-bind (7711)
  chore: Enable modernize-use-auto (7707)
  build: Add protobuf dependencies to Nix (7706)
  feat: Enable ConfidentialTransfer and BatchV1_1 (7698)
  chore: Enable modernize-unary-static-assert (7705)
  chore: Make clang-tidy happy on macOS (7701)
  chore: Improve pre-commit hooks (7702)
  refactor: Retire DisallowIncomingV1 fix (7364)
  build: Add verify-headers target to cleanup headers (7670)
2026-07-07 18:17:50 -04:00
Ed Hennis
640798696b Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop:
  refactor: Retire InnerObjTemplate fix (7368)
  fix: Disable AMM creation with Vault shares (7666)
  test: Add tests for TMProofPathResponse and TMReplayDeltaResponse invalid hash/key sizes (7593)
  ci: [DEPENDABOT] bump actions/setup-python from 6.2.0 to 6.3.0 (7657)
  build: Don't reuse binaries between different C++ versions (7681)
  chore: Update pre-commit hooks && actions (7686)
  feat: Add an invariant to ensure object deletion also deletes its pseudo-account (7445)
  feat: Add Batch (XLS-56) V1_1 (6446)
  feat: Introduce lending 1.1 amendment and add `MemoData` field to `VaultDelete` transaction (6324)
2026-07-01 16:10:38 -04:00
Ed Hennis
47512afad5 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop: (69 commits)
  chore: Use std::ranges where possible (7634)
  ci: Use macOS 26 Tahoe with apple-clang 21 (7601)
  build: Mark sec256k1 and mpt-crypto as transitive headers (7658)
  chore: Add a script to nicely format clang-tidy output (7650)
  chore: Enable most bugprone checks (7643)
  feat: Confidential Transfer for MPT (5860)
  fix: Use trustline balance direction to validate IOU PaymentMint/PaymentBurn (7584)
  fix: Unify freeze checks for pseudo-account deposit/withdraw (7382)
  fix: Block delegate tx from being queued (7640)
  chore: Enable groups of clang-tidy checks by default (7637)
  ci: Better determine when we need to run full clang-tidy (7635)
  refactor: Retire NFTokenReserve fix (7367)
  refactor: Retire Clawback amendment (7353)
  refactor: Rename (mostly keylet) functions to more closely match the docs (7059)
  build: Switch to a new conan XRPLF remote, again (7638)
  chore: Revert "build: Switch to a new conan XRPLF remote (7622)" (7623)
  build: Switch to a new conan XRPLF remote (7622)
  build: Align xrpld RPM packaging with DEB package (7529)
  chore: Use clang-tidy v22 new features (7427)
  build: Patch nix binaries in CMake (7539)
  ...
2026-06-30 17:16:29 -04:00
Ed Hennis
ff3708a757 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop: (48 commits)
  test: Add null check unit test for `Oracle::aggregatePrice` (7306)
  ci: Patch conan recipe for Nix to be able to use on macOS (7532)
  ci: Run sanitizers on release builds too (7527)
  fix: Correct hybrid offer deletion on credential expiry (6843)
  ci: Make sanitizer flags lists in the profile, not a string (7449)
  ci: Make configurations launch on certain event types (7447)
  fix: Add [[maybe_unused]] to fix320Enabled for assert=OFF builds (7446)
  ci: Add `gh` and `file` to nix packages (7444)
  fix: Disable transaction invariants (7409)
  perf: Dispatch "hasInvalidAmount()" on type tag instead of dynamic_cast (7402)
  refactor: Retire fixUniversalNumber amendment (5962)
  test: Do not create data directory for memory databases (7323)
  ci: Launch upload-conan-deps on profile change (7442)
  fix: Fix Number comparison operator (7406)
  feat: Use C++ 23 standard (7431)
  refactor: Introduce XRPL_ASSERT_IF for amendment-gated assertions (7378)
  refactor: Change config section and key string literals into constants (7095)
  refactor: Use `std::move` and `std::string_view` where possible (7424)
  refactor: Use const function arguments where possible (7423)
  ci: Use XRPLF/actions build-multiarch-image workflow (7428)
  ...
2026-06-12 11:50:43 -04:00
Ed Hennis
391a1e442c Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop: (41 commits)
  release: Bump version to 3.2.0-rc2 (7348)
  refactor: Enable support for `fixCleanup3_2_0` amendment (7347)
  release: Bump version to 3.2.0-rc1 (7335)
  fix: Fix a rounding error at the `Number::maxRep` cusp (7051)
  ci: Only push docker images in XRPLF/rippled (7330)
  ci: [DEPENDABOT] bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (7322)
  ci: [DEPENDABOT] bump codecov/codecov-action from 6.0.0 to 6.0.1 (7321)
  ci: [DEPENDABOT] bump docker/build-push-action from 7.1.0 to 7.2.0 (7320)
  ci: [DEPENDABOT] bump docker/metadata-action from 6.0.0 to 6.1.0 (7319)
  ci: [DEPENDABOT] bump docker/login-action from 4.1.0 to 4.2.0 (7318)
  fix: Update `clang-tidy` to include `src/tests` directory header check (7307)
  chore: Pin Python packages for codegen using uv (7329)
  style: Use shfmt instead of bashate (7326)
  fix: Fix edge-case where vault-depositor may get stuck (7139)
  fix: Fix `VaultInvariant` and `VaultDeposit` precision bugs at IOU scale boundaries (7272)
  ci: Add clang to nix images (7308)
  fix: Include management-fee delta in doOverpayment assertion (7039)
  fix: Fix clang-tidy pre-commit hook to locate compile_commands.json from repo root (7325)
  fix: Use consistent scale for `debtTotal` (7093)
  fix: Skip deleted book directories and non-root modifications in `ValidBookDirectory` invariant (7312)
  ...
2026-05-27 15:33:45 -04:00
Ed Hennis
9e77212900 Merge branch 'develop' into ximinez/directory 2026-05-19 16:53:55 -04:00
Ed Hennis
93f5a0e217 Merge branch 'develop' into ximinez/directory 2026-05-19 10:15:35 -04:00
Ed Hennis
71367f361c Merge branch 'develop' into ximinez/directory 2026-05-19 05:16:12 -04:00
Ed Hennis
931d21b2a7 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop:
  refactor: Clean up comments post-clang-tidy changes (7283)
  release: Set version to 3.3.0-b0 (7280)
  refactor: Rename static constants (7120)
  refactor: Use `isFlag` where possible instead of bitwise math (7278)
  ci: Update XRPLF/actions (7281)
2026-05-16 09:49:20 -04:00
Ed Hennis
c99feb82e7 Merge branch 'develop' into ximinez/directory 2026-05-14 20:39:26 -04:00
Ed Hennis
7b53a5e0c5 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop:
  chore: Consolidate fix amendments (7134)
  ci: Add Conan retry (7147)
  fix: Backport Permissioned Domains fixes (7016)
  refactor: Move unhex lookup table out of function (7104)
  refactor: Improve Forwarded header field parsing (7126)
  fix: Check network ID in `transactionSignFor` (7102)
  feat: Implement nix-based Dockerfile for CI (7083)
2026-05-14 10:46:04 -04:00
Ed Hennis
4a02518497 Merge branch 'develop' into ximinez/directory 2026-05-13 12:03:59 -04:00
Ed Hennis
0b6c3630cc Merge branch 'develop' into ximinez/directory 2026-05-12 19:19:05 -04:00
Ed Hennis
4d04ba5be5 Merge branch 'develop' into ximinez/directory 2026-05-12 16:26:28 -04:00
Ed Hennis
ed53557d41 Merge branch 'develop' into ximinez/directory 2026-05-11 13:34:31 -04:00
Ed Hennis
e4d10393f3 Merge branch 'develop' into ximinez/directory 2026-05-07 18:10:21 -04:00
Ed Hennis
fcc7f57e82 Merge branch 'develop' into ximinez/directory 2026-05-07 14:18:48 -04:00
Ed Hennis
a25229f154 Merge branch 'develop' into ximinez/directory 2026-05-07 13:28:50 -04:00
Ed Hennis
eb6eaf2532 Merge branch 'develop' into ximinez/directory 2026-05-06 22:34:41 -04:00
Ed Hennis
ff987fc7c6 Merge branch 'develop' into ximinez/directory 2026-05-06 14:18:20 -04:00
Ed Hennis
d21137c4c1 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop:
  fix: Fix regressions in `server_definitions` (7008)
  chore: Do not duplicate sanitizer flags (7058)
  ci: Run pre-commit on diff in clang-tidy workflow (7078)
  ci: Use XRPLF/create-issue (7076)
  ci: Rewrite clang-tidy workflow(s) in a reusable manner (7062)
  chore: Ignore identifier-naming update in git blame (7066)
  refactor: Enable clang-tidy `readability-identifier-naming` check (6571)
2026-05-05 16:45:19 -04:00
Ed Hennis
ac7db2e621 Remove orphaned code that was introduced by a bad merge or rebase 2026-05-05 15:48:00 -04:00
Ed Hennis
42005a8080 Merge branch 'develop' into ximinez/directory 2026-05-01 13:59:22 -04:00
Ed Hennis
1f7b1b3a78 Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory
* XRPLF/develop:
  feat: Create new transaction testing framework `TxTest` (6537)
  feat: Add cleanup amendment for 3.2.0 (7037)
  fix: Fix ubsan flagged issues (6151)
2026-04-28 15:28:15 -05:00
Ed Hennis
80b90544c5 Merge branch 'develop' into ximinez/directory 2026-04-25 14:46:02 -04:00
Ed Hennis
00b9a8cd67 Merge branch 'develop' into ximinez/directory 2026-04-23 15:56:20 -04:00
Ed Hennis
3be49f814a Merge branch 'develop' into ximinez/directory 2026-04-22 23:40:54 -04:00
Ed Hennis
1674fabe81 Merge branch 'develop' into ximinez/directory 2026-04-22 14:49:21 -04:00
Ed Hennis
6dfa47ce7a Merge branch 'develop' into ximinez/directory 2026-04-22 13:10:52 -04:00
Ed Hennis
bef095be65 Merge branch 'develop' into ximinez/directory 2026-04-21 18:58:08 -04:00
Ed Hennis
8e5d774c36 Merge branch 'develop' into ximinez/directory 2026-04-20 17:49:55 -04:00
Ed Hennis
fb8fb30f6c Merge branch 'develop' into ximinez/directory 2026-04-20 15:45:12 -04:00
Ed Hennis
a553001125 Merge branch 'develop' into ximinez/directory 2026-04-20 11:39:16 -04:00
Ed Hennis
57782e84ee Merge branch 'develop' into ximinez/directory 2026-04-17 18:14:35 -04:00
Ed Hennis
9d5076c8a9 Merge branch 'develop' into ximinez/directory 2026-04-16 13:44:45 -04:00
Ed Hennis
1af379e09f Merge branch 'develop' into ximinez/directory 2026-04-15 19:06:37 -04:00
Ed Hennis
1ced0875ae Merge branch 'develop' into ximinez/directory 2026-04-15 14:29:04 -04:00
Ed Hennis
53e6d7580a rabbit hole: refactor dirAdd to find gaps in "full" directories.
- This would potentially be very expensive to implement, so don't.
- However, it might be a good start for a ledger fix option.
2026-04-13 19:50:28 -04:00
49 changed files with 91 additions and 3738 deletions

View File

@@ -1,5 +1,5 @@
{
"image_tag": "sha-40cdf49",
"image_tag": "sha-3122de8",
"configs": {
"ubuntu": [
{

View File

@@ -13,7 +13,6 @@ on:
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "bin/check-tools.sh"
- "bin/default-loader-path.sh"
- "bin/install-sanitizer-libs.sh"
pull_request:
paths:
@@ -25,7 +24,6 @@ on:
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "bin/check-tools.sh"
- "bin/default-loader-path.sh"
- "bin/install-sanitizer-libs.sh"
workflow_dispatch:

View File

@@ -1,114 +0,0 @@
# Verifies the committed snapshots of `bin/check-tools.sh` output for each Nix
# environment (see nix/check-tools/). If the environment changes — a new image
# tag, an updated flake.lock, a different tool list — without the matching
# snapshot being regenerated and committed, this workflow fails so the drift is
# caught in review.
#
# To regenerate the snapshots, see nix/check-tools/README.md.
name: Check tools
on:
pull_request:
paths:
- ".github/workflows/check-tools.yml"
- ".github/scripts/strategy-matrix/linux.json"
- "bin/check-tools.sh"
- "nix/**"
- "flake.nix"
- "flake.lock"
- "rust-toolchain.toml"
push:
branches:
- "develop"
paths:
- ".github/workflows/check-tools.yml"
- ".github/scripts/strategy-matrix/linux.json"
- "bin/check-tools.sh"
- "nix/**"
- "flake.nix"
- "flake.lock"
- "rust-toolchain.toml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
# The nix-nixos image tag is pinned alongside the build matrix in linux.json,
# so snapshots are checked against the exact image CI builds against.
linux-image-tag:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Read nix image tag
id: tag
run: echo "tag=$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)" >>"${GITHUB_OUTPUT}"
# One job for all environments; they differ only in whether the tools come
# from the nix-nixos container (Linux) or `nix develop` (macOS).
check-tools:
needs: linux-image-tag
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
snapshot: nix/check-tools/nix-ubuntu-amd64.txt
nix_develop: false
- runner: ubuntu-24.04-arm
snapshot: nix/check-tools/nix-ubuntu-arm64.txt
nix_develop: false
- runner: macos-26-apple-clang-21
snapshot: nix/check-tools/macos.txt
nix_develop: true
runs-on: ${{ matrix.runner }}
# Linux runs inside the pinned nix-nixos image; macOS runs natively and uses
# the flake's dev shell instead (see the run step below).
container: ${{ !matrix.nix_develop && format('ghcr.io/xrplf/xrpld/nix-ubuntu:{0}', needs.linux-image-tag.outputs.tag) || null }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
with:
enable_ccache: false
- name: Regenerate snapshot
env:
CHECK_TOOLS_SKIP_CLONE: "1"
# check-tools.sh skips some macOS tools when CI is set; the snapshots
# capture the full `nix develop` environment, so unset it here.
CI: ""
run: |
if [ "${{ matrix.nix_develop }}" = "true" ]; then
# `nix develop` prints the dev-shell greeting first; keep only the
# check-tools.sh output (from the "Detected OS:" line onward).
nix --extra-experimental-features "nix-command flakes" develop \
-c bash bin/check-tools.sh | sed -n '/^Detected OS:/,$p' >"${{ matrix.snapshot }}"
else
bash bin/check-tools.sh >"${{ matrix.snapshot }}"
fi
- name: Verify snapshot is up to date
run: |
if ! git diff --exit-code -- "${{ matrix.snapshot }}"; then
echo "::error::${{ matrix.snapshot }} is out of date. Regenerate it (see nix/check-tools/README.md) and commit the result."
exit 1
fi
- name: Upload regenerated snapshot
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: check-tools-${{ runner.os }}-${{ runner.arch }}
path: ${{ matrix.snapshot }}

View File

@@ -90,7 +90,6 @@ jobs:
.clang-tidy
.codecov.yml
bin/check-tools.sh
bin/default-loader-path.sh
cfg/**
cmake/**
conan/**

View File

@@ -28,7 +28,6 @@ on:
- ".clang-tidy"
- ".codecov.yml"
- "bin/check-tools.sh"
- "bin/default-loader-path.sh"
- "cfg/**"
- "cmake/**"
- "conan/**"

View File

@@ -41,7 +41,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3122de8
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

View File

@@ -34,7 +34,7 @@ jobs:
needs: [determine-files]
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-40cdf49"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-3122de8"
permissions:
contents: read
issues: write

View File

@@ -40,7 +40,7 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3122de8
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}

View File

@@ -30,10 +30,8 @@ missing=()
checked=0
# check <name> [probe-command...]
# Runs the probe (default: "<name> --version"), capturing both stdout and
# stderr, and prints one aligned line: the status, the name, and the first
# non-blank line of the probe output (its version). Records <name> as missing
# if the command is not found or exits non-zero.
# Runs the probe (default: "<name> --version") quietly. Records <name> as
# missing if the command is not found or exits non-zero.
check() {
local name="$1"
shift
@@ -42,11 +40,10 @@ check() {
probe=("${name}" --version)
fi
echo "Checking ${name}..."
checked=$((checked + 1))
local output version
if output="$("${probe[@]}" 2>&1)"; then
version="$(printf '%s\n' "${output}" | grep -m1 '[^[:space:]]' || true)"
printf ' [ ok ] %-20s %s\n' "${name}" "${version}"
if "${probe[@]}" | head -n 1; then
printf ' [ ok ] %s\n' "${name}"
else
printf ' [MISS] %s\n' "${name}"
missing+=("${name}")
@@ -88,14 +85,12 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
check file
check less
check make
# net-tools netstat reports "net-tools X.Y"; macOS ships BSD netstat with no
# version flag, so fall back to a presence marker there.
check netstat sh -c 'command -v netstat >/dev/null && { netstat --version 2>&1 | grep -m1 -oE "net-tools [0-9.]+" || echo present; }'
check netstat which netstat
check ninja
check perl perl -e 'print "$^V\n"'
check perl
check pkg-config
check vim
check zip bash -c 'zip --version 2>&1 | grep -m1 -oE "Zip [0-9.]+"'
check zip
# These tools are present in our Linux CI images and in local development
# setups, but not in the macOS CI environment. So check them everywhere

View File

@@ -29,27 +29,6 @@ if(CMAKE_GENERATOR STREQUAL "Xcode")
set(is_xcode TRUE)
endif()
# --------------------------------------------------------------------
# Nix toolchain detection
# --------------------------------------------------------------------
# True when the C++ compiler resolves into the Nix store. CMAKE_CXX_COMPILER may
# be referenced through a symlink outside the store (a Nix profile, a /usr/bin
# alternative, ...), so resolve the real path before matching.
set(is_nix_compiler FALSE)
get_filename_component(_cxx_real "${CMAKE_CXX_COMPILER}" REALPATH)
if(_cxx_real MATCHES "^/nix/store/")
set(is_nix_compiler TRUE)
endif()
unset(_cxx_real)
# True inside the Nix CI Docker image, identified by the /nix/ci-env tree it
# ships (see nix/docker/Dockerfile). The dev shell and bare systems don't have
# it, so it distinguishes the CI image from other Nix-compiler environments.
set(is_ci_image FALSE)
if(EXISTS "/nix/ci-env/bin")
set(is_ci_image TRUE)
endif()
# --------------------------------------------------------------------
# Operating system detection
# --------------------------------------------------------------------

View File

@@ -1,37 +1,26 @@
#[===================================================================[
Patch executables to run in non-Nix environments.
The Nix toolchain links binaries against an ELF interpreter (loader)
that lives in the Nix store, so the resulting binaries don't run elsewhere.
`patch_nix_binary` adds a POST_BUILD step that resets the interpreter
to the system default loader and drops the rpath.
The Nix-based CI image links binaries against an ELF interpreter (loader)
that lives in the Nix store, so the resulting binaries don't run elsewhere
(including once installed from the .deb package). `patch_nix_binary` adds a
POST_BUILD step that resets the interpreter to the system default loader and
drops the rpath.
This runs by default for Nix-toolchain builds (determined by whether the compiler resolves under /nix/store/).
Those builds are where binaries get a Nix-store loader.
It is opted out of by setting the XRPLD_NO_PATCH_NIX_BINARY environment variable
the plain Nix dev shells set it, since their binaries link a newer glibc
and must not be retargeted to the system loader.
Non-Nix builds (a system compiler, already using the system loader) and sanitizer builds
(runtime libraries resolved through the rpath) are skipped too.
Everywhere else `patch_nix_binary` is a no-op.
The default loader is resolved by bin/default-loader-path.sh.
This is only active inside the Nix-based image, detected by the presence of
/tmp/loader-path.sh (shipped by that image, resolves the default loader). It
is skipped for sanitizer builds, whose runtime libraries are resolved through
the rpath. Everywhere else `patch_nix_binary` is a no-op.
#]===================================================================]
include_guard(GLOBAL)
include(CompilationEnv)
# Resolves the system default ELF loader path for the current architecture.
set(_loader_path_script "${CMAKE_SOURCE_DIR}/bin/default-loader-path.sh")
# Provided by the Nix-based CI image; prints the system default ELF loader path.
set(_loader_path_script "/tmp/loader-path.sh")
if(
is_linux
AND NOT SANITIZERS_ENABLED
AND is_nix_compiler
AND NOT DEFINED ENV{XRPLD_NO_PATCH_NIX_BINARY}
)
if(is_linux AND NOT SANITIZERS_ENABLED AND EXISTS "${_loader_path_script}")
execute_process(
COMMAND "${_loader_path_script}"
OUTPUT_VARIABLE DEFAULT_LOADER_PATH

View File

@@ -171,8 +171,9 @@ else()
# Clang wrapper supplies those paths itself (via -nostdinc++), so at compile time the
# flag is unused -> Clang errors under our -Werror. At link time the flag IS consumed
# (it selects the C++ runtime), so we move it there instead of dropping it entirely.
get_filename_component(_cxx_real "${CMAKE_CXX_COMPILER}" REALPATH)
if(
is_nix_compiler
_cxx_real MATCHES "^/nix/store/"
AND is_linux
AND is_clang
AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc"

View File

@@ -36,19 +36,6 @@ elseif(is_gcc)
endif()
endif()
# A Nix compiler is only meant to be used from a managed environment: the xrpld
# dev shell (which exports XRPL_DEVSHELL) or the CI image. Using one from a bare
# shell usually means a leaked toolchain (picked up via PATH or a Conan profile)
# and leads to confusing breakage, so fail early with guidance.
if(is_nix_compiler AND NOT is_ci_image AND NOT DEFINED ENV{XRPL_DEVSHELL})
message(
FATAL_ERROR
"A Nix compiler (${CMAKE_CXX_COMPILER}) is being used outside the xrpld "
"dev shell. Enter it with `nix develop` (see docs/build/nix.md) before "
"configuring the build."
)
endif()
# check for in-source build and fail
if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
message(

8
docs/build/nix.md vendored
View File

@@ -154,14 +154,6 @@ conan install .. --output-folder . --build '*' --settings build_type=Release
To update `flake.lock` to the latest revision use `nix flake update` command.
## Tooling snapshots
The tool versions in each Nix environment are recorded in
[`nix/check-tools/`](../../nix/check-tools) and verified by CI. If you change the
environment (bump the CI image tag, update `flake.lock`, or edit the tool list in
`bin/check-tools.sh`), CI fails until you regenerate and commit the affected
snapshot — see [`nix/check-tools/README.md`](../../nix/check-tools/README.md).
## Troubleshooting
See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues,

View File

@@ -43,14 +43,7 @@ enum ApplyFlags : std::uint32_t {
// Transaction shouldn't be applied
// Signatures shouldn't be checked
TapDryRun = 0x1000,
// Transaction is being preflighted as the payload of a
// TransactionProposalCreate. Its signatures are collected on-ledger
// afterward, so signature-presence checks (e.g. Batch signer matching)
// are skipped at proposal-creation time (On-Chain Cosigner spec
// §5.3.1.2).
TapProposal = 0x2000
TapDryRun = 0x1000
};
constexpr ApplyFlags

View File

@@ -1,87 +0,0 @@
#pragma once
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TxFormats.h>
#include <cstdint>
namespace xrpl::proposal {
/**
* Owner-reserve increments held by a proposal of an ordinary transaction.
*/
constexpr std::uint32_t kProposalOwnerCount = 5;
/**
* Owner-reserve increments held by a proposal of a Batch transaction. A
* proposed Batch stores up to eight inner transactions plus multi-account
* signatures, so it reserves more than an ordinary proposed transaction.
*/
constexpr std::uint32_t kBatchProposalOwnerCount = 10;
/**
* Owner-reserve increments held by a proposal of the given transaction.
*/
inline std::uint32_t
proposalOwnerCount(STObject const& proposedTx)
{
return proposedTx.getFieldU16(sfTransactionType) == ttBATCH ? kBatchProposalOwnerCount
: kProposalOwnerCount;
}
/**
* Whether the proposed transaction is itself a proposal transaction, which
* would nest one proposal inside another.
*
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN and ttTRANSACTION_PROPOSAL_CANCEL
* once those transactions exist.
*/
inline bool
isProposalTx(STObject const& proposedTx)
{
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
}
/**
* Whether the proposed transaction is independently submittable through the
* ordinary multi-sign path: not a nested proposal, not a pseudo-transaction,
* not itself flagged as someone else's inner batch transaction, and — if it
* is a Batch — none of its own inner transactions is a nested proposal or a
* pseudo-transaction either. A Batch inner transaction cannot itself be
* pseudo (preflight0 rejects the pseudo/tfInnerBatchTxn combination
* generically), but that guard lives outside this feature, so it is checked
* again here rather than relied upon.
*/
bool
isValidProposal(STObject const& proposedTx);
/**
* Whether the proposed transaction carries any signature field.
*
* A proposal is stored in unsigned canonical form; signatures may only ever
* arrive through TransactionProposalSign. Shared by the create-time check and
* the invariant that guards the stored entry, so the two cannot drift apart.
*/
inline bool
hasSignatureField(STObject const& proposedTx)
{
return proposedTx.isFieldPresent(sfTxnSignature) || proposedTx.isFieldPresent(sfSigners) ||
proposedTx.isFieldPresent(sfBatchSigners) ||
proposedTx.isFieldPresent(sfCounterpartySignature) ||
proposedTx.isFieldPresent(sfSponsorSignature);
}
/**
* Whether the proposed transaction's SigningPubKey is present and empty, as
* unsigned canonical form requires. An absent field is not the same as an
* empty one, and a populated one means the payload was already signed.
*/
inline bool
hasEmptySigningPubKey(STObject const& proposedTx)
{
return proposedTx.isFieldPresent(sfSigningPubKey) &&
proposedTx.getFieldVL(sfSigningPubKey).empty();
}
} // namespace xrpl::proposal

View File

@@ -187,20 +187,6 @@ check(uint256 const& key) noexcept
}
/** @} */
/**
* A TransactionProposal
*/
/** @{ */
Keylet
txProposal(AccountID const& target, std::uint32_t ticketSequence) noexcept;
inline Keylet
txProposal(uint256 const& key) noexcept
{
return {ltTRANSACTION_PROPOSAL, key};
}
/** @} */
/**
* A DepositPreauth
*/

View File

@@ -15,7 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(DefragDirectories, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)

View File

@@ -636,24 +636,5 @@ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({
{sfSponseeNode, SoeRequired},
}))
/** A ledger object holding a pending transaction proposal.
The proposed transaction is stored unsigned in ProposedTransaction, and
signatures accumulate on it over time. It becomes fully signed either from
a single signature by its own Account (or that account's Delegate), or once
the weight collected in its Signers field meets that account's quorum. At
that point the stored transaction is one that anyone may copy and submit.
\sa keylet::txProposal
*/
LEDGER_ENTRY(ltTRANSACTION_PROPOSAL, 0x0091, TransactionProposal, transaction_proposal, ({
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfOwner, SoeRequired},
{sfProposedTransaction, SoeRequired},
{sfExpiration, SoeRequired},
{sfOwnerNode, SoeRequired},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -216,7 +216,6 @@ TYPED_SFIELD(sfLoanID, UINT256, 38)
TYPED_SFIELD(sfReferenceHolding, UINT256, 39)
TYPED_SFIELD(sfBlindingFactor, UINT256, 40)
TYPED_SFIELD(sfObjectID, UINT256, 41)
TYPED_SFIELD(sfProposalID, UINT256, 42)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -358,7 +357,6 @@ TYPED_SFIELD(sfHighSponsor, ACCOUNT, 28)
TYPED_SFIELD(sfLowSponsor, ACCOUNT, 29)
TYPED_SFIELD(sfCounterpartySponsor, ACCOUNT, 30)
TYPED_SFIELD(sfSponsee, ACCOUNT, 31)
TYPED_SFIELD(sfSigningFor, ACCOUNT, 32)
// vector of 256-bit
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::kSmdNever)
@@ -424,7 +422,6 @@ UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35)
UNTYPED_SFIELD(sfBook, OBJECT, 36)
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning)
UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning)
UNTYPED_SFIELD(sfProposedTransaction, OBJECT, 39)
// array of objects (common)
// ARRAY/1 is reserved for end of array

View File

@@ -1194,20 +1194,6 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet,
{sfRemainingOwnerCount, SoeOptional},
}))
/** This transaction posts an unsigned transaction on-ledger as a
TransactionProposal, pending multi-signature collection. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 92, TransactionProposalCreate,
Delegation::NotDelegable,
featureCosign,
NoPriv,
({
{sfProposedTransaction, SoeRequired},
{sfExpiration, SoeRequired},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html

View File

@@ -1,242 +0,0 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::ledger_entries {
class TransactionProposalBuilder;
/**
* @brief Ledger Entry: TransactionProposal
*
* Type: ltTRANSACTION_PROPOSAL (0x0091)
* RPC Name: transaction_proposal
*
* Immutable wrapper around SLE providing type-safe field access.
* Use TransactionProposalBuilder to construct new ledger entries.
*/
class TransactionProposal : public LedgerEntryBase
{
public:
static constexpr LedgerEntryType entryType = ltTRANSACTION_PROPOSAL;
/**
* @brief Construct a TransactionProposal ledger entry wrapper from an existing SLE object.
* @throws std::runtime_error if the ledger entry type doesn't match.
*/
explicit TransactionProposal(SLE::const_pointer sle)
: LedgerEntryBase(std::move(sle))
{
// Verify ledger entry type
if (sle_->getType() != entryType)
{
throw std::runtime_error("Invalid ledger entry type for TransactionProposal");
}
}
// Ledger entry-specific field getters
/**
* @brief Get sfPreviousTxnID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getPreviousTxnID() const
{
return this->sle_->at(sfPreviousTxnID);
}
/**
* @brief Get sfPreviousTxnLgrSeq (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT32::type::value_type
getPreviousTxnLgrSeq() const
{
return this->sle_->at(sfPreviousTxnLgrSeq);
}
/**
* @brief Get sfOwner (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_ACCOUNT::type::value_type
getOwner() const
{
return this->sle_->at(sfOwner);
}
/**
* @brief Get sfProposedTransaction (SoeRequired)
* @note This is an untyped field (unknown).
* @return The field value.
*/
[[nodiscard]]
STObject
getProposedTransaction() const
{
return this->sle_->getFieldObject(sfProposedTransaction);
}
/**
* @brief Get sfExpiration (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT32::type::value_type
getExpiration() const
{
return this->sle_->at(sfExpiration);
}
/**
* @brief Get sfOwnerNode (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT64::type::value_type
getOwnerNode() const
{
return this->sle_->at(sfOwnerNode);
}
};
/**
* @brief Builder for TransactionProposal ledger entries.
*
* Provides a fluent interface for constructing ledger entries with method chaining.
* Uses STObject internally for flexible ledger entry construction.
* Inherits common field setters from LedgerEntryBuilderBase.
*/
class TransactionProposalBuilder : public LedgerEntryBuilderBase<TransactionProposalBuilder>
{
public:
/**
* @brief Construct a new TransactionProposalBuilder with required fields.
* @param previousTxnID The sfPreviousTxnID field value.
* @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value.
* @param owner The sfOwner field value.
* @param proposedTransaction The sfProposedTransaction field value.
* @param expiration The sfExpiration field value.
* @param ownerNode The sfOwnerNode field value.
*/
TransactionProposalBuilder(std::decay_t<typename SF_UINT256::type::value_type> const& previousTxnID,std::decay_t<typename SF_UINT32::type::value_type> const& previousTxnLgrSeq,std::decay_t<typename SF_ACCOUNT::type::value_type> const& owner,STObject const& proposedTransaction,std::decay_t<typename SF_UINT32::type::value_type> const& expiration,std::decay_t<typename SF_UINT64::type::value_type> const& ownerNode)
: LedgerEntryBuilderBase<TransactionProposalBuilder>(ltTRANSACTION_PROPOSAL)
{
setPreviousTxnID(previousTxnID);
setPreviousTxnLgrSeq(previousTxnLgrSeq);
setOwner(owner);
setProposedTransaction(proposedTransaction);
setExpiration(expiration);
setOwnerNode(ownerNode);
}
/**
* @brief Construct a TransactionProposalBuilder from an existing SLE object.
* @param sle The existing ledger entry to copy from.
* @throws std::runtime_error if the ledger entry type doesn't match.
*/
TransactionProposalBuilder(SLE::const_pointer sle)
{
if (sle->at(sfLedgerEntryType) != ltTRANSACTION_PROPOSAL)
{
throw std::runtime_error("Invalid ledger entry type for TransactionProposal");
}
object_ = *sle;
}
/**
* @brief Ledger entry-specific field setters
*/
/**
* @brief Set sfPreviousTxnID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalBuilder&
setPreviousTxnID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfPreviousTxnID] = value;
return *this;
}
/**
* @brief Set sfPreviousTxnLgrSeq (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalBuilder&
setPreviousTxnLgrSeq(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfPreviousTxnLgrSeq] = value;
return *this;
}
/**
* @brief Set sfOwner (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalBuilder&
setOwner(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfOwner] = value;
return *this;
}
/**
* @brief Set sfProposedTransaction (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalBuilder&
setProposedTransaction(STObject const& value)
{
object_.setFieldObject(sfProposedTransaction, value);
return *this;
}
/**
* @brief Set sfExpiration (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Set sfOwnerNode (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalBuilder&
setOwnerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfOwnerNode] = value;
return *this;
}
/**
* @brief Build and return the completed TransactionProposal wrapper.
* @param index The ledger entry index.
* @return The constructed ledger entry wrapper.
*/
TransactionProposal
build(uint256 const& index)
{
return TransactionProposal{std::make_shared<SLE>(std::move(object_), index)};
}
};
} // namespace xrpl::ledger_entries

View File

@@ -1,155 +0,0 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class TransactionProposalCreateBuilder;
/**
* @brief Transaction: TransactionProposalCreate
*
* Type: ttTRANSACTION_PROPOSAL_CREATE (92)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use TransactionProposalCreateBuilder to construct new transactions.
*/
class TransactionProposalCreate : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CREATE;
/**
* @brief Construct a TransactionProposalCreate transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit TransactionProposalCreate(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for TransactionProposalCreate");
}
}
// Transaction-specific field getters
/**
* @brief Get sfProposedTransaction (SoeRequired)
* @note This is an untyped field.
* @return The field value.
*/
[[nodiscard]]
STObject
getProposedTransaction() const
{
return this->tx_->getFieldObject(sfProposedTransaction);
}
/**
* @brief Get sfExpiration (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT32::type::value_type
getExpiration() const
{
return this->tx_->at(sfExpiration);
}
};
/**
* @brief Builder for TransactionProposalCreate transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class TransactionProposalCreateBuilder : public TransactionBuilderBase<TransactionProposalCreateBuilder>
{
public:
/**
* @brief Construct a new TransactionProposalCreateBuilder with required fields.
* @param account The account initiating the transaction.
* @param proposedTransaction The sfProposedTransaction field value.
* @param expiration The sfExpiration field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
TransactionProposalCreateBuilder(SF_ACCOUNT::type::value_type account,
STObject const& proposedTransaction, std::decay_t<typename SF_UINT32::type::value_type> const& expiration, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<TransactionProposalCreateBuilder>(ttTRANSACTION_PROPOSAL_CREATE, account, sequence, fee)
{
setProposedTransaction(proposedTransaction);
setExpiration(expiration);
}
/**
* @brief Construct a TransactionProposalCreateBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
TransactionProposalCreateBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CREATE)
{
throw std::runtime_error("Invalid transaction type for TransactionProposalCreateBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfProposedTransaction (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalCreateBuilder&
setProposedTransaction(STObject const& value)
{
object_.setFieldObject(sfProposedTransaction, value);
return *this;
}
/**
* @brief Set sfExpiration (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalCreateBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Build and return the TransactionProposalCreate wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
TransactionProposalCreate
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return TransactionProposalCreate{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -59,10 +59,6 @@ public:
, j(j)
{
XRPL_ASSERT((flags & TapBatch) == TapBatch, "Batch apply flag should be set");
XRPL_ASSERT_IF(
(flags & TapProposal) != TapNone,
(flags & TapDryRun) != TapNone,
"xrpl::PreflightContext : proposal preflight implies dry run");
}
PreflightContext(
@@ -74,10 +70,6 @@ public:
: registry(registry), tx(tx), rules(std::move(rules)), flags(flags), j(j)
{
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
XRPL_ASSERT_IF(
(flags & TapProposal) != TapNone,
(flags & TapDryRun) != TapNone,
"xrpl::PreflightContext : proposal preflight implies dry run");
}
PreflightContext&

View File

@@ -1,44 +0,0 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class TransactionProposalCreate : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit TransactionProposalCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl

View File

@@ -1,49 +0,0 @@
# check-tools snapshots
These files capture the output of [`bin/check-tools.sh`](../../bin/check-tools.sh)
— the versions of the development tooling — in each Nix environment:
| File | Environment |
| --------------------- | ----------------------------------- |
| `nix-nixos-amd64.txt` | `nix-nixos` CI image, `linux/amd64` |
| `nix-nixos-arm64.txt` | `nix-nixos` CI image, `linux/arm64` |
| `macos.txt` | macOS, inside `nix develop` |
The [`check-tools`](../../.github/workflows/check-tools.yml) workflow regenerates
each snapshot in its environment and fails if it differs from the committed file.
So if you change the environment (bump the image tag in
[`linux.json`](../../.github/scripts/strategy-matrix/linux.json), update
`flake.lock`, change the tool list in `check-tools.sh`, …) you must regenerate
and commit the affected snapshots.
Each snapshot is `check-tools.sh` stdout with the git-clone connectivity check
skipped (`CHECK_TOOLS_SKIP_CLONE=1`), so it contains only deterministic version
data. On macOS the dev-shell greeting that `nix develop` prints first is dropped
with `sed -n '/^Detected OS:/,$p'`.
## Regenerating
The two Linux snapshots come from the `nix-nixos` image (Docker or a compatible
runtime such as Apple `container`). The image tag is pinned in `linux.json`:
```bash
img="ghcr.io/xrplf/xrpld/nix-nixos:$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)"
for arch in amd64 arm64; do
container run --rm -i -e CHECK_TOOLS_SKIP_CLONE=1 -a "${arch}" --entrypoint bash "${img}" -s \
<bin/check-tools.sh >"nix/check-tools/nix-nixos-${arch}.txt"
done
```
(With Docker, replace `container run … -a "${arch}"` with
`docker run … --platform "linux/${arch}"`.)
The macOS snapshot is generated locally. `CI=` is unset so `check-tools.sh`
checks the full dev-shell tool set (it otherwise skips some tools when `CI` is
set):
```bash
CI= nix develop -c bash -c 'CHECK_TOOLS_SKIP_CLONE=1 bash bin/check-tools.sh' |
sed -n '/^Detected OS:/,$p' \
>nix/check-tools/macos.txt
```

View File

@@ -1,47 +0,0 @@
Detected OS: macos (Darwin arm64)
Core build tools:
[ ok ] cmake cmake version 4.1.2
[ ok ] conan Conan version 2.28.1
[ ok ] git git version 2.54.0
[ ok ] python3 Python 3.13.13
Development tooling:
[ ok ] ccache ccache version 4.13.6
[ ok ] clang clang version 22.1.7
[ ok ] clang++ clang version 22.1.7
[ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0
[ ok ] curl curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
[ ok ] file file-5.47
[ ok ] less less 692 (PCRE2 regular expressions)
[ ok ] make GNU Make 4.4.1
[ ok ] netstat present
[ ok ] ninja 1.13.2
[ ok ] perl v5.42.0
[ ok ] pkg-config 0.29.2
[ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
[ ok ] zip Zip 3.0
[ ok ] clang-format clang-format version 22.1.7
[ ok ] dot dot - graphviz version 12.2.1 (0)
[ ok ] doxygen 1.16.1
[ ok ] gcovr gcovr 8.4
[ ok ] gh gh version 2.94.0 (nixpkgs)
[ ok ] git-cliff git-cliff 2.13.1
[ ok ] git-lfs git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3)
[ ok ] gpg gpg (GnuPG) 2.4.9
[ ok ] pre-commit pre-commit 4.5.1
[ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
Rust toolchain:
[ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21)
[ ok ] cargo-audit cargo-audit-audit 0.22.1
[ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5
[ ok ] cargo-nextest cargo-nextest 0.9.137
[ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14)
[ ok ] rust-analyzer rust-analyzer 1.95.0 (59807616 2026-04-14)
[ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14)
[ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14)
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
All 36 checked tools are present and runnable.

View File

@@ -1,55 +0,0 @@
Detected OS: linux (Linux x86_64)
Core build tools:
[ ok ] cmake cmake version 4.1.2
[ ok ] conan Conan version 2.28.1
[ ok ] git git version 2.54.0
[ ok ] python3 Python 3.13.13
Development tooling:
[ ok ] ccache ccache version 4.13.6
[ ok ] clang clang version 22.1.7
[ ok ] clang++ clang version 22.1.7
[ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0
[ ok ] curl curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
[ ok ] file file-5.47
[ ok ] less less 692 (PCRE2 regular expressions)
[ ok ] make GNU Make 4.4.1
[ ok ] netstat net-tools 2.10
[ ok ] ninja 1.13.2
[ ok ] perl v5.42.0
[ ok ] pkg-config 0.29.2
[ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
[ ok ] zip Zip 3.0
[ ok ] clang-format clang-format version 22.1.7
[ ok ] dot dot - graphviz version 12.2.1 (0)
[ ok ] doxygen 1.16.1
[ ok ] gcovr gcovr 8.4
[ ok ] gh gh version 2.94.0 (nixpkgs)
[ ok ] git-cliff git-cliff 2.13.1
[ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux amd64; go 1.26.3)
[ ok ] gpg gpg (GnuPG) 2.4.9
[ ok ] pre-commit pre-commit 4.5.1
[ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
Rust toolchain:
[ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21)
[ ok ] cargo-audit cargo-audit-audit 0.22.1
[ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5
[ ok ] cargo-nextest cargo-nextest 0.9.137
[ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14)
[ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14)
[ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14)
[ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14)
GCC toolchain:
[ ok ] gcc gcc (GCC) 15.2.0
[ ok ] g++ g++ (GCC) 15.2.0
[ ok ] gcov gcov (GCC) 15.2.0
Mold:
[ ok ] mold mold 2.41.0 (compatible with GNU ld)
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
All 40 checked tools are present and runnable.

View File

@@ -1,55 +0,0 @@
Detected OS: linux (Linux aarch64)
Core build tools:
[ ok ] cmake cmake version 4.1.2
[ ok ] conan Conan version 2.28.1
[ ok ] git git version 2.54.0
[ ok ] python3 Python 3.13.13
Development tooling:
[ ok ] ccache ccache version 4.13.6
[ ok ] clang clang version 22.1.7
[ ok ] clang++ clang version 22.1.7
[ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0
[ ok ] curl curl 8.20.0 (aarch64-unknown-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
[ ok ] file file-5.47
[ ok ] less less 692 (PCRE2 regular expressions)
[ ok ] make GNU Make 4.4.1
[ ok ] netstat net-tools 2.10
[ ok ] ninja 1.13.2
[ ok ] perl v5.42.0
[ ok ] pkg-config 0.29.2
[ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
[ ok ] zip Zip 3.0
[ ok ] clang-format clang-format version 22.1.7
[ ok ] dot dot - graphviz version 12.2.1 (0)
[ ok ] doxygen 1.16.1
[ ok ] gcovr gcovr 8.4
[ ok ] gh gh version 2.94.0 (nixpkgs)
[ ok ] git-cliff git-cliff 2.13.1
[ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux arm64; go 1.26.3)
[ ok ] gpg gpg (GnuPG) 2.4.9
[ ok ] pre-commit pre-commit 4.5.1
[ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
Rust toolchain:
[ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21)
[ ok ] cargo-audit cargo-audit-audit 0.22.1
[ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5
[ ok ] cargo-nextest cargo-nextest 0.9.137
[ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14)
[ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14)
[ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14)
[ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14)
GCC toolchain:
[ ok ] gcc gcc (GCC) 15.2.0
[ ok ] g++ g++ (GCC) 15.2.0
[ ok ] gcov gcov (GCC) 15.2.0
Mold:
[ ok ] mold mold 2.41.0 (compatible with GNU ld)
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
All 40 checked tools are present and runnable.

View File

@@ -50,17 +50,12 @@ let
# compilerName is the command used to print the version, or null for none.
makeShell =
{
shellName,
stdenv,
compilerName,
version ? null,
versionedTools ? [ ],
extraPackages ? [ ],
warningHook ? "",
# Opt out of PatchNixBinary.cmake retargeting binaries to the system
# loader. The plain toolchain links a newer glibc, so it must not be
# patched; the custom toolchain patches by default.
noPatchNixBinary ? false,
}:
let
compilerVersionHook =
@@ -78,20 +73,14 @@ let
tools = versionedTools;
});
in
(pkgs.mkShell.override { inherit stdenv; }) (
{
packages = commonPackages ++ versionedLinks ++ extraPackages;
# Marks a managed dev shell, so the build (XrplSanity.cmake) can tell an
# intentional Nix toolchain from one leaked into a bare shell.
XRPL_DEVSHELL = shellName;
shellHook = ''
echo "Welcome to xrpld development shell";
${compilerVersionHook}
${warningHook}
'';
}
// pkgs.lib.optionalAttrs noPatchNixBinary { XRPLD_NO_PATCH_NIX_BINARY = "1"; }
);
(pkgs.mkShell.override { inherit stdenv; }) {
packages = commonPackages ++ versionedLinks ++ extraPackages;
shellHook = ''
echo "Welcome to xrpld development shell";
${compilerVersionHook}
${warningHook}
'';
};
in
rec {
# macOS: Nix Clang. Linux: Nix GCC.
@@ -100,7 +89,6 @@ rec {
# gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no
# custom glibc, so they fall back to the plain nixpkgs toolchain.
gcc = makeShell {
shellName = "gcc";
stdenv = customGccStdenv;
compilerName = "gcc";
version = gccVersion;
@@ -109,7 +97,6 @@ rec {
};
clang = makeShell {
shellName = "clang";
stdenv = customClangStdenv;
compilerName = "clang";
version = llvmVersion;
@@ -118,7 +105,6 @@ rec {
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
no-compiler = makeShell {
shellName = "no-compiler";
stdenv = pkgs.stdenvNoCC;
compilerName = null;
};
@@ -129,23 +115,19 @@ rec {
# makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc.
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
gcc-plain = makeShell {
shellName = "gcc-plain";
stdenv = plainGccStdenv;
compilerName = "gcc";
version = gccVersion;
versionedTools = gccVersionedTools;
extraPackages = [ plainGcov ];
warningHook = plainWarningHook;
noPatchNixBinary = true;
};
clang-plain = makeShell {
shellName = "clang-plain";
stdenv = plainClangStdenv;
compilerName = "clang";
version = llvmVersion;
versionedTools = clangVersionedTools;
warningHook = plainWarningHook;
noPatchNixBinary = true;
};
}

View File

@@ -58,7 +58,7 @@ ENV GIT_SSL_CAINFO="/nix/ci-env/etc/ssl/certs/ca-bundle.crt"
# Externally-built dynamically-linked ELF binaries hard-code the loader path
# (e.g. /lib64/ld-linux-x86-64.so.2) in their PT_INTERP header. Install it
# from the Nix store when the base image doesn't already provide one.
COPY bin/default-loader-path.sh /tmp/loader-path.sh
COPY nix/docker/loader-path.sh /tmp/loader-path.sh
RUN <<EOF
target="$(/tmp/loader-path.sh)"

View File

@@ -75,9 +75,8 @@ toolchain being present at runtime. Two pieces make that work:
- **An expected dynamic linker in the image.**
Binaries built in Nix environments reference a dynamic linker from Nix store paths, which won't be present in the base image. However,
[`bin/default-loader-path.sh`](../../bin/default-loader-path.sh) reports the
expected loader path for the current architecture, so we can patch the binaries
to use the correct loader.
[`loader-path.sh`](./loader-path.sh) reports the expected loader path for the
current architecture, so we can patch the binaries to use the correct loader.
The build then verifies all of this end to end, and the C++ and Rust programs
go through the same pipeline: each is compiled in `final`, has its `PT_INTERP`
@@ -94,8 +93,8 @@ whose resulting binary is patched and run like the others.
| File | Purpose |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. |
| [`./test_files/rust/`](./test_files/rust) | Rust smoke test: rustc sources + a cargo proc-macro workspace + compile/run scripts. |
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
| [`/bin/default-loader-path.sh`](../../bin/default-loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |

View File

@@ -16,23 +16,7 @@ let
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
'';
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
# cargo has a linker). That default may be different from the clang we pin here,
# so it shadows our clang and the build can silently use a different compiler
# version. Drop that cc from every propagation channel instead of pinning a
# replacement: the toolchain then carries no compiler and cargo just uses the
# active shell's stdenv cc. Must cover all channels — rust-overlay uses both
# propagatedBuildInputs and depsHostHostPropagated.
rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
rustToolchain =
let
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
in
rustToolchainBase.overrideAttrs (old: {
propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]);
depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]);
});
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
# names (gcc, g++, clang-tidy, ...). Several tools probe for a

View File

@@ -26,6 +26,14 @@ namespace xrpl {
namespace directory {
struct Gap
{
uint64_t const page;
SLE::pointer node;
uint64_t const nextPage;
SLE::pointer next;
};
std::uint64_t
createRoot(
ApplyView& view,
@@ -126,7 +134,9 @@ insertPage(
if (page == 0)
return std::nullopt;
if (!view.rules().enabled(fixDirectoryLimit) && page >= kDirNodeMaxPages) // Old pages limit
{
return std::nullopt;
}
// We are about to create a new node; we'll link it to
// the chain first:
@@ -147,12 +157,8 @@ insertPage(
// Save some space by not specifying the value 0 since it's the default.
if (page != 1)
node->setFieldU64(sfIndexPrevious, page - 1);
XRPL_ASSERT_PARTS(!nextPage, "xrpl::directory::insertPage", "nextPage has default value");
/* Reserved for future use when directory pages may be inserted in
* between two other pages instead of only at the end of the chain.
if (nextPage)
node->setFieldU64(sfIndexNext, nextPage);
*/
describe(node);
view.insert(node);
@@ -168,7 +174,7 @@ ApplyView::dirAdd(
uint256 const& key,
std::function<void(SLE::ref)> const& describe)
{
auto root = peek(directory);
auto const root = peek(directory);
if (!root)
{
@@ -178,6 +184,43 @@ ApplyView::dirAdd(
auto [page, node, indexes] = directory::findPreviousPage(*this, directory, root);
if (rules().enabled(featureDefragDirectories))
{
// If there are more nodes than just the root, and there's no space in
// the last one, walk backwards to find one with space, or to find one
// missing.
std::optional<directory::Gap> gapPages;
while (page && indexes.size() >= kDIR_NODE_MAX_PAGES)
{
// Find a page with space, or a gap in pages.
auto [prevPage, prevNode, prevIndexes] =
directory::findPreviousPage(*this, directory, node);
if (!gapPages && prevPage != page - 1)
gapPages.emplace(prevPage, prevNode, page, node);
page = prevPage;
node = prevNode;
indexes = prevIndexes;
}
// We looped through all the pages back to the root.
if (!page)
{
// If we found a gap, use it.
if (gapPages)
{
return directory::insertPage(
*this,
gapPages->page,
gapPages->node,
gapPages->nextPage,
gapPages->next,
key,
directory,
describe);
}
std::tie(page, node, indexes) = directory::findPreviousPage(*this, directory, root);
}
}
// If there's space, we use it:
if (indexes.size() < kDirNodeMaxEntries)
{

View File

@@ -1,39 +0,0 @@
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
namespace xrpl::proposal {
bool
isValidProposal(STObject const& proposedTx)
{
if (isProposalTx(proposedTx))
return false;
if (isPseudoTx(proposedTx))
return false;
if (proposedTx.isFieldPresent(sfFlags) &&
(proposedTx.getFieldU32(sfFlags) & tfInnerBatchTxn) != 0u)
return false;
if (proposedTx.getFieldU16(sfTransactionType) == ttBATCH &&
proposedTx.isFieldPresent(sfRawTransactions))
{
STArray const& innerTxns = proposedTx.getFieldArray(sfRawTransactions);
for (STObject const& inner : innerTxns)
{
if (isProposalTx(inner) || isPseudoTx(inner))
return false;
}
}
return true;
}
} // namespace xrpl::proposal

View File

@@ -5,7 +5,6 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/OracleHelpers.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -57,7 +56,6 @@ isReserveSponsorAllowed(TxType txType)
ttACCOUNT_SET,
ttREGULAR_KEY_SET,
ttSPONSORSHIP_TRANSFER,
ttTRANSACTION_PROPOSAL_CREATE,
};
return kReserveSponsorAllowed.contains(txType);
}
@@ -257,8 +255,6 @@ isLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& accoun
// to tecNO_PERMISSION.
return false;
}
case ltTRANSACTION_PROPOSAL:
return sle.getAccountID(sfOwner) == account;
default:
// LCOV_EXCL_START
UNREACHABLE("xrpl::isLedgerEntryOwner : object is not supported by sponsorship.");
@@ -282,7 +278,6 @@ isLedgerEntrySupportedBySponsorship(SLE const& sle)
case ltSIGNER_LIST:
case ltCREDENTIAL:
case ltRIPPLE_STATE:
case ltTRANSACTION_PROPOSAL:
return true;
default:
return false;
@@ -309,11 +304,6 @@ getLedgerEntryOwnerCount(SLE const& sle)
return 1;
return 2 + static_cast<std::uint32_t>(sle.getFieldArray(sfSignerEntries).size());
}
case ltTRANSACTION_PROPOSAL:
// Mirror TransactionProposalCreate's own reserve sizing so that
// creation and sponsorship accounting agree: a proposed Batch
// reserves more than an ordinary proposal.
return proposal::proposalOwnerCount(sle.getFieldObject(sfProposedTransaction));
case ltACCOUNT_ROOT:
// LCOV_EXCL_START
UNREACHABLE("AccountRoots are not supported by object sponsorship.");

View File

@@ -104,7 +104,6 @@ enum class LedgerNameSpace : std::uint16_t {
LoanBroker = 'l', // lower-case L
Loan = 'L',
Sponsorship = '>',
TransactionProposal = 'y',
// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
Contract [[deprecated]] = 'c',
@@ -359,14 +358,6 @@ check(AccountID const& id, std::uint32_t seq) noexcept
return {ltCHECK, indexHash(LedgerNameSpace::Check, id, seq)};
}
Keylet
txProposal(AccountID const& target, std::uint32_t ticketSequence) noexcept
{
return {
ltTRANSACTION_PROPOSAL,
indexHash(LedgerNameSpace::TransactionProposal, target, ticketSequence)};
}
Keylet
depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept
{

View File

@@ -324,17 +324,6 @@ preflight(
beast::Journal j)
{
PreflightContext const pfCtx(registry, tx, rules, flags, j);
// XRPL_ASSERT_IF in the PreflightContext constructor only fires in debug
// builds; re-check the same invariant here so a release build can't
// silently skip a proposed transaction's signature-presence checks
// outside of a dry run.
if ((flags & TapProposal) != TapNone && (flags & TapDryRun) == TapNone)
{
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
return {pfCtx, {tefEXCEPTION, TxConsequences{tx}}};
}
try
{
return {pfCtx, invokePreflight(pfCtx)};
@@ -356,14 +345,6 @@ preflight(
beast::Journal j)
{
PreflightContext const pfCtx(registry, tx, parentBatchId, rules, flags, j);
// See the comment in the other preflight() overload above.
if ((flags & TapProposal) != TapNone && (flags & TapDryRun) == TapNone)
{
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
return {pfCtx, {tefEXCEPTION, TxConsequences{tx}}};
}
try
{
return {pfCtx, invokePreflight(pfCtx)};

View File

@@ -5,7 +5,6 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
@@ -80,10 +79,7 @@ LoanSet::preflight(PreflightContext const& ctx)
return tx.getFieldObject(sfCounterpartySignature);
return std::nullopt;
}();
// A proposed LoanSet is stored unsigned; its CounterpartySignature is
// collected on-ledger afterward, so its absence here is expected, not an
// error (On-Chain Cosigner spec §5.3.1.2).
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig && (ctx.flags & TapProposal) == 0)
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig)
{
JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
return temBAD_SIGNER;

View File

@@ -1,319 +0,0 @@
#include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/SignerEntries.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/applySteps.h>
#include <algorithm>
#include <cstdint>
#include <exception>
#include <expected>
#include <memory>
namespace xrpl {
NotTEC
TransactionProposalCreate::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfExpiration] == 0)
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: zero expiration.";
return temBAD_EXPIRATION;
}
STObject const proposedTx = ctx.tx.getFieldObject(sfProposedTransaction);
// The proposed transaction must pass its own static checks under the
// current rules, so no statically-dead proposal can be stored. This also
// guarantees every field common to all transactions (TransactionType,
// Account, Fee, Sequence, ...) is present, since applyTemplate throws
// otherwise; the checks below can therefore read those fields directly
// without re-checking presence. TapDryRun accepts the unsigned canonical
// form without a signature check; TapProposal additionally skips
// signature-presence checks (e.g. Batch signer matching), which are
// deferred to submission time (On-Chain Cosigner spec §5.3.1.2). A
// proposedTx that fails here is rejected with its own type's preflight
// code (or temMALFORMED if it isn't even a valid instance of that type),
// ahead of the Cosigner-specific structural checks below.
try
{
STTx const stx{STObject{proposedTx}};
auto const inner =
xrpl::preflight(ctx.registry, ctx.rules, stx, TapDryRun | TapProposal, ctx.j);
if (!isTesSuccess(inner.ter))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
"failed preflight: "
<< transHuman(inner.ter);
// Surface the proposed transaction type's own preflight code
// rather than collapsing it to a generic error (On-Chain Cosigner
// spec §5.3.1).
return inner.ter;
}
}
catch (std::exception const& e)
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is "
"malformed: "
<< e.what();
return temMALFORMED;
}
// The proposed transaction must be independently submittable through the
// ordinary multi-sign path: no nested proposals, no pseudo-transactions,
// no batch inner transactions — and, if it is a Batch, none of its own
// inner transactions may be a nested proposal or a pseudo-transaction
// either.
if (!proposal::isValidProposal(proposedTx))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is not "
"independently submittable.";
return temINVALID;
}
// The proposed transaction is stored in its unsigned canonical form; the
// ledger populates its signature fields as contributions arrive.
if (proposal::hasSignatureField(proposedTx))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
"carries signature fields.";
return temBAD_SIGNER;
}
if (!proposal::hasEmptySigningPubKey(proposedTx))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
"SigningPubKey must be present and empty.";
return temBAD_SIGNER;
}
// The proposed transaction must be ticket-based: it must carry a
// TicketSequence and must not use a live Sequence. Sequence is a required
// common field, so "no Sequence" is expressed as a Sequence of 0 rather
// than an absent field. A ticket decouples the proposal from the target
// account's live sequence, so unrelated target-account activity cannot
// invalidate it while signatures are collected (On-Chain Cosigner spec
// §4.2.1).
if (!proposedTx.isFieldPresent(sfTicketSequence) || proposedTx.getFieldU32(sfSequence) != 0)
return temSEQ_AND_TICKET;
// If this transaction itself is paying with a Ticket, and the proposed
// transaction is targeting that same account and Ticket, then applying
// this transaction consumes the very Ticket the proposal depends on
// before the proposal is even stored: the proposal would be dead on
// arrival, and its only recourse would be TransactionProposalCancel.
if (ctx.tx.getSeqProxy().isTicket() &&
proposedTx.getAccountID(sfAccount) == ctx.tx.getAccountID(sfAccount) &&
proposedTx.getFieldU32(sfTicketSequence) == ctx.tx.getSeqProxy().value())
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
"reuses the Ticket this transaction itself consumes.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
TransactionProposalCreate::preclaim(PreclaimContext const& ctx)
{
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: already expired.";
return tecEXPIRED;
}
auto const proposedTx = ctx.tx.getFieldObject(sfProposedTransaction);
// Once the proposed transaction's own ledger bound has passed it can never
// be applied, so the proposal is dead on arrival. The bound is the one the
// ordinary path uses for tefMAX_LEDGER: the last ledger in which the
// proposed transaction may still be submitted (On-Chain Cosigner spec
// §4.5).
if (proposedTx.isFieldPresent(sfLastLedgerSequence) &&
proposedTx.getFieldU32(sfLastLedgerSequence) <= ctx.view.seq())
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
"LastLedgerSequence has passed.";
return tecEXPIRED;
}
AccountID const target = proposedTx.getAccountID(sfAccount);
auto const sleTarget = ctx.view.read(keylet::account(target));
if (!sleTarget)
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: target account "
"does not exist.";
return tecNO_TARGET;
}
// A pseudo-account cannot authorize a transaction through a SignerList.
if (isPseudoAccount(sleTarget))
return tecNO_PERMISSION;
// Only the target account itself, an account on its SignerList, or (if
// the proposed transaction's own type has been delegated by the target,
// Permission Delegation / XLS-75) that delegate or an account on the
// delegate's own SignerList, may create a proposal against it. Otherwise
// any account could spam or squat the target's Tickets with unwanted
// proposals (On-Chain Cosigner V1 scope).
if (AccountID const proposer = ctx.tx.getAccountID(sfAccount); proposer != target)
{
// Whether `proposer` is `account` itself or an entry on `account`'s
// applicable SignerList.
auto isAuthorizedFor = [&](AccountID const& account) -> std::expected<bool, TER> {
if (proposer == account)
return true;
auto const sleSigners = ctx.view.read(keylet::signerList(account));
if (!sleSigners)
return false;
auto const accountSigners = SignerEntries::deserialize(*sleSigners, ctx.j, "ledger");
if (!accountSigners)
return std::unexpected(TER{accountSigners.error()});
return std::ranges::any_of(
*accountSigners, [&](auto const& entry) { return entry.account == proposer; });
};
auto isSigner = isAuthorizedFor(target);
if (!isSigner)
return isSigner.error();
// A delegate that the target has granted permission over the
// proposed transaction's own type — or one of that delegate's own
// signers — is equally authorized: it will need to help complete
// the proposed transaction's own authorization anyway once the
// proposal is submitted.
if (!*isSigner && proposedTx.isFieldPresent(sfDelegate))
{
AccountID const delegateAccount = proposedTx.getAccountID(sfDelegate);
// NOLINTNEXTLINE(readability-suspicious-call-argument)
auto const sleDelegate = ctx.view.read(keylet::delegate(target, delegateAccount));
if (sleDelegate &&
isTesSuccess(checkTxPermission(sleDelegate, STTx{STObject{proposedTx}})))
{
isSigner = isAuthorizedFor(delegateAccount);
if (!isSigner)
return isSigner.error();
}
}
if (!*isSigner)
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposer is "
"not the target account, one of its "
"signers, or an authorized delegate.";
return tecNO_PERMISSION;
}
}
std::uint32_t const ticketSequence = proposedTx.getFieldU32(sfTicketSequence);
// The proposal reserves the ticket for as long as it exists (On-Chain
// Cosigner spec §4.2.1, §5.3.2): a ticket that doesn't exist yet can't be
// reserved.
if (!ctx.view.exists(keylet::ticket(target, ticketSequence)))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: target ticket "
"does not exist.";
return tefNO_TICKET;
}
if (ctx.view.exists(keylet::txProposal(target, ticketSequence)))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: duplicate proposal.";
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
TransactionProposalCreate::doApply()
{
auto const sle = view().peek(keylet::account(accountID_));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const proposedTx = ctx_.tx.getFieldObject(sfProposedTransaction);
std::uint32_t const ownerCount = proposal::proposalOwnerCount(proposedTx);
// The proposal holds a full transaction plus its collected signatures, so
// it reserves more than a typical ledger entry (5 increments; 10 for a
// proposed Batch).
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(),
sle,
preFeeBalance_,
{.ownerCountDelta = static_cast<int>(ownerCount)},
ctx_.journal);
!isTesSuccess(ret))
return ret;
AccountID const target = proposedTx.getAccountID(sfAccount);
std::uint32_t const ticketSequence = proposedTx.getFieldU32(sfTicketSequence);
Keylet const proposalKeylet = keylet::txProposal(target, ticketSequence);
auto sleProposal = std::make_shared<SLE>(proposalKeylet);
sleProposal->setAccountID(sfOwner, accountID_);
sleProposal->setFieldObject(sfProposedTransaction, proposedTx);
sleProposal->setFieldU32(sfExpiration, ctx_.tx[sfExpiration]);
view().insert(sleProposal);
auto viewJ = ctx_.registry.get().getJournal("View");
{
auto const page = view().dirInsert(
keylet::ownerDir(accountID_), proposalKeylet, describeOwnerDir(accountID_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
sleProposal->setFieldU64(sfOwnerNode, *page);
}
increaseOwnerCount(ctx_.getApplyViewContext(), sle, ownerCount, viewJ);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleProposal);
return tesSUCCESS;
}
void
TransactionProposalCreate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work). Object-level
// invariants for the TransactionProposal ledger entry (unsigned canonical
// form, non-zero Expiration, correct ProposalID key, sorted/unique signer
// arrays) belong in a protocol-level ValidTransactionProposal check.
}
bool
TransactionProposalCreate::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl

View File

@@ -199,11 +199,7 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
bool const isAccountReserveSponsorship =
isCreateOrReassign && reserveSponsor && !ctx.tx.isFieldPresent(sfObjectID);
// A proposed SponsorshipTransfer is stored unsigned; its SponsorSignature
// is collected on-ledger afterward, so its absence here is expected, not
// an error (On-Chain Cosigner spec §5.3.1.2).
if (isAccountReserveSponsorship && !ctx.tx.isFieldPresent(sfSponsorSignature) &&
(ctx.flags & TapProposal) == 0)
if (isAccountReserveSponsorship && !ctx.tx.isFieldPresent(sfSponsorSignature))
{
JLOG(ctx.j.debug()) << "preflight: account sponsorship requires sfSponsorSignature";
return temMALFORMED;

View File

@@ -406,13 +406,6 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
{
XRPL_ASSERT(
ctx.tx.getTxnType() == ttBATCH, "xrpl::Batch::preflightSigValidated : batch transaction");
// A proposed Batch is stored unsigned; its BatchSigners are collected
// on-ledger afterward, so the signer-presence match belongs to submission
// time, not proposal creation (On-Chain Cosigner spec §5.3.1.2).
if ((ctx.flags & TapProposal) != 0)
return tesSUCCESS;
auto const parentBatchId = ctx.tx.getTransactionID();
auto const outerAccount = ctx.tx.getAccountID(sfAccount);
// Accounts that must sign the batch: each inner authorizer and counterparty

File diff suppressed because it is too large Load Diff

View File

@@ -1,350 +0,0 @@
#include <test/jtx/proposal.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <test/jtx/batch.h>
#include <test/jtx/multisign.h>
#include <test/jtx/ticket.h>
#include <test/jtx/utility.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <map>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test::jtx::proposal {
json::Value
create(Account const& proposer, json::Value const& proposedTx, std::uint32_t expiration)
{
json::Value jv;
jv[jss::TransactionType] = jss::TransactionProposalCreate;
jv[jss::Account] = proposer.human();
jv[sfProposedTransaction.jsonName] = proposedTx;
jv[sfExpiration.jsonName] = expiration;
return jv;
}
json::Value
unsignedPayload(Env const& env, json::Value tx, std::uint32_t ticketSeq)
{
// Unsigned canonical form: an empty SigningPubKey and no signature fields
// at all. Signatures may only ever arrive through TransactionProposalSign.
tx[jss::SigningPubKey] = "";
// Ticket-based rather than sequence-based. Sequence is a required common
// field, so "no Sequence" is expressed as a Sequence of 0.
tx[jss::Sequence] = 0;
tx[sfTicketSequence.jsonName] = ticketSeq;
// The target account pays this fee when the completed transaction is
// submitted, so it is fixed now. A fee already chosen by the caller stands.
fillFee(tx, *env.current());
return tx;
}
json::Value
innerTx(json::Value tx, std::uint32_t seq)
{
return batch::Inner{std::move(tx), seq}.getTxn();
}
json::Value
unsignedBatch(
Env const& env,
Account const& target,
std::uint32_t ticketSeq,
std::uint32_t flags,
std::vector<json::Value> const& inners,
std::optional<std::uint32_t> numSigners)
{
// Each inner account other than the outer one will contribute one
// BatchSigners entry once the signatures are collected, and the outer fee
// has to cover them from the start (Batch::calculateBaseFee).
std::uint32_t const signers = numSigners ? *numSigners : [&]() {
std::set<std::string> participants;
for (auto const& inner : inners)
{
if (auto const account = inner[jss::Account].asString(); account != target.human())
participants.insert(account);
}
return static_cast<std::uint32_t>(participants.size());
}();
json::Value jv = batch::outer(
target,
0,
batch::calcBatchFee(env, signers, static_cast<std::uint32_t>(inners.size())),
flags);
json::Value& rawTransactions = jv[jss::RawTransactions];
for (auto const& inner : inners)
rawTransactions[rawTransactions.size()][jss::RawTransaction] = inner;
return unsignedPayload(env, std::move(jv), ticketSeq);
}
void
authorizeProposer(Env& env, Account const& target, Account const& proposer)
{
env(signers(target, 1, {{proposer, 1}}));
env.close();
}
std::uint32_t
createTicket(Env& env, Account const& account, std::uint32_t count)
{
// The tickets a TicketCreate makes are numbered from the sequence that
// follows the one it consumes.
std::uint32_t const firstTicketSeq = env.seq(account) + 1;
env(ticket::create(account, count));
env.close();
return firstTicketSeq;
}
std::uint32_t
expiration(Env& env, NetClock::duration delta)
{
return (env.now() + delta).time_since_epoch().count();
}
SLE::const_pointer
entry(Env const& env, AccountID const& target, std::uint32_t ticketSeq)
{
return env.le(keylet::txProposal(target, ticketSeq));
}
SLE::const_pointer
entry(Env const& env, Account const& target, std::uint32_t ticketSeq)
{
return entry(env, target.id(), ticketSeq);
}
namespace {
// The keys an account's owner directory lists, each with the page it sits on.
// The pages are read directly, so a key with nothing behind it is still seen.
std::map<uint256, std::uint64_t>
ownerDirKeys(ReadView const& view, AccountID const& account)
{
std::map<uint256, std::uint64_t> keys;
auto const root = keylet::ownerDir(account);
std::uint64_t page = 0;
for (auto sle = view.read(root); sle;)
{
for (auto const& key : sle->getFieldV256(sfIndexes))
keys.emplace(key, page);
page = sle->getFieldU64(sfIndexNext);
if (page == 0)
break;
sle = view.read(keylet::page(root, page));
}
return keys;
}
// Whether two reads of a ledger entry found it unchanged, or found nothing both
// times. Entries that are there are compared whole.
bool
unchanged(SLE::const_pointer const& before, SLE::const_pointer const& after)
{
if (!before || !after)
return !before && !after;
return before->key() == after->key() &&
static_cast<STObject const&>(*before) == static_cast<STObject const&>(*after);
}
// Whether a TransactionProposal entry may carry the field when it is created.
// A fresh proposal has gathered no signatures, so it carries nothing else —
// except a Sponsor, which a reserve-sponsored creation stamps on the entry from
// the start.
bool
isCreationField(SField const& field)
{
return field == sfLedgerEntryType || field == sfFlags || field == sfOwner ||
field == sfProposedTransaction || field == sfExpiration || field == sfOwnerNode ||
field == sfPreviousTxnID || field == sfPreviousTxnLgrSeq || field == sfSponsor;
}
} // namespace
void
verify::Create::operator()(Env& env, JTx& jt) const
{
// Only a TransactionProposalCreate carries the fields read below, and a
// condition that quietly checks nothing is worse than none at all.
if (jt.jv[jss::TransactionType].asString() != jss::TransactionProposalCreate.cStr())
Throw<std::logic_error>("proposal::verify::create: not a TransactionProposalCreate");
// Funclets run before the transaction is applied, so everything read here
// is the state the effects are measured against.
auto const& proposedTx = jt.jv[sfProposedTransaction.jsonName];
auto const parsedTarget = parseBase58<AccountID>(proposedTx[jss::Account].asString());
// A payload naming no usable target is a malformed case a test is making
// on purpose, and has no ledger effect to measure.
if (!parsedTarget)
return;
auto const target = *parsedTarget;
auto const proposer = env.lookup(jt.jv[jss::Account].asString());
auto const ticketSeq = proposedTx[sfTicketSequence.jsonName].asUInt();
auto const expiration = jt.jv[sfExpiration.jsonName].asUInt();
auto const cost = proposedTx[jss::TransactionType].asString() == jss::Batch.cStr()
? kBatchProposalOwnerCount
: kProposalOwnerCount;
std::optional<Account> reserveSponsor;
if (jt.jv.isMember(sfSponsor.jsonName) &&
(jt.jv[sfSponsorFlags.jsonName].asUInt() & spfSponsorReserve) != 0)
reserveSponsor.emplace(env.lookup(jt.jv[sfSponsor.jsonName].asString()));
// Every entry the transaction could touch, read whole, so what follows can
// say that nothing moved rather than that the fields we named did not.
auto const& view = *env.current();
auto const proposalKeylet = keylet::txProposal(target, ticketSeq);
auto const ownerCountBefore = env.ownerCount(proposer);
auto const sponsoredOwnerCountBefore = env.sponsoredOwnerCount(proposer);
auto const sponsoringOwnerCountBefore =
reserveSponsor ? std::optional{env.sponsoringOwnerCount(*reserveSponsor)} : std::nullopt;
auto const proposalBefore = view.read(proposalKeylet);
auto const targetBefore = view.read(keylet::account(target));
auto const ticketBefore = view.read(keylet::ticket(target, ticketSeq));
auto const proposerDirBefore = ownerDirKeys(view, proposer.id());
auto const targetDirBefore = ownerDirKeys(view, target);
jt.require.emplace_back([=](Env& applied) {
auto& test = applied.test;
auto const& view = *applied.current();
auto const created = isTesSuccess(applied.ter());
// The proposer owns the proposal even when another account covers its
// reserve. A proposed Batch costs more owner-count increments.
test.expect(
applied.ownerCount(proposer) == ownerCountBefore + (created ? cost : 0),
"proposal reserve");
test.expect(
applied.sponsoredOwnerCount(proposer) ==
sponsoredOwnerCountBefore + (created && reserveSponsor ? cost : 0),
"proposal sponsored owner count");
if (reserveSponsor)
{
test.expect(
applied.sponsoringOwnerCount(*reserveSponsor) ==
*sponsoringOwnerCountBefore + (created ? cost : 0),
"proposal sponsoring owner count");
}
// The target's ticket is left for the proposed transaction, including
// when the target is also the proposer.
test.expect(
unchanged(ticketBefore, view.read(keylet::ticket(target, ticketSeq))),
"proposal target ticket");
// Nothing else of a distinct target's moves: the proposal belongs in
// the proposer's account and owner directory.
if (target != proposer.id())
{
test.expect(
unchanged(targetBefore, view.read(keylet::account(target))),
"proposal target account");
test.expect(ownerDirKeys(view, target) == targetDirBefore, "proposal target directory");
}
auto const sleProposal = view.read(proposalKeylet);
if (!created)
{
// A create that did not succeed leaves the proposal as it found
// it, down to the last field.
test.expect(unchanged(proposalBefore, sleProposal), "proposal unchanged");
// Nor did the directory gain a listing for an entry that does not
// exist.
test.expect(
ownerDirKeys(view, proposer.id()) == proposerDirBefore, "proposal owner directory");
return;
}
// A successful create must have created the entry, not overwritten one
// that was already there. The checks below read the entry after the
// write, so they pass either way; this is what rules an overwrite out.
if (!test.expect(!proposalBefore, "proposal is new") ||
!test.expect(sleProposal, "proposal entry"))
return;
// What is on the ledger is what was submitted: a proposal is only worth
// collecting signatures against if the transaction it stores is the one
// proposed, so the payload is compared whole.
test.expect(sleProposal->getAccountID(sfOwner) == proposer.id(), "proposal owner");
test.expect(sleProposal->getFieldU32(sfExpiration) == expiration, "proposal expiration");
test.expect(
reserveSponsor ? sleProposal->isFieldPresent(sfSponsor) &&
sleProposal->getAccountID(sfSponsor) == reserveSponsor->id()
: !sleProposal->isFieldPresent(sfSponsor),
"proposal sponsor");
auto const& stored = sleProposal->getFieldObject(sfProposedTransaction);
test.expect(stored == parse(proposedTx), "proposal payload");
// Unsigned canonical form, keyed by the target and ticket the payload
// names (On-Chain Cosigner spec §6.1).
test.expect(
xrpl::proposal::hasEmptySigningPubKey(stored) &&
!xrpl::proposal::hasSignatureField(stored),
"proposal payload unsigned");
test.expect(
stored.getAccountID(sfAccount) == target &&
stored.getFieldU32(sfTicketSequence) == ticketSeq &&
stored.getFieldU32(sfSequence) == 0,
"proposal payload target");
// Nothing beyond the fields a fresh proposal is created with. An
// STObject carries a placeholder for each optional field its format
// allows, so each field is asked whether it is really present.
test.expect(sleProposal->getFieldU32(sfFlags) == 0, "proposal flags");
for (auto const& field : *sleProposal)
{
if (field.getSType() != STI_NOTPRESENT)
{
test.expect(
isCreationField(field.getFName()),
"proposal field " + field.getFName().getName());
}
}
// The proposal is listed in the proposer's directory on the page its
// OwnerNode names, and nothing else listed moved.
auto expectedDir = proposerDirBefore;
expectedDir.emplace(sleProposal->key(), sleProposal->getFieldU64(sfOwnerNode));
test.expect(ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
});
}
} // namespace xrpl::test::jtx::proposal

View File

@@ -1,191 +0,0 @@
#pragma once
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <cstdint>
#include <optional>
#include <vector>
/**
* @brief Helpers for constructing TransactionProposal test transactions.
*/
namespace xrpl::test::jtx::proposal {
// The owner-reserve increments a proposal holds against its proposer. Tests
// spend these rather than repeating their values, so they follow the transactor
// instead of checking it; TransactionProposalCreate_test pins the values
// themselves, so a change to them has to be a deliberate one.
using xrpl::proposal::kBatchProposalOwnerCount;
using xrpl::proposal::kProposalOwnerCount;
/**
* @brief Build a TransactionProposalCreate carrying an unsigned proposed
* transaction.
*
* @param proposer The account creating and paying the reserve for the proposal.
* @param proposedTx The proposed transaction, in unsigned canonical form; see
* unsignedPayload().
* @param expiration Absolute time after which the proposal may no longer be
* completed.
* @return The TransactionProposalCreate JSON object.
*/
json::Value
create(Account const& proposer, json::Value const& proposedTx, std::uint32_t expiration);
/**
* @brief Conditions that check what a proposal transaction did to the ledger.
*
* Each is named for the generator it verifies, so a submission and its check
* read as a pair, and the transactions that later sign or complete a proposal
* get their own entries here rather than sharing one.
*/
namespace verify {
/**
* @brief The condition returned by create(); see it for what is checked.
*/
class Create
{
public:
/**
* @throws std::logic_error if attached to another transaction type.
*/
void
operator()(Env&, JTx&) const;
};
/**
* @brief Check the ledger effects a TransactionProposalCreate must have,
* whatever its outcome: on tesSUCCESS a new proposal holding what was
* submitted, listed in the proposer's directory and paid for by its reserve;
* otherwise nothing moved. Nothing of the target's moves either way.
*
* @code
* env(proposal::create(alice, payload, expiration), proposal::verify::create());
* @endcode
*/
[[nodiscard]] inline Create
create()
{
return Create{};
}
} // namespace verify
/**
* @brief Put a transaction of any type into the form a proposal stores it in:
* unsigned and ticket-based, with the fee the target account will pay fixed
* now.
*
* This takes whatever any jtx generator produces — @c pay(), @c loan::set(),
* @c sponsor::transfer(), an outer @c Batch — and applies only what the
* proposal itself demands of the payload, so a test never hand-rolls those
* fields. It leaves the rest of @p tx untouched, which is what lets a test
* build one valid payload and then break exactly one rule of it.
*
* The payload is ticket-based so unrelated activity on the target account
* cannot invalidate it while signatures are collected. A Fee already set on
* @p tx is left alone, so a payload whose fee is not the base fee — a Batch,
* say — can carry its own; otherwise the fee is filled in the same way as for
* an ordinary submission.
*
* @param env The test environment providing ledger fee settings.
* @param tx The transaction to propose.
* @param ticketSeq A ticket sequence owned by @p tx's account.
* @return The proposed transaction JSON object.
*/
json::Value
unsignedPayload(Env const& env, json::Value tx, std::uint32_t ticketSeq);
/**
* @brief Put a transaction into the form an inner transaction of a proposed
* Batch takes, as @c batch::Inner does for an ordinary Batch.
*
* @param tx The transaction to nest.
* @param seq The sequence number of @p tx's own account.
* @return The inner transaction JSON object.
*/
json::Value
innerTx(json::Value tx, std::uint32_t seq);
/**
* @brief An unsigned outer Batch payload holding @p inners.
*
* A proposed Batch stores no BatchSigners — the participants' signatures are
* collected on-ledger afterwards — but its fee is fixed now and must already
* cover them. By default one signer is assumed for each inner account other
* than @p target, which is what those participants will contribute.
*
* @param env The test environment providing ledger fee settings.
* @param target The outer account of the Batch, and the proposal's target.
* @param ticketSeq A ticket sequence owned by @p target.
* @param flags The Batch mode flags, e.g. @c tfAllOrNothing.
* @param inners The inner transactions; see innerTx().
* @param numSigners Overrides the number of signatures the fee accounts for.
* @return The proposed Batch JSON object.
*/
json::Value
unsignedBatch(
Env const& env,
Account const& target,
std::uint32_t ticketSeq,
std::uint32_t flags,
std::vector<json::Value> const& inners,
std::optional<std::uint32_t> numSigners = std::nullopt);
/**
* @brief Give @p proposer a place on @p target's SignerList, so it may
* create proposals against @p target.
*
* Only the target account itself, or an account on its SignerList, may
* create a TransactionProposalCreate against it (On-Chain Cosigner V1
* authorization). Sets a minimal one-signer, quorum-1 list; the quorum
* itself is irrelevant here since it only governs the proposed
* transaction's own completion, not who may propose it.
*
* @param env The test environment.
* @param target The account whose SignerList is set.
* @param proposer The account to add to it.
*/
void
authorizeProposer(Env& env, Account const& target, Account const& proposer);
/**
* @brief Create tickets for a proposal to be built against, and close the
* ledger.
*
* @param env The test environment.
* @param account The account that will own the tickets, i.e. the target of the
* proposals to come.
* @param count How many tickets to create.
* @return The first ticket sequence created; the rest follow it.
*/
std::uint32_t
createTicket(Env& env, Account const& account, std::uint32_t count = 1);
/**
* @brief An absolute expiration @p delta past the environment's current time.
*
* @c expiration(env, 0s) is an expiration that has already passed.
*/
std::uint32_t
expiration(Env& env, NetClock::duration delta);
/**
* @brief The proposal stored against a target account's ticket.
* @return empty if no such proposal exists.
*/
[[nodiscard]] SLE::const_pointer
entry(Env const& env, AccountID const& target, std::uint32_t ticketSeq);
[[nodiscard]] SLE::const_pointer
entry(Env const& env, Account const& target, std::uint32_t ticketSeq);
} // namespace xrpl::test::jtx::proposal

View File

@@ -1,223 +0,0 @@
// Auto-generated unit tests for ledger entry TransactionProposal
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/TransactionProposal.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
namespace xrpl::ledger_entries {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed for both the
// builder's STObject and the wrapper's SLE.
TEST(TransactionProposalTests, BuilderSettersRoundTrip)
{
uint256 const index{1u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const ownerValue = canonical_ACCOUNT();
auto const proposedTransactionValue = canonical_OBJECT();
auto const expirationValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
TransactionProposalBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
ownerValue,
proposedTransactionValue,
expirationValue,
ownerNodeValue
};
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
EXPECT_TRUE(builder.validate());
auto const entry = builder.build(index);
EXPECT_TRUE(entry.validate());
{
auto const& expected = previousTxnIDValue;
auto const actual = entry.getPreviousTxnID();
expectEqualField(expected, actual, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const actual = entry.getPreviousTxnLgrSeq();
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = ownerValue;
auto const actual = entry.getOwner();
expectEqualField(expected, actual, "sfOwner");
}
{
auto const& expected = proposedTransactionValue;
auto const actual = entry.getProposedTransaction();
expectEqualField(expected, actual, "sfProposedTransaction");
}
{
auto const& expected = expirationValue;
auto const actual = entry.getExpiration();
expectEqualField(expected, actual, "sfExpiration");
}
{
auto const& expected = ownerNodeValue;
auto const actual = entry.getOwnerNode();
expectEqualField(expected, actual, "sfOwnerNode");
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(*ledgerIndex, index);
EXPECT_EQ(entry.getKey(), index);
}
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
// from that SLE, build a new wrapper, and verify all fields (and validate()).
TEST(TransactionProposalTests, BuilderFromSleRoundTrip)
{
uint256 const index{2u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const ownerValue = canonical_ACCOUNT();
auto const proposedTransactionValue = canonical_OBJECT();
auto const expirationValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto sle = std::make_shared<SLE>(TransactionProposal::entryType, index);
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfOwner) = ownerValue;
sle->setFieldObject(sfProposedTransaction, proposedTransactionValue);
sle->at(sfExpiration) = expirationValue;
sle->at(sfOwnerNode) = ownerNodeValue;
TransactionProposalBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
auto const entryFromBuilder = builderFromSle.build(index);
TransactionProposal entryFromSle{sle};
EXPECT_TRUE(entryFromBuilder.validate());
EXPECT_TRUE(entryFromSle.validate());
{
auto const& expected = previousTxnIDValue;
auto const fromSle = entryFromSle.getPreviousTxnID();
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
expectEqualField(expected, fromSle, "sfPreviousTxnID");
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = ownerValue;
auto const fromSle = entryFromSle.getOwner();
auto const fromBuilder = entryFromBuilder.getOwner();
expectEqualField(expected, fromSle, "sfOwner");
expectEqualField(expected, fromBuilder, "sfOwner");
}
{
auto const& expected = proposedTransactionValue;
auto const fromSle = entryFromSle.getProposedTransaction();
auto const fromBuilder = entryFromBuilder.getProposedTransaction();
expectEqualField(expected, fromSle, "sfProposedTransaction");
expectEqualField(expected, fromBuilder, "sfProposedTransaction");
}
{
auto const& expected = expirationValue;
auto const fromSle = entryFromSle.getExpiration();
auto const fromBuilder = entryFromBuilder.getExpiration();
expectEqualField(expected, fromSle, "sfExpiration");
expectEqualField(expected, fromBuilder, "sfExpiration");
}
{
auto const& expected = ownerNodeValue;
auto const fromSle = entryFromSle.getOwnerNode();
auto const fromBuilder = entryFromBuilder.getOwnerNode();
expectEqualField(expected, fromSle, "sfOwnerNode");
expectEqualField(expected, fromBuilder, "sfOwnerNode");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(TransactionProposalTests, WrapperThrowsOnWrongEntryType)
{
uint256 const index{3u};
// Build a valid ledger entry of a different type
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(TransactionProposal{wrongEntry.getSle()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(TransactionProposalTests, BuilderThrowsOnWrongEntryType)
{
uint256 const index{4u};
// Build a valid ledger entry of a different type
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(TransactionProposalBuilder{wrongEntry.getSle()}, std::runtime_error);
}
}

View File

@@ -1,162 +0,0 @@
// Auto-generated unit tests for transaction TransactionProposalCreate
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/TransactionProposalCreate.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsTransactionProposalCreateTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testTransactionProposalCreate"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const proposedTransactionValue = canonical_OBJECT();
auto const expirationValue = canonical_UINT32();
TransactionProposalCreateBuilder builder{
accountValue,
proposedTransactionValue,
expirationValue,
sequenceValue,
feeValue
};
// Set optional fields
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = proposedTransactionValue;
auto const actual = tx.getProposedTransaction();
expectEqualField(expected, actual, "sfProposedTransaction");
}
{
auto const& expected = expirationValue;
auto const actual = tx.getExpiration();
expectEqualField(expected, actual, "sfExpiration");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsTransactionProposalCreateTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testTransactionProposalCreateFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const proposedTransactionValue = canonical_OBJECT();
auto const expirationValue = canonical_UINT32();
// Build an initial transaction
TransactionProposalCreateBuilder initialBuilder{
accountValue,
proposedTransactionValue,
expirationValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
TransactionProposalCreateBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = proposedTransactionValue;
auto const actual = rebuiltTx.getProposedTransaction();
expectEqualField(expected, actual, "sfProposedTransaction");
}
{
auto const& expected = expirationValue;
auto const actual = rebuiltTx.getExpiration();
expectEqualField(expected, actual, "sfExpiration");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsTransactionProposalCreateTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(TransactionProposalCreate{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsTransactionProposalCreateTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(TransactionProposalCreateBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}

View File

@@ -741,30 +741,6 @@ parseSponsorship(
return keylet::sponsorship(*sponsorID, *sponseeID).key;
}
static std::expected<uint256, json::Value>
parseTransactionProposal(
json::Value const& params,
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
return parseObjectID(params, fieldName, "hex string");
auto const targetID =
LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress");
if (!targetID)
return std::unexpected(targetID.error());
// The proposed transaction's TicketSequence (a proposed transaction is
// ticket-only), mirroring how parseTicket looks up a Ticket object.
auto const ticketSequence =
LedgerEntryHelpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest");
if (!ticketSequence)
return std::unexpected(ticketSequence.error());
return keylet::txProposal(*targetID, *ticketSequence).key;
}
static std::expected<uint256, json::Value>
parseTicket(
json::Value const& params,