Compare commits

..

33 Commits

Author SHA1 Message Date
Ed Hennis
4e064f57a9 Fix clang-tidy: const correctness 2026-04-28 15:54:10 -05:00
Ed Hennis
fee6f2c85f Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-28 16:23:22 -04:00
Ed Hennis
b37f1a0837 Fix build error: missing XRPL_ASSERT params 2026-04-26 23:19:11 -05:00
Ed Hennis
44e438b2f2 Merge remote-tracking branch 'XRPLF/develop' into ximinez/loanpay-assertion-develop
* XRPLF/develop:
  chore: Enable clang-tidy modernize-use-nodiscard check (7015)
  fix: Resolve MSVC Debug build failure in JobQueue.h; re-enable _CRTDBG_MAP_ALLOC in CI (6993)
  docs: Update hybrid offer invariant comment (7007)
  fix: Fix flaky CI tests (7005)
  docs: Update bug bounty information (7006)
  fix: Make assorted Payments fixes (6585)
  refactor: Move `LendingHelpers` into `libxrpl/ledger/helpers` (6638)
2026-04-25 13:43:53 -05:00
Ed Hennis
8efafbe993 Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-23 15:55:54 -04:00
Ed Hennis
2c6ad964dc Address AI code review: check rounding mode 2026-04-23 00:04:37 -04:00
Ed Hennis
9d8cede450 Update src/test/app/Loan_test.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 00:00:31 -04:00
Ed Hennis
2a019fde43 Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-22 23:39:34 -04:00
Ed Hennis
934e431af5 Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-22 14:48:59 -04:00
Ed Hennis
168b4dff5f Address AI review feedback: Avoid null derefs in test 2026-04-22 13:31:53 -04:00
Ed Hennis
1a97a6b24f Apply suggestions from AI code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 13:26:07 -04:00
Ed Hennis
ec1faeff0f Merge remote-tracking branch 'XRPLF/develop' into ximinez/loanpay-assertion-develop
* XRPLF/develop:
  chore: Optionally run clang-tidy via pre-commit (6680)
  refactor: Add transaction-specific invariant checking (6551)
  style: Add bashate pre-commit hook to unify bash style (6994)
2026-04-22 13:07:55 -04:00
Ed Hennis
94327d04ba clang-tidy fixes: Use std::ranges::minmax_element 2026-04-21 19:33:26 -04:00
Ed Hennis
afe8614d94 Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-21 18:48:16 -04:00
Ed Hennis
3b1b5ef522 Merge remote-tracking branch 'XRPLF/develop' into ximinez/loanpay-assertion-develop
* XRPLF/develop:
  chore: Remove empty Taker.h (6984)
  chore: Enable clang-tidy modernize checks (6975)
  ci: Upload clang-tidy git diff (6983)
  fix: Add rounding to Vault invariants (6217) (6955)
2026-04-21 15:04:10 -04:00
Ed Hennis
c22b38dd03 Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-20 17:49:31 -04:00
Ed Hennis
60984c19a3 Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-20 15:44:48 -04:00
Ed Hennis
068d8458bb Merge branch 'develop' into ximinez/loanpay-assertion-develop 2026-04-20 11:38:29 -04:00
Ed Hennis
4304b3acb4 Fix clang-tidy issues 2026-04-17 18:51:07 -04:00
Ed Hennis
29af96bfa1 Address copilot review feedback: correct sign of borrowerDelta 2026-04-17 18:46:50 -04:00
Ed Hennis
7944d03795 Merge remote-tracking branch 'upstream/develop' into ximinez/loanpay-assertion-develop
* upstream/develop: (30 commits)
  chore: Enable clang-tidy include cleaner (6947)
  fix: Change AMMClawback return code to tecNO_PERMISSION (6946)
  ci: [DEPENDABOT] bump actions/upload-pages-artifact from 4.0.0 to 5.0.0 (6927)
  ci: [DEPENDABOT] bump actions/upload-artifact from 7.0.0 to 7.0.1 (6928)
  chore: Enable clang-tidy readability checks (6930)
  fix: Fix unity build for book step (6942)
  chore: Move codegen venv setup into build stage (6617)
  chore: Enable most clang-tidy bugprone checks (6929)
  refactor: Improve exception handling (6540) (6735)
  refactor: Remove unused notTooManyOffers function from NFTokenUtils (6737)
  fix: Change `Tuning::bookOffers` minimum limit to 1 (6812)
  chore: Make pre-commit line ending conversions work on Windows (6832) (6833)
  fix: Add description for `terLOCKED` error (6811)
  fix: Address AI reviewer comments for Permission Delegation (6675)
  refactor: Combine `AMMHelpers` and `AMMUtils` (6733)
  feat: Add MPT support to DEX (5285)
  fix: Handle WSClient write failure when server closes WebSocket (6671)
  ci: Change conditions for uploading artifacts in public/private/org repos (6734)
  refactor: Rename non-functional uses of `ripple(d)` to `xrpl(d)` (6676)
  refactor: Move more helper files into `libxrpl/ledger/helpers` (6731)
  ...
