Compare commits

..
Author SHA1 Message Date
grajanna 9e221267af fix: Default to jemalloc and skip glibc trimming
Enable jemalloc by default in Conan and CMake and propagate it to Conan
consumers. Compile out glibc malloc trimming and instrumentation when
jemalloc is enabled.

Keep GNU malloc with trimming available as an opt-out and retain the
system allocator for sanitizer configurations. Update allocator tests
and document both build choices.
2026-09-24 21:59:17 +00:00
68 changed files with 358 additions and 5500 deletions

No files matched your search

+1 -1
View File
@@ -41,4 +41,4 @@ runs:
- name: Determine release channel and package release
id: release_info
uses: XRPLF/actions/release-info@ebcf6cea14eee258697308a51fea55cad777581b
uses: XRPLF/actions/release-info@7cc0e4a8d9d0b838f92c48d312856b190341bbba
@@ -194,9 +194,6 @@ tests.libxrpl > xrpl.resource
tests.libxrpl > xrpl.server
tests.libxrpl > xrpl.shamap
tests.libxrpl > xrpl.tx
tests.xrpld > xrpld.rpc
tests.xrpld > xrpl.json
tests.xrpld > xrpl.protocol
xrpl.conditions > xrpl.basics
xrpl.conditions > xrpl.protocol
xrpl.config > xrpl.basics
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
base_image: debian:bookworm
- name: rhel
base_image: registry.access.redhat.com/ubi9/ubi:latest
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@696384b292577293292daed06af0306d1b83bd7d
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
with:
image_name: xrpld/nix-${{ matrix.distro.name }}
dockerfile: nix/docker/Dockerfile
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
# AlmaLinux rather than UBI, which does not ship rpm-sign.
- name: rhel
base_image: almalinux:10
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@696384b292577293292daed06af0306d1b83bd7d
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
with:
image_name: xrpld/packaging-${{ matrix.distro.name }}
dockerfile: package/docker/Dockerfile
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
permissions:
contents: read
packages: write
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@696384b292577293292daed06af0306d1b83bd7d
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@034e87065fcd0100214cf0672923bd38d193cf78
with:
image_name: xrpld/pre-commit
dockerfile: bin/pre-commit/Dockerfile
-1
View File
@@ -50,4 +50,3 @@ jobs:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
-1
View File
@@ -129,4 +129,3 @@ jobs:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -257,7 +257,6 @@ jobs:
run: |
./bin/check-nix-store-refs.sh "${BUILD_DIR}/xrpld"
./bin/check-nix-store-refs.sh "${BUILD_DIR}/xrpl_tests"
./bin/check-nix-store-refs.sh "${BUILD_DIR}/xrpld_tests"
- name: Show ccache statistics
if: ${{ inputs.ccache_enabled }}
@@ -339,9 +338,7 @@ jobs:
- name: Run the separate tests
if: ${{ !inputs.build_only }}
working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
run: |
./xrpl_tests
./xrpld_tests
run: ./xrpl_tests
- name: Run the embedded tests
if: ${{ !inputs.build_only }}
+4 -63
View File
@@ -1,5 +1,5 @@
# Build, verify and publish Linux packages from the pre-built xrpld and
# validator-keys artifacts, in these stages:
# validator-keys artifacts, in three stages:
#
# - 'package' builds and signs one format per config that carries a "package"
# map in linux.json; that map names the container image and the format
@@ -9,8 +9,6 @@
# never reaches Nexus
# - 'publish' uploads with the image's publish_pkg.py, doing a --dry-run
# unless 'publish: true'
# - 'docker' builds an Ubuntu image from the tested DEB, pushing it to Docker
# Hub only with 'publish: true'
#
# Only linux/amd64 is supported; the runner is hardcoded in the jobs below.
name: Package
@@ -39,9 +37,6 @@ on:
signing_key:
description: "Armoured PGP private key used to sign the RPMs. Required when publishing."
required: false
dockerhub_token:
description: "A Docker Hub organization access token for xrplf, with push access to xrplf/xrpld. Required when publishing."
required: false
defaults:
run:
@@ -203,6 +198,9 @@ jobs:
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
# The name says which of the two this is, because the job runs either way:
# with publish false it passes --dry-run and uploads nothing, and a job
# called "publish ..." succeeding on a pull request reads like a release.
name: "publish ${{ matrix.xrpld_artifact_name }}${{ !inputs.publish && ' (dry run)' || '' }}"
permissions:
contents: read
@@ -244,60 +242,3 @@ jobs:
--package-dir "${PACKAGE_DIR}" \
--nexus-url "${NEXUS_URL}" \
${DRY_RUN_OPTION}
docker:
needs: [test-install-deb, test-install-rpm]
name: "docker${{ !inputs.publish && ' (dry run)' || '' }}"
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 5
env:
IMAGE: xrplf/xrpld:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "*-pkg"
merge-multiple: true
path: ${{ env.PACKAGE_DIR }}
- name: Build image
env:
CONTEXT: image-context
run: |
mkdir -p "${CONTEXT}"
find "${PACKAGE_DIR}" -type f -name 'xrpld_[0-9]*.deb' -exec cp {} "${CONTEXT}/" \;
docker build --pull --file package/image/Dockerfile --tag "${IMAGE}" "${CONTEXT}"
- name: Start the server
run: |
container="$(docker run --detach "${IMAGE}" --standalone --silent --conf /etc/xrpld/xrpld.cfg)"
trap 'docker rm --force "${container}" >/dev/null' EXIT
for _ in $(seq 30); do
output="$(docker exec "${container}" xrpld --conf /etc/xrpld/xrpld.cfg server_info || true)"
if [[ "${output}" == *'"status" : "success"'* ]]; then
exit 0
fi
sleep 2
done
docker logs "${container}"
exit 1
# Docker Hub is public, so a private build never reaches it.
- name: Log in to Docker Hub
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: xrplf
password: ${{ secrets.dockerhub_token }}
- name: Push image
if: ${{ inputs.publish && github.event.repository.visibility == 'public' }}
run: docker push "${IMAGE}"
+21 -5
View File
@@ -68,17 +68,33 @@ jobs:
run: conan remote login "${REMOTE_NAME}" "${CONAN_LOGIN_USERNAME_XRPLF}" --password "${CONAN_PASSWORD_XRPLF}"
- name: Upload Conan recipe (version)
env:
VERSION: ${{ steps.release_info.outputs.version }}
run: |
conan export . --version="${VERSION}"
conan upload --confirm --check --remote="${REMOTE_NAME}" "xrpl/${VERSION}"
conan export . --version=${{ steps.release_info.outputs.version }}
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.release_info.outputs.version }}
# When this workflow is triggered by a push event, it will always be when merging into the
# 'develop' branch, see on-trigger.yml.
- name: Upload Conan recipe (develop)
if: ${{ github.ref == 'refs/heads/develop' }}
if: ${{ github.event_name == 'push' }}
run: |
conan export . --version=develop
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/develop
# When this workflow is triggered by a pull request event, it will always be when merging into
# one of the 'release' branches, see on-pr.yml.
- name: Upload Conan recipe (rc)
if: ${{ github.event_name == 'pull_request' }}
run: |
conan export . --version=rc
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/rc
# When this workflow is triggered by a push event, it will always be when tagging a final
# release, see on-tag.yml.
- name: Upload Conan recipe (release)
if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: |
conan export . --version=release
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/release
outputs:
ref: xrpl/${{ steps.release_info.outputs.version }}
+21
View File
@@ -304,6 +304,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details.
| ---------------- | ------------- | ----------------------------------------------------------------------------- |
| `assert` | OFF | Force enabling assertions. |
| `coverage` | OFF | Prepare the coverage report. |
| `jemalloc` | ON | Use jemalloc instead of the system allocator; disables glibc malloc trimming. |
| `rust` | OFF | Build the Rust crates and the C++ code that depends on them. |
| `tests` | OFF | Build tests. |
| `unity` | OFF | Configure a unity build. |
@@ -317,6 +318,26 @@ memory) since they concatenate sources into fewer translation units. Non-unity
builds may be faster for incremental builds, and can be helpful for detecting
`#include` omissions.
### Memory allocator
Conan and CMake use jemalloc by default. In jemalloc builds, `mallocTrim` does
not call glibc's `malloc_trim` or collect trim metrics; jemalloc manages its own
arenas and memory reclamation.
To use the system allocator instead (GNU/glibc malloc with the existing
`malloc_trim(0)` behavior on Linux/glibc), add `--options '&:jemalloc=False'`
to each `conan install` command and `-Djemalloc=OFF` to the CMake configure
command. Keep the Conan and CMake options consistent. To switch an existing
system-allocator build to jemalloc, rerun Conan with
`--options '&:jemalloc=True'` and configure CMake with `-Djemalloc=ON`, since
existing build directories may cache the old option value.
The `sanitizers` Conan profile disables jemalloc when sanitizer instrumentation
is enabled so that sanitizers can intercept the system allocator. CMake also
defaults to the system allocator when `SANITIZERS` is set; use `-Djemalloc=OFF`
if reusing a build directory previously configured with jemalloc. Platforms
other than Linux/glibc do not support malloc trimming, regardless of this option.
### Rust crates
The Rust crates in `crates/` are only part of the build when `rust` is ON. With
-2
View File
@@ -115,7 +115,6 @@ find_package(secp256k1 REQUIRED)
find_package(SOCI REQUIRED)
find_package(SQLite3 REQUIRED)
find_package(xxHash REQUIRED)
find_package(xrpl-rpc-spec REQUIRED)
target_link_libraries(
xrpl_libs
@@ -176,7 +175,6 @@ include(XrplPackaging)
if(tests)
include(CTest)
add_subdirectory(src/tests/libxrpl)
add_subdirectory(src/tests/xrpld)
endif()
if(benchmark)
+1 -8
View File
@@ -296,14 +296,7 @@ if(xrpld)
target_sources(xrpld PRIVATE ${sources})
endif()
target_link_libraries(
xrpld
Xrpl::boost
Xrpl::opts
Xrpl::libs
xrpl.libxrpl
rpcspec::rpcspec
)
target_link_libraries(xrpld Xrpl::boost Xrpl::opts Xrpl::libs xrpl.libxrpl)
exclude_if_included(xrpld)
# define a macro for tests that might need to
# be excluded or run differently in CI environment
+1 -1
View File
@@ -48,7 +48,7 @@ setup_target_for_coverage_gcovr(
"include/xrpl/beast/test"
"include/xrpl/beast/unit_test"
"${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
DEPENDENCIES xrpld xrpl_tests xrpld_tests
DEPENDENCIES xrpld xrpl_tests
)
add_code_coverage_to_target(opts INTERFACE)
+11 -1
View File
@@ -131,7 +131,17 @@ else()
set(use_lld OFF CACHE BOOL "try lld linker, clang only" FORCE)
endif()
option(jemalloc "Enables jemalloc for heap profiling" OFF)
# Sanitizers need to intercept the system allocator.
if(SANITIZERS_ENABLED)
set(JEMALLOC_DEFAULT OFF)
else()
set(JEMALLOC_DEFAULT ON)
endif()
option(
jemalloc
"Use jemalloc instead of the system allocator"
${JEMALLOC_DEFAULT}
)
option(werr "treat warnings as errors" OFF)
option(
local_protobuf
+2 -7
View File
@@ -3,7 +3,6 @@
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"xrpl-rpc-spec/0.1.19#870b2d3abcfbbf13b61c2d3c69495060%1790348286.187549",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
@@ -20,8 +19,8 @@
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
"grpc/1.81.1#b87796a4269034856cbc1a2522db16eb%1788275071.530512",
"fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1785888854.601666",
"grpc/1.81.1#f729f6d75992d20f9c72828e9142d62f%1783945160.094135",
"fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1782494504.298",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
"corrosion/0.6.1#bfa292df0a957bc70a450ff316cd9435%1786119416.131296",
@@ -46,10 +45,6 @@
],
"python_requires": [],
"overrides": {
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c": [
null,
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c"
],
"protobuf/[>=5.27.0 <7]": [
"protobuf/6.33.5"
],
+2
View File
@@ -98,6 +98,8 @@ tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags"
&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{ sanitizers }}", "SANITIZERS_COMPILER_FLAGS": "{{ sanitizer_compiler_flags | join(' ') }}", "SANITIZERS_LINKER_FLAGS": "{{ sanitizer_linker_flags | join(' ') }}"}
[options]
# Let sanitizers intercept the system allocator rather than jemalloc.
&:jemalloc=False
{% if enable_asan %}
# Build Boost.Context with ucontext backend (not fcontext) so that
# ASAN fiber-switching annotations (__sanitizer_start/finish_switch_fiber)
+3 -3
View File
@@ -36,7 +36,6 @@ class Xrpl(ConanFile):
"nudb/2.0.9",
"openssl/3.6.3",
"soci/4.0.3",
"xrpl-rpc-spec/0.1.19",
"zlib/1.3.2",
]
@@ -53,7 +52,7 @@ class Xrpl(ConanFile):
"benchmark": True,
"coverage": False,
"fPIC": True,
"jemalloc": False,
"jemalloc": True,
"rocksdb": True,
"shared": False,
"static": True,
@@ -115,7 +114,6 @@ class Xrpl(ConanFile):
"soci/*:shared": False,
"soci/*:with_sqlite3": True,
"soci/*:with_boost": True,
"xrpl-rpc-spec/*:server": "xrpld",
"xxhash/*:shared": False,
}
@@ -230,5 +228,7 @@ class Xrpl(ConanFile):
"xxhash::xxhash",
"zlib::zlib",
]
if self.options.jemalloc:
libxrpl.requires.append("jemalloc::jemalloc")
if self.options.rocksdb:
libxrpl.requires.append("rocksdb::librocksdb")
+10 -10
View File
@@ -57,9 +57,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.202"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13f6de320895f42e6e081abb5c7983bedcf0b6d0ff9323de0d33f620c8ac1199"
checksum = "824894a4a85dca76d4c95c2b9098c036f5a29f627b30c12780774f6654e60974"
dependencies = [
"cc",
"cxx-build",
@@ -72,9 +72,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.202"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fde53ca86b9704a943fef0f1e1d836239a6aedca5de3c65fa9f97ec0bd46d39"
checksum = "f1ae0b651ea5b0000b19513aef5a03f194d7e3486f2d9258b658da8677fe9036"
dependencies = [
"cc",
"codespan-reporting",
@@ -87,9 +87,9 @@ dependencies = [
[[package]]
name = "cxxbridge-cmd"
version = "1.0.202"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bae89236c811fd4d08ed3441759ac4ac98d752cbfd3de341315ba16ad20ec3"
checksum = "fb05f91d3fb8435d9bab6ac5ce6ac1868be774325fb7fb2a91be39393b21388e"
dependencies = [
"clap",
"codespan-reporting",
@@ -101,15 +101,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.202"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49045042e5fced01b80742aba5508de82aa4f13677ed3fa2b4cda709c40c5918"
checksum = "bf293202e0e3e98495785745389e8d0755b217e66f19194a5c695c25e03282ef"
[[package]]
name = "cxxbridge-macro"
version = "1.0.202"
version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b181252e2e3d3b5d183afbdc033a3958b445eb3e1a0ae65fc3d4f5259f5da6fd"
checksum = "ca001d746947c7249ed9d332a10f7a59daedbafeb0ec68c5c18a7db7a93f6ccc"
dependencies = [
"indexmap",
"proc-macro2",
+3 -17
View File
@@ -1,19 +1,5 @@
# `xrpld` Docker Image
`xrpld` is published to Docker Hub as [`xrplf/xrpld`](https://hub.docker.com/r/xrplf/xrpld):
the `xrpld` DEB package installed on Ubuntu 26.04, running as the `xrpld` user.
Each release is tagged with its version, `xrplf/xrpld:<version>`, and
`xrplf/xrpld:develop` follows the `develop` branch.
See [`package/README.md`](../package/README.md#docker-image) for how it is built
and tagged.
```bash
docker run --detach --name xrpld \
--volume xrpld-db:/var/lib/xrpld \
--publish 2459:2459 \
xrplf/xrpld:develop
```
The admin ports (5005, 6006 and 50051) listen on `127.0.0.1` in the shipped
configuration; to reach them from outside the container, mount your own over
`/etc/xrpld/xrpld.cfg`.
- Some info relating to Docker containers can be found here: [../Builds/containers](../Builds/containers)
- Images for building and testing xrpld can be found here: [thejohnfreeman/rippled-docker](https://github.com/thejohnfreeman/rippled-docker/)
- These images do not have xrpld. They have all the tools necessary to build xrpld.
+10 -12
View File
@@ -13,11 +13,11 @@ namespace xrpl {
// -----------------------------------------------------------------------------
// Allocator interaction note:
// - This facility invokes glibc's malloc_trim(0) on Linux/glibc to request that
// ptmalloc return free heap pages to the OS.
// - If an alternative allocator (e.g. jemalloc or tcmalloc) is linked or
// preloaded (LD_PRELOAD), calling glibc's malloc_trim typically has no effect
// on the *active* heap. The call is harmless but may not reclaim memory
// because those allocators manage their own arenas.
// ptmalloc return free heap pages to the OS, unless built with jemalloc.
// - Builds with jemalloc disable trimming and its instrumentation entirely.
// - Other linked or preloaded allocators (LD_PRELOAD) are not detected. Calling
// glibc's malloc_trim typically has no effect on their heaps because those
// allocators manage their own arenas.
// - Only glibc sbrk/arena space is eligible for trimming; large mmap-backed
// allocations are usually returned to the OS on free regardless of trimming.
// - Call at known reclamation points (e.g., after cache sweeps / online delete)
@@ -47,18 +47,16 @@ struct MallocTrimReport
* @brief Attempt to return freed memory to the operating system.
*
* On Linux with glibc malloc, this issues ::malloc_trim(0), which may release
* free space from ptmalloc arenas back to the kernel. On other platforms, or if
* a different allocator is in use, this function is a no-op and the report will
* indicate that trimming is unsupported or had no effect.
* free space from ptmalloc arenas back to the kernel. On other platforms, or
* when built with jemalloc, this function performs no trimming or instrumentation
* and returns a default report with supported=false.
*
* @param tag Identifier for logging/debugging purposes.
* @param journal Journal for diagnostic logging.
* @return Report containing before/after metrics and the trim result.
*
* @note If an alternative allocator (jemalloc/tcmalloc) is linked or preloaded,
* calling glibc's malloc_trim may have no effect on the active heap. The
* call is harmless but typically does not reclaim memory under those
* allocators.
* @note Other linked or preloaded allocators are not detected at runtime.
* Calling glibc's malloc_trim may have no effect on their heaps.
*
* @note Only memory served from glibc's sbrk/arena heaps is eligible for trim.
* Large allocations satisfied via mmap are usually returned on free
+1 -8
View File
@@ -43,14 +43,7 @@ enum ApplyFlags : std::uint32_t {
// Transaction shouldn't be applied
// Signatures shouldn't be checked
TapDryRun = 0x1000,
// Transaction is being preflighted as the payload of a
// TransactionProposalCreate. Its signatures are collected on-ledger
// afterward, so signature-presence checks (e.g. Batch signer matching)
// are skipped at proposal-creation time (On-Chain Cosigner spec
// §5.3.1.2).
TapProposal = 0x2000
TapDryRun = 0x1000
};
constexpr ApplyFlags
@@ -1,38 +0,0 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <cstdint>
namespace xrpl {
template <typename ViewT>
class TransactionProposalEntry : public SLEBase<ViewT, ltTRANSACTION_PROPOSAL>
{
public:
using Base = SLEBase<ViewT, ltTRANSACTION_PROPOSAL>;
// Inherit base constructors: adopt an existing SLE, or resolve one from a
// Keylet against the view.
using Base::Base;
explicit TransactionProposalEntry(
AccountID const& target,
std::uint32_t ticketSequence,
Base::ViewRefType view,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: Base(keylet::txProposal(target, ticketSequence), view, j)
{
}
};
using TransactionProposalEntryR = TransactionProposalEntry<ReadView>;
using TransactionProposalEntryW = TransactionProposalEntry<ApplyView>;
} // namespace xrpl
@@ -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
-14
View File
@@ -183,20 +183,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,6 @@
// 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(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
@@ -651,24 +651,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
@@ -220,7 +220,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)
@@ -361,7 +360,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)
@@ -423,7 +421,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
@@ -1147,18 +1147,6 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
{sfZKProof, SoeRequired},
}))
/** This transaction posts an unsigned transaction on-ledger as a
TransactionProposal, pending multi-signature collection. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
({.amendment = featureCosign}),
({
{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 (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::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
-8
View File
@@ -60,10 +60,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(
@@ -75,10 +71,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&
-17
View File
@@ -386,23 +386,6 @@ preflight(
PreclaimResult
preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, OpenView const& view);
/**
* Type-erased overload of Transactor::invokeCheckPermission.
*
* Dispatches on the transaction type to Transactor::invokeCheckPermission<T>
* so a caller that only has an STTx still gets the same verdict submission
* uses: transaction-level permission, then granular permissions and
* checkGranularSandbox, then that type's checkGranularSemantics. Does not
* check SignerList or signing keys.
*
* An unknown transaction type (should not occur after a successful preflight)
* is treated as an internal invariant violation: UNREACHABLE is fired and the
* type-erased fallback return is temUNKNOWN, mirroring the sibling
* invokePreflight/invokePreclaim/invokeApply overloads in this header.
*/
NotTEC
invokeCheckPermission(ReadView const& view, STTx const& tx);
/**
* Compute only the expected base fee for a transaction.
*
@@ -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
+2 -22
View File
@@ -13,8 +13,6 @@ package/
docker/
Dockerfile Packaging image, built by `build-packaging-images.yml`; installs its tooling with `bin/install-packaging-tools.sh`
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
image/
Dockerfile The xrpld Docker image, installing the built DEB on Ubuntu (see "Docker image")
rpm/
xrpld.spec RPM spec
debian/ Debian control files (control.in, lintian-overrides.in, rules, copyright, docs, links, source/format).
@@ -108,7 +106,7 @@ Operators switch between the flavours as described in
### Via CI
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
`reusable-package.yml`, which runs in these stages:
`reusable-package.yml`, which runs in three stages:
1. `package` fans out one job per config carrying a `package` map, building and
signing in that config's container, and uploading `<config>-pkg` alongside
@@ -119,8 +117,6 @@ Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
in the container of every distro that format targets and running the binaries
there, so one that cannot be installed never reaches Nexus.
3. `publish` uploads both artifacts, or lists what it would upload.
4. `docker` builds the [Docker image](#docker-image) from the tested DEB, and
pushes it when publishing.
The packaging script derives the package version from the downloaded binary's
`xrpld --version` output; no CMake configure or build step is needed inside the
@@ -214,7 +210,6 @@ The `release-info` action decides the channel from the event, and
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` |
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
@@ -225,8 +220,6 @@ Only a tag names a channel — do not extend that to `develop`, where
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
version during a release cycle, which would send develop builds into `stable`.
Versions sort in row order, so moving to a more mature channel never downgrades.
A tag matching none of the release patterns, such as `X.Y.Z-hotfix1`, publishes
to `custom`, which sits outside that order.
The action decides the package release number on the same split: a tag's version
is unique, so its packages are release 1, while develop repeats the same version
@@ -271,15 +264,6 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
installs it at `/usr/local/bin/publish_pkg.py` for other XRPLF repositories that
build their packages elsewhere.
## Docker image
The `docker` job installs the tested `xrpld` DEB on `ubuntu:26.04` using
[`image/Dockerfile`](image/Dockerfile), checks that the server starts, and,
with `publish: true`, pushes it to `xrplf/xrpld` on Docker Hub using the
`DOCKERHUB_TOKEN` secret, an organization access token for `xrplf`. A tag's
image is tagged with the tag name, `xrplf/xrpld:<version>`, and a develop image
as `xrplf/xrpld:develop`. Private builds are never pushed.
## How `build_pkg.py` works
`build_pkg.py` derives the `xrpld` software version from
@@ -313,7 +297,6 @@ With `PKG_RELEASE=1`, the package metadata becomes:
| `3.2.0-b0+abc1234` | `3.2.0~b0+abc1234-1%{?dist}` | `3.2.0~b0+abc1234-1` |
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
| `3.2.0-custom-1` | `3.2.0~custom.1-1%{?dist}` | `3.2.0~custom.1-1` |
`build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it
from the build host, so the RHEL image can track a newer release without
@@ -323,10 +306,7 @@ The Debian changelog entry carries the channel passed as `--channel`, which
only accepts the channels in the table above plus `UNRELEASED`, the Debian
convention for a build that targets no channel at all — what local and CMake
builds pass, since nothing publishes them. An unsupported pre-release, and
build metadata on a final release such as `3.2.0+abc123`, are both rejected,
except in the `custom` and `private` channels, which accept any version and
switch each `-` inside the pre-release or build metadata to `.`, so
`3.4.0-custom-1` packages as `3.4.0~custom.1`.
build metadata on a final release such as `3.2.0+abc123`, are both rejected.
The RPM path intentionally uses `~` in `Version`, matching the Debian
pre-release ordering convention, so RPM filenames/NVRs begin with forms like
+8 -20
View File
@@ -19,10 +19,7 @@ from pathlib import Path
# This script lives in the repository it packages.
SRC_DIR = Path(__file__).resolve().parents[1]
PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)$")
# Channels a tag of any version is published to, rather than only bN/rcN.
ANY_VERSION_CHANNELS = ("custom", "private")
PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)(\+.*)?$")
# The package name a variant suffixes, and the name every variant keeps for its
# on-disk paths (/usr/bin/xrpld, /etc/xrpld, xrpld.service).
@@ -71,21 +68,14 @@ def capture(*command: object) -> str:
).stdout.strip()
def package_version(reported: str, channel: str) -> str:
def package_version(reported: str) -> str:
"""Normalise a reported version into one the package formats accept.
A pre-release switches to '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before
the final 3.2.0; a no-op for a final release. The custom and private
channels accept any pre-release and build metadata, with any '-' inside
either switched to '.' (3.4.0-custom-1 -> 3.4.0~custom.1).
the final 3.2.0; a no-op for a final release.
"""
# Metadata first, as it may contain '-' too.
release, plus, metadata = reported.partition("+")
base, _, pre_release = release.partition("-")
if channel in ANY_VERSION_CHANNELS:
pre_release = pre_release.replace("-", ".")
metadata = metadata.replace("-", ".")
version = (f"{base}~{pre_release}" if pre_release else base) + plus + metadata
base, _, pre_release = reported.partition("-")
version = f"{base}~{pre_release}" if pre_release else base
# BuildInfo already SemVer-validates the version. Packaging adds one narrower
# constraint: after normalisation the version must not contain '-', because
@@ -94,9 +84,7 @@ def package_version(reported: str, channel: str) -> str:
f"unsupported version {reported!r}: {version!r} cannot contain '-'. "
"Use a single-token pre-release like 3.2.0-b1 or 3.2.0-rc2."
)
if channel in ANY_VERSION_CHANNELS:
return version
assert pre_release or not plus, (
assert pre_release or "+" not in reported, (
f"unsupported version {reported!r}: "
"build metadata is only supported on bN/rcN pre-releases."
)
@@ -313,7 +301,7 @@ def main() -> None:
parser.add_argument(
"--channel",
required=True,
choices=("stable", "rc", "beta", "custom", "develop", "private", "UNRELEASED"),
choices=("stable", "rc", "beta", "develop", "private", "UNRELEASED"),
help="release channel, written to debian/changelog",
)
args = parser.parse_args()
@@ -331,7 +319,7 @@ def main() -> None:
check_binaries(build_dir)
reported = read_version(build_dir / "xrpld")
version = package_version(reported, channel)
version = package_version(reported)
epoch = source_date_epoch()
# rpmbuild and dpkg-buildpackage both honour this for file timestamps.
+1 -1
View File
@@ -87,7 +87,7 @@ def main() -> None:
parser.add_argument(
"--channel",
required=True,
choices=("stable", "rc", "beta", "custom", "develop", "private"),
choices=("stable", "rc", "beta", "develop", "private"),
help="release channel, selecting the deb-<channel> and rpm-<channel>-hosted repositories",
)
parser.add_argument(
-15
View File
@@ -1,15 +0,0 @@
FROM ubuntu:26.04
RUN --mount=type=bind,target=/tmp/package \
apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends /tmp/package/*.deb \
&& rm -rf /var/lib/apt/lists/* \
&& ln -sf /dev/stdout /var/log/xrpld/debug.log
USER xrpld
WORKDIR /var/lib/xrpld
EXPOSE 2459 5005 6006 50051
ENTRYPOINT ["/usr/bin/xrpld"]
CMD ["--net", "--silent", "--conf", "/etc/xrpld/xrpld.cfg"]
+7 -5
View File
@@ -7,7 +7,8 @@
#include <string_view>
#if defined(__GLIBC__) && BOOST_OS_LINUX
// jemalloc manages its own arenas; glibc trimming and metrics do not apply.
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
#include <sys/resource.h>
#include <malloc.h>
@@ -43,7 +44,7 @@ namespace detail {
// cSpell:ignore statm
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
inline int
mallocTrimWithPad(std::size_t padBytes)
@@ -69,7 +70,7 @@ parseStatmRSSkB(std::string const& statm)
return (resident * pageSize) / 1024;
}
#endif // __GLIBC__ && BOOST_OS_LINUX
#endif // __GLIBC__ && BOOST_OS_LINUX && !PROFILE_JEMALLOC
} // namespace detail
@@ -80,8 +81,9 @@ mallocTrim(std::string_view tag, beast::Journal journal)
MallocTrimReport report;
#if !(defined(__GLIBC__) && BOOST_OS_LINUX)
JLOG(journal.debug()) << "malloc_trim not supported on this platform (tag=" << tag << ")";
#if !(defined(__GLIBC__) && BOOST_OS_LINUX) || defined(PROFILE_JEMALLOC)
JLOG(journal.debug()) << "malloc_trim not supported by this build's platform or allocator (tag="
<< tag << ")";
#else
// Keep glibc malloc_trim padding at 0 (default): 12h Mainnet tests across 0/256KB/1MB/16MB
// showed no clear, consistent benefit from custom padding—0 provided the best overall balance
@@ -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.");
+1 -1
View File
@@ -23,7 +23,7 @@ namespace {
//------------------------------------------------------------------------------
// clang-format off
// NOLINTNEXTLINE(readability-identifier-naming)
char const* const versionString = "3.5.0-custom-1"
char const* const versionString = "3.5.0-b0"
// clang-format on
;
-9
View File
@@ -104,7 +104,6 @@ enum class LedgerNameSpace : std::uint16_t {
LoanBroker = 'l', // lower-case L
Loan = 'L',
Sponsorship = '>',
TransactionProposal = 'y',
// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
Contract [[deprecated]] = 'c',
@@ -362,14 +361,6 @@ check(AccountID const& id, SeqProxy const& seq) noexcept
return {ltCHECK, indexHash(LedgerNameSpace::Check, id, seq.value())};
}
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
{
-44
View File
@@ -341,19 +341,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)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
return {pfCtx, {tefEXCEPTION, TxConsequences{tx}}};
// LCOV_EXCL_STOP
}
try
{
return {pfCtx, invokePreflight(pfCtx)};
@@ -375,16 +362,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)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
return {pfCtx, {tefEXCEPTION, TxConsequences{tx}}};
// LCOV_EXCL_STOP
}
try
{
return {pfCtx, invokePreflight(pfCtx)};
@@ -396,27 +373,6 @@ preflight(
}
}
NotTEC
invokeCheckPermission(ReadView const& view, STTx const& tx)
{
try
{
return withTxnType(view.rules(), tx.getTxnType(), [&]<typename T>() {
return Transactor::invokeCheckPermission<T>(view, tx);
});
}
// LCOV_EXCL_START
catch (UnknownTxnType const& e)
{
// Should never happen
JLOG(debugLog().fatal()) << "Unknown transaction type in invokeCheckPermission: "
<< e.txnType;
UNREACHABLE("xrpl::invokeCheckPermission : unknown transaction type");
return temUNKNOWN;
}
// LCOV_EXCL_STOP
}
PreclaimResult
preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, OpenView const& view)
{
@@ -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>
@@ -88,10 +87,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,365 +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/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/SeqProxy.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>
#include <string>
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 "the same [stateless format]
// checks it would receive if submitted directly" (On-Chain Cosigner
// spec §5.3.1 rule 2), so no statically-dead proposal can be stored.
// That is exactly the pair checkValidity runs on a direct submission:
// xrpl::preflight (the transactor's own preflight chain) and
// passesLocalChecks. TapDryRun accepts the unsigned canonical form;
// TapProposal skips signature-presence checks (§5.3.1.2). A failure
// surfaces the proposed type's own code, or temMALFORMED if the
// payload is not even a valid instance of that type.
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);
return inner.ter;
}
if (std::string reason; !passesLocalChecks(stx, reason))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
"fails local checks: "
<< reason;
return temMALFORMED;
}
}
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;
// deserialize itself returns unexpected(temMALFORMED) when
// sfSignerEntries is missing or an element is not an sfSignerEntry.
// Those are the right codes for a transaction object. Here the object
// is an on-ledger ltSIGNER_LIST (sfSignerEntries is SoeRequired;
// each element is an sfSignerEntry). A corrupt SLE can still throw
// from the STObject accessors deserialize calls: getFieldArray
// ("Wrong field type") or getAccountID/getFieldU16 ("Field not
// found") when an sfSignerEntry is missing required fields. Either
// the expected<> error or a throw is unexpected ledger state, not a
// malformed TransactionProposalCreate, so tefBAD_LEDGER (rather
// than tefINTERNAL, which is reserved for truly unreachable code
// paths) is the right code.
try
{
auto const accountSigners =
SignerEntries::deserialize(*sleSigners, ctx.j, "ledger");
if (!accountSigners)
{
// Only reachable if the on-ledger SignerList is corrupt
// (SignerListSet re-runs the same deserialize on write).
// Exercised by testCorruptSignerList via an OpenLedger
// overlay that produces the same failure modes.
JLOG(ctx.j.fatal()) << "TransactionProposalCreate: unparseable SignerList: "
<< transToken(accountSigners.error());
return std::unexpected(tefBAD_LEDGER);
}
return std::ranges::any_of(
*accountSigners, [&](auto const& entry) { return entry.account == proposer; });
}
catch (std::exception const& e)
{
// Same as above: only reachable via ledger corruption that
// makes an STObject accessor throw. Exercised by
// testCorruptSignerList.
JLOG(ctx.j.fatal())
<< "TransactionProposalCreate: unparseable SignerList: " << e.what();
return std::unexpected(tefBAD_LEDGER);
}
};
auto isSigner = isAuthorizedFor(target);
if (!isSigner)
return isSigner.error();
// A delegate that the target has granted permission over the
// proposed transaction — 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. xrpl::invokeCheckPermission is the type-erased
// submission hierarchy (not checkTxPermission alone, which would
// reject a matching granular grant). Qualify xrpl:: so the inherited
// Transactor template is not chosen; it cannot deduce T here. A
// failed grant is still "not authorized" and becomes
// tecNO_PERMISSION below — Create is already signed, so do not leak
// the pre-sign terNO_DELEGATE_PERMISSION.
if (!*isSigner && proposedTx.isFieldPresent(sfDelegate))
{
AccountID const delegateAccount = proposedTx.getAccountID(sfDelegate);
STTx const proposedStTx{STObject{proposedTx}};
if (isTesSuccess(xrpl::invokeCheckPermission(ctx.view, proposedStTx)))
{
// A grant cannot exist without a funded authorize (DelegateSet
// uses tecNO_TARGET; AccountDelete of the delegatee removes the
// Delegate SLE). Do not treat a missing account as a Create-time
// user error — that would extra-validate the proposed tx. If
// permission passed anyway, the ledger is corrupt.
if (!ctx.view.exists(keylet::account(delegateAccount)))
return tefINTERNAL; // LCOV_EXCL_LINE
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, SeqProxy::rawTicket(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
@@ -201,11 +201,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;
+1 -20
View File
@@ -348,20 +348,8 @@ Batch::preflight(PreflightContext const& ctx)
return temINVALID_FLAG;
auto const innerAccount = stx.getAccountID(sfAccount);
// TransactionProposalCreate preflights a proposed Batch with
// TapDryRun | TapProposal so signature-presence checks are deferred
// to collection time (On-Chain Cosigner spec §5.3.1.2). Inner
// preflight used to pass only TapBatch, so those bits never reached
// the inners: an unsigned account-reserve SponsorshipTransfer then
// demanded sfSponsorSignature and the Create failed with
// temINVALID_INNER_BATCH. Spec §6.1.1 names an inner Sponsor as a
// collectable slot, so forward TapProposal/TapDryRun. Always OR in
// TapBatch — PreflightContext with a parentBatchId requires it.
// LoanSet already short-circuits on tfInnerBatchTxn; it is also in
// kDisabledTxTypes, so it never reaches this call.
ApplyFlags const innerFlags = TapBatch | (ctx.flags & (TapProposal | TapDryRun));
if (auto const preflightResult =
xrpl::preflight(ctx.registry, ctx.rules, parentBatchId, stx, innerFlags, ctx.j);
xrpl::preflight(ctx.registry, ctx.rules, parentBatchId, stx, TapBatch, ctx.j);
!isTesSuccess(preflightResult.ter))
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
@@ -427,13 +415,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
+208 -299
View File
@@ -42,7 +42,6 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <functional>
#include <optional>
#include <source_location>
@@ -176,41 +175,22 @@ keyAtEpoch(
} // namespace
namespace {
// Renders the check's own construction site (usually in mpt.cpp) so it can be
// embedded as the "reason" alongside the test call site reported as file/line.
std::string
checkSiteReason(std::source_location const& checkLoc)
{
return std::string("check ") + std::filesystem::path{checkLoc.file_name()}.filename().string() +
"(" + std::to_string(checkLoc.line()) + ")";
}
} // namespace
void
MptFlags::operator()(Env& env) const
{
env.test.expect(
tester_.checkFlags(flags_, holder_),
checkSiteReason(checkLoc_),
testLoc_.file_name(),
testLoc_.line());
env.test.expect(tester_.checkFlags(flags_, holder_));
}
void
MptBalance::operator()(Env& env) const
{
env.test.expect(
amount_ == tester_.getBalance(account_),
checkSiteReason(checkLoc_),
testLoc_.file_name(),
testLoc_.line());
env.test.expect(amount_ == tester_.getBalance(account_));
}
void
RequireAny::operator()(Env& env) const
{
env.test.expect(cb_(), checkSiteReason(checkLoc_), testLoc_.file_name(), testLoc_.line());
env.test.expect(cb_());
}
std::unordered_map<std::string, Account>
@@ -226,7 +206,7 @@ MPTTester::makeHolders(std::vector<Account> const& holders)
return accounts;
}
MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg, std::source_location const& loc)
MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg)
: env_(env)
, issuer_(std::move(issuer))
, holders_(makeHolders(arg.holders))
@@ -258,7 +238,7 @@ MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg, std::source_l
env_.require(Owners(*arg.auditor, 0));
}
if (arg.create)
create(*arg.create, loc);
create(*arg.create);
}
MPTTester::MPTTester(
@@ -293,7 +273,7 @@ makeMPTCreate(MPTInitDef const& arg)
.authHolder = arg.authHolder};
}
MPTTester::MPTTester(MPTInitDef const& arg, std::source_location const& loc)
MPTTester::MPTTester(MPTInitDef const& arg)
: MPTTester{
arg.env,
arg.issuer,
@@ -302,8 +282,7 @@ MPTTester::MPTTester(MPTInitDef const& arg, std::source_location const& loc)
.fund = arg.fund,
.close = arg.close,
.create = makeMPTCreate(arg),
},
loc}
}}
{
}
@@ -340,7 +319,7 @@ MPTTester::createJV(MPTCreate const& arg)
}
void
MPTTester::create(MPTCreate const& arg, std::source_location const& loc)
MPTTester::create(MPTCreate const& arg)
{
if (id_)
Throw<std::runtime_error>("MPT can't be reused");
@@ -353,30 +332,30 @@ MPTTester::create(MPTCreate const& arg, std::source_location const& loc)
.metadata = arg.metadata,
.immutableFlags = arg.immutableFlags,
.domainID = arg.domainID});
if (!isTesSuccess(submit(arg, {jv, loc})))
if (!isTesSuccess(submit(arg, jv)))
{
// Verify issuance doesn't exist
env_.require(RequireAny(
[&]() -> bool { return env_.le(keylet::mptokenIssuance(*id_)) == nullptr; }, loc));
[&]() -> bool { return env_.le(keylet::mptokenIssuance(*id_)) == nullptr; }));
id_.reset();
}
else
{
env_.require(MptFlags(*this, arg.flags.value_or(0), std::nullopt, loc));
env_.require(MptFlags(*this, arg.flags.value_or(0)));
auto authAndPay = [&](auto const& accts, auto const&& getAcct) {
for (auto const& it : accts)
{
authorize({.account = getAcct(it)}, loc);
authorize({.account = getAcct(it)});
if ((arg.flags.value_or(0) & tfMPTRequireAuth) && arg.authHolder)
authorize({.account = issuer_, .holder = getAcct(it)}, loc);
authorize({.account = issuer_, .holder = getAcct(it)});
if (arg.pay && arg.pay->first.empty())
pay(issuer_, getAcct(it), arg.pay->second, std::nullopt, std::nullopt, loc);
pay(issuer_, getAcct(it), arg.pay->second);
}
if (arg.pay)
{
for (auto const& p : arg.pay->first)
pay(issuer_, p, arg.pay->second, std::nullopt, std::nullopt, loc);
pay(issuer_, p, arg.pay->second);
}
};
if (arg.authorize)
@@ -418,13 +397,13 @@ MPTTester::destroyJV(MPTDestroy const& arg)
}
void
MPTTester::destroy(MPTDestroy const& arg, std::source_location const& loc)
MPTTester::destroy(MPTDestroy const& arg)
{
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
json::Value const jv =
destroyJV({.issuer = arg.issuer ? arg.issuer : issuer_, .id = arg.id ? arg.id : id_});
submit(arg, {jv, loc});
submit(arg, jv);
}
Account const&
@@ -452,7 +431,7 @@ MPTTester::authorizeJV(MPTAuthorize const& arg)
}
void
MPTTester::authorize(MPTAuthorize const& arg, std::source_location const& loc)
MPTTester::authorize(MPTAuthorize const& arg)
{
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
@@ -461,7 +440,7 @@ MPTTester::authorize(MPTAuthorize const& arg, std::source_location const& loc)
.holder = arg.holder,
.id = arg.id ? arg.id : id_,
});
if (auto const result = submit(arg, {jv, loc}); isTesSuccess(result))
if (auto const result = submit(arg, jv); isTesSuccess(result))
{
// Issuer authorizes
if (!arg.account || *arg.account == issuer_)
@@ -470,12 +449,12 @@ MPTTester::authorize(MPTAuthorize const& arg, std::source_location const& loc)
// issuer un-authorizes the holder
if (arg.flags.value_or(0) == tfMPTUnauthorize)
{
env_.require(MptFlags(*this, flags, arg.holder, loc));
env_.require(MptFlags(*this, flags, arg.holder));
// issuer authorizes the holder
}
else
{
env_.require(MptFlags(*this, flags | lsfMPTAuthorized, arg.holder, loc));
env_.require(MptFlags(*this, flags | lsfMPTAuthorized, arg.holder));
}
}
// Holder authorizes
@@ -483,17 +462,13 @@ MPTTester::authorize(MPTAuthorize const& arg, std::source_location const& loc)
{
auto const flags = getFlags(arg.account);
// holder creates a token
env_.require(MptFlags(*this, flags, arg.account, loc));
env_.require(MptBalance(*this, *arg.account, 0, loc));
env_.require(MptFlags(*this, flags, arg.account));
env_.require(MptBalance(*this, *arg.account, 0));
}
else
{
// Verify that the MPToken doesn't exist.
forObject(
[&](SLEP const& sle) {
return env_.test.expect(!sle, "", loc.file_name(), loc.line());
},
arg.account);
forObject([&](SLEP const& sle) { return env_.test.BEAST_EXPECT(!sle); }, arg.account);
}
}
else if (
@@ -502,31 +477,27 @@ MPTTester::authorize(MPTAuthorize const& arg, std::source_location const& loc)
if (result == tecDUPLICATE)
{
// Verify that MPToken already exists
env_.require(RequireAny(
[&]() -> bool {
return env_.le(keylet::mptoken(*id_, arg.account->id())) != nullptr;
},
loc));
env_.require(RequireAny([&]() -> bool {
return env_.le(keylet::mptoken(*id_, arg.account->id())) != nullptr;
}));
}
else
{
// Verify MPToken doesn't exist if holder failed authorizing(unless
// it already exists)
env_.require(RequireAny(
[&]() -> bool {
return env_.le(keylet::mptoken(*id_, arg.account->id())) == nullptr;
},
loc));
env_.require(RequireAny([&]() -> bool {
return env_.le(keylet::mptoken(*id_, arg.account->id())) == nullptr;
}));
}
}
}
void
MPTTester::authorizeHolders(Holders const& holders, std::source_location const& loc)
MPTTester::authorizeHolders(Holders const& holders)
{
for (auto const& holder : holders)
{
authorize({.account = holder}, loc);
authorize({.account = holder});
}
}
@@ -574,7 +545,7 @@ MPTTester::setJV(MPTSet const& arg)
}
void
MPTTester::set(MPTSet const& arg, std::source_location const& loc)
MPTTester::set(MPTSet const& arg)
{
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
@@ -589,7 +560,7 @@ MPTTester::set(MPTSet const& arg, std::source_location const& loc)
.domainID = arg.domainID,
.issuerPubKey = arg.issuerPubKey,
.auditorPubKey = arg.auditorPubKey});
if (submit(arg, {jv, loc}) == tesSUCCESS && arg.flags.value_or(0) != 0u)
if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0) != 0u)
{
auto require = [&](std::optional<Account> const& holder, bool unchanged) {
auto flags = getFlags(holder);
@@ -615,7 +586,7 @@ MPTTester::set(MPTSet const& arg, std::source_location const& loc)
}
}
}
env_.require(MptFlags(*this, flags, holder, loc));
env_.require(MptFlags(*this, flags, holder));
};
if (arg.account)
require(std::nullopt, arg.holder.has_value());
@@ -624,48 +595,43 @@ MPTTester::set(MPTSet const& arg, std::source_location const& loc)
if (arg.issuerPubKey)
{
env_.require(RequireAny(
[&]() -> bool {
return forObject([&](SLEP const& sle) -> bool {
if (sle)
env_.require(RequireAny([&]() -> bool {
return forObject([&](SLEP const& sle) -> bool {
if (sle)
{
auto const issuerPubKey = getPubKey(issuer_);
if (!issuerPubKey)
{
auto const issuerPubKey = getPubKey(issuer_);
if (!issuerPubKey)
{
Throw<std::runtime_error>(
"MPTTester::set: issuer's pubkey is not set");
}
return strHex((*sle)[sfIssuerEncryptionKey]) == strHex(*issuerPubKey);
Throw<std::runtime_error>("MPTTester::set: issuer's pubkey is not set");
}
return false;
});
},
loc));
return strHex((*sle)[sfIssuerEncryptionKey]) == strHex(*issuerPubKey);
}
return false;
});
}));
}
if (arg.auditorPubKey)
{
env_.require(RequireAny(
[&]() -> bool {
return forObject([&](SLEP const& sle) -> bool {
if (sle)
env_.require(RequireAny([&]() -> bool {
return forObject([&](SLEP const& sle) -> bool {
if (sle)
{
if (!auditor_.has_value())
Throw<std::runtime_error>("MPTTester::set: auditor is not set");
auto const auditorPubKey = getPubKey(*auditor_);
if (!auditorPubKey)
{
if (!auditor_.has_value())
Throw<std::runtime_error>("MPTTester::set: auditor is not set");
auto const auditorPubKey = getPubKey(*auditor_);
if (!auditorPubKey)
{
Throw<std::runtime_error>(
"MPTTester::set: auditor's pubkey is not set");
}
return strHex((*sle)[sfAuditorEncryptionKey]) == strHex(*auditorPubKey);
Throw<std::runtime_error>(
"MPTTester::set: auditor's pubkey is not set");
}
return false;
});
},
loc));
return strHex((*sle)[sfAuditorEncryptionKey]) == strHex(*auditorPubKey);
}
return false;
});
}));
}
}
}
@@ -828,8 +794,7 @@ MPTTester::pay(
Account const& dest,
std::int64_t amount,
std::optional<TER> err,
std::optional<std::vector<std::string>> credentials,
std::source_location const& loc)
std::optional<std::vector<std::string>> credentials)
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
@@ -840,15 +805,13 @@ MPTTester::pay(
if (credentials)
{
env_(
WithSourceLocation<json::Value>{jtx::pay(src, dest, mpt(amount)), loc},
jtx::pay(src, dest, mpt(amount)),
Ter(err.value_or(tesSUCCESS)),
credentials::Ids(*credentials));
}
else
{
env_(
WithSourceLocation<json::Value>{jtx::pay(src, dest, mpt(amount)), loc},
Ter(err.value_or(tesSUCCESS)));
env_(jtx::pay(src, dest, mpt(amount)), Ter(err.value_or(tesSUCCESS)));
}
if (!isTesSuccess(env_.ter()))
@@ -857,23 +820,23 @@ MPTTester::pay(
env_.close();
if (src == issuer_)
{
env_.require(MptBalance(*this, src, srcAmt + amount, loc));
env_.require(MptBalance(*this, dest, destAmt + amount, loc));
env_.require(MptBalance(*this, src, srcAmt + amount));
env_.require(MptBalance(*this, dest, destAmt + amount));
}
else if (dest == issuer_)
{
env_.require(MptBalance(*this, src, srcAmt - amount, loc));
env_.require(MptBalance(*this, dest, destAmt - amount, loc));
env_.require(MptBalance(*this, src, srcAmt - amount));
env_.require(MptBalance(*this, dest, destAmt - amount));
}
else
{
STAmount const saAmount = {*id_, amount};
auto const actual = multiply(saAmount, transferRate(*env_.current(), *id_)).mpt().value();
// Sender pays the transfer fee if any
env_.require(MptBalance(*this, src, srcAmt - actual, loc));
env_.require(MptBalance(*this, dest, destAmt + amount, loc));
env_.require(MptBalance(*this, src, srcAmt - actual));
env_.require(MptBalance(*this, dest, destAmt + amount));
// Outstanding amount is reduced by the transfer fee if any
env_.require(MptBalance(*this, issuer_, outstandingAmt - (actual - amount), loc));
env_.require(MptBalance(*this, issuer_, outstandingAmt - (actual - amount)));
}
}
@@ -882,23 +845,20 @@ MPTTester::claw(
Account const& issuer,
Account const& holder,
std::int64_t amount,
std::optional<TER> err,
std::source_location const& loc)
std::optional<TER> err)
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
auto const issuerAmt = getBalance(issuer);
auto const holderAmt = getBalance(holder);
env_(
WithSourceLocation<json::Value>{jtx::claw(issuer, mpt(amount), holder), loc},
Ter(err.value_or(tesSUCCESS)));
env_(jtx::claw(issuer, mpt(amount), holder), Ter(err.value_or(tesSUCCESS)));
if (!isTesSuccess(env_.ter()))
amount = 0;
if (close_)
env_.close();
env_.require(MptBalance(*this, issuer, issuerAmt - std::min(holderAmt, amount), loc));
env_.require(MptBalance(*this, holder, holderAmt - std::min(holderAmt, amount), loc));
env_.require(MptBalance(*this, issuer, issuerAmt - std::min(holderAmt, amount)));
env_.require(MptBalance(*this, holder, holderAmt - std::min(holderAmt, amount)));
}
PrettyAmount
@@ -1263,7 +1223,7 @@ MPTTester::fillConversionCiphertexts(
}
void
MPTTester::convert(MPTConvert const& arg, std::source_location const& loc)
MPTTester::convert(MPTConvert const& arg)
{
json::Value const jv = convertJV(arg, ticketOrSeq(arg.ticketSeq, arg.account));
@@ -1295,37 +1255,27 @@ MPTTester::convert(MPTConvert const& arg, std::source_location const& loc)
auto const prevOutstanding = getIssuanceOutstandingBalance();
if (submit(arg, {jv, loc}) == tesSUCCESS)
if (submit(arg, jv) == tesSUCCESS)
{
auto const postConfidentialOutstanding = getIssuanceConfidentialBalance();
auto const postOutstanding = getIssuanceOutstandingBalance();
env_.require(MptBalance(*this, account, holderAmt - amt, loc));
env_.require(RequireAny(
[&]() -> bool {
return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
},
loc));
env_.require(RequireAny(
[&]() -> bool {
return prevConfidentialOutstanding + amt == postConfidentialOutstanding;
},
loc));
env_.require(MptBalance(*this, account, holderAmt - amt));
env_.require(RequireAny([&]() -> bool {
return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
}));
env_.require(RequireAny([&]() -> bool {
return prevConfidentialOutstanding + amt == postConfidentialOutstanding;
}));
env_.require(RequireAny(
[&]() -> bool {
return getEncryptedBalance(account, holderEncryptedInbox).has_value();
},
loc));
env_.require(RequireAny(
[&]() -> bool {
return getEncryptedBalance(account, holderEncryptedSpending).has_value();
},
loc));
env_.require(RequireAny(
[&]() -> bool {
return getEncryptedBalance(account, issuerEncryptedBalance).has_value();
},
loc));
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(account, holderEncryptedInbox).has_value();
}));
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(account, holderEncryptedSpending).has_value();
}));
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(account, issuerEncryptedBalance).has_value();
}));
auto const postInboxBalance = getDecryptedBalance(account, holderEncryptedInbox);
auto const postIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance);
@@ -1341,60 +1291,53 @@ MPTTester::convert(MPTConvert const& arg, std::source_location const& loc)
if (!postAuditorBalance)
Throw<std::runtime_error>("Failed to get post-convert auditor balance");
env_.require(RequireAny(
[&]() -> bool {
return getEncryptedBalance(account, auditorEncryptedBalance).has_value();
},
loc));
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(account, auditorEncryptedBalance).has_value();
}));
// auditor's encrypted balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevAuditorBalance + amt == *postAuditorBalance; }, loc));
[&]() -> bool { return *prevAuditorBalance + amt == *postAuditorBalance; }));
}
// spending balance should not change
env_.require(RequireAny(
[&]() -> bool { return *postSpendingBalance == *prevSpendingBalance; }, loc));
env_.require(
RequireAny([&]() -> bool { return *postSpendingBalance == *prevSpendingBalance; }));
// issuer's encrypted balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevIssuerBalance + amt == *postIssuerBalance; }, loc));
env_.require(
RequireAny([&]() -> bool { return *prevIssuerBalance + amt == *postIssuerBalance; }));
// holder's inbox balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevInboxBalance + amt == *postInboxBalance; }, loc));
env_.require(
RequireAny([&]() -> bool { return *prevInboxBalance + amt == *postInboxBalance; }));
// sum of holder's inbox and spending balance should equal to issuer's
// encrypted balance
env_.require(RequireAny(
[&]() -> bool {
return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
},
loc));
env_.require(RequireAny([&]() -> bool {
return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
}));
if (arg.holderPubKey)
{
env_.require(RequireAny(
[&]() -> bool {
return forObject(
[&](SLEP const& sle) -> bool {
if (sle)
env_.require(RequireAny([&]() -> bool {
return forObject(
[&](SLEP const& sle) -> bool {
if (sle)
{
auto const holderPubKey = getPubKey(account);
if (!holderPubKey)
{
auto const holderPubKey = getPubKey(account);
if (!holderPubKey)
{
Throw<std::runtime_error>(
"MPTTester::convert: holder's pubkey is "
"not set");
}
return strHex((*sle)[sfHolderEncryptionKey]) ==
strHex(*holderPubKey);
Throw<std::runtime_error>(
"MPTTester::convert: holder's pubkey is "
"not set");
}
return false;
},
account);
},
loc));
return strHex((*sle)[sfHolderEncryptionKey]) == strHex(*holderPubKey);
}
return false;
},
account);
}));
}
}
}
@@ -1429,7 +1372,7 @@ MPTTester::convertJV(MPTConvert const& arg, std::uint32_t seq)
}
void
MPTTester::send(MPTConfidentialSend const& arg, std::source_location const& loc)
MPTTester::send(MPTConfidentialSend const& arg)
{
json::Value const jv = sendJV(arg, ticketOrSeq(arg.ticketSeq, arg.account));
@@ -1476,7 +1419,7 @@ MPTTester::send(MPTConfidentialSend const& arg, std::source_location const& loc)
auto const prevCOA = getIssuanceConfidentialBalance();
auto const prevOA = getIssuanceOutstandingBalance();
if (submit(arg, {jv, loc}) == tesSUCCESS)
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
@@ -1498,48 +1441,39 @@ MPTTester::send(MPTConfidentialSend const& arg, std::source_location const& loc)
Throw<std::runtime_error>("Failed to get Post-send balance");
// Public balances unchanged
env_.require(MptBalance(*this, account, senderPubAmt, loc));
env_.require(MptBalance(*this, dest, destPubAmt, loc));
env_.require(MptBalance(*this, account, senderPubAmt));
env_.require(MptBalance(*this, dest, destPubAmt));
// OA and COA unchanged
env_.require(
RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }, loc));
env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }, loc));
env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }));
env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }));
// Verify sender changes
env_.require(RequireAny(
[&]() -> bool {
return *prevSenderSpending >= amt &&
*postSenderSpending == *prevSenderSpending - amt;
},
loc));
env_.require(RequireAny([&]() -> bool { return postSenderInbox == prevSenderInbox; }, loc));
env_.require(RequireAny(
[&]() -> bool {
return *prevSenderIssuer >= amt && *postSenderIssuer == *prevSenderIssuer - amt;
},
loc));
env_.require(RequireAny([&]() -> bool {
return *prevSenderSpending >= amt && *postSenderSpending == *prevSenderSpending - amt;
}));
env_.require(RequireAny([&]() -> bool { return postSenderInbox == prevSenderInbox; }));
env_.require(RequireAny([&]() -> bool {
return *prevSenderIssuer >= amt && *postSenderIssuer == *prevSenderIssuer - amt;
}));
// Verify destination changes
env_.require(RequireAny([&]() -> bool { return *postDestInbox == *prevDestInbox + amt; }));
env_.require(RequireAny([&]() -> bool { return *postDestSpending == *prevDestSpending; }));
env_.require(
RequireAny([&]() -> bool { return *postDestInbox == *prevDestInbox + amt; }, loc));
env_.require(
RequireAny([&]() -> bool { return *postDestSpending == *prevDestSpending; }, loc));
env_.require(
RequireAny([&]() -> bool { return *postDestIssuer == *prevDestIssuer + amt; }, loc));
RequireAny([&]() -> bool { return *postDestIssuer == *prevDestIssuer + amt; }));
// Cross checks
env_.require(RequireAny(
[&]() -> bool { return *postSenderInbox + *postSenderSpending == *postSenderIssuer; },
loc));
[&]() -> bool { return *postSenderInbox + *postSenderSpending == *postSenderIssuer; }));
env_.require(RequireAny(
[&]() -> bool { return *postDestInbox + *postDestSpending == *postDestIssuer; }, loc));
[&]() -> bool { return *postDestInbox + *postDestSpending == *postDestIssuer; }));
// Version: sender increments by 1; receiver version is unchanged by incoming sends
env_.require(RequireAny(
[&]() -> bool { return getMPTokenVersion(account) == prevSenderVersion + 1; }, loc));
[&]() -> bool { return getMPTokenVersion(account) == prevSenderVersion + 1; }));
env_.require(
RequireAny([&]() -> bool { return getMPTokenVersion(dest) == prevDestVersion; }, loc));
RequireAny([&]() -> bool { return getMPTokenVersion(dest) == prevDestVersion; }));
if (arg.auditorEncryptedAmt || auditor_)
{
@@ -1548,24 +1482,19 @@ MPTTester::send(MPTConfidentialSend const& arg, std::source_location const& loc)
if (!postSenderAuditor || !postDestAuditor)
Throw<std::runtime_error>("Failed to get Post-send balance");
env_.require(RequireAny(
[&]() -> bool {
return *postSenderAuditor == *postSenderIssuer &&
*postDestAuditor == *postDestIssuer;
},
loc));
env_.require(RequireAny([&]() -> bool {
return *postSenderAuditor == *postSenderIssuer &&
*postDestAuditor == *postDestIssuer;
}));
// verify sender
env_.require(RequireAny(
[&]() -> bool {
return *prevSenderAuditor >= amt &&
*postSenderAuditor == *prevSenderAuditor - amt;
},
loc));
env_.require(RequireAny([&]() -> bool {
return *prevSenderAuditor >= amt && *postSenderAuditor == *prevSenderAuditor - amt;
}));
// verify dest
env_.require(RequireAny(
[&]() -> bool { return *postDestAuditor == *prevDestAuditor + amt; }, loc));
env_.require(
RequireAny([&]() -> bool { return *postDestAuditor == *prevDestAuditor + amt; }));
}
}
}
@@ -1797,7 +1726,7 @@ computeNextSendChainState(
}
void
MPTTester::confidentialClaw(MPTConfidentialClawback const& arg, std::source_location const& loc)
MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
{
json::Value jv;
auto const account = arg.account ? *arg.account : issuer_;
@@ -1837,39 +1766,34 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg, std::source_loca
auto const prevOA = getIssuanceOutstandingBalance();
auto const prevVersion = getMPTokenVersion(holder);
if (submit(arg, {jv, loc}) == tesSUCCESS)
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
auto const postVersion = getMPTokenVersion(holder);
// Verify holder's public balance is unchanged
env_.require(MptBalance(*this, holder, holderPubAmt, loc));
env_.require(MptBalance(*this, holder, holderPubAmt));
// Verify COA and OA are reduced correctly
env_.require(
RequireAny([&]() -> bool { return prevCOA >= amt && postCOA == prevCOA - amt; }, loc));
env_.require(RequireAny(
[&]() -> bool {
return prevOA && postOA && *prevOA >= amt && *postOA == *prevOA - amt;
},
loc));
RequireAny([&]() -> bool { return prevCOA >= amt && postCOA == prevCOA - amt; }));
env_.require(RequireAny([&]() -> bool {
return prevOA && postOA && *prevOA >= amt && *postOA == *prevOA - amt;
}));
// Verify holder's confidential balances are zeroed out
env_.require(RequireAny(
[&]() -> bool { return getDecryptedBalance(holder, holderEncryptedInbox) == 0; }, loc));
[&]() -> bool { return getDecryptedBalance(holder, holderEncryptedInbox) == 0; }));
env_.require(RequireAny(
[&]() -> bool { return getDecryptedBalance(holder, holderEncryptedSpending) == 0; },
loc));
[&]() -> bool { return getDecryptedBalance(holder, holderEncryptedSpending) == 0; }));
env_.require(RequireAny(
[&]() -> bool { return getDecryptedBalance(holder, issuerEncryptedBalance) == 0; },
loc));
[&]() -> bool { return getDecryptedBalance(holder, issuerEncryptedBalance) == 0; }));
env_.require(RequireAny(
[&]() -> bool { return getDecryptedBalance(holder, auditorEncryptedBalance) == 0; },
loc));
[&]() -> bool { return getDecryptedBalance(holder, auditorEncryptedBalance) == 0; }));
// Verify version is incremented
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }, loc));
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
}
}
@@ -2001,7 +1925,7 @@ MPTTester::mergeInboxJV(MPTMergeInbox const& arg) const
}
void
MPTTester::mergeInbox(MPTMergeInbox const& arg, std::source_location const& loc)
MPTTester::mergeInbox(MPTMergeInbox const& arg)
{
json::Value const jv = mergeInboxJV(arg);
Account const& account = requireValue(arg.account, "account");
@@ -2019,7 +1943,7 @@ MPTTester::mergeInbox(MPTMergeInbox const& arg, std::source_location const& loc)
if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
Throw<std::runtime_error>("Failed to get pre-mergeInbox balances");
if (submit(arg, {jv, loc}) == tesSUCCESS)
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
@@ -2035,20 +1959,17 @@ MPTTester::mergeInbox(MPTMergeInbox const& arg, std::source_location const& loc)
!prevIssuerEncrypted || !postInboxEncrypted || !postIssuerEncrypted)
Throw<std::runtime_error>("Failed to get post-mergeInbox balances");
env_.require(MptBalance(*this, account, holderPubAmt, loc));
env_.require(
RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }, loc));
env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }, loc));
env_.require(MptBalance(*this, account, holderPubAmt));
env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }));
env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }));
env_.require(RequireAny(
[&]() -> bool {
return *postSpendingBalance == *prevInboxBalance + *prevSpendingBalance &&
*postInboxBalance == 0;
},
loc));
env_.require(RequireAny([&]() -> bool {
return *postSpendingBalance == *prevInboxBalance + *prevSpendingBalance &&
*postInboxBalance == 0;
}));
env_.require(
RequireAny([&]() -> bool { return *prevIssuerBalance == *postIssuerBalance; }, loc));
RequireAny([&]() -> bool { return *prevIssuerBalance == *postIssuerBalance; }));
auto const holderPubKey = getPubKey(account);
if (!holderPubKey)
@@ -2059,23 +1980,18 @@ MPTTester::mergeInbox(MPTMergeInbox const& arg, std::source_location const& loc)
if (!expectedInbox)
Throw<std::runtime_error>("Failed to get canonical zero encryption");
env_.require(RequireAny([&]() -> bool { return *postInboxEncrypted == *expectedInbox; }));
env_.require(
RequireAny([&]() -> bool { return *postInboxEncrypted == *expectedInbox; }, loc));
env_.require(RequireAny(
[&]() -> bool { return *postIssuerEncrypted == *prevIssuerEncrypted; }, loc));
env_.require(RequireAny(
[&]() -> bool {
return postAuditorEncrypted.has_value() == prevAuditorEncrypted.has_value() &&
(!postAuditorEncrypted || *postAuditorEncrypted == *prevAuditorEncrypted);
},
loc));
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }, loc));
RequireAny([&]() -> bool { return *postIssuerEncrypted == *prevIssuerEncrypted; }));
env_.require(RequireAny([&]() -> bool {
return postAuditorEncrypted.has_value() == prevAuditorEncrypted.has_value() &&
(!postAuditorEncrypted || *postAuditorEncrypted == *prevAuditorEncrypted);
}));
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
env_.require(RequireAny(
[&]() -> bool {
return *postSpendingBalance + *postInboxBalance == *postIssuerBalance;
},
loc));
env_.require(RequireAny([&]() -> bool {
return *postSpendingBalance + *postInboxBalance == *postIssuerBalance;
}));
}
}
@@ -2110,7 +2026,7 @@ MPTTester::getMPTokenVersion(Account const account) const
}
void
MPTTester::convertBack(MPTConvertBack const& arg, std::source_location const& loc)
MPTTester::convertBack(MPTConvertBack const& arg)
{
json::Value const jv = convertBackJV(arg, ticketOrSeq(arg.ticketSeq, arg.account));
@@ -2138,22 +2054,18 @@ MPTTester::convertBack(MPTConvertBack const& arg, std::source_location const& lo
auto const prevOutstanding = getIssuanceOutstandingBalance();
auto const prevVersion = getMPTokenVersion(account);
if (submit(arg, {jv, loc}) == tesSUCCESS)
if (submit(arg, jv) == tesSUCCESS)
{
auto const postConfidentialOutstanding = getIssuanceConfidentialBalance();
auto const postOutstanding = getIssuanceOutstandingBalance();
auto const postVersion = getMPTokenVersion(account);
env_.require(MptBalance(*this, account, holderAmt + amt, loc));
env_.require(RequireAny(
[&]() -> bool {
return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
},
loc));
env_.require(RequireAny(
[&]() -> bool {
return prevConfidentialOutstanding - amt == postConfidentialOutstanding;
},
loc));
env_.require(MptBalance(*this, account, holderAmt + amt));
env_.require(RequireAny([&]() -> bool {
return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
}));
env_.require(RequireAny([&]() -> bool {
return prevConfidentialOutstanding - amt == postConfidentialOutstanding;
}));
auto const postInboxBalance = getDecryptedBalance(account, holderEncryptedInbox);
auto const postIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance);
@@ -2171,31 +2083,28 @@ MPTTester::convertBack(MPTConvertBack const& arg, std::source_location const& lo
// auditor's encrypted balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevAuditorBalance - amt == *postAuditorBalance; }, loc));
[&]() -> bool { return *prevAuditorBalance - amt == *postAuditorBalance; }));
}
// inbox balance should not change
env_.require(
RequireAny([&]() -> bool { return *postInboxBalance == *prevInboxBalance; }, loc));
env_.require(RequireAny([&]() -> bool { return *postInboxBalance == *prevInboxBalance; }));
// issuer's encrypted balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevIssuerBalance - amt == *postIssuerBalance; }, loc));
env_.require(
RequireAny([&]() -> bool { return *prevIssuerBalance - amt == *postIssuerBalance; }));
// holder's spending balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevSpendingBalance - amt == *postSpendingBalance; }, loc));
[&]() -> bool { return *prevSpendingBalance - amt == *postSpendingBalance; }));
// holder's confidential balance version is updated correctly
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }, loc));
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
// sum of holder's inbox and spending balance should equal to issuer's
// encrypted balance
env_.require(RequireAny(
[&]() -> bool {
return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
},
loc));
env_.require(RequireAny([&]() -> bool {
return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
}));
}
}
@@ -2263,7 +2172,7 @@ MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq)
}
void
MPTTester::mirrorUpdate(MPTMirrorUpdate const& arg, std::source_location const& loc)
MPTTester::mirrorUpdate(MPTMirrorUpdate const& arg)
{
json::Value jv;
jv[jss::TransactionType] = jss::ConfidentialMPTMirrorUpdate;
@@ -2288,7 +2197,7 @@ MPTTester::mirrorUpdate(MPTMirrorUpdate const& arg, std::source_location const&
jv[sfZKProof] = strHex(gMakeZeroBuffer(kEcEqualityProofLength));
}
submit(arg, {jv, loc});
submit(arg, jv);
}
} // namespace xrpl::test::jtx
-352
View File
@@ -1,352 +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/SeqProxy.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, SeqProxy::rawTicket(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, SeqProxy::rawTicket(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
+22 -78
View File
@@ -32,7 +32,6 @@
#include <cstring>
#include <functional>
#include <optional>
#include <source_location>
#include <stdexcept>
#include <string>
#include <unordered_map>
@@ -76,19 +75,13 @@ private:
MPTTester& tester_;
std::uint32_t flags_;
std::optional<Account> holder_;
// Test call site (forwarded from the MPTTester method).
std::source_location testLoc_;
// The site where this check object was constructed (usually in mpt.cpp).
std::source_location checkLoc_;
public:
MptFlags(
MPTTester& tester,
std::uint32_t flags,
std::optional<Account> const& holder = std::nullopt,
std::source_location const& testLoc = std::source_location::current(),
std::source_location const& checkLoc = std::source_location::current())
: tester_(tester), flags_(flags), holder_(holder), testLoc_(testLoc), checkLoc_(checkLoc)
std::optional<Account> const& holder = std::nullopt)
: tester_(tester), flags_(flags), holder_(holder)
{
}
@@ -105,23 +98,10 @@ private:
MPTTester const& tester_;
Account const& account_;
std::int64_t const amount_;
// Test call site (forwarded from the MPTTester method).
std::source_location testLoc_;
// The site where this check object was constructed (usually in mpt.cpp).
std::source_location checkLoc_;
public:
MptBalance(
MPTTester& tester,
Account const& account,
std::int64_t amount,
std::source_location const& testLoc = std::source_location::current(),
std::source_location const& checkLoc = std::source_location::current())
: tester_(tester)
, account_(account)
, amount_(amount)
, testLoc_(testLoc)
, checkLoc_(checkLoc)
MptBalance(MPTTester& tester, Account const& account, std::int64_t amount)
: tester_(tester), account_(account), amount_(amount)
{
}
@@ -136,17 +116,9 @@ class RequireAny
{
private:
std::function<bool()> cb_;
// Test call site (forwarded from the MPTTester method).
std::source_location testLoc_;
// The site where this check object was constructed (usually in mpt.cpp).
std::source_location checkLoc_;
public:
RequireAny(
std::function<bool()> const& cb,
std::source_location const& testLoc = std::source_location::current(),
std::source_location const& checkLoc = std::source_location::current())
: cb_(cb), testLoc_(testLoc), checkLoc_(checkLoc)
RequireAny(std::function<bool()> const& cb) : cb_(cb)
{
}
@@ -516,14 +488,8 @@ public:
static constexpr auto holderEncryptedSpending = EncryptedBalanceType::HolderEncryptedSpending;
static constexpr auto auditorEncryptedBalance = EncryptedBalanceType::AuditorEncryptedBalance;
MPTTester(
Env& env,
Account issuer,
MPTInit const& constr = {},
std::source_location const& loc = std::source_location::current());
MPTTester(
MPTInitDef const& constr,
std::source_location const& loc = std::source_location::current());
MPTTester(Env& env, Account issuer, MPTInit const& constr = {});
MPTTester(MPTInitDef const& constr);
MPTTester(
Env& env,
Account issuer,
@@ -533,44 +499,34 @@ public:
operator MPT() const;
void
create(
MPTCreate const& arg = MPTCreate{},
std::source_location const& loc = std::source_location::current());
create(MPTCreate const& arg = MPTCreate{});
static json::Value
createJV(MPTCreate const& arg = MPTCreate{});
void
destroy(
MPTDestroy const& arg = MPTDestroy{},
std::source_location const& loc = std::source_location::current());
destroy(MPTDestroy const& arg = MPTDestroy{});
static json::Value
destroyJV(MPTDestroy const& arg = MPTDestroy{});
void
authorize(
MPTAuthorize const& arg = MPTAuthorize{},
std::source_location const& loc = std::source_location::current());
authorize(MPTAuthorize const& arg = MPTAuthorize{});
static json::Value
authorizeJV(MPTAuthorize const& arg = MPTAuthorize{});
void
authorizeHolders(
Holders const& holders,
std::source_location const& loc = std::source_location::current());
authorizeHolders(Holders const& holders);
void
set(MPTSet const& set = {}, std::source_location const& loc = std::source_location::current());
set(MPTSet const& set = {});
static json::Value
setJV(MPTSet const& set = {});
void
convert(
MPTConvert const& arg = MPTConvert{},
std::source_location const& loc = std::source_location::current());
convert(MPTConvert const& arg = MPTConvert{});
/**
* @brief Build a confidential convert JV without submitting it.
@@ -584,17 +540,13 @@ public:
convertJV(MPTConvert const& arg, std::uint32_t seq);
void
mergeInbox(
MPTMergeInbox const& arg = MPTMergeInbox{},
std::source_location const& loc = std::source_location::current());
mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{});
[[nodiscard]] json::Value
mergeInboxJV(MPTMergeInbox const& arg = MPTMergeInbox{}) const;
void
send(
MPTConfidentialSend const& arg = MPTConfidentialSend{},
std::source_location const& loc = std::source_location::current());
send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
/**
* @brief Build a confidential send JV.
@@ -632,9 +584,7 @@ public:
chainAfterSend(Account const& sender, std::uint64_t sendAmt, json::Value const& jv) const;
void
convertBack(
MPTConvertBack const& arg = MPTConvertBack{},
std::source_location const& loc = std::source_location::current());
convertBack(MPTConvertBack const& arg = MPTConvertBack{});
/**
* @brief Build a confidential convertBack JV without submitting it.
@@ -650,14 +600,10 @@ public:
convertBackJV(MPTConvertBack const& arg, std::uint32_t seq);
void
confidentialClaw(
MPTConfidentialClawback const& arg = MPTConfidentialClawback{},
std::source_location const& loc = std::source_location::current());
confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
void
mirrorUpdate(
MPTMirrorUpdate const& arg = MPTMirrorUpdate{},
std::source_location const& loc = std::source_location::current());
mirrorUpdate(MPTMirrorUpdate const& arg = MPTMirrorUpdate{});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
@@ -727,16 +673,14 @@ public:
Account const& dest,
std::int64_t amount,
std::optional<TER> err = std::nullopt,
std::optional<std::vector<std::string>> credentials = std::nullopt,
std::source_location const& loc = std::source_location::current());
std::optional<std::vector<std::string>> credentials = std::nullopt);
void
claw(
Account const& issuer,
Account const& holder,
std::int64_t amount,
std::optional<TER> err = std::nullopt,
std::source_location const& loc = std::source_location::current());
std::optional<TER> err = std::nullopt);
[[nodiscard]] PrettyAmount
mpt(std::int64_t amount) const;
@@ -851,7 +795,7 @@ private:
template <typename A>
TER
submit(A const& arg, WithSourceLocation<json::Value> jv)
submit(A const& arg, json::Value jv)
{
auto const expectedFlags = Txflags(arg.flags.value_or(0));
auto const expectedTer = Ter(arg.err.value_or(tesSUCCESS));
@@ -859,7 +803,7 @@ private:
if constexpr (requires { arg.fee; })
{
if (arg.fee)
jv.value[jss::Fee] = to_string(*arg.fee);
jv[jss::Fee] = to_string(*arg.fee);
}
std::optional<std::uint32_t> ticketSeq;
-191
View File
@@ -1,191 +0,0 @@
#pragma once
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <cstdint>
#include <optional>
#include <vector>
/**
* @brief Helpers for constructing TransactionProposal test transactions.
*/
namespace xrpl::test::jtx::proposal {
// The owner-reserve increments a proposal holds against its proposer. Tests
// spend these rather than repeating their values, so they follow the transactor
// instead of checking it; TransactionProposalCreate_test pins the values
// themselves, so a change to them has to be a deliberate one.
using xrpl::proposal::kBatchProposalOwnerCount;
using xrpl::proposal::kProposalOwnerCount;
/**
* @brief Build a TransactionProposalCreate carrying an unsigned proposed
* transaction.
*
* @param proposer The account creating and paying the reserve for the proposal.
* @param proposedTx The proposed transaction, in unsigned canonical form; see
* unsignedPayload().
* @param expiration Absolute time after which the proposal may no longer be
* completed.
* @return The TransactionProposalCreate JSON object.
*/
json::Value
create(Account const& proposer, json::Value const& proposedTx, std::uint32_t expiration);
/**
* @brief Conditions that check what a proposal transaction did to the ledger.
*
* Each is named for the generator it verifies, so a submission and its check
* read as a pair, and the transactions that later sign or complete a proposal
* get their own entries here rather than sharing one.
*/
namespace verify {
/**
* @brief The condition returned by create(); see it for what is checked.
*/
class Create
{
public:
/**
* @throws std::logic_error if attached to another transaction type.
*/
void
operator()(Env&, JTx&) const;
};
/**
* @brief Check the ledger effects a TransactionProposalCreate must have,
* whatever its outcome: on tesSUCCESS a new proposal holding what was
* submitted, listed in the proposer's directory and paid for by its reserve;
* otherwise nothing moved. Nothing of the target's moves either way.
*
* @code
* env(proposal::create(alice, payload, expiration), proposal::verify::create());
* @endcode
*/
[[nodiscard]] inline Create
create()
{
return Create{};
}
} // namespace verify
/**
* @brief Put a transaction of any type into the form a proposal stores it in:
* unsigned and ticket-based, with the fee the target account will pay fixed
* now.
*
* This takes whatever any jtx generator produces — @c pay(), @c loan::set(),
* @c sponsor::transfer(), an outer @c Batch — and applies only what the
* proposal itself demands of the payload, so a test never hand-rolls those
* fields. It leaves the rest of @p tx untouched, which is what lets a test
* build one valid payload and then break exactly one rule of it.
*
* The payload is ticket-based so unrelated activity on the target account
* cannot invalidate it while signatures are collected. A Fee already set on
* @p tx is left alone, so a payload whose fee is not the base fee — a Batch,
* say — can carry its own; otherwise the fee is filled in the same way as for
* an ordinary submission.
*
* @param env The test environment providing ledger fee settings.
* @param tx The transaction to propose.
* @param ticketSeq A ticket sequence owned by @p tx's account.
* @return The proposed transaction JSON object.
*/
json::Value
unsignedPayload(Env const& env, json::Value tx, std::uint32_t ticketSeq);
/**
* @brief Put a transaction into the form an inner transaction of a proposed
* Batch takes, as @c batch::Inner does for an ordinary Batch.
*
* @param tx The transaction to nest.
* @param seq The sequence number of @p tx's own account.
* @return The inner transaction JSON object.
*/
json::Value
innerTx(json::Value tx, std::uint32_t seq);
/**
* @brief An unsigned outer Batch payload holding @p inners.
*
* A proposed Batch stores no BatchSigners — the participants' signatures are
* collected on-ledger afterwards — but its fee is fixed now and must already
* cover them. By default one signer is assumed for each inner account other
* than @p target, which is what those participants will contribute.
*
* @param env The test environment providing ledger fee settings.
* @param target The outer account of the Batch, and the proposal's target.
* @param ticketSeq A ticket sequence owned by @p target.
* @param flags The Batch mode flags, e.g. @c tfAllOrNothing.
* @param inners The inner transactions; see innerTx().
* @param numSigners Overrides the number of signatures the fee accounts for.
* @return The proposed Batch JSON object.
*/
json::Value
unsignedBatch(
Env const& env,
Account const& target,
std::uint32_t ticketSeq,
std::uint32_t flags,
std::vector<json::Value> const& inners,
std::optional<std::uint32_t> numSigners = std::nullopt);
/**
* @brief Give @p proposer a place on @p target's SignerList, so it may
* create proposals against @p target.
*
* Only the target account itself, or an account on its SignerList, may
* create a TransactionProposalCreate against it (On-Chain Cosigner V1
* authorization). Sets a minimal one-signer, quorum-1 list; the quorum
* itself is irrelevant here since it only governs the proposed
* transaction's own completion, not who may propose it.
*
* @param env The test environment.
* @param target The account whose SignerList is set.
* @param proposer The account to add to it.
*/
void
authorizeProposer(Env& env, Account const& target, Account const& proposer);
/**
* @brief Create tickets for a proposal to be built against, and close the
* ledger.
*
* @param env The test environment.
* @param account The account that will own the tickets, i.e. the target of the
* proposals to come.
* @param count How many tickets to create.
* @return The first ticket sequence created; the rest follow it.
*/
std::uint32_t
createTicket(Env& env, Account const& account, std::uint32_t count = 1);
/**
* @brief An absolute expiration @p delta past the environment's current time.
*
* @c expiration(env, 0s) is an expiration that has already passed.
*/
std::uint32_t
expiration(Env& env, NetClock::duration delta);
/**
* @brief The proposal stored against a target account's ticket.
* @return empty if no such proposal exists.
*/
[[nodiscard]] SLE::const_pointer
entry(Env const& env, AccountID const& target, std::uint32_t ticketSeq);
[[nodiscard]] SLE::const_pointer
entry(Env const& env, Account const& target, std::uint32_t ticketSeq);
} // namespace xrpl::test::jtx::proposal
-61
View File
@@ -9,7 +9,6 @@
#include <test/jtx/owners.h> // IWYU pragma: keep
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/proposal.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ticket.h>
@@ -36,7 +35,6 @@
#include <xrpl/tx/transactors/nft/NFTokenMint.h>
#include <algorithm>
#include <chrono> // IWYU pragma: keep
#include <cstdint>
#include <iterator>
#include <optional>
@@ -1701,64 +1699,6 @@ public:
BEAST_EXPECT(res[jss::result].isMember(jss::marker));
}
// A TransactionProposal blocks AccountDelete (it is not in
// nonObligationDeleter) but was omitted from kDeletionBlockers, so
// deletion_blockers_only reported a clean directory. Tickets on the
// same account are auto-removed at delete time and must stay omitted.
void
testDeletionBlockersProposal()
{
testcase("deletion_blockers_only includes TransactionProposal");
using namespace jtx;
using namespace std::chrono_literals;
Env env{*this, testableAmendments()};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const ticketSeq = proposal::createTicket(env, alice);
env(proposal::create(
alice,
proposal::unsignedPayload(env, pay(alice, bob, XRP(1)), ticketSeq),
proposal::expiration(env, 100s)),
proposal::verify::create());
env.close();
json::Value params;
params[jss::account] = alice.human();
params[jss::deletion_blockers_only] = true;
params[jss::ledger_index] = "validated";
{
auto const resp = env.rpc("json", "account_objects", to_string(params));
auto const& aobjs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(aobjs.isArray() && aobjs.size() == 1))
{
BEAST_EXPECT(aobjs[0u][sfLedgerEntryType.jsonName] == jss::TransactionProposal);
BEAST_EXPECT(aobjs[0u][sfOwner.jsonName] == alice.human());
}
}
{
params[jss::type] = jss::transaction_proposal;
auto const resp = env.rpc("json", "account_objects", to_string(params));
auto const& aobjs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(aobjs.isArray() && aobjs.size() == 1))
BEAST_EXPECT(aobjs[0u][sfLedgerEntryType.jsonName] == jss::TransactionProposal);
}
{
params[jss::type] = jss::check;
auto const resp = env.rpc("json", "account_objects", to_string(params));
auto const& aobjs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(aobjs.isArray() && aobjs.size() == 0);
}
}
void
run() override
{
@@ -1771,7 +1711,6 @@ public:
testAccountObjectMarker();
testSponsoredFilter();
testAccountObjectDoesntShowCancelledOffers();
testDeletionBlockersProposal();
}
};
+2 -89
View File
@@ -15,7 +15,6 @@
#include <test/jtx/offer.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/proposal.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ticket.h>
#include <test/jtx/token.h>
@@ -328,7 +327,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
FieldType const typeID,
std::string const& expectedError,
bool required = true,
std::optional<std::string> typeNameOverride = std::nullopt,
std::source_location const location = std::source_location::current())
{
forAllApiVersions([&, this](unsigned apiVersion) {
@@ -351,8 +349,8 @@ class LedgerEntry_test : public beast::unit_test::Suite
correctRequest[fieldName] = fieldValue;
json::Value const jrr = env.rpc(
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
auto const expectedErrMsg = rpc::expectedFieldMessage(
fieldName, typeNameOverride.value_or(getTypeName(typeID)));
auto const expectedErrMsg =
rpc::expectedFieldMessage(fieldName, getTypeName(typeID));
checkErrorValue(jrr, expectedError, expectedErrMsg, location);
};
@@ -428,7 +426,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
FieldType::HashField,
"malformedRequest",
true,
std::nullopt,
location);
}
@@ -444,7 +441,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
test::jtx::Env& env,
json::StaticString const& parentField,
std::vector<Subfield> const& subfields,
std::optional<std::string> parentTypeNameOverride = std::nullopt,
std::source_location const location = std::source_location::current())
{
testMalformedField(
@@ -454,7 +450,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
FieldType::HashOrObjectField,
"malformedRequest",
true,
parentTypeNameOverride,
location);
json::Value correctOutput;
@@ -2032,87 +2027,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
}
}
void
testTransactionProposal()
{
testcase("TransactionProposal");
using namespace test::jtx;
using namespace std::literals::chrono_literals;
Env env{*this};
Account const target{"target"};
Account const bob{"bob"};
env.fund(XRP(10000), target, bob);
env.close();
// A ticket for the proposal to be built against, and the proposal
// itself (an unsigned Payment payload).
std::uint32_t const ticketSeq = proposal::createTicket(env, target);
env(proposal::create(
target,
proposal::unsignedPayload(env, pay(target, bob, XRP(1)), ticketSeq),
proposal::expiration(env, 100s)),
proposal::verify::create());
env.close();
std::string const ledgerHash{to_string(env.closed()->header().hash)};
auto const proposalIndex = to_string(keylet::txProposal(target.id(), ticketSeq).key);
{
// Request by target account and ticket sequence.
json::Value jvParams;
jvParams[jss::transaction_proposal][jss::account] = target.human();
jvParams[jss::transaction_proposal][jss::ticket_seq] = ticketSeq;
jvParams[jss::ledger_hash] = ledgerHash;
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::TransactionProposal);
BEAST_EXPECT(proposalIndex == jrr[jss::node][jss::index].asString());
}
{
// Request by object index (hex string form).
json::Value jvParams;
jvParams[jss::transaction_proposal] = proposalIndex;
jvParams[jss::ledger_hash] = ledgerHash;
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::TransactionProposal);
BEAST_EXPECT(proposalIndex == jrr[jss::node][jss::index].asString());
}
{
// No proposal exists against this (account, ticket_seq) pair.
json::Value jvParams;
jvParams[jss::transaction_proposal][jss::account] = target.human();
jvParams[jss::transaction_proposal][jss::ticket_seq] = ticketSeq + 1;
jvParams[jss::ledger_hash] = ledgerHash;
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Lookup by an index of the wrong entry type.
json::Value jvParams;
jvParams[jss::transaction_proposal] = to_string(keylet::account(target).key);
jvParams[jss::ledger_hash] = ledgerHash;
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
}
{
// Malformed cases (missing / wrong-type subfields, and a
// non-object non-hex-string parent value). Once the parent has
// been shown not to be an object, parseTransactionProposal names
// "hex string" — not "hex string or object" — as the form still
// on the table.
runLedgerEntryTest(
env,
jss::transaction_proposal,
{
{.fieldName = jss::account, .malformedErrorMsg = "malformedAddress"},
{.fieldName = jss::ticket_seq, .malformedErrorMsg = "malformedRequest"},
},
"hex string");
}
}
void
testDID()
{
@@ -2833,7 +2747,6 @@ public:
testSignerList();
testSponsorship();
testTicket();
testTransactionProposal();
testDID();
testInvalidOracleLedgerEntry();
testOracleLedgerEntry();
+9 -6
View File
@@ -12,7 +12,7 @@ using namespace xrpl;
// cSpell:ignore statm
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
namespace xrpl::detail {
long
parseStatmRSSkB(std::string const& statm);
@@ -48,7 +48,7 @@ TEST(MallocTrimReport, structure)
EXPECT_EQ(report.deltaKB(), 0);
}
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
TEST(ParseStatmRSSkB, standard_format)
{
using xrpl::detail::parseStatmRSSkB;
@@ -127,7 +127,7 @@ TEST(MallocTrim, without_debug_logging)
MallocTrimReport const report = mallocTrim("without_debug", journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1});
@@ -149,7 +149,7 @@ TEST(MallocTrim, empty_tag)
beast::Journal const journal{beast::Journal::getNullSink()};
MallocTrimReport const report = mallocTrim("", journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
#else
@@ -179,7 +179,7 @@ TEST(MallocTrim, with_debug_logging)
MallocTrimReport const report = mallocTrim("debug_test", journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
EXPECT_GE(report.durationUs.count(), 0);
@@ -188,9 +188,12 @@ TEST(MallocTrim, with_debug_logging)
#else
EXPECT_EQ(report.supported, false);
EXPECT_EQ(report.trimResult, -1);
EXPECT_EQ(report.rssBeforeKB, -1);
EXPECT_EQ(report.rssAfterKB, -1);
EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1});
EXPECT_EQ(report.minfltDelta, -1);
EXPECT_EQ(report.majfltDelta, -1);
EXPECT_EQ(report.deltaKB(), 0);
#endif
}
@@ -203,7 +206,7 @@ TEST(MallocTrim, repeated_calls)
{
MallocTrimReport const report = mallocTrim("iteration_" + std::to_string(i), journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
#else
-1
View File
@@ -33,7 +33,6 @@
#include <xrpl/ledger/entries/SignerListEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/SponsorshipEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/TicketEntry.h>
#include <xrpl/ledger/entries/TransactionProposalEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/VaultEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/XChainOwnedClaimIDEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/XChainOwnedCreateAccountClaimIDEntry.h> // IWYU pragma: keep
@@ -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);
}
}
-116
View File
@@ -1,116 +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/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <utility>
namespace xrpl::test {
namespace {
// A bare STObject carrying only sfTransactionType. isValidProposal accepts an
// STObject (not an STTx) precisely so that this file's defense-in-depth
// checks can be tested without the surrounding STTx format validation.
inline STObject
txOfType(std::uint16_t txType)
{
STObject tx(sfGeneric);
tx.setFieldU16(sfTransactionType, txType);
return tx;
}
// A bare Batch STObject wrapping the given inner transaction as its single
// sfRawTransactions entry.
inline STObject
batchWrapping(STObject inner)
{
STArray rawTxns(sfRawTransactions);
rawTxns.push_back(std::move(inner));
STObject batch(sfGeneric);
batch.setFieldU16(sfTransactionType, ttBATCH);
batch.setFieldArray(sfRawTransactions, rawTxns);
return batch;
}
} // namespace
// The happy path — an ordinary Payment is independently submittable.
TEST(ProposalHelpers, plain_payment_is_valid)
{
EXPECT_TRUE(proposal::isValidProposal(txOfType(ttPAYMENT)));
}
// A nested TransactionProposalCreate. In practice STTx construction rejects
// this earlier (the payload lacks TransactionProposalCreate's own template
// fields), but the defense here re-checks that guard so the two cannot
// drift apart.
TEST(ProposalHelpers, nested_proposal_is_rejected)
{
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttTRANSACTION_PROPOSAL_CREATE)));
}
// Any pseudo-transaction — see STTx::isPseudoTx. Also normally caught earlier
// by STTx construction / preflight0.
TEST(ProposalHelpers, pseudo_tx_is_rejected)
{
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttAMENDMENT)));
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttFEE)));
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttUNL_MODIFY)));
}
// tfInnerBatchTxn marks a transaction as an inner leg of an enclosing Batch,
// so it must never stand on its own as a proposed transaction. preflight0
// rejects the standalone case with temINVALID_INNER_BATCH before we get
// here; the guard is re-checked so the two cannot drift apart.
TEST(ProposalHelpers, inner_batch_flag_is_rejected)
{
STObject tx = txOfType(ttPAYMENT);
tx.setFieldU32(sfFlags, tfInnerBatchTxn);
EXPECT_FALSE(proposal::isValidProposal(tx));
}
// A Flags value that is present but does not include tfInnerBatchTxn must
// not be rejected — the check is bit-specific, not "any flag present".
TEST(ProposalHelpers, other_flags_are_accepted)
{
STObject tx = txOfType(ttPAYMENT);
tx.setFieldU32(sfFlags, tfFullyCanonicalSig);
EXPECT_TRUE(proposal::isValidProposal(tx));
}
// A Batch wrapping a plain inner is fine — the loop is only there to catch
// specifically forbidden inner types.
TEST(ProposalHelpers, batch_with_plain_inner_is_valid)
{
EXPECT_TRUE(proposal::isValidProposal(batchWrapping(txOfType(ttPAYMENT))));
}
// A Batch whose inner is itself a proposal must be rejected.
TEST(ProposalHelpers, batch_with_nested_proposal_inner_is_rejected)
{
EXPECT_FALSE(proposal::isValidProposal(batchWrapping(txOfType(ttTRANSACTION_PROPOSAL_CREATE))));
}
// A Batch whose inner is a pseudo-transaction must be rejected.
TEST(ProposalHelpers, batch_with_pseudo_inner_is_rejected)
{
EXPECT_FALSE(proposal::isValidProposal(batchWrapping(txOfType(ttAMENDMENT))));
}
// A Batch with no sfRawTransactions field skips the inner-loop entirely.
// Not something the transactor would ever emit, but the branch exists in the
// helper (the field is optional at the STObject level) and should hold.
TEST(ProposalHelpers, batch_without_raw_transactions_is_valid)
{
EXPECT_TRUE(proposal::isValidProposal(txOfType(ttBATCH)));
}
} // namespace xrpl::test
-33
View File
@@ -1,33 +0,0 @@
include(GoogleTest)
find_package(GTest REQUIRED)
# kept separate from xrpl_tests so that the libxrpl test binary stays free of server code.
add_executable(xrpld_tests main.cpp)
patch_nix_binary(xrpld_tests)
set_target_properties(
xrpld_tests
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
target_include_directories(
xrpld_tests
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_SOURCE_DIR}/src"
)
target_link_libraries(
xrpld_tests
PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl rpcspec::rpcspec
)
set(test_modules rpc)
foreach(module IN LISTS test_modules)
file(
GLOB_RECURSE sources
CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/${module}/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/${module}.cpp"
)
target_sources(xrpld_tests PRIVATE ${sources})
endforeach()
gtest_discover_tests(xrpld_tests DISCOVERY_TIMEOUT 60)
-9
View File
@@ -1,9 +0,0 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
int
main(int argc, char** argv)
{
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}
-412
View File
@@ -1,412 +0,0 @@
#include <xrpld/rpc/detail/SpecBridge.hpp>
#include <xrpld/rpc/detail/JsonObjectView.hpp>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <gtest/gtest.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/Types.hpp>
#include <rpcspec/handlers/ledger/Spec.hpp>
#include <cstdint>
#include <string>
#include <string_view>
#include <variant>
namespace xrpl {
namespace {
using ObjectView = rpc::JsonObjectView;
json::Value
makeParams()
{
static constexpr auto kParams = R"JSON({
"str": "hello",
"num": 42,
"big": 3000000000,
"neg": -7,
"real": 1.5,
"flag": true,
"nothing": null,
"obj": {"inner": "deep"},
"arr": ["a", "b"]
})JSON";
json::Value params;
EXPECT_TRUE(json::Reader{}.parse(kParams, params));
return params;
}
} // namespace
TEST(JsonView, field_types)
{
auto params = makeParams();
ObjectView const root{params};
auto const str = root.child("str");
EXPECT_TRUE(str.present());
EXPECT_EQ(str.key(), "str");
EXPECT_TRUE(str.isString());
EXPECT_EQ(str.asString(), "hello");
EXPECT_TRUE(str.is<std::string>());
EXPECT_FALSE(str.isBool());
EXPECT_FALSE(str.isInt64());
EXPECT_FALSE(str.isObject());
EXPECT_FALSE(str.isArray());
// uint32-ness is about the value, not the storage: a non-negative Int answers true.
auto const num = root.child("num");
EXPECT_TRUE(num.isInt64());
EXPECT_EQ(num.asInt64(), 42);
EXPECT_TRUE(num.isUint32());
EXPECT_EQ(num.asUint32(), 42U);
EXPECT_TRUE(num.is<std::int64_t>());
EXPECT_TRUE(num.is<std::uint32_t>());
auto const big = root.child("big");
EXPECT_TRUE(big.isInt64());
EXPECT_EQ(big.asInt64(), 3000000000LL);
EXPECT_TRUE(big.isUint32());
EXPECT_EQ(big.asUint32(), 3000000000U);
auto const neg = root.child("neg");
EXPECT_TRUE(neg.isInt64());
EXPECT_EQ(neg.asInt64(), -7);
EXPECT_FALSE(neg.isUint32());
EXPECT_FALSE(neg.is<std::uint32_t>());
auto const real = root.child("real");
EXPECT_TRUE(real.isDouble());
EXPECT_EQ(real.asDouble(), 1.5);
EXPECT_TRUE(real.is<double>());
EXPECT_FALSE(real.isInt64());
auto const flag = root.child("flag");
EXPECT_TRUE(flag.isBool());
EXPECT_TRUE(flag.asBool());
EXPECT_TRUE(flag.is<bool>());
// A boolean is not a number, though json::Value::isIntegral() says otherwise.
EXPECT_FALSE(flag.isInt64());
EXPECT_FALSE(flag.isUint32());
// Present, but null: no predicate claims it.
auto const nothing = root.child("nothing");
EXPECT_TRUE(nothing.present());
EXPECT_FALSE(nothing.isString());
EXPECT_FALSE(nothing.isInt64());
EXPECT_FALSE(nothing.isBool());
EXPECT_FALSE(nothing.isDouble());
EXPECT_FALSE(nothing.isObject());
EXPECT_FALSE(nothing.isArray());
auto const obj = root.child("obj");
EXPECT_TRUE(obj.isObject());
EXPECT_TRUE(obj.is<::rpc::spec::JsonObject>());
EXPECT_EQ(obj.objectSize(), 1U);
EXPECT_EQ(obj.arraySize(), 0U);
auto const arr = root.child("arr");
EXPECT_TRUE(arr.isArray());
EXPECT_TRUE(arr.is<::rpc::spec::JsonArray>());
EXPECT_EQ(arr.arraySize(), 2U);
EXPECT_EQ(arr.objectSize(), 0U);
}
TEST(JsonView, field_absent)
{
auto params = makeParams();
ObjectView const root{params};
// Every predicate has to answer for an absent field: that is how a spec asks whether
// an optional field was supplied.
auto const missing = root.child("nope");
EXPECT_FALSE(missing.present());
EXPECT_EQ(missing.key(), "nope");
EXPECT_FALSE(missing.isString());
EXPECT_FALSE(missing.isInt64());
EXPECT_FALSE(missing.isUint32());
EXPECT_FALSE(missing.isBool());
EXPECT_FALSE(missing.isDouble());
EXPECT_FALSE(missing.isObject());
EXPECT_FALSE(missing.isArray());
EXPECT_EQ(missing.arraySize(), 0U);
EXPECT_EQ(missing.objectSize(), 0U);
EXPECT_FALSE(missing.is<std::string>());
EXPECT_FALSE(missing.is<::rpc::spec::JsonObject>());
// Walking down from an absent field stays absent rather than faulting.
EXPECT_FALSE(missing.child("deeper").present());
EXPECT_FALSE(missing.element(0).present());
// Same for a non-object asked for a child, or a non-array for an element.
EXPECT_FALSE(root.child("str").child("inner").present());
EXPECT_FALSE(root.child("str").element(0).present());
}
TEST(JsonView, field_navigation)
{
auto params = makeParams();
ObjectView const root{params};
auto const inner = root.child("obj").child("inner");
EXPECT_TRUE(inner.present());
EXPECT_EQ(inner.asString(), "deep");
EXPECT_EQ(inner.key(), "inner");
// An element borrows its parent's key.
auto const first = root.child("arr").element(0);
EXPECT_TRUE(first.present());
EXPECT_EQ(first.asString(), "a");
EXPECT_EQ(first.key(), "arr");
EXPECT_EQ(root.child("arr").element(1).asString(), "b");
EXPECT_FALSE(root.child("arr").element(2).present());
}
TEST(JsonView, field_set)
{
auto params = makeParams();
ObjectView root{params};
// What a modifier does: normalise in place, so the converter after it sees that.
root.child("str").set(std::string_view{"world"});
EXPECT_EQ(params["str"].asString(), "world");
root.child("num").set(std::uint32_t{7});
EXPECT_EQ(params["num"].asUInt(), 7U);
root.child("neg").set(std::int64_t{-1});
EXPECT_EQ(params["neg"].asInt(), -1);
root.child("flag").set(false);
EXPECT_FALSE(params["flag"].asBool());
root.child("real").set(2.5);
EXPECT_EQ(params["real"].asDouble(), 2.5);
// Write access reaches through a child.
root.child("obj").child("inner").set(std::string_view{"changed"});
EXPECT_EQ(params["obj"]["inner"].asString(), "changed");
}
TEST(JsonView, field_set_spans_both_32_bit_types)
{
auto params = makeParams();
ObjectView root{params};
// The spec's toNumber modifier produces values up to UINT32_MAX. json::Int is 32 bits,
// so routing everything through it would wrap those negative and lose the value.
root.child("num").set(std::int64_t{3000000000});
auto const big = root.child("num");
EXPECT_TRUE(big.isUint32());
EXPECT_EQ(big.asUint32(), 3000000000U);
EXPECT_EQ(big.asInt64(), 3000000000LL);
root.child("neg").set(std::int64_t{-2000000000});
auto const negative = root.child("neg");
EXPECT_TRUE(negative.isInt64());
EXPECT_EQ(negative.asInt64(), -2000000000LL);
EXPECT_FALSE(negative.isUint32());
}
TEST(JsonView, mutable_navigation)
{
auto params = makeParams();
ObjectView root{params};
EXPECT_FALSE(root.child("nope").present());
EXPECT_FALSE(root.child("obj").child("nope").present());
EXPECT_FALSE(root.child("str").child("inner").present());
EXPECT_FALSE(root.child("arr").element(2).present());
EXPECT_FALSE(root.child("str").element(0).present());
EXPECT_FALSE(params.isMember("nope"));
EXPECT_FALSE(params["obj"].isMember("nope"));
auto second = root.child("arr").element(1);
ASSERT_TRUE(second.present());
EXPECT_EQ(second.key(), "arr");
second.set(std::string_view{"z"});
EXPECT_EQ(params["arr"][1u].asString(), "z");
json::Value array{json::ValueType::Array};
ObjectView arrayRoot{array};
EXPECT_FALSE(arrayRoot.child("anything").present());
}
TEST(JsonView, object_root)
{
auto params = makeParams();
ObjectView const root{params};
EXPECT_TRUE(root.isObject());
EXPECT_FALSE(root.isArray());
EXPECT_TRUE(root.child("str").present());
EXPECT_FALSE(root.child("nope").present());
// Must not create the key: the non-const operator[] would have inserted a null.
EXPECT_FALSE(params.isMember("nope"));
json::Value array{json::ValueType::Array};
ObjectView const arrayRoot{array};
EXPECT_TRUE(arrayRoot.isArray());
EXPECT_FALSE(arrayRoot.isObject());
EXPECT_FALSE(arrayRoot.child("anything").present());
}
TEST(JsonView, mutable_root_over_const_source_reads)
{
auto const params = makeParams();
ObjectView root{params};
auto const str = root.child("str");
ASSERT_TRUE(str.present());
EXPECT_EQ(str.asString(), "hello");
EXPECT_EQ(root.child("obj").child("inner").asString(), "deep");
EXPECT_EQ(root.child("arr").element(1).asString(), "b");
EXPECT_FALSE(root.child("nope").present());
}
TEST(SpecBridge, shared_spec_parses_json_value)
{
json::Value params{json::ValueType::Object};
params["ledger_index"] = 42U;
params["binary"] = true;
params["transactions"] = true;
auto const input = ::rpc::spec::handlers::ledger::kSpec.parse(params, 2);
ASSERT_TRUE(input.has_value());
EXPECT_TRUE(input->binary);
EXPECT_TRUE(input->transactions);
EXPECT_FALSE(input->expand);
ASSERT_TRUE(std::holds_alternative<std::uint32_t>(input->ledger.value));
EXPECT_EQ(std::get<std::uint32_t>(input->ledger.value), 42U);
}
TEST(SpecBridge, shared_spec_reports_a_bad_json_value)
{
json::Value params{json::ValueType::Object};
params["ledger_index"] = "not-a-ledger";
auto const input = ::rpc::spec::handlers::ledger::kSpec.parse(params, 2);
ASSERT_FALSE(input.has_value());
EXPECT_EQ(std::get<::rpc::XrpldError>(input.error().code), RpcInvalidParams);
}
TEST(SpecBridge, shared_spec_warns_on_a_deprecated_field)
{
json::Value params{json::ValueType::Object};
params["type"] = "hashes";
auto const warnings = ::rpc::spec::handlers::ledger::kSpec.check(params, 2);
ASSERT_EQ(warnings.size(), 1U);
EXPECT_EQ(warnings[0].code, ::rpc::WarningCode::WarnRpcDeprecated);
}
TEST(SpecBridge, inject_spec_warnings)
{
std::string const kDeprecatedBase{
::rpc::getWarningInfo(::rpc::WarningCode::WarnRpcDeprecated).message};
{
json::Value out{json::ValueType::Object};
rpc::injectSpecWarnings(out, {});
EXPECT_FALSE(out.isMember(jss::warnings));
}
{
json::Value out{json::ValueType::Object};
rpc::injectSpecWarnings(
out,
{
{
.code = ::rpc::WarningCode::WarnRpcDeprecated,
.field = "type",
.message = "Field 'type' is deprecated.",
},
});
ASSERT_TRUE(out[jss::warnings].isArray());
ASSERT_EQ(out[jss::warnings].size(), 1U);
EXPECT_EQ(out[jss::warnings][0u][jss::id].asInt(), WarnRpcFieldsDeprecated);
EXPECT_EQ(
out[jss::warnings][0u][jss::message], kDeprecatedBase + " Field 'type' is deprecated.");
}
{
// One entry per code, both details appended.
json::Value out{json::ValueType::Object};
rpc::injectSpecWarnings(
out,
{
{
.code = ::rpc::WarningCode::WarnRpcDeprecated,
.field = "type",
.message = "Field 'type' is deprecated.",
},
{
.code = ::rpc::WarningCode::WarnRpcDeprecated,
.field = "ledger",
.message = "Field 'ledger' is deprecated.",
},
});
ASSERT_EQ(out[jss::warnings].size(), 1U);
EXPECT_EQ(
out[jss::warnings][0u][jss::message],
kDeprecatedBase + " Field 'type' is deprecated. Field 'ledger' is deprecated.");
}
{
// Ordered by code, so the response does not depend on field visit order.
json::Value out{json::ValueType::Object};
rpc::injectSpecWarnings(
out,
{
{
.code = ::rpc::WarningCode::WarnRpcDeprecated,
.field = "type",
.message = "Field 'type' is deprecated.",
},
{.code = ::rpc::WarningCode::WarnRpcOutdated, .field = "", .message = ""},
});
ASSERT_EQ(out[jss::warnings].size(), 2U);
EXPECT_EQ(
out[jss::warnings][0u][jss::id].asInt(),
static_cast<int>(::rpc::WarningCode::WarnRpcOutdated));
EXPECT_EQ(
out[jss::warnings][0u][jss::message],
std::string{::rpc::getWarningInfo(::rpc::WarningCode::WarnRpcOutdated).message});
EXPECT_EQ(out[jss::warnings][1u][jss::id].asInt(), WarnRpcFieldsDeprecated);
}
{
// A handler's own warnings are kept.
json::Value out{json::ValueType::Object};
json::Value existing{json::ValueType::Array};
json::Value& entry = existing.append(json::Value{json::ValueType::Object});
entry[jss::id] = WarnRpcUnsupportedMajority;
out[jss::warnings] = existing;
rpc::injectSpecWarnings(
out,
{
{
.code = ::rpc::WarningCode::WarnRpcDeprecated,
.field = "type",
.message = "Field 'type' is deprecated.",
},
});
ASSERT_EQ(out[jss::warnings].size(), 2U);
EXPECT_EQ(out[jss::warnings][0u][jss::id].asInt(), WarnRpcUnsupportedMajority);
EXPECT_EQ(out[jss::warnings][1u][jss::id].asInt(), WarnRpcFieldsDeprecated);
}
}
} // namespace xrpl
-288
View File
@@ -1,288 +0,0 @@
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_forwards.h>
#include <xrpl/json/json_value.h>
#include <rpcspec/Concepts.hpp>
#include <rpcspec/Types.hpp>
#include <cstddef>
#include <cstdint>
#include <limits> // IWYU pragma: keep
#include <string>
#include <string_view>
#include <type_traits>
namespace xrpl::rpc {
/**
* The json::Value backend for the spec DSL: a view of one resolved field.
*
* The DSL is written against the SomeFieldView / SomeObjectView concepts rather than any
* JSON type.
*
* A null pointer means the field is absent, which every predicate has to answer for. Which
* of the two pointers is set carries const-correctness: a view built from a const value
* cannot be written through, so a modifier cannot run during the check phase.
*/
class JsonFieldView
{
json::Value const* readValue_;
json::Value* writeValue_;
std::string_view key_;
public:
JsonFieldView(json::Value* value, std::string_view key) noexcept
: readValue_(value), writeValue_(value), key_(key)
{
}
JsonFieldView(json::Value const* value, std::string_view key) noexcept
: readValue_(value), writeValue_(nullptr), key_(key)
{
}
[[nodiscard]] static JsonFieldView
absent(std::string_view key) noexcept
{
return {static_cast<json::Value const*>(nullptr), key};
}
[[nodiscard]] std::string_view
key() const noexcept
{
return key_;
}
[[nodiscard]] bool
present() const noexcept
{
return readValue_ != nullptr;
}
// Not json::Value::isIntegral(), which is also true for a boolean: boost's is_int64()
// is not, and a spec must not accept `true` as a number on one server only.
[[nodiscard]] bool
isInt64() const noexcept
{
return readValue_ != nullptr && (readValue_->isInt() || readValue_->isUInt());
}
[[nodiscard]] std::int64_t
asInt64() const
{
return readValue_->isInt() ? static_cast<std::int64_t>(readValue_->asInt())
: static_cast<std::int64_t>(readValue_->asUInt());
}
// A question about the value, not the storage: a non-negative Int is a uint32.
// Both of json::Value's integer types are 32 bits, so that is the only check.
[[nodiscard]] bool
isUint32() const noexcept
{
if (readValue_ == nullptr)
return false;
if (readValue_->isUInt())
return true;
return readValue_->isInt() && readValue_->asInt() >= 0;
}
[[nodiscard]] std::uint32_t
asUint32() const
{
return readValue_->isUInt() ? readValue_->asUInt()
: static_cast<std::uint32_t>(readValue_->asInt());
}
[[nodiscard]] bool
isBool() const noexcept
{
return readValue_ != nullptr && readValue_->isBool();
}
[[nodiscard]] bool
asBool() const
{
return readValue_->asBool();
}
[[nodiscard]] bool
isString() const noexcept
{
return readValue_ != nullptr && readValue_->isString();
}
[[nodiscard]] std::string_view
asString() const
{
auto const* str = readValue_->asCString();
return str != nullptr ? std::string_view{str} : std::string_view{};
}
[[nodiscard]] bool
isDouble() const noexcept
{
return readValue_ != nullptr && readValue_->isDouble();
}
[[nodiscard]] double
asDouble() const
{
return readValue_->asDouble();
}
[[nodiscard]] bool
isObject() const noexcept
{
return readValue_ != nullptr && readValue_->isObject();
}
[[nodiscard]] bool
isArray() const noexcept
{
return readValue_ != nullptr && readValue_->isArray();
}
[[nodiscard]] std::size_t
arraySize() const noexcept
{
if (readValue_ == nullptr || !readValue_->isArray())
return 0;
return readValue_->size();
}
[[nodiscard]] std::size_t
objectSize() const noexcept
{
if (readValue_ == nullptr || !readValue_->isObject())
return 0;
return readValue_->size();
}
[[nodiscard]] JsonFieldView
child(std::string_view childKey) const
{
// Guarded rather than indexed: the non-const operator[] inserts a null member.
std::string const key{childKey};
if (writeValue_ != nullptr)
{
if (!writeValue_->isObject() || !writeValue_->isMember(key))
return absent(childKey);
return {&(*writeValue_)[key], childKey};
}
if (readValue_ == nullptr || !readValue_->isObject() || !readValue_->isMember(key))
return absent(childKey);
return {&(*readValue_)[key], childKey};
}
// Inherits this field's key, so an error about an element names the array.
[[nodiscard]] JsonFieldView
element(std::size_t idx) const
{
if (writeValue_ != nullptr)
{
if (!writeValue_->isArray() || idx >= writeValue_->size())
return absent(key_);
return {&(*writeValue_)[static_cast<json::UInt>(idx)], key_};
}
if (readValue_ == nullptr || !readValue_->isArray() || idx >= readValue_->size())
return absent(key_);
return {&(*readValue_)[static_cast<json::UInt>(idx)], key_};
}
template <typename T>
[[nodiscard]] bool
is() const noexcept
{
if constexpr (std::is_same_v<T, std::int64_t>)
{
return isInt64();
}
else if constexpr (std::is_same_v<T, std::uint32_t>)
{
return isUint32();
}
else if constexpr (std::is_same_v<T, bool>)
{
return isBool();
}
else if constexpr (std::is_same_v<T, std::string>)
{
return isString();
}
else if constexpr (std::is_same_v<T, double>)
{
return isDouble();
}
else if constexpr (std::is_same_v<T, ::rpc::spec::JsonObject>)
{
return isObject();
}
else if constexpr (std::is_same_v<T, ::rpc::spec::JsonArray>)
{
return isArray();
}
else
{
static_assert(false, "xrpl::rpc::JsonFieldView::is : unsupported type");
}
}
// Only reachable from a modifier, which only ever runs against a mutable view.
//
// json::Value has no 64-bit integer type, so the value goes into whichever of its two
// 32-bit types can hold it. Casting everything to json::Int would wrap anything above
// INT32_MAX, and the spec's toNumber modifier deliberately produces values up to
// UINT32_MAX (ledger_entry's oracle_document_id), which the boost backend stores fine.
void
set(std::int64_t value)
{
if (value >= 0)
{
XRPL_ASSERT(
value <= std::numeric_limits<json::UInt>::max(),
"xrpl::rpc::JsonFieldView::set : value representable as json::UInt");
*writeValue_ = static_cast<json::UInt>(value);
}
else
{
XRPL_ASSERT(
value >= std::numeric_limits<json::Int>::min(),
"xrpl::rpc::JsonFieldView::set : value representable as json::Int");
*writeValue_ = static_cast<json::Int>(value);
}
}
void
set(std::uint32_t value)
{
*writeValue_ = static_cast<json::UInt>(value);
}
void
set(std::string_view value)
{
*writeValue_ = std::string{value};
}
void
set(bool value)
{
*writeValue_ = value;
}
void
set(double value)
{
*writeValue_ = value;
}
};
static_assert(::rpc::spec::SomeFieldView<JsonFieldView>);
} // namespace xrpl::rpc
-83
View File
@@ -1,83 +0,0 @@
#pragma once
#include <xrpld/rpc/detail/JsonFieldView.hpp>
#include <xrpl/json/json_value.h>
#include <rpcspec/Concepts.hpp>
#include <string>
#include <string_view>
#include <utility>
namespace xrpl::rpc {
/**
* The request params object the spec DSL resolves fields from.
*
* A separate type from JsonFieldView, as in the boost backend, so that a root — which
* has no key of its own — cannot be passed where a field is expected.
*/
class JsonObjectView
{
json::Value const* readValue_;
json::Value* writeValue_;
public:
explicit JsonObjectView(json::Value& value) noexcept : readValue_(&value), writeValue_(&value)
{
}
explicit JsonObjectView(json::Value const& value) noexcept
: readValue_(&value), writeValue_(nullptr)
{
}
[[nodiscard]] bool
isObject() const noexcept
{
return readValue_->isObject();
}
[[nodiscard]] bool
isArray() const noexcept
{
return readValue_->isArray();
}
[[nodiscard]] JsonFieldView
child(std::string_view key)
{
if (writeValue_ == nullptr)
return std::as_const(*this).child(key);
std::string const name{key};
if (!writeValue_->isObject() || !writeValue_->isMember(name))
return JsonFieldView::absent(key);
return {&(*writeValue_)[name], key};
}
[[nodiscard]] JsonFieldView
child(std::string_view key) const
{
std::string const name{key};
if (!readValue_->isObject() || !readValue_->isMember(name))
return JsonFieldView::absent(key);
return {&(*readValue_)[name], key};
}
};
static_assert(::rpc::spec::SomeObjectView<JsonObjectView>);
} // namespace xrpl::rpc
namespace rpc::spec {
// Binds json::Value to xrpld's view, so a spec can be handed request params directly.
template <>
struct ObjectViewFor<::json::Value>
{
using Type = ::xrpl::rpc::JsonObjectView;
};
} // namespace rpc::spec
-51
View File
@@ -1,51 +0,0 @@
#pragma once
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/jss.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/Types.hpp>
#include <map>
#include <string>
#include <vector>
namespace xrpl::rpc {
// Warnings are grouped by code into one entry each: the code's standard message followed by
// every per-field detail, space separated.
//
// Entries already in the response are kept, so a handler may add its own.
inline void
injectSpecWarnings(json::Value& object, ::rpc::spec::Warnings const& warnings)
{
if (warnings.empty())
return;
// Ordered, so the response does not depend on the order fields were visited in.
std::map<::rpc::WarningCode, std::vector<std::string>> grouped;
for (auto const& warning : warnings)
grouped[warning.code].push_back(warning.message);
json::Value& array = object.isMember(jss::warnings)
? object[jss::warnings]
: (object[jss::warnings] = json::Value{json::ValueType::Array});
for (auto const& [code, messages] : grouped)
{
std::string message{::rpc::getWarningInfo(code).message};
for (auto const& detail : messages)
{
if (detail.empty())
continue;
message += ' ';
message += detail;
}
json::Value& entry = array.append(json::Value{json::ValueType::Object});
entry[jss::id] = static_cast<int>(code);
entry[jss::message] = message;
}
}
} // namespace xrpl::rpc
@@ -314,7 +314,6 @@ doAccountObjects(rpc::JsonContext& context)
{.name = jss::permissioned_domain, .type = ltPERMISSIONED_DOMAIN},
{.name = jss::vault, .type = ltVAULT},
{.name = jss::sponsorship, .type = ltSPONSORSHIP},
{.name = jss::transaction_proposal, .type = ltTRANSACTION_PROPOSAL},
};
typeFilter.emplace();
@@ -751,35 +751,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)
{
// In the non-object branch the caller must supply the proposal ID as a
// hex string (the object form is the {account, ticket_seq} pair handled
// below). Passing "hex string" here — rather than the more general
// "hex string or object" default — makes the error message name the
// exact form still on the table once an object has been ruled out.
if (!params.isObject())
return parseObjectID(params, fieldName, "hex string");
auto const targetID =
ledger_entry_helpers::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 =
ledger_entry_helpers::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,