Compare commits

..

4 Commits

Author SHA1 Message Date
Mayukha Vadari
537c520e32 Merge branch 'develop' into mvadari/test-debugging 2026-06-24 20:51:37 -04:00
Mayukha Vadari
310bfc7b94 fix: Improve test debuggability 2026-06-24 20:44:47 -04:00
Mayukha Vadari
afc0b7ab8c add build errors 2026-06-24 20:14:13 -04:00
Michael Legleux
556d62a0de build: Align xrpld RPM packaging with DEB package (#7529) 2026-06-24 23:53:46 +00:00
36 changed files with 300 additions and 2619 deletions

View File

@@ -206,6 +206,6 @@ CheckOptions:
readability-identifier-naming.PublicMemberSuffix: ""
readability-identifier-naming.GlobalFunctionIgnoredRegexp: "^(to_string|hash_append|tuple_hash)$"
HeaderFilterRegex: '^.*/(tests?|xrpl|xrpld|validator-keys-tool)/.*\.(h|hpp|ipp)$'
HeaderFilterRegex: '^.*/(tests?|xrpl|xrpld)/.*\.(h|hpp|ipp)$'
ExcludeHeaderFilterRegex: '^.*/protocol_autogen/.*\.(h|hpp)$'
WarningsAsErrors: "*"

View File

@@ -50,8 +50,7 @@
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"extra_cmake_args": "-Dvalidator_keys=ON"
"arch": ["amd64"]
}
],
@@ -59,8 +58,7 @@
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"extra_cmake_args": "-Dvalidator_keys=ON"
"arch": ["amd64"]
}
]
},

View File

