mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-27 05:57:09 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e221267af |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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}"
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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
@@ -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")
|
||||
Generated
+10
-10
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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&
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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.");
|
||||
|
||||
@@ -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
|
||||
;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user