mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-16 23:27:01 +00:00
Compare commits
2 Commits
develop
...
legleux/va
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06337079f0 | ||
|
|
bd8351c653 |
51
.github/scripts/strategy-matrix/linux.json
vendored
51
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,56 +1,12 @@
|
||||
{
|
||||
"image_tag": "sha-fe4c8ae",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
"compiler": ["gcc", "clang"],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"arch": ["amd64", "arm64"]
|
||||
},
|
||||
|
||||
{
|
||||
"compiler": ["gcc", "clang"],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"arch": ["amd64"],
|
||||
"sanitizers": ["address", "undefinedbehavior"]
|
||||
},
|
||||
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "coverage",
|
||||
"extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0"
|
||||
},
|
||||
{
|
||||
"compiler": ["clang"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "voidstar",
|
||||
"extra_cmake_args": "-Dvoidstar=ON"
|
||||
},
|
||||
{
|
||||
"compiler": ["clang"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "reffee",
|
||||
"extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=1000"
|
||||
},
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "unity",
|
||||
"extra_cmake_args": "-Dunity=ON",
|
||||
"exclude_event_types": ["pull_request"]
|
||||
}
|
||||
],
|
||||
|
||||
"debian": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"]
|
||||
"arch": ["amd64"],
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -58,7 +14,8 @@
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"]
|
||||
"arch": ["amd64"],
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
61
.github/workflows/on-pr.yml
vendored
61
.github/workflows/on-pr.yml
vendored
@@ -67,6 +67,7 @@ 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
|
||||
@@ -77,6 +78,7 @@ jobs:
|
||||
include/**
|
||||
src/**
|
||||
tests/**
|
||||
validator-keys-tool/**
|
||||
CMakeLists.txt
|
||||
conanfile.py
|
||||
conan.lock
|
||||
@@ -103,27 +105,6 @@ jobs:
|
||||
outputs:
|
||||
go: ${{ steps.go.outputs.go == 'true' }}
|
||||
|
||||
check-levelization:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-levelization.yml
|
||||
|
||||
check-rename:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-rename.yml
|
||||
|
||||
clang-tidy:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-clang-tidy.yml
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
with:
|
||||
check_only_changed: true
|
||||
create_issue_on_failure: false
|
||||
|
||||
build-test:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
@@ -131,7 +112,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [linux, macos, windows]
|
||||
os: [linux]
|
||||
with:
|
||||
# Enable ccache only for events targeting the XRPLF repository, since
|
||||
# other accounts will not have access to our remote cache storage.
|
||||
@@ -145,43 +126,17 @@ jobs:
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-package.yml
|
||||
|
||||
upload-recipe:
|
||||
needs:
|
||||
- should-run
|
||||
- build-test
|
||||
# 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
|
||||
secrets:
|
||||
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
|
||||
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
|
||||
|
||||
notify-clio:
|
||||
needs: upload-recipe
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Notify the Clio repository about the newly proposed release version, so
|
||||
# it can be checked for compatibility before the release is actually made.
|
||||
- name: Notify Clio
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CLIO_NOTIFY_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \
|
||||
-F "client_payload[ref]=${{ needs.upload-recipe.outputs.recipe_ref }}" \
|
||||
-F "client_payload[pr_url]=${PR_URL}"
|
||||
test-conan-package:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-test-conan-package.yml
|
||||
|
||||
passed:
|
||||
if: failure() || cancelled()
|
||||
needs:
|
||||
- check-levelization
|
||||
- check-rename
|
||||
- clang-tidy
|
||||
- build-test
|
||||
- package
|
||||
- upload-recipe
|
||||
- notify-clio
|
||||
- test-conan-package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fail
|
||||
|
||||
5
.github/workflows/on-tag.yml
vendored
5
.github/workflows/on-tag.yml
vendored
@@ -16,8 +16,13 @@ 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 }}
|
||||
|
||||
23
.github/workflows/on-trigger.yml
vendored
23
.github/workflows/on-trigger.yml
vendored
@@ -24,6 +24,7 @@ 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"
|
||||
@@ -34,6 +35,7 @@ on:
|
||||
- "include/**"
|
||||
- "src/**"
|
||||
- "tests/**"
|
||||
- "validator-keys-tool/**"
|
||||
- "CMakeLists.txt"
|
||||
- "conanfile.py"
|
||||
- "conan.lock"
|
||||
@@ -65,21 +67,12 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
clang-tidy:
|
||||
uses: ./.github/workflows/reusable-clang-tidy.yml
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
with:
|
||||
check_only_changed: false
|
||||
create_issue_on_failure: ${{ github.event_name == 'schedule' }}
|
||||
|
||||
build-test:
|
||||
uses: ./.github/workflows/reusable-build-test.yml
|
||||
strategy:
|
||||
fail-fast: ${{ github.event_name == 'merge_group' }}
|
||||
matrix:
|
||||
os: [linux, macos, windows]
|
||||
os: [linux]
|
||||
with:
|
||||
# Enable ccache only for events targeting the XRPLF repository, since
|
||||
# other accounts will not have access to our remote cache storage.
|
||||
@@ -91,14 +84,8 @@ jobs:
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
upload-recipe:
|
||||
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
|
||||
secrets:
|
||||
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
|
||||
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
|
||||
test-conan-package:
|
||||
uses: ./.github/workflows/reusable-test-conan-package.yml
|
||||
|
||||
package:
|
||||
needs: build-test
|
||||
|
||||
14
.github/workflows/reusable-build-test-config.yml
vendored
14
.github/workflows/reusable-build-test-config.yml
vendored
@@ -235,6 +235,9 @@ jobs:
|
||||
run: |
|
||||
loader="$(/tmp/loader-path.sh)"
|
||||
patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/xrpld"
|
||||
if [ -x "${{ env.BUILD_DIR }}/validator-keys" ]; then
|
||||
patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/validator-keys"
|
||||
fi
|
||||
|
||||
# We're only running aarch64 Linux builds in Ubuntu-based images, so this is kept simple
|
||||
- name: Install libatomic (Linux aarch64)
|
||||
@@ -253,12 +256,21 @@ jobs:
|
||||
curl ${CCACHE_REMOTE_STORAGE%|*}/status || true
|
||||
fi
|
||||
|
||||
- name: Stage binary artifacts (Linux)
|
||||
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
mkdir -p "${BUILD_DIR}/artifacts"
|
||||
cp "${BUILD_DIR}/xrpld" "${BUILD_DIR}/artifacts/xrpld"
|
||||
if [ -x "${BUILD_DIR}/validator-keys" ]; then
|
||||
cp "${BUILD_DIR}/validator-keys" "${BUILD_DIR}/artifacts/validator-keys"
|
||||
fi
|
||||
|
||||
- name: Upload the binary (Linux)
|
||||
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: xrpld-${{ inputs.config_name }}
|
||||
path: ${{ env.BUILD_DIR }}/xrpld
|
||||
path: ${{ env.BUILD_DIR }}/artifacts/*
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
4
.github/workflows/reusable-package.yml
vendored
4
.github/workflows/reusable-package.yml
vendored
@@ -77,8 +77,8 @@ jobs:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Make binary executable
|
||||
run: chmod +x "${BUILD_DIR}/xrpld"
|
||||
- name: Make binaries executable
|
||||
run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys"
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
|
||||
35
.github/workflows/reusable-test-conan-package.yml
vendored
Normal file
35
.github/workflows/reusable-test-conan-package.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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: 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="$(nproc)"
|
||||
@@ -133,9 +133,9 @@ endif()
|
||||
|
||||
include(XrplCore)
|
||||
include(XrplProtocolAutogen)
|
||||
include(XrplValidatorKeys)
|
||||
include(XrplInstall)
|
||||
include(XrplPackaging)
|
||||
include(XrplValidatorKeys)
|
||||
|
||||
if(tests)
|
||||
include(CTest)
|
||||
|
||||
@@ -25,6 +25,19 @@ 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}
|
||||
@@ -38,7 +51,7 @@ add_custom_target(
|
||||
${CMAKE_COMMAND} -E env ${package_env}
|
||||
${CMAKE_SOURCE_DIR}/package/build_pkg.sh
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS xrpld
|
||||
DEPENDS xrpld validator-keys
|
||||
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
option(
|
||||
validator_keys
|
||||
"Enables building of validator-keys tool as a separate target (imported via FetchContent)"
|
||||
"Enables building of the vendored validator-keys tool as a separate target"
|
||||
OFF
|
||||
)
|
||||
|
||||
if(validator_keys)
|
||||
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}")
|
||||
include(GNUInstallDirs)
|
||||
|
||||
FetchContent_Declare(
|
||||
validator_keys
|
||||
GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git
|
||||
GIT_TAG "${current_branch}"
|
||||
add_subdirectory(
|
||||
"${CMAKE_SOURCE_DIR}/validator-keys-tool"
|
||||
"${CMAKE_BINARY_DIR}/validator-keys-tool"
|
||||
)
|
||||
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})
|
||||
install(
|
||||
TARGETS validator-keys
|
||||
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Linux Packaging
|
||||
|
||||
This directory contains all files needed to build RPM and Debian packages for `xrpld`.
|
||||
This directory contains all files needed to build RPM and Debian packages for
|
||||
`xrpld`. The packages also include the `validator-keys` utility.
|
||||
|
||||
## Directory layout
|
||||
|
||||
@@ -49,16 +50,17 @@ To print the exact image tags for the current `linux.json`:
|
||||
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 `xrpld`
|
||||
binary artifact, detects the package format from the container, and calls
|
||||
`build_pkg.sh` directly — no CMake configure or build step is needed inside
|
||||
the packaging job.
|
||||
`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
|
||||
build step is needed inside the packaging job.
|
||||
|
||||
### Locally (mirrors CI)
|
||||
|
||||
With an `xrpld` binary already built at `build/xrpld`, run the packaging step
|
||||
inside the same container CI uses. The image tag is derived from `linux.json`
|
||||
so you don't need to hardcode a SHA.
|
||||
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.
|
||||
|
||||
```bash
|
||||
# From the repo root. Pick any image flagged with `"package": true` in
|
||||
@@ -92,6 +94,7 @@ needed, but the host toolchain replaces the pinned CI image:
|
||||
```bash
|
||||
cmake \
|
||||
-Dxrpld=ON \
|
||||
-Dvalidator_keys=ON \
|
||||
-Dxrpld_version=2.4.0-local \
|
||||
-Dtests=OFF \
|
||||
..
|
||||
@@ -111,13 +114,13 @@ to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of
|
||||
environment variable. Flags override env vars; env vars override the built-in
|
||||
defaults. Run `./package/build_pkg.sh --help` for the same table:
|
||||
|
||||
| Flag | Env var | Default | Purpose |
|
||||
| -------------------------- | ------------------- | ----------------------------- | ----------------------------------- |
|
||||
| `--src-dir DIR` | `SRC_DIR` | `$PWD` | repo root |
|
||||
| `--build-dir DIR` | `BUILD_DIR` | `$PWD/build` | directory holding pre-built `xrpld` |
|
||||
| `--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 |
|
||||
| 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 package format (`deb` or `rpm`) is inferred from the host's package
|
||||
manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). Hosts without one of those
|
||||
@@ -142,7 +145,7 @@ into the staging area, and invokes the platform build tool.
|
||||
### DEB
|
||||
|
||||
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
|
||||
2. Stages the binary, configs, `README.md`, and `LICENSE.md`.
|
||||
2. Stages the binaries, 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 `-`).
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build an RPM or Debian package from a pre-built xrpld binary.
|
||||
# Build an RPM or Debian package from pre-built xrpld and validator-keys
|
||||
# binaries.
|
||||
#
|
||||
# Flags override env vars; env vars override defaults. Env vars are intended
|
||||
# for CMake/systemd/CI integration; flags are for explicit invocation.
|
||||
@@ -12,7 +13,7 @@ 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 xrpld [BUILD_DIR; default: $PWD/build]
|
||||
--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]
|
||||
@@ -133,6 +134,7 @@ 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"
|
||||
|
||||
@@ -20,4 +20,5 @@ Depends:
|
||||
Description: XRP Ledger daemon
|
||||
Reference implementation of the XRP Ledger protocol.
|
||||
Participates in the peer-to-peer network, processes transactions,
|
||||
and maintains a local ledger copy.
|
||||
and maintains a local ledger copy. Includes validator-keys for
|
||||
validator key management.
|
||||
|
||||
@@ -19,6 +19,7 @@ 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
|
||||
install -D -m 0755 update-xrpld debian/xrpld/usr/libexec/xrpld/update-xrpld
|
||||
|
||||
@@ -21,6 +21,8 @@ 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
|
||||
:
|
||||
@@ -30,6 +32,7 @@ transactions, and maintains the ledger database.
|
||||
|
||||
%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
|
||||
|
||||
@@ -77,6 +80,7 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
%dir %{_libexecdir}/%{name}
|
||||
|
||||
%{_bindir}/%{name}
|
||||
%{_bindir}/validator-keys
|
||||
|
||||
%config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg
|
||||
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt
|
||||
|
||||
2
tests/conan/.gitignore
vendored
Normal file
2
tests/conan/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Conan test_package build output (cmake_layout)
|
||||
/build/
|
||||
@@ -1,12 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
set(name example)
|
||||
set(name validator-keys-conan-test)
|
||||
set(version 0.1.0)
|
||||
|
||||
project(${name} VERSION ${version} LANGUAGES CXX)
|
||||
|
||||
find_package(xrpl CONFIG REQUIRED)
|
||||
|
||||
add_executable(example)
|
||||
target_sources(example PRIVATE src/example.cpp)
|
||||
target_link_libraries(example PRIVATE xrpl::libxrpl)
|
||||
# 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}"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from conan.tools.build import can_run
|
||||
from conan.tools.cmake import CMake, cmake_layout
|
||||
@@ -6,15 +6,13 @@ from conan.tools.cmake import CMake, cmake_layout
|
||||
from conan import ConanFile
|
||||
|
||||
|
||||
class Example(ConanFile):
|
||||
name = "example"
|
||||
class ValidatorKeysConanTest(ConanFile):
|
||||
name = "validator-keys-conan-test"
|
||||
license = "ISC"
|
||||
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,19 +23,20 @@ class Example(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()
|
||||
|
||||
def package(self):
|
||||
cmake = CMake(self)
|
||||
cmake.install()
|
||||
cmake.build(target="validator-keys")
|
||||
|
||||
def test(self):
|
||||
if can_run(self):
|
||||
cmd_path = Path(self.build_folder) / self.cpp.build.bindir / "example"
|
||||
self.run(cmd_path, env="conanrun")
|
||||
cmd = os.path.join(self.cpp.build.bindir, "validator-keys")
|
||||
self.run(f'"{cmd}" --unittest', env="conanrun")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
4
validator-keys-tool/.git-blame-ignore-revs
Normal file
4
validator-keys-tool/.git-blame-ignore-revs
Normal file
@@ -0,0 +1,4 @@
|
||||
# This feature requires Git >= 2.24
|
||||
# To use it by default in git blame:
|
||||
# git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
8ae260cb466d4cd0d4db378e5ce0acb8e4432f7c
|
||||
34
validator-keys-tool/CMakeLists.txt
Normal file
34
validator-keys-tool/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
cmake_minimum_required(VERSION 3.11)
|
||||
project(validator-keys-tool)
|
||||
|
||||
#[===========================================[
|
||||
This project is built as part of the rippled
|
||||
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()
|
||||
27
validator-keys-tool/README.md
Normal file
27
validator-keys-tool/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# validator-keys-tool
|
||||
|
||||
Rippled validator key generation tool
|
||||
|
||||
## Build
|
||||
|
||||
If you do not have package `xrpl` in your local Conan cache, it can be added by following the instructions in the [BUILD.md](https://github.com/XRPLF/rippled/blob/master/BUILD.md#patched-recipes) file in the rippled GitHub repository.
|
||||
|
||||
The build requirements and commands are the exact same as
|
||||
[those](https://github.com/XRPLF/rippled/blob/develop/BUILD.md) for rippled.
|
||||
In short:
|
||||
|
||||
```
|
||||
mkdir .build
|
||||
cd .build
|
||||
conan install .. --output-folder . --build missing
|
||||
cmake -DCMAKE_POLICY_DEFAULT_CMP0091=NEW \
|
||||
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=conan_toolchain.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
..
|
||||
cmake --build .
|
||||
./validator-keys --unittest # or ctest --test-dir .
|
||||
```
|
||||
|
||||
## Guide
|
||||
|
||||
[Validator Keys Tool Guide](doc/validator-keys-tool-guide.md)
|
||||
25
validator-keys-tool/RELEASENOTES.md
Normal file
25
validator-keys-tool/RELEASENOTES.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Release Notes
|
||||
|
||||
# Change Log
|
||||
|
||||
# Releases
|
||||
|
||||
## Version 0.3.2
|
||||
|
||||
This release overhauls the Travis CI configuration to cover more cases more robustly, and fixes a Windows build error introduced in 0.3.1.
|
||||
|
||||
### New and Improved Features
|
||||
|
||||
- Restructure Travis CI builds to use rippled's infrastructure [[#16](https://github.com/ripple/validator-keys-tool/pull/16)].
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Restores the windows.h include removed in 0.3.1, which is required for Windows builds.
|
||||
|
||||
## Version 0.3.1
|
||||
|
||||
This version brings the code up to date with the rippled code base's internal APIs and structures.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Update includes paths [[#14](https://github.com/ripple/validator-keys-tool/pull/14)].
|
||||
137
validator-keys-tool/cmake/KeysCov.cmake
Normal file
137
validator-keys-tool/cmake/KeysCov.cmake
Normal file
@@ -0,0 +1,137 @@
|
||||
#[===================================================================[
|
||||
coverage report target
|
||||
|
||||
Copied from rippled https://github.com/ripple/rippled/blob/develop/Builds/CMake/RippledCov.cmake
|
||||
#]===================================================================]
|
||||
|
||||
# cspell: words xcrun
|
||||
|
||||
if(coverage)
|
||||
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()
|
||||
else()
|
||||
message(STATUS "Coverage: neither clang nor gcc")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Coverage disabled")
|
||||
endif()
|
||||
87
validator-keys-tool/cmake/KeysInterface.cmake
Normal file
87
validator-keys-tool/cmake/KeysInterface.cmake
Normal file
@@ -0,0 +1,87 @@
|
||||
#[===================================================================[
|
||||
rippled compile options/settings via an interface library
|
||||
#]===================================================================]
|
||||
|
||||
# cspell: words Wsuggest fprofile ftest
|
||||
|
||||
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}>:RIPPLE_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()
|
||||
103
validator-keys-tool/cmake/KeysSanity.cmake
Normal file
103
validator-keys-tool/cmake/KeysSanity.cmake
Normal file
@@ -0,0 +1,103 @@
|
||||
#[===================================================================[
|
||||
convenience variables and sanity checks
|
||||
#]===================================================================]
|
||||
|
||||
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()
|
||||
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE)
|
||||
if(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()
|
||||
|
||||
get_directory_property(has_parent PARENT_DIRECTORY)
|
||||
if(has_parent)
|
||||
set(is_root_project OFF)
|
||||
else()
|
||||
set(is_root_project ON)
|
||||
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
|
||||
"Rippled requires a 64 bit target architecture.\n"
|
||||
"The most likely cause of this warning is trying to build rippled with a 32-bit OS."
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE AND NOT HOMEBREW)
|
||||
find_program(HOMEBREW brew)
|
||||
endif()
|
||||
117
validator-keys-tool/doc/validator-keys-tool-guide.md
Normal file
117
validator-keys-tool/doc/validator-keys-tool-guide.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Validator Keys Tool Guide
|
||||
|
||||
<!-- cspell: words Iiwib hvssbqmgz -->
|
||||
|
||||
This guide explains how to set up a validator so its public key does not have to
|
||||
change if the rippled 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 a rippled 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 rippled 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 rippled.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 rippled 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 rippled.
|
||||
|
||||
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 rippled.cfg file with these values and restart rippled:
|
||||
|
||||
# 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 rippled. 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
|
||||
```
|
||||
276
validator-keys-tool/src/ValidatorKeys.cpp
Normal file
276
validator-keys-tool/src/ValidatorKeys.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
#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 <fstream>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
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&)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
std::ofstream o(keyFile.string(), std::ios_base::trunc);
|
||||
if (o.fail())
|
||||
throw std::runtime_error("Cannot open key file: " + keyFile.string());
|
||||
|
||||
o << 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
|
||||
158
validator-keys-tool/src/ValidatorKeys.h
Normal file
158
validator-keys-tool/src/ValidatorKeys.h
Normal file
@@ -0,0 +1,158 @@
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#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
|
||||
455
validator-keys-tool/src/ValidatorKeysTool.cpp
Normal file
455
validator-keys-tool/src/ValidatorKeysTool.cpp
Normal file
@@ -0,0 +1,455 @@
|
||||
#include <ValidatorKeysTool.h>
|
||||
|
||||
// cspell: words STRINGIZE
|
||||
|
||||
#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>
|
||||
|
||||
#ifdef BOOST_MSVC
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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 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 rippled.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();
|
||||
auto const len = 72;
|
||||
for (auto i = 0; i < tokenStr.size(); i += len)
|
||||
std::cout << tokenStr.substr(i, len) << 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 rippled.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";
|
||||
|
||||
auto const len = 72;
|
||||
for (auto i = 0; i < revocation.size(); i += len)
|
||||
std::cout << revocation.substr(i, len) << 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 rippled.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();
|
||||
auto const len = 72;
|
||||
for (auto i = 0; i < tokenStr.size(); i += len)
|
||||
std::cout << tokenStr.substr(i, len) << 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
|
||||
}
|
||||
30
validator-keys-tool/src/ValidatorKeysTool.h
Normal file
30
validator-keys-tool/src/ValidatorKeysTool.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#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);
|
||||
58
validator-keys-tool/src/test/KeyFileGuard.h
Normal file
58
validator-keys-tool/src/test/KeyFileGuard.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
Write a key file dir and remove when done.
|
||||
*/
|
||||
class KeyFileGuard
|
||||
{
|
||||
private:
|
||||
using path = boost::filesystem::path;
|
||||
path subDir_;
|
||||
beast::unit_test::Suite& test_;
|
||||
|
||||
auto
|
||||
rmDir(path const& toRm)
|
||||
{
|
||||
if (is_directory(toRm))
|
||||
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)
|
||||
: subDir_(subDir), test_(test)
|
||||
{
|
||||
using namespace boost::filesystem;
|
||||
|
||||
if (!exists(subDir_))
|
||||
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
|
||||
{
|
||||
using namespace boost::filesystem;
|
||||
|
||||
rmDir(subDir_);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
// if we throw here, just let it die.
|
||||
test_.log << "Error in ~KeyFileGuard: " << e.what() << std::endl;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
287
validator-keys-tool/src/test/ValidatorKeysTool_test.cpp
Normal file
287
validator-keys-tool/src/test/ValidatorKeysTool_test.cpp
Normal file
@@ -0,0 +1,287 @@
|
||||
#include <test/KeyFileGuard.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <ValidatorKeys.h>
|
||||
#include <ValidatorKeysTool.h>
|
||||
|
||||
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(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& 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(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(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
|
||||
374
validator-keys-tool/src/test/ValidatorKeys_test.cpp
Normal file
374
validator-keys-tool/src/test/ValidatorKeys_test.cpp
Normal file
@@ -0,0 +1,374 @@
|
||||
#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 <ValidatorKeys.h>
|
||||
|
||||
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& e)
|
||||
{
|
||||
BEAST_EXPECT(e.what() == expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
std::array<KeyType, 2> const keyTypes{{KeyType::Ed25519, KeyType::Secp256k1}};
|
||||
|
||||
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& 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& 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));
|
||||
|
||||
auto fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
BEAST_EXPECT(keys == fileKeys);
|
||||
|
||||
// Overwrite file with new sequence
|
||||
keys.createValidatorToken(KeyType::Secp256k1);
|
||||
keys.writeToFile(keyFile);
|
||||
|
||||
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& 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& 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
|
||||
Reference in New Issue
Block a user