@@ -67,7 +67,6 @@ jobs:
.github/workflows/reusable-package.yml
.github/workflows/reusable-strategy-matrix.yml
.github/workflows/reusable-test.yml
.github/workflows/reusable-test-conan-package.yml
.github/workflows/reusable-upload-recipe.yml
.clang-tidy
.codecov.yml
@@ -79,7 +78,6 @@ jobs:
include/**
src/**
tests/**
validator-keys-tool/**
CMakeLists.txt
conanfile.py
conan.lock
@@ -148,16 +146,10 @@ jobs:
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-package.yml
test-conan-package:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-test-conan-package.yml
upload-recipe:
needs:
- should-run
- build-test
- test-conan-package
# Only run when committing to a PR that targets a release branch.
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
uses: ./.github/workflows/reusable-upload-recipe.yml
@@ -189,7 +181,6 @@ jobs:
- clang-tidy
- build-test
- package
- test-conan-package
- upload-recipe
- notify-clio
runs-on: ubuntu-latest

View File

@@ -16,13 +16,8 @@ defaults:
shell: bash
jobs:
test-conan-package:
if: ${{ github.repository == 'XRPLF/rippled' }}
uses: ./.github/workflows/reusable-test-conan-package.yml
upload-recipe:
if: ${{ github.repository == 'XRPLF/rippled' }}
needs: test-conan-package
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}

View File

@@ -24,7 +24,6 @@ on:
- ".github/workflows/reusable-package.yml"
- ".github/workflows/reusable-strategy-matrix.yml"
- ".github/workflows/reusable-test.yml"
- ".github/workflows/reusable-test-conan-package.yml"
- ".github/workflows/reusable-upload-recipe.yml"
- ".clang-tidy"
- ".codecov.yml"
@@ -36,7 +35,6 @@ on:
- "include/**"
- "src/**"
- "tests/**"
- "validator-keys-tool/**"
- "CMakeLists.txt"
- "conanfile.py"
- "conan.lock"
@@ -94,13 +92,8 @@ jobs:
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-conan-package:
uses: ./.github/workflows/reusable-test-conan-package.yml
upload-recipe:
needs:
- build-test
- test-conan-package
needs: build-test
# Only run when pushing to the develop branch.
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: ./.github/workflows/reusable-upload-recipe.yml

View File

@@ -227,7 +227,8 @@ jobs:
--build . \
--config "${BUILD_TYPE}" \
--parallel "${BUILD_NPROC}" \
--target "${CMAKE_TARGET}"
--target "${CMAKE_TARGET}" \
2>&1 | tee build.log
- name: Show ccache statistics
if: ${{ inputs.ccache_enabled }}
@@ -247,25 +248,6 @@ jobs:
retention-days: 3
if-no-files-found: error
- name: Check validator-keys binary (Linux)
id: validator_keys_binary
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
run: |
if [ -x "${BUILD_DIR}/validator-keys" ]; then
echo "present=true" >>"${GITHUB_OUTPUT}"
else
echo "present=false" >>"${GITHUB_OUTPUT}"
fi
- name: Upload validator-keys binary (Linux)
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' && steps.validator_keys_binary.outputs.present == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: validator-keys-${{ inputs.config_name }}
path: ${{ env.BUILD_DIR }}/validator-keys
retention-days: 3
if-no-files-found: error
- name: Upload the test binary (Linux)
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -344,7 +326,7 @@ jobs:
LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
- name: Show test failure summary
if: ${{ failure() && !inputs.build_only }}
if: ${{ failure() }}
env:
WORKING_DIR: ${{ runner.os == 'Windows' && format('{0}\{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
run: |
@@ -355,13 +337,17 @@ jobs:
cd "${WORKING_DIR}"
if [ ! -f unittest.log ]; then
if [ -f unittest.log ]; then
if ! grep -E "failed" unittest.log | grep -vE "^I[0-9]|^[0-9]+> (ERR:|FTL:)"; then
echo "unittest.log present but no failure lines found."
fi
else
echo "unittest.log not found; embedded tests may not have run."
exit 0
fi
if ! grep -E "failed" unittest.log; then
echo "Log present but no failure lines found in unittest.log."
if [ -f build.log ]; then
if ! grep -E "error:" build.log; then
echo "build.log present but no compile errors found."
fi
fi
fi
- name: Debug failure (Linux)
if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }}

View File

@@ -39,23 +39,8 @@ jobs:
working-directory: .github/scripts/strategy-matrix
run: ./generate.py --packaging >>"${GITHUB_OUTPUT}"
generate-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
package:
needs: [generate-matrix, generate-version]
needs: [generate-matrix]
if: ${{ github.event.repository.visibility == 'public' }}
strategy:
fail-fast: false
@@ -77,19 +62,18 @@ jobs:
name: ${{ matrix.artifact_name }}
path: ${{ env.BUILD_DIR }}
- name: Make binaries executable
run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys"
- name: Make binary executable
run: chmod +x "${BUILD_DIR}/xrpld"
- name: Build package
env:
PKG_VERSION: ${{ needs.generate-version.outputs.version }}
PKG_RELEASE: ${{ inputs.pkg_release }}
run: ./package/build_pkg.sh
- name: Upload package artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.artifact_name }}-pkg-${{ needs.generate-version.outputs.version }}
name: ${{ matrix.artifact_name }}-pkg
path: |
${{ env.BUILD_DIR }}/debbuild/*.deb
${{ env.BUILD_DIR }}/debbuild/*.ddeb

View File

@@ -1,39 +0,0 @@
# Build the Conan package and run the consumer test package.
name: Test Conan package
# This workflow can only be triggered by other workflows.
on:
workflow_call:
defaults:
run:
shell: bash
jobs:
test-conan-package:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
timeout-minutes: 90
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Conan
uses: ./.github/actions/setup-conan
- name: Detect build parallelism
uses: XRPLF/actions/get-nproc@cf0433aa74563aead044a1e395610c96d65a37cf
id: nproc
- name: Export Conan package under test
run: conan export . --version=head
- name: Run Conan package test
working-directory: tests/conan
run: |
conan test . xrpl/head \
--profile:all ci \
--build=missing \
--settings:all build_type=Release \
--conf:all tools.build:jobs="${{ steps.nproc.outputs.nproc }}"

View File

@@ -135,9 +135,9 @@ endif()
include(XrplCore)
include(XrplProtocolAutogen)
include(XrplValidatorKeys)
include(XrplInstall)
include(XrplPackaging)
include(XrplValidatorKeys)
if(tests)
include(CTest)

View File

@@ -25,23 +25,9 @@ if(NOT (RPMBUILD_EXECUTABLE OR DPKG_BUILDPACKAGE_EXECUTABLE))
return()
endif()
if(NOT TARGET xrpld)
message(STATUS "xrpld=ON is required; 'package' target not available")
return()
endif()
if(NOT TARGET validator-keys)
message(
STATUS
"validator_keys=ON is required; 'package' target not available"
)
return()
endif()
set(package_env
SRC_DIR=${CMAKE_SOURCE_DIR}
BUILD_DIR=${CMAKE_BINARY_DIR}
PKG_VERSION=${xrpld_version}
PKG_RELEASE=${pkg_release}
)
@@ -51,7 +37,7 @@ add_custom_target(
${CMAKE_COMMAND} -E env ${package_env}
${CMAKE_SOURCE_DIR}/package/build_pkg.sh
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld validator-keys
DEPENDS xrpld
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
VERBATIM
)

View File

@@ -1,22 +1,26 @@
option(
validator_keys
"Enables building of the vendored validator-keys tool as a separate target"
"Enables building of validator-keys tool as a separate target (imported via FetchContent)"
OFF
)
if(validator_keys)
include(GNUInstallDirs)
git_branch(current_branch)
# default to tracking VK master branch unless we are on release
if(NOT (current_branch STREQUAL "release"))
set(current_branch "master")
endif()
message(STATUS "Tracking ValidatorKeys branch: ${current_branch}")
add_subdirectory(
"${CMAKE_SOURCE_DIR}/validator-keys-tool"
"${CMAKE_BINARY_DIR}/validator-keys-tool"
FetchContent_Declare(
validator_keys
GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git
GIT_TAG "${current_branch}"
)
FetchContent_MakeAvailable(validator_keys)
set_target_properties(
validator-keys
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
install(
TARGETS validator-keys
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime
)
install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()

View File

@@ -116,10 +116,8 @@ words:
- fcontext
- finalizers
- firewalled
- fprofile
- fmtdur
- fsanitize
- ftest
- funclets
- gcov
- gcovr
@@ -129,11 +127,9 @@ words:
- gpgcheck
- gpgkey
- hotwallet
- hvssbqmgz
- hwaddress
- hwrap
- ifndef
- Iiwib
- inequation
- insuf
- insuff
@@ -279,7 +275,6 @@ words:
- sslws
- statsd
- STATSDCOLLECTOR
- STRINGIZE
- stissue
- stnum
- stobj
@@ -306,6 +301,7 @@ words:
- txs
- ubsan
- UBSAN
- ufdio
- umant
- unacquired
- unambiguity
@@ -340,12 +336,10 @@ words:
- writeme
- wsrch
- wthread
- Wsuggest
- xbridge
- xchain
- ximinez
- XMACRO
- xcrun
- xrpkuwait
- xrpl
- xrpld

View File

@@ -1,16 +1,15 @@
# Linux Packaging
This directory contains all files needed to build RPM and Debian packages for
`xrpld`. The packages also include the `validator-keys` utility.
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)
build_pkg.sh Staging and build script (called by the CMake `package` target and CI)
rpm/
xrpld.spec RPM spec (xrpld_version/pkg_release passed via rpmbuild --define)
debian/ Debian control files (control, rules, install, links, conffiles, ...)
xrpld.spec RPM spec
debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, source/format)
shared/
xrpld.service systemd unit file (used by both RPM and DEB)
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
@@ -22,21 +21,19 @@ package/
Packaging targets and their container images are declared in
[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json)
inside `package_configs` configurations. Today only
`linux/amd64` is emitted. 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/xrpld/packaging-<distro>:sha-<git_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.
under `package_configs`, one entry per distro. Today only `linux/amd64` is
emitted. Each entry pins its full container image in an `image` field; to move
to a new image, edit that field and both CI and local builds pick it up. The
package format (deb or rpm) is inferred at build time from the container's
package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm).
| Package type | Image (derived from `linux.json`) | Tool required |
| ------------ | ---------------------------------------------------- | --------------------------------------------------------------- |
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<git_sha>` | `rpmbuild` |
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<git_sha>` | `dpkg-buildpackage`, `debhelper (>= 13)`, `dh-sequence-systemd` |
| Package type | Image (`package_configs.<distro>[].image` in `linux.json`) | Tools required |
| ------------ | ---------------------------------------------------------- | --------------------------------------------------- |
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild` |
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha>` | `dpkg-buildpackage`, debhelper with compat level 13 |
To print the exact image tags for the current `linux.json`:
To print the full packaging matrix (artifact names and images) for the current
`linux.json`:
```bash
./.github/scripts/strategy-matrix/generate.py --packaging
@@ -47,37 +44,34 @@ To print the exact image tags for the current `linux.json`:
### 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, os}` entries, then fan out to
`reusable-package.yml` per entry. That workflow downloads the pre-built binary
artifact containing `xrpld` and `validator-keys`, detects the package format
from the container, and calls `build_pkg.sh` directly — no CMake configure or
`reusable-package.yml`. That workflow generates its own packaging matrix from
`package_configs` in `linux.json` (via `generate.py --packaging`) and fans out
one job per distro. Each job downloads the pre-built `xrpld` binary artifact and
runs in that distro's container, so the package format follows from the
container's package manager. The packaging script derives the package version
from the downloaded binary's `xrpld --version` output; no CMake configure or
build step is needed inside the packaging job.
### Locally (mirrors CI)
With `xrpld` and `validator-keys` binaries already built at `build/xrpld` and
`build/validator-keys`, 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.
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)
# From the repo root. Each distro's container image is the `image` field of its
# package_configs entry in linux.json; the package format is inferred from the
# container's package manager. Example for the rpm-producing image (use
# .package_configs.debian[0].image for the deb image):
IMAGE=$(jq -r '.package_configs.rhel[0].image' .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 --pkg-version "$VERSION" --pkg-release "$PKG_RELEASE"
"${IMAGE}" \
./package/build_pkg.sh --pkg-release "${PKG_RELEASE}"
# Output:
# build/debbuild/*.deb (DEB + dbgsym .ddeb)
@@ -93,42 +87,73 @@ needed, but the host toolchain replaces the pinned CI image:
```bash
cmake \
-Dxrpld=ON \
-Dvalidator_keys=ON \
-Dxrpld_version=2.4.0-local \
-Dpkg_release=1 \
-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. The packaging script installs
to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of
The `cmake/XrplPackaging.cmake` module defines the `package` 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. The packaging script
installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of
`CMAKE_INSTALL_PREFIX`.
The package version is not a CMake input on this path: `build_pkg.sh` derives it
from the just-built `xrpld` binary's `xrpld --version` output. The package
release defaults to 1 and is overridable with `-Dpkg_release=N`.
## How `build_pkg.sh` works
`build_pkg.sh` accepts long-form flags, each of which can also be set via an
environment variable. Flags override env vars; env vars override the built-in
defaults. Run `./package/build_pkg.sh --help` for the same table:
`build_pkg.sh` derives the `xrpld` software version from
`${BUILD_DIR}/xrpld --version` in both package formats.
| Flag | Env var | Default | Purpose |
| -------------------------- | ------------------- | ----------------------------- | ------------------------------------ |
| `--src-dir DIR` | `SRC_DIR` | `$PWD` | repo root |
| `--build-dir DIR` | `BUILD_DIR` | `$PWD/build` | directory holding pre-built binaries |
| `--pkg-version STR` | `PKG_VERSION` | parsed from `xrpld --version` | version string, e.g. `3.2.0-b1` |
| `--pkg-release N` | `PKG_RELEASE` | `1` | package release number |
| `--source-date-epoch SECS` | `SOURCE_DATE_EPOCH` | latest git commit ctime | reproducibility timestamp |
The binary's version is already SemVer-validated by `BuildInfo`.
`build_pkg.sh` converts pre-release versions such as `3.2.0-b1` or
`3.2.0-rc1` from `-` to `~` for package metadata so pre-releases sort before
the final release. If that normalized package version still contains `-`,
packaging fails because RPM forbids `-` in `Version`, and Debian uses `-` as
the upstream/revision separator.
`pkg_version` is the normalized package metadata version derived inside
`build_pkg.sh` from the binary-reported `xrpld` version (`-` pre-release
separator converted to `~`). It is not a separate user input.
`PKG_RELEASE` is a different value: the package release iteration for that
`xrpld` version. RPM receives the normalized `pkg_version` and `PKG_RELEASE` as
the `pkg_version` and `pkg_release` macros for its `Version` and `Release`
values; DEB writes them as `${pkg_version}-${PKG_RELEASE}` in
`debian/changelog`.
With `PKG_RELEASE=1`, the package metadata becomes:
| Input version | RPM version/release | Debian version |
| ------------------ | ---------------------------- | -------------------- |
| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` |
| `3.2.0-b0+abc1234` | `3.2.0~b0+abc1234-1%{?dist}` | `3.2.0~b0+abc1234-1` |
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
The Debian changelog entry carries the repository component: final releases use
`stable`, `b0` builds, including `b0+metadata`, use `develop`, and `bN`/`rcN`
pre-releases use `unstable`.
Build metadata on a final release, such as `3.2.0+abc123`, is rejected.
The RPM path intentionally uses `~` in `Version`, matching the Debian
pre-release ordering convention, so RPM filenames/NVRs begin with forms like
`xrpld-3.2.0~b1-...` and `xrpld-3.2.0~rc1-...` instead of encoding
pre-releases with an older `0.<release>.<suffix>` RPM `Release` value.
The package format (`deb` or `rpm`) is inferred from the host's package
manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). Hosts without one of those
fail early.
Flags are for explicit invocation; environment variables are intended for
CMake/systemd/CI integration. The CI workflow and the CMake `package` target
both invoke `build_pkg.sh` with no flags, configuring it entirely via env
(see `cmake/XrplPackaging.cmake`).
CMake/CI integration. The CI workflow and the CMake `package` target both invoke
`build_pkg.sh` with no flags; CMake supplies `SRC_DIR`, `BUILD_DIR`, and
`PKG_RELEASE` via env, while CI supplies `BUILD_DIR` and `PKG_RELEASE` via env
and lets the script use defaults for the rest.
It resolves `SRC_DIR` and `BUILD_DIR` to absolute paths, then calls
`stage_common()` to copy the binary, config files, and shared support files
@@ -137,18 +162,32 @@ into the staging area, and invokes the platform build tool.
### 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.
2. Copies `xrpld.spec` and all shared source files (binary, configs, service files) into `SOURCES/`.
3. Runs `rpmbuild -bb`, passing the normalized package metadata version as the
`pkg_version` RPM macro and `PKG_RELEASE` as the `pkg_release` RPM macro.
The spec uses manual `install` commands to place files, disables `dwz`, and
writes uncompressed RPM payloads while generating debuginfo packages.
4. Output: `rpmbuild/RPMS/x86_64/xrpld-*.rpm`
The uncompressed RPM payload setting is intentionally unconditional for
generated RPMs. It trades larger RPM artifacts for much shorter package
build/validation time, which keeps RPM package validation in the same rough time
class as Debian package validation.
RPM upgrades intentionally do not restart a running `xrpld` service. The spec
uses `%systemd_postun`, matching Debian's `dh_installsystemd
--no-stop-on-upgrade` behavior; operators pick up the new binary on the next
service restart.
### DEB
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
2. Stages the binaries, configs, `README.md`, and `LICENSE.md`.
2. Stages the binary, configs, `README.md`, and `LICENSE.md`.
3. Copies `package/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.
5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`,
where `pkg_version` is derived from the binary-reported `xrpld` version.
6. Runs `dpkg-buildpackage -b --no-sign -d` (`-d` skips the build-dependency check, since the binary is already built). `debian/rules` uses manual `install` commands.
7. Output: `debbuild/*.deb` and `debbuild/*.ddeb` (dbgsym package)
## Post-build verification
@@ -164,11 +203,14 @@ 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:
`build_pkg.sh` already defaults `SOURCE_DATE_EPOCH` to the latest git commit
time, or the current time outside a git tree, and exports it (override with
`--source-date-epoch` / `SOURCE_DATE_EPOCH`); the RPM spec clamps file
modification times to it via `%build_mtime_policy`. The remaining variables
below further improve reproducibility but are _not_ set by the script — export
them yourself if needed:
```bash
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
export TZ=UTC
export LC_ALL=C.UTF-8
export GZIP=-n

View File

@@ -1,23 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
# Build an RPM or Debian package from pre-built xrpld and validator-keys
# binaries.
# Build an RPM or Debian package from a pre-built xrpld binary.
#
# Flags override env vars; env vars override defaults. Env vars are intended
# for CMake/systemd/CI integration; flags are for explicit invocation.
# Flags override env vars; env vars override defaults.
usage() {
cat <<'EOF'
Usage: build_pkg.sh [options]
Options (each can also be set via the env var shown):
--src-dir DIR repo root [SRC_DIR; default: $PWD]
--build-dir DIR directory holding binaries [BUILD_DIR; default: $PWD/build]
--pkg-version STR version, e.g. 3.2.0-b1 [PKG_VERSION; default: parsed from xrpld --version]
--pkg-release N package release number [PKG_RELEASE; default: 1]
--source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; default: latest git commit ctime]
-h, --help show this help and exit
--src-dir DIR repo root [SRC_DIR; default: ${PWD}]
--build-dir DIR directory holding xrpld [BUILD_DIR; default: ${PWD}/build]
--pkg-release N package release iteration [PKG_RELEASE; default: 1]
--source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; latest git ctime; fallback: current time]
-h, --help show this help and exit
EOF
}
@@ -31,8 +28,7 @@ need_arg() {
# Seed from env. CLI parsing below overrides these directly.
SRC_DIR="${SRC_DIR:-}"
BUILD_DIR="${BUILD_DIR:-}"
PKG_VERSION="${PKG_VERSION:-}"
PKG_RELEASE="${PKG_RELEASE:-}"
PKG_RELEASE="${PKG_RELEASE:-1}"
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-}"
while [[ $# -gt 0 ]]; do
@@ -47,11 +43,6 @@ while [[ $# -gt 0 ]]; do
BUILD_DIR="$2"
shift 2
;;
--pkg-version)
need_arg "$@"
PKG_VERSION="$2"
shift 2
;;
--pkg-release)
need_arg "$@"
PKG_RELEASE="$2"
@@ -75,19 +66,61 @@ while [[ $# -gt 0 ]]; do
done
SRC_DIR="$(cd "${SRC_DIR:-${PWD}}" && pwd)"
BUILD_DIR="$(cd "${BUILD_DIR:-${PWD}/build}" && pwd)"
PKG_RELEASE="${PKG_RELEASE:-1}"
if [[ -z "${PKG_VERSION}" ]]; then
PKG_VERSION="$("${BUILD_DIR}/xrpld" --version | awk 'NR==1 {print $3; exit}')"
BUILD_DIR="${BUILD_DIR:-${PWD}/build}"
if [[ ! -d "${BUILD_DIR}" ]]; then
echo "build_pkg.sh: build directory not found: ${BUILD_DIR}" >&2
echo "Build xrpld before packaging, or set BUILD_DIR to the directory containing xrpld." >&2
exit 1
fi
BUILD_DIR="$(cd "${BUILD_DIR}" && pwd)"
if [[ -z "${PKG_VERSION}" ]]; then
echo "PKG_VERSION is empty (not provided and could not be derived)." >&2
xrpld_binary="${BUILD_DIR}/xrpld"
if [[ ! -x "${xrpld_binary}" ]]; then
echo "build_pkg.sh: expected executable xrpld binary at ${xrpld_binary}." >&2
echo "Build xrpld before packaging, or set BUILD_DIR to the directory containing xrpld." >&2
exit 1
fi
VERSION="${PKG_VERSION}"
xrpld_version="$("${xrpld_binary}" --version | awk 'NR == 1 { print $3 }')"
if [[ -z "${xrpld_version}" ]]; then
echo "build_pkg.sh: unable to derive xrpld version from ${xrpld_binary} --version." >&2
exit 1
fi
# The version as the package formats consume it: identical to xrpld_version
# except a pre-release uses '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before
# the final 3.2.0; a no-op for a final release. Lowercase = derived internally,
# not an input (cf. pkg_type).
pkg_version="${xrpld_version}"
pre_release=""
if [[ "${xrpld_version}" == *-* ]]; then
pre_release="${xrpld_version#*-}"
pkg_version="${xrpld_version%%-*}~${pre_release}"
fi
# BuildInfo already SemVer-validates the binary's version. Packaging adds one
# narrower constraint: after pre-release normalization, the package version must
# not contain '-' because RPM forbids it in Version and Debian uses it as the
# upstream/revision separator.
if [[ "${pkg_version}" == *-* ]]; then
echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2
echo "Package version '${pkg_version}' cannot contain '-'." >&2
echo "Use a single-token pre-release like 3.2.0-b1 or 3.2.0-rc2." >&2
exit 1
fi
if [[ -z "${pre_release}" && "${xrpld_version}" == *+* ]]; then
echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2
echo "Build metadata is only supported on bN/rcN pre-releases." >&2
exit 1
fi
if [[ -n "${pre_release}" && ! "${pre_release}" =~ ^(b0|b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then
echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2
echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2
exit 1
fi
if command -v apt-get >/dev/null 2>&1; then
pkg_type=deb
@@ -99,32 +132,15 @@ else
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)"
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=""
# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). Neither an
# RPM Version nor a Debian upstream version may contain '-' (it's the NVR /
# version-revision separator), and the convention here is single-token
# suffixes like b1 or rc2. Fail early with a clear message rather than letting
# the package tooling blow up or silently mangle dashes.
if [[ "${VER_SUFFIX}" == *-* ]]; then
echo "build_pkg.sh: multi-segment pre-release in VERSION='${VERSION}' (suffix '${VER_SUFFIX}')." >&2
echo "Use single-token suffixes like 3.2.0-b1 or 3.2.0-rc2." >&2
exit 1
fi
CHANGELOG_DATE="$(date -u -R -d "@${SOURCE_DATE_EPOCH}")"
SHARED="${SRC_DIR}/package/shared"
DEBIAN_DIR="${SRC_DIR}/package/debian"
@@ -135,7 +151,6 @@ stage_common() {
mkdir -p "${dest}"
cp "${BUILD_DIR}/xrpld" "${dest}/xrpld"
cp "${BUILD_DIR}/validator-keys" "${dest}/validator-keys"
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"
@@ -145,7 +160,6 @@ stage_common() {
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset"
}
build_rpm() {
@@ -156,18 +170,11 @@ build_rpm() {
cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec"
stage_common "${topdir}/SOURCES"
# Pre-releases use the modern rpm '~' convention (rpm >= 4.10): the suffix
# goes in Version (e.g. 3.2.0~b1), which rpmvercmp sorts *before* the final
# 3.2.0 — identical semantics to Debian's '~'. Release is just the package
# release number. This replaces the older "0.<release>.<suffix>" Release
# hack and keeps the RPM and DEB version strings symmetric.
local rpm_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}"
set -x
rpmbuild -bb \
--define "_topdir ${topdir}" \
--define "xrpld_version ${rpm_version}" \
--define "xrpld_release ${PKG_RELEASE}" \
--define "pkg_version ${pkg_version}" \
--define "pkg_release ${PKG_RELEASE}" \
"${topdir}/SPECS/xrpld.spec"
}
@@ -184,23 +191,26 @@ build_deb() {
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate"
# 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
# Choose the Debian repository component for this package.
# 3.2.0 -> stable, *-b0[+metadata] -> develop,
# bN/rcN pre-releases -> unstable.
local deb_component
if [[ -z "${pre_release}" ]]; then
deb_component="stable"
elif [[ "${pre_release}" =~ ^b0(\+.*)?$ ]]; then
deb_component="develop"
elif [[ "${pre_release}" =~ ^(b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then
deb_component="unstable"
else
echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2
echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2
exit 1
fi
# Debian version is <upstream>[~<pre>]-<pkg release>.
cat >"${staging}/debian/changelog" <<EOF
xrpld (${deb_full_version}) ${deb_distribution}; urgency=medium
* Release ${VERSION}.
xrpld (${pkg_version}-${PKG_RELEASE}) ${deb_component}; urgency=medium
* Release ${xrpld_version}.
-- XRPL Foundation <contact@xrplf.org> ${CHANGELOG_DATE}
EOF

View File

@@ -18,8 +18,6 @@ Depends:
${shlibs:Depends},
${misc:Depends}
Description: XRP Ledger daemon
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.
This package also includes the validator-keys tool for
validator key management.
Reference implementation of the XRP Ledger protocol.
Participates in the peer-to-peer network, processes transactions,
and maintains a local ledger copy.

View File

@@ -18,7 +18,6 @@ override_dh_installsysusers:
override_dh_install:
install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld
install -D -m 0755 validator-keys debian/xrpld/usr/bin/validator-keys
install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg
install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt

View File

@@ -1,6 +1,14 @@
%if "%{?pkg_version}" == ""
%{error:pkg_version must be defined}
%endif
%if "%{?pkg_release}" == ""
%{error:pkg_release must be defined}
%endif
Name: xrpld
Version: %{xrpld_version}
Release: %{xrpld_release}%{?dist}
Version: %{pkg_version}
Release: %{pkg_release}%{?dist}
Summary: XRP Ledger daemon
License: ISC
@@ -11,6 +19,9 @@ BuildRequires: systemd-rpm-macros
%undefine _debugsource_packages
%debug_package
# Intentionally trade larger RPM artifacts for faster package validation.
%global _binary_payload w.ufdio
%global _find_debuginfo_dwz_opts %{nil}
%build_mtime_policy clamp_to_source_date_epoch
@@ -21,8 +32,6 @@ BuildRequires: systemd-rpm-macros
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.
This package also includes the validator-keys tool for validator key
management.
%prep
:
@@ -32,7 +41,6 @@ management.
%install
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{name}
install -Dm0755 %{_sourcedir}/validator-keys %{buildroot}%{_bindir}/validator-keys
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{name}/xrpld.cfg
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{name}/validators.txt
@@ -40,7 +48,10 @@ install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{
install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
install -Dm0644 %{_sourcedir}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
install -Dm0644 %{_sourcedir}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50-xrpld.preset
install -Dm0644 /dev/null %{buildroot}%{_presetdir}/50-xrpld.preset
cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
enable xrpld.service
EOF
# Logrotate config
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name}
@@ -65,7 +76,7 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%systemd_preun xrpld.service
%postun
%systemd_postun_with_restart xrpld.service
%systemd_postun xrpld.service
%files
%license %{_docdir}/%{name}/LICENSE.md
@@ -74,7 +85,6 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%dir %{_sysconfdir}/%{name}
%{_bindir}/%{name}
%{_bindir}/validator-keys
%config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt

View File

@@ -1,2 +0,0 @@
# /usr/lib/systemd/system-preset/50-xrpld.preset
enable xrpld.service

View File

@@ -3,6 +3,7 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/fee.h>
#include <test/jtx/mpt.h>
#include <test/jtx/pay.h>
@@ -100,6 +101,12 @@ class Invariants_test : public beast::unit_test::Suite
return xrpl::test::jtx::testableAmendments() | fixCleanup3_1_3 | fixCleanup3_2_0;
}
test::jtx::Env
makeEnv(FeatureBitset features)
{
return {*this, test::jtx::envconfig(), features, nullptr, beast::Severity::Disabled};
}
/** Run a specific test case to put the ledger into a state that will be
* detected by an invariant. Simulates the actions of a transaction that
* would violate an invariant.
@@ -128,7 +135,7 @@ class Invariants_test : public beast::unit_test::Suite
TxAccount setTxAccount = TxAccount::None)
{
doInvariantCheck(
test::jtx::Env(*this, defaultAmendments()),
makeEnv(defaultAmendments()),
expectLogs,
precheck,
fee,
@@ -1405,7 +1412,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDomain" + std::string(fixEnabled ? " fix" : "");
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain with no rules."}},
[](Account const& a1, Account const& a2, ApplyContext& ac) {
return createPermissionedDomain(ac, a1, a2, 0).get();
@@ -1418,7 +1425,7 @@ class Invariants_test : public beast::unit_test::Suite
static constexpr auto kTooBig = kMaxPermissionedDomainCredentialsArraySize + 1;
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
[](Account const& a1, Account const& a2, ApplyContext& ac) {
return !!createPermissionedDomain(ac, a1, a2, kTooBig);
@@ -1429,7 +1436,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDomain 3";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain credentials aren't sorted"}},
[](Account const& a1, Account const& a2, ApplyContext& ac) {
auto slePd = createPermissionedDomain(ac, a1, a2, 0);
@@ -1453,7 +1460,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDomain 4";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain credentials aren't unique"}},
[](Account const& a1, Account const& a2, ApplyContext& ac) {
auto slePd = createPermissionedDomain(ac, a1, a2, 0);
@@ -1476,7 +1483,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDomain Set 1";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain with no rules."}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
// create PD
@@ -1497,7 +1504,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDomain Set 2";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
// create PD
@@ -1528,7 +1535,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDomain Set 3";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain credentials aren't sorted"}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
// create PD
@@ -1558,7 +1565,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDomain Set 4";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"permissioned domain credentials aren't unique"}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
// create PD
@@ -1599,7 +1606,7 @@ class Invariants_test : public beast::unit_test::Suite
{
testcase << "PermissionedDomain set 2 domains ";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
fixEnabled ? badMoreThan1 : emptyV,
[](Account const& a1, Account const& a2, ApplyContext& ac) {
createPermissionedDomain(ac, a1, a2);
@@ -1645,7 +1652,7 @@ class Invariants_test : public beast::unit_test::Suite
{
testcase << "PermissionedDomain set 0 domains ";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
fixEnabled ? badNoDomains : emptyV,
[](Account const&, Account const&, ApplyContext&) { return true; },
XRPAmount{},
@@ -1668,7 +1675,7 @@ class Invariants_test : public beast::unit_test::Suite
env1.close();
doInvariantCheck(
Env(*this, features),
makeEnv(features),
a1,
a2,
fixEnabled ? badNoDomains : emptyV,
@@ -1709,7 +1716,7 @@ class Invariants_test : public beast::unit_test::Suite
{
testcase << "PermissionedDomain del, create domain ";
doInvariantCheck(
Env(*this, features),
makeEnv(features),
fixEnabled ? badNotDeleted : emptyV,
[](Account const& a1, Account const& a2, ApplyContext& ac) {
createPermissionedDomain(ac, a1, a2);
@@ -1889,7 +1896,7 @@ class Invariants_test : public beast::unit_test::Suite
testcase << "PermissionedDEX" + std::string(fixEnabled ? " fix" : "");
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"domain doesn't exist"}},
[](Account const& a1, Account const&, ApplyContext& ac) {
Keylet const offerKey = keylet::offer(a1.id(), 10);
@@ -1916,7 +1923,7 @@ class Invariants_test : public beast::unit_test::Suite
// missing domain ID in offer object
doInvariantCheck(
Env(*this, features),
makeEnv(features),
{{"hybrid offer is malformed"}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
Keylet const offerKey = keylet::offer(a2.id(), 10);
@@ -4230,7 +4237,7 @@ class Invariants_test : public beast::unit_test::Suite
};
doInvariantCheck(
Env{*this, defaultAmendments() - fixCleanup3_2_0},
makeEnv(defaultAmendments() - fixCleanup3_2_0),
{},
[](Account const&, Account const&, ApplyContext&) { return true; },
XRPAmount{},
@@ -4749,7 +4756,7 @@ class Invariants_test : public beast::unit_test::Suite
// sfHighLimit issue, not the keylet currency).
testcase << "overwrite: NoXRPTrustLines" + std::string(fixEnabled ? " fix" : "");
doInvariantCheck(
Env(*this, features),
makeEnv(features),
fixEnabled ? std::vector<std::string>{{"an XRP trust line was created"}}
: std::vector<std::string>{},
[&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
@@ -4777,7 +4784,7 @@ class Invariants_test : public beast::unit_test::Suite
// Regression: bad deep-freeze trust line followed by a valid one.
testcase << "overwrite: NoDeepFreeze" + std::string(fixEnabled ? " fix" : "");
doInvariantCheck(
Env(*this, features),
makeEnv(features),
fixEnabled ? std::vector<std::string>{{"a trust line with deep freeze flag without "
"normal freeze was created"}}
: std::vector<std::string>{},
@@ -4811,7 +4818,7 @@ class Invariants_test : public beast::unit_test::Suite
// still fires ("a MPT issuance was created").
testcase << "overwrite: NoZeroEscrow MPT" + std::string(fixEnabled ? " fix" : "");
doInvariantCheck(
Env(*this, features),
makeEnv(features),
fixEnabled ? std::vector<std::string>{{"escrow specifies invalid amount"}}
: std::vector<std::string>{{"a MPT issuance was created"}},
[](Account const& a1, Account const&, ApplyContext& ac) {

View File

@@ -1,2 +0,0 @@
# Conan test_package build output (cmake_layout)
/build/

View File

@@ -1,21 +1,12 @@
cmake_minimum_required(VERSION 3.21)
set(name validator-keys-conan-test)
set(name example)
set(version 0.1.0)
project(${name} LANGUAGES CXX)
project(${name} VERSION ${version} LANGUAGES CXX)
find_package(xrpl CONFIG REQUIRED)
# Build the in-repo validator-keys-tool source instead of fetching it from
# GitHub. Keep it out of the default build; the test recipe builds the target
# explicitly.
add_subdirectory(
${CMAKE_CURRENT_SOURCE_DIR}/../../validator-keys-tool
${CMAKE_BINARY_DIR}/validator-keys-tool
EXCLUDE_FROM_ALL
)
set_target_properties(
validator-keys
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
add_executable(example)
target_sources(example PRIVATE src/example.cpp)
target_link_libraries(example PRIVATE xrpl::libxrpl)

View File

@@ -1,4 +1,4 @@
import os
from pathlib import Path
from conan.tools.build import can_run
from conan.tools.cmake import CMake, cmake_layout
@@ -6,15 +6,15 @@ from conan.tools.cmake import CMake, cmake_layout
from conan import ConanFile
class ValidatorKeysConanTest(ConanFile):
name = "validator-keys-conan-test"
class Example(ConanFile):
name = "example"
license = "ISC"
author = (
"John Freeman <jfreeman08@gmail.com>, Michael Legleux <mlegleux@ripple.com>"
)
author = "John Freeman <jfreeman08@gmail.com>, Michael Legleux <mlegleux@ripple.com"
settings = "os", "compiler", "build_type", "arch"
requires = ["xrpl/head"]
default_options = {
"xrpl/*:xrpld": False,
}
@@ -25,20 +25,19 @@ class ValidatorKeysConanTest(ConanFile):
if self.version is None:
self.version = "0.1.0"
def requirements(self):
# Test whatever reference is being created/tested rather than a
# hardcoded version, so this test_package works for any xrpl version.
self.requires(self.tested_reference_str)
def layout(self):
cmake_layout(self)
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build(target="validator-keys")
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()
def test(self):
if can_run(self):
cmd = os.path.join(self.cpp.build.bindir, "validator-keys")
self.run(f'"{cmd}" --unittest', env="conanrun")
cmd_path = Path(self.build_folder) / self.cpp.build.bindir / "example"
self.run(cmd_path, env="conanrun")

View File

@@ -0,0 +1,10 @@
#include <xrpl/protocol/BuildInfo.h>
#include <cstdio>
int
main(int argc, char const** argv)
{
std::printf("%s\n", xrpl::BuildInfo::getVersionString().c_str());
return 0;
}

View File

@@ -1,4 +0,0 @@
# This feature requires Git >= 2.24
# To use it by default in git blame:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
8ae260cb466d4cd0d4db378e5ce0acb8e4432f7c

View File

@@ -1,34 +0,0 @@
cmake_minimum_required(VERSION 3.11)
project(validator-keys-tool)
#[===========================================[
This project is built as part of the xrpld
repository's Conan test package. The parent
project calls find_package(xrpl) and adds this
directory, providing the xrpl::libxrpl target.
#]===========================================]
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
if(NOT TARGET xrpl::libxrpl)
find_package(xrpl CONFIG REQUIRED)
endif()
include(KeysSanity)
include(KeysCov)
include(KeysInterface)
add_executable(
validator-keys
src/ValidatorKeys.cpp
src/ValidatorKeysTool.cpp
# UNIT TESTS:
src/test/ValidatorKeys_test.cpp
src/test/ValidatorKeysTool_test.cpp
)
target_include_directories(validator-keys PRIVATE src)
target_link_libraries(validator-keys xrpl::libxrpl Keys::opts)
include(CTest)
if(BUILD_TESTING)
add_test(test validator-keys --unittest)
endif()

View File

@@ -1,140 +0,0 @@
# validator-keys-tool
Xrpld validator key generation tool
## Build
Use the same build process as xrpld from the repository root, enabling the
optional `validator_keys` target and building `validator-keys`:
```
mkdir .build
cd .build
conan install .. --output-folder . --build=missing --settings=build_type=Release
cmake -DCMAKE_POLICY_DEFAULT_CMP0091=NEW \
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-Dvalidator_keys=ON \
..
cmake --build . --target validator-keys
./validator-keys --unittest
```
The Conan test package in `tests/conan` builds the same target against the
exported `xrpl` package.
## Usage Guide
This guide explains how to set up a validator so its public key does not have to
change if the xrpld config and/or server are compromised.
A validator uses a public/private key pair. The validator is identified by the
public key. The private key should be tightly controlled. It is used to:
- sign tokens authorizing an xrpld server to run as the validator identified by
this public key.
- sign revocations indicating that the private key has been compromised and the
validator public key should no longer be trusted.
Each new token invalidates all previous tokens for the validator public key. The
current token needs to be present in the xrpld config file.
Servers that trust the validator will adapt automatically when the token changes.
### Validator Keys
When first setting up a validator, use the `validator-keys` tool to generate its
key pair:
```
$ validator-keys create_keys
```
Sample output:
```
Validator keys stored in /home/ubuntu/.ripple/validator-keys.json
```
Keep the key file in a secure but recoverable location, such as an encrypted USB
flash drive. Do not modify its contents.
### Validator Token
After first creating the [validator keys](#validator-keys) or if the previous
token has been compromised, use the `validator-keys` tool to create a new
validator token:
```
$ validator-keys create_token
```
Sample output:
```
Update xrpld.cfg file with these values:
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
[validator_token]
eyJ2YWxpZGF0aW9uX3NlY3J|dF9rZXkiOiI5ZWQ0NWY4NjYyNDFjYzE4YTI3NDdiNT
QzODdjMDYyNTkwNzk3MmY0ZTcxOTAyMzFmYWE5Mzc0NTdmYT|kYWY2IiwibWFuaWZl
c3QiOiJKQUFBQUFGeEllMUZ0d21pbXZHdEgyaUNjTUpxQzlnVkZLaWxHZncxL3ZDeE
hYWExwbGMyR25NaEFrRTFhZ3FYeEJ3RHdEYklENk9NU1l1TTBGREFscEFnTms4U0tG
bjdNTzJmZGtjd1JRSWhBT25ndTlzQUtxWFlvdUorbDJWMFcrc0FPa1ZCK1pSUzZQU2
hsSkFmVXNYZkFpQnNWSkdlc2FhZE9KYy9hQVpva1MxdnltR21WcmxIUEtXWDNZeXd1
NmluOEhBU1FLUHVnQkQ2N2tNYVJGR3ZtcEFUSGxHS0pkdkRGbFdQWXk1QXFEZWRGdj
VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0ifQ==
```
For a new validator, add the `[validator_token]` value to the xrpld config file.
For a pre-existing validator, replace the old `[validator_token]` value with the
newly generated one. A valid config file may only contain one `[validator_token]`
value. After the config is updated, restart xrpld.
There is a hard limit of 4,294,967,293 tokens that can be generated for a given
validator key pair.
### Key Revocation
If a validator private key is compromised, the key must be revoked permanently.
To revoke the validator key, use the `validator-keys` tool to generate a
revocation, which indicates to other servers that the key is no longer valid:
```
$ validator-keys revoke_keys
```
Sample output:
```
WARNING: This will revoke your validator keys!
Update xrpld.cfg file with these values and restart xrpld:
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
[validator_key_revocation]
JP////9xIe0hvssbqmgzFH4/NDp1z|3ShkmCtFXuC5A0IUocppHopnASQN2MuMD1Puoyjvnr
jQ2KJSO/2tsjRhjO6q0QQHppslQsKNSXWxjGQNIEa6nPisBOKlDDcJVZAMP4QcIyNCadzgM=
```
Add the `[validator_key_revocation]` value to this validator's config and
restart xrpld. Rename the old key file and generate new
[validator keys](#validator-keys) and a corresponding
[validator token](#validator-token).
### Signing
The `validator-keys` tool can be used to sign arbitrary data with the validator
key.
```
$ validator-keys sign "your data to sign"
```
Sample output:
```
B91B73536235BBA028D344B81DBCBECF19C1E0034AC21FB51C2351A138C9871162F3193D7C41A49FB7AABBC32BC2B116B1D5701807BE462D8800B5AEA4F0550D
```

View File

@@ -1,133 +0,0 @@
#[===================================================================[
coverage report target
#]===================================================================]
include_guard(GLOBAL)
if(NOT coverage)
message(STATUS "Coverage disabled")
return()
endif()
if(NOT (is_clang OR is_gcc))
message(STATUS "Coverage: neither clang nor gcc")
return()
endif()
if(is_clang)
if(APPLE)
execute_process(
COMMAND xcrun -f llvm-profdata
OUTPUT_VARIABLE LLVM_PROFDATA
OUTPUT_STRIP_TRAILING_WHITESPACE
)
else()
find_program(LLVM_PROFDATA llvm-profdata)
endif()
if(NOT LLVM_PROFDATA)
message(
WARNING
"unable to find llvm-profdata - skipping coverage_report target"
)
endif()
if(APPLE)
execute_process(
COMMAND xcrun -f llvm-cov
OUTPUT_VARIABLE LLVM_COV
OUTPUT_STRIP_TRAILING_WHITESPACE
)
else()
find_program(LLVM_COV llvm-cov)
endif()
if(NOT LLVM_COV)
message(
WARNING
"unable to find llvm-cov - skipping coverage_report target"
)
endif()
set(extract_pattern "")
if(coverage_core_only)
set(extract_pattern "${CMAKE_CURRENT_SOURCE_DIR}/src/")
endif()
if(LLVM_COV AND LLVM_PROFDATA)
add_custom_target(
coverage_report
USES_TERMINAL
COMMAND
${CMAKE_COMMAND} -E echo
"Generating coverage - results will be in ${CMAKE_BINARY_DIR}/coverage/index.html."
COMMAND ${CMAKE_COMMAND} -E echo "Running validator-keys tests."
COMMAND
validator-keys
--unittest$<$<BOOL:${coverage_test}>:=${coverage_test}>
COMMAND
${LLVM_PROFDATA} merge -sparse default.profraw -o rip.profdata
COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:"
COMMAND
${LLVM_COV} report -instr-profile=rip.profdata
$<TARGET_FILE:validator-keys> ${extract_pattern}
# generate html report
COMMAND
${LLVM_COV} show -format=html
-output-dir=${CMAKE_BINARY_DIR}/coverage
-instr-profile=rip.profdata $<TARGET_FILE:validator-keys>
${extract_pattern}
BYPRODUCTS coverage/index.html
)
endif()
elseif(is_gcc)
find_program(LCOV lcov)
if(NOT LCOV)
message(WARNING "unable to find lcov - skipping coverage_report target")
endif()
find_program(GENHTML genhtml)
if(NOT GENHTML)
message(
WARNING
"unable to find genhtml - skipping coverage_report target"
)
endif()
set(extract_pattern "*")
if(coverage_core_only)
set(extract_pattern "*/src/*")
endif()
if(LCOV AND GENHTML)
add_custom_target(
coverage_report
USES_TERMINAL
COMMAND
${CMAKE_COMMAND} -E echo
"Generating coverage- results will be in ${CMAKE_BINARY_DIR}/coverage/index.html."
# create baseline info file
COMMAND
${LCOV} --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d .
-i -o baseline.info | grep -v "ignoring data for external file"
# run tests
COMMAND
${CMAKE_COMMAND} -E echo
"Running validator-keys tests for coverage report."
COMMAND
validator-keys
--unittest$<$<BOOL:${coverage_test}>:=${coverage_test}>
# Create test coverage data file
COMMAND
${LCOV} --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d .
-o tests.info | grep -v "ignoring data for external file"
# Combine baseline and test coverage data
COMMAND ${LCOV} -a baseline.info -a tests.info -o lcov-all.info
# extract our files
COMMAND ${LCOV} -e lcov-all.info "${extract_pattern}" -o lcov.info
COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:"
COMMAND ${LCOV} --summary lcov.info
# generate HTML report
COMMAND ${GENHTML} -o ${CMAKE_BINARY_DIR}/coverage lcov.info
BYPRODUCTS coverage/index.html
)
endif()
endif()

View File

@@ -1,87 +0,0 @@
#[===================================================================[
rippled compile options/settings via an interface library
#]===================================================================]
include_guard(GLOBAL)
add_library(keys_opts INTERFACE)
add_library(Keys::opts ALIAS keys_opts)
target_compile_definitions(
keys_opts
INTERFACE
BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
$<$<BOOL:${boost_show_deprecated}>:
BOOST_ASIO_NO_DEPRECATED
BOOST_FILESYSTEM_NO_DEPRECATED
>
$<$<NOT:$<BOOL:${boost_show_deprecated}>>:
BOOST_COROUTINES_NO_DEPRECATION_WARNING
BOOST_BEAST_ALLOW_DEPRECATED
BOOST_FILESYSTEM_DEPRECATED
>
$<$<BOOL:${beast_hashers}>:
USE_BEAST_HASHER
>
$<$<BOOL:${beast_no_unit_test_inline}>:BEAST_NO_UNIT_TEST_INLINE=1>
$<$<BOOL:${beast_disable_autolink}>:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1>
$<$<BOOL:${single_io_service_thread}>:XRPL_SINGLE_IO_SERVICE_THREAD=1>
)
target_compile_options(
keys_opts
INTERFACE
$<$<AND:$<BOOL:${is_gcc}>,$<COMPILE_LANGUAGE:CXX>>:-Wsuggest-override>
$<$<BOOL:${perf}>:-fno-omit-frame-pointer>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-fprofile-arcs
-ftest-coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-fprofile-instr-generate
-fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>
)
target_link_libraries(
keys_opts
INTERFACE
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-fprofile-arcs
-ftest-coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-fprofile-instr-generate
-fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>
)
if(jemalloc)
if(static)
set(JEMALLOC_USE_STATIC ON CACHE BOOL "" FORCE)
endif()
find_package(jemalloc REQUIRED)
target_compile_definitions(keys_opts INTERFACE PROFILE_JEMALLOC)
target_include_directories(
keys_opts
SYSTEM
INTERFACE ${JEMALLOC_INCLUDE_DIRS}
)
target_link_libraries(keys_opts INTERFACE ${JEMALLOC_LIBRARIES})
get_filename_component(JEMALLOC_LIB_PATH ${JEMALLOC_LIBRARIES} DIRECTORY)
## TODO see if we can use the BUILD_RPATH target property (is it transitive?)
set(CMAKE_BUILD_RPATH ${CMAKE_BUILD_RPATH} ${JEMALLOC_LIB_PATH})
endif()
if(san)
target_compile_options(
keys_opts
INTERFACE
# sanitizers recommend minimum of -O1 for reasonable performance
$<$<CONFIG:Debug>:-O1>
${SAN_FLAG}
-fno-omit-frame-pointer
)
target_compile_definitions(
keys_opts
INTERFACE
$<$<STREQUAL:${san},address>:SANITIZER=ASAN>
$<$<STREQUAL:${san},thread>:SANITIZER=TSAN>
$<$<STREQUAL:${san},memory>:SANITIZER=MSAN>
$<$<STREQUAL:${san},undefined>:SANITIZER=UBSAN>
)
target_link_libraries(keys_opts INTERFACE ${SAN_FLAG} ${SAN_LIB})
endif()

View File

@@ -1,107 +0,0 @@
#[===================================================================[
convenience variables and sanity checks
#]===================================================================]
include_guard(GLOBAL)
get_directory_property(has_parent PARENT_DIRECTORY)
if(has_parent)
set(is_root_project OFF)
else()
set(is_root_project ON)
endif()
if(NOT ep_procs)
include(ProcessorCount)
ProcessorCount(ep_procs)
if(ep_procs GREATER 1)
# never use more than half of cores for EP builds
math(EXPR ep_procs "${ep_procs} / 2")
message(STATUS "Using ${ep_procs} cores for ExternalProject builds.")
endif()
endif()
get_property(is_multiconfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(is_multiconfig STREQUAL "NOTFOUND")
if(
${CMAKE_GENERATOR} STREQUAL "Xcode"
OR ${CMAKE_GENERATOR} MATCHES "^Visual Studio"
)
set(is_multiconfig TRUE)
endif()
endif()
if(is_root_project)
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE)
endif()
if(is_root_project AND NOT is_multiconfig)
if(NOT CMAKE_BUILD_TYPE)
message(STATUS "Build type not specified - defaulting to Release")
set(CMAKE_BUILD_TYPE Release CACHE STRING "build type" FORCE)
elseif(
NOT (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL Release)
)
# for simplicity, these are the only two config types we care about. Limiting
# the build types simplifies dealing with external project builds especially
message(
FATAL_ERROR
" *** Only Debug or Release build types are currently supported ***"
)
endif()
endif()
if("${CMAKE_CXX_COMPILER_ID}" MATCHES ".*Clang") # both Clang and AppleClang
set(is_clang TRUE)
if(
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang"
AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0
)
message(FATAL_ERROR "This project requires clang 7 or later")
endif()
# TODO min AppleClang version check ?
elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
set(is_gcc TRUE)
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0)
message(FATAL_ERROR "This project requires GCC 7 or later")
endif()
endif()
if(CMAKE_GENERATOR STREQUAL "Xcode")
set(is_xcode TRUE)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(is_linux TRUE)
else()
set(is_linux FALSE)
endif()
if("$ENV{CI}" STREQUAL "true" OR "$ENV{CONTINUOUS_INTEGRATION}" STREQUAL "true")
set(is_ci TRUE)
else()
set(is_ci FALSE)
endif()
# check for in-source build and fail
if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
message(
FATAL_ERROR
"Builds (in-source) are not allowed in "
"${CMAKE_CURRENT_SOURCE_DIR}. Please remove CMakeCache.txt and the CMakeFiles "
"directory from ${CMAKE_CURRENT_SOURCE_DIR} and try building in a separate directory."
)
endif()
if(MSVC AND CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
message(FATAL_ERROR "Visual Studio 32-bit build is not supported.")
endif()
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(
FATAL_ERROR
"validator-keys requires a 64 bit target architecture.\n"
"The most likely cause of this warning is trying to build on a 32-bit OS."
)
endif()
if(APPLE AND NOT HOMEBREW)
find_program(HOMEBREW brew)
endif()

View File

@@ -1,360 +0,0 @@
#include <ValidatorKeys.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Sign.h>
#include <boost/algorithm/clamp.hpp>
#include <boost/filesystem.hpp>
#include <boost/regex.hpp>
#include <algorithm>
#include <array>
#include <fstream>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#ifndef _WIN32
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#endif
namespace xrpl {
namespace {
void
writeKeyFile(boost::filesystem::path const& keyFile, std::string const& contents)
{
#ifdef _WIN32
std::ofstream o(keyFile.string(), std::ios_base::trunc);
if (o.fail())
throw std::runtime_error("Cannot open key file: " + keyFile.string());
o << contents;
o.close();
if (o.fail())
throw std::runtime_error("Failed to write key file: " + keyFile.string());
#else
auto const path = keyFile.string();
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1)
throw std::runtime_error("Cannot open key file: " + path);
auto closeFile = [&path, &fd]() {
if (fd != -1 && ::close(fd) == -1)
{
fd = -1;
throw std::runtime_error("Failed to close key file: " + path);
}
fd = -1;
};
if (::fchmod(fd, S_IRUSR | S_IWUSR) == -1)
{
auto const error = errno;
::close(fd);
fd = -1;
errno = error;
throw std::runtime_error("Cannot set key file permissions: " + path);
}
auto const* data = contents.data();
auto bytesLeft = contents.size();
while (bytesLeft != 0)
{
auto const written = ::write(fd, data, bytesLeft);
if (written == -1)
{
if (errno == EINTR)
continue;
auto const error = errno;
::close(fd);
fd = -1;
errno = error;
throw std::runtime_error("Failed to write key file: " + path);
}
if (written == 0)
{
::close(fd);
fd = -1;
throw std::runtime_error("Failed to write key file: " + path);
}
auto const bytesWritten = static_cast<std::size_t>(written);
data += bytesWritten;
bytesLeft -= bytesWritten;
}
closeFile();
#endif
}
} // namespace
std::string
ValidatorToken::toString() const
{
json::Value jv;
jv["validation_secret_key"] = strHex(secretKey);
jv["manifest"] = manifest;
return xrpl::base64Encode(to_string(jv));
}
ValidatorKeys::ValidatorKeys(KeyType const& keyType)
: keyType_(keyType)
, tokenSequence_(0)
, revoked_(false)
, keys_(generateKeyPair(keyType_, randomSeed()))
{
}
ValidatorKeys::ValidatorKeys(
KeyType const& keyType,
SecretKey const& secretKey,
std::uint32_t tokenSequence,
bool revoked)
: keyType_(keyType)
, tokenSequence_(tokenSequence)
, revoked_(revoked)
, keys_({derivePublicKey(keyType_, secretKey), secretKey})
{
}
ValidatorKeys
ValidatorKeys::make_ValidatorKeys(boost::filesystem::path const& keyFile)
{
std::ifstream ifsKeys(keyFile.c_str(), std::ios::in);
if (!ifsKeys)
throw std::runtime_error("Failed to open key file: " + keyFile.string());
json::Reader reader;
json::Value jKeys;
if (!reader.parse(ifsKeys, jKeys))
{
throw std::runtime_error("Unable to parse json key file: " + keyFile.string());
}
static std::array<std::string, 4> const requiredFields{
{"key_type", "secret_key", "token_sequence", "revoked"}};
for (auto field : requiredFields)
{
if (!jKeys.isMember(field))
{
throw std::runtime_error(
"Key file '" + keyFile.string() + "' is missing \"" + field + "\" field");
}
}
auto const keyType = keyTypeFromString(jKeys["key_type"].asString());
if (!keyType)
{
throw std::runtime_error(
"Key file '" + keyFile.string() +
"' contains invalid \"key_type\" field: " + jKeys["key_type"].toStyledString());
}
auto const secret =
parseBase58<SecretKey>(TokenType::NodePrivate, jKeys["secret_key"].asString());
if (!secret)
{
throw std::runtime_error(
"Key file '" + keyFile.string() +
"' contains invalid \"secret_key\" field: " + jKeys["secret_key"].toStyledString());
}
std::uint32_t tokenSequence;
try
{
if (!jKeys["token_sequence"].isIntegral())
throw std::runtime_error("");
tokenSequence = jKeys["token_sequence"].asUInt();
}
catch (std::runtime_error const&)
{
throw std::runtime_error(
"Key file '" + keyFile.string() + "' contains invalid \"token_sequence\" field: " +
jKeys["token_sequence"].toStyledString());
}
if (!jKeys["revoked"].isBool())
throw std::runtime_error(
"Key file '" + keyFile.string() +
"' contains invalid \"revoked\" field: " + jKeys["revoked"].toStyledString());
ValidatorKeys vk(*keyType, *secret, tokenSequence, jKeys["revoked"].asBool());
if (jKeys.isMember("domain"))
{
if (!jKeys["domain"].isString())
throw std::runtime_error(
"Key file '" + keyFile.string() +
"' contains invalid \"domain\" field: " + jKeys["domain"].toStyledString());
vk.domain(jKeys["domain"].asString());
}
if (jKeys.isMember("manifest"))
{
if (!jKeys["manifest"].isString())
throw std::runtime_error(
"Key file '" + keyFile.string() +
"' contains invalid \"manifest\" field: " + jKeys["manifest"].toStyledString());
auto ret = strUnHex(jKeys["manifest"].asString());
if (!ret || ret->size() == 0)
throw std::runtime_error(
"Key file '" + keyFile.string() +
"' contains invalid \"manifest\" field: " + jKeys["manifest"].toStyledString());
vk.manifest_.clear();
vk.manifest_.reserve(ret->size());
std::copy(ret->begin(), ret->end(), std::back_inserter(vk.manifest_));
}
return vk;
}
void
ValidatorKeys::writeToFile(boost::filesystem::path const& keyFile) const
{
using namespace boost::filesystem;
json::Value jv;
jv["key_type"] = to_string(keyType_);
jv["public_key"] = toBase58(TokenType::NodePublic, keys_.publicKey);
jv["secret_key"] = toBase58(TokenType::NodePrivate, keys_.secretKey);
jv["token_sequence"] = json::UInt(tokenSequence_);
jv["revoked"] = revoked_;
if (!domain_.empty())
jv["domain"] = domain_;
if (!manifest_.empty())
jv["manifest"] = strHex(makeSlice(manifest_));
if (!keyFile.parent_path().empty())
{
boost::system::error_code ec;
if (!exists(keyFile.parent_path()))
boost::filesystem::create_directories(keyFile.parent_path(), ec);
if (ec || !is_directory(keyFile.parent_path()))
throw std::runtime_error("Cannot create directory: " + keyFile.parent_path().string());
}
writeKeyFile(keyFile, jv.toStyledString());
}
boost::optional<ValidatorToken>
ValidatorKeys::createValidatorToken(KeyType const& keyType)
{
if (revoked() || std::numeric_limits<std::uint32_t>::max() - 1 <= tokenSequence_)
return boost::none;
++tokenSequence_;
auto const tokenSecret = generateSecretKey(keyType, randomSeed());
auto const tokenPublic = derivePublicKey(keyType, tokenSecret);
STObject st(sfGeneric);
st[sfSequence] = tokenSequence_;
st[sfPublicKey] = keys_.publicKey;
st[sfSigningPubKey] = tokenPublic;
if (!domain_.empty())
st[sfDomain] = makeSlice(domain_);
xrpl::sign(st, HashPrefix::Manifest, keyType, tokenSecret);
xrpl::sign(st, HashPrefix::Manifest, keyType_, keys_.secretKey, sfMasterSignature);
Serializer s;
st.add(s);
manifest_.clear();
manifest_.reserve(s.size());
std::copy(s.begin(), s.end(), std::back_inserter(manifest_));
return ValidatorToken{xrpl::base64Encode(manifest_.data(), manifest_.size()), tokenSecret};
}
std::string
ValidatorKeys::revoke()
{
revoked_ = true;
STObject st(sfGeneric);
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
st[sfPublicKey] = keys_.publicKey;
xrpl::sign(st, HashPrefix::Manifest, keyType_, keys_.secretKey, sfMasterSignature);
Serializer s;
st.add(s);
manifest_.clear();
manifest_.reserve(s.size());
std::copy(s.begin(), s.end(), std::back_inserter(manifest_));
return xrpl::base64Encode(manifest_.data(), manifest_.size());
}
std::string
ValidatorKeys::sign(std::string const& data) const
{
return strHex(xrpl::sign(keys_.publicKey, keys_.secretKey, makeSlice(data)));
}
void
ValidatorKeys::domain(std::string d)
{
if (!d.empty())
{
// A valid domain for a validator must be at least 4 characters
// long, should contain at least one . and should not be longer
// that 128 characters.
if (d.size() < 4 || d.size() > 128)
throw std::runtime_error("The domain must be between 4 and 128 characters long.");
// This regular expression should do a decent job of weeding out
// obviously wrong domain names but it isn't perfect. It does not
// really support IDNs. If this turns out to be an issue, a more
// thorough regex can be used or this check can just be removed.
static boost::regex const re(
"^" // Beginning of line
"(" // Hostname or domain name
"(?!-)" // - must not begin with '-'
"[a-zA-Z0-9-]{1,63}" // - only alphanumeric and '-'
"(?<!-)" // - must not end with '-'
"\\." // segment separator
")+" // 1 or more segments
"[A-Za-z]{2,63}" // TLD
"$" // End of line
,
boost::regex_constants::optimize);
if (!boost::regex_match(d, re))
throw std::runtime_error(
"The domain field must use the '[host.][subdomain.]domain.tld' "
"format");
}
domain_ = std::move(d);
}
} // namespace xrpl

View File

@@ -1,161 +0,0 @@
#pragma once
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/SecretKey.h>
#include <boost/optional.hpp>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace boost {
namespace filesystem {
class path;
}
} // namespace boost
namespace xrpl {
struct ValidatorToken
{
std::string const manifest;
SecretKey const secretKey;
/// Returns base64-encoded JSON object
std::string
toString() const;
};
class ValidatorKeys
{
private:
KeyType keyType_;
// struct used to contain both public and secret keys
struct Keys
{
PublicKey publicKey;
SecretKey secretKey;
Keys() = delete;
Keys(std::pair<PublicKey, SecretKey> p) : publicKey(p.first), secretKey(p.second)
{
}
};
std::vector<std::uint8_t> manifest_;
std::uint32_t tokenSequence_;
bool revoked_;
std::string domain_;
Keys keys_;
public:
explicit ValidatorKeys(KeyType const& keyType);
ValidatorKeys(
KeyType const& keyType,
SecretKey const& secretKey,
std::uint32_t sequence,
bool revoked = false);
/** Returns ValidatorKeys constructed from JSON file
@param keyFile Path to JSON key file
@throws std::runtime_error if file content is invalid
*/
static ValidatorKeys
make_ValidatorKeys(boost::filesystem::path const& keyFile);
~ValidatorKeys() = default;
ValidatorKeys(ValidatorKeys const&) = default;
ValidatorKeys&
operator=(ValidatorKeys const&) = default;
inline bool
operator==(ValidatorKeys const& rhs) const
{
// SecretKey::operator== is deleted to discourage non-constant-time
// comparison. The public key is derived deterministically from the
// secret key, so comparing public keys is equivalent here.
return revoked_ == rhs.revoked_ && keyType_ == rhs.keyType_ &&
tokenSequence_ == rhs.tokenSequence_ && keys_.publicKey == rhs.keys_.publicKey;
}
/** Write keys to JSON file
@param keyFile Path to file to write
@note Overwrites existing key file
@throws std::runtime_error if unable to create parent directory
*/
void
writeToFile(boost::filesystem::path const& keyFile) const;
/** Returns validator token for current sequence
@param keyType Key type for the token keys
*/
boost::optional<ValidatorToken>
createValidatorToken(KeyType const& keyType = KeyType::Secp256k1);
/** Revokes validator keys
@return base64-encoded key revocation
*/
std::string
revoke();
/** Signs string with validator key
@param data String to sign
@return hex-encoded signature
*/
std::string
sign(std::string const& data) const;
/** Returns the public key. */
PublicKey const&
publicKey() const
{
return keys_.publicKey;
}
/** Returns true if keys are revoked. */
bool
revoked() const
{
return revoked_;
}
/** Returns the domain associated with this key, if any */
std::string
domain() const
{
return domain_;
}
/** Sets the domain associated with this key */
void
domain(std::string d);
/** Returns the last manifest we generated for this domain, if available. */
std::vector<std::uint8_t>
manifest() const
{
return manifest_;
}
/** Returns the sequence number of the last manifest generated. */
std::uint32_t
sequence() const
{
return tokenSequence_;
}
};
} // namespace xrpl