2026-04-17 16:13:26 -04:00
Ed Hennis
b4b47e80ba Merge branch 'develop' into ximinez/loanpay-assertion 2026-04-01 13:39:09 -04:00
Ed Hennis
c8e1d02dee Merge branch 'develop' into ximinez/loanpay-assertion 2026-04-01 11:45:36 -04:00
Ed Hennis
f68b3326df Review feedback from @tapanito
- Return a default value in LoanPay balanceScale if the exponent list is
  empty.
- Amendment gate the "roundedAmount" change in overpayment.
- Improve comments and logging.
documentation
2026-03-31 21:02:28 -04:00
Ed Hennis
da1cb2d7ce Merge remote-tracking branch 'upstream/develop' into ximinez/loanpay-assertion
* upstream/develop: (149 commits)
  fix: Fix previous ledger size typo in RCLConsensus (6696)
  chore: Enable clang-tidy misc checks (6655)
  ci: Use pull_request_target to check for signed commits (6697)
  chore: Remove unnecessary clang-format off/on directives (6682)
  fix: Fix Workers::stop() race between m_allPaused and m_runningTaskCount (6574)
  ci: Only publish docs in public repos (6687)
  chore: Enable remaining clang-tidy `performance` checks (6648)
  refactor: Address PR comments after the modularisation PRs (6389)
  chore: Fix clang-tidy header filter (6686)
  ci: [DEPENDABOT] bump actions/deploy-pages from 4.0.5 to 5.0.0 (6684)
  ci: [DEPENDABOT] bump codecov/codecov-action from 5.5.3 to 6.0.0 (6685)
  fix: Guard Coro::resume() against completed coroutines (6608)
  refactor: Split LoanInvariant into LoanBrokerInvariant and LoanInvariant (6674)
  ci: Don't publish docs on release branches (6673)
  refactor: Make function naming in ServiceRegistry consistent (6390)
  chore: Shorten job names to stay within Linux 15-char thread limit (6669)
  fix: Improve loan invariant message (6668)
  ci: Upload artifacts only in public repositories (6670)
  ci: Add conflicting-pr workflow (6656)
  chore: Add more AI tools to .gitignore (6658)
  ...
2026-03-31 18:54:27 -04:00
Ed Hennis
0797a5d4db Address an outdated TODO comment 2026-03-31 18:53:10 -04:00
Ed Hennis
d33ee7ed46 Merge commit '2c1fad1023' into HEAD
* commit '2c1fad1023':
  chore: Apply clang-format width 100 (6387)
2026-03-31 18:53:02 -04:00
Ed Hennis
53183c71e8 Update formatting 2026-03-31 18:44:32 -04:00
Ed Hennis
05eaef5233 Merge commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c' into HEAD
* commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c': (22 commits)
  chore: Set clang-format width to 100 in config file (6387)
  chore: Set cmake-format width to 100 (6386)
  ci: Add clang tidy workflow to ci (6369)
  refactor: Modularize app/tx (6228)
  refactor: Decouple app/tx from `Application` and `Config` (6227)
  chore: Update clang-format to 21.1.8 (6352)
  refactor: Modularize `HashRouter`, `Conditions`, and `OrderBookDB` (6226)
  chore: Fix minor issues in comments (6346)
  refactor: Modularize the NetworkOPs interface (6225)
  chore: Fix `gcov` lib coverage build failure on macOS (6350)
  refactor: Modularize RelationalDB (6224)
  refactor: Modularize WalletDB and Manifest (6223)
  fix: Update invariant checks for Permissioned Domains (6134)
  refactor: Change main thread name to `xrpld-main` (6336)
  refactor: Fix spelling issues in tests (6199)
  test: Add file and line location to Env (6276)
  chore: Remove CODEOWNERS (6337)
  perf: Remove unnecessary caches (5439)
  chore: Restore unity builds (6328)
  refactor: Update secp256k1 to 0.7.1 (6331)
  ...
