Compare commits

...

21 Commits

Author SHA1 Message Date
Michael Legleux
cb5354e4fe PR comments 2026-04-28 16:52:40 -07:00
Michael Legleux
19da83abf3 package: refactor packaging; make builds reproducible; stop requiring git 2026-04-28 16:22:22 -07:00
Michael Legleux
8a02c904a6 fix indentation 2026-04-28 16:22:22 -07:00
Michael Legleux
e7ca96362f rm rpm template; try to get version from binary 2026-04-28 16:22:21 -07:00
Michael Legleux
4593754347 f 2026-04-28 16:22:21 -07:00
Michael Legleux
2caa18ddd5 update matrix to build packages 2026-04-28 16:22:21 -07:00
Michael Legleux
8dbf9b8ab4 add a little more info to the result of the update check 2026-04-28 16:22:21 -07:00
Michael Legleux
94ef68090c Migrate updater from cron to systemd timer; use journald + flock
- systemd oneshot service + timer replaces cron job
- randomized scheduling and persistent timers
- journald logging with SyslogIdentifier
- flock-based concurrency control
- remove legacy update.log file
2026-04-28 16:22:21 -07:00
Michael Legleux
8fa81e24ed guard more 2026-04-28 16:22:21 -07:00
Michael Legleux
4e1c3b0922 Pin packaging image versions 2026-04-28 16:22:21 -07:00
Michael Legleux
f148d9a4e7 pr comments 2026-04-28 16:22:20 -07:00
Michael Legleux
453e3efcc9 PR comments and more tweaks 2026-04-28 16:22:20 -07:00
Michael Legleux
8b4ff1c171 pr comments 2026-04-28 16:22:20 -07:00
Michael Legleux
c33630f859 more tweaks 2026-04-28 16:22:20 -07:00
Michael Legleux
94103e97d7 cp lic/readme 2026-04-28 16:22:20 -07:00
Michael Legleux
cfcb09933a more fixes/updates 2026-04-28 16:22:20 -07:00
Michael Legleux
e36ba02762 update download-artifacts version 2026-04-28 16:22:19 -07:00
Michael Legleux
fe47e5adb2 handle tmpfiles in rpm 2026-04-28 16:22:19 -07:00
Michael Legleux
96a6922bce address review comments 2026-04-28 16:22:19 -07:00
Michael Legleux
7721bda445 feat: Build Linux packages in GitHub 2026-04-28 16:22:19 -07:00
Jingchen
46b997b774 feat: Create new transaction testing framework TxTest (#6537)
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 14:16:10 +00:00
44 changed files with 3449 additions and 32 deletions

View File

@@ -188,10 +188,16 @@ test.toplevel > xrpl.json
test.unit_test > xrpl.basics
test.unit_test > xrpl.protocol
tests.libxrpl > xrpl.basics
tests.libxrpl > xrpl.core
tests.libxrpl > xrpl.json
tests.libxrpl > xrpl.ledger
tests.libxrpl > xrpl.net
tests.libxrpl > xrpl.nodestore
tests.libxrpl > xrpl.protocol
tests.libxrpl > xrpl.protocol_autogen
tests.libxrpl > xrpl.server
tests.libxrpl > xrpl.shamap
tests.libxrpl > xrpl.tx
xrpl.conditions > xrpl.basics
xrpl.conditions > xrpl.protocol
xrpl.core > xrpl.basics

View File

@@ -4,14 +4,15 @@ import itertools
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
THIS_DIR = Path(__file__).parent.resolve()
@dataclass
class Config:
architecture: list[dict]
os: list[dict]
architecture: list[dict[str, Any]]
os: list[dict[str, Any]]
build_type: list[str]
cmake_args: list[str]
@@ -32,7 +33,52 @@ We will further set additional CMake arguments as follows:
"""
def generate_strategy_matrix(all: bool, config: Config) -> list:
def build_config_name(
os_entry: dict[str, Any], architecture: dict[str, Any], build_type: str
) -> str:
parts = [os_entry["distro_name"]]
for key in ("distro_version", "compiler_name", "compiler_version"):
if value := os_entry[key]:
parts.append(value)
platform = architecture["platform"]
parts.append(platform[platform.find("/") + 1 :])
parts.append(build_type.lower())
return "-".join(parts)
def build_container_image(os_entry: dict[str, Any]) -> str:
image = f"ghcr.io/xrplf/ci/{os_entry['distro_name']}-{os_entry['distro_version']}"
tag = f"{os_entry['compiler_name']}-{os_entry['compiler_version']}-sha-{os_entry['image_sha']}"
return f"{image}:{tag}"
def generate_packaging_matrix(config: Config) -> list[dict[str, str]]:
"""Emit packaging entries for each os entry with `package: true`.
Packaging always uses Release build on linux/amd64. The package format
(deb or rpm) is inferred at runtime from the container's package manager.
"""
architecture = next(
(a for a in config.architecture if a["platform"] == "linux/amd64"),
None,
)
if architecture is None:
raise Exception("linux/amd64 architecture required for packaging")
entries = []
for os_entry in config.os:
if not os_entry.get("package", False):
continue
config_name = build_config_name(os_entry, architecture, "Release")
entries.append(
{
"artifact_name": f"xrpld-{config_name}",
"container_image": build_container_image(os_entry),
}
)
return entries
def generate_strategy_matrix(all: bool, config: Config) -> list[dict[str, Any]]:
configurations = []
for architecture, os, build_type, cmake_args in itertools.product(
config.architecture, config.os, config.build_type, config.cmake_args
@@ -100,14 +146,15 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
continue
# RHEL:
# - 9 using GCC 12: Debug on linux/amd64.
# - 9 using GCC 12: Debug and Release on linux/amd64
# (Release is required for RPM packaging).
# - 10 using Clang: Release on linux/amd64.
if os["distro_name"] == "rhel":
skip = True
if os["distro_version"] == "9":
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
and build_type == "Debug"
and build_type in ["Debug", "Release"]
and architecture["platform"] == "linux/amd64"
):
skip = False
@@ -122,7 +169,8 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
continue
# Ubuntu:
# - Jammy using GCC 12: Debug on linux/arm64.
# - Jammy using GCC 12: Debug on linux/arm64, Release on
# linux/amd64 (Release is required for DEB packaging).
# - Noble using GCC 14: Release on linux/amd64.
# - Noble using Clang 18: Debug on linux/amd64.
# - Noble using Clang 19: Release on linux/arm64.
@@ -135,6 +183,12 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
and architecture["platform"] == "linux/arm64"
):
skip = False
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
and build_type == "Release"
and architecture["platform"] == "linux/amd64"
):
skip = False
elif os["distro_version"] == "noble":
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-14"
@@ -216,17 +270,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
# Generate a unique name for the configuration, e.g. macos-arm64-debug
# or debian-bookworm-gcc-12-amd64-release.
config_name = os["distro_name"]
if (n := os["distro_version"]) != "":
config_name += f"-{n}"
if (n := os["compiler_name"]) != "":
config_name += f"-{n}"
if (n := os["compiler_version"]) != "":
config_name += f"-{n}"
config_name += (
f"-{architecture['platform'][architecture['platform'].find('/')+1:]}"
)
config_name += f"-{build_type.lower()}"
config_name = build_config_name(os, architecture, build_type)
if "-Dcoverage=ON" in cmake_args:
config_name += "-coverage"
if "-Dunity=ON" in cmake_args:
@@ -330,10 +374,19 @@ if __name__ == "__main__":
required=False,
type=Path,
)
parser.add_argument(
"-p",
"--packaging",
help="Emit the packaging matrix (derived from the 'package' field on os entries) instead of the build/test matrix.",
action="store_true",
)
args = parser.parse_args()
matrix = []
if args.config is None or args.config == "":
if args.packaging:
config_path = args.config if args.config else THIS_DIR / "linux.json"
matrix += generate_packaging_matrix(read_config(config_path))
elif args.config is None or args.config == "":
matrix += generate_strategy_matrix(
args.all, read_config(THIS_DIR / "linux.json")
)

View File

@@ -120,7 +120,8 @@
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "ab4d1f0"
"image_sha": "ab4d1f0",
"package": true
},
{
"distro_name": "rhel",
@@ -162,7 +163,8 @@
"distro_version": "jammy",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "ab4d1f0"
"image_sha": "ab4d1f0",
"package": true
},
{
"distro_name": "ubuntu",

View File

@@ -67,6 +67,7 @@ jobs:
.github/workflows/reusable-build-test.yml
.github/workflows/reusable-clang-tidy.yml
.github/workflows/reusable-clang-tidy-files.yml
.github/workflows/reusable-package.yml
.github/workflows/reusable-strategy-matrix.yml
.github/workflows/reusable-test.yml
.github/workflows/reusable-upload-recipe.yml
@@ -81,6 +82,8 @@ jobs:
CMakeLists.txt
conanfile.py
conan.lock
package/**
- name: Check whether to run
# This step determines whether the rest of the workflow should
# run. The rest of the workflow will run if this job runs AND at
@@ -137,6 +140,30 @@ jobs:
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
generate-version:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
package:
needs: [should-run, build-test, generate-version]
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-package.yml
with:
version: ${{ needs.generate-version.outputs.version }}
upload-recipe:
needs:
- should-run

View File

@@ -1,5 +1,5 @@
# This workflow uploads the libxrpl recipe to the Conan remote when a versioned
# tag is pushed.
# This workflow uploads the libxrpl recipe to the Conan remote and builds
# release packages when a versioned tag is pushed.
name: Tag
on:
@@ -22,3 +22,39 @@ jobs:
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
build-test:
if: ${{ github.repository == 'XRPLF/rippled' }}
uses: ./.github/workflows/reusable-build-test.yml
strategy:
fail-fast: true
matrix:
os: [linux]
with:
ccache_enabled: false
os: ${{ matrix.os }}
strategy_matrix: minimal
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
generate-version:
if: ${{ github.repository == 'XRPLF/rippled' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
package:
needs: [build-test, generate-version]
uses: ./.github/workflows/reusable-package.yml
with:
version: ${{ needs.generate-version.outputs.version }}

View File

@@ -24,6 +24,7 @@ on:
- ".github/workflows/reusable-build-test.yml"
- ".github/workflows/reusable-clang-tidy.yml"
- ".github/workflows/reusable-clang-tidy-files.yml"
- ".github/workflows/reusable-package.yml"
- ".github/workflows/reusable-strategy-matrix.yml"
- ".github/workflows/reusable-test.yml"
- ".github/workflows/reusable-upload-recipe.yml"
@@ -38,6 +39,7 @@ on:
- "CMakeLists.txt"
- "conanfile.py"
- "conan.lock"
- "package/**"
# Run at 06:32 UTC on every day of the week from Monday through Friday. This
# will force all dependencies to be rebuilt, which is useful to verify that
@@ -98,3 +100,24 @@ jobs:
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
generate-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
package:
needs: [build-test, generate-version]
uses: ./.github/workflows/reusable-package.yml
with:
version: ${{ needs.generate-version.outputs.version }}

91
.github/workflows/reusable-package.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
# Build Linux packages (DEB and RPM) from pre-built binary artifacts.
# Discovers which configurations to package from linux.json (entries with
# "package": true) and fans out one job per entry.
name: Package
on:
workflow_call:
inputs:
pkg_release:
description: "Package release number. Increment when repackaging the same executable."
required: false
type: string
default: "1"
version:
description: "Version string used for naming the output artifact."
required: true
type: string
defaults:
run:
shell: bash
env:
BUILD_DIR: build
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
- name: Generate packaging matrix
id: generate
working-directory: .github/scripts/strategy-matrix
run: |
./generate.py --packaging --config=linux.json >> "${GITHUB_OUTPUT}"
package:
needs: generate-matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
name: "${{ matrix.artifact_name }}"
permissions:
contents: read
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: ${{ matrix.container_image }}
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set source date epoch
run: |
echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV"
- name: Download pre-built binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.artifact_name }}
path: ${{ env.BUILD_DIR }}
- name: Make binary executable
run: chmod +x "${BUILD_DIR}/xrpld"
- name: Build package
env:
PKG_VERSION: ${{ inputs.version }}
PKG_RELEASE: ${{ inputs.pkg_release }}
SOURCE_DATE_EPOCH: ${{ env.SOURCE_DATE_EPOCH }}
run: |
./package/build_pkg.sh . "$BUILD_DIR" "$PKG_VERSION" "$PKG_RELEASE"
- name: Upload package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ matrix.artifact_name }}-pkg-${{ inputs.version }}
path: |
${{ env.BUILD_DIR }}/debbuild/*.deb
${{ env.BUILD_DIR }}/debbuild/*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/*.rpm
if-no-files-found: error

View File

@@ -39,7 +39,8 @@ jobs:
- name: Generate strategy matrix
working-directory: .github/scripts/strategy-matrix
id: generate
env:
GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }}
GENERATE_OPTION: ${{ inputs.strategy_matrix == 'all' && '--all' || '' }}
run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >> "${GITHUB_OUTPUT}"
run: |
./generate.py \
${{ inputs.strategy_matrix == 'all' && '--all' || '' }} \
${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }} \
>> "${GITHUB_OUTPUT}"

View File

@@ -134,6 +134,7 @@ endif()
include(XrplCore)
include(XrplProtocolAutogen)
include(XrplInstall)
include(XrplPackaging)
include(XrplValidatorKeys)
if(tests)

View File

@@ -12,14 +12,14 @@ if(is_root_project AND TARGET xrpld)
install(
FILES "${CMAKE_CURRENT_SOURCE_DIR}/cfg/xrpld-example.cfg"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/xrpld"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}"
RENAME xrpld.cfg
COMPONENT runtime
)
install(
FILES "${CMAKE_CURRENT_SOURCE_DIR}/cfg/validators-example.txt"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/xrpld"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}"
RENAME validators.txt
COMPONENT runtime
)

136
cmake/XrplPackaging.cmake Normal file
View File

@@ -0,0 +1,136 @@
#[===================================================================[
Linux packaging support: RPM and Debian targets + install tests
#]===================================================================]
if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/opt/xrpld")
message(
STATUS
"Packaging targets require -DCMAKE_INSTALL_PREFIX=/opt/xrpld "
"(current: '${CMAKE_INSTALL_PREFIX}'); skipping."
)
return()
endif()
if(NOT DEFINED pkg_release)
set(pkg_release 1)
endif()
find_program(RPMBUILD_EXECUTABLE rpmbuild)
find_program(DPKG_BUILDPACKAGE_EXECUTABLE dpkg-buildpackage)
if(RPMBUILD_EXECUTABLE OR DPKG_BUILDPACKAGE_EXECUTABLE)
add_custom_target(
package
COMMAND
${CMAKE_SOURCE_DIR}/package/build_pkg.sh ${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR} "${xrpld_version}" ${pkg_release}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
VERBATIM
)
else()
message(
STATUS
"Neither rpmbuild nor dpkg-buildpackage found; 'package' target not available"
)
endif()
#[===================================================================[
CTest fixtures for package install verification (requires docker)
#]===================================================================]
find_program(DOCKER_EXECUTABLE docker)
if(NOT DOCKER_EXECUTABLE)
message(STATUS "docker not found; package install tests not available")
return()
endif()
set(DEB_TEST_IMAGE
"geerlingguy/docker-ubuntu2204-ansible@sha256:bbe4c56c16c57c902554b9a47833590926b7a7d4440aef3d9851473b9f7be9d4"
)
set(RPM_TEST_IMAGE
"geerlingguy/docker-rockylinux9-ansible@sha256:790c2db9add93c0daa903ace816f352c9c04abb046ecfa12c581e8d4c59f41d6"
)
# Only register install-test fixtures for package formats the host can build,
# since the smoketest needs a corresponding .deb/.rpm artifact in build/.
set(PKG_TYPES "")
if(DPKG_BUILDPACKAGE_EXECUTABLE)
list(APPEND PKG_TYPES deb)
endif()
if(RPMBUILD_EXECUTABLE)
list(APPEND PKG_TYPES rpm)
endif()
foreach(PKG IN LISTS PKG_TYPES)
if(PKG STREQUAL "deb")
set(IMAGE ${DEB_TEST_IMAGE})
else()
set(IMAGE ${RPM_TEST_IMAGE})
endif()
# This image runs systemd for full testing xrpld.service
add_test(
NAME ${PKG}_container_start
COMMAND
sh -c
"docker rm -f xrpld_${PKG}_install_test 2>/dev/null || true && \
docker run -d \
--name xrpld_${PKG}_install_test \
--cgroupns host \
--volume '${CMAKE_SOURCE_DIR}:/root:ro' \
--volume /sys/fs/cgroup:/sys/fs/cgroup:rw \
--tmpfs /run/lock \
${IMAGE}"
)
set_tests_properties(
${PKG}_container_start
PROPERTIES FIXTURES_SETUP ${PKG}_container LABELS packaging
)
# On CI: always stop. Locally: leave running on failure for diagnosis.
add_test(
NAME ${PKG}_container_stop
COMMAND
sh -c
"if [ -n \"$CI\" ] || ! docker exec xrpld_${PKG}_install_test test -f /tmp/test_failed 2>/dev/null; then \
docker rm -f xrpld_${PKG}_install_test; \
else \
echo 'Tests failed — leaving xrpld_${PKG}_install_test running for diagnosis'; \
echo 'Clean up with: docker rm -f xrpld_${PKG}_install_test'; \
fi"
)
set_tests_properties(
${PKG}_container_stop
PROPERTIES FIXTURES_CLEANUP ${PKG}_container LABELS packaging
)
add_test(
NAME ${PKG}_install
COMMAND
docker exec -w /root xrpld_${PKG}_install_test bash
/root/package/test/smoketest.sh local
)
set_tests_properties(
${PKG}_install
PROPERTIES
FIXTURES_REQUIRED ${PKG}_container
FIXTURES_SETUP ${PKG}_installed
LABELS packaging
TIMEOUT 600
)
add_test(
NAME ${PKG}_install_paths
COMMAND
docker exec -w /root xrpld_${PKG}_install_test sh
/root/package/test/check_install_paths.sh
)
set_tests_properties(
${PKG}_install_paths
PROPERTIES
FIXTURES_REQUIRED "${PKG}_container;${PKG}_installed"
LABELS packaging
TIMEOUT 60
)
endforeach()

View File

@@ -97,12 +97,15 @@ words:
- desync
- desynced
- determ
- disablerepo
- distro
- doxyfile
- dxrpl
- enabled
- enablerepo
- endmacro
- exceptioned
- EXPECT_STREQ
- Falco
- fcontext
- finalizers
@@ -158,6 +161,7 @@ words:
- Merkle
- Metafuncton
- misprediction
- missingok
- mptbalance
- MPTDEX
- mptflags
@@ -189,7 +193,9 @@ words:
- NOLINT
- NOLINTNEXTLINE
- nonxrp
- noreplace
- noripple
- notifempty
- nudb
- nullptr
- nunl
@@ -209,6 +215,7 @@ words:
- preauthorize
- preauthorizes
- preclaim
- preun
- protobuf
- protos
- ptrs
@@ -243,12 +250,14 @@ words:
- sfields
- shamap
- shamapitem
- shlibs
- sidechain
- SIGGOOD
- sle
- sles
- soci
- socidb
- SRPMS
- sslws
- statsd
- STATSDCOLLECTOR
@@ -276,8 +285,8 @@ words:
- txn
- txns
- txs
- UBSAN
- ubsan
- UBSAN
- umant
- unacquired
- unambiguity
@@ -314,7 +323,6 @@ words:
- xbridge
- xchain
- ximinez
- EXPECT_STREQ
- XMACRO
- xrpkuwait
- xrpl

View File

@@ -130,6 +130,19 @@ public:
return sle_->at(sfFlags);
}
/**
* @brief Check if a specific flag is set.
*
* @param f The flag bitmask to check
* @return true if all bits in f are set in the flags field
*/
[[nodiscard]]
bool
isFlag(std::uint32_t f) const
{
return sle_->isFlag(f);
}
/**
* @brief Get the underlying SLE object.
*

160
package/README.md Normal file
View File

@@ -0,0 +1,160 @@
# Linux Packaging
This directory contains all files needed to build RPM and Debian packages for `xrpld`.
## Directory layout
```
package/
build_pkg.sh Staging and build script (called by CMake targets and CI)
rpm/
xrpld.spec RPM spec (xrpld_version/pkg_release passed via rpmbuild --define)
debian/ Debian control files (control, rules, install, links, conffiles, ...)
shared/
xrpld.service systemd unit file (used by both RPM and DEB)
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
xrpld.tmpfiles tmpfiles.d config (used by both RPM and DEB)
xrpld.logrotate logrotate config (installed to /opt/xrpld/bin/, user activates)
update-xrpld.sh auto-update script (installed to /opt/xrpld/bin/)
update-xrpld-cron cron entry for auto-update (installed to /opt/xrpld/bin/)
test/
smoketest.sh Package install smoke test
check_install_paths.sh Verify install paths and compat symlinks
```
## Prerequisites
Packaging targets and their container images are declared in
[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json)
via a `"package": true` field on specific os entries. The package format
(deb or rpm) is inferred at build time from the container's package manager
(`apt-get` -> deb, `dnf`/`yum` -> rpm). The image tag is composed as
`ghcr.io/xrplf/ci/{distro}-{version}:{compiler}-{cver}-sha-{image_sha}`
the same scheme used by `reusable-build-test.yml`. Bump `image_sha` in
`linux.json` and both CI and local builds pick up the new image with no
workflow edits.
| Package type | Image (derived from `linux.json`) | Tool required |
| ------------ | ---------------------------------------------------- | --------------------------------------------------------------- |
| RPM | `ghcr.io/xrplf/ci/rhel-9:gcc-12-sha-<git_sha>` | `rpmbuild` |
| DEB | `ghcr.io/xrplf/ci/ubuntu-jammy:gcc-12-sha-<git_sha>` | `dpkg-buildpackage`, `debhelper (>= 13)`, `dh-sequence-systemd` |
To print the exact image tags for the current `linux.json`:
```bash
./.github/scripts/strategy-matrix/generate.py --packaging --config=.github/scripts/strategy-matrix/linux.json
```
## Building packages
### Via CI
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
`reusable-strategy-matrix.yml` with `mode: packaging` to generate the matrix of
`{artifact_name, container_image}` entries, then fan out to
`reusable-package.yml` per entry. That workflow downloads the pre-built `xrpld`
binary artifact, detects the package format from the container, and calls
`build_pkg.sh` directly — no CMake configure or build step is needed inside
the packaging job.
### Locally (mirrors CI)
With an `xrpld` binary already built at `build/xrpld`, run the packaging step
inside the same container CI uses. The image tag is derived from `linux.json`
so you don't need to hardcode a SHA.
```bash
# From the repo root. Pick any image flagged with `"package": true` in
# linux.json; the package format is inferred from the container's package
# manager. Example for the rpm-producing image:
IMAGE=$(jq -r '
.os | map(select(.package == true))[0] |
"ghcr.io/xrplf/ci/\(.distro_name)-\(.distro_version):\(.compiler_name)-\(.compiler_version)-sha-\(.image_sha)"
' .github/scripts/strategy-matrix/linux.json)
VERSION=2.4.0-local
PKG_RELEASE=1
docker run --rm \
-v "$(pwd):/src" \
-w /src \
"$IMAGE" \
./package/build_pkg.sh . build "$VERSION" "$PKG_RELEASE"
# Output:
# build/debbuild/*.deb (DEB + dbgsym .ddeb)
# build/rpmbuild/RPMS/x86_64/*.rpm
```
### Via CMake (host-side target)
If you run CMake configure on a host that has `rpmbuild` or `dpkg-buildpackage`
installed natively, you can use the CMake target directly — no container
needed, but the host toolchain replaces the pinned CI image:
```bash
cmake \
-DCMAKE_INSTALL_PREFIX=/opt/xrpld \
-Dxrpld=ON \
-Dxrpld_version=2.4.0-local \
-Dtests=OFF \
..
cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL
```
The `cmake/XrplPackaging.cmake` module defines the target only if at least one
of `rpmbuild` / `dpkg-buildpackage` is present; `build_pkg.sh` then infers the
package format from the host's package manager. `CMAKE_INSTALL_PREFIX` must
be `/opt/xrpld`; if it is not, both targets are skipped with a `STATUS`
message.
## How `build_pkg.sh` works
`build_pkg.sh <src_dir> <build_dir> [version] [pkg_release]` stages
all files and invokes the platform build tool. It resolves `src_dir` and
`build_dir` to absolute paths, then calls `stage_common()` to copy the binary,
config files, and shared support files into the staging area. The package
format is taken from the `PKG_TYPE` env var if set; otherwise it is inferred
from the available package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm).
### RPM
1. Creates the standard `rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}` tree inside the build directory.
2. Copies `xrpld.spec` and all source files (binary, configs, service files) into `SOURCES/`.
3. Runs `rpmbuild -bb --define "xrpld_version ..." --define "pkg_release ..."`. The spec uses manual `install` commands to place files.
4. Output: `rpmbuild/RPMS/x86_64/xrpld-*.rpm`
### DEB
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
2. Stages the binary, configs, `README.md`, and `LICENSE.md`.
3. Copies `package/deb/debian/` control files into `debbuild/source/debian/`.
4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically.
5. Generates a minimal `debian/changelog` (pre-release versions use `~` instead of `-`).
6. Runs `dpkg-buildpackage -b --no-sign`. `debian/rules` uses manual `install` commands.
7. Output: `debbuild/*.deb` and `debbuild/*.ddeb` (dbgsym package)
## Post-build verification
```bash
# DEB
dpkg-deb -c debbuild/*.deb | grep -E 'systemd|sysusers|tmpfiles'
lintian -I debbuild/*.deb
# RPM
rpm -qlp rpmbuild/RPMS/x86_64/*.rpm
```
## Reproducibility
The following environment variables improve build reproducibility. They are not
set automatically by `build_pkg.sh`; set them manually if needed:
```bash
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
export TZ=UTC
export LC_ALL=C.UTF-8
export GZIP=-n
export DEB_BUILD_OPTIONS="noautodbgsym reproducible=+fixfilepath"
```

134
package/build_pkg.sh Executable file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env bash
set -euo pipefail
# Build an RPM or Debian package from a pre-built xrpld binary.
#
# Usage: build_pkg.sh <src_dir> <build_dir> [version] [pkg_release]
# src_dir : path to repository root
# build_dir : directory containing the pre-built xrpld binary
# version : package version string (e.g. 3.2.0-b1)
# pkg_release : package release number (default: 1)
#
# The package format is taken from the PKG_TYPE env var if set; otherwise it
# is inferred from the available package manager (apt-get -> deb, dnf/yum -> rpm).
SRC_DIR="$(cd "${1:?src_dir required}" && pwd)"
BUILD_DIR="$(cd "${2:?build_dir required}" && pwd)"
VERSION="${3:-$("${BUILD_DIR}/xrpld" --version | awk 'NR==1 {print $3}')}"
PKG_RELEASE="${4:-1}"
if [[ -z "${PKG_TYPE:-}" ]]; then
if command -v apt-get >/dev/null 2>&1; then
PKG_TYPE=deb
elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then
PKG_TYPE=rpm
else
echo "Cannot infer PKG_TYPE: no apt-get, dnf, or yum on PATH." >&2
exit 1
fi
fi
if [[ -z "${SOURCE_DATE_EPOCH:-}" ]]; then
if git -C "$SRC_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
SOURCE_DATE_EPOCH="$(git -C "$SRC_DIR" log -1 --format=%ct)"
else
SOURCE_DATE_EPOCH="$(date +%s)"
fi
fi
export SOURCE_DATE_EPOCH
CHANGELOG_DATE="$(date -u -R -d "@$SOURCE_DATE_EPOCH")"
# Split VERSION at the first '-' into base and optional pre-release suffix.
# Examples: "3.2.0" -> ("3.2.0", ""); "3.2.0-b1" -> ("3.2.0", "b1").
VER_BASE="${VERSION%%-*}"
VER_SUFFIX="${VERSION#*-}"
[[ "${VER_SUFFIX}" == "${VERSION}" ]] && VER_SUFFIX=""
SHARED="${SRC_DIR}/package/shared"
DEBIAN_DIR="${SRC_DIR}/package/debian"
# Stage files that both packaging systems consume using the same filenames.
stage_common() {
local dest="$1"
mkdir -p "${dest}"
cp "${BUILD_DIR}/xrpld" "${dest}/xrpld"
cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg"
cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt"
cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md"
cp "${SRC_DIR}/README.md" "${dest}/README.md"
cp "${SHARED}/xrpld.service" "${dest}/xrpld.service"
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
cp "${SHARED}/update-xrpld.sh" "${dest}/update-xrpld.sh"
cp "${SHARED}/update-xrpld-cron" "${dest}/update-xrpld-cron"
cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service"
cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer"
cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset"
}
build_rpm() {
local topdir="${BUILD_DIR}/rpmbuild"
rm -rf "${topdir}"
mkdir -p "${topdir}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec"
stage_common "${topdir}/SOURCES"
# RPM Version can't contain '-'. A pre-release goes in Release with a
# leading "0." so 3.2.0-b1 sorts before the final 3.2.0-<pkg_release>.
local rpm_release="${PKG_RELEASE}"
[[ -n "${VER_SUFFIX}" ]] && rpm_release="0.${VER_SUFFIX}.${PKG_RELEASE}"
set -x
rpmbuild -bb \
--define "_topdir ${topdir}" \
--define "xrpld_version ${VER_BASE}" \
--define "xrpld_release ${rpm_release}" \
"${topdir}/SPECS/xrpld.spec"
}
build_deb() {
local staging="${BUILD_DIR}/debbuild/source"
rm -rf "${staging}"
mkdir -p "${staging}"
stage_common "${staging}"
cp -r "${DEBIAN_DIR}" "${staging}/debian"
# Debhelper auto-discovers these only from debian/.
cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service"
cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers"
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
cp "${staging}/update-xrpld.service" "${staging}/debian/xrpld.update-xrpld.service"
cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer"
# Debian '~' marks a pre-release; 3.2.0~b1 sorts before 3.2.0.
local deb_full_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}-${PKG_RELEASE}"
# Derive release channel from the version suffix:
# (none) -> stable (tagged release)
# b0 -> develop (develop-branch build)
# b<N>, rc<N> -> unstable (pre-release)
local deb_distribution
case "${VER_SUFFIX}" in
"") deb_distribution="stable" ;;
b0) deb_distribution="develop" ;;
*) deb_distribution="unstable" ;;
esac
cat > "${staging}/debian/changelog" <<EOF
xrpld (${deb_full_version}) ${deb_distribution}; urgency=medium
* Release ${VERSION}.
-- XRPL Foundation <contact@xrplf.org> ${CHANGELOG_DATE}
EOF
chmod +x "${staging}/debian/rules"
set -x
( cd "${staging}" && dpkg-buildpackage -b --no-sign -d )
}
"build_${PKG_TYPE}"

23
package/debian/control Normal file
View File

@@ -0,0 +1,23 @@
Source: xrpld
Section: net
Priority: optional
Maintainer: XRPL Foundation <contact@xrplf.org>
Rules-Requires-Root: no
Build-Depends:
debhelper-compat (= 13)
Standards-Version: 4.7.0
Homepage: https://github.com/XRPLF/rippled
Vcs-Git: https://github.com/XRPLF/rippled.git
Vcs-Browser: https://github.com/XRPLF/rippled
Package: xrpld
Section: net
Priority: optional
Architecture: any
Depends:
${shlibs:Depends},
${misc:Depends}
Description: XRP Ledger daemon
Reference implementation of the XRP Ledger protocol. Participates
in the peer-to-peer network, processes transactions, and maintains
a local ledger copy.

20
package/debian/copyright Normal file
View File

@@ -0,0 +1,20 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: rippled
Source: https://github.com/XRPLF/rippled
Files: *
Copyright: 2012-2026 Ripple Labs Inc.
License: ISC
License: ISC
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

34
package/debian/rules Normal file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
export DH_OPTIONS = -v
%:
dh $@
override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
@:
override_dh_auto_install:
install -Dm0755 xrpld debian/tmp/opt/xrpld/bin/xrpld
install -Dm0644 xrpld.cfg debian/tmp/opt/xrpld/etc/xrpld.cfg
install -Dm0644 validators.txt debian/tmp/opt/xrpld/etc/validators.txt
install -Dm0644 xrpld.logrotate debian/tmp/opt/xrpld/bin/xrpld.logrotate
install -Dm0755 update-xrpld.sh debian/tmp/opt/xrpld/bin/update-xrpld.sh
install -Dm0644 update-xrpld-cron debian/tmp/opt/xrpld/bin/update-xrpld-cron
install -Dm0644 README.md debian/tmp/usr/share/doc/xrpld/README.md
install -Dm0644 LICENSE.md debian/tmp/usr/share/doc/xrpld/LICENSE.md
# update-xrpld.service is a Type=oneshot fired by update-xrpld.timer; installing
# it without --no-start would run the update on package install.
override_dh_installsystemd:
dh_installsystemd xrpld.service
dh_installsystemd --name=update-xrpld --no-enable --no-start update-xrpld.service update-xrpld.timer
execute_before_dh_installtmpfiles:
dh_installsysusers
override_dh_installsysusers:
override_dh_dwz:
@:

View File

@@ -0,0 +1 @@
3.0 (quilt)

View File

@@ -0,0 +1,2 @@
/opt/xrpld/etc/xrpld.cfg
/opt/xrpld/etc/validators.txt

View File

@@ -0,0 +1,10 @@
opt/xrpld/bin/xrpld
opt/xrpld/bin/xrpld.logrotate
opt/xrpld/bin/update-xrpld.sh
opt/xrpld/bin/update-xrpld-cron
opt/xrpld/etc/xrpld.cfg
opt/xrpld/etc/validators.txt
usr/share/doc/xrpld/README.md
usr/share/doc/xrpld/LICENSE.md

View File

@@ -0,0 +1,10 @@
opt/xrpld/etc etc/opt/xrpld
opt/xrpld/bin/xrpld usr/bin/xrpld
# remove when "rippled" deprecated
opt/xrpld/bin/xrpld opt/xrpld/bin/rippled
opt/xrpld/bin/xrpld usr/local/bin/rippled
opt/xrpld/etc/xrpld.cfg opt/xrpld/etc/rippled.cfg
opt/xrpld opt/ripple
etc/opt/xrpld etc/opt/ripple

111
package/rpm/xrpld.spec Normal file
View File

@@ -0,0 +1,111 @@
Name: xrpld
Version: %{xrpld_version}
Release: %{xrpld_release}%{?dist}
Summary: XRP Ledger daemon
License: ISC
URL: https://github.com/XRPLF/rippled
ExclusiveArch: x86_64 aarch64
BuildRequires: systemd-rpm-macros
%undefine _debugsource_packages
%debug_package
%build_mtime_policy clamp_to_source_date_epoch
%{?systemd_requires}
%{?sysusers_requires_compat}
%description
xrpld is the reference implementation of the XRP Ledger protocol. It
participates in the peer-to-peer XRP Ledger network, processes
transactions, and maintains the ledger database.
%prep
:
%install
rm -rf %{buildroot}
SRC=%{_sourcedir}
install -Dm0755 ${SRC}/xrpld %{buildroot}/opt/xrpld/bin/xrpld
install -Dm0644 ${SRC}/xrpld.cfg %{buildroot}/opt/xrpld/etc/xrpld.cfg
install -Dm0644 ${SRC}/validators.txt %{buildroot}/opt/xrpld/etc/validators.txt
mkdir -p %{buildroot}/etc/opt %{buildroot}/usr/bin %{buildroot}/usr/local/bin
ln -s /opt/xrpld/etc %{buildroot}/etc/opt/xrpld
ln -s /opt/xrpld/bin/xrpld %{buildroot}/usr/bin/xrpld
# TODO: remove when rippled deprecated
ln -s xrpld %{buildroot}/opt/xrpld/bin/rippled
ln -s /opt/xrpld/bin/xrpld %{buildroot}/usr/local/bin/rippled
ln -s xrpld.cfg %{buildroot}/opt/xrpld/etc/rippled.cfg
ln -s /opt/xrpld %{buildroot}/opt/ripple
ln -s /etc/opt/xrpld %{buildroot}/etc/opt/ripple
install -Dm0644 ${SRC}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
install -Dm0644 ${SRC}/update-xrpld.service %{buildroot}%{_unitdir}/update-xrpld.service
install -Dm0644 ${SRC}/update-xrpld.timer %{buildroot}%{_unitdir}/update-xrpld.timer
install -Dm0644 ${SRC}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
install -Dm0644 ${SRC}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
install -Dm0644 ${SRC}/50-xrpld.preset %{buildroot}%{_presetdir}/50-xrpld.preset
install -Dm0755 ${SRC}/update-xrpld.sh %{buildroot}/opt/xrpld/bin/update-xrpld.sh
install -Dm0644 ${SRC}/update-xrpld-cron %{buildroot}/opt/xrpld/bin/update-xrpld-cron
install -Dm0644 ${SRC}/xrpld.logrotate %{buildroot}/opt/xrpld/bin/xrpld.logrotate
install -Dm0644 ${SRC}/LICENSE.md %{buildroot}/opt/xrpld/share/LICENSE.md
install -Dm0644 ${SRC}/README.md %{buildroot}/opt/xrpld/share/README.md
%pre
%sysusers_create_package xrpld %{_sourcedir}/xrpld.sysusers
%post
systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%systemd_post xrpld.service update-xrpld.timer
%preun
%systemd_preun xrpld.service update-xrpld.timer
%postun
%systemd_postun_with_restart xrpld.service
%files
%license /opt/xrpld/share/LICENSE.md
%doc /opt/xrpld/share/README.md
%dir /opt/xrpld
%dir /opt/xrpld/bin
%dir /opt/xrpld/etc
/opt/xrpld/bin/xrpld
/opt/xrpld/bin/xrpld.logrotate
/opt/xrpld/bin/update-xrpld.sh
/opt/xrpld/bin/update-xrpld-cron
/usr/bin/xrpld
/etc/opt/xrpld
%config(noreplace) /opt/xrpld/etc/xrpld.cfg
%config(noreplace) /opt/xrpld/etc/validators.txt
%{_unitdir}/xrpld.service
%{_unitdir}/update-xrpld.service
%{_unitdir}/update-xrpld.timer
%{_presetdir}/50-xrpld.preset
%{_sysusersdir}/xrpld.conf
%{_tmpfilesdir}/xrpld.conf
%ghost %dir /var/lib/xrpld
%ghost %dir /var/log/xrpld
# TODO: remove when rippled deprecated
/opt/xrpld/bin/rippled
/usr/local/bin/rippled
/opt/xrpld/etc/rippled.cfg
/etc/opt/ripple
/opt/ripple

View File

@@ -0,0 +1,4 @@
# /usr/lib/systemd/system-preset/50-xrpld.preset
enable xrpld.service
# Don't enable automatic updates
disable update-xrpld.timer

View File

@@ -0,0 +1,9 @@
# For automatic updates, symlink this file to /etc/cron.d/
# Do not remove the newline at the end of this cron script
# bash required for use of RANDOM below.
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
# invoke check/update script with random delay up to 59 mins
0 * * * * root sleep $((RANDOM*3540/32768)) && /opt/xrpld/bin/update-xrpld.sh

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Check for and install xrpld package updates
Documentation=man:systemd.service(5)
Wants=network-online.target
After=network-online.target
ConditionPathExists=/opt/xrpld/bin/update-xrpld.sh
ConditionPathExists=/opt/xrpld/bin/xrpld
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/lock/xrpld-update.lock /opt/xrpld/bin/update-xrpld.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=update-xrpld
TimeoutStartSec=30min
PrivateTmp=true

145
package/shared/update-xrpld.sh Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -euo pipefail
# Optional: also write logs to a legacy file in addition to journald.
# By default, this script logs to systemd/journald, viewable via:
# journalctl -t update-xrpld
#
# Uncomment the line below if you need a flat file for compatibility with
# external tooling, manual inspection, or environments where journald logs
# are not persisted or easily accessible.
#
# Note: This duplicates all output (stdout/stderr) to both journald and the file.
# It is generally not needed on modern systems and may cause log file growth
# if left enabled long-term.
#
# Requires /var/log/xrpld/ to exist and be writable by the service (root).
#
# exec > >(tee -a /var/log/xrpld/update.log) 2>&1
PATH=/usr/sbin:/usr/bin:/sbin:/bin
PKG_NAME=${PKG_NAME:-xrpld}
log() {
# If running under systemd/journald, let it handle timestamps.
if [[ -n "${JOURNAL_STREAM:-}" ]]; then
printf '%s\n' "$*"
else
printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"
fi
}
require_root() {
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
log "RESULT: failed reason=not-root"
exit 1
fi
}
get_installed_version() {
if command -v dpkg-query >/dev/null 2>&1; then
dpkg-query -W -f='${Version}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
elif command -v rpm >/dev/null 2>&1; then
rpm -q --qf '%{VERSION}-%{RELEASE}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
else
printf 'unknown'
fi
}
trap 'log "RESULT: failed reason=script-error exit_code=$?"' ERR
apt_can_update() {
apt-get update -qq
apt-get -s --only-upgrade install "$PKG_NAME" 2>/dev/null | grep -q "^Inst ${PKG_NAME}\b"
}
apt_apply_update() {
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
"$PKG_NAME"
}
get_rpm_pm() {
if command -v dnf >/dev/null 2>&1; then
printf 'dnf\n'
elif command -v yum >/dev/null 2>&1; then
printf 'yum\n'
else
return 1
fi
}
rpm_refresh_metadata() {
local pm=$1
if [[ "$pm" == "dnf" ]]; then
dnf makecache --refresh -q >/dev/null
else
yum clean expire-cache -q >/dev/null
fi
}
rpm_can_update() {
local pm=$1
rpm_refresh_metadata "$pm"
local rc=0
set +e
"$pm" check-update -q "$PKG_NAME" >/dev/null 2>&1
rc=$?
set -e
if [[ $rc -eq 100 ]]; then
return 0
elif [[ $rc -eq 0 ]]; then
return 1
else
log "$pm check-update failed with exit code $rc"
exit 1
fi
}
rpm_apply_update() {
local pm=$1
"$pm" update -y "$PKG_NAME"
}
restart_service() {
systemctl restart "${PKG_NAME}.service"
log "${PKG_NAME} service restarted successfully"
}
main() {
require_root
if command -v apt-get >/dev/null 2>&1; then
log "Checking for ${PKG_NAME} updates via apt"
if apt_can_update; then
log "Update available; installing"
apt_apply_update
restart_service
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
else
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
fi
return
fi
local rpm_pm=""
if rpm_pm="$(get_rpm_pm)"; then
log "Checking for ${PKG_NAME} updates via ${rpm_pm}"
if rpm_can_update "$rpm_pm"; then
log "Update available; installing"
rpm_apply_update "$rpm_pm"
restart_service
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
else
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
fi
return
fi
log "RESULT: failed reason=no-package-manager"
exit 1
}
main "$@"

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Daily xrpld update check
[Timer]
OnCalendar=*-*-* 00:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,14 @@
/var/log/xrpld/*.log {
daily
minsize 200M
rotate 7
nocreate
missingok
notifempty
compress
compresscmd /usr/bin/gzip
compressext .gz
postrotate
/opt/xrpld/bin/xrpld --conf /etc/opt/xrpld/xrpld.cfg logrotate
endscript
}

View File

@@ -0,0 +1,22 @@
[Unit]
Description=XRP Ledger Daemon
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
ExecStart=/opt/xrpld/bin/xrpld --net --silent --conf /etc/opt/xrpld/xrpld.cfg
Restart=always
RestartSec=5s
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
User=xrpld
Group=xrpld
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1 @@
u xrpld - "XRP Ledger daemon" /var/lib/xrpld /sbin/nologin

View File

@@ -0,0 +1,2 @@
d /var/lib/xrpld 0750 xrpld xrpld -
d /var/log/xrpld 0750 xrpld xrpld -

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env sh
# Validate installed paths and compat symlinks for xrpld packages.
set -e
set -x
trap 'test $? -ne 0 && touch /tmp/test_failed' EXIT
check() { test $1 "$2" || { echo "FAIL: $1 $2"; exit 1; }; }
check_resolves_to() {
actual=$(readlink -f "$1")
[ "$actual" = "$2" ] || { echo "FAIL: $1 resolves to $actual, expected $2"; exit 1; }
}
# compat directory symlinks — existence and resolved target
check -L /opt/ripple
check_resolves_to /opt/ripple /opt/xrpld
check -L /etc/opt/xrpld
check_resolves_to /etc/opt/xrpld /opt/xrpld/etc
check -L /etc/opt/ripple
check_resolves_to /etc/opt/ripple /opt/xrpld/etc
# config accessible via all expected paths
check -f /opt/xrpld/etc/xrpld.cfg
check -f /opt/xrpld/etc/rippled.cfg
check -f /etc/opt/xrpld/xrpld.cfg
check -f /etc/opt/xrpld/rippled.cfg
check -f /etc/opt/ripple/xrpld.cfg
check -f /etc/opt/ripple/rippled.cfg
if systemctl is-system-running >/dev/null 2>&1; then
# service file sanity check
SERVICE=$(systemctl cat xrpld)
echo "$SERVICE" | grep -q 'ExecStart=/opt/xrpld/bin/xrpld' || { echo "FAIL: ExecStart wrong"; echo "$SERVICE"; exit 1; }
echo "$SERVICE" | grep -q 'User=xrpld' || { echo "FAIL: User not xrpld"; echo "$SERVICE"; exit 1; }
fi
# binary accessible via all expected paths
/opt/xrpld/bin/xrpld --version
/opt/xrpld/bin/rippled --version
/opt/ripple/bin/xrpld --version
/opt/ripple/bin/rippled --version
/usr/bin/xrpld --version
/usr/local/bin/rippled --version

91
package/test/smoketest.sh Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Install a locally-built package and run basic verification.
#
# Usage: smoketest.sh local
# Expects packages in build/{dpkg,rpm}/packages/ or build/debbuild/ / build/rpmbuild/RPMS/
set -o pipefail
set -x
rm -f /tmp/test_failed /tmp/unittest_results
trap 'test $? -ne 0 && touch /tmp/test_failed' EXIT
install_from=$1
. /etc/os-release
case ${ID} in
ubuntu|debian)
pkgtype="dpkg"
;;
fedora|centos|rhel|rocky|almalinux)
pkgtype="rpm"
;;
*)
echo "unrecognized distro!"
exit 1
;;
esac
if [ "${install_from}" != "local" ]; then
echo "only 'local' install mode is supported"
exit 1
fi
# Install the package
if [ "${pkgtype}" = "dpkg" ] ; then
apt-get -y update
# Find .deb files — check both possible output locations
mapfile -t debs < <(find build/debbuild/ build/dpkg/packages/ -name '*.deb' ! -name '*dbgsym*' 2>/dev/null)
if [ ${#debs[@]} -eq 0 ]; then
echo "No .deb files found"
exit 1
fi
dpkg --no-debsig -i "${debs[@]}" || apt-get -y install -f || { echo "DEB install failed"; exit 1; }
elif [ "${pkgtype}" = "rpm" ] ; then
# Find .rpm files — check both possible output locations
mapfile -t rpms < <(find build/rpmbuild/RPMS/ build/rpm/packages/ -name '*.rpm' \
! -name '*debug*' ! -name '*devel*' ! -name '*.src.rpm' 2>/dev/null)
if [ ${#rpms[@]} -eq 0 ]; then
echo "No .rpm files found"
exit 1
fi
rpm -i "${rpms[@]}" || { echo "RPM install failed"; exit 1; }
fi
# Verify installed version
if ! VERSION_OUTPUT=$(/opt/xrpld/bin/xrpld --version); then
echo "xrpld --version failed; binary not installed correctly"
exit 1
fi
INSTALLED=$(echo "$VERSION_OUTPUT" | head -1 | awk '{print $NF}')
echo "Installed version: ${INSTALLED}"
# Run unit tests
if [ -n "${CI:-}" ]; then
unittest_jobs=$(nproc)
else
unittest_jobs=16
fi
(
cd /tmp
/opt/xrpld/bin/xrpld --unittest --unittest-jobs "${unittest_jobs}" > /tmp/unittest_results || true
)
if [ ! -s /tmp/unittest_results ]; then
echo "Unit test results file is empty — xrpld may have crashed"
exit 1
fi
num_failures=$(tail /tmp/unittest_results -n1 | grep -oP '\d+(?= failures)')
if [ -z "$num_failures" ]; then
echo "Could not parse unit test results — expected summary line not found"
exit 1
fi
if [ "${num_failures}" -ne 0 ]; then
echo "$num_failures unit test(s) failed:"
grep 'failed:' /tmp/unittest_results
exit 1
fi
# Compat path checks
"$(dirname "${BASH_SOURCE[0]}")/check_install_paths.sh"

View File

@@ -8,9 +8,12 @@ add_custom_target(xrpl.tests)
# Test helpers
add_library(xrpl.helpers.test STATIC)
target_sources(xrpl.helpers.test PRIVATE helpers/TestSink.cpp)
target_sources(
xrpl.helpers.test
PRIVATE helpers/Account.cpp helpers/TestSink.cpp helpers/TxTest.cpp
)
target_include_directories(xrpl.helpers.test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(xrpl.helpers.test PRIVATE xrpl.libxrpl)
target_link_libraries(xrpl.helpers.test PUBLIC xrpl.libxrpl gtest::gtest)
# Common library dependencies for the rest of the tests.
add_library(xrpl.imports.test INTERFACE)
@@ -32,6 +35,10 @@ xrpl_add_test(json)
target_link_libraries(xrpl.test.json PRIVATE xrpl.imports.test)
add_dependencies(xrpl.tests xrpl.test.json)
xrpl_add_test(tx)
target_link_libraries(xrpl.test.tx PRIVATE xrpl.imports.test)
add_dependencies(xrpl.tests xrpl.test.tx)
xrpl_add_test(protocol_autogen)
target_link_libraries(xrpl.test.protocol_autogen PRIVATE xrpl.imports.test)
add_dependencies(xrpl.tests xrpl.test.protocol_autogen)

View File

@@ -0,0 +1,19 @@
#include <helpers/Account.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
namespace xrpl::test {
Account const Account::master{"masterpassphrase"};
Account::Account(std::string_view name, KeyType type)
: name_(name)
, keyPair_(generateKeyPair(type, generateSeed(name_)))
, id_(calcAccountID(keyPair_.first))
{
}
} // namespace xrpl::test

View File

@@ -0,0 +1,81 @@
#pragma once
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <string>
#include <string_view>
#include <utility>
namespace xrpl::test {
/**
* @brief A test account with cryptographic keys.
*
* Generates keys deterministically from a name, making tests reproducible.
* The same name always produces the same AccountID and keys.
*/
class Account
{
public:
/**
* @brief The master account that holds all XRP in genesis.
*
* This account is created in the genesis ledger with all 100 billion XRP.
* It uses the well-known seed "masterpassphrase".
*/
static Account const master;
/**
* @brief Create an account from a name.
*
* Keys are derived deterministically from the name.
*
* @param name Human-readable name for the account.
* @param type Key type to use (defaults to secp256k1).
*/
explicit Account(std::string_view name, KeyType type = KeyType::secp256k1);
/** @brief Return the human-readable name. */
std::string const&
name() const noexcept
{
return name_;
}
/** @brief Return the AccountID. */
AccountID const&
id() const noexcept
{
return id_;
}
/** @brief Return the public key. */
PublicKey const&
pk() const noexcept
{
return keyPair_.first;
}
/** @brief Return the secret key. */
SecretKey const&
sk() const noexcept
{
return keyPair_.second;
}
/** @brief Implicit conversion to AccountID. */
operator AccountID const&() const noexcept
{
return id_;
}
private:
std::string name_;
std::pair<PublicKey, SecretKey> keyPair_;
AccountID id_;
};
} // namespace xrpl::test

View File

@@ -0,0 +1,132 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/UintTypes.h>
#include <helpers/Account.h>
#include <concepts>
#include <string>
#include <string_view>
#include <type_traits>
namespace xrpl::test {
/**
* @brief Represents an IOU (issued currency) for testing.
*
* Provides a clear, explicit API for creating currencies issued by an account.
* This replaces the cryptic `Account::operator[]` from the jtx framework.
*
* @code
* Account gw("gateway");
* IOU USD("USD", gw);
*
* auto issue = USD.issue(); // Get the Issue
* auto asset = USD.asset(); // Get the Asset
* auto amt = USD.amount(100); // Get STAmount of 100 USD
* @endcode
*/
class IOU
{
public:
/**
* @brief Construct an IOU from a currency code and issuing account.
* @param currencyCode A 3-character ISO currency code (e.g., "USD").
* @param issuer The account that issues this currency.
*/
IOU(std::string_view currencyCode, Account const& issuer)
: currency_(to_currency(std::string(currencyCode))), issuer_(issuer.id())
{
XRPL_ASSERT(!isXRP(currency_), "IOU: currency code must not resolve to XRP");
}
/**
* @brief Construct an IOU from a Currency and issuing account.
* @param currency The Currency object.
* @param issuer The account that issues this currency.
*/
IOU(Currency currency, Account const& issuer)
: currency_(std::move(currency)), issuer_(issuer.id())
{
XRPL_ASSERT(!isXRP(currency_), "IOU: currency code must not resolve to XRP");
}
/**
* @brief Get the Issue (currency + issuer pair).
* @return An Issue object representing this IOU.
*/
[[nodiscard]] Issue
issue() const
{
return Issue{currency_, issuer_};
}
/**
* @brief Get the Asset.
* @return An Asset object representing this IOU.
*/
[[nodiscard]] Asset
asset() const
{
return Asset{issue()};
}
/**
* @brief Create an STAmount of this IOU.
*
* Works with any arithmetic type (int, double, etc.) by converting
* to string and parsing. This matches the jtx IOU behaviour.
*
* @tparam T An arithmetic type.
* @param value The amount as any arithmetic type.
* @return An STAmount representing value units of this IOU.
*/
template <typename T>
requires std::is_arithmetic_v<T>
[[nodiscard]] STAmount
amount(T value) const
{
return amountFromString(issue(), to_string(value));
}
/**
* @brief Create an STAmount of this IOU from a Number.
* @param value The amount as a Number.
* @return An STAmount representing value units of this IOU.
*/
[[nodiscard]] STAmount
amount(Number const& value) const
{
return STAmount{issue(), value};
}
/**
* @brief Get the currency.
* @return The currency.
*/
[[nodiscard]] Currency const&
currency() const
{
return currency_;
}
/**
* @brief Get the issuer account ID.
* @return The issuer's AccountID.
*/
[[nodiscard]] AccountID const&
issuer() const
{
return issuer_;
}
private:
Currency currency_;
AccountID issuer_;
};
} // namespace xrpl::test

View File

@@ -0,0 +1,111 @@
#pragma once
#include <xrpl/basics/chrono.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/shamap/Family.h>
#include <memory>
namespace xrpl {
namespace test {
/** Test implementation of Family for unit tests.
Uses an in-memory NodeStore database and simple caches.
The missingNode methods throw since tests shouldn't encounter missing nodes.
*/
class TestFamily : public Family
{
private:
std::unique_ptr<NodeStore::Database> db_;
TestStopwatch clock_;
std::shared_ptr<FullBelowCache> fbCache_;
std::shared_ptr<TreeNodeCache> tnCache_;
NodeStore::DummyScheduler scheduler_;
beast::Journal j_;
public:
explicit TestFamily(beast::Journal j)
: fbCache_(std::make_shared<FullBelowCache>("TestFamily full below cache", clock_, j))
, tnCache_(
std::make_shared<TreeNodeCache>(
"TestFamily tree node cache",
65536,
std::chrono::minutes{1},
clock_,
j))
, j_(j)
{
Section config;
config.set("type", "memory");
config.set("path", "TestFamily");
db_ = NodeStore::Manager::instance().make_Database(megabytes(4), scheduler_, 1, config, j);
}
NodeStore::Database&
db() override
{
return *db_;
}
NodeStore::Database const&
db() const override
{
return *db_;
}
beast::Journal const&
journal() override
{
return j_;
}
std::shared_ptr<FullBelowCache>
getFullBelowCache() override
{
return fbCache_;
}
std::shared_ptr<TreeNodeCache>
getTreeNodeCache() override
{
return tnCache_;
}
void
sweep() override
{
fbCache_->sweep();
tnCache_->sweep();
}
void
missingNodeAcquireBySeq(std::uint32_t refNum, uint256 const& nodeHash) override
{
Throw<std::runtime_error>("TestFamily: missing node (by seq)");
}
void
missingNodeAcquireByHash(uint256 const& refHash, std::uint32_t refNum) override
{
Throw<std::runtime_error>("TestFamily: missing node (by hash)");
}
void
reset() override
{
fbCache_->reset();
tnCache_->reset();
}
/** Access the test clock for time manipulation in tests. */
TestStopwatch&
clock()
{
return clock_;
}
};
} // namespace test
} // namespace xrpl

View File

@@ -0,0 +1,378 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/PendingSaves.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <boost/asio/io_context.hpp>
#include <helpers/TestFamily.h>
#include <helpers/TestSink.h>
#include <optional>
#include <stdexcept>
namespace xrpl {
namespace test {
/** Logs implementation that creates TestSink instances. */
class TestLogs : public Logs
{
public:
explicit TestLogs(beast::severities::Severity level = beast::severities::kWarning) : Logs(level)
{
}
std::unique_ptr<beast::Journal::Sink>
makeSink(std::string const&, beast::severities::Severity threshold) override
{
return std::make_unique<TestSink>(threshold);
}
};
/** Simple NetworkIDService implementation for tests. */
class TestNetworkIDService final : public NetworkIDService
{
public:
explicit TestNetworkIDService(std::uint32_t networkID = 0) : networkID_(networkID)
{
}
[[nodiscard]] std::uint32_t
getNetworkID() const noexcept override
{
return networkID_;
}
private:
std::uint32_t networkID_;
};
/** Test implementation of ServiceRegistry for unit tests.
This class provides real implementations for services that can be
instantiated from libxrpl (such as Logs, io_context, caches), and
throws std::logic_error for services that require the full Application.
Tests can subclass this to provide additional services they need.
*/
class TestServiceRegistry : public ServiceRegistry
{
TestLogs logs_{beast::severities::kWarning};
boost::asio::io_context io_context_;
TestFamily family_{logs_.journal("TestFamily")};
LoadFeeTrack feeTrack_{logs_.journal("LoadFeeTrack")};
TestNetworkIDService networkIDService_;
HashRouter hashRouter_{HashRouter::Setup{}, stopwatch()};
NodeCache tempNodeCache_{
"TempNodeCache",
16384,
std::chrono::minutes{1},
stopwatch(),
logs_.journal("TaggedCache")};
CachedSLEs cachedSLEs_{
"CachedSLEs",
16384,
std::chrono::minutes{1},
stopwatch(),
logs_.journal("TaggedCache")};
PendingSaves pendingSaves_;
std::optional<uint256> trapTxID_;
public:
TestServiceRegistry() = default;
~TestServiceRegistry() override = default;
// Core infrastructure services
CollectorManager&
getCollectorManager() override
{
throw std::logic_error("TestServiceRegistry::getCollectorManager() not implemented");
}
Family&
getNodeFamily() override
{
return family_;
}
TimeKeeper&
getTimeKeeper() override
{
throw std::logic_error("TestServiceRegistry::timeKeeper() not implemented");
}
JobQueue&
getJobQueue() override
{
throw std::logic_error("TestServiceRegistry::getJobQueue() not implemented");
}
NodeCache&
getTempNodeCache() override
{
return tempNodeCache_;
}
CachedSLEs&
getCachedSLEs() override
{
return cachedSLEs_;
}
NetworkIDService&
getNetworkIDService() override
{
return networkIDService_;
}
// Protocol and validation services
AmendmentTable&
getAmendmentTable() override
{
throw std::logic_error("TestServiceRegistry::getAmendmentTable() not implemented");
}
HashRouter&
getHashRouter() override
{
return hashRouter_;
}
LoadFeeTrack&
getFeeTrack() override
{
return feeTrack_;
}
LoadManager&
getLoadManager() override
{
throw std::logic_error("TestServiceRegistry::getLoadManager() not implemented");
}
RCLValidations&
getValidations() override
{
throw std::logic_error("TestServiceRegistry::getValidations() not implemented");
}
ValidatorList&
getValidators() override
{
throw std::logic_error("TestServiceRegistry::validators() not implemented");
}
ValidatorSite&
getValidatorSites() override
{
throw std::logic_error("TestServiceRegistry::validatorSites() not implemented");
}
ManifestCache&
getValidatorManifests() override
{
throw std::logic_error("TestServiceRegistry::validatorManifests() not implemented");
}
ManifestCache&
getPublisherManifests() override
{
throw std::logic_error("TestServiceRegistry::publisherManifests() not implemented");
}
// Network services
Overlay&
getOverlay() override
{
throw std::logic_error("TestServiceRegistry::overlay() not implemented");
}
Cluster&
getCluster() override
{
throw std::logic_error("TestServiceRegistry::cluster() not implemented");
}
PeerReservationTable&
getPeerReservations() override
{
throw std::logic_error("TestServiceRegistry::peerReservations() not implemented");
}
Resource::Manager&
getResourceManager() override
{
throw std::logic_error("TestServiceRegistry::getResourceManager() not implemented");
}
// Storage services
NodeStore::Database&
getNodeStore() override
{
throw std::logic_error("TestServiceRegistry::getNodeStore() not implemented");
}
SHAMapStore&
getSHAMapStore() override
{
throw std::logic_error("TestServiceRegistry::getSHAMapStore() not implemented");
}
RelationalDatabase&
getRelationalDatabase() override
{
throw std::logic_error("TestServiceRegistry::getRelationalDatabase() not implemented");
}
// Ledger services
InboundLedgers&
getInboundLedgers() override
{
throw std::logic_error("TestServiceRegistry::getInboundLedgers() not implemented");
}
InboundTransactions&
getInboundTransactions() override
{
throw std::logic_error("TestServiceRegistry::getInboundTransactions() not implemented");
}
TaggedCache<uint256, AcceptedLedger>&
getAcceptedLedgerCache() override
{
throw std::logic_error("TestServiceRegistry::getAcceptedLedgerCache() not implemented");
}
LedgerMaster&
getLedgerMaster() override
{
throw std::logic_error("TestServiceRegistry::getLedgerMaster() not implemented");
}
LedgerCleaner&
getLedgerCleaner() override
{
throw std::logic_error("TestServiceRegistry::getLedgerCleaner() not implemented");
}
LedgerReplayer&
getLedgerReplayer() override
{
throw std::logic_error("TestServiceRegistry::getLedgerReplayer() not implemented");
}
PendingSaves&
getPendingSaves() override
{
return pendingSaves_;
}
OpenLedger&
getOpenLedger() override
{
throw std::logic_error("TestServiceRegistry::openLedger() not implemented");
}
OpenLedger const&
getOpenLedger() const override
{
throw std::logic_error("TestServiceRegistry::openLedger() const not implemented");
}
// Transaction and operation services
NetworkOPs&
getOPs() override
{
throw std::logic_error("TestServiceRegistry::getOPs() not implemented");
}
OrderBookDB&
getOrderBookDB() override
{
throw std::logic_error("TestServiceRegistry::getOrderBookDB() not implemented");
}
TransactionMaster&
getMasterTransaction() override
{
throw std::logic_error("TestServiceRegistry::getMasterTransaction() not implemented");
}
TxQ&
getTxQ() override
{
throw std::logic_error("TestServiceRegistry::getTxQ() not implemented");
}
PathRequestManager&
getPathRequestManager() override
{
throw std::logic_error("TestServiceRegistry::getPathRequestManager() not implemented");
}
// Server services
ServerHandler&
getServerHandler() override
{
throw std::logic_error("TestServiceRegistry::getServerHandler() not implemented");
}
perf::PerfLog&
getPerfLog() override
{
throw std::logic_error("TestServiceRegistry::getPerfLog() not implemented");
}
// Configuration and state
bool
isStopping() const override
{
return false;
}
beast::Journal
getJournal(std::string const& name) override
{
return logs_.journal(name);
}
boost::asio::io_context&
getIOContext() override
{
return io_context_;
}
Logs&
getLogs() override
{
return logs_;
}
std::optional<uint256> const&
getTrapTxID() const override
{
return trapTxID_;
}
DatabaseCon&
getWalletDB() override
{
throw std::logic_error("TestServiceRegistry::getWalletDB() not implemented");
}
// Temporary: Get the underlying Application
Application&
getApp() override
{
throw std::logic_error(
"TestServiceRegistry::app() not implemented - no Application available in tests");
}
};
} // namespace test
} // namespace xrpl

View File

@@ -0,0 +1,252 @@
#include <helpers/TxTest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CanonicalTXSet.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
#include <xrpl/protocol_autogen/ledger_entries/RippleState.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <xrpl/protocol_autogen/transactions/Payment.h>
#include <xrpl/tx/apply.h>
#include <helpers/Account.h>
#include <helpers/IOU.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
namespace xrpl::test {
//------------------------------------------------------------------------------
// Feature helpers
//------------------------------------------------------------------------------
FeatureBitset
allFeatures()
{
static FeatureBitset const features = [] {
auto const& sa = allAmendments();
std::vector<uint256> feats;
feats.reserve(sa.size());
for ([[maybe_unused]] auto const& [name, _] : sa)
{
if (auto const f = getRegisteredFeature(name); f.has_value())
feats.push_back(*f);
}
return FeatureBitset(feats);
}();
return features;
}
//------------------------------------------------------------------------------
// TxTest
//------------------------------------------------------------------------------
TxTest::TxTest(std::optional<FeatureBitset> features)
{
// Convert FeatureBitset to unordered_set for Rules constructor
auto const featureBits = features.value_or(allFeatures());
foreachFeature(featureBits, [&](uint256 const& f) { featureSet_.insert(f); });
// Create rules with the specified features
rules_.emplace(featureSet_);
// Default fees for testing
Fees const fees{XRPAmount{10}, XRPAmount{10000000}, XRPAmount{2000000}};
// Create a genesis ledger as the base
closedLedger_ = std::make_shared<Ledger>(
create_genesis,
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
*rules_,
fees,
std::vector<uint256>{featureSet_.begin(), featureSet_.end()},
registry_.getNodeFamily());
// Initialize time from the genesis ledger
now_ = closedLedger_->header().closeTime;
// Create an open view on top of the genesis ledger
openLedger_ =
std::make_shared<OpenView>(open_ledger, closedLedger_.get(), *rules_, closedLedger_);
}
bool
TxTest::isEnabled(uint256 const& feature) const
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
return rules_->enabled(feature);
}
Rules const&
TxTest::getRules() const
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
return *rules_;
}
[[nodiscard]] TxResult
TxTest::submit(std::shared_ptr<STTx const> stx)
{
auto result = apply(registry_, *openLedger_, *stx, tapNONE, registry_.getJournal("apply"));
// Track successfully applied transactions for canonical reordering on close
// We make a copy since the TransactionBase doesn't own the STTx
if (result.applied)
pendingTxs_.push_back(stx);
return TxResult{
.ter = result.ter,
.applied = result.applied,
.metadata = std::move(result).metadata,
.tx = std::move(stx)};
}
void
TxTest::createAccount(Account const& account, XRPAmount xrp, uint32_t accountFlags)
{
auto const paymentTer =
submit(transactions::PaymentBuilder{Account::master, account, xrp}, Account::master).ter;
if (paymentTer != tesSUCCESS)
{
throw std::runtime_error("TxTest::createAccount: failed to create account");
}
close();
if (accountFlags != 0)
{
auto const accountSetTer =
submit(transactions::AccountSetBuilder{account}.setSetFlag(accountFlags), account).ter;
if (accountSetTer != tesSUCCESS)
{
throw std::runtime_error("TxTest::createAccount: failed to set account flags");
}
close();
}
}
ledger_entries::AccountRoot
TxTest::getAccountRoot(AccountID const& id) const
{
auto const sle = getOpenLedger().read(keylet::account(id));
if (!sle)
Throw<std::runtime_error>("TxTest::getAccountRoot: account not found");
return ledger_entries::AccountRoot{std::const_pointer_cast<SLE const>(sle)};
}
OpenView&
TxTest::getOpenLedger()
{
return *openLedger_;
}
OpenView const&
TxTest::getOpenLedger() const
{
return *openLedger_;
}
ReadView const&
TxTest::getClosedLedger() const
{
return *closedLedger_;
}
void
TxTest::close()
{
// Build a new closed ledger from the previous closed ledger,
// similar to how buildLedgerImpl works:
// 1. Create a new Ledger from the previous closed ledger
// 2. Re-apply transactions in canonical order
// 3. Mark it as accepted/immutable
auto const& prevLedger = *closedLedger_;
auto const ledgerCloseTime = now_ + prevLedger.header().closeTimeResolution;
now_ = ledgerCloseTime;
auto newLedger = std::make_shared<Ledger>(prevLedger, ledgerCloseTime);
CanonicalTXSet txSet(prevLedger.header().hash);
for (auto const& tx : pendingTxs_)
txSet.insert(tx);
{
OpenView accum(&*newLedger);
for (auto const& [key, tx] : txSet)
{
auto result = apply(registry_, accum, *tx, tapNONE, registry_.getJournal("apply"));
if (!result.applied)
{
throw std::runtime_error("TxTest::close: failed to apply transaction");
}
}
accum.apply(*newLedger);
}
newLedger->setAccepted(ledgerCloseTime, newLedger->header().closeTimeResolution, true);
closedLedger_ = newLedger;
pendingTxs_.clear();
openLedger_ =
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
std::make_shared<OpenView>(open_ledger, closedLedger_.get(), *rules_, closedLedger_);
}
void
TxTest::advanceTime(NetClock::duration duration)
{
now_ += duration;
}
NetClock::time_point
TxTest::getCloseTime() const
{
return now_;
}
STAmount
TxTest::getBalance(AccountID const& account, IOU const& iou) const
{
auto const sle = openLedger_->read(keylet::line(account, iou.issue()));
if (!sle)
return STAmount{iou.issue(), 0};
auto const rippleState = ledger_entries::RippleState{sle};
auto balance = rippleState.getBalance();
if (iou.issue().account == account)
{
throw std::logic_error("TxTest::getBalance: account is issuer");
}
balance.get<Issue>().account = iou.issue().account;
if (account > iou.issue().account)
balance.negate();
return balance;
}
} // namespace xrpl::test

View File

@@ -0,0 +1,364 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
#include <xrpl/tx/applySteps.h>
#include <helpers/Account.h>
#include <helpers/IOU.h>
#include <helpers/TestServiceRegistry.h>
#include <cmath>
#include <concepts>
#include <memory>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <unordered_set>
#include <vector>
namespace xrpl::test {
//------------------------------------------------------------------------------
// Amount helpers
//------------------------------------------------------------------------------
/**
* @brief Convert XRP to drops (integral types).
* @param xrp The amount in XRP.
* @return The equivalent amount in drops as XRPAmount.
*/
template <std::integral T>
constexpr XRPAmount
XRP(T xrp)
{
return XRPAmount{static_cast<std::int64_t>(xrp) * DROPS_PER_XRP.drops()};
}
/**
* @brief Convert XRP to drops (floating point types).
* @param xrp The amount in XRP (may be fractional).
* @return The equivalent amount in drops as XRPAmount.
*/
template <std::floating_point T>
XRPAmount
XRP(T xrp)
{
return XRPAmount{static_cast<std::int64_t>(std::round(xrp * DROPS_PER_XRP.drops()))};
}
/**
* @brief Convert XRP to drops (Number type).
* @param xrp The amount in XRP as a Number.
* @return The equivalent amount in drops as XRPAmount.
*/
inline XRPAmount
XRP(Number const& xrp)
{
return XRPAmount{static_cast<std::int64_t>(xrp * DROPS_PER_XRP.drops())};
}
//------------------------------------------------------------------------------
// Flag helpers
//------------------------------------------------------------------------------
/**
* @brief Convert AccountSet flag (asf) to LedgerState flag (lsf).
* @param asf The AccountSet flag value.
* @return The corresponding LedgerState flag.
* @throws std::runtime_error if the flag is not supported.
*
* Supported flags:
* asfRequireDest, asfRequireAuth, asfDisallowXRP, asfDisableMaster,
* asfNoFreeze, asfGlobalFreeze, asfDefaultRipple, asfDepositAuth,
* asfAllowTrustLineClawback, asfDisallowIncomingCheck,
* asfDisallowIncomingNFTokenOffer, asfDisallowIncomingPayChan,
* asfDisallowIncomingTrustline, asfAllowTrustLineLocking
*/
constexpr std::uint32_t
asfToLsf(std::uint32_t asf)
{
switch (asf)
{
case asfRequireDest:
return lsfRequireDestTag;
case asfRequireAuth:
return lsfRequireAuth;
case asfDisallowXRP:
return lsfDisallowXRP;
case asfDisableMaster:
return lsfDisableMaster;
case asfNoFreeze:
return lsfNoFreeze;
case asfGlobalFreeze:
return lsfGlobalFreeze;
case asfDefaultRipple:
return lsfDefaultRipple;
case asfDepositAuth:
return lsfDepositAuth;
case asfAllowTrustLineClawback:
return lsfAllowTrustLineClawback;
case asfDisallowIncomingCheck:
return lsfDisallowIncomingCheck;
case asfDisallowIncomingNFTokenOffer:
return lsfDisallowIncomingNFTokenOffer;
case asfDisallowIncomingPayChan:
return lsfDisallowIncomingPayChan;
case asfDisallowIncomingTrustline:
return lsfDisallowIncomingTrustline;
case asfAllowTrustLineLocking:
return lsfAllowTrustLineLocking;
default:
throw std::runtime_error("Unknown asf flag");
}
}
//------------------------------------------------------------------------------
// Feature helpers
//------------------------------------------------------------------------------
/**
* @brief Returns all testable amendments.
* @note This is similar to jtx::testable_amendments() but for the TxTest framework.
*/
FeatureBitset
allFeatures();
//------------------------------------------------------------------------------
// TxResult
//------------------------------------------------------------------------------
/**
* @brief Result of a transaction submission in TxTest.
*
* Contains the TER code, whether the transaction was applied,
* optional metadata, and a reference to the submitted transaction.
* Use standard gtest macros (EXPECT_EQ, EXPECT_TRUE, etc.) to verify results.
*/
struct TxResult
{
TER ter; /**< The transaction engine result code. */
bool applied; /**< Whether the transaction was applied to the ledger. */
std::optional<TxMeta> metadata; /**< Transaction metadata, if available. */
std::shared_ptr<STTx const> tx; /**< Pointer to the submitted transaction. */
};
/**
* @brief A lightweight transaction testing harness.
*
* Unlike the JTx framework which requires a full Application and RPC layer,
* TxTest applies transactions directly to an OpenView using the transactor
* pipeline (preflight -> preclaim -> doApply).
*
* This makes it suitable for:
* - Unit testing individual transactors
* - Testing transaction validation logic
* - Fast, focused tests without full server infrastructure
*
* @code
* TxTest env;
* env.submit(paymentTx).expectSuccess();
* env.submit(badTx).expectTer(tecNO_ENTRY);
* @endcode
*/
class TxTest
{
public:
/**
* @brief Construct a TxTest environment.
*
* Creates a genesis ledger and an open view on top of it.
*
* @param features Optional set of features to enable. If not specified,
* uses all testable amendments.
*/
explicit TxTest(std::optional<FeatureBitset> features = std::nullopt);
/**
* @brief Check if a feature is enabled.
* @param feature The feature to check.
* @return True if the feature is enabled.
*/
[[nodiscard]] bool
isEnabled(uint256 const& feature) const;
/**
* @brief Get the current rules.
* @return The current consensus rules.
*/
[[nodiscard]] Rules const&
getRules() const;
/**
* @brief Submit a transaction from a builder.
*
* Convenience overload that accepts transaction builders.
* Automatically sets sequence and fee before submission.
*
* @tparam T A type derived from TransactionBuilderBase.
* @param builder The transaction builder.
* @param signer The account to sign with.
* @return TxResult containing the result code, applied status, and metadata.
*/
template <typename T>
requires std::
derived_from<std::decay_t<T>, transactions::TransactionBuilderBase<std::decay_t<T>>>
[[nodiscard]] TxResult
submit(T&& builder, Account const& signer)
{
auto const& obj = builder.getSTObject();
auto accountId = obj[sfAccount];
// Only set sequence if not using a ticket (ticket sets sequence to 0)
if (!obj.isFieldPresent(sfTicketSequence))
{
builder.setSequence(getAccountRoot(accountId).getSequence());
}
else
{
builder.setSequence(0);
}
builder.setFee(XRPAmount(10));
return submit(builder.build(signer.pk(), signer.sk()).getSTTx());
}
/**
* @brief Submit a transaction to the open ledger.
*
* Applies the transaction through the full transactor pipeline:
* preflight -> preclaim -> doApply -> invariant checks
*
* Invariant checks are automatically run after doApply. If any
* invariant fails, the result will be tecINVARIANT_FAILED.
*
* @param stx The transaction to submit.
* @return TxResult containing the result code, applied status, and metadata.
*/
[[nodiscard]] TxResult
submit(std::shared_ptr<STTx const> stx);
/**
* @brief Create a new account in the ledger.
*
* Sends a Payment from the master account to create and fund the account.
* Closes the ledger after creation. If accountFlags is non-zero, submits
* an AccountSet transaction and closes again.
*
* @param account The account to create.
* @param xrp The initial XRP balance.
* @param accountFlags Optional account flags to set. Defaults to 0
* (no flags).
*/
void
createAccount(Account const& account, XRPAmount xrp, uint32_t accountFlags = 0);
/**
* @brief Get the account root object from the current open ledger.
* @param id The account ID.
* @return The AccountRoot ledger entry.
* @throws std::runtime_error if the account does not exist.
* @todo Once we make keylet strongly typed, we can ditch this method.
*/
[[nodiscard]] ledger_entries::AccountRoot
getAccountRoot(AccountID const& id) const;
/**
* @brief Get the current open ledger view.
* @return A mutable reference to the open ledger.
*/
[[nodiscard]] OpenView&
getOpenLedger();
/**
* @brief Get the current open ledger view (const).
* @return A const reference to the open ledger.
*/
[[nodiscard]] OpenView const&
getOpenLedger() const;
/**
* @brief Get the closed (base) ledger view.
* @return A const reference to the closed ledger.
*/
[[nodiscard]] ReadView const&
getClosedLedger() const;
/**
* @brief Close the current ledger.
*
* Creates a new closed ledger from the current open ledger.
* All pending transactions are re-applied in canonical order.
*/
void
close();
/**
* @brief Advance time without closing the ledger.
*
* Useful for testing time-dependent features like escrow release
* times or offer expirations.
*
* @param duration The amount of time to advance.
*/
void
advanceTime(NetClock::duration duration);
/**
* @brief Get the current ledger close time.
* @return The current close time.
*/
[[nodiscard]] NetClock::time_point
getCloseTime() const;
/**
* @brief Get the balance of an IOU for an account.
*
* Returns the balance from the perspective of the specified account.
* If the trust line doesn't exist, returns zero.
*
* @param account The account to check.
* @param iou The IOU to check the balance for.
* @return The balance as an STAmount.
* @todo Once we make keylet strongly typed, we can ditch this method.
*/
[[nodiscard]] STAmount
getBalance(AccountID const& account, IOU const& iou) const;
/**
* @brief Get the service registry.
* @return A reference to the service registry.
*/
ServiceRegistry&
getServiceRegistry()
{
return registry_;
}
private:
TestServiceRegistry registry_;
std::unordered_set<uint256, beast::uhash<>> featureSet_;
std::optional<Rules> rules_;
std::shared_ptr<Ledger const> closedLedger_;
std::shared_ptr<OpenView> openLedger_;
/** Transactions submitted to the open ledger, for canonical reordering on close. */
std::vector<std::shared_ptr<STTx const>> pendingTxs_;
/** Current time (can be advanced arbitrarily for testing). */
NetClock::time_point now_;
};
} // namespace xrpl::test

View File

@@ -0,0 +1,804 @@
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
#include <xrpl/protocol_autogen/transactions/Payment.h>
#include <xrpl/protocol_autogen/transactions/SetRegularKey.h>
#include <xrpl/protocol_autogen/transactions/SignerListSet.h>
#include <xrpl/protocol_autogen/transactions/TicketCreate.h>
#include <xrpl/protocol_autogen/transactions/TrustSet.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/IOU.h>
#include <helpers/TxTest.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::test {
TEST(AccountSet, NullAccountSet)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10));
auto& view = env.getOpenLedger();
// ask for the ledger entry - account root, to check its flags
auto sle = view.read(keylet::account(alice));
EXPECT_NE(sle, nullptr);
ledger_entries::AccountRoot const accountRoot(sle);
EXPECT_EQ(accountRoot.getFlags(), 0);
}
TEST(AccountSet, MostFlags)
{
Account const alice("alice");
TxTest env;
env.createAccount(alice, XRP(10000));
// Give alice a regular key so she can legally set and clear
// her asfDisableMaster flag.
Account const aliceRegularKey{"aliceRegularKey", KeyType::secp256k1};
env.createAccount(aliceRegularKey, XRP(10000));
env.close();
EXPECT_EQ(
env.submit(transactions::SetRegularKeyBuilder{alice}.setRegularKey(aliceRegularKey), alice)
.ter,
tesSUCCESS);
env.close();
auto testFlags = [&alice, &aliceRegularKey, &env](
std::initializer_list<std::uint32_t> goodFlags) {
std::uint32_t const orig_flags = env.getAccountRoot(alice).getFlags();
for (std::uint32_t flag{1u}; flag < std::numeric_limits<std::uint32_t>::digits; ++flag)
{
if (flag == asfNoFreeze)
{
// The asfNoFreeze flag can't be cleared. It is tested
// elsewhere.
continue;
}
if (flag == asfAuthorizedNFTokenMinter)
{
// The asfAuthorizedNFTokenMinter flag requires the
// presence or absence of the sfNFTokenMinter field in
// the transaction. It is tested elsewhere.
continue;
}
if (flag == asfDisallowIncomingCheck || flag == asfDisallowIncomingPayChan ||
flag == asfDisallowIncomingNFTokenOffer || flag == asfDisallowIncomingTrustline)
{
// These flags are part of the DisallowIncoming amendment
// and are tested elsewhere
continue;
}
if (flag == asfAllowTrustLineClawback)
{
// The asfAllowTrustLineClawback flag can't be cleared. It
// is tested elsewhere.
continue;
}
if (flag == asfAllowTrustLineLocking)
{
// These flags are part of the AllowTokenLocking amendment
// and are tested elsewhere
continue;
}
if (std::ranges::find(goodFlags, flag) != goodFlags.end())
{
// Good flag
EXPECT_FALSE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setClearFlag(flag),
aliceRegularKey)
.ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
std::uint32_t const now_flags = env.getAccountRoot(alice).getFlags();
EXPECT_EQ(now_flags, orig_flags);
}
else
{
// Bad flag
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
tesSUCCESS);
env.close();
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setClearFlag(flag),
aliceRegularKey)
.ter,
tesSUCCESS);
env.close();
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
}
}
};
testFlags({
asfRequireDest,
asfRequireAuth,
asfDisallowXRP,
asfGlobalFreeze,
asfDisableMaster,
asfDefaultRipple,
asfDepositAuth,
});
}
TEST(AccountSet, SetAndResetAccountTxnID)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
std::uint32_t const orig_flags = env.getAccountRoot(alice).getFlags();
// asfAccountTxnID is special and not actually set as a flag,
// so we check the field presence instead
EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfAccountTxnID), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasAccountTxnID());
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfAccountTxnID), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
std::uint32_t const now_flags = env.getAccountRoot(alice).getFlags();
EXPECT_EQ(now_flags, orig_flags);
}
TEST(AccountSet, SetNoFreeze)
{
TxTest env;
Account const alice("alice");
Account const eric("eric");
env.createAccount(alice, XRP(10000));
env.close();
// Set eric as alice's regular key (eric doesn't need to be funded)
EXPECT_EQ(
env.submit(transactions::SetRegularKeyBuilder{alice}.setRegularKey(eric), alice).ter,
tesSUCCESS);
env.close();
// Verify alice doesn't have NoFreeze flag
EXPECT_FALSE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
// Setting NoFreeze with regular key should fail - requires master key
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), eric).ter,
tecNEED_MASTER_KEY);
env.close();
// Setting NoFreeze with master key should succeed
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), alice).ter,
tesSUCCESS);
env.close();
// Verify alice now has NoFreeze flag
EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
// Try to clear NoFreeze - transaction succeeds but flag remains set
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfNoFreeze), alice).ter,
tesSUCCESS);
env.close();
// Verify flag is still set (NoFreeze cannot be cleared once set)
EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
}
TEST(AccountSet, Domain)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// The Domain field is represented as the hex string of the lowercase
// ASCII of the domain. For example, the domain example.com would be
// represented as "6578616d706c652e636f6d".
//
// To remove the Domain field from an account, send an AccountSet with
// the Domain set to an empty string.
std::string const domain = "example.com";
// Set domain
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain)), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasDomain());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain));
// Clear domain by setting empty
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setDomain(Slice{}), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasDomain());
// The upper limit on the length is 256 bytes
// (defined as DOMAIN_BYTES_MAX in SetAccount)
// test the edge cases: 255, 256, 257.
std::size_t const maxLength = 256;
for (std::size_t len = maxLength - 1; len <= maxLength + 1; ++len)
{
std::string const domain2 = std::string(len - domain.length() - 1, 'a') + "." + domain;
EXPECT_EQ(domain2.length(), len);
if (len <= maxLength)
{
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain2)), alice)
.ter,
tesSUCCESS);
env.close();
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain2));
}
else
{
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain2)), alice)
.ter,
telBAD_DOMAIN);
env.close();
}
}
}
TEST(AccountSet, MessageKey)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Generate a random ed25519 key pair for the message key
auto const rkp = randomKeyPair(KeyType::ed25519);
// Set the message key
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setMessageKey(rkp.first.slice()), alice)
.ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasMessageKey());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getMessageKey(), rkp.first.slice());
// Clear the message key by setting to empty
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setMessageKey(Slice{}), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasMessageKey());
// Try to set an invalid public key - should fail
using namespace std::string_literals;
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setMessageKey(
makeSlice("NOT_REALLY_A_PUBKEY"s)),
alice)
.ter,
telBAD_PUBLIC_KEY);
}
TEST(AccountSet, WalletID)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
std::string_view const locator =
"9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF05";
uint256 locatorHash{};
EXPECT_TRUE(locatorHash.parseHex(locator));
// Set the wallet locator
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setWalletLocator(locatorHash), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasWalletLocator());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getWalletLocator(), locatorHash);
// Clear the wallet locator by setting to zero
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setWalletLocator(beast::zero), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasWalletLocator());
}
TEST(AccountSet, EmailHash)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
std::string_view const mh = "5F31A79367DC3137FADA860C05742EE6";
uint128 emailHash{};
EXPECT_TRUE(emailHash.parseHex(mh));
// Set the email hash
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setEmailHash(emailHash), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasEmailHash());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getEmailHash(), emailHash);
// Clear the email hash by setting to zero
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setEmailHash(beast::zero), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasEmailHash());
}
TEST(AccountSet, TransferRate)
{
struct TestCase
{
double set;
TER code;
double get;
};
// Test data: {rate to set, expected TER, expected stored rate}
std::vector<TestCase> const testData = {
{1.0, tesSUCCESS, 1.0},
{1.1, tesSUCCESS, 1.1},
{2.0, tesSUCCESS, 2.0},
{2.1, temBAD_TRANSFER_RATE, 2.0}, // > 2.0 is invalid
{0.0, tesSUCCESS, 1.0}, // 0 clears the rate (default = 1.0)
{2.0, tesSUCCESS, 2.0},
{0.9, temBAD_TRANSFER_RATE, 2.0}, // < 1.0 is invalid
};
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
for (auto const& r : testData)
{
auto const rateValue = static_cast<std::uint32_t>(QUALITY_ONE * r.set);
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTransferRate(rateValue), alice)
.ter,
r.code);
env.close();
// If the field is not present, expect the default value (1.0)
if (!env.getAccountRoot(alice).hasTransferRate())
{
EXPECT_EQ(r.get, 1.0);
}
else
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(
*env.getAccountRoot(alice).getTransferRate(),
static_cast<std::uint32_t>(r.get * QUALITY_ONE));
}
}
}
TEST(AccountSet, BadInputs)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Setting and clearing the same flag is invalid
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfDisallowXRP)
.setClearFlag(asfDisallowXRP),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireAuth)
.setClearFlag(asfRequireAuth),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireDest)
.setClearFlag(asfRequireDest),
alice)
.ter,
temINVALID_FLAG);
// Setting asf flag while also using corresponding tf flag is invalid
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfDisallowXRP)
.setFlags(tfAllowXRP),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireAuth)
.setFlags(tfOptionalAuth),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireDest)
.setFlags(tfOptionalDestTag),
alice)
.ter,
temINVALID_FLAG);
// Using invalid flags (mask) is invalid
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireDest)
.setFlags(tfAccountSetMask),
alice)
.ter,
temINVALID_FLAG);
// Disabling master key without an alternative key is invalid
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfDisableMaster), alice).ter,
tecNO_ALTERNATIVE_KEY);
}
TEST(AccountSet, RequireAuthWithDir)
{
TxTest env;
Account const alice("alice");
Account const bob("bob");
env.createAccount(alice, XRP(10000));
env.close();
// alice should have an empty directory
EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
// Give alice a signer list, then there will be stuff in the directory
// Build the SignerEntries array
STArray signerEntries(1);
{
signerEntries.push_back(STObject::makeInnerObject(sfSignerEntry));
STObject& entry = signerEntries.back();
entry[sfAccount] = bob.id();
entry[sfSignerWeight] = std::uint16_t{1};
}
EXPECT_EQ(
env.submit(
transactions::SignerListSetBuilder{alice, 1}.setSignerEntries(signerEntries), alice)
.ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
// Setting RequireAuth should fail because alice has owner objects
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
tecOWNERS);
// Remove the signer list (quorum = 0, no entries)
EXPECT_EQ(env.submit(transactions::SignerListSetBuilder{alice, 0}, alice).ter, tesSUCCESS);
env.close();
EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
// Now setting RequireAuth should succeed
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
tesSUCCESS);
}
TEST(AccountSet, Ticket)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Get alice's current sequence - the ticket will be created at seq + 1
std::uint32_t const aliceSeqBefore = env.getAccountRoot(alice.id()).getSequence();
std::uint32_t const ticketSeq = aliceSeqBefore + 1;
// Create a ticket
EXPECT_EQ(env.submit(transactions::TicketCreateBuilder{alice, 1}, alice).ter, tesSUCCESS);
env.close();
// Verify alice has 1 owner object (the ticket)
EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 1u);
// Verify ticket exists
EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
// Try using a ticket that alice doesn't have
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq + 1), alice)
.ter,
terPRE_TICKET);
env.close();
// Verify ticket still exists
EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
// Get alice's sequence before using the ticket
std::uint32_t const aliceSeq = env.getAccountRoot(alice.id()).getSequence();
// Actually use alice's ticket (noop AccountSet)
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
tesSUCCESS);
env.close();
// Verify ticket is consumed (no owner objects)
EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 0u);
EXPECT_FALSE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
// Verify alice's sequence did NOT advance (ticket use doesn't increment seq)
EXPECT_EQ(env.getAccountRoot(alice.id()).getSequence(), aliceSeq);
// Try re-using a ticket that alice already used
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
tefNO_TICKET);
}
TEST(AccountSet, BadSigningKey)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Build a valid transaction first, then corrupt the signing key
auto stx = transactions::AccountSetBuilder{alice}
.setSequence(env.getAccountRoot(alice.id()).getSequence())
.setFee(XRPAmount{10})
.build(alice.pk(), alice.sk())
.getSTTx();
// Create a copy with a bad signing key
STObject obj = *stx;
obj.setFieldVL(sfSigningPubKey, makeSlice(std::string("badkey")));
auto result = env.submit(std::make_shared<STTx>(std::move(obj)));
EXPECT_EQ(result.ter, temBAD_SIGNATURE);
EXPECT_FALSE(result.applied);
}
TEST(AccountSet, Gateway)
{
Account const alice("alice");
Account const bob("bob");
Account const gw("gateway");
IOU const USD("USD", gw);
// Test gateway with a variety of allowed transfer rates
for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125)
{
TxTest env;
env.createAccount(gw, XRP(10000), asfDefaultRipple);
env.createAccount(alice, XRP(10000), asfDefaultRipple);
env.createAccount(bob, XRP(10000), asfDefaultRipple);
env.close();
// Set up trust lines: alice and bob trust gw for USD
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(USD.amount(10)), alice)
.ter,
tesSUCCESS);
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(USD.amount(10)), bob).ter,
tesSUCCESS);
env.close();
// Set transfer rate on the gateway
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{gw}.setTransferRate(
static_cast<std::uint32_t>(transferRate * QUALITY_ONE)),
gw)
.ter,
tesSUCCESS);
env.close();
// Calculate the amount with transfer rate applied
auto const amount = USD.amount(1);
Rate const rate(static_cast<std::uint32_t>(transferRate * QUALITY_ONE));
auto const amountWithRate = multiply(amount, rate);
// Gateway pays alice 10 USD
EXPECT_EQ(
env.submit(transactions::PaymentBuilder{gw, alice, USD.amount(10)}, gw).ter,
tesSUCCESS);
env.close();
// Alice pays bob 1 USD (with sendmax to cover transfer fee)
EXPECT_EQ(
env.submit(
transactions::PaymentBuilder{alice, bob, USD.amount(1)}.setSendMax(
USD.amount(10)),
alice)
.ter,
tesSUCCESS);
env.close();
// Check balances
EXPECT_EQ(env.getBalance(alice.id(), USD), USD.amount(10) - amountWithRate);
EXPECT_EQ(env.getBalance(bob.id(), USD), USD.amount(1));
}
// Test out-of-bounds legacy transfer rates (4.0 and 4.294967295)
// These require direct ledger modification since the transactor blocks them
for (std::uint32_t const transferRate : {4000000000U, 4294967295U})
{
TxTest env;
env.createAccount(gw, XRP(10000), asfDefaultRipple);
env.createAccount(alice, XRP(10000), asfDefaultRipple);
env.createAccount(bob, XRP(10000), asfDefaultRipple);
env.close();
// Set up trust lines
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(USD.amount(10)), alice)
.ter,
tesSUCCESS);
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(USD.amount(10)), bob).ter,
tesSUCCESS);
env.close();
// Set an acceptable transfer rate first (we'll hack it later)
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{gw}.setTransferRate(
static_cast<std::uint32_t>(2.0 * QUALITY_ONE)),
gw)
.ter,
tesSUCCESS);
env.close();
// Directly modify the ledger to set an out-of-bounds transfer rate
// This bypasses the transactor's validation
auto& view = env.getOpenLedger();
auto slePtr = view.read(keylet::account(gw.id()));
ASSERT_NE(slePtr, nullptr);
auto sleCopy = std::make_shared<SLE>(*slePtr);
(*sleCopy)[sfTransferRate] = transferRate;
view.rawReplace(sleCopy);
// Calculate the amount with the legacy transfer rate
auto const amount = USD.amount(1);
auto const amountWithRate = multiply(amount, Rate(transferRate));
// Gateway pays alice 10 USD
EXPECT_EQ(
env.submit(transactions::PaymentBuilder{gw, alice, USD.amount(10)}, gw).ter,
tesSUCCESS);
// Alice pays bob 1 USD
EXPECT_EQ(
env.submit(
transactions::PaymentBuilder{alice, bob, amount}.setSendMax(USD.amount(10)),
alice)
.ter,
tesSUCCESS);
// Check balances
EXPECT_EQ(env.getBalance(alice.id(), USD), USD.amount(10) - amountWithRate);
EXPECT_EQ(env.getBalance(bob.id(), USD), amount);
}
}
} // namespace xrpl::test

View File

@@ -0,0 +1,8 @@
#include <gtest/gtest.h>
int
main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}