View File

@@ -1,454 +0,0 @@
#include <ValidatorKeysTool.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/beast/core/SemanticVersion.h>
#include <xrpl/beast/unit_test.h>
#include <boost/filesystem.hpp>
#include <boost/format.hpp>
#include <boost/preprocessor/stringize.hpp>
#include <boost/program_options.hpp>
#include <ValidatorKeys.h>
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <map>
#include <stdexcept>
//------------------------------------------------------------------------------
// The build version number. You must edit this for each release
// and follow the format described at http://semver.org/
//--------------------------------------------------------------------------
char const* const versionString =
"0.3.2"
#if defined(DEBUG) || defined(SANITIZER)
"+"
#ifdef DEBUG
"DEBUG"
#ifdef SANITIZER
"."
#endif
#endif
#ifdef SANITIZER
BOOST_PP_STRINGIZE(SANITIZER)
#endif
#endif
//--------------------------------------------------------------------------
;
static constexpr std::size_t kConfigLineLength = 72;
static int
runUnitTests()
{
using namespace beast::unit_test;
reporter r;
bool const anyFailed = r.runEach(globalSuites());
if (anyFailed)
return EXIT_FAILURE; // LCOV_EXCL_LINE
return EXIT_SUCCESS;
}
void
createKeyFile(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
if (exists(keyFile))
throw std::runtime_error("Refusing to overwrite existing key file: " + keyFile.string());
ValidatorKeys const keys(KeyType::Ed25519);
keys.writeToFile(keyFile);
std::cout << "Validator keys stored in " << keyFile.string()
<< "\n\nThis file should be stored securely and not shared.\n\n";
}
void
createToken(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
if (keys.revoked())
throw std::runtime_error("Validator keys have been revoked.");
auto const token = keys.createValidatorToken();
if (!token)
throw std::runtime_error(
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.");
// Update key file with new token sequence
keys.writeToFile(keyFile);
std::cout << "Update xrpld.cfg file with these values and restart xrpld:\n\n";
std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey())
<< "\n\n";
std::cout << "[validator_token]\n";
auto const tokenStr = token->toString();
for (std::size_t i = 0; i < tokenStr.size(); i += kConfigLineLength)
std::cout << tokenStr.substr(i, kConfigLineLength) << std::endl;
std::cout << std::endl;
}
void
createRevocation(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
if (keys.revoked())
std::cout << "WARNING: Validator keys have already been revoked!\n\n";
else
std::cout << "WARNING: This will revoke your validator keys!\n\n";
auto const revocation = keys.revoke();
// Update key file with new token sequence
keys.writeToFile(keyFile);
std::cout << "Update xrpld.cfg file with these values and restart xrpld:\n\n";
std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey())
<< "\n\n";
std::cout << "[validator_key_revocation]\n";
for (std::size_t i = 0; i < revocation.size(); i += kConfigLineLength)
std::cout << revocation.substr(i, kConfigLineLength) << std::endl;
std::cout << std::endl;
}
void
attestDomain(xrpl::ValidatorKeys const& keys)
{
using namespace xrpl;
if (keys.domain().empty())
{
std::cout << "No attestation is necessary if no domain is specified!\n";
std::cout << "If you have an attestation in your xrpl-ledger.toml\n";
std::cout << "you should remove it at this time.\n";
return;
}
std::cout << "The domain attestation for validator "
<< toBase58(TokenType::NodePublic, keys.publicKey()) << " is:\n\n";
std::cout << "attestation=\""
<< keys.sign(
"[domain-attestation-blob:" + keys.domain() + ":" +
toBase58(TokenType::NodePublic, keys.publicKey()) + "]")
<< "\"\n\n";
std::cout << "You should include it in your xrp-ledger.toml file in the\n";
std::cout << "section for this validator.\n";
}
void
attestDomain(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
if (keys.revoked())
throw std::runtime_error("Operation error: The specified master key has been revoked!");
attestDomain(keys);
}
void
setDomain(std::string const& domain, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
if (keys.revoked())
throw std::runtime_error("Operation error: The specified master key has been revoked!");
if (domain == keys.domain())
{
if (domain.empty())
std::cout << "The domain name was already cleared!\n";
else
std::cout << "The domain name was already set.\n";
return;
}
// Set the domain and generate a new token
keys.domain(domain);
auto const token = keys.createValidatorToken();
if (!token)
throw std::runtime_error(
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.");
// Flush to disk
keys.writeToFile(keyFile);
if (domain.empty())
std::cout << "The domain name has been cleared.\n";
else
std::cout << "The domain name has been set to: " << domain << "\n\n";
attestDomain(keys);
std::cout << "\n";
std::cout << "You also need to update the xrpld.cfg file to add a new\n";
std::cout << "validator token and restart xrpld:\n\n";
std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey())
<< "\n\n";
std::cout << "[validator_token]\n";
auto const tokenStr = token->toString();
for (std::size_t i = 0; i < tokenStr.size(); i += kConfigLineLength)
std::cout << tokenStr.substr(i, kConfigLineLength) << std::endl;
std::cout << "\n";
}
void
signData(std::string const& data, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
if (data.empty())
throw std::runtime_error("Syntax error: Must specify data string to sign");
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
if (keys.revoked())
std::cout << "WARNING: Validator keys have been revoked!\n\n";
std::cout << keys.sign(data) << std::endl;
std::cout << std::endl;
}
void
generateManifest(std::string const& type, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
auto const m = keys.manifest();
if (m.empty())
{
std::cout << "The last manifest generated is unavailable. You can\n";
std::cout << "generate a new one.\n\n";
return;
}
if (type == "base64")
{
std::cout << "Manifest #" << keys.sequence() << " (Base64):\n";
std::cout << base64Encode(m.data(), m.size()) << "\n\n";
return;
}
if (type == "hex")
{
std::cout << "Manifest #" << keys.sequence() << " (Hex):\n";
std::cout << strHex(makeSlice(m)) << "\n\n";
return;
}
std::cout << "Unknown encoding '" << type << "'\n";
}
int
runCommand(
std::string const& command,
std::vector<std::string> const& args,
boost::filesystem::path const& keyFile)
{
using namespace std;
static map<string, vector<string>::size_type> const commandArgs = {
{"create_keys", 0},
{"create_token", 0},
{"revoke_keys", 0},
{"set_domain", 1},
{"clear_domain", 0},
{"attest_domain", 0},
{"show_manifest", 1},
{"sign", 1},
};
auto const iArgs = commandArgs.find(command);
if (iArgs == commandArgs.end())
throw std::runtime_error("Unknown command: " + command);
if (args.size() != iArgs->second)
throw std::runtime_error("Syntax error: Wrong number of arguments");
if (command == "create_keys")
createKeyFile(keyFile);
else if (command == "create_token")
createToken(keyFile);
else if (command == "revoke_keys")
createRevocation(keyFile);
else if (command == "set_domain")
setDomain(args[0], keyFile);
else if (command == "clear_domain")
setDomain("", keyFile);
else if (command == "attest_domain")
attestDomain(keyFile);
else if (command == "sign")
signData(args[0], keyFile);
else if (command == "show_manifest")
generateManifest(args[0], keyFile);
return 0;
}
// LCOV_EXCL_START
static std::string
getEnvVar(char const* name)
{
std::string value;
auto const v = getenv(name);
if (v != nullptr)
value = v;
return value;
}
void
printHelp(boost::program_options::options_description const& desc)
{
std::cerr << "validator-keys [options] <command> [<argument> ...]\n"
<< desc << std::endl
<< "Commands: \n"
" create_keys Generate validator keys.\n"
" create_token Generate validator token.\n"
" revoke_keys Revoke validator keys.\n"
" sign <data> Sign string with validator "
"key.\n"
" show_manifest [hex|base64] Displays the last generated "
"manifest\n"
" set_domain <domain> Associate a domain with the "
"validator key.\n"
" clear_domain Disassociate a domain from a "
"validator key.\n"
" attest_domain Produce the attestation string "
"for a domain.\n";
}
// LCOV_EXCL_STOP
std::string const&
getVersionString()
{
static std::string const value = [] {
std::string const s = versionString;
beast::SemanticVersion v;
if (!v.parse(s) || v.print() != s)
throw std::logic_error(s + ": Bad version string"); // LCOV_EXCL_LINE
return s;
}();
return value;
}
int
main(int argc, char** argv)
{
namespace po = boost::program_options;
po::variables_map vm;
// Set up option parsing.
//
po::options_description general("General Options");
general.add_options()("help,h", "Display this message.")(
"keyfile", po::value<std::string>(), "Specify the key file.")(
"unittest,u", "Perform unit tests.")("version", "Display the build version.");
po::options_description hidden("Hidden options");
hidden.add_options()("command", po::value<std::string>(), "Command.")(
"arguments",
po::value<std::vector<std::string>>()->default_value(std::vector<std::string>(), "empty"),
"Arguments.");
po::positional_options_description p;
p.add("command", 1).add("arguments", -1);
po::options_description cmdline_options;
cmdline_options.add(general).add(hidden);
// Parse options, if no error.
try
{
po::store(
po::command_line_parser(argc, argv)
.options(cmdline_options) // Parse options.
.positional(p)
.run(),
vm);
po::notify(vm); // Invoke option notify functions.
}
// LCOV_EXCL_START
catch (std::exception const&)
{
std::cerr << "validator-keys: Incorrect command line syntax." << std::endl;
std::cerr << "Use '--help' for a list of options." << std::endl;
return EXIT_FAILURE;
}
// LCOV_EXCL_STOP
// Run the unit tests if requested.
// The unit tests will exit the application with an appropriate return code.
if (vm.count("unittest"))
return runUnitTests();
// LCOV_EXCL_START
if (vm.count("version"))
{
std::cout << "validator-keys version " << getVersionString() << std::endl;
return 0;
}
if (vm.count("help") || !vm.count("command"))
{
printHelp(general);
return EXIT_SUCCESS;
}
std::string const homeDir = getEnvVar("HOME");
std::string const defaultKeyFile =
(homeDir.empty() ? boost::filesystem::current_path().string() : homeDir) +
"/.ripple/validator-keys.json";
try
{
using namespace boost::filesystem;
path keyFile = vm.count("keyfile") ? vm["keyfile"].as<std::string>() : defaultKeyFile;
return runCommand(
vm["command"].as<std::string>(),
vm["arguments"].as<std::vector<std::string>>(),
keyFile);
}
catch (std::exception const& e)
{
std::cerr << e.what() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
// LCOV_EXCL_STOP
}

View File

@@ -1,31 +0,0 @@
#pragma once
#include <string>
#include <vector>
namespace boost {
namespace filesystem {
class path;
}
} // namespace boost
std::string const&
getVersionString();
void
createKeyFile(boost::filesystem::path const& keyFile);
void
createToken(boost::filesystem::path const& keyFile);
void
createRevocation(boost::filesystem::path const& keyFile);
void
signData(std::string const& data, boost::filesystem::path const& keyFile);
int
runCommand(
std::string const& command,
std::vector<std::string> const& arg,
boost::filesystem::path const& keyFile);

View File

@@ -1,59 +0,0 @@
#pragma once
#include <xrpl/beast/unit_test.h>
#include <boost/filesystem.hpp>
#include <ostream>
#include <stdexcept>
#include <string>
namespace xrpl {
/**
Write a key file dir and remove when done.
*/
class KeyFileGuard
{
private:
using Path = boost::filesystem::path;
beast::unit_test::Suite& test_;
Path subDir_;
void
rmDir(Path const& toRm)
{
if (boost::filesystem::is_directory(toRm))
boost::filesystem::remove_all(toRm);
else
test_.log << "Expected " << toRm.string() << " to be an existing directory."
<< std::endl;
}
public:
KeyFileGuard(beast::unit_test::Suite& test, std::string const& subDir)
: test_(test), subDir_(subDir)
{
if (!boost::filesystem::exists(subDir_))
boost::filesystem::create_directory(subDir_);
else
// Cannot run the test. Someone created a file or directory
// where we want to put our directory
throw std::runtime_error("Cannot create directory: " + subDir_.string());
}
~KeyFileGuard()
{
try
{
rmDir(subDir_);
}
catch (std::exception const& e)
{
// if we throw here, just let it die.
test_.log << "Error in ~KeyFileGuard: " << e.what() << std::endl;
}
}
};
} // namespace xrpl

View File

@@ -1,295 +0,0 @@
#include <test/KeyFileGuard.h>
#include <xrpl/protocol/SecretKey.h>
#include <ValidatorKeys.h>
#include <ValidatorKeysTool.h>
#include <exception>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace xrpl {
namespace tests {
class ValidatorKeysTool_test : public beast::unit_test::Suite
{
private:
// Allow cout to be redirected. Destructor restores old cout streambuf.
class CoutRedirect
{
public:
CoutRedirect(std::stringstream& sStream) : old_(std::cout.rdbuf(sStream.rdbuf()))
{
}
~CoutRedirect()
{
std::cout.rdbuf(old_);
}
private:
std::streambuf* const old_;
};
void
testCreateKeyFile()
{
testcase("Create Key File");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
createKeyFile(keyFile);
BEAST_EXPECT(exists(keyFile));
std::string const expectedError =
"Refusing to overwrite existing key file: " + keyFile.string();
std::string error;
try
{
createKeyFile(keyFile);
}
catch (std::exception const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
}
void
testCreateToken()
{
testcase("Create Token");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
auto testToken = [this](path const& keyFile, std::string const& expectedError) {
try
{
createToken(keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(std::string{e.what()} == expectedError);
}
};
{
std::string const expectedError = "Failed to open key file: " + keyFile.string();
testToken(keyFile, expectedError);
}
createKeyFile(keyFile);
{
std::string const expectedError = "";
testToken(keyFile, expectedError);
}
{
auto const keyType = KeyType::Ed25519;
auto const kp = generateKeyPair(keyType, randomSeed());
auto keys =
ValidatorKeys(keyType, kp.second, std::numeric_limits<std::uint32_t>::max() - 1);
keys.writeToFile(keyFile);
std::string const expectedError =
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.";
testToken(keyFile, expectedError);
}
{
createRevocation(keyFile);
std::string const expectedError = "Validator keys have been revoked.";
testToken(keyFile, expectedError);
}
}
void
testCreateRevocation()
{
testcase("Create Revocation");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
auto expectedError = "Failed to open key file: " + keyFile.string();
std::string error;
try
{
createRevocation(keyFile);
}
catch (std::runtime_error const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
createKeyFile(keyFile);
BEAST_EXPECT(exists(keyFile));
createRevocation(keyFile);
createRevocation(keyFile);
}
void
testSign()
{
testcase("Sign");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
auto testSign =
[this](std::string const& data, path const& keyFile, std::string const& expectedError) {
try
{
signData(data, keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(std::string{e.what()} == expectedError);
}
};
std::string const data = "data to sign";
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
{
std::string const expectedError = "Failed to open key file: " + keyFile.string();
testSign(data, keyFile, expectedError);
}
createKeyFile(keyFile);
BEAST_EXPECT(exists(keyFile));
{
std::string const emptyData = "";
std::string const expectedError = "Syntax error: Must specify data string to sign";
testSign(emptyData, keyFile, expectedError);
}
{
std::string const expectedError = "";
testSign(data, keyFile, expectedError);
}
}
void
testRunCommand()
{
testcase("Run Command");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
auto testCommand = [this](
std::string const& command,
std::vector<std::string> const& args,
path const& keyFile,
std::string const& expectedError) {
try
{
runCommand(command, args, keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(std::string{e.what()} == expectedError);
}
};
std::vector<std::string> const noArgs;
std::vector<std::string> const oneArg = {"some data"};
std::vector<std::string> const twoArgs = {"data", "more data"};
std::string const noError = "";
std::string const argError = "Syntax error: Wrong number of arguments";
{
std::string const command = "unknown";
std::string const expectedError = "Unknown command: " + command;
testCommand(command, noArgs, keyFile, expectedError);
testCommand(command, oneArg, keyFile, expectedError);
testCommand(command, twoArgs, keyFile, expectedError);
}
{
std::string const command = "create_keys";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "create_token";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "revoke_keys";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "sign";
testCommand(command, noArgs, keyFile, argError);
testCommand(command, oneArg, keyFile, noError);
testCommand(command, twoArgs, keyFile, argError);
}
}
public:
void
run() override
{
getVersionString();
testCreateKeyFile();
testCreateToken();
testCreateRevocation();
testSign();
testRunCommand();
}
};
BEAST_DEFINE_TESTSUITE(ValidatorKeysTool, keys, xrpl);
} // namespace tests
} // namespace xrpl

View File

@@ -1,408 +0,0 @@
#include <test/KeyFileGuard.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Sign.h>
#include <boost/filesystem.hpp>
#include <ValidatorKeys.h>
#include <array>
#include <cstdint>
#include <fstream>
#include <limits>
#include <map>
#include <stdexcept>
#include <string>
#ifndef _WIN32
#include <sys/stat.h>
#endif
namespace xrpl {
namespace tests {
class ValidatorKeys_test : public beast::unit_test::Suite
{
private:
void
testKeyFile(
boost::filesystem::path const& keyFile,
json::Value const& jv,
std::string const& expectedError)
{
{
std::ofstream o(keyFile.string(), std::ios_base::trunc);
o << jv.toStyledString();
o.close();
}
try
{
ValidatorKeys::make_ValidatorKeys(keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(std::string{e.what()} == expectedError);
}
}
std::array<KeyType, 2> const keyTypes{{KeyType::Ed25519, KeyType::Secp256k1}};
#ifndef _WIN32
void
expectOwnerOnlyKeyFile(boost::filesystem::path const& keyFile)
{
struct stat fileStatus;
BEAST_EXPECT(::stat(keyFile.string().c_str(), &fileStatus) == 0);
BEAST_EXPECT((fileStatus.st_mode & 0777) == (S_IRUSR | S_IWUSR));
}
#endif
void
testMakeValidatorKeys()
{
testcase("Make Validator Keys");
using namespace boost::filesystem;
path const subdir = "test_key_file";
path const keyFile = subdir / "validator_keys.json";
for (auto const keyType : keyTypes)
{
ValidatorKeys const keys(keyType);
KeyFileGuard const g(*this, subdir.string());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
auto const keys2 = ValidatorKeys::make_ValidatorKeys(keyFile);
BEAST_EXPECT(keys == keys2);
}
{
// Require expected fields
KeyFileGuard g(*this, subdir.string());
auto expectedError = "Failed to open key file: " + keyFile.string();
std::string error;
try
{
ValidatorKeys::make_ValidatorKeys(keyFile);
}
catch (std::runtime_error const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
expectedError = "Unable to parse json key file: " + keyFile.string();
{
std::ofstream o(keyFile.string(), std::ios_base::trunc);
o << "{{}";
o.close();
}
try
{
ValidatorKeys::make_ValidatorKeys(keyFile);
}
catch (std::runtime_error const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
json::Value jv;
jv["dummy"] = "field";
expectedError = "Key file '" + keyFile.string() + "' is missing \"key_type\" field";
testKeyFile(keyFile, jv, expectedError);
jv["key_type"] = "dummy keytype";
expectedError = "Key file '" + keyFile.string() + "' is missing \"secret_key\" field";
testKeyFile(keyFile, jv, expectedError);
jv["secret_key"] = "dummy secret";
expectedError =
"Key file '" + keyFile.string() + "' is missing \"token_sequence\" field";
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = "dummy sequence";
expectedError = "Key file '" + keyFile.string() + "' is missing \"revoked\" field";
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = "dummy revoked";
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"key_type\" field: " + jv["key_type"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
auto const keyType = KeyType::Ed25519;
jv["key_type"] = to_string(keyType);
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"secret_key\" field: " + jv["secret_key"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
ValidatorKeys const keys(keyType);
{
auto const kp = generateKeyPair(keyType, randomSeed());
jv["secret_key"] = toBase58(TokenType::NodePrivate, kp.second);
}
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"token_sequence\" field: " +
jv["token_sequence"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = -1;
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"token_sequence\" field: " +
jv["token_sequence"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = json::UInt(std::numeric_limits<std::uint32_t>::max());
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"revoked\" field: " + jv["revoked"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = false;
expectedError = "";
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = true;
testKeyFile(keyFile, jv, expectedError);
}
}
void
testCreateValidatorToken()
{
testcase("Create Validator Token");
for (auto const keyType : keyTypes)
{
ValidatorKeys keys(keyType);
std::uint32_t sequence = 0;
for (auto const tokenKeyType : keyTypes)
{
auto const token = keys.createValidatorToken(tokenKeyType);
if (!BEAST_EXPECT(token))
continue;
auto const tokenPublicKey = derivePublicKey(tokenKeyType, token->secretKey);
STObject st(sfGeneric);
auto const manifest = xrpl::base64Decode(token->manifest);
SerialIter sit(manifest.data(), manifest.size());
st.set(sit);
auto const seq = get(st, sfSequence);
BEAST_EXPECT(seq);
BEAST_EXPECT(*seq == ++sequence);
auto const tpk = get<PublicKey>(st, sfSigningPubKey);
BEAST_EXPECT(tpk);
BEAST_EXPECT(*tpk == tokenPublicKey);
BEAST_EXPECT(verify(st, HashPrefix::Manifest, tokenPublicKey));
auto const pk = get<PublicKey>(st, sfPublicKey);
BEAST_EXPECT(pk);
BEAST_EXPECT(*pk == keys.publicKey());
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
}
}
auto const keyType = KeyType::Ed25519;
auto const kp = generateKeyPair(keyType, randomSeed());
auto keys =
ValidatorKeys(keyType, kp.second, std::numeric_limits<std::uint32_t>::max() - 1);
BEAST_EXPECT(!keys.createValidatorToken(keyType));
keys.revoke();
BEAST_EXPECT(!keys.createValidatorToken(keyType));
}
void
testRevoke()
{
testcase("Revoke");
for (auto const keyType : keyTypes)
{
ValidatorKeys keys(keyType);
auto const revocation = keys.revoke();
STObject st(sfGeneric);
auto const manifest = xrpl::base64Decode(revocation);
SerialIter sit(manifest.data(), manifest.size());
st.set(sit);
auto const seq = get(st, sfSequence);
BEAST_EXPECT(seq);
BEAST_EXPECT(*seq == std::numeric_limits<std::uint32_t>::max());
auto const pk = get(st, sfPublicKey);
BEAST_EXPECT(pk);
BEAST_EXPECT(*pk == keys.publicKey());
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
}
}
void
testSign()
{
testcase("Sign");
std::map<KeyType, std::string> expected(
{{KeyType::Ed25519,
"2EE541D6825791BF5454C571D2B363EAB3F01C73159B1F"
"237AC6D38663A82B9D5EAD262D5F776B916E68247A1F082090F3BAE7ABC939"
"C8F29B0DC759FD712300"},
{KeyType::Secp256k1,
"3045022100F142C27BF83D8D4541C7A4E759DE64A672"
"51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5"
"55438D64A229CD0B4BA2AE33402443B20409"}});
std::string const data = "data to sign";
for (auto const keyType : keyTypes)
{
auto const sk = generateSecretKey(keyType, generateSeed("test"));
ValidatorKeys keys(keyType, sk, 1);
auto const signature = keys.sign(data);
BEAST_EXPECT(expected[keyType] == signature);
auto const ret = strUnHex(signature);
BEAST_EXPECT(ret);
BEAST_EXPECT(ret->size());
BEAST_EXPECT(verify(keys.publicKey(), makeSlice(data), makeSlice(*ret)));
}
}
void
testWriteToFile()
{
testcase("Write to File");
using namespace boost::filesystem;
auto const keyType = KeyType::Ed25519;
ValidatorKeys keys(keyType);
{
path const subdir = "test_key_file";
path const keyFile = subdir / "validator_keys.json";
KeyFileGuard g(*this, subdir.string());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
#ifndef _WIN32
expectOwnerOnlyKeyFile(keyFile);
#endif
auto fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
// Overwrite file with new sequence
#ifndef _WIN32
BEAST_EXPECT(
::chmod(keyFile.string().c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) == 0);
#endif
keys.createValidatorToken(KeyType::Secp256k1);
keys.writeToFile(keyFile);
#ifndef _WIN32
expectOwnerOnlyKeyFile(keyFile);
#endif
fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
}
{
// Write to key file in current relative directory
path const keyFile = "test_validator_keys.json";
if (!exists(keyFile))
{
keys.writeToFile(keyFile);
remove(keyFile.string());
}
else
{
// Cannot run the test. Someone created a file
// where we want to put our key file
Throw<std::runtime_error>("Cannot create key file: " + keyFile.string());
}
}
{
// Create key file directory
path const subdir = "test_key_file";
path const keyFile = subdir / "directories/to/create/validator_keys.json";
KeyFileGuard g(*this, subdir.string());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
auto const fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
}
{
// Fail if file cannot be opened for write
path const subdir = "test_key_file";
KeyFileGuard g(*this, subdir.string());
path const badKeyFile = subdir / ".";
auto expectedError = "Cannot open key file: " + badKeyFile.string();
std::string error;
try
{
keys.writeToFile(badKeyFile);
}
catch (std::runtime_error const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
// Fail if parent directory is existing file
path const keyFile = subdir / "validator_keys.json";
keys.writeToFile(keyFile);
path const conflictingPath = keyFile / "validators_keys.json";
expectedError = "Cannot create directory: " + conflictingPath.parent_path().string();
try
{
keys.writeToFile(conflictingPath);
}
catch (std::runtime_error const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
}
}
public:
void
run() override
{
testMakeValidatorKeys();
testCreateValidatorToken();
testRevoke();
testSign();
testWriteToFile();
}
};
BEAST_DEFINE_TESTSUITE(ValidatorKeys, keys, xrpl);
} // namespace tests
} // namespace xrpl