2026-03-31 18:42:49 -04:00
Ed Hennis
e44fc46ad0 Verify and log LoanPay fund conservation in all builds
- A warning will be logged if there's a mismatch.
2026-03-31 18:22:16 -04:00
Ed Hennis
3f8f5a081f Review feedback from @tapanito: readability, duplicate checks 2026-03-31 18:22:16 -04:00
Ed Hennis
14236fb767 Fix formatting 2026-02-04 17:26:45 -05:00
Ed Hennis
c0b6712064 Fix touchy "funds are conserved" assertion in LoanPay
- Add "Yield Theft via Rounding Manipulation" test, which used to
  reliably triggered it. The test now verifies that no "yield theft"
  occurs.
2026-02-04 17:18:10 -05:00
35 changed files with 372 additions and 1313 deletions

View File

@@ -4,15 +4,14 @@ 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[str, Any]]
os: list[dict[str, Any]]
architecture: list[dict]
os: list[dict]
build_type: list[str]
cmake_args: list[str]
@@ -33,52 +32,7 @@ We will further set additional CMake arguments as follows:
"""
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]]:
def generate_strategy_matrix(all: bool, config: Config) -> list:
configurations = []
for architecture, os, build_type, cmake_args in itertools.product(
config.architecture, config.os, config.build_type, config.cmake_args
@@ -146,15 +100,14 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict[str, Any]]:
continue
# RHEL:
# - 9 using GCC 12: Debug and Release on linux/amd64
# (Release is required for RPM packaging).
# - 9 using GCC 12: Debug on linux/amd64.
# - 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 in ["Debug", "Release"]
and build_type == "Debug"
and architecture["platform"] == "linux/amd64"
):
skip = False
@@ -169,8 +122,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict[str, Any]]:
continue
# Ubuntu:
# - Jammy using GCC 12: Debug on linux/arm64, Release on
# linux/amd64 (Release is required for DEB packaging).
# - Jammy using GCC 12: Debug on linux/arm64.
# - Noble using GCC 14: Release on linux/amd64.
# - Noble using Clang 18: Debug on linux/amd64.
# - Noble using Clang 19: Release on linux/arm64.
@@ -183,12 +135,6 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict[str, Any]]:
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"
@@ -270,7 +216,17 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict[str, Any]]:
# Generate a unique name for the configuration, e.g. macos-arm64-debug
# or debian-bookworm-gcc-12-amd64-release.
config_name = build_config_name(os, architecture, build_type)
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()}"
if "-Dcoverage=ON" in cmake_args:
config_name += "-coverage"
if "-Dunity=ON" in cmake_args:
@@ -374,19 +330,10 @@ 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.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 == "":
if args.config is None or args.config == "":
matrix += generate_strategy_matrix(
args.all, read_config(THIS_DIR / "linux.json")
)

View File

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

View File

@@ -67,7 +67,6 @@ 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
@@ -82,8 +81,6 @@ 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
@@ -140,30 +137,6 @@ jobs:
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
generate-version:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
package:
needs: [should-run, build-test, generate-version]
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-package.yml
with:
version: ${{ needs.generate-version.outputs.version }}
upload-recipe:
needs:
- should-run

View File

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

View File

@@ -24,7 +24,6 @@ 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"
@@ -39,7 +38,6 @@ 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
@@ -100,24 +98,3 @@ 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 }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,160 +0,0 @@
# 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"
```

View File

@@ -1,134 +0,0 @@
#!/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}"

View File

@@ -1,23 +0,0 @@
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.

View File

@@ -1,20 +0,0 @@
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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
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

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
[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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
#!/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

View File

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

View File

@@ -1857,8 +1857,18 @@ loanMakePayment(
// -------------------------------------------------------------
// overpayment handling
//
// If the "fixSecurity3_1_3" amendment is enabled, truncate "amount",
// at the loan scale. If the raw value is used, the overpayment
// amount could be meaningless dust. Trying to process such a small
// amount will, at best, waste time when all the result values round
// to zero. At worst, it can cause logical errors with tiny amounts
// of interest that don't add up correctly.
auto const roundedAmount = view.rules().enabled(fixSecurity3_1_3)
? roundToAsset(asset, amount, loanScale, Number::towards_zero)
: amount;
if (paymentType == LoanPaymentType::overpayment && loan->isFlag(lsfLoanOverpayment) &&
paymentRemainingProxy > 0 && totalPaid < amount &&
paymentRemainingProxy > 0 && totalPaid < roundedAmount &&
numPayments < loanMaximumPaymentsPerTransaction)
{
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
@@ -1867,7 +1877,7 @@ loanMakePayment(
// It shouldn't be possible for the overpayment to be greater than
// totalValueOutstanding, because that would have been processed as
// another normal payment. But cap it just in case.
Number const overpayment = std::min(amount - totalPaid, *totalValueOutstandingProxy);
Number const overpayment = std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
detail::ExtendedPaymentComponents const overpaymentComponents =
detail::computeOverpaymentComponents(

View File

@@ -30,6 +30,7 @@
#include <bit>
#include <cstdint>
#include <memory>
#include <vector>
namespace xrpl {
@@ -143,6 +144,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
Number const numPaymentEstimate = static_cast<std::int64_t>(amount / regularPayment);
// Charge one base fee per paymentsPerFeeIncrement payments, rounding up.
// This set round is safe because there's a mode guard just above
Number::setround(Number::upward);
auto const feeIncrements = std::max(
std::int64_t(1),
@@ -440,9 +442,10 @@ LoanPay::doApply()
// Vault object state changes
view.update(vaultSle);
Number const assetsAvailableBefore = *assetsAvailableProxy;
Number const assetsTotalBefore = *assetsTotalProxy;
#if !NDEBUG
{
Number const assetsAvailableBefore = *assetsAvailableProxy;
Number const pseudoAccountBalanceBefore = accountHolds(
view,
vaultPseudoAccount,
@@ -466,16 +469,6 @@ LoanPay::doApply()
"xrpl::LoanPay::doApply",
"assets available must not be greater than assets outstanding");
if (*assetsAvailableProxy > *assetsTotalProxy)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Vault assets available must not be greater "
"than assets outstanding. Available: "
<< *assetsAvailableProxy << ", Total: " << *assetsTotalProxy;
return tecINTERNAL;
// LCOV_EXCL_STOP
}
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
<< ", total paid to vault rounded: " << totalPaidToVaultRounded
<< ", total paid to broker: " << totalPaidToBroker
@@ -501,12 +494,68 @@ LoanPay::doApply()
associateAsset(*vaultSle, asset);
// Duplicate some checks after rounding
Number const assetsAvailableAfter = *assetsAvailableProxy;
Number const assetsTotalAfter = *assetsTotalProxy;
XRPL_ASSERT_PARTS(
*assetsAvailableProxy <= *assetsTotalProxy,
assetsAvailableAfter <= assetsTotalAfter,
"xrpl::LoanPay::doApply",
"assets available must not be greater than assets outstanding");
if (assetsAvailableAfter == assetsAvailableBefore)
{
// An unchanged assetsAvailable indicates that the amount paid to the
// vault was zero, or rounded to zero. That should be impossible, but I
// can't rule it out for extreme edge cases, so fail gracefully if it
// happens.
//
// LCOV_EXCL_START
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
<< "Before: " << assetsAvailableBefore //
<< ", After: " << assetsAvailableAfter;
return tecPRECISION_LOSS;
// LCOV_EXCL_STOP
}
if (paymentParts->valueChange != beast::zero && assetsTotalAfter == assetsTotalBefore)
{
// Non-zero valueChange with an unchanged assetsTotal indicates that the
// actual value change rounded to zero. That should be impossible, but I
// can't rule it out for extreme edge cases, so fail gracefully if it
// happens.
//
// LCOV_EXCL_START
JLOG(j_.warn())
<< "LoanPay: Vault assets expected change, but unchanged after rounding: " //
<< "Before: " << assetsTotalBefore //
<< ", After: " << assetsTotalAfter //
<< ", ValueChange: " << paymentParts->valueChange;
return tecPRECISION_LOSS;
// LCOV_EXCL_STOP
}
if (paymentParts->valueChange == beast::zero && assetsTotalAfter != assetsTotalBefore)
{
// A change in assetsTotal when there was no valueChange indicates that
// something really weird happened. That should be flat out impossible.
//
// LCOV_EXCL_START
JLOG(j_.fatal()) << "LoanPay: Vault assets changed unexpectedly after rounding: " //
<< "Before: " << assetsTotalBefore //
<< ", After: " << assetsTotalAfter //
<< ", ValueChange: " << paymentParts->valueChange;
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (assetsAvailableAfter > assetsTotalAfter)
{
// Assets available are not allowed to be larger than assets total.
// LCOV_EXCL_START
JLOG(j_.fatal()) << "LoanPay: Vault assets available must not be greater "
"than assets outstanding. Available: "
<< assetsAvailableAfter << ", Total: " << assetsTotalAfter;
return tecINTERNAL;
// LCOV_EXCL_STOP
}
#if !NDEBUG
// These three values are used to check that funds are conserved after the transfers
auto const accountBalanceBefore = accountHolds(
view,
account_,
@@ -535,7 +584,6 @@ LoanPay::doApply()
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
#endif
if (totalPaidToVaultRounded != beast::zero)
{
@@ -571,19 +619,22 @@ LoanPay::doApply()
return ter;
#if !NDEBUG
Number const assetsAvailableAfter = *assetsAvailableProxy;
Number const pseudoAccountBalanceAfter = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
XRPL_ASSERT_PARTS(
assetsAvailableAfter == pseudoAccountBalanceAfter,
"xrpl::LoanPay::doApply",
"vault pseudo balance agrees after");
{
Number const pseudoAccountBalanceAfter = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
XRPL_ASSERT_PARTS(
assetsAvailableAfter == pseudoAccountBalanceAfter,
"xrpl::LoanPay::doApply",
"vault pseudo balance agrees after");
}
#endif
// Check that funds are conserved
auto const accountBalanceAfter = accountHolds(
view,
account_,
@@ -612,14 +663,121 @@ LoanPay::doApply()
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
auto const balanceScale = [&]() {
// Find a reasonable scale to use for the balance comparisons.
//
// First find the minimum and maximum exponent of all the non-zero balances, before and
// after. If min and max are equal, use that value. If they are not, use "max + 1" to reduce
// rounding discrepancies without making the result meaningless. Cap the scale at
// STAmount::cMaxOffset, just in case the numbers are all very large.
std::vector<int> exponents;
for (auto const& a : {
accountBalanceBefore,
vaultBalanceBefore,
brokerBalanceBefore,
accountBalanceAfter,
vaultBalanceAfter,
brokerBalanceAfter,
})
{
// Exclude zeroes
if (a != beast::zero)
exponents.push_back(a.exponent());
}
if (exponents.empty())
{
UNREACHABLE("xrpl::LoanPay::doApply : all zeroes");
return 0;
}
auto const [minItr, maxItr] = std::ranges::minmax_element(exponents);
auto const min = *minItr;
auto const max = *maxItr;
JLOG(j_.trace()) << "Min scale: " << min << ", max scale: " << max;
// IOU rounding can be interesting. We want all the balance checks to agree, but don't want
// to round to such an extreme that it becomes meaningless. e.g. Everything rounds to one
// digit. So add 1 to the max (reducing the number of digits after the decimal point by 1)
// if the scales are not already all the same.
return std::min(min == max ? max : max + 1, STAmount::cMaxOffset);
}();
// No object changes are made below this point
XRPL_ASSERT_PARTS(
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore ==
accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter,
Number::getround() == Number::to_nearest,
"xrpl::LoanPay::doApply",
"Number rounding to_nearest");
NumberRoundModeGuard const mg(Number::to_nearest);
auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
auto const vaultBalanceBeforeRounded = roundToScale(vaultBalanceBefore, balanceScale);
auto const brokerBalanceBeforeRounded = roundToScale(brokerBalanceBefore, balanceScale);
auto const totalBalanceBefore = accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore;
auto const totalBalanceBeforeRounded = roundToScale(totalBalanceBefore, balanceScale);
JLOG(j_.trace()) << "Before: " //
<< "account " << Number(accountBalanceBeforeRounded) << " ("
<< Number(accountBalanceBefore) << ")"
<< ", vault " << Number(vaultBalanceBeforeRounded) << " ("
<< Number(vaultBalanceBefore) << ")"
<< ", broker " << Number(brokerBalanceBeforeRounded) << " ("
<< Number(brokerBalanceBefore) << ")"
<< ", total " << Number(totalBalanceBeforeRounded) << " ("
<< Number(totalBalanceBefore) << ")";
auto const accountBalanceAfterRounded = roundToScale(accountBalanceAfter, balanceScale);
auto const vaultBalanceAfterRounded = roundToScale(vaultBalanceAfter, balanceScale);
auto const brokerBalanceAfterRounded = roundToScale(brokerBalanceAfter, balanceScale);
auto const totalBalanceAfter = accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter;
auto const totalBalanceAfterRounded = roundToScale(totalBalanceAfter, balanceScale);
JLOG(j_.trace()) << "After: " //
<< "account " << Number(accountBalanceAfterRounded) << " ("
<< Number(accountBalanceAfter) << ")"
<< ", vault " << Number(vaultBalanceAfterRounded) << " ("
<< Number(vaultBalanceAfter) << ")"
<< ", broker " << Number(brokerBalanceAfterRounded) << " ("
<< Number(brokerBalanceAfter) << ")"
<< ", total " << Number(totalBalanceAfterRounded) << " ("
<< Number(totalBalanceAfter) << ")";
auto const accountBalanceChange = accountBalanceAfter - accountBalanceBefore;
auto const vaultBalanceChange = vaultBalanceAfter - vaultBalanceBefore;
auto const brokerBalanceChange = brokerBalanceAfter - brokerBalanceBefore;
auto const totalBalanceChange = accountBalanceChange + vaultBalanceChange + brokerBalanceChange;
auto const totalBalanceChangeRounded = roundToScale(totalBalanceChange, balanceScale);
JLOG(j_.trace()) << "Changes: " //
<< "account " << to_string(accountBalanceChange) //
<< ", vault " << to_string(vaultBalanceChange) //
<< ", broker " << to_string(brokerBalanceChange) //
<< ", total " << to_string(totalBalanceChangeRounded) << " ("
<< Number(totalBalanceChange) << ")";
if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
{
JLOG(j_.warn()) << "Total rounded balances don't match"
<< (totalBalanceChangeRounded == beast::zero ? ", but total changes do"
: "");
}
if (totalBalanceChangeRounded != beast::zero)
{
JLOG(j_.warn()) << "Total balance changes don't match"
<< (totalBalanceBeforeRounded == totalBalanceAfterRounded
? ", but total balances do"
: "");
}
// Rounding for IOUs can be weird, so check a few different ways to show
// that funds are conserved.
XRPL_ASSERT_PARTS(
totalBalanceBeforeRounded == totalBalanceAfterRounded ||
totalBalanceChangeRounded == beast::zero,
"xrpl::LoanPay::doApply",
"funds are conserved (with rounding)");
XRPL_ASSERT_PARTS(
accountBalanceAfter >= beast::zero, "xrpl::LoanPay::doApply", "positive account balance");
XRPL_ASSERT_PARTS(
accountBalanceAfter < accountBalanceBefore || account_ == asset.getIssuer(),
"xrpl::LoanPay::doApply",
@@ -640,7 +798,6 @@ LoanPay::doApply()
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
"xrpl::LoanPay::doApply",
"vault and/or broker balance increased");
#endif
return tesSUCCESS;
}

View File

@@ -370,16 +370,11 @@ protected:
env.balance(vaultPseudo, broker.asset).number());
if (ownerCount == 0)
{
// Allow some slop for rounding IOUs
// TODO: This needs to be an exact match once all the
// other rounding issues are worked out.
// The Vault must be perfectly balanced if there
// are no loans outstanding
auto const total = vaultSle->at(sfAssetsTotal);
auto const available = vaultSle->at(sfAssetsAvailable);
env.test.BEAST_EXPECT(
total == available ||
(!broker.asset.integral() && available != 0 &&
((total - available) / available < Number(1, -6))));
env.test.BEAST_EXPECT(total == available);
env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
}
}
@@ -7068,6 +7063,140 @@ protected:
BEAST_EXPECT(afterSecondCoverAvailable == 0);
}
void
testYieldTheftRounding(std::uint32_t flags)
{
testcase("Rounding manipulation does not permit yield theft");
using namespace jtx;
using namespace loan;
// 1. Setup Environment
Env env(*this, all);
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1000), issuer, lender, borrower);
env.close();
// 2. Asset Selection
PrettyAsset const iou = issuer["USD"];
env(trust(lender, iou(100'000'000)));
env(trust(borrower, iou(100'000'000)));
env(pay(issuer, lender, iou(100'000'000)));
env(pay(issuer, borrower, iou(100'000'000)));
env.close();
// 3. Create Vault and Broker with High Debt Limit (100M)
auto const brokerInfo = createVaultAndBroker(
env,
iou,
lender,
{
.vaultDeposit = 5'000'000,
.debtMax = Number{100'000'000},
.coverDeposit = 500'000,
});
auto const [currentSeq, vaultKeylet] = [&]() {
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const currentSeq = brokerSle->at(sfLoanSequence);
auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID));
return std::make_tuple(currentSeq, vaultKeylet);
}();
// 4. Loan Parameters (Attack Vector)
Number const principal = 1'000'000;
TenthBips32 const interestRate = TenthBips32{1}; // 0.001%
std::uint32_t const paymentInterval = 86400;
std::uint32_t const paymentTotal = 3650;
auto const loanSetFee = fee(env.current()->fees().base * 2);
env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags),
sig(sfCounterpartySignature, lender),
loan::interestRate(interestRate),
loan::paymentInterval(paymentInterval),
loan::paymentTotal(paymentTotal),
fee(loanSetFee));
env.close();
// --- RETRIEVE OBJECTS & SETUP ATTACK ---
auto borrowerBalance = [&]() { return env.balance(borrower, iou); };
auto const borrowerScale = static_cast<STAmount const&>(borrowerBalance()).exponent();
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq);
auto const maybePeriodicPayment = [&]() -> std::optional<STAmount> {
auto const loanSle = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSle))
return std::nullopt;
// Construct Payment
return STAmount{iou, loanSle->at(sfPeriodicPayment)};
}();
if (!maybePeriodicPayment)
return;
auto const periodicPayment = *maybePeriodicPayment;
auto const roundedPayment = roundToScale(periodicPayment, borrowerScale, Number::upward);
// ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution
STAmount const paymentBuffer{iou, Number(1, -9)};
STAmount const attackPayment = periodicPayment + paymentBuffer;
auto const maybeInitialVaultAssets = [&]() -> std::optional<Number> {
auto const vault = env.le(vaultKeylet);
if (!BEAST_EXPECT(vault))
return std::nullopt;
return vault->at(sfAssetsTotal);
}();
if (!maybeInitialVaultAssets)
return;
auto const initialVaultAssets = *maybeInitialVaultAssets;
// 5. Execution Loop
int yieldTheftCount = 0;
auto previousAssetsTotal = initialVaultAssets;
for (int i = 0; i < 100; ++i)
{
auto const balanceBefore = borrowerBalance();
env(pay(borrower, loanKeylet.key, attackPayment, flags));
env.close();
auto const borrowerDelta = balanceBefore - borrowerBalance();
BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum());
auto const loanSle = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSle))
break;
auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)};
BEAST_EXPECT(
(roundToScale(updatedPayment, borrowerScale, Number::upward) == roundedPayment));
BEAST_EXPECT(
(updatedPayment == periodicPayment) ||
(flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment));
auto const currentVaultSle = env.le(vaultKeylet);
if (!BEAST_EXPECT(currentVaultSle))
break;
auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal);
auto const delta = currentAssetsTotal - previousAssetsTotal;
BEAST_EXPECT(
(delta == beast::zero && borrowerDelta <= roundedPayment) ||
(delta > beast::zero && borrowerDelta > roundedPayment));
// If tx succeeded but Assets Total didn't change, interest was
// stolen.
if (delta == beast::zero && borrowerDelta > roundedPayment)
{
yieldTheftCount++;
}
previousAssetsTotal = currentAssetsTotal;
}
BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount));
}
// Tests that vault withdrawals work correctly when the vault has unrealized
// loss from an impaired loan, ensuring the invariant check properly
// accounts for the loss.
@@ -7206,6 +7335,11 @@ public:
testLoanPayLateFullPaymentBypassesPenalties();
testLoanCoverMinimumRoundingExploit();
#endif
for (auto const flags : {0u, tfLoanOverpayment})
{
testYieldTheftRounding(flags);
}
testWithdrawReflectsUnrealizedLoss();
testInvalidLoanSet();