mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-26 08:40:56 +00:00
Compare commits
42 Commits
ripple/cos
...
ximinez/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81e06a9ba8 | ||
|
|
b3141d91fa | ||
|
|
1b1cd4c260 | ||
|
|
a482d6aced | ||
|
|
299627298f | ||
|
|
640798696b | ||
|
|
47512afad5 | ||
|
|
ff3708a757 | ||
|
|
391a1e442c | ||
|
|
9e77212900 | ||
|
|
93f5a0e217 | ||
|
|
71367f361c | ||
|
|
931d21b2a7 | ||
|
|
c99feb82e7 | ||
|
|
7b53a5e0c5 | ||
|
|
4a02518497 | ||
|
|
0b6c3630cc | ||
|
|
4d04ba5be5 | ||
|
|
ed53557d41 | ||
|
|
e4d10393f3 | ||
|
|
fcc7f57e82 | ||
|
|
a25229f154 | ||
|
|
eb6eaf2532 | ||
|
|
ff987fc7c6 | ||
|
|
d21137c4c1 | ||
|
|
ac7db2e621 | ||
|
|
42005a8080 | ||
|
|
1f7b1b3a78 | ||
|
|
80b90544c5 | ||
|
|
00b9a8cd67 | ||
|
|
3be49f814a | ||
|
|
1674fabe81 | ||
|
|
6dfa47ce7a | ||
|
|
bef095be65 | ||
|
|
8e5d774c36 | ||
|
|
fb8fb30f6c | ||
|
|
a553001125 | ||
|
|
57782e84ee | ||
|
|
9d5076c8a9 | ||
|
|
1af379e09f | ||
|
|
1ced0875ae | ||
|
|
53e6d7580a |
2
.github/scripts/strategy-matrix/linux.json
vendored
2
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-40cdf49",
|
||||
"image_tag": "sha-3122de8",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
|
||||
2
.github/workflows/build-nix-images.yml
vendored
2
.github/workflows/build-nix-images.yml
vendored
@@ -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:
|
||||
|
||||
|
||||
114
.github/workflows/check-tools.yml
vendored
114
.github/workflows/check-tools.yml
vendored
@@ -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 }}
|
||||
1
.github/workflows/on-pr.yml
vendored
1
.github/workflows/on-pr.yml
vendored
@@ -90,7 +90,6 @@ jobs:
|
||||
.clang-tidy
|
||||
.codecov.yml
|
||||
bin/check-tools.sh
|
||||
bin/default-loader-path.sh
|
||||
cfg/**
|
||||
cmake/**
|
||||
conan/**
|
||||
|
||||
1
.github/workflows/on-trigger.yml
vendored
1
.github/workflows/on-trigger.yml
vendored
@@ -28,7 +28,6 @@ on:
|
||||
- ".clang-tidy"
|
||||
- ".codecov.yml"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "cfg/**"
|
||||
- "cmake/**"
|
||||
- "conan/**"
|
||||
|
||||
2
.github/workflows/publish-docs.yml
vendored
2
.github/workflows/publish-docs.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/reusable-clang-tidy.yml
vendored
2
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/reusable-upload-recipe.yml
vendored
2
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
8
docs/build/nix.md
vendored
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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&
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user