mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Compare commits
21 Commits
gregtatcam
...
legleux/li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb5354e4fe | ||
|
|
19da83abf3 | ||
|
|
8a02c904a6 | ||
|
|
e7ca96362f | ||
|
|
4593754347 | ||
|
|
2caa18ddd5 | ||
|
|
8dbf9b8ab4 | ||
|
|
94ef68090c | ||
|
|
8fa81e24ed | ||
|
|
4e1c3b0922 | ||
|
|
f148d9a4e7 | ||
|
|
453e3efcc9 | ||
|
|
8b4ff1c171 | ||
|
|
c33630f859 | ||
|
|
94103e97d7 | ||
|
|
cfcb09933a | ||
|
|
e36ba02762 | ||
|
|
fe47e5adb2 | ||
|
|
96a6922bce | ||
|
|
7721bda445 | ||
|
|
46b997b774 |
@@ -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
|
||||
|
||||
89
.github/scripts/strategy-matrix/generate.py
vendored
89
.github/scripts/strategy-matrix/generate.py
vendored
@@ -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")
|
||||
)
|
||||
|
||||
6
.github/scripts/strategy-matrix/linux.json
vendored
6
.github/scripts/strategy-matrix/linux.json
vendored
@@ -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",
|
||||
|
||||
27
.github/workflows/on-pr.yml
vendored
27
.github/workflows/on-pr.yml
vendored
@@ -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
|
||||
|
||||
40
.github/workflows/on-tag.yml
vendored
40
.github/workflows/on-tag.yml
vendored
@@ -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 }}
|
||||
|
||||
23
.github/workflows/on-trigger.yml
vendored
23
.github/workflows/on-trigger.yml
vendored
@@ -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
91
.github/workflows/reusable-package.yml
vendored
Normal 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
|
||||
@@ -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}"
|
||||
|
||||
@@ -134,6 +134,7 @@ endif()
|
||||
include(XrplCore)
|
||||
include(XrplProtocolAutogen)
|
||||
include(XrplInstall)
|
||||
include(XrplPackaging)
|
||||
include(XrplValidatorKeys)
|
||||
|
||||
if(tests)
|
||||
|
||||
@@ -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
136
cmake/XrplPackaging.cmake
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
160
package/README.md
Normal 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
134
package/build_pkg.sh
Executable 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
23
package/debian/control
Normal 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
20
package/debian/copyright
Normal 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
34
package/debian/rules
Normal 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:
|
||||
@:
|
||||
1
package/debian/source/format
Normal file
1
package/debian/source/format
Normal file
@@ -0,0 +1 @@
|
||||
3.0 (quilt)
|
||||
2
package/debian/xrpld.conffiles
Normal file
2
package/debian/xrpld.conffiles
Normal file
@@ -0,0 +1,2 @@
|
||||
/opt/xrpld/etc/xrpld.cfg
|
||||
/opt/xrpld/etc/validators.txt
|
||||
10
package/debian/xrpld.install
Normal file
10
package/debian/xrpld.install
Normal 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
|
||||
10
package/debian/xrpld.links
Normal file
10
package/debian/xrpld.links
Normal 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
111
package/rpm/xrpld.spec
Normal 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
|
||||
4
package/shared/50-xrpld.preset
Normal file
4
package/shared/50-xrpld.preset
Normal 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
|
||||
9
package/shared/update-xrpld-cron
Normal file
9
package/shared/update-xrpld-cron
Normal 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
|
||||
16
package/shared/update-xrpld.service
Normal file
16
package/shared/update-xrpld.service
Normal 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
145
package/shared/update-xrpld.sh
Executable 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 "$@"
|
||||
10
package/shared/update-xrpld.timer
Normal file
10
package/shared/update-xrpld.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Daily xrpld update check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 00:00:00
|
||||
RandomizedDelaySec=1h
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
package/shared/xrpld.logrotate
Normal file
14
package/shared/xrpld.logrotate
Normal 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
|
||||
}
|
||||
22
package/shared/xrpld.service
Normal file
22
package/shared/xrpld.service
Normal 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
|
||||
1
package/shared/xrpld.sysusers
Normal file
1
package/shared/xrpld.sysusers
Normal file
@@ -0,0 +1 @@
|
||||
u xrpld - "XRP Ledger daemon" /var/lib/xrpld /sbin/nologin
|
||||
2
package/shared/xrpld.tmpfiles
Normal file
2
package/shared/xrpld.tmpfiles
Normal file
@@ -0,0 +1,2 @@
|
||||
d /var/lib/xrpld 0750 xrpld xrpld -
|
||||
d /var/log/xrpld 0750 xrpld xrpld -
|
||||
45
package/test/check_install_paths.sh
Executable file
45
package/test/check_install_paths.sh
Executable 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
91
package/test/smoketest.sh
Executable 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"
|
||||
@@ -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)
|
||||
|
||||
19
src/tests/libxrpl/helpers/Account.cpp
Normal file
19
src/tests/libxrpl/helpers/Account.cpp
Normal 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
|
||||
81
src/tests/libxrpl/helpers/Account.h
Normal file
81
src/tests/libxrpl/helpers/Account.h
Normal 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
|
||||
132
src/tests/libxrpl/helpers/IOU.h
Normal file
132
src/tests/libxrpl/helpers/IOU.h
Normal 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
|
||||
111
src/tests/libxrpl/helpers/TestFamily.h
Normal file
111
src/tests/libxrpl/helpers/TestFamily.h
Normal 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
|
||||
378
src/tests/libxrpl/helpers/TestServiceRegistry.h
Normal file
378
src/tests/libxrpl/helpers/TestServiceRegistry.h
Normal 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
|
||||
252
src/tests/libxrpl/helpers/TxTest.cpp
Normal file
252
src/tests/libxrpl/helpers/TxTest.cpp
Normal 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
|
||||
364
src/tests/libxrpl/helpers/TxTest.h
Normal file
364
src/tests/libxrpl/helpers/TxTest.h
Normal 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
|
||||
804
src/tests/libxrpl/tx/AccountSet.cpp
Normal file
804
src/tests/libxrpl/tx/AccountSet.cpp
Normal 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
|
||||
8
src/tests/libxrpl/tx/main.cpp
Normal file
8
src/tests/libxrpl/tx/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in New Issue
Block a user