From 8ba6053ebdc529b1b4f16d7b6aafdf48230d7ed7 Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Tue, 16 Jun 2026 10:47:06 -0700 Subject: [PATCH 1/3] ci: test validator keys tool as libxrpl consumer --- .github/scripts/strategy-matrix/linux.json | 6 +- .github/workflows/on-pr.yml | 9 + .github/workflows/on-tag.yml | 5 + .github/workflows/on-trigger.yml | 9 +- .../workflows/reusable-build-test-config.yml | 14 +- .github/workflows/reusable-package.yml | 4 +- .../workflows/reusable-test-conan-package.yml | 35 ++ CMakeLists.txt | 2 +- cmake/XrplPackaging.cmake | 15 +- cmake/XrplValidatorKeys.cmake | 22 +- package/README.md | 35 +- package/build_pkg.sh | 6 +- package/debian/control | 3 +- package/debian/rules | 1 + package/rpm/xrpld.spec | 4 + tests/conan/.gitignore | 2 + tests/conan/CMakeLists.txt | 18 +- tests/conan/conanfile.py | 23 +- tests/conan/src/example.cpp | 10 - validator-keys-tool/.git-blame-ignore-revs | 4 + validator-keys-tool/CMakeLists.txt | 34 ++ validator-keys-tool/README.md | 27 ++ validator-keys-tool/RELEASENOTES.md | 25 + validator-keys-tool/cmake/KeysCov.cmake | 137 ++++++ validator-keys-tool/cmake/KeysInterface.cmake | 87 ++++ validator-keys-tool/cmake/KeysSanity.cmake | 103 ++++ .../doc/validator-keys-tool-guide.md | 117 +++++ validator-keys-tool/src/ValidatorKeys.cpp | 276 +++++++++++ validator-keys-tool/src/ValidatorKeys.h | 158 ++++++ validator-keys-tool/src/ValidatorKeysTool.cpp | 455 ++++++++++++++++++ validator-keys-tool/src/ValidatorKeysTool.h | 30 ++ validator-keys-tool/src/test/KeyFileGuard.h | 58 +++ .../src/test/ValidatorKeysTool_test.cpp | 287 +++++++++++ .../src/test/ValidatorKeys_test.cpp | 374 ++++++++++++++ 34 files changed, 2329 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/reusable-test-conan-package.yml create mode 100644 tests/conan/.gitignore delete mode 100644 tests/conan/src/example.cpp create mode 100644 validator-keys-tool/.git-blame-ignore-revs create mode 100644 validator-keys-tool/CMakeLists.txt create mode 100644 validator-keys-tool/README.md create mode 100644 validator-keys-tool/RELEASENOTES.md create mode 100644 validator-keys-tool/cmake/KeysCov.cmake create mode 100644 validator-keys-tool/cmake/KeysInterface.cmake create mode 100644 validator-keys-tool/cmake/KeysSanity.cmake create mode 100644 validator-keys-tool/doc/validator-keys-tool-guide.md create mode 100644 validator-keys-tool/src/ValidatorKeys.cpp create mode 100644 validator-keys-tool/src/ValidatorKeys.h create mode 100644 validator-keys-tool/src/ValidatorKeysTool.cpp create mode 100644 validator-keys-tool/src/ValidatorKeysTool.h create mode 100644 validator-keys-tool/src/test/KeyFileGuard.h create mode 100644 validator-keys-tool/src/test/ValidatorKeysTool_test.cpp create mode 100644 validator-keys-tool/src/test/ValidatorKeys_test.cpp diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index a9b85b766a7..faac3b100ff 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -50,7 +50,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "extra_cmake_args": "-Dvalidator_keys=ON" } ], @@ -58,7 +59,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "extra_cmake_args": "-Dvalidator_keys=ON" } ] }, diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 4b2edeb93dc..e30133bc744 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -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 @@ -145,10 +147,16 @@ jobs: if: ${{ needs.should-run.outputs.go == 'true' }} uses: ./.github/workflows/reusable-package.yml + test-conan-package: + needs: should-run + if: ${{ needs.should-run.outputs.go == 'true' }} + uses: ./.github/workflows/reusable-test-conan-package.yml + upload-recipe: needs: - should-run - build-test + - test-conan-package # Only run when committing to a PR that targets a release branch. if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }} uses: ./.github/workflows/reusable-upload-recipe.yml @@ -180,6 +188,7 @@ jobs: - clang-tidy - build-test - package + - test-conan-package - upload-recipe - notify-clio runs-on: ubuntu-latest diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 42d5827cab0..d6d64597f4d 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -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 }} diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 74bca820198..3659601c326 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -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" @@ -91,8 +93,13 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + test-conan-package: + uses: ./.github/workflows/reusable-test-conan-package.yml + upload-recipe: - needs: build-test + needs: + - build-test + - test-conan-package # 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 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 95e6b0cbe29..ba52eae7804 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -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 diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 0e3f6570066..25409a1278b 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -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: diff --git a/.github/workflows/reusable-test-conan-package.yml b/.github/workflows/reusable-test-conan-package.yml new file mode 100644 index 00000000000..3ae4a109d5c --- /dev/null +++ b/.github/workflows/reusable-test-conan-package.yml @@ -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)" diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dbe60a220e..a9988cbab71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,9 +133,9 @@ endif() include(XrplCore) include(XrplProtocolAutogen) +include(XrplValidatorKeys) include(XrplInstall) include(XrplPackaging) -include(XrplValidatorKeys) if(tests) include(CTest) diff --git a/cmake/XrplPackaging.cmake b/cmake/XrplPackaging.cmake index fe885c200c8..972002afcb8 100644 --- a/cmake/XrplPackaging.cmake +++ b/cmake/XrplPackaging.cmake @@ -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 ) diff --git a/cmake/XrplValidatorKeys.cmake b/cmake/XrplValidatorKeys.cmake index 0e511b6a88b..253b72ead44 100644 --- a/cmake/XrplValidatorKeys.cmake +++ b/cmake/XrplValidatorKeys.cmake @@ -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() diff --git a/package/README.md b/package/README.md index 63c2ab88fc0..7ebc4b79ba2 100644 --- a/package/README.md +++ b/package/README.md @@ -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 @@ -48,16 +49,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 @@ -91,6 +93,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 \ .. @@ -110,13 +113,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 @@ -141,7 +144,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 `-`). diff --git a/package/build_pkg.sh b/package/build_pkg.sh index e2ec8fee3d5..b9d79a1662f 100755 --- a/package/build_pkg.sh +++ b/package/build_pkg.sh @@ -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] @@ -134,6 +135,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" diff --git a/package/debian/control b/package/debian/control index 45d2acbbea5..570765024fb 100644 --- a/package/debian/control +++ b/package/debian/control @@ -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. diff --git a/package/debian/rules b/package/debian/rules index 16574bca3fd..8f880b81927 100644 --- a/package/debian/rules +++ b/package/debian/rules @@ -18,6 +18,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 diff --git a/package/rpm/xrpld.spec b/package/rpm/xrpld.spec index 5595fd0d8da..33856200c03 100644 --- a/package/rpm/xrpld.spec +++ b/package/rpm/xrpld.spec @@ -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 @@ -71,6 +74,7 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : %dir %{_sysconfdir}/%{name} %{_bindir}/%{name} +%{_bindir}/validator-keys %config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg %config(noreplace) %{_sysconfdir}/%{name}/validators.txt diff --git a/tests/conan/.gitignore b/tests/conan/.gitignore new file mode 100644 index 00000000000..6f06fd1f5da --- /dev/null +++ b/tests/conan/.gitignore @@ -0,0 +1,2 @@ +# Conan test_package build output (cmake_layout) +/build/ diff --git a/tests/conan/CMakeLists.txt b/tests/conan/CMakeLists.txt index 871c4c60c01..6d4553f3ffa 100644 --- a/tests/conan/CMakeLists.txt +++ b/tests/conan/CMakeLists.txt @@ -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}" +) diff --git a/tests/conan/conanfile.py b/tests/conan/conanfile.py index 71e43ef25a1..a4d67ba4b7b 100644 --- a/tests/conan/conanfile.py +++ b/tests/conan/conanfile.py @@ -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 import ConanFile -class Example(ConanFile): - name = "example" +class ValidatorKeysConanTest(ConanFile): + name = "validator-keys-conan-test" license = "ISC" author = "John Freeman , Michael Legleux - -#include - -int -main(int argc, char const** argv) -{ - std::printf("%s\n", xrpl::BuildInfo::getVersionString().c_str()); - return 0; -} diff --git a/validator-keys-tool/.git-blame-ignore-revs b/validator-keys-tool/.git-blame-ignore-revs new file mode 100644 index 00000000000..1ad2552abdf --- /dev/null +++ b/validator-keys-tool/.git-blame-ignore-revs @@ -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 diff --git a/validator-keys-tool/CMakeLists.txt b/validator-keys-tool/CMakeLists.txt new file mode 100644 index 00000000000..9bf479104bf --- /dev/null +++ b/validator-keys-tool/CMakeLists.txt @@ -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() diff --git a/validator-keys-tool/README.md b/validator-keys-tool/README.md new file mode 100644 index 00000000000..501406753b6 --- /dev/null +++ b/validator-keys-tool/README.md @@ -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) diff --git a/validator-keys-tool/RELEASENOTES.md b/validator-keys-tool/RELEASENOTES.md new file mode 100644 index 00000000000..4d4c1b4a9f5 --- /dev/null +++ b/validator-keys-tool/RELEASENOTES.md @@ -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)]. diff --git a/validator-keys-tool/cmake/KeysCov.cmake b/validator-keys-tool/cmake/KeysCov.cmake new file mode 100644 index 00000000000..987af0fea4c --- /dev/null +++ b/validator-keys-tool/cmake/KeysCov.cmake @@ -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$<$:=${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 + $ ${extract_pattern} + # generate html report + COMMAND + ${LLVM_COV} show -format=html + -output-dir=${CMAKE_BINARY_DIR}/coverage + -instr-profile=rip.profdata $ + ${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$<$:=${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() diff --git a/validator-keys-tool/cmake/KeysInterface.cmake b/validator-keys-tool/cmake/KeysInterface.cmake new file mode 100644 index 00000000000..2badcdb4fe5 --- /dev/null +++ b/validator-keys-tool/cmake/KeysInterface.cmake @@ -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 + $<$: + BOOST_ASIO_NO_DEPRECATED + BOOST_FILESYSTEM_NO_DEPRECATED + > + $<$>: + BOOST_COROUTINES_NO_DEPRECATION_WARNING + BOOST_BEAST_ALLOW_DEPRECATED + BOOST_FILESYSTEM_DEPRECATED + > + $<$: + USE_BEAST_HASHER + > + $<$:BEAST_NO_UNIT_TEST_INLINE=1> + $<$:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1> + $<$:RIPPLE_SINGLE_IO_SERVICE_THREAD=1> +) +target_compile_options( + keys_opts + INTERFACE + $<$,$>:-Wsuggest-override> + $<$:-fno-omit-frame-pointer> + $<$,$>:-fprofile-arcs + -ftest-coverage> + $<$,$>:-fprofile-instr-generate + -fcoverage-mapping> + $<$:-pg> + $<$,$>:-p> +) + +target_link_libraries( + keys_opts + INTERFACE + $<$,$>:-fprofile-arcs + -ftest-coverage> + $<$,$>:-fprofile-instr-generate + -fcoverage-mapping> + $<$:-pg> + $<$,$>:-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 + $<$:-O1> + ${SAN_FLAG} + -fno-omit-frame-pointer + ) + target_compile_definitions( + keys_opts + INTERFACE + $<$:SANITIZER=ASAN> + $<$:SANITIZER=TSAN> + $<$:SANITIZER=MSAN> + $<$:SANITIZER=UBSAN> + ) + target_link_libraries(keys_opts INTERFACE ${SAN_FLAG} ${SAN_LIB}) +endif() diff --git a/validator-keys-tool/cmake/KeysSanity.cmake b/validator-keys-tool/cmake/KeysSanity.cmake new file mode 100644 index 00000000000..c1e5ea0f875 --- /dev/null +++ b/validator-keys-tool/cmake/KeysSanity.cmake @@ -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() diff --git a/validator-keys-tool/doc/validator-keys-tool-guide.md b/validator-keys-tool/doc/validator-keys-tool-guide.md new file mode 100644 index 00000000000..8d0839a8d00 --- /dev/null +++ b/validator-keys-tool/doc/validator-keys-tool-guide.md @@ -0,0 +1,117 @@ +# Validator Keys Tool Guide + + + +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 +``` diff --git a/validator-keys-tool/src/ValidatorKeys.cpp b/validator-keys-tool/src/ValidatorKeys.cpp new file mode 100644 index 00000000000..70e348b157e --- /dev/null +++ b/validator-keys-tool/src/ValidatorKeys.cpp @@ -0,0 +1,276 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +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 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(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 +ValidatorKeys::createValidatorToken(KeyType const& keyType) +{ + if (revoked() || std::numeric_limits::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::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 '-' + "(? +#include + +#include + +#include +#include +#include + +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 p) : publicKey(p.first), secretKey(p.second) + { + } + }; + + std::vector 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 + 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 + manifest() const + { + return manifest_; + } + + /** Returns the sequence number of the last manifest generated. */ + std::uint32_t + sequence() const + { + return tokenSequence_; + } +}; + +} // namespace xrpl diff --git a/validator-keys-tool/src/ValidatorKeysTool.cpp b/validator-keys-tool/src/ValidatorKeysTool.cpp new file mode 100644 index 00000000000..8fa322f9964 --- /dev/null +++ b/validator-keys-tool/src/ValidatorKeysTool.cpp @@ -0,0 +1,455 @@ +#include + +// cspell: words STRINGIZE + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#ifdef BOOST_MSVC +#include +#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 const& args, + boost::filesystem::path const& keyFile) +{ + using namespace std; + + static map::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] [ ...]\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 Sign string with validator " + "key.\n" + " show_manifest [hex|base64] Displays the last generated " + "manifest\n" + " set_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(), "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(), "Command.")( + "arguments", + po::value>()->default_value(std::vector(), "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() : defaultKeyFile; + + return runCommand( + vm["command"].as(), + vm["arguments"].as>(), + keyFile); + } + catch (std::exception const& e) + { + std::cerr << e.what() << "\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; + // LCOV_EXCL_STOP +} diff --git a/validator-keys-tool/src/ValidatorKeysTool.h b/validator-keys-tool/src/ValidatorKeysTool.h new file mode 100644 index 00000000000..2a3a925e0d7 --- /dev/null +++ b/validator-keys-tool/src/ValidatorKeysTool.h @@ -0,0 +1,30 @@ +#include + +#include + +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 const& arg, + boost::filesystem::path const& keyFile); diff --git a/validator-keys-tool/src/test/KeyFileGuard.h b/validator-keys-tool/src/test/KeyFileGuard.h new file mode 100644 index 00000000000..a3ad8ea8922 --- /dev/null +++ b/validator-keys-tool/src/test/KeyFileGuard.h @@ -0,0 +1,58 @@ +#include + +#include + +#include + +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 diff --git a/validator-keys-tool/src/test/ValidatorKeysTool_test.cpp b/validator-keys-tool/src/test/ValidatorKeysTool_test.cpp new file mode 100644 index 00000000000..89736d7aae6 --- /dev/null +++ b/validator-keys-tool/src/test/ValidatorKeysTool_test.cpp @@ -0,0 +1,287 @@ +#include + +#include + +#include +#include + +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::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 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 const noArgs; + std::vector const oneArg = {"some data"}; + std::vector 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 diff --git a/validator-keys-tool/src/test/ValidatorKeys_test.cpp b/validator-keys-tool/src/test/ValidatorKeys_test.cpp new file mode 100644 index 00000000000..0adecf50197 --- /dev/null +++ b/validator-keys-tool/src/test/ValidatorKeys_test.cpp @@ -0,0 +1,374 @@ +#include + +#include +#include +#include +#include + +#include + +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 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::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(st, sfSigningPubKey); + BEAST_EXPECT(tpk); + BEAST_EXPECT(*tpk == tokenPublicKey); + BEAST_EXPECT(verify(st, HashPrefix::Manifest, tokenPublicKey)); + + auto const pk = get(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::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::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 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("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 From 894f6906f5661846133f8ff0dd4cdb21c760c2d7 Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Thu, 18 Jun 2026 11:33:56 -0700 Subject: [PATCH 2/3] fix: update validator-keys naming --- tests/conan/CMakeLists.txt | 3 +-- validator-keys-tool/CMakeLists.txt | 2 +- validator-keys-tool/README.md | 19 ++++++++++--------- validator-keys-tool/RELEASENOTES.md | 4 ++-- validator-keys-tool/cmake/KeysInterface.cmake | 2 +- validator-keys-tool/cmake/KeysSanity.cmake | 4 ++-- .../doc/validator-keys-tool-guide.md | 16 ++++++++-------- validator-keys-tool/src/ValidatorKeysTool.cpp | 6 +++--- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/conan/CMakeLists.txt b/tests/conan/CMakeLists.txt index 6d4553f3ffa..8e3d8301417 100644 --- a/tests/conan/CMakeLists.txt +++ b/tests/conan/CMakeLists.txt @@ -1,9 +1,8 @@ cmake_minimum_required(VERSION 3.21) set(name validator-keys-conan-test) -set(version 0.1.0) -project(${name} VERSION ${version} LANGUAGES CXX) +project(${name} LANGUAGES CXX) find_package(xrpl CONFIG REQUIRED) diff --git a/validator-keys-tool/CMakeLists.txt b/validator-keys-tool/CMakeLists.txt index 9bf479104bf..4504b7c2a28 100644 --- a/validator-keys-tool/CMakeLists.txt +++ b/validator-keys-tool/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.11) project(validator-keys-tool) #[===========================================[ - This project is built as part of the rippled + This project is built as part of the xrpld repository's Conan test package. The parent project calls find_package(xrpl) and adds this directory, providing the xrpl::libxrpl target. diff --git a/validator-keys-tool/README.md b/validator-keys-tool/README.md index 501406753b6..80d75613fb1 100644 --- a/validator-keys-tool/README.md +++ b/validator-keys-tool/README.md @@ -1,27 +1,28 @@ # validator-keys-tool -Rippled validator key generation tool +Xrpld 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: +Build from the xrpld repository root, enabling the optional +`validator_keys` target: ``` mkdir .build cd .build -conan install .. --output-folder . --build missing +conan install .. --output-folder . --build missing --settings build_type=Release cmake -DCMAKE_POLICY_DEFAULT_CMP0091=NEW \ -DCMAKE_TOOLCHAIN_FILE:FILEPATH=conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ + -Dvalidator_keys=ON \ .. -cmake --build . -./validator-keys --unittest # or ctest --test-dir . +cmake --build . --target validator-keys +./validator-keys --unittest ``` +The Conan test package in `tests/conan` builds the same target against the +exported `xrpl` package. + ## Guide [Validator Keys Tool Guide](doc/validator-keys-tool-guide.md) diff --git a/validator-keys-tool/RELEASENOTES.md b/validator-keys-tool/RELEASENOTES.md index 4d4c1b4a9f5..3681960b992 100644 --- a/validator-keys-tool/RELEASENOTES.md +++ b/validator-keys-tool/RELEASENOTES.md @@ -10,7 +10,7 @@ This release overhauls the Travis CI configuration to cover more cases more robu ### New and Improved Features -- Restructure Travis CI builds to use rippled's infrastructure [[#16](https://github.com/ripple/validator-keys-tool/pull/16)]. +- Restructure Travis CI builds to use xrpld's infrastructure [[#16](https://github.com/ripple/validator-keys-tool/pull/16)]. ### Bug Fixes @@ -18,7 +18,7 @@ This release overhauls the Travis CI configuration to cover more cases more robu ## Version 0.3.1 -This version brings the code up to date with the rippled code base's internal APIs and structures. +This version brings the code up to date with the xrpld code base's internal APIs and structures. ### Bug Fixes diff --git a/validator-keys-tool/cmake/KeysInterface.cmake b/validator-keys-tool/cmake/KeysInterface.cmake index 2badcdb4fe5..f75840814b1 100644 --- a/validator-keys-tool/cmake/KeysInterface.cmake +++ b/validator-keys-tool/cmake/KeysInterface.cmake @@ -24,7 +24,7 @@ target_compile_definitions( > $<$:BEAST_NO_UNIT_TEST_INLINE=1> $<$:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1> - $<$:RIPPLE_SINGLE_IO_SERVICE_THREAD=1> + $<$:XRPL_SINGLE_IO_SERVICE_THREAD=1> ) target_compile_options( keys_opts diff --git a/validator-keys-tool/cmake/KeysSanity.cmake b/validator-keys-tool/cmake/KeysSanity.cmake index c1e5ea0f875..f74144ad8b6 100644 --- a/validator-keys-tool/cmake/KeysSanity.cmake +++ b/validator-keys-tool/cmake/KeysSanity.cmake @@ -93,8 +93,8 @@ 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." + "validator-keys requires a 64 bit target architecture.\n" + "The most likely cause of this warning is trying to build on a 32-bit OS." ) endif() diff --git a/validator-keys-tool/doc/validator-keys-tool-guide.md b/validator-keys-tool/doc/validator-keys-tool-guide.md index 8d0839a8d00..5ef65da1b1b 100644 --- a/validator-keys-tool/doc/validator-keys-tool-guide.md +++ b/validator-keys-tool/doc/validator-keys-tool-guide.md @@ -3,18 +3,18 @@ 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. +change if the xrpld config and/or server are compromised. A validator uses a public/private key pair. The validator is identified by the public key. The private key should be tightly controlled. It is used to: -- sign tokens authorizing a rippled server to run as the validator identified +- sign tokens authorizing an xrpld server to run as the validator identified by this public key. - sign revocations indicating that the private key has been compromised and the validator public key should no longer be trusted. Each new token invalidates all previous tokens for the validator public key. -The current token needs to be present in the rippled config file. +The current token needs to be present in the xrpld config file. Servers that trust the validator will adapt automatically when the token changes. @@ -50,7 +50,7 @@ validator token: Sample output: ``` - Update rippled.cfg file with these values: + Update xrpld.cfg file with these values: # validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr @@ -65,10 +65,10 @@ Sample output: VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0ifQ== ``` -For a new validator, add the [validator_token] value to the rippled config file. +For a new validator, add the [validator_token] value to the xrpld config file. For a pre-existing validator, replace the old [validator_token] value with the newly generated one. A valid config file may only contain one [validator_token] -value. After the config is updated, restart rippled. +value. After the config is updated, restart xrpld. There is a hard limit of 4,294,967,293 tokens that can be generated for a given validator key pair. @@ -88,7 +88,7 @@ Sample output: ``` WARNING: This will revoke your validator keys! - Update rippled.cfg file with these values and restart rippled: + Update xrpld.cfg file with these values and restart xrpld: # validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr @@ -98,7 +98,7 @@ Sample output: ``` 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 +restart xrpld. Rename the old key file and generate new [validator keys](#validator-keys) and a corresponding [validator token](#validator-token). ## Signing diff --git a/validator-keys-tool/src/ValidatorKeysTool.cpp b/validator-keys-tool/src/ValidatorKeysTool.cpp index 8fa322f9964..65ae9835347 100644 --- a/validator-keys-tool/src/ValidatorKeysTool.cpp +++ b/validator-keys-tool/src/ValidatorKeysTool.cpp @@ -88,7 +88,7 @@ createToken(boost::filesystem::path const& keyFile) // 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 << "Update xrpld.cfg file with these values and restart xrpld:\n\n"; std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; std::cout << "[validator_token]\n"; @@ -118,7 +118,7 @@ createRevocation(boost::filesystem::path const& keyFile) // 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 << "Update xrpld.cfg file with these values and restart xrpld:\n\n"; std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; std::cout << "[validator_key_revocation]\n"; @@ -206,7 +206,7 @@ setDomain(std::string const& domain, boost::filesystem::path const& keyFile) attestDomain(keys); std::cout << "\n"; - std::cout << "You also need to update the rippled.cfg file to add a new\n"; + std::cout << "You also need to update the xrpld.cfg file to add a new\n"; std::cout << "validator token and restart xrpld:\n\n"; std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; From 1f7c2db774f2a59aac468c91d9855d6cca9542e4 Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Thu, 18 Jun 2026 13:33:56 -0700 Subject: [PATCH 3/3] rm vkt releasenotes --- validator-keys-tool/RELEASENOTES.md | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 validator-keys-tool/RELEASENOTES.md diff --git a/validator-keys-tool/RELEASENOTES.md b/validator-keys-tool/RELEASENOTES.md deleted file mode 100644 index 3681960b992..00000000000 --- a/validator-keys-tool/RELEASENOTES.md +++ /dev/null @@ -1,25 +0,0 @@ -# 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 xrpld'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 xrpld code base's internal APIs and structures. - -### Bug Fixes - -- Update includes paths [[#14](https://github.com/ripple/validator-keys-tool/pull/14)].