mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-15 12:01:30 +00:00
Compare commits
18 Commits
yinyi/mirr
...
dangell7/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa789d95fd | ||
|
|
3dc27ca77d | ||
|
|
5cda41fad0 | ||
|
|
88a9bc76bc | ||
|
|
5e7da075fa | ||
|
|
b51d8c1140 | ||
|
|
82f95502ac | ||
|
|
0467fd7144 | ||
|
|
3ffba1678e | ||
|
|
9d2428967d | ||
|
|
7bd7ea79dd | ||
|
|
728db41016 | ||
|
|
ff5aa537e1 | ||
|
|
9403736199 | ||
|
|
028783661d | ||
|
|
21890d9daf | ||
|
|
060957ed39 | ||
|
|
1381483c7a |
@@ -121,6 +121,6 @@ CheckOptions:
|
||||
readability-identifier-naming.PublicMemberSuffix: ""
|
||||
readability-identifier-naming.GlobalFunctionIgnoredRegexp: "^(to_string|hash_append|tuple_hash)$"
|
||||
|
||||
HeaderFilterRegex: '^.*/(tests?|xrpl|xrpld)/.*\.(h|hpp|ipp)$'
|
||||
HeaderFilterRegex: '^.*/(tests?|tools|xrpl|xrpld)/.*\.(h|hpp|ipp)$'
|
||||
ExcludeHeaderFilterRegex: '^.*/protocol_autogen/.*\.(h|hpp)$'
|
||||
WarningsAsErrors: "*"
|
||||
|
||||
@@ -67,9 +67,10 @@ words:
|
||||
- Britto
|
||||
- Btrfs
|
||||
- Buildx
|
||||
- canonicality
|
||||
- canonicalised
|
||||
- canonicality
|
||||
- cctools
|
||||
- CGNAT
|
||||
- changespq
|
||||
- checkme
|
||||
- choco
|
||||
@@ -137,9 +138,11 @@ words:
|
||||
- gpgkey
|
||||
- Hinnant
|
||||
- hotwallet
|
||||
- hvssbqmgz
|
||||
- hwaddress
|
||||
- hwrap
|
||||
- ifndef
|
||||
- Iiwib
|
||||
- inequation
|
||||
- insuf
|
||||
- insuff
|
||||
@@ -152,6 +155,7 @@ words:
|
||||
- jemalloc
|
||||
- jlog
|
||||
- jtnofill
|
||||
- keyfile
|
||||
- keylet
|
||||
- keylets
|
||||
- keyvadb
|
||||
@@ -177,11 +181,10 @@ words:
|
||||
- mathbunnyru
|
||||
- mcmodel
|
||||
- MEMORYSTATUSEX
|
||||
- MPTAMM
|
||||
- MPTDEX
|
||||
- Merkle
|
||||
- misprediction
|
||||
- missingok
|
||||
- MPTAMM
|
||||
- mptbalance
|
||||
- MPTDEX
|
||||
- mptflags
|
||||
@@ -256,8 +259,8 @@ words:
|
||||
- replayer
|
||||
- repodata
|
||||
- repomd
|
||||
- rerandomize
|
||||
- rerandomization
|
||||
- rerandomize
|
||||
- rerandomized
|
||||
- rerandomizes
|
||||
- rerere
|
||||
@@ -280,8 +283,8 @@ words:
|
||||
- rustup
|
||||
- sahyadri
|
||||
- Satoshi
|
||||
- scons
|
||||
- Schnorr
|
||||
- scons
|
||||
- secp
|
||||
- sendq
|
||||
- seqit
|
||||
@@ -311,6 +314,7 @@ words:
|
||||
- stobject
|
||||
- stpath
|
||||
- stpathset
|
||||
- STRINGIZE
|
||||
- sttx
|
||||
- stvar
|
||||
- stvector
|
||||
@@ -346,6 +350,7 @@ words:
|
||||
- unfindable
|
||||
- unflatten
|
||||
- unfund
|
||||
- ungated
|
||||
- unimpair
|
||||
- unroutable
|
||||
- unscalable
|
||||
@@ -385,5 +390,3 @@ words:
|
||||
- xxhash
|
||||
- xxhasher
|
||||
- zstdio
|
||||
- CGNAT
|
||||
- ungated
|
||||
|
||||
@@ -194,6 +194,15 @@ tests.libxrpl > xrpl.resource
|
||||
tests.libxrpl > xrpl.server
|
||||
tests.libxrpl > xrpl.shamap
|
||||
tests.libxrpl > xrpl.tx
|
||||
tests.tools > tools.validator-keys
|
||||
tests.tools > xrpl.basics
|
||||
tests.tools > xrpl.json
|
||||
tests.tools > xrpl.protocol
|
||||
tests.tools > xrpl.server
|
||||
tools.validator-keys > xrpl.basics
|
||||
tools.validator-keys > xrpl.json
|
||||
tools.validator-keys > xrpl.protocol
|
||||
tools.validator-keys > xrpl.server
|
||||
xrpl.conditions > xrpl.basics
|
||||
xrpl.conditions > xrpl.protocol
|
||||
xrpl.config > xrpl.basics
|
||||
|
||||
3
.github/scripts/rename/cmake.sh
vendored
3
.github/scripts/rename/cmake.sh
vendored
@@ -61,9 +61,6 @@ ${SED_COMMAND} -i 's/ripple.pb.h/xrpl.pb.h/' include/xrpl/protocol/messages.h
|
||||
${SED_COMMAND} -i 's/ripple.pb.h/xrpl.pb.h/' BUILD.md
|
||||
${SED_COMMAND} -i 's/ripple.pb.h/xrpl.pb.h/' BUILD.md
|
||||
|
||||
# Restore the name of the validator keys repository.
|
||||
${SED_COMMAND} -i 's@xrpl/validator-keys-tool@ripple/validator-keys-tool@' cmake/XrplValidatorKeys.cmake
|
||||
|
||||
# Ensure the name of the binary and config remain 'rippled' for now.
|
||||
${SED_COMMAND} -i -E 's/xrpld(-example)?\.cfg/rippled\1.cfg/g' cmake/XrplInstall.cmake
|
||||
if grep -q '"xrpld"' cmake/XrplCore.cmake; then
|
||||
|
||||
2
.github/scripts/strategy-matrix/linux.json
vendored
2
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-473fe44",
|
||||
"image_tag": "sha-060957e",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
|
||||
6
.github/workflows/build-nix-images.yml
vendored
6
.github/workflows/build-nix-images.yml
vendored
@@ -5,15 +5,13 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- ".github/workflows/build-nix-images.yml"
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
- "nix/**"
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "!nix/check-tools/*.txt"
|
||||
- "bin/check-tools.sh"
|
||||
- "!nix/check-tools/**"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
pull_request:
|
||||
@@ -25,7 +23,7 @@ on:
|
||||
- "nix/**"
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "!nix/check-tools/*.txt"
|
||||
- "!nix/check-tools/**"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
|
||||
2
.github/workflows/cargo-audit.yml
vendored
2
.github/workflows/cargo-audit.yml
vendored
@@ -34,7 +34,7 @@ permissions:
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to open an issue on scheduled failures.
|
||||
|
||||
2
.github/workflows/publish-docs.yml
vendored
2
.github/workflows/publish-docs.yml
vendored
@@ -41,7 +41,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -277,9 +277,9 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Run the validator-keys tests
|
||||
if: ${{ env.VALIDATOR_KEYS_ENABLED == 'true' }}
|
||||
if: ${{ env.VALIDATOR_KEYS_ENABLED == 'true' && !inputs.build_only }}
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
run: ./validator-keys --unittest
|
||||
run: ./validator_keys_tests
|
||||
|
||||
- name: Upload the validator-keys binary
|
||||
if: ${{ env.PACKAGING_ARTIFACTS_ENABLED == 'true' && env.VALIDATOR_KEYS_ENABLED == 'true' }}
|
||||
|
||||
3
.github/workflows/reusable-clang-tidy.yml
vendored
3
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
needs: [determine-files]
|
||||
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-473fe44"
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-060957e"
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -85,6 +85,7 @@ jobs:
|
||||
-Dtests=ON \
|
||||
-Dwerr=ON \
|
||||
-Dxrpld=ON \
|
||||
-Dvalidator_keys=ON \
|
||||
-Dverify_headers=ON \
|
||||
-Drust=ON \
|
||||
..
|
||||
|
||||
17
.github/workflows/reusable-package.yml
vendored
17
.github/workflows/reusable-package.yml
vendored
@@ -209,6 +209,23 @@ jobs:
|
||||
}
|
||||
echo "package=${package}" >>"${GITHUB_OUTPUT}"
|
||||
|
||||
# Debian 11 went end-of-life on 2026-08-31
|
||||
# (https://www.debian.org/News/2026/20260831) and its packages are
|
||||
# already partly gone from deb.debian.org, so switch to the
|
||||
# snapshot.debian.org entries the image ships commented out in its
|
||||
# sources.list: they are pinned to the snapshot the image was built
|
||||
# from, so they serve every version it needs and never go away.
|
||||
# Snapshots keep their original, long-passed Valid-Until, hence the
|
||||
# disabled check; the retries absorb snapshot.debian.org's throttling.
|
||||
- name: Switch Debian 11 to snapshot.debian.org
|
||||
if: ${{ matrix.image == 'debian:11' }}
|
||||
run: |
|
||||
sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list
|
||||
printf '%s\n' \
|
||||
'Acquire::Check-Valid-Until "false";' \
|
||||
'Acquire::Retries "3";' \
|
||||
>/etc/apt/apt.conf.d/99snapshot
|
||||
|
||||
- name: Install the DEB
|
||||
if: ${{ matrix.package_type == 'deb' }}
|
||||
env:
|
||||
|
||||
14
.github/workflows/reusable-rust.yml
vendored
14
.github/workflows/reusable-rust.yml
vendored
@@ -1,8 +1,9 @@
|
||||
# Clippy, coverage and documentation for the Rust crates in crates/. Each runs
|
||||
# as an independent job on a GitHub-hosted runner, but inside the same container
|
||||
# image used to build the crates in the C++/Corrosion path, so the toolchain
|
||||
# (and therefore the lints, coverage instrumentation and the cargo cache) matches
|
||||
# what production builds use.
|
||||
# (and therefore the lints and the cargo cache) matches what production builds
|
||||
# use. Coverage is the exception: it needs the nightly rustc that honours
|
||||
# #[coverage(off)], which the image carries alongside the pinned stable.
|
||||
#
|
||||
# Rust unit tests are deliberately NOT run here. They run as part of the C++
|
||||
# build (reusable-build-test-config.yml), which already compiles the crates on a
|
||||
@@ -27,7 +28,7 @@ permissions:
|
||||
jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -40,11 +41,14 @@ jobs:
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Use the nightly Rust toolchain
|
||||
run: rust-nightly path >>"${GITHUB_PATH}"
|
||||
|
||||
- name: Use cargo artifacts cache
|
||||
uses: ./.github/actions/cargo-cache
|
||||
|
||||
@@ -66,7 +70,7 @@ jobs:
|
||||
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
2
.github/workflows/reusable-upload-recipe.yml
vendored
2
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -40,7 +40,7 @@ defaults:
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
|
||||
env:
|
||||
REMOTE_NAME: ${{ inputs.remote_name }}
|
||||
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
|
||||
|
||||
@@ -70,6 +70,11 @@ repos:
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false # rustfmt formats the whole workspace
|
||||
- id: check-coverage-attrs
|
||||
name: check Rust coverage attributes
|
||||
entry: ./bin/pre-commit/check_rust_coverage_attrs.py
|
||||
language: python
|
||||
files: ^crates/.*\.rs$
|
||||
|
||||
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
|
||||
rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7
|
||||
|
||||
@@ -38,5 +38,6 @@ Paths below reflect the current layout; update this section if modularization mo
|
||||
- `include/xrpl/` + `src/libxrpl/` — the core protocol library: ledger, shamap, consensus, crypto, json, resource, nodestore, rdb, peerfinder, and `tx/` (transaction application: `Transactor.cpp`, `applySteps.cpp`, invariants, payment paths). `tx/transactors/` has one file per transaction type, grouped by subsystem: `escrow/`, `vault/`, `lending/`, `sponsor/`, `nft/`, `token/` (MPT), `payment_channel/`, `permissioned_domain/`, `dex/`, `oracle/`, `did/`, `credentials/`, `bridge/`, `check/`, `delegate/`, `account/`, `system/`. Any change to transaction-processing behavior must be gated behind an Amendment.
|
||||
- `src/xrpld/` — the server application built on top of `libxrpl`: `app`, `core`, `overlay` (P2P networking), `peerfinder`, `perflog`, `rpc`, `shamap`. `main` builds an `ApplicationImp` implementing `Application`; most components hold a reference to it (`app_`), giving broad cross-component access — expect to trace call chains through `Application&`.
|
||||
- `src/test/` — unit tests mirroring the subsystems above, plus `jtx/` (the transaction-building test DSL — e.g. `jtx/escrow.h`, `jtx/vault.h`, `jtx/sponsor.h`, `jtx/permissioned_dex.h`) and `unit_test/` (the custom test framework itself, derived from Beast).
|
||||
- `src/tests/` — unit tests for `libxrpl` written in `gtest`, gradually replacing the `src/test` equivalents.
|
||||
- `src/tests/` — unit tests for `libxrpl` written in `gtest`, gradually replacing the `src/test` equivalents; `src/tests/tools/` holds the `gtest` suites of the tools below.
|
||||
- `src/tools/` — standalone binaries built on `libxrpl` behind their own CMake option: `validator-keys` (`-Dvalidator_keys=ON`), the validator and publisher key tool, whose library `xrpl.validator-keys` is what its tests link.
|
||||
- `crates/` — a Rust workspace (only built with `-Dxrpld -Drust=ON`) bridged into C++ via `cxxbridge`/the `cxx` crate; currently just a `hello_world` interop scaffold. Requires the Rust toolchain pinned in `rust-toolchain.toml` (the Nix devshell provides it automatically).
|
||||
|
||||
@@ -92,7 +92,8 @@ If you create new source files, they must be organized as follows:
|
||||
- If the files are in any of the `libxrpl` modules, the headers (`.h`) must go
|
||||
under `include/xrpl`, and source (`.cpp`) files must go under
|
||||
`src/libxrpl`.
|
||||
- All other non-test files must go under `src/xrpld`.
|
||||
- All other non-test files must go under `src/xrpld`, except a standalone tool
|
||||
built as its own binary on top of `libxrpl`, which goes under `src/tools`.
|
||||
- New test source files should use `gtest` and go under `src/tests`, unless that isn't possible, in which case they should use our legacy test framework and go under `src/test`.
|
||||
- All benchmark source files must go under `src/benchmarks`.
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
|
||||
check cargo-nextest cargo nextest --version
|
||||
check clippy-driver
|
||||
check rust-analyzer
|
||||
check rust-nightly rust-nightly run rustc --version
|
||||
check rustc
|
||||
check rustfmt
|
||||
fi
|
||||
|
||||
149
bin/pre-commit/check_rust_coverage_attrs.py
Executable file
149
bin/pre-commit/check_rust_coverage_attrs.py
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check that Rust unit tests stay out of the coverage report.
|
||||
|
||||
cargo-llvm-cov instruments the test code along with everything else, so a test
|
||||
module that is not excluded counts its own body as covered and inflates the
|
||||
reported number. Excluding it takes two attributes:
|
||||
|
||||
* every `#[cfg(test)]` module carries
|
||||
`#[cfg_attr(coverage_nightly, coverage(off))]`;
|
||||
* every crate root (lib.rs, main.rs) carries
|
||||
`#![cfg_attr(coverage_nightly, feature(coverage_attribute))]`, which the
|
||||
attribute above needs in order to compile.
|
||||
|
||||
Both are inert outside the coverage job: cargo-llvm-cov defines
|
||||
`coverage_nightly` only when it runs on a nightly toolchain.
|
||||
|
||||
The crate-root gate is checked even in a crate that has no tests yet, because
|
||||
that is what lets the first test module added later carry the attribute without
|
||||
a build failure. Missing it is a hard error, so it cannot go unnoticed; a
|
||||
missing `coverage(off)` fails open, which is why this check exists.
|
||||
|
||||
Matching is on exact attribute text, which works because `cargo fmt` runs over
|
||||
the whole workspace in the hook ahead of this one: rustfmt puts every attribute
|
||||
on its own line and normalizes what is inside it, turning `#[cfg( test )]`
|
||||
and `#[cfg(test,)]` alike into `#[cfg(test)]`. So there is nothing here that
|
||||
parses Rust. The price is that a cfg this file does not spell out literally --
|
||||
`all(test, ...)`, `any(test, ...)`, `not(test)` -- is reported rather than
|
||||
classified, on the grounds that guessing at coverage semantics is how a check
|
||||
like this ends up quietly wrong.
|
||||
|
||||
Usage: ./bin/pre-commit/check_rust_coverage_attrs.py <file1> <file2> ...
|
||||
|
||||
Exit status is non-zero if any violation is found.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
CRATE_ROOTS = {"lib.rs", "main.rs"}
|
||||
|
||||
FEATURE_ATTR = "#![cfg_attr(coverage_nightly, feature(coverage_attribute))]"
|
||||
COVERAGE_OFF_ATTR = "#[cfg_attr(coverage_nightly, coverage(off))]"
|
||||
CFG_TEST_ATTR = "#[cfg(test)]"
|
||||
|
||||
# Any other cfg that mentions `test`. String literals are blanked before this
|
||||
# runs, so `feature = "test"` does not read as the `test` cfg.
|
||||
RE_CFG_MENTIONS_TEST = re.compile(r"^#\[cfg\(.*\btest\b.*\)\]$")
|
||||
RE_STRING = re.compile(r'"(?:[^"\\]|\\.)*"')
|
||||
RE_MOD = re.compile(r"^(?:pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Finding:
|
||||
line: int
|
||||
label: str
|
||||
message: str
|
||||
|
||||
|
||||
def _check_module(attrs: list[str], line: int, name: str) -> list[Finding]:
|
||||
"""Findings for one module, given the attributes attached to it."""
|
||||
if COVERAGE_OFF_ATTR in attrs:
|
||||
return [] # excluded from coverage; which cfg gates it does not matter
|
||||
if CFG_TEST_ATTR in attrs:
|
||||
return [
|
||||
Finding(
|
||||
line,
|
||||
"missing-coverage-off",
|
||||
f"`mod {name}` is #[cfg(test)] but not excluded from coverage; "
|
||||
f"add {COVERAGE_OFF_ATTR}",
|
||||
)
|
||||
]
|
||||
unclassified = [
|
||||
attr for attr in attrs if RE_CFG_MENTIONS_TEST.match(RE_STRING.sub('""', attr))
|
||||
]
|
||||
if unclassified:
|
||||
return [
|
||||
Finding(
|
||||
line,
|
||||
"unclassified-cfg",
|
||||
f"`mod {name}` is gated on {unclassified[0]}, which this check "
|
||||
f"cannot tell apart from a module that ships in the library; "
|
||||
f"add {COVERAGE_OFF_ATTR} if it is test-only, or teach this "
|
||||
f"check the cfg if it is not",
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def _check_test_modules(lines: list[str]) -> list[Finding]:
|
||||
"""Findings for every test module that is not excluded from coverage."""
|
||||
findings: list[Finding] = []
|
||||
attrs: list[str] = []
|
||||
attrs_line = 0
|
||||
for number, raw in enumerate(lines, start=1):
|
||||
stripped = raw.strip()
|
||||
# Blank lines and comments are allowed between an attribute and its item.
|
||||
if not stripped or stripped.startswith("//"):
|
||||
continue
|
||||
if stripped.startswith("#["):
|
||||
if not attrs:
|
||||
attrs_line = number
|
||||
attrs.append(stripped)
|
||||
continue
|
||||
module = RE_MOD.match(stripped)
|
||||
if module is not None and attrs:
|
||||
findings += _check_module(attrs, attrs_line, module.group(1))
|
||||
attrs = []
|
||||
return findings
|
||||
|
||||
|
||||
def _check_crate_root(name: str, lines: list[str]) -> list[Finding]:
|
||||
"""A finding if a crate root is missing the coverage_attribute feature gate."""
|
||||
if name not in CRATE_ROOTS:
|
||||
return []
|
||||
if any(line.strip() == FEATURE_ATTR for line in lines):
|
||||
return []
|
||||
return [
|
||||
Finding(
|
||||
1,
|
||||
"missing-feature-gate",
|
||||
f"crate root is missing {FEATURE_ATTR}",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def check_source(name: str, text: str) -> list[Finding]:
|
||||
"""Findings for one file's contents; `name` is its base name (lib.rs, ...)."""
|
||||
lines = text.splitlines()
|
||||
return _check_crate_root(name, lines) + _check_test_modules(lines)
|
||||
|
||||
|
||||
def check_file(path: Path) -> list[Finding]:
|
||||
return check_source(path.name, path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
total = 0
|
||||
for path in (Path(name) for name in sys.argv[1:]):
|
||||
for finding in check_file(path):
|
||||
total += 1
|
||||
print(f"{path}:{finding.line}: {finding.label}: {finding.message}")
|
||||
return 1 if total else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,43 +1,12 @@
|
||||
option(
|
||||
validator_keys
|
||||
"Enables building of validator-keys tool as a separate target (imported via FetchContent)"
|
||||
"Enables building of the validator-keys tool as a separate target"
|
||||
OFF
|
||||
)
|
||||
|
||||
if(validator_keys)
|
||||
# Own the install destination below rather than relying on another module
|
||||
# having pulled this in first.
|
||||
include(GNUInstallDirs)
|
||||
|
||||
# Pinned to an exact commit, not a branch: the tool ships inside our
|
||||
# packages, so the same xrpld version must always package the same
|
||||
# validator-keys. Bump this deliberately.
|
||||
set(validator_keys_commit "4c0fb75eec9601c711645998c904507e87e910ae")
|
||||
message(STATUS "Using ValidatorKeys commit: ${validator_keys_commit}")
|
||||
|
||||
FetchContent_Declare(
|
||||
validator_keys
|
||||
GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git
|
||||
GIT_TAG "${validator_keys_commit}"
|
||||
)
|
||||
FetchContent_MakeAvailable(validator_keys)
|
||||
# The tool's own CMakeLists excludes the target from 'all' when it is built
|
||||
# as a subproject. Undo that, so validator_keys=ON really does build it.
|
||||
set_target_properties(
|
||||
validator-keys
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
EXCLUDE_FROM_ALL OFF
|
||||
EXCLUDE_FROM_DEFAULT_BUILD OFF
|
||||
)
|
||||
# We ship this binary, so like xrpld it must not keep the Nix store's ELF
|
||||
# loader, or it cannot run on the target distro at all.
|
||||
patch_nix_binary(validator-keys)
|
||||
|
||||
configure_file(
|
||||
"${validator_keys_SOURCE_DIR}/LICENSE"
|
||||
"${CMAKE_BINARY_DIR}/validator-keys-LICENSE"
|
||||
COPYONLY
|
||||
)
|
||||
install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
add_subdirectory(src/tools/validator-keys)
|
||||
if(tests)
|
||||
add_subdirectory(src/tests/tools/validator-keys)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -8,6 +8,9 @@ cxx = { version = "1.0.198", features = ["c++20"] }
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage)', 'cfg(coverage_nightly)' ] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
overflow-checks = true
|
||||
|
||||
@@ -8,3 +8,6 @@ crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
cxx.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||
|
||||
#[cxx::bridge(namespace = "rs::hello_world")]
|
||||
mod ffi {
|
||||
extern "Rust" {
|
||||
@@ -8,3 +10,14 @@ mod ffi {
|
||||
pub fn hello_world() -> String {
|
||||
"hello_world".to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hello_world_returns_hello_world() {
|
||||
assert_eq!(hello_world(), "hello_world")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +129,11 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temARRAY_TOO_LARGE,
|
||||
temBAD_TRANSFER_FEE,
|
||||
temINVALID_INNER_BATCH,
|
||||
|
||||
temBAD_MPT,
|
||||
temBAD_CIPHERTEXT,
|
||||
temINVALID_BYTECODE,
|
||||
temTEMP_DISABLED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -179,6 +182,8 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefNO_DST_PARTIAL,
|
||||
tefBAD_PATH_COUNT,
|
||||
tefNO_BYTECODE,
|
||||
tefBYTECODE_NOT_INCLUDED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -370,6 +375,8 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecNO_DELEGATE_PERMISSION = 198,
|
||||
tecBAD_PROOF = 199,
|
||||
tecNO_SPONSOR_PERMISSION = 200,
|
||||
tecOUT_OF_GAS = 201,
|
||||
tecBYTECODE_REJECTED = 202,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -309,6 +309,11 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
|
||||
{sfBaseFeeDrops, SoeOptional},
|
||||
{sfReserveBaseDrops, SoeOptional},
|
||||
{sfReserveIncrementDrops, SoeOptional},
|
||||
// Smart Escrow fields
|
||||
{sfGasLimit, SoeOptional},
|
||||
{sfBytecodeSizeLimit, SoeOptional},
|
||||
{sfGasPrice, SoeOptional},
|
||||
|
||||
{sfPreviousTxnID, SoeOptional},
|
||||
{sfPreviousTxnLgrSeq, SoeOptional},
|
||||
}))
|
||||
@@ -339,6 +344,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({
|
||||
{sfCondition, SoeOptional},
|
||||
{sfCancelAfter, SoeOptional},
|
||||
{sfFinishAfter, SoeOptional},
|
||||
{sfBytecode, SoeOptional},
|
||||
{sfData, SoeOptional},
|
||||
{sfSourceTag, SoeOptional},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
{sfOwnerNode, SoeRequired},
|
||||
|
||||
@@ -123,6 +123,11 @@ TYPED_SFIELD(sfIssuerKeyEpoch, UINT32, 77)
|
||||
TYPED_SFIELD(sfAuditorKeyEpoch, UINT32, 78)
|
||||
TYPED_SFIELD(sfIssuerKeyMirrorEpoch, UINT32, 79)
|
||||
TYPED_SFIELD(sfAuditorKeyMirrorEpoch, UINT32, 80)
|
||||
TYPED_SFIELD(sfGasLimit, UINT32, 81)
|
||||
TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82)
|
||||
TYPED_SFIELD(sfGasPrice, UINT32, 83)
|
||||
TYPED_SFIELD(sfGas, UINT32, 84)
|
||||
TYPED_SFIELD(sfGasUsed, UINT32, 85)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -238,6 +243,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2)
|
||||
TYPED_SFIELD(sfVMReturnCode, INT32, 3)
|
||||
|
||||
// currency amount (common)
|
||||
TYPED_SFIELD(sfAmount, AMOUNT, 1)
|
||||
@@ -322,6 +328,7 @@ TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
|
||||
TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
|
||||
TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -66,11 +66,13 @@ TRANSACTION(ttPAYMENT, 0, Payment,
|
||||
#endif
|
||||
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfDestination, SoeRequired},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfCondition, SoeOptional},
|
||||
{sfCancelAfter, SoeOptional},
|
||||
{sfFinishAfter, SoeOptional},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
{sfBytecode, SoeOptional},
|
||||
{sfData, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction type completes an existing escrow. */
|
||||
@@ -83,6 +85,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({.delegable = Delegation::Delegab
|
||||
{sfFulfillment, SoeOptional},
|
||||
{sfCondition, SoeOptional},
|
||||
{sfCredentialIDs, SoeOptional},
|
||||
{sfGas, SoeOptional},
|
||||
}))
|
||||
|
||||
|
||||
@@ -1161,6 +1164,10 @@ TRANSACTION(ttFEE, 101, SetFee,
|
||||
{sfBaseFeeDrops, SoeOptional},
|
||||
{sfReserveBaseDrops, SoeOptional},
|
||||
{sfReserveIncrementDrops, SoeOptional},
|
||||
// Smart Escrow fields
|
||||
{sfGasLimit, SoeOptional},
|
||||
{sfBytecodeSizeLimit, SoeOptional},
|
||||
{sfGasPrice, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the network's negative UNL
|
||||
|
||||
@@ -174,6 +174,54 @@ public:
|
||||
return this->sle_->isFieldPresent(sfFinishAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBytecode (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getBytecode() const
|
||||
{
|
||||
if (hasBytecode())
|
||||
return this->sle_->at(sfBytecode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBytecode is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBytecode() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfBytecode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfData (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getData() const
|
||||
{
|
||||
if (hasData())
|
||||
return this->sle_->at(sfData);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfData is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasData() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfSourceTag (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -453,6 +501,28 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBytecode (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowBuilder&
|
||||
setBytecode(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBytecode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfData (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowBuilder&
|
||||
setData(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfData] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfSourceTag (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -213,6 +213,78 @@ public:
|
||||
return this->sle_->isFieldPresent(sfReserveIncrementDrops);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasLimit() const
|
||||
{
|
||||
if (hasGasLimit())
|
||||
return this->sle_->at(sfGasLimit);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasLimit() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfGasLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getBytecodeSizeLimit() const
|
||||
{
|
||||
if (hasBytecodeSizeLimit())
|
||||
return this->sle_->at(sfBytecodeSizeLimit);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBytecodeSizeLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBytecodeSizeLimit() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfBytecodeSizeLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasPrice (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasPrice() const
|
||||
{
|
||||
if (hasGasPrice())
|
||||
return this->sle_->at(sfGasPrice);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasPrice is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasPrice() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfGasPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfPreviousTxnID (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -375,6 +447,39 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
FeeSettingsBuilder&
|
||||
setGasLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
FeeSettingsBuilder&
|
||||
setBytecodeSizeLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBytecodeSizeLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasPrice (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
FeeSettingsBuilder&
|
||||
setGasPrice(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasPrice] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfPreviousTxnID (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -58,6 +58,32 @@ public:
|
||||
return this->tx_->at(sfDestination);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfDestinationTag (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getDestinationTag() const
|
||||
{
|
||||
if (hasDestinationTag())
|
||||
{
|
||||
return this->tx_->at(sfDestinationTag);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfDestinationTag is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasDestinationTag() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfDestinationTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
@@ -149,29 +175,55 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfDestinationTag (SoeOptional)
|
||||
* @brief Get sfBytecode (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getDestinationTag() const
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getBytecode() const
|
||||
{
|
||||
if (hasDestinationTag())
|
||||
if (hasBytecode())
|
||||
{
|
||||
return this->tx_->at(sfDestinationTag);
|
||||
return this->tx_->at(sfBytecode);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfDestinationTag is present.
|
||||
* @brief Check if sfBytecode is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasDestinationTag() const
|
||||
hasBytecode() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfDestinationTag);
|
||||
return this->tx_->isFieldPresent(sfBytecode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfData (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getData() const
|
||||
{
|
||||
if (hasData())
|
||||
{
|
||||
return this->tx_->at(sfData);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfData is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasData() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfData);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -232,6 +284,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfDestinationTag (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowCreateBuilder&
|
||||
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfDestinationTag] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
@@ -278,13 +341,24 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfDestinationTag (SoeOptional)
|
||||
* @brief Set sfBytecode (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowCreateBuilder&
|
||||
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
setBytecode(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfDestinationTag] = value;
|
||||
object_[sfBytecode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfData (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowCreateBuilder&
|
||||
setData(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfData] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,32 @@ public:
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfCredentialIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGas (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGas() const
|
||||
{
|
||||
if (hasGas())
|
||||
{
|
||||
return this->tx_->at(sfGas);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGas is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGas() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfGas);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -249,6 +275,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGas (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
EscrowFinishBuilder&
|
||||
setGas(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGas] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the EscrowFinish wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -254,6 +254,84 @@ public:
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfReserveIncrementDrops);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasLimit() const
|
||||
{
|
||||
if (hasGasLimit())
|
||||
{
|
||||
return this->tx_->at(sfGasLimit);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasLimit() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfGasLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getBytecodeSizeLimit() const
|
||||
{
|
||||
if (hasBytecodeSizeLimit())
|
||||
{
|
||||
return this->tx_->at(sfBytecodeSizeLimit);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfBytecodeSizeLimit is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasBytecodeSizeLimit() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfBytecodeSizeLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfGasPrice (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getGasPrice() const
|
||||
{
|
||||
if (hasGasPrice())
|
||||
{
|
||||
return this->tx_->at(sfGasPrice);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfGasPrice is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasGasPrice() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfGasPrice);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -386,6 +464,39 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SetFeeBuilder&
|
||||
setGasLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfBytecodeSizeLimit (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SetFeeBuilder&
|
||||
setBytecodeSizeLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfBytecodeSizeLimit] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfGasPrice (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SetFeeBuilder&
|
||||
setGasPrice(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfGasPrice] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the SetFee wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class STObject;
|
||||
|
||||
/*
|
||||
Validator key manifests
|
||||
-----------------------
|
||||
@@ -296,6 +298,56 @@ deserializeManifest(
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* The fields of a manifest before it is signed: @p masterKey delegates to
|
||||
* @p signingKey at @p sequence, with an optional domain.
|
||||
*
|
||||
* Both signatures cover @ref manifestSigningData of the returned object. Set
|
||||
* them as sfSignature and sfMasterSignature, then serialize.
|
||||
*/
|
||||
STObject
|
||||
makeManifestFields(
|
||||
PublicKey const& masterKey,
|
||||
PublicKey const& signingKey,
|
||||
std::uint32_t sequence,
|
||||
std::string const& domain = {});
|
||||
|
||||
/**
|
||||
* The fields of a revocation before it is signed: @p masterKey at the
|
||||
* largest sequence, with no signing key.
|
||||
*/
|
||||
STObject
|
||||
makeRevocationFields(PublicKey const& masterKey);
|
||||
|
||||
/**
|
||||
* The bytes a manifest's signatures cover: HashPrefix::Manifest followed by
|
||||
* the fields without the signatures.
|
||||
*/
|
||||
Blob
|
||||
manifestSigningData(STObject const& fields);
|
||||
|
||||
/**
|
||||
* A manifest signed by both keys.
|
||||
*
|
||||
* @return The serialized manifest, as `Manifest::serialized` holds it
|
||||
*/
|
||||
std::string
|
||||
makeManifest(
|
||||
PublicKey const& masterKey,
|
||||
SecretKey const& masterSecret,
|
||||
PublicKey const& signingKey,
|
||||
SecretKey const& signingSecret,
|
||||
std::uint32_t sequence,
|
||||
std::string const& domain = {});
|
||||
|
||||
/**
|
||||
* A revocation of @p masterKey signed by it.
|
||||
*
|
||||
* @return The serialized manifest, as `Manifest::serialized` holds it
|
||||
*/
|
||||
std::string
|
||||
makeRevocation(PublicKey const& masterKey, SecretKey const& masterSecret);
|
||||
|
||||
inline bool
|
||||
operator==(Manifest const& lhs, Manifest const& rhs)
|
||||
{
|
||||
|
||||
@@ -131,6 +131,9 @@ Rust toolchain:
|
||||
✅ rust-analyzer
|
||||
rust-analyzer 1.97.1 (8bab26f4 2026-07-14)
|
||||
/nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
|
||||
✅ rust-nightly
|
||||
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
|
||||
/nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly
|
||||
✅ rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
|
||||
@@ -140,4 +143,4 @@ Rust toolchain:
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
✅ All 44 checked tools are present and runnable.
|
||||
✅ All 45 checked tools are present and runnable.
|
||||
|
||||
@@ -131,6 +131,9 @@ Rust toolchain:
|
||||
✅ rust-analyzer
|
||||
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
|
||||
/nix/store/lr3m97p3hx1k22a7c44pb0wa7rbayhfi-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer
|
||||
✅ rust-nightly
|
||||
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
|
||||
/nix/store/j7kf7a5h4xypzp6x1skg4dsdx2k4fwb3-rust-nightly/bin/rust-nightly
|
||||
✅ rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc
|
||||
@@ -168,4 +171,4 @@ Mold:
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
✅ All 52 checked tools are present and runnable.
|
||||
✅ All 53 checked tools are present and runnable.
|
||||
|
||||
@@ -131,6 +131,9 @@ Rust toolchain:
|
||||
✅ rust-analyzer
|
||||
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
|
||||
/nix/store/262830dlw2517lnagfx7i7agqgl4fmsd-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer
|
||||
✅ rust-nightly
|
||||
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
|
||||
/nix/store/c59pxk1yikdlf129qwyg4fplmxcrha0k-rust-nightly/bin/rust-nightly
|
||||
✅ rustc
|
||||
rustc 1.97.1 (8bab26f4f 2026-07-14)
|
||||
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc
|
||||
@@ -168,4 +171,4 @@ Mold:
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
✅ All 52 checked tools are present and runnable.
|
||||
✅ All 53 checked tools are present and runnable.
|
||||
|
||||
@@ -10,6 +10,8 @@ RUN mkdir -p ~/.config/nix && \
|
||||
COPY nix/ci-env.nix /tmp/build/nix/ci-env.nix
|
||||
COPY nix/linux.nix /tmp/build/nix/linux.nix
|
||||
COPY nix/packages.nix /tmp/build/nix/packages.nix
|
||||
COPY nix/rust-nightly.sh /tmp/build/nix/rust-nightly.sh
|
||||
COPY nix/rust.nix /tmp/build/nix/rust.nix
|
||||
COPY nix/utils.nix /tmp/build/nix/utils.nix
|
||||
COPY flake.nix /tmp/build/
|
||||
COPY flake.lock /tmp/build/
|
||||
|
||||
@@ -16,23 +16,7 @@ let
|
||||
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
|
||||
'';
|
||||
|
||||
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
|
||||
# cargo has a linker). That default may be different from the clang we pin here,
|
||||
# so it shadows our clang and the build can silently use a different compiler
|
||||
# version. Drop that cc from every propagation channel instead of pinning a
|
||||
# replacement: the toolchain then carries no compiler and cargo just uses the
|
||||
# active shell's stdenv cc. Must cover all channels — rust-overlay uses both
|
||||
# propagatedBuildInputs and depsHostHostPropagated.
|
||||
rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
|
||||
rustToolchain =
|
||||
let
|
||||
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
|
||||
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
|
||||
in
|
||||
rustToolchainBase.overrideAttrs (old: {
|
||||
propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]);
|
||||
depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]);
|
||||
});
|
||||
rust = import ./rust.nix { inherit pkgs; };
|
||||
|
||||
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
|
||||
# names (gcc, g++, clang-tidy, ...). Several tools probe for a
|
||||
@@ -108,41 +92,38 @@ in
|
||||
mkGcov
|
||||
;
|
||||
|
||||
commonPackages = with pkgs; [
|
||||
clangToolLinks
|
||||
runClangTidyLink
|
||||
ccache
|
||||
clangbuildanalyzer
|
||||
clangTools
|
||||
cmake
|
||||
conan
|
||||
curlMinimal # needed for codecov/codecov-action
|
||||
doxygen
|
||||
file # needed for cpack in Clio
|
||||
gcovr
|
||||
gh
|
||||
git
|
||||
git-cliff
|
||||
git-lfs
|
||||
gnumake
|
||||
gnupg # needed for signing commits & codecov/codecov-action
|
||||
graphviz
|
||||
less # needed for git diff
|
||||
mold
|
||||
nettools # provides netstat, used to debug failures in CI
|
||||
ninja
|
||||
patchelf
|
||||
perl # needed for openssl
|
||||
pkg-config
|
||||
pre-commit
|
||||
python3
|
||||
runClangTidy
|
||||
vim
|
||||
zip
|
||||
# Rust packages
|
||||
cargo-audit
|
||||
cargo-llvm-cov
|
||||
cargo-nextest
|
||||
rustToolchain
|
||||
];
|
||||
commonPackages =
|
||||
(with pkgs; [
|
||||
clangToolLinks
|
||||
runClangTidyLink
|
||||
ccache
|
||||
clangbuildanalyzer
|
||||
clangTools
|
||||
cmake
|
||||
conan
|
||||
curlMinimal # needed for codecov/codecov-action
|
||||
doxygen
|
||||
file # needed for cpack in Clio
|
||||
gcovr
|
||||
gh
|
||||
git
|
||||
git-cliff
|
||||
git-lfs
|
||||
gnumake
|
||||
gnupg # needed for signing commits & codecov/codecov-action
|
||||
graphviz
|
||||
less # needed for git diff
|
||||
mold
|
||||
nettools # provides netstat, used to debug failures in CI
|
||||
ninja
|
||||
patchelf
|
||||
perl # needed for openssl
|
||||
pkg-config
|
||||
pre-commit
|
||||
python3
|
||||
runClangTidy
|
||||
vim
|
||||
zip
|
||||
])
|
||||
++ rust.packages;
|
||||
}
|
||||
|
||||
23
nix/rust-nightly.sh
Normal file
23
nix/rust-nightly.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!@runtimeShell@
|
||||
# Reaches the nightly Rust toolchain, which is deliberately kept off PATH.
|
||||
# Packaged by nix/rust.nix, which explains why.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "usage: rust-nightly (path | run <command>...)" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
case "${1-}" in
|
||||
path) printf '%s\n' "@rustNightlyBin@" ;;
|
||||
run)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then
|
||||
usage
|
||||
fi
|
||||
export PATH="@rustNightlyBin@:${PATH}"
|
||||
exec "$@"
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
84
nix/rust.nix
Normal file
84
nix/rust.nix
Normal file
@@ -0,0 +1,84 @@
|
||||
# The Rust half of the tool set shared by the CI environment and the dev shell:
|
||||
# the stable toolchain pinned by rust-toolchain.toml, the nightly the Rust
|
||||
# coverage job needs, and the cargo plugins. Consumed by packages.nix.
|
||||
{ pkgs }:
|
||||
let
|
||||
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
|
||||
# cargo has a linker). That default may be different from the clang we pin
|
||||
# elsewhere, so it shadows our clang and the build can silently use a different
|
||||
# compiler version. Drop that cc from every propagation channel instead of
|
||||
# pinning a replacement: the toolchain then carries no compiler and cargo just
|
||||
# uses the active shell's stdenv cc.
|
||||
#
|
||||
# The channel list is every list mkDerivation propagates to a dependent's
|
||||
# environment (including the two legacy aliases). rust-overlay currently only
|
||||
# uses propagatedBuildInputs and depsHostHostPropagated, but covering all of
|
||||
# them means an upstream switch to another channel cannot quietly put the
|
||||
# compiler back on PATH.
|
||||
dropDefaultCc =
|
||||
toolchain:
|
||||
let
|
||||
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
|
||||
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
|
||||
in
|
||||
toolchain.overrideAttrs (
|
||||
old:
|
||||
pkgs.lib.genAttrs [
|
||||
"depsBuildBuildPropagated"
|
||||
"propagatedNativeBuildInputs" # alias of depsBuildHostPropagated
|
||||
"depsBuildTargetPropagated"
|
||||
"depsHostHostPropagated"
|
||||
"propagatedBuildInputs" # alias of depsHostTargetPropagated
|
||||
"depsTargetTargetPropagated"
|
||||
] (channel: withoutDefaultCc (old.${channel} or [ ]))
|
||||
);
|
||||
|
||||
rustToolchain = dropDefaultCc (pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml);
|
||||
|
||||
# cargo-llvm-cov honours the #[coverage(off)] that keeps unit tests out of the
|
||||
# coverage report only under a nightly rustc, and looks for llvm-profdata and
|
||||
# llvm-cov in that same toolchain's sysroot — hence llvm-tools-preview.
|
||||
#
|
||||
# Not every nightly ships every component, so `nightly.latest` breaks on the
|
||||
# days llvm-tools-preview is absent; selectLatestNightlyWith walks back to the
|
||||
# newest one that has it. The result is the newest such nightly *known to the
|
||||
# locked rust-overlay*, which means updating flake.lock moves the compiler that
|
||||
# produces the coverage numbers — and with it the rustc version recorded in
|
||||
# nix/check-tools/*.txt, so those snapshots need regenerating alongside.
|
||||
rustNightly = dropDefaultCc (
|
||||
pkgs.rust-bin.selectLatestNightlyWith (
|
||||
toolchain: toolchain.minimal.override { extensions = [ "llvm-tools-preview" ]; }
|
||||
)
|
||||
);
|
||||
|
||||
# A second toolchain cannot go on PATH: its cargo and rustc would collide with
|
||||
# the pinned stable's in the ci-env buildEnv, which resolves collisions by
|
||||
# picking one silently. Reaching the nightly only through this wrapper keeps it
|
||||
# in the image closure (the Docker build copies the whole closure, not just
|
||||
# what is linked into /bin) while leaving it inactive everywhere that does not
|
||||
# ask for it.
|
||||
#
|
||||
# The script's `path` subcommand exists for scopes wider than one command — a
|
||||
# CI job appending to $GITHUB_PATH, so that the cargo cache action's own
|
||||
# `rustc -vV` probe, which runs in a step of its own, agrees with the toolchain
|
||||
# the build will use.
|
||||
rustNightlyScript = pkgs.replaceVarsWith {
|
||||
name = "rust-nightly";
|
||||
src = ./rust-nightly.sh;
|
||||
dir = "bin";
|
||||
isExecutable = true;
|
||||
replacements = {
|
||||
inherit (pkgs) runtimeShell;
|
||||
rustNightlyBin = "${rustNightly}/bin";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = [
|
||||
pkgs.cargo-audit
|
||||
pkgs.cargo-llvm-cov
|
||||
pkgs.cargo-nextest
|
||||
rustNightlyScript
|
||||
rustToolchain
|
||||
];
|
||||
}
|
||||
@@ -73,9 +73,10 @@ artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`) after that
|
||||
same config, so a packaged config must keep `-Dvalidator_keys=ON`. Those configs
|
||||
are not `minimal`, so `on-pr.yml` only packages once a PR runs the full matrix.
|
||||
|
||||
`validator-keys` is fetched from an exact commit pinned in
|
||||
`validator-keys` is built from the source in
|
||||
[`src/tools/validator-keys`](../src/tools/validator-keys), enabled by
|
||||
[`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given
|
||||
`xrpld` version always packages the same tool; bump that commit deliberately.
|
||||
`xrpld` version always packages the tool from the same tree.
|
||||
|
||||
### Locally (mirrors CI)
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ def check_binaries(build_dir: Path) -> None:
|
||||
# No package goes out without the attribution.
|
||||
notice = build_dir / "validator-keys-LICENSE"
|
||||
assert notice.is_file(), (
|
||||
f"missing {notice}. cmake/XrplValidatorKeys.cmake copies it out of the "
|
||||
"fetched validator-keys-tool source, so reconfigure with -Dvalidator_keys=ON."
|
||||
f"missing {notice}. cmake/XrplValidatorKeys.cmake copies it from "
|
||||
"src/tools/validator-keys, so reconfigure with -Dvalidator_keys=ON."
|
||||
)
|
||||
|
||||
# Catches a binary still pointing at the Nix store's ELF loader, since
|
||||
|
||||
@@ -16,8 +16,7 @@ Copyright: 2016, Ripple Labs Inc.
|
||||
2011, The Bitcoin developers
|
||||
2003-2005, Tom Wu
|
||||
License: ISC and BSL-1.0 and MIT and Tom-Wu
|
||||
Comment: Built from https://github.com/ripple/validator-keys-tool at the commit
|
||||
pinned in cmake/XrplValidatorKeys.cmake. Besides ISC-licensed code it
|
||||
Comment: Built from src/tools/validator-keys in this repository. Besides ISC-licensed code it
|
||||
incorporates work under the Boost Software License 1.0 (ASIO), the MIT/X11
|
||||
license (Bitcoin) and Tom Wu's license, whose terms require its notice to be
|
||||
retained intact. The complete upstream notice is therefore shipped verbatim as
|
||||
|
||||
@@ -59,6 +59,10 @@ STValidation::validationFormat()
|
||||
{sfBaseFeeDrops, SoeOptional},
|
||||
{sfReserveBaseDrops, SoeOptional},
|
||||
{sfReserveIncrementDrops, SoeOptional},
|
||||
// featureSmartEscrow
|
||||
{sfGasLimit, SoeOptional},
|
||||
{sfBytecodeSizeLimit, SoeOptional},
|
||||
{sfGasPrice, SoeOptional},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -108,6 +108,8 @@ transResults()
|
||||
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
|
||||
MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"),
|
||||
MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."),
|
||||
MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."),
|
||||
MAKE_ERROR(tecBYTECODE_REJECTED, "The custom WASM code that was run rejected your transaction."),
|
||||
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
@@ -133,6 +135,8 @@ transResults()
|
||||
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
|
||||
MAKE_ERROR(tefNO_DST_PARTIAL, "Partial payment to create account not allowed."),
|
||||
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
|
||||
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
|
||||
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
@@ -204,6 +208,8 @@ transResults()
|
||||
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
|
||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
|
||||
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <xrpl/protocol/SOTemplate.h>
|
||||
#include <xrpl/protocol/STExchange.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
@@ -260,6 +261,76 @@ Manifest::getMasterSignature() const
|
||||
return st.getFieldVL(sfMasterSignature);
|
||||
}
|
||||
|
||||
STObject
|
||||
makeManifestFields(
|
||||
PublicKey const& masterKey,
|
||||
PublicKey const& signingKey,
|
||||
std::uint32_t sequence,
|
||||
std::string const& domain)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = sequence;
|
||||
st[sfPublicKey] = masterKey;
|
||||
st[sfSigningPubKey] = signingKey;
|
||||
if (!domain.empty())
|
||||
st[sfDomain] = makeSlice(domain);
|
||||
return st;
|
||||
}
|
||||
|
||||
STObject
|
||||
makeRevocationFields(PublicKey const& masterKey)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
||||
st[sfPublicKey] = masterKey;
|
||||
return st;
|
||||
}
|
||||
|
||||
Blob
|
||||
manifestSigningData(STObject const& fields)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(HashPrefix::Manifest);
|
||||
fields.addWithoutSigningFields(s);
|
||||
return s.peekData();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string
|
||||
serializeManifest(STObject const& st)
|
||||
{
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string
|
||||
makeManifest(
|
||||
PublicKey const& masterKey,
|
||||
SecretKey const& masterSecret,
|
||||
PublicKey const& signingKey,
|
||||
SecretKey const& signingSecret,
|
||||
std::uint32_t sequence,
|
||||
std::string const& domain)
|
||||
{
|
||||
auto st = makeManifestFields(masterKey, signingKey, sequence, domain);
|
||||
auto const data = manifestSigningData(st);
|
||||
set(st, sfSignature, sign(signingKey, signingSecret, makeSlice(data)));
|
||||
set(st, sfMasterSignature, sign(masterKey, masterSecret, makeSlice(data)));
|
||||
return serializeManifest(st);
|
||||
}
|
||||
|
||||
std::string
|
||||
makeRevocation(PublicKey const& masterKey, SecretKey const& masterSecret)
|
||||
{
|
||||
auto st = makeRevocationFields(masterKey);
|
||||
set(st, sfMasterSignature, sign(masterKey, masterSecret, makeSlice(manifestSigningData(st))));
|
||||
return serializeManifest(st);
|
||||
}
|
||||
|
||||
std::optional<ValidatorToken>
|
||||
loadValidatorToken(std::vector<std::string> const& blob, beast::Journal journal)
|
||||
{
|
||||
|
||||
@@ -88,8 +88,13 @@ EscrowCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
// Only require featureMPTokensV1 when the escrow amount is an MPT and
|
||||
// fixCleanup3_2_0 is active; XRP/IOU escrows are unaffected by this gate.
|
||||
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
|
||||
return ctx.rules.enabled(featureMPTokensV1);
|
||||
return true;
|
||||
{
|
||||
if (!ctx.rules.enabled(featureMPTokensV1))
|
||||
return false;
|
||||
}
|
||||
|
||||
return (!ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfData)) ||
|
||||
ctx.rules.enabled(featureSmartEscrow);
|
||||
}
|
||||
|
||||
template <ValidIssueType T>
|
||||
|
||||
@@ -65,7 +65,13 @@ checkCondition(Slice f, Slice c)
|
||||
bool
|
||||
EscrowFinish::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
|
||||
if (ctx.tx.isFieldPresent(sfCredentialIDs) && !ctx.rules.enabled(featureCredentials))
|
||||
return false;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfGas) && !ctx.rules.enabled(featureSmartEscrow))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
|
||||
@@ -123,6 +123,12 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfReserveIncrementDrops))
|
||||
return temDISABLED;
|
||||
}
|
||||
// The ttFEE transaction format defines these fields as optional,
|
||||
// but they are unconditionally forbidden until FeeVoteImpl is
|
||||
// updated to populate them (SmartEscrow behavioral port).
|
||||
if (ctx.tx.isFieldPresent(sfGasLimit) || ctx.tx.isFieldPresent(sfBytecodeSizeLimit) ||
|
||||
ctx.tx.isFieldPresent(sfGasPrice))
|
||||
return temDISABLED;
|
||||
return tesSUCCESS;
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
|
||||
@@ -106,75 +106,25 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
static std::string
|
||||
makeManifestString(
|
||||
PublicKey const& pk,
|
||||
SecretKey const& sk,
|
||||
PublicKey const& spk,
|
||||
SecretKey const& ssk,
|
||||
int seq)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = pk;
|
||||
st[sfSigningPubKey] = spk;
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
sign(st, HashPrefix::Manifest, *publicKeyType(spk), ssk);
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
sign(st, HashPrefix::Manifest, *publicKeyType(pk), sk, sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
return base64Encode(std::string(static_cast<char const*>(s.data()), s.size()));
|
||||
}
|
||||
|
||||
std::string
|
||||
makeRevocationString(SecretKey const& sk, KeyType type, bool invalidSig = false)
|
||||
{
|
||||
auto const pk = derivePublicKey(type, sk);
|
||||
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
||||
st[sfPublicKey] = pk;
|
||||
|
||||
sign(
|
||||
st, HashPrefix::Manifest, type, invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
||||
BEAST_EXPECT(invalidSig ^ verify(st, HashPrefix::Manifest, pk, sfMasterSignature));
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
return base64Encode(std::string(static_cast<char const*>(s.data()), s.size()));
|
||||
return base64Encode(buildRevocation(sk, type, invalidSig).serialized);
|
||||
}
|
||||
|
||||
Manifest
|
||||
makeRevocation(SecretKey const& sk, KeyType type, bool invalidSig = false)
|
||||
buildRevocation(SecretKey const& sk, KeyType type, bool invalidSig = false)
|
||||
{
|
||||
auto const pk = derivePublicKey(type, sk);
|
||||
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
||||
st[sfPublicKey] = pk;
|
||||
|
||||
sign(
|
||||
st, HashPrefix::Manifest, type, invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
||||
BEAST_EXPECT(invalidSig ^ verify(st, HashPrefix::Manifest, pk, sfMasterSignature));
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
std::string const m(static_cast<char const*>(s.data()), s.size());
|
||||
if (auto r = deserializeManifest(m))
|
||||
return std::move(*r);
|
||||
Throw<std::runtime_error>("Could not create a revocation manifest");
|
||||
return *deserializeManifest(std::string{}); // Silence compiler warning.
|
||||
auto m = deserializeManifest(makeRevocation(pk, invalidSig ? randomSecretKey() : sk));
|
||||
if (!m)
|
||||
Throw<std::runtime_error>("Could not create a revocation manifest");
|
||||
BEAST_EXPECT(invalidSig ^ m->verify());
|
||||
return std::move(*m);
|
||||
}
|
||||
|
||||
Manifest
|
||||
makeManifest(
|
||||
buildManifest(
|
||||
SecretKey const& sk,
|
||||
KeyType type,
|
||||
SecretKey const& ssk,
|
||||
@@ -184,27 +134,12 @@ public:
|
||||
{
|
||||
auto const pk = derivePublicKey(type, sk);
|
||||
auto const spk = derivePublicKey(stype, ssk);
|
||||
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = pk;
|
||||
st[sfSigningPubKey] = spk;
|
||||
|
||||
sign(st, HashPrefix::Manifest, stype, ssk);
|
||||
BEAST_EXPECT(verify(st, HashPrefix::Manifest, spk));
|
||||
|
||||
sign(
|
||||
st, HashPrefix::Manifest, type, invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
||||
BEAST_EXPECT(invalidSig ^ verify(st, HashPrefix::Manifest, pk, sfMasterSignature));
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
std::string const m(static_cast<char const*>(s.data()), s.size());
|
||||
if (auto r = deserializeManifest(m))
|
||||
return std::move(*r);
|
||||
Throw<std::runtime_error>("Could not create a manifest");
|
||||
return *deserializeManifest(std::string{}); // Silence compiler warning.
|
||||
auto m = deserializeManifest(
|
||||
makeManifest(pk, invalidSig ? randomSecretKey() : sk, spk, ssk, seq));
|
||||
if (!m)
|
||||
Throw<std::runtime_error>("Could not create a manifest");
|
||||
BEAST_EXPECT(invalidSig ^ m->verify());
|
||||
return std::move(*m);
|
||||
}
|
||||
|
||||
static Manifest
|
||||
@@ -311,7 +246,8 @@ public:
|
||||
auto const pk = derivePublicKey(KeyType::Ed25519, sk);
|
||||
auto const kp = randomKeyPair(KeyType::Secp256k1);
|
||||
|
||||
std::string const cfgManifest = makeManifestString(pk, sk, kp.first, kp.second, 0);
|
||||
std::string const cfgManifest =
|
||||
base64Encode(makeManifest(pk, sk, kp.first, kp.second, 0));
|
||||
|
||||
BEAST_EXPECT(
|
||||
loaded.load(*dbCon, "ValidatorManifests", cfgManifest, emptyRevocation));
|
||||
@@ -330,7 +266,7 @@ public:
|
||||
auto const pk = derivePublicKey(keyType, sk);
|
||||
auto const kp = randomKeyPair(KeyType::Secp256k1);
|
||||
std::vector<std::string> const nonRevocation = {
|
||||
makeManifestString(pk, sk, kp.first, kp.second, 0)};
|
||||
base64Encode(makeManifest(pk, sk, kp.first, kp.second, 0))};
|
||||
|
||||
BEAST_EXPECT(
|
||||
!loaded.load(*dbCon, "ValidatorManifests", emptyManifest, nonRevocation));
|
||||
@@ -359,7 +295,7 @@ public:
|
||||
auto const sk = randomSecretKey();
|
||||
auto const pk = derivePublicKey(KeyType::Ed25519, sk);
|
||||
auto const kp = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const m = makeManifest(sk, KeyType::Ed25519, kp.second, KeyType::Secp256k1, 0);
|
||||
auto const m = buildManifest(sk, KeyType::Ed25519, kp.second, KeyType::Secp256k1, 0);
|
||||
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = 0;
|
||||
@@ -397,7 +333,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::Accepted ==
|
||||
cache.applyManifest(
|
||||
makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0),
|
||||
buildManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0),
|
||||
ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk);
|
||||
@@ -410,7 +346,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::Accepted ==
|
||||
cache.applyManifest(
|
||||
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1),
|
||||
buildManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1),
|
||||
ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk);
|
||||
@@ -421,7 +357,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::BadEphemeralKey ==
|
||||
cache.applyManifest(
|
||||
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2),
|
||||
buildManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2),
|
||||
ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk);
|
||||
@@ -433,7 +369,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::Accepted ==
|
||||
cache.applyManifest(
|
||||
makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCapPolicy::Capped));
|
||||
buildRevocation(sk, KeyType::Ed25519), ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.revoked(pk));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == pk);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first);
|
||||
@@ -877,27 +813,27 @@ public:
|
||||
auto const kpA0 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const kpA1 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const sA0 =
|
||||
makeManifest(skA, KeyType::Ed25519, kpA0.second, KeyType::Secp256k1, 0);
|
||||
buildManifest(skA, KeyType::Ed25519, kpA0.second, KeyType::Secp256k1, 0);
|
||||
auto const sA1 =
|
||||
makeManifest(skA, KeyType::Ed25519, kpA1.second, KeyType::Secp256k1, 1);
|
||||
buildManifest(skA, KeyType::Ed25519, kpA1.second, KeyType::Secp256k1, 1);
|
||||
auto const sA2 =
|
||||
makeManifest(skA, KeyType::Ed25519, kpA1.second, KeyType::Secp256k1, 2);
|
||||
auto const sAMax = makeRevocation(skA, KeyType::Ed25519);
|
||||
buildManifest(skA, KeyType::Ed25519, kpA1.second, KeyType::Secp256k1, 2);
|
||||
auto const sAMax = buildRevocation(skA, KeyType::Ed25519);
|
||||
|
||||
auto const skB = randomSecretKey();
|
||||
auto const kpB0 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const kpB1 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const kpB2 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const sB0 =
|
||||
makeManifest(skB, KeyType::Ed25519, kpB0.second, KeyType::Secp256k1, 0);
|
||||
auto const sB1 = makeManifest(
|
||||
buildManifest(skB, KeyType::Ed25519, kpB0.second, KeyType::Secp256k1, 0);
|
||||
auto const sB1 = buildManifest(
|
||||
skB,
|
||||
KeyType::Ed25519,
|
||||
kpB1.second,
|
||||
KeyType::Secp256k1,
|
||||
1,
|
||||
true); // invalidSig
|
||||
auto const sB2 = makeManifest(skB, KeyType::Ed25519, kpB2.second, KeyType::Ed25519, 2);
|
||||
auto const sB2 = buildManifest(skB, KeyType::Ed25519, kpB2.second, KeyType::Ed25519, 2);
|
||||
|
||||
auto const fake = sB2.serialized + '\0';
|
||||
|
||||
@@ -962,7 +898,7 @@ public:
|
||||
cache.applyManifest(clone(sB2), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
|
||||
auto const sC0 = makeManifest(
|
||||
auto const sC0 = buildManifest(
|
||||
kpB2.second, KeyType::Ed25519, randomSecretKey(), KeyType::Ed25519, 47);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sC0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
|
||||
@@ -14,14 +14,9 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -74,50 +69,6 @@ private:
|
||||
return derivePublicKey(KeyType::Ed25519, randomSecretKey());
|
||||
}
|
||||
|
||||
static std::string
|
||||
makeManifestString(
|
||||
PublicKey const& pk,
|
||||
SecretKey const& sk,
|
||||
PublicKey const& spk,
|
||||
SecretKey const& ssk,
|
||||
int seq)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = pk;
|
||||
|
||||
if (seq != std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
st[sfSigningPubKey] = spk;
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
sign(st, HashPrefix::Manifest, *publicKeyType(spk), ssk);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
sign(st, HashPrefix::Manifest, *publicKeyType(pk), sk, sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
static std::string
|
||||
makeRevocationString(PublicKey const& pk, SecretKey const& sk)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
||||
st[sfPublicKey] = pk;
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
sign(st, HashPrefix::Manifest, *publicKeyType(pk), sk, sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
static Validator
|
||||
randomValidator()
|
||||
{
|
||||
@@ -127,8 +78,8 @@ private:
|
||||
return {
|
||||
.masterPublic = masterPublic,
|
||||
.signingPublic = signingKeys.first,
|
||||
.manifest = base64Encode(makeManifestString(
|
||||
masterPublic, secret, signingKeys.first, signingKeys.second, 1))};
|
||||
.manifest = base64Encode(
|
||||
makeManifest(masterPublic, secret, signingKeys.first, signingKeys.second, 1))};
|
||||
}
|
||||
|
||||
static std::string
|
||||
@@ -232,7 +183,7 @@ private:
|
||||
auto const localMasterSecret = randomSecretKey();
|
||||
auto const localMasterPublic = derivePublicKey(KeyType::Ed25519, localMasterSecret);
|
||||
|
||||
std::string const cfgManifest(makeManifestString(
|
||||
std::string const cfgManifest(makeManifest(
|
||||
localMasterPublic, localMasterSecret, localSigningPublicOuter, localSigningSecret, 1));
|
||||
|
||||
auto format = [](PublicKey const& publicKey, char const* comment = nullptr) {
|
||||
@@ -461,12 +412,7 @@ private:
|
||||
// -- thus should not be loaded
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
pubManifests.applyManifest(
|
||||
*deserializeManifest(makeManifestString(
|
||||
pubRevokedPublic,
|
||||
pubRevokedSecret,
|
||||
pubRevokedSigning.first,
|
||||
pubRevokedSigning.second,
|
||||
std::numeric_limits<std::uint32_t>::max())),
|
||||
*deserializeManifest(makeRevocation(pubRevokedPublic, pubRevokedSecret)),
|
||||
ManifestRateLimitCapPolicy::Capped);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
|
||||
@@ -503,12 +449,7 @@ private:
|
||||
// -- thus should not be loaded
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
pubManifests.applyManifest(
|
||||
*deserializeManifest(makeManifestString(
|
||||
pubRevokedPublic,
|
||||
pubRevokedSecret,
|
||||
pubRevokedSigning.first,
|
||||
pubRevokedSigning.second,
|
||||
std::numeric_limits<std::uint32_t>::max())),
|
||||
*deserializeManifest(makeRevocation(pubRevokedPublic, pubRevokedSecret)),
|
||||
ManifestRateLimitCapPolicy::Capped);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
|
||||
@@ -605,7 +546,7 @@ private:
|
||||
auto const publisherPublic = derivePublicKey(KeyType::Ed25519, publisherSecret);
|
||||
auto const hexPublic = strHex(publisherPublic.begin(), publisherPublic.end());
|
||||
auto const pubSigningKeys1 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const manifest1 = base64Encode(makeManifestString(
|
||||
auto const manifest1 = base64Encode(makeManifest(
|
||||
publisherPublic, publisherSecret, pubSigningKeys1.first, pubSigningKeys1.second, 1));
|
||||
|
||||
std::vector<std::string> const cfgPublisherKeys({strHex(publisherPublic)});
|
||||
@@ -769,7 +710,7 @@ private:
|
||||
ListDisposition::Invalid);
|
||||
|
||||
// do not use list from untrusted publisher
|
||||
auto const untrustedManifest = base64Encode(makeManifestString(
|
||||
auto const untrustedManifest = base64Encode(makeManifest(
|
||||
randomMasterKey(), publisherSecret, pubSigningKeys1.first, pubSigningKeys1.second, 1));
|
||||
|
||||
checkResult(
|
||||
@@ -834,7 +775,7 @@ private:
|
||||
// apply list with new publisher key updated by manifest. Also send some
|
||||
// old lists along with the old manifest
|
||||
auto const pubSigningKeys2 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto manifest2 = base64Encode(makeManifestString(
|
||||
auto manifest2 = base64Encode(makeManifest(
|
||||
publisherPublic, publisherSecret, pubSigningKeys2.first, pubSigningKeys2.second, 2));
|
||||
|
||||
auto const sequence4 = 4;
|
||||
@@ -956,7 +897,7 @@ private:
|
||||
// do not apply list with revoked publisher key
|
||||
// applied list is removed due to revoked publisher key
|
||||
auto const signingKeysMax = randomKeyPair(KeyType::Secp256k1);
|
||||
auto maxManifest = base64Encode(makeRevocationString(publisherPublic, publisherSecret));
|
||||
auto maxManifest = base64Encode(makeRevocation(publisherPublic, publisherSecret));
|
||||
|
||||
auto const sequence9 = 9;
|
||||
auto const blob9 = makeList(lists.at(9), sequence9, validUntil.time_since_epoch().count());
|
||||
@@ -1004,7 +945,7 @@ private:
|
||||
auto const publisherPublic = derivePublicKey(KeyType::Ed25519, publisherSecret);
|
||||
auto const hexPublic = strHex(publisherPublic.begin(), publisherPublic.end());
|
||||
auto const pubSigningKeys1 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const manifest = base64Encode(makeManifestString(
|
||||
auto const manifest = base64Encode(makeManifest(
|
||||
publisherPublic, publisherSecret, pubSigningKeys1.first, pubSigningKeys1.second, 1));
|
||||
|
||||
std::vector<std::string> const cfgPublisherKeys({strHex(publisherPublic)});
|
||||
@@ -1223,8 +1164,8 @@ private:
|
||||
BEAST_EXPECT(!trustedKeysOuter->trusted(signingPublic1));
|
||||
|
||||
// Should trust the ephemeral signing key from the applied manifest
|
||||
auto m1 = deserializeManifest(makeManifestString(
|
||||
masterPublic, masterPrivate, signingPublic1, signingKeys1.second, 1));
|
||||
auto m1 = deserializeManifest(
|
||||
makeManifest(masterPublic, masterPrivate, signingPublic1, signingKeys1.second, 1));
|
||||
|
||||
BEAST_EXPECT(
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
@@ -1239,8 +1180,8 @@ private:
|
||||
// from the newest applied manifest
|
||||
auto const signingKeys2 = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const signingPublic2 = signingKeys2.first;
|
||||
auto m2 = deserializeManifest(makeManifestString(
|
||||
masterPublic, masterPrivate, signingPublic2, signingKeys2.second, 2));
|
||||
auto m2 = deserializeManifest(
|
||||
makeManifest(masterPublic, masterPrivate, signingPublic2, signingKeys2.second, 2));
|
||||
BEAST_EXPECT(
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCapPolicy::Capped) ==
|
||||
@@ -1256,7 +1197,7 @@ private:
|
||||
auto const signingKeysMax = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const signingPublicMax = signingKeysMax.first;
|
||||
activeValidatorsOuter.emplace(calcNodeID(signingPublicMax));
|
||||
auto max = deserializeManifest(makeRevocationString(masterPublic, masterPrivate));
|
||||
auto max = deserializeManifest(makeRevocation(masterPublic, masterPrivate));
|
||||
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(max->revoked());
|
||||
@@ -1420,7 +1361,7 @@ private:
|
||||
std::vector<std::string> const emptyCfgKeys;
|
||||
auto const publisherKeys = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const pubSigningKeys = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const manifest = base64Encode(makeManifestString(
|
||||
auto const manifest = base64Encode(makeManifest(
|
||||
publisherKeys.first,
|
||||
publisherKeys.second,
|
||||
pubSigningKeys.first,
|
||||
@@ -1626,7 +1567,7 @@ private:
|
||||
auto const publisherSecret = randomSecretKey();
|
||||
auto const publisherPublic = derivePublicKey(KeyType::Ed25519, publisherSecret);
|
||||
auto const pubSigningKeys = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const manifest = base64Encode(makeManifestString(
|
||||
auto const manifest = base64Encode(makeManifest(
|
||||
publisherPublic,
|
||||
publisherSecret,
|
||||
pubSigningKeys.first,
|
||||
@@ -1718,7 +1659,7 @@ private:
|
||||
auto const publisherSecret = randomSecretKey();
|
||||
auto const publisherPublic = derivePublicKey(KeyType::Ed25519, publisherSecret);
|
||||
auto const pubSigningKeys = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const manifest = base64Encode(makeManifestString(
|
||||
auto const manifest = base64Encode(makeManifest(
|
||||
publisherPublic,
|
||||
publisherSecret,
|
||||
pubSigningKeys.first,
|
||||
@@ -1935,7 +1876,7 @@ private:
|
||||
auto const publisherSecret = randomSecretKey();
|
||||
auto const publisherPublic = derivePublicKey(KeyType::Ed25519, publisherSecret);
|
||||
auto const pubSigningKeys = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const manifest = base64Encode(makeManifestString(
|
||||
auto const manifest = base64Encode(makeManifest(
|
||||
publisherPublic,
|
||||
publisherSecret,
|
||||
pubSigningKeys.first,
|
||||
@@ -2533,16 +2474,18 @@ private:
|
||||
auto const pubSigningKeys = randomKeyPair(KeyType::Secp256k1);
|
||||
cfgPublishers.push_back(strHex(publisherPublic));
|
||||
|
||||
constexpr auto kRevoked = std::numeric_limits<std::uint32_t>::max();
|
||||
auto const manifest = base64Encode(makeManifestString(
|
||||
publisherPublic,
|
||||
publisherSecret,
|
||||
pubSigningKeys.first,
|
||||
pubSigningKeys.second,
|
||||
i < countRevoked ? kRevoked : 1));
|
||||
bool const revoked = i < countRevoked;
|
||||
auto const manifest = base64Encode(
|
||||
revoked ? makeRevocation(publisherPublic, publisherSecret)
|
||||
: makeManifest(
|
||||
publisherPublic,
|
||||
publisherSecret,
|
||||
pubSigningKeys.first,
|
||||
pubSigningKeys.second,
|
||||
1));
|
||||
publishers.push_back(
|
||||
Publisher{
|
||||
.revoked = i < countRevoked,
|
||||
.revoked = revoked,
|
||||
.pubKey = publisherPublic,
|
||||
.signingKeys = pubSigningKeys,
|
||||
.manifest = manifest,
|
||||
|
||||
@@ -7,14 +7,10 @@
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
@@ -111,31 +107,6 @@ public:
|
||||
std::string manifest;
|
||||
};
|
||||
|
||||
static std::string
|
||||
makeManifestString(
|
||||
PublicKey const& pk,
|
||||
SecretKey const& sk,
|
||||
PublicKey const& spk,
|
||||
SecretKey const& ssk,
|
||||
int seq)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = pk;
|
||||
st[sfSigningPubKey] = spk;
|
||||
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access) publicKeyType returns value for valid
|
||||
// keys
|
||||
sign(st, HashPrefix::Manifest, *publicKeyType(spk), ssk);
|
||||
sign(st, HashPrefix::Manifest, *publicKeyType(pk), sk, sfMasterSignature);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
return base64Encode(std::string(static_cast<char const*>(s.data()), s.size()));
|
||||
}
|
||||
|
||||
static Validator
|
||||
randomValidator()
|
||||
{
|
||||
@@ -145,8 +116,8 @@ public:
|
||||
return {
|
||||
.masterPublic = masterPublic,
|
||||
.signingPublic = signingKeys.first,
|
||||
.manifest =
|
||||
makeManifestString(masterPublic, secret, signingKeys.first, signingKeys.second, 1)};
|
||||
.manifest = base64Encode(
|
||||
makeManifest(masterPublic, secret, signingKeys.first, signingKeys.second, 1))};
|
||||
}
|
||||
|
||||
// TrustedPublisherServer must be accessed through a shared_ptr.
|
||||
@@ -174,8 +145,8 @@ public:
|
||||
, publisherPublic_{derivePublicKey(KeyType::Ed25519, publisherSecret_)}
|
||||
{
|
||||
auto const keys = randomKeyPair(KeyType::Secp256k1);
|
||||
auto const manifest =
|
||||
makeManifestString(publisherPublic_, publisherSecret_, keys.first, keys.second, 1);
|
||||
auto const manifest = base64Encode(
|
||||
makeManifest(publisherPublic_, publisherSecret_, keys.first, keys.second, 1));
|
||||
|
||||
std::vector<BlobInfo> blobInfo;
|
||||
blobInfo.reserve(futures.size() + 1);
|
||||
|
||||
@@ -27,6 +27,8 @@ TEST(EscrowTests, BuilderSettersRoundTrip)
|
||||
auto const conditionValue = canonical_VL();
|
||||
auto const cancelAfterValue = canonical_UINT32();
|
||||
auto const finishAfterValue = canonical_UINT32();
|
||||
auto const bytecodeValue = canonical_VL();
|
||||
auto const dataValue = canonical_VL();
|
||||
auto const sourceTagValue = canonical_UINT32();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
@@ -49,6 +51,8 @@ TEST(EscrowTests, BuilderSettersRoundTrip)
|
||||
builder.setCondition(conditionValue);
|
||||
builder.setCancelAfter(cancelAfterValue);
|
||||
builder.setFinishAfter(finishAfterValue);
|
||||
builder.setBytecode(bytecodeValue);
|
||||
builder.setData(dataValue);
|
||||
builder.setSourceTag(sourceTagValue);
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setDestinationNode(destinationNodeValue);
|
||||
@@ -132,6 +136,22 @@ TEST(EscrowTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasFinishAfter());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = bytecodeValue;
|
||||
auto const actualOpt = entry.getBytecode();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfBytecode");
|
||||
EXPECT_TRUE(entry.hasBytecode());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = dataValue;
|
||||
auto const actualOpt = entry.getData();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfData");
|
||||
EXPECT_TRUE(entry.hasData());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = sourceTagValue;
|
||||
auto const actualOpt = entry.getSourceTag();
|
||||
@@ -192,6 +212,8 @@ TEST(EscrowTests, BuilderFromSleRoundTrip)
|
||||
auto const conditionValue = canonical_VL();
|
||||
auto const cancelAfterValue = canonical_UINT32();
|
||||
auto const finishAfterValue = canonical_UINT32();
|
||||
auto const bytecodeValue = canonical_VL();
|
||||
auto const dataValue = canonical_VL();
|
||||
auto const sourceTagValue = canonical_UINT32();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
@@ -210,6 +232,8 @@ TEST(EscrowTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfCondition) = conditionValue;
|
||||
sle->at(sfCancelAfter) = cancelAfterValue;
|
||||
sle->at(sfFinishAfter) = finishAfterValue;
|
||||
sle->at(sfBytecode) = bytecodeValue;
|
||||
sle->at(sfData) = dataValue;
|
||||
sle->at(sfSourceTag) = sourceTagValue;
|
||||
sle->at(sfDestinationTag) = destinationTagValue;
|
||||
sle->at(sfOwnerNode) = ownerNodeValue;
|
||||
@@ -340,6 +364,32 @@ TEST(EscrowTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfFinishAfter");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = bytecodeValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getBytecode();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getBytecode();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfBytecode");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfBytecode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = dataValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getData();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getData();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfData");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfData");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = sourceTagValue;
|
||||
|
||||
@@ -477,6 +527,10 @@ TEST(EscrowTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getCancelAfter().has_value());
|
||||
EXPECT_FALSE(entry.hasFinishAfter());
|
||||
EXPECT_FALSE(entry.getFinishAfter().has_value());
|
||||
EXPECT_FALSE(entry.hasBytecode());
|
||||
EXPECT_FALSE(entry.getBytecode().has_value());
|
||||
EXPECT_FALSE(entry.hasData());
|
||||
EXPECT_FALSE(entry.getData().has_value());
|
||||
EXPECT_FALSE(entry.hasSourceTag());
|
||||
EXPECT_FALSE(entry.getSourceTag().has_value());
|
||||
EXPECT_FALSE(entry.hasDestinationTag());
|
||||
|
||||
@@ -27,6 +27,9 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip)
|
||||
auto const baseFeeDropsValue = canonical_AMOUNT();
|
||||
auto const reserveBaseDropsValue = canonical_AMOUNT();
|
||||
auto const reserveIncrementDropsValue = canonical_AMOUNT();
|
||||
auto const gasLimitValue = canonical_UINT32();
|
||||
auto const bytecodeSizeLimitValue = canonical_UINT32();
|
||||
auto const gasPriceValue = canonical_UINT32();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
@@ -40,6 +43,9 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip)
|
||||
builder.setBaseFeeDrops(baseFeeDropsValue);
|
||||
builder.setReserveBaseDrops(reserveBaseDropsValue);
|
||||
builder.setReserveIncrementDrops(reserveIncrementDropsValue);
|
||||
builder.setGasLimit(gasLimitValue);
|
||||
builder.setBytecodeSizeLimit(bytecodeSizeLimitValue);
|
||||
builder.setGasPrice(gasPriceValue);
|
||||
builder.setPreviousTxnID(previousTxnIDValue);
|
||||
builder.setPreviousTxnLgrSeq(previousTxnLgrSeqValue);
|
||||
|
||||
@@ -108,6 +114,30 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasReserveIncrementDrops());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasLimitValue;
|
||||
auto const actualOpt = entry.getGasLimit();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfGasLimit");
|
||||
EXPECT_TRUE(entry.hasGasLimit());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = bytecodeSizeLimitValue;
|
||||
auto const actualOpt = entry.getBytecodeSizeLimit();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit");
|
||||
EXPECT_TRUE(entry.hasBytecodeSizeLimit());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasPriceValue;
|
||||
auto const actualOpt = entry.getGasPrice();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfGasPrice");
|
||||
EXPECT_TRUE(entry.hasGasPrice());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
auto const actualOpt = entry.getPreviousTxnID();
|
||||
@@ -144,6 +174,9 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip)
|
||||
auto const baseFeeDropsValue = canonical_AMOUNT();
|
||||
auto const reserveBaseDropsValue = canonical_AMOUNT();
|
||||
auto const reserveIncrementDropsValue = canonical_AMOUNT();
|
||||
auto const gasLimitValue = canonical_UINT32();
|
||||
auto const bytecodeSizeLimitValue = canonical_UINT32();
|
||||
auto const gasPriceValue = canonical_UINT32();
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
|
||||
@@ -156,6 +189,9 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfBaseFeeDrops) = baseFeeDropsValue;
|
||||
sle->at(sfReserveBaseDrops) = reserveBaseDropsValue;
|
||||
sle->at(sfReserveIncrementDrops) = reserveIncrementDropsValue;
|
||||
sle->at(sfGasLimit) = gasLimitValue;
|
||||
sle->at(sfBytecodeSizeLimit) = bytecodeSizeLimitValue;
|
||||
sle->at(sfGasPrice) = gasPriceValue;
|
||||
sle->at(sfPreviousTxnID) = previousTxnIDValue;
|
||||
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
|
||||
|
||||
@@ -259,6 +295,45 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfReserveIncrementDrops");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasLimitValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getGasLimit();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getGasLimit();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfGasLimit");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfGasLimit");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = bytecodeSizeLimitValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getBytecodeSizeLimit();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getBytecodeSizeLimit();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfBytecodeSizeLimit");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfBytecodeSizeLimit");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasPriceValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getGasPrice();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getGasPrice();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfGasPrice");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfGasPrice");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
|
||||
@@ -351,6 +426,12 @@ TEST(FeeSettingsTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getReserveBaseDrops().has_value());
|
||||
EXPECT_FALSE(entry.hasReserveIncrementDrops());
|
||||
EXPECT_FALSE(entry.getReserveIncrementDrops().has_value());
|
||||
EXPECT_FALSE(entry.hasGasLimit());
|
||||
EXPECT_FALSE(entry.getGasLimit().has_value());
|
||||
EXPECT_FALSE(entry.hasBytecodeSizeLimit());
|
||||
EXPECT_FALSE(entry.getBytecodeSizeLimit().has_value());
|
||||
EXPECT_FALSE(entry.hasGasPrice());
|
||||
EXPECT_FALSE(entry.getGasPrice().has_value());
|
||||
EXPECT_FALSE(entry.hasPreviousTxnID());
|
||||
EXPECT_FALSE(entry.getPreviousTxnID().has_value());
|
||||
EXPECT_FALSE(entry.hasPreviousTxnLgrSeq());
|
||||
|
||||
@@ -30,11 +30,13 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
auto const conditionValue = canonical_VL();
|
||||
auto const cancelAfterValue = canonical_UINT32();
|
||||
auto const finishAfterValue = canonical_UINT32();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const bytecodeValue = canonical_VL();
|
||||
auto const dataValue = canonical_VL();
|
||||
|
||||
EscrowCreateBuilder builder{
|
||||
accountValue,
|
||||
@@ -45,10 +47,12 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setCondition(conditionValue);
|
||||
builder.setCancelAfter(cancelAfterValue);
|
||||
builder.setFinishAfter(finishAfterValue);
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setBytecode(bytecodeValue);
|
||||
builder.setData(dataValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
@@ -78,6 +82,14 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = tx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
EXPECT_TRUE(tx.hasDestinationTag());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = conditionValue;
|
||||
auto const actualOpt = tx.getCondition();
|
||||
@@ -103,11 +115,19 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = tx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
EXPECT_TRUE(tx.hasDestinationTag());
|
||||
auto const& expected = bytecodeValue;
|
||||
auto const actualOpt = tx.getBytecode();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecode should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBytecode");
|
||||
EXPECT_TRUE(tx.hasBytecode());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = dataValue;
|
||||
auto const actualOpt = tx.getData();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfData");
|
||||
EXPECT_TRUE(tx.hasData());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -127,11 +147,13 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
auto const conditionValue = canonical_VL();
|
||||
auto const cancelAfterValue = canonical_UINT32();
|
||||
auto const finishAfterValue = canonical_UINT32();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const bytecodeValue = canonical_VL();
|
||||
auto const dataValue = canonical_VL();
|
||||
|
||||
// Build an initial transaction
|
||||
EscrowCreateBuilder initialBuilder{
|
||||
@@ -142,10 +164,12 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setDestinationTag(destinationTagValue);
|
||||
initialBuilder.setCondition(conditionValue);
|
||||
initialBuilder.setCancelAfter(cancelAfterValue);
|
||||
initialBuilder.setFinishAfter(finishAfterValue);
|
||||
initialBuilder.setDestinationTag(destinationTagValue);
|
||||
initialBuilder.setBytecode(bytecodeValue);
|
||||
initialBuilder.setData(dataValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
@@ -176,6 +200,13 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = rebuiltTx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = conditionValue;
|
||||
auto const actualOpt = rebuiltTx.getCondition();
|
||||
@@ -198,10 +229,17 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = rebuiltTx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
auto const& expected = bytecodeValue;
|
||||
auto const actualOpt = rebuiltTx.getBytecode();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecode should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBytecode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = dataValue;
|
||||
auto const actualOpt = rebuiltTx.getData();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfData");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -263,14 +301,18 @@ TEST(TransactionsEscrowCreateTests, OptionalFieldsReturnNullopt)
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasDestinationTag());
|
||||
EXPECT_FALSE(tx.getDestinationTag().has_value());
|
||||
EXPECT_FALSE(tx.hasCondition());
|
||||
EXPECT_FALSE(tx.getCondition().has_value());
|
||||
EXPECT_FALSE(tx.hasCancelAfter());
|
||||
EXPECT_FALSE(tx.getCancelAfter().has_value());
|
||||
EXPECT_FALSE(tx.hasFinishAfter());
|
||||
EXPECT_FALSE(tx.getFinishAfter().has_value());
|
||||
EXPECT_FALSE(tx.hasDestinationTag());
|
||||
EXPECT_FALSE(tx.getDestinationTag().has_value());
|
||||
EXPECT_FALSE(tx.hasBytecode());
|
||||
EXPECT_FALSE(tx.getBytecode().has_value());
|
||||
EXPECT_FALSE(tx.hasData());
|
||||
EXPECT_FALSE(tx.getData().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip)
|
||||
auto const fulfillmentValue = canonical_VL();
|
||||
auto const conditionValue = canonical_VL();
|
||||
auto const credentialIDsValue = canonical_VECTOR256();
|
||||
auto const gasValue = canonical_UINT32();
|
||||
|
||||
EscrowFinishBuilder builder{
|
||||
accountValue,
|
||||
@@ -47,6 +48,7 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip)
|
||||
builder.setFulfillment(fulfillmentValue);
|
||||
builder.setCondition(conditionValue);
|
||||
builder.setCredentialIDs(credentialIDsValue);
|
||||
builder.setGas(gasValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
@@ -100,6 +102,14 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(tx.hasCredentialIDs());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasValue;
|
||||
auto const actualOpt = tx.getGas();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGas should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfGas");
|
||||
EXPECT_TRUE(tx.hasGas());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
@@ -121,6 +131,7 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip)
|
||||
auto const fulfillmentValue = canonical_VL();
|
||||
auto const conditionValue = canonical_VL();
|
||||
auto const credentialIDsValue = canonical_VECTOR256();
|
||||
auto const gasValue = canonical_UINT32();
|
||||
|
||||
// Build an initial transaction
|
||||
EscrowFinishBuilder initialBuilder{
|
||||
@@ -134,6 +145,7 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip)
|
||||
initialBuilder.setFulfillment(fulfillmentValue);
|
||||
initialBuilder.setCondition(conditionValue);
|
||||
initialBuilder.setCredentialIDs(credentialIDsValue);
|
||||
initialBuilder.setGas(gasValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
@@ -185,6 +197,13 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip)
|
||||
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasValue;
|
||||
auto const actualOpt = rebuiltTx.getGas();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGas should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfGas");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
@@ -250,6 +269,8 @@ TEST(TransactionsEscrowFinishTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(tx.getCondition().has_value());
|
||||
EXPECT_FALSE(tx.hasCredentialIDs());
|
||||
EXPECT_FALSE(tx.getCredentialIDs().has_value());
|
||||
EXPECT_FALSE(tx.hasGas());
|
||||
EXPECT_FALSE(tx.getGas().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip)
|
||||
auto const baseFeeDropsValue = canonical_AMOUNT();
|
||||
auto const reserveBaseDropsValue = canonical_AMOUNT();
|
||||
auto const reserveIncrementDropsValue = canonical_AMOUNT();
|
||||
auto const gasLimitValue = canonical_UINT32();
|
||||
auto const bytecodeSizeLimitValue = canonical_UINT32();
|
||||
auto const gasPriceValue = canonical_UINT32();
|
||||
|
||||
SetFeeBuilder builder{
|
||||
accountValue,
|
||||
@@ -53,6 +56,9 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip)
|
||||
builder.setBaseFeeDrops(baseFeeDropsValue);
|
||||
builder.setReserveBaseDrops(reserveBaseDropsValue);
|
||||
builder.setReserveIncrementDrops(reserveIncrementDropsValue);
|
||||
builder.setGasLimit(gasLimitValue);
|
||||
builder.setBytecodeSizeLimit(bytecodeSizeLimitValue);
|
||||
builder.setGasPrice(gasPriceValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
@@ -134,6 +140,30 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(tx.hasReserveIncrementDrops());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasLimitValue;
|
||||
auto const actualOpt = tx.getGasLimit();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasLimit should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfGasLimit");
|
||||
EXPECT_TRUE(tx.hasGasLimit());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = bytecodeSizeLimitValue;
|
||||
auto const actualOpt = tx.getBytecodeSizeLimit();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecodeSizeLimit should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit");
|
||||
EXPECT_TRUE(tx.hasBytecodeSizeLimit());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasPriceValue;
|
||||
auto const actualOpt = tx.getGasPrice();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasPrice should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfGasPrice");
|
||||
EXPECT_TRUE(tx.hasGasPrice());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
@@ -158,6 +188,9 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip)
|
||||
auto const baseFeeDropsValue = canonical_AMOUNT();
|
||||
auto const reserveBaseDropsValue = canonical_AMOUNT();
|
||||
auto const reserveIncrementDropsValue = canonical_AMOUNT();
|
||||
auto const gasLimitValue = canonical_UINT32();
|
||||
auto const bytecodeSizeLimitValue = canonical_UINT32();
|
||||
auto const gasPriceValue = canonical_UINT32();
|
||||
|
||||
// Build an initial transaction
|
||||
SetFeeBuilder initialBuilder{
|
||||
@@ -174,6 +207,9 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip)
|
||||
initialBuilder.setBaseFeeDrops(baseFeeDropsValue);
|
||||
initialBuilder.setReserveBaseDrops(reserveBaseDropsValue);
|
||||
initialBuilder.setReserveIncrementDrops(reserveIncrementDropsValue);
|
||||
initialBuilder.setGasLimit(gasLimitValue);
|
||||
initialBuilder.setBytecodeSizeLimit(bytecodeSizeLimitValue);
|
||||
initialBuilder.setGasPrice(gasPriceValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
@@ -248,6 +284,27 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip)
|
||||
expectEqualField(expected, *actualOpt, "sfReserveIncrementDrops");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasLimitValue;
|
||||
auto const actualOpt = rebuiltTx.getGasLimit();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasLimit should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfGasLimit");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = bytecodeSizeLimitValue;
|
||||
auto const actualOpt = rebuiltTx.getBytecodeSizeLimit();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecodeSizeLimit should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = gasPriceValue;
|
||||
auto const actualOpt = rebuiltTx.getGasPrice();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasPrice should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfGasPrice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
@@ -319,6 +376,12 @@ TEST(TransactionsSetFeeTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(tx.getReserveBaseDrops().has_value());
|
||||
EXPECT_FALSE(tx.hasReserveIncrementDrops());
|
||||
EXPECT_FALSE(tx.getReserveIncrementDrops().has_value());
|
||||
EXPECT_FALSE(tx.hasGasLimit());
|
||||
EXPECT_FALSE(tx.getGasLimit().has_value());
|
||||
EXPECT_FALSE(tx.hasBytecodeSizeLimit());
|
||||
EXPECT_FALSE(tx.getBytecodeSizeLimit().has_value());
|
||||
EXPECT_FALSE(tx.hasGasPrice());
|
||||
EXPECT_FALSE(tx.getGasPrice().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
src/tests/tools/validator-keys/CMakeLists.txt
Normal file
24
src/tests/tools/validator-keys/CMakeLists.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
find_package(GTest REQUIRED)
|
||||
|
||||
add_executable(
|
||||
validator_keys_tests
|
||||
Commands.cpp
|
||||
ListSigning.cpp
|
||||
SigningKeys.cpp
|
||||
"${CMAKE_SOURCE_DIR}/src/tests/libxrpl/main.cpp"
|
||||
)
|
||||
patch_nix_binary(validator_keys_tests)
|
||||
set_target_properties(
|
||||
validator_keys_tests
|
||||
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_include_directories(
|
||||
validator_keys_tests
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(
|
||||
validator_keys_tests
|
||||
PRIVATE GTest::gtest GTest::gmock xrpl.validator-keys
|
||||
)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(validator_keys_tests)
|
||||
552
src/tests/tools/validator-keys/Commands.cpp
Normal file
552
src/tests/tools/validator-keys/Commands.cpp
Normal file
@@ -0,0 +1,552 @@
|
||||
#include <tools/validator-keys/Commands.h>
|
||||
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <tools/validator-keys/ListSigning.h>
|
||||
#include <tools/validator-keys/SigningKeys.h>
|
||||
|
||||
#include <Fixtures.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::tools::test {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string const kArgError = "Syntax error: Wrong number of arguments";
|
||||
std::string const kRevokedOperation = "Operation error: The specified master key has been revoked!";
|
||||
std::string const kExhausted =
|
||||
"Maximum number of tokens have already been generated.\n"
|
||||
"Revoke validator keys if previous token has been compromised.";
|
||||
|
||||
class CommandsTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
TempDir dir_;
|
||||
ToolOptions options_ = optionsFor(dir_.file("validator_keys.json"));
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
file(std::string const& name) const
|
||||
{
|
||||
return dir_.file(name);
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::string
|
||||
commandError(
|
||||
std::string const& command,
|
||||
std::vector<std::string> const& args,
|
||||
ToolOptions const& opts)
|
||||
{
|
||||
return errorOf([&] { run(command, args, opts); });
|
||||
}
|
||||
|
||||
[[nodiscard]] static SigningKeys
|
||||
keys(ToolOptions const& opts)
|
||||
{
|
||||
return SigningKeys::makeSigningKeys(opts.keyFile);
|
||||
}
|
||||
|
||||
// The token base64 inside a [validator_token] or [validator_manifest] block.
|
||||
static std::string
|
||||
blockBody(std::string const& text, std::string const& section)
|
||||
{
|
||||
auto pos = text.find("[" + section + "]\n");
|
||||
if (pos == std::string::npos)
|
||||
{
|
||||
ADD_FAILURE() << "no [" << section << "] block in: " << text;
|
||||
return {};
|
||||
}
|
||||
std::string body;
|
||||
for (auto rest = text.substr(pos + section.size() + 3); !rest.empty();)
|
||||
{
|
||||
auto const eol = rest.find('\n');
|
||||
auto const line = rest.substr(0, eol);
|
||||
if (line.empty())
|
||||
break;
|
||||
body += line;
|
||||
rest = eol == std::string::npos ? "" : rest.substr(eol + 1);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(CommandsTest, dispatch)
|
||||
{
|
||||
EXPECT_EQ(commandError("unknown", {}, options_), "Unknown command: unknown");
|
||||
EXPECT_EQ(commandError("create_keys", {"x"}, options_), kArgError);
|
||||
EXPECT_EQ(commandError("finish_token", {}, options_), kArgError);
|
||||
EXPECT_EQ(commandError("finish_token", {"a", "b", "c"}, options_), kArgError);
|
||||
EXPECT_EQ(commandError("finish_sign_list", {"a"}, options_), kArgError);
|
||||
EXPECT_FALSE(getVersionString().empty());
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, create_keys)
|
||||
{
|
||||
auto const r = run("create_keys", {}, options_);
|
||||
EXPECT_EQ(r.rc, EXIT_SUCCESS);
|
||||
EXPECT_NE(
|
||||
r.out.find("Validator keys stored in " + options_.keyFile.string()), std::string::npos);
|
||||
EXPECT_TRUE(keys(options_).hasSecret());
|
||||
EXPECT_EQ(
|
||||
commandError("create_keys", {}, options_),
|
||||
"Refusing to overwrite existing key file: " + options_.keyFile.string());
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, create_external)
|
||||
{
|
||||
SigningKeys const external(KeyType::Ed25519);
|
||||
auto const& key = external.publicKey();
|
||||
for (auto const& encoded :
|
||||
{toBase58(TokenType::NodePublic, key), strHex(key), base64Encode(key.data(), key.size())})
|
||||
{
|
||||
ToolOptions const opts =
|
||||
optionsFor(file("external-" + std::to_string(encoded.size()) + ".json"));
|
||||
EXPECT_EQ(run("create_external", {encoded}, opts).rc, EXIT_SUCCESS);
|
||||
auto const loaded = keys(opts);
|
||||
EXPECT_FALSE(loaded.hasSecret());
|
||||
EXPECT_EQ(loaded.publicKey(), key);
|
||||
}
|
||||
EXPECT_EQ(
|
||||
commandError("create_external", {"abcd"}, options_), "Unable to parse public key: abcd");
|
||||
auto badHex = strHex(key);
|
||||
badHex.insert(badHex.size() / 2, "n");
|
||||
EXPECT_EQ(
|
||||
commandError("create_external", {badHex}, options_),
|
||||
"Unable to parse public key: " + badHex);
|
||||
run("create_external", {strHex(key)}, options_);
|
||||
EXPECT_EQ(
|
||||
commandError("create_external", {strHex(key)}, options_),
|
||||
"Refusing to overwrite existing key file: " + options_.keyFile.string());
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, create_token)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
commandError("create_token", {}, options_),
|
||||
"Failed to open key file: " + options_.keyFile.string());
|
||||
run("create_keys", {}, options_);
|
||||
|
||||
auto const r = run("create_token", {}, options_);
|
||||
EXPECT_EQ(r.rc, EXIT_SUCCESS);
|
||||
auto const token = required(loadValidatorToken({blockBody(r.out, "validator_token")}));
|
||||
auto const m = required(deserializeManifest(base64Decode(token.manifest)));
|
||||
EXPECT_EQ(m.sequence, 1u);
|
||||
EXPECT_EQ(required(m.signingKey), derivePublicKey(KeyType::Secp256k1, token.validationSecret));
|
||||
|
||||
// Written to a file readable by the owner only, as an ed25519 token
|
||||
ToolOptions toFile = options_;
|
||||
toFile.tokenKeyType = KeyType::Ed25519;
|
||||
toFile.outFile = file("token.txt");
|
||||
EXPECT_NE(run("create_token", {}, toFile).out.find("written to"), std::string::npos);
|
||||
auto const written = loadTokenFile(*toFile.outFile);
|
||||
auto const writtenManifest = required(deserializeManifest(base64Decode(written.manifest)));
|
||||
EXPECT_EQ(
|
||||
required(writtenManifest.signingKey),
|
||||
derivePublicKey(KeyType::Ed25519, written.validationSecret));
|
||||
EXPECT_EQ(
|
||||
std::filesystem::status(*toFile.outFile).permissions() &
|
||||
(std::filesystem::perms::group_all | std::filesystem::perms::others_all),
|
||||
std::filesystem::perms::none);
|
||||
EXPECT_EQ(keys(options_).sequence(), 2u);
|
||||
|
||||
// A symlink is not written through
|
||||
ToolOptions linked = options_;
|
||||
linked.outFile = file("link.txt");
|
||||
std::filesystem::create_symlink(file("elsewhere.txt"), *linked.outFile);
|
||||
EXPECT_EQ(
|
||||
commandError("create_token", {}, linked),
|
||||
"Refusing to write through a symlink: " + linked.outFile->string());
|
||||
EXPECT_EQ(keys(options_).sequence(), 2u);
|
||||
|
||||
// An unwritable output path fails before the sequence is consumed
|
||||
ToolOptions unwritable = options_;
|
||||
unwritable.outFile = file("missing/token.txt");
|
||||
EXPECT_EQ(
|
||||
commandError("create_token", {}, unwritable),
|
||||
"Cannot write output file: " + unwritable.outFile->string());
|
||||
EXPECT_EQ(keys(options_).sequence(), 2u);
|
||||
|
||||
// The output may not replace the key file
|
||||
ToolOptions aliased = options_;
|
||||
aliased.outFile = options_.keyFile;
|
||||
EXPECT_EQ(
|
||||
commandError("create_token", {}, aliased),
|
||||
"--out names an input file: " + options_.keyFile.string());
|
||||
EXPECT_EQ(keys(options_).sequence(), 2u);
|
||||
|
||||
{
|
||||
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
|
||||
SigningKeys(KeyType::Ed25519, kp.second, std::numeric_limits<std::uint32_t>::max() - 1)
|
||||
.writeToFile(options_.keyFile);
|
||||
EXPECT_EQ(commandError("create_token", {}, options_), kExhausted);
|
||||
}
|
||||
run("revoke_keys", {}, options_);
|
||||
EXPECT_EQ(commandError("create_token", {}, options_), "Validator keys have been revoked.");
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, external_token)
|
||||
{
|
||||
// The signer stands in for the hardware holding the master key
|
||||
SigningKeys const signer(KeyType::Ed25519);
|
||||
run("create_external", {toBase58(TokenType::NodePublic, signer.publicKey())}, options_);
|
||||
|
||||
for (auto const encode : {0, 1})
|
||||
{
|
||||
auto const start = run("start_token", {}, options_);
|
||||
EXPECT_EQ(start.rc, EXIT_SUCCESS);
|
||||
auto const bytes = start.out.substr(0, start.out.find('\n'));
|
||||
auto const sig = signer.signHex(bytes);
|
||||
auto const sigBytes = required(strUnHex(sig));
|
||||
auto const encoded = (encode != 0) ? base64Encode(sigBytes.data(), sigBytes.size()) : sig;
|
||||
auto const finish = run("finish_token", {encoded}, options_);
|
||||
EXPECT_EQ(finish.rc, EXIT_SUCCESS);
|
||||
auto const token = required(loadValidatorToken({blockBody(finish.out, "validator_token")}));
|
||||
auto const m = required(deserializeManifest(base64Decode(token.manifest)));
|
||||
EXPECT_EQ(m.sequence, encode ? 2u : 1u);
|
||||
}
|
||||
EXPECT_EQ(
|
||||
commandError("finish_token", {"bad signature"}, options_), "Invalid signature encoding");
|
||||
run("start_token", {}, options_);
|
||||
EXPECT_EQ(
|
||||
commandError("finish_token", {signer.sign("foo")}, options_),
|
||||
"Manifest is not properly signed");
|
||||
EXPECT_EQ(keys(options_).sequence(), 2u);
|
||||
|
||||
// An external signing key too: two signatures, a manifest without a secret
|
||||
SigningKeys const signingKey(KeyType::Ed25519);
|
||||
ToolOptions both = options_;
|
||||
both.signingKey = signingKey.publicKey();
|
||||
both.outFile = file("manifest.txt");
|
||||
auto const start = run("start_token", {}, both);
|
||||
auto const bytes = start.out.substr(0, start.out.find('\n'));
|
||||
EXPECT_EQ(
|
||||
commandError("finish_token", {signer.signHex(bytes)}, both),
|
||||
"The pending token's signing key is external; pass its signature too");
|
||||
auto const finish =
|
||||
run("finish_token", {signer.signHex(bytes), signingKey.signHex(bytes)}, both);
|
||||
EXPECT_EQ(finish.rc, EXIT_SUCCESS);
|
||||
auto const manifest = loadManifestFile(*both.outFile);
|
||||
EXPECT_EQ(required(manifest.signingKey), signingKey.publicKey());
|
||||
EXPECT_EQ(manifest.sequence, 3u);
|
||||
|
||||
// The domain is stored for the next token; the attestation bytes are printed
|
||||
// for the external signer
|
||||
EXPECT_EQ(run("attest_domain", {}, options_).out.find("No attestation is necessary"), 0u);
|
||||
auto const domain = run("set_domain", {"validator.example.com"}, options_);
|
||||
EXPECT_NE(domain.out.find("run start_token and finish_token"), std::string::npos);
|
||||
EXPECT_EQ(keys(options_).domain(), "validator.example.com");
|
||||
auto const attest = run("attest_domain", {}, options_);
|
||||
EXPECT_EQ(
|
||||
attest.out.substr(0, attest.out.find('\n')),
|
||||
strHex(makeSlice(keys(options_).attestationData())));
|
||||
EXPECT_NE(attest.err.find("Sign these bytes"), std::string::npos);
|
||||
auto const next = run("start_token", {}, options_);
|
||||
auto const nextBytes = next.out.substr(0, next.out.find('\n'));
|
||||
auto const finished = run("finish_token", {signer.signHex(nextBytes)}, options_);
|
||||
auto const token = required(loadValidatorToken({blockBody(finished.out, "validator_token")}));
|
||||
auto const m = required(deserializeManifest(base64Decode(token.manifest)));
|
||||
EXPECT_EQ(m.domain, "validator.example.com");
|
||||
|
||||
// Revocation in two steps
|
||||
auto const startRevoke = run("start_revoke_keys", {}, options_);
|
||||
EXPECT_NE(startRevoke.err.find("This will revoke"), std::string::npos);
|
||||
auto const revokeBytes = startRevoke.out.substr(0, startRevoke.out.find('\n'));
|
||||
EXPECT_EQ(
|
||||
commandError("finish_revoke_keys", {signer.sign("foo")}, options_),
|
||||
"Manifest is not properly signed");
|
||||
EXPECT_FALSE(keys(options_).revoked());
|
||||
auto const revoked = run("finish_revoke_keys", {signer.signHex(revokeBytes)}, options_);
|
||||
EXPECT_NE(revoked.out.find("[validator_key_revocation]"), std::string::npos);
|
||||
EXPECT_TRUE(keys(options_).revoked());
|
||||
EXPECT_NE(
|
||||
run("start_revoke_keys", {}, options_).err.find("already been revoked"), std::string::npos);
|
||||
EXPECT_EQ(commandError("start_token", {}, options_), "Validator keys have been revoked.");
|
||||
EXPECT_EQ(commandError("finish_token", {"00"}, options_), "Validator keys have been revoked.");
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, revoke_keys)
|
||||
{
|
||||
run("create_keys", {}, options_);
|
||||
auto const first = run("revoke_keys", {}, options_);
|
||||
EXPECT_NE(first.err.find("This will revoke"), std::string::npos);
|
||||
EXPECT_NE(first.out.find("[validator_key_revocation]"), std::string::npos);
|
||||
auto const again = run("revoke_keys", {}, options_);
|
||||
EXPECT_NE(again.err.find("already been revoked"), std::string::npos);
|
||||
EXPECT_EQ(commandError("set_domain", {"validator.example.com"}, options_), kRevokedOperation);
|
||||
EXPECT_EQ(commandError("attest_domain", {}, options_), kRevokedOperation);
|
||||
EXPECT_NE(run("sign", {"data"}, options_).err.find("have been revoked"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, domain)
|
||||
{
|
||||
run("create_keys", {}, options_);
|
||||
EXPECT_NE(run("show_manifest", {"hex"}, options_).out.find("unavailable"), std::string::npos);
|
||||
EXPECT_NE(run("clear_domain", {}, options_).out.find("already cleared"), std::string::npos);
|
||||
|
||||
auto const set = run("set_domain", {"validator.example.com"}, options_);
|
||||
EXPECT_NE(set.out.find("has been set to: validator.example.com"), std::string::npos);
|
||||
EXPECT_NE(set.out.find("attestation=\""), std::string::npos);
|
||||
EXPECT_NE(set.out.find("[validator_token]"), std::string::npos);
|
||||
EXPECT_NE(
|
||||
run("set_domain", {"validator.example.com"}, options_).out.find("already set"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(run("attest_domain", {}, options_).out.find("attestation=\""), std::string::npos);
|
||||
EXPECT_NE(run("show_manifest", {"base64"}, options_).out.find("(Base64)"), std::string::npos);
|
||||
EXPECT_NE(run("show_manifest", {"hex"}, options_).out.find("(Hex)"), std::string::npos);
|
||||
EXPECT_EQ(commandError("show_manifest", {"other"}, options_), "Unknown encoding 'other'");
|
||||
EXPECT_NE(run("clear_domain", {}, options_).out.find("has been cleared"), std::string::npos);
|
||||
EXPECT_EQ(
|
||||
commandError("set_domain", {"-bad.example"}, options_),
|
||||
"The domain field must use the '[host.][subdomain.]domain.tld' format");
|
||||
|
||||
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
|
||||
SigningKeys(KeyType::Ed25519, kp.second, std::numeric_limits<std::uint32_t>::max() - 1)
|
||||
.writeToFile(options_.keyFile);
|
||||
EXPECT_EQ(commandError("set_domain", {"other.example.com"}, options_), kExhausted);
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, sign)
|
||||
{
|
||||
run("create_keys", {}, options_);
|
||||
EXPECT_EQ(
|
||||
commandError("sign", {""}, options_), "Syntax error: Must specify data string to sign");
|
||||
auto const loaded = keys(options_);
|
||||
EXPECT_EQ(run("sign", {"data"}, options_).out, loaded.sign("data") + "\n");
|
||||
EXPECT_EQ(run("sign_hex", {"00FF"}, options_).out, loaded.signHex("00FF") + "\n");
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, list_commands)
|
||||
{
|
||||
// The publisher: a key file and a token carrying an ed25519 signing key
|
||||
ToolOptions publisher = optionsFor(file("publisher.json"));
|
||||
publisher.tokenKeyType = KeyType::Ed25519;
|
||||
publisher.tokenFile = file("publisher-token.txt");
|
||||
publisher.outFile = publisher.tokenFile;
|
||||
run("create_keys", {}, publisher);
|
||||
run("create_token", {}, publisher);
|
||||
publisher.outFile.reset();
|
||||
auto const master = keys(publisher).publicKey();
|
||||
|
||||
auto const unsignedList = file("unsigned.json");
|
||||
auto const now = netClockNow();
|
||||
writeFile(unsignedList, unsignedListText(makeValidators(2), 2026091301, now + 3600));
|
||||
|
||||
ToolOptions noToken = publisher;
|
||||
noToken.tokenFile.reset();
|
||||
EXPECT_EQ(
|
||||
commandError("sign_list", {unsignedList.string()}, noToken),
|
||||
"sign_list needs --token-file");
|
||||
|
||||
// Sign to a file, then verify with every check on
|
||||
ToolOptions signer = publisher;
|
||||
signer.outFile = file("vl.json");
|
||||
EXPECT_NE(
|
||||
run("sign_list", {unsignedList.string()}, signer).out.find("written to"),
|
||||
std::string::npos);
|
||||
|
||||
ToolOptions verifier = optionsFor(publisher.keyFile);
|
||||
verifier.validatorsFile = unsignedList;
|
||||
verifier.expectedKey = master;
|
||||
auto const verified = run("verify_list", {signer.outFile->string()}, verifier);
|
||||
EXPECT_EQ(verified.rc, EXIT_SUCCESS) << verified.out;
|
||||
{
|
||||
ToolOptions wrong = verifier;
|
||||
wrong.expectedKey = SigningKeys(KeyType::Ed25519).publicKey();
|
||||
EXPECT_EQ(run("verify_list", {signer.outFile->string()}, wrong).rc, EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// To stdout without --out
|
||||
{
|
||||
ToolOptions const stdoutSigner = publisher;
|
||||
auto const r = run("sign_list", {unsignedList.string()}, stdoutSigner);
|
||||
json::Reader reader;
|
||||
json::Value jv;
|
||||
EXPECT_TRUE(reader.parse(r.out, jv)) << r.out;
|
||||
EXPECT_EQ(jv[jss::public_key].asString(), strHex(master));
|
||||
}
|
||||
|
||||
// Version 2, appended to itself, then appended again after a key rotation
|
||||
ToolOptions v2 = signer;
|
||||
v2.listVersion = 2;
|
||||
v2.outFile = file("vl2.json");
|
||||
run("sign_list", {unsignedList.string()}, v2);
|
||||
v2.appendFile = v2.outFile;
|
||||
v2.outFile = file("vl2b.json");
|
||||
run("sign_list", {unsignedList.string()}, v2);
|
||||
EXPECT_EQ(run("verify_list", {v2.outFile->string()}, verifier).rc, EXIT_SUCCESS);
|
||||
{
|
||||
ToolOptions rotated = v2;
|
||||
rotated.outFile = file("publisher-token-2.txt");
|
||||
run("create_token", {}, rotated);
|
||||
rotated.tokenFile = rotated.outFile;
|
||||
rotated.appendFile = v2.outFile;
|
||||
rotated.outFile = file("vl2c.json");
|
||||
run("sign_list", {unsignedList.string()}, rotated);
|
||||
auto const r = run("verify_list", {rotated.outFile->string()}, verifier);
|
||||
EXPECT_EQ(r.rc, EXIT_SUCCESS) << r.out;
|
||||
json::Reader reader;
|
||||
json::Value report;
|
||||
reader.parse(r.out, report);
|
||||
EXPECT_EQ(report["manifest_sequence"].asUInt(), 2u);
|
||||
EXPECT_EQ(report["blobs"].size(), 3u);
|
||||
}
|
||||
|
||||
// Output paths that cannot be opened, inputs that cannot be read
|
||||
{
|
||||
ToolOptions bad = signer;
|
||||
bad.outFile = file("missing/vl.json");
|
||||
EXPECT_EQ(
|
||||
commandError("sign_list", {unsignedList.string()}, bad),
|
||||
"Cannot write output file: " + bad.outFile->string());
|
||||
|
||||
// The output may not replace the list it signs
|
||||
bad.outFile = unsignedList;
|
||||
EXPECT_EQ(
|
||||
commandError("sign_list", {unsignedList.string()}, bad),
|
||||
"--out names an input file: " + unsignedList.string());
|
||||
|
||||
// A failed command leaves an existing output as it was
|
||||
bad.outFile = file("kept.json");
|
||||
writeFile(*bad.outFile, "previous");
|
||||
EXPECT_EQ(
|
||||
commandError("sign_list", {file("missing.json").string()}, bad),
|
||||
"Failed to open file: " + file("missing.json").string());
|
||||
EXPECT_EQ(readFile(*bad.outFile), "previous");
|
||||
EXPECT_FALSE(std::filesystem::exists(file("kept.json.tmp")));
|
||||
}
|
||||
EXPECT_EQ(
|
||||
commandError("verify_list", {file("missing.json").string()}, verifier),
|
||||
"Failed to open file: " + file("missing.json").string());
|
||||
writeFile(file("not.json"), "nope\n");
|
||||
EXPECT_EQ(
|
||||
commandError("verify_list", {file("not.json").string()}, verifier),
|
||||
"Not a JSON document: " + file("not.json").string());
|
||||
|
||||
// Tokens that cannot sign a list: an invalid manifest, a secret of another key
|
||||
auto const writeToken = [&](std::filesystem::path const& path, ValidatorToken const& token) {
|
||||
writeFile(path, "[validator_token]\n" + tokenToBase64(token) + "\n");
|
||||
};
|
||||
auto const secret = generateSecretKey(KeyType::Ed25519, randomSeed());
|
||||
{
|
||||
ToolOptions badManifest = signer;
|
||||
badManifest.tokenFile = file("bad-manifest-token.txt");
|
||||
writeToken(
|
||||
*badManifest.tokenFile, ValidatorToken{.manifest = "AAAA", .validationSecret = secret});
|
||||
EXPECT_EQ(
|
||||
commandError("sign_list", {unsignedList.string()}, badManifest),
|
||||
"The token's manifest is not valid");
|
||||
ToolOptions wrongSecret = signer;
|
||||
wrongSecret.tokenFile = file("wrong-secret-token.txt");
|
||||
writeToken(
|
||||
*wrongSecret.tokenFile,
|
||||
ValidatorToken{
|
||||
.manifest = loadTokenFile(*publisher.tokenFile).manifest,
|
||||
.validationSecret = secret});
|
||||
EXPECT_EQ(
|
||||
commandError("sign_list", {unsignedList.string()}, wrongSecret),
|
||||
"The token's secret does not match its manifest");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CommandsTest, external_list_signing)
|
||||
{
|
||||
// The master key delegates to a signing key it never holds; the list is then
|
||||
// signed in two steps with that key.
|
||||
ToolOptions const publisher = optionsFor(file("publisher.json"));
|
||||
run("create_keys", {}, publisher);
|
||||
SigningKeys const external(KeyType::Ed25519);
|
||||
ToolOptions delegate = publisher;
|
||||
delegate.signingKey = external.publicKey();
|
||||
delegate.outFile = file("manifest.txt");
|
||||
auto const start = run("start_token", {}, delegate);
|
||||
auto const bytes = start.out.substr(0, start.out.find('\n'));
|
||||
run("finish_token", {keys(publisher).signHex(bytes), external.signHex(bytes)}, delegate);
|
||||
|
||||
auto const unsignedList = file("unsigned.json");
|
||||
writeFile(unsignedList, unsignedListText(makeValidators(2), 2026091301, netClockNow() + 3600));
|
||||
|
||||
ToolOptions hardware = publisher;
|
||||
EXPECT_EQ(
|
||||
commandError("start_sign_list", {unsignedList.string()}, hardware),
|
||||
"start_sign_list needs --manifest-file");
|
||||
EXPECT_EQ(
|
||||
commandError("finish_sign_list", {"00", unsignedList.string()}, hardware),
|
||||
"finish_sign_list needs --manifest-file");
|
||||
hardware.manifestFile = delegate.outFile;
|
||||
|
||||
auto const toSign = run("start_sign_list", {unsignedList.string()}, hardware);
|
||||
auto const listBytes = toSign.out.substr(0, toSign.out.find('\n'));
|
||||
EXPECT_EQ(listBytes, strHex(makeSlice(loadUnsignedList(unsignedList).canonical)));
|
||||
|
||||
hardware.outFile = file("vl.json");
|
||||
EXPECT_EQ(
|
||||
commandError(
|
||||
"finish_sign_list",
|
||||
{keys(publisher).signHex(listBytes), unsignedList.string()},
|
||||
hardware),
|
||||
"The signature does not verify under the manifest's signing key");
|
||||
EXPECT_EQ(
|
||||
run("finish_sign_list", {external.signHex(listBytes), unsignedList.string()}, hardware).rc,
|
||||
EXIT_SUCCESS);
|
||||
ToolOptions verifier = optionsFor(publisher.keyFile);
|
||||
verifier.validatorsFile = unsignedList;
|
||||
EXPECT_EQ(run("verify_list", {hardware.outFile->string()}, verifier).rc, EXIT_SUCCESS);
|
||||
|
||||
// A version 2 append under the same manifest works; after a rotation it
|
||||
// cannot re-sign the earlier blobs and says so
|
||||
ToolOptions v2 = hardware;
|
||||
v2.listVersion = 2;
|
||||
v2.outFile = file("vl2.json");
|
||||
run("finish_sign_list", {external.signHex(listBytes), unsignedList.string()}, v2);
|
||||
v2.appendFile = v2.outFile;
|
||||
v2.outFile = file("vl2b.json");
|
||||
run("finish_sign_list", {external.signHex(listBytes), unsignedList.string()}, v2);
|
||||
EXPECT_EQ(run("verify_list", {v2.outFile->string()}, verifier).rc, EXIT_SUCCESS);
|
||||
{
|
||||
ToolOptions rotated = publisher;
|
||||
rotated.outFile = file("token.txt");
|
||||
run("create_token", {}, rotated);
|
||||
ToolOptions append = v2;
|
||||
append.manifestFile.reset();
|
||||
append.tokenFile = rotated.outFile;
|
||||
append.appendFile = v2.outFile;
|
||||
append.outFile = file("vl2c.json");
|
||||
EXPECT_EQ(run("sign_list", {unsignedList.string()}, append).rc, EXIT_SUCCESS);
|
||||
EXPECT_EQ(run("verify_list", {append.outFile->string()}, verifier).rc, EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
// A revoked manifest signs nothing
|
||||
SigningKeys revokedKeys(KeyType::Ed25519);
|
||||
ToolOptions revoked = hardware;
|
||||
revoked.manifestFile = file("revoked-manifest.txt");
|
||||
writeFile(*revoked.manifestFile, revokedKeys.revoke() + "\n");
|
||||
EXPECT_EQ(
|
||||
commandError("start_sign_list", {unsignedList.string()}, revoked),
|
||||
"The manifest is revoked");
|
||||
EXPECT_EQ(
|
||||
commandError("finish_sign_list", {"00", unsignedList.string()}, revoked),
|
||||
"The manifest is revoked");
|
||||
}
|
||||
|
||||
} // namespace xrpl::tools::test
|
||||
163
src/tests/tools/validator-keys/Fixtures.h
Normal file
163
src/tests/tools/validator-keys/Fixtures.h
Normal file
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <tools/validator-keys/Commands.h>
|
||||
#include <tools/validator-keys/ListSigning.h>
|
||||
#include <tools/validator-keys/SigningKeys.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::tools::test {
|
||||
|
||||
/**
|
||||
* What one command run produced.
|
||||
*/
|
||||
struct Run
|
||||
{
|
||||
int rc;
|
||||
std::string out;
|
||||
std::string err;
|
||||
};
|
||||
|
||||
inline Run
|
||||
run(std::string const& command, std::vector<std::string> const& args, ToolOptions const& options)
|
||||
{
|
||||
std::ostringstream out;
|
||||
std::ostringstream err;
|
||||
int const rc = runCommand(command, args, options, out, err);
|
||||
return {.rc = rc, .out = out.str(), .err = err.str()};
|
||||
}
|
||||
|
||||
/**
|
||||
* The message of the std::runtime_error @p f throws, or an empty string.
|
||||
*/
|
||||
inline std::string
|
||||
errorOf(std::function<void()> const& f)
|
||||
{
|
||||
try
|
||||
{
|
||||
f();
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
inline ToolOptions
|
||||
optionsFor(std::filesystem::path const& keyFile)
|
||||
{
|
||||
ToolOptions options;
|
||||
options.keyFile = keyFile;
|
||||
return options;
|
||||
}
|
||||
|
||||
inline void
|
||||
writeFile(std::filesystem::path const& file, std::string const& text)
|
||||
{
|
||||
std::error_code ec;
|
||||
writeFileContents(ec, file, text);
|
||||
ASSERT_FALSE(ec) << file;
|
||||
}
|
||||
|
||||
inline std::string
|
||||
readFile(std::filesystem::path const& file)
|
||||
{
|
||||
std::error_code ec;
|
||||
auto const text = getFileContents(ec, file);
|
||||
EXPECT_FALSE(ec) << file;
|
||||
return text;
|
||||
}
|
||||
|
||||
inline bool
|
||||
sameSecret(SecretKey const& a, SecretKey const& b)
|
||||
{
|
||||
return std::equal(a.begin(), a.end(), b.begin());
|
||||
}
|
||||
|
||||
/**
|
||||
* The value a test relies on being present; an empty optional fails the test
|
||||
* with an exception at the line that expected it.
|
||||
*/
|
||||
template <class T>
|
||||
T
|
||||
required(std::optional<T> value)
|
||||
{
|
||||
if (!value)
|
||||
throw std::runtime_error("required value is missing");
|
||||
return std::move(*value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A publisher: master keys and the token carrying its ed25519 signing key.
|
||||
*/
|
||||
struct Publisher
|
||||
{
|
||||
SigningKeys keys{KeyType::Ed25519};
|
||||
ValidatorToken token;
|
||||
Manifest manifest;
|
||||
PublicKey signingKey;
|
||||
|
||||
Publisher()
|
||||
: token(keys.createToken(KeyType::Ed25519))
|
||||
, manifest(required(deserializeManifest(base64Decode(token.manifest))))
|
||||
, signingKey(required(manifest.signingKey))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
inline std::vector<ValidatorToken>
|
||||
makeValidators(std::size_t count)
|
||||
{
|
||||
std::vector<ValidatorToken> validators;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
SigningKeys keys(KeyType::Ed25519);
|
||||
validators.push_back(keys.createToken(KeyType::Secp256k1));
|
||||
}
|
||||
return validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unsigned list text a publisher's prepare step writes: one validator per
|
||||
* token, each with its manifest.
|
||||
*/
|
||||
inline std::string
|
||||
unsignedListText(
|
||||
std::vector<ValidatorToken> const& validators,
|
||||
std::uint32_t sequence,
|
||||
std::uint32_t expiration,
|
||||
std::optional<std::uint32_t> effective = std::nullopt)
|
||||
{
|
||||
std::string text = "{\n \"sequence\": " + std::to_string(sequence);
|
||||
if (effective)
|
||||
text += ",\n \"effective\": " + std::to_string(*effective);
|
||||
text += ",\n \"expiration\": " + std::to_string(expiration) + ",\n \"validators\": [";
|
||||
bool first = true;
|
||||
for (auto const& v : validators)
|
||||
{
|
||||
auto const m = required(deserializeManifest(base64Decode(v.manifest)));
|
||||
text += first ? "\n" : ",\n";
|
||||
first = false;
|
||||
text += R"( {"validation_public_key": ")" + strHex(m.masterKey) + R"(", "manifest": ")" +
|
||||
v.manifest + "\"}";
|
||||
}
|
||||
text += "\n ]\n}\n";
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace xrpl::tools::test
|
||||
518
src/tests/tools/validator-keys/ListSigning.cpp
Normal file
518
src/tests/tools/validator-keys/ListSigning.cpp
Normal file
@@ -0,0 +1,518 @@
|
||||
#include <tools/validator-keys/ListSigning.h>
|
||||
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <tools/validator-keys/SigningKeys.h>
|
||||
|
||||
#include <Fixtures.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::tools::test {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string const kNotObject = "Not a JSON object";
|
||||
|
||||
class ListSigningTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
Publisher const publisher_;
|
||||
std::vector<ValidatorToken> const validators_ = makeValidators(3);
|
||||
std::uint32_t const now_ = 1000;
|
||||
UnsignedList const list_ = parseUnsignedList(unsignedListText(validators_, 7, now_ + 100));
|
||||
std::string const signature_ =
|
||||
signList(list_, publisher_.signingKey, publisher_.token.validationSecret);
|
||||
|
||||
[[nodiscard]] json::Value
|
||||
signed1() const
|
||||
{
|
||||
return makeSignedList(
|
||||
publisher_.token.manifest,
|
||||
publisher_.manifest.masterKey,
|
||||
list_,
|
||||
signature_,
|
||||
1,
|
||||
std::nullopt,
|
||||
{});
|
||||
}
|
||||
|
||||
[[nodiscard]] json::Value
|
||||
signed2() const
|
||||
{
|
||||
return makeSignedList(
|
||||
publisher_.token.manifest,
|
||||
publisher_.manifest.masterKey,
|
||||
list_,
|
||||
signature_,
|
||||
2,
|
||||
std::nullopt,
|
||||
{});
|
||||
}
|
||||
|
||||
static std::string
|
||||
errorOfParse(std::string const& text)
|
||||
{
|
||||
return errorOf([&] { parseUnsignedList(text); });
|
||||
}
|
||||
|
||||
// Verifies and expects @p error among the report's errors.
|
||||
void
|
||||
expectError(
|
||||
json::Value const& doc,
|
||||
std::string const& error,
|
||||
std::optional<UnsignedList> const& roster = std::nullopt,
|
||||
std::optional<PublicKey> const& key = std::nullopt,
|
||||
std::optional<std::uint32_t> at = std::nullopt) const
|
||||
{
|
||||
auto const report = verifyList(doc, roster, key, at.value_or(now_));
|
||||
EXPECT_FALSE(report["ok"].asBool());
|
||||
bool found = false;
|
||||
for (auto const& e : report["errors"])
|
||||
found = found || e.asString() == error;
|
||||
EXPECT_TRUE(found) << to_string(report);
|
||||
}
|
||||
|
||||
void
|
||||
expectOk(json::Value const& doc) const
|
||||
{
|
||||
auto const report = verifyList(doc, std::nullopt, std::nullopt, now_);
|
||||
EXPECT_TRUE(report["ok"].asBool()) << to_string(report);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(ListSigningTest, canonical_json)
|
||||
{
|
||||
// Whitespace and comments outside strings go, one space follows each comma
|
||||
// and colon, key order and string contents stay.
|
||||
EXPECT_EQ(
|
||||
canonicalJson("{ \"b\" :1,\n\t\"a\": [ 1 , 2 ] , \"s\":\"x, y: z\" }"),
|
||||
"{\"b\": 1, \"a\": [1, 2], \"s\": \"x, y: z\"}");
|
||||
EXPECT_EQ(canonicalJson("{\"e\": \"a\\\"b\"}"), "{\"e\": \"a\\\"b\"}");
|
||||
EXPECT_EQ(
|
||||
canonicalJson("{\"sequence\": 1, // reviewed\n \"x\": /* two */ 2}"),
|
||||
"{\"sequence\": 1, \"x\": 2}");
|
||||
EXPECT_EQ(canonicalJson("{\"u\": \"http://x\"}"), "{\"u\": \"http://x\"}");
|
||||
|
||||
EXPECT_EQ(errorOf([] { canonicalJson("[1, 2]"); }), kNotObject);
|
||||
EXPECT_EQ(errorOf([] { canonicalJson("{\"a\": "); }), kNotObject);
|
||||
EXPECT_EQ(errorOf([] { canonicalJson("{\"a\": 1} // open"); }), "");
|
||||
EXPECT_EQ(errorOf([] { canonicalJson("{\"a\": 1 /* open"); }), kNotObject);
|
||||
}
|
||||
|
||||
TEST_F(ListSigningTest, parse_unsigned_list)
|
||||
{
|
||||
{
|
||||
auto const l = parseUnsignedList(unsignedListText(validators_, 5, 1000, 500));
|
||||
EXPECT_EQ(l.sequence, 5u);
|
||||
EXPECT_EQ(l.expiration, 1000u);
|
||||
EXPECT_EQ(l.effective, 500u);
|
||||
EXPECT_EQ(l.validators.size(), 3u);
|
||||
EXPECT_EQ(
|
||||
l.validators[0],
|
||||
required(deserializeManifest(base64Decode(validators_[0].manifest))).masterKey);
|
||||
}
|
||||
{
|
||||
auto const l = parseUnsignedList(unsignedListText(validators_, 5, 1000));
|
||||
EXPECT_FALSE(l.effective);
|
||||
EXPECT_TRUE(l.canonical.starts_with("{\"sequence\": 5, \"expiration\": 1000, "));
|
||||
}
|
||||
|
||||
std::string const sequenceError = "\"sequence\" must be an integer from 1 to 2147483647";
|
||||
std::string const expirationError = "\"expiration\" must be an integer from 1 to 2147483647";
|
||||
EXPECT_EQ(errorOfParse("{\"expiration\": 1, \"validators\": []}"), sequenceError);
|
||||
EXPECT_EQ(
|
||||
errorOfParse("{\"sequence\": 0, \"expiration\": 1, \"validators\": []}"), sequenceError);
|
||||
EXPECT_EQ(
|
||||
errorOfParse("{\"sequence\": true, \"expiration\": 1, \"validators\": []}"), sequenceError);
|
||||
EXPECT_EQ(
|
||||
errorOfParse("{\"sequence\": 2147483648, \"expiration\": 1, \"validators\": []}"),
|
||||
sequenceError);
|
||||
EXPECT_EQ(errorOfParse("{\"sequence\": 1, \"validators\": []}"), expirationError);
|
||||
EXPECT_EQ(
|
||||
errorOfParse("{\"sequence\": 1, \"expiration\": -1, \"validators\": []}"), expirationError);
|
||||
EXPECT_EQ(
|
||||
errorOfParse("{\"sequence\": 1, \"expiration\": 0, \"validators\": []}"), expirationError);
|
||||
{
|
||||
auto twice = validators_;
|
||||
twice.push_back(validators_[0]);
|
||||
auto const key =
|
||||
strHex(required(deserializeManifest(base64Decode(validators_[0].manifest))).masterKey);
|
||||
EXPECT_EQ(
|
||||
errorOfParse(unsignedListText(twice, 1, 1000)),
|
||||
"\"validators\" lists " + key + " more than once");
|
||||
}
|
||||
EXPECT_EQ(
|
||||
errorOfParse(
|
||||
"{\"sequence\": 1, \"effective\": 1000, \"expiration\": 1000, \"validators\": []}"),
|
||||
"\"effective\" must be earlier than \"expiration\"");
|
||||
EXPECT_EQ(
|
||||
errorOfParse(
|
||||
"{\"sequence\": 1, \"effective\": \"x\", \"expiration\": 1000, \"validators\": []}"),
|
||||
"\"effective\" must be an integer from 0 to 2147483647");
|
||||
EXPECT_EQ(
|
||||
errorOfParse("{\"sequence\": 1, \"expiration\": 1000, \"validators\": []}"),
|
||||
"\"validators\" must be a non-empty array");
|
||||
EXPECT_EQ(
|
||||
errorOfParse("{\"sequence\": 1, \"expiration\": 1000, \"validators\": [{}]}"),
|
||||
"every validator needs a \"validation_public_key\" string");
|
||||
for (auto const* bad :
|
||||
{"zz", "ED00", "FF00000000000000000000000000000000000000000000000000000000000000"})
|
||||
{
|
||||
EXPECT_EQ(
|
||||
errorOfParse(
|
||||
std::string(
|
||||
"{\"sequence\": 1, \"expiration\": 1000, \"validators\": "
|
||||
"[{\"validation_public_key\": \"") +
|
||||
bad + "\"}]}"),
|
||||
std::string("\"validation_public_key\" is not a hex public key: ") + bad);
|
||||
}
|
||||
{
|
||||
auto const key =
|
||||
strHex(required(deserializeManifest(base64Decode(validators_[0].manifest))).masterKey);
|
||||
auto const other =
|
||||
strHex(required(deserializeManifest(base64Decode(validators_[1].manifest))).masterKey);
|
||||
auto const entry = [](std::string const& key, std::string const& manifest) {
|
||||
return "{\"sequence\": 1, \"expiration\": 1000, \"validators\": "
|
||||
"[{\"validation_public_key\": \"" +
|
||||
key + R"(", "manifest": )" + manifest + "}]}";
|
||||
};
|
||||
EXPECT_EQ(
|
||||
errorOfParse(entry(other, "\"" + validators_[0].manifest + "\"")),
|
||||
"\"manifest\" belongs to another key than " + other);
|
||||
EXPECT_EQ(errorOfParse(entry(key, "\"AAAA\"")), "\"manifest\" does not verify for " + key);
|
||||
EXPECT_EQ(errorOfParse(entry(key, "5")), "\"manifest\" must be a base64 string for " + key);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ListSigningTest, files)
|
||||
{
|
||||
TempDir const dir;
|
||||
|
||||
// A token file as create_token writes it: header, comment, 72-character lines
|
||||
auto const tokenFile = std::filesystem::path(dir.file("token.txt"));
|
||||
{
|
||||
std::string text = "# validator public key: " +
|
||||
toBase58(TokenType::NodePublic, publisher_.keys.publicKey()) +
|
||||
"\n\n[validator_token]\n";
|
||||
auto const body = tokenToBase64(publisher_.token);
|
||||
for (std::size_t i = 0; i < body.size(); i += 72)
|
||||
text += body.substr(i, 72) + "\n";
|
||||
writeFile(tokenFile, text);
|
||||
}
|
||||
auto const token = loadTokenFile(tokenFile);
|
||||
EXPECT_EQ(token.manifest, publisher_.token.manifest);
|
||||
EXPECT_TRUE(sameSecret(token.validationSecret, publisher_.token.validationSecret));
|
||||
|
||||
auto const manifestFile = std::filesystem::path(dir.file("manifest.txt"));
|
||||
writeFile(manifestFile, "# publisher manifest\n" + publisher_.token.manifest + "\n");
|
||||
auto const manifest = loadManifestFile(manifestFile);
|
||||
EXPECT_EQ(manifest.masterKey, publisher_.manifest.masterKey);
|
||||
EXPECT_EQ(manifest.signingKey, publisher_.manifest.signingKey);
|
||||
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { loadTokenFile(manifestFile); }),
|
||||
"Not a validator token: " + manifestFile.string());
|
||||
auto const bad = std::filesystem::path(dir.file("bad-manifest.txt"));
|
||||
writeFile(bad, "AAAA\n");
|
||||
EXPECT_EQ(errorOf([&] { loadManifestFile(bad); }), "Not a valid manifest: " + bad.string());
|
||||
auto const missing = std::filesystem::path(dir.file("missing.txt"));
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { loadManifestFile(missing); }), "Failed to open file: " + missing.string());
|
||||
|
||||
auto const listFile = std::filesystem::path(dir.file("unsigned.json"));
|
||||
writeFile(listFile, unsignedListText(validators_, 3, 5000));
|
||||
EXPECT_EQ(loadUnsignedList(listFile).sequence, 3u);
|
||||
}
|
||||
|
||||
TEST_F(ListSigningTest, sign_and_verify_version_1)
|
||||
{
|
||||
auto const v1 = signed1();
|
||||
EXPECT_EQ(v1[jss::version].asUInt(), 1u);
|
||||
EXPECT_EQ(v1[jss::public_key].asString(), strHex(publisher_.manifest.masterKey));
|
||||
EXPECT_EQ(v1[jss::manifest].asString(), publisher_.token.manifest);
|
||||
EXPECT_EQ(base64Decode(v1[jss::blob].asString()), list_.canonical);
|
||||
EXPECT_EQ(v1[jss::signature].asString(), signature_);
|
||||
|
||||
auto const report = verifyList(v1, list_, publisher_.manifest.masterKey, now_);
|
||||
EXPECT_TRUE(report["ok"].asBool()) << to_string(report);
|
||||
EXPECT_EQ(report["blobs"].size(), 1u);
|
||||
EXPECT_EQ(report["blobs"][0u][jss::sequence].asUInt(), 7u);
|
||||
EXPECT_EQ(report["blobs"][0u][jss::validators].asUInt(), 3u);
|
||||
EXPECT_FALSE(report["blobs"][0u]["expired"].asBool());
|
||||
EXPECT_EQ(report["manifest_sequence"].asUInt(), 1u);
|
||||
EXPECT_EQ(report["signing_key"].asString(), strHex(publisher_.signingKey));
|
||||
|
||||
EXPECT_EQ(
|
||||
errorOf([&] {
|
||||
makeSignedList(
|
||||
publisher_.token.manifest,
|
||||
publisher_.manifest.masterKey,
|
||||
list_,
|
||||
signature_,
|
||||
3,
|
||||
std::nullopt,
|
||||
{});
|
||||
}),
|
||||
"Unsupported list version");
|
||||
}
|
||||
|
||||
TEST_F(ListSigningTest, sign_and_verify_version_2)
|
||||
{
|
||||
auto const v2 = signed2();
|
||||
EXPECT_EQ(v2[jss::version].asUInt(), 2u);
|
||||
EXPECT_EQ(v2[jss::blobs_v2].size(), 1u);
|
||||
EXPECT_FALSE(v2.isMember(jss::blob));
|
||||
expectOk(v2);
|
||||
|
||||
// A second blob appended under the same manifest
|
||||
auto const later = parseUnsignedList(unsignedListText(validators_, 8, now_ + 300, now_ + 200));
|
||||
auto const laterSig = signList(later, publisher_.signingKey, publisher_.token.validationSecret);
|
||||
auto const v2b = makeSignedList(
|
||||
publisher_.token.manifest, publisher_.manifest.masterKey, later, laterSig, 2, v2, {});
|
||||
EXPECT_EQ(v2b[jss::blobs_v2].size(), 2u);
|
||||
EXPECT_EQ(v2b[jss::blobs_v2][0u][jss::signature].asString(), signature_);
|
||||
{
|
||||
auto const report = verifyList(v2b, std::nullopt, std::nullopt, now_);
|
||||
EXPECT_TRUE(report["ok"].asBool()) << to_string(report);
|
||||
EXPECT_EQ(report["blobs"][1u][jss::effective].asUInt(), now_ + 200);
|
||||
}
|
||||
|
||||
// After the signing key rotates the earlier blobs are signed again with the
|
||||
// new key, because a server verifies every blob under the newest manifest.
|
||||
SigningKeys rotated = publisher_.keys;
|
||||
auto const token2 = rotated.createToken(KeyType::Ed25519);
|
||||
auto const manifest2 = required(deserializeManifest(base64Decode(token2.manifest)));
|
||||
auto const signingKey2 = required(manifest2.signingKey);
|
||||
auto const resign = [&](std::string const& bytes) {
|
||||
return strHex(sign(signingKey2, token2.validationSecret, makeSlice(bytes)));
|
||||
};
|
||||
auto const third = parseUnsignedList(unsignedListText(validators_, 9, now_ + 400, now_ + 350));
|
||||
EXPECT_EQ(
|
||||
errorOf([&] {
|
||||
makeSignedList(
|
||||
token2.manifest, manifest2.masterKey, third, resign(third.canonical), 2, v2b, {});
|
||||
}),
|
||||
"The list to append to was signed under another manifest and its blobs need signing "
|
||||
"again");
|
||||
auto const v2c = makeSignedList(
|
||||
token2.manifest, manifest2.masterKey, third, resign(third.canonical), 2, v2b, resign);
|
||||
EXPECT_EQ(v2c[jss::blobs_v2].size(), 3u);
|
||||
EXPECT_EQ(v2c[jss::manifest].asString(), token2.manifest);
|
||||
EXPECT_NE(v2c[jss::blobs_v2][0u][jss::signature].asString(), signature_);
|
||||
EXPECT_EQ(
|
||||
v2c[jss::blobs_v2][0u][jss::blob].asString(), v2b[jss::blobs_v2][0u][jss::blob].asString());
|
||||
EXPECT_FALSE(v2c[jss::blobs_v2][0u].isMember(jss::manifest));
|
||||
{
|
||||
auto const report = verifyList(v2c, std::nullopt, std::nullopt, now_);
|
||||
EXPECT_TRUE(report["ok"].asBool()) << to_string(report);
|
||||
EXPECT_EQ(report["manifest_sequence"].asUInt(), 2u);
|
||||
EXPECT_EQ(report["signing_key"].asString(), strHex(signingKey2));
|
||||
}
|
||||
|
||||
// Append refuses the wrong shape, another publisher, a broken blob and a full list
|
||||
EXPECT_EQ(
|
||||
errorOf([&] {
|
||||
makeSignedList(
|
||||
publisher_.token.manifest,
|
||||
publisher_.manifest.masterKey,
|
||||
list_,
|
||||
signature_,
|
||||
1,
|
||||
v2,
|
||||
{});
|
||||
}),
|
||||
"A version 1 list holds one blob; use version 2 to append");
|
||||
EXPECT_EQ(
|
||||
errorOf([&] {
|
||||
makeSignedList(
|
||||
publisher_.token.manifest,
|
||||
publisher_.manifest.masterKey,
|
||||
list_,
|
||||
signature_,
|
||||
2,
|
||||
signed1(),
|
||||
{});
|
||||
}),
|
||||
"The list to append to is not a version 2 list");
|
||||
{
|
||||
Publisher const other;
|
||||
EXPECT_EQ(
|
||||
errorOf([&] {
|
||||
makeSignedList(
|
||||
other.token.manifest, other.manifest.masterKey, list_, signature_, 2, v2, {});
|
||||
}),
|
||||
"The list to append to belongs to another master key");
|
||||
}
|
||||
{
|
||||
auto broken = v2;
|
||||
broken[jss::blobs_v2][0u][jss::blob] = 5;
|
||||
EXPECT_EQ(
|
||||
errorOf([&] {
|
||||
makeSignedList(
|
||||
token2.manifest, manifest2.masterKey, third, "00", 2, broken, resign);
|
||||
}),
|
||||
"The list to append to holds an invalid blob");
|
||||
}
|
||||
{
|
||||
auto full = v2;
|
||||
while (full[jss::blobs_v2].size() < 5)
|
||||
full[jss::blobs_v2].append(full[jss::blobs_v2][0u]);
|
||||
EXPECT_EQ(
|
||||
errorOf([&] {
|
||||
makeSignedList(
|
||||
publisher_.token.manifest,
|
||||
publisher_.manifest.masterKey,
|
||||
list_,
|
||||
signature_,
|
||||
2,
|
||||
full,
|
||||
{});
|
||||
}),
|
||||
"The list to append to already holds 5 blobs");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ListSigningTest, verify_rejects)
|
||||
{
|
||||
auto const good = signed1();
|
||||
|
||||
{
|
||||
auto tampered = good;
|
||||
auto text = list_.canonical;
|
||||
text.replace(text.find("\"sequence\": 7"), 13, "\"sequence\": 9");
|
||||
tampered[jss::blob] = base64Encode(text);
|
||||
expectError(tampered, "blob 0: the signature does not verify under the signing key");
|
||||
}
|
||||
expectError(good, "blob 0: expired", std::nullopt, std::nullopt, now_ + 100);
|
||||
{
|
||||
Publisher const other;
|
||||
auto wrong = good;
|
||||
wrong[jss::manifest] = other.token.manifest;
|
||||
expectError(wrong, "\"public_key\" is not the manifest's master key");
|
||||
expectError(
|
||||
good, "the master key is not the expected key", std::nullopt, other.manifest.masterKey);
|
||||
}
|
||||
{
|
||||
auto const roster = parseUnsignedList(unsignedListText(makeValidators(2), 1, now_ + 100));
|
||||
expectError(good, "blob 0: the validators differ from the expected list", roster);
|
||||
}
|
||||
expectError(json::Value(json::ValueType::Array), "the list is not a JSON object");
|
||||
for (auto const version : {0, 3})
|
||||
{
|
||||
auto bad = good;
|
||||
bad[jss::version] = version;
|
||||
expectError(bad, "\"version\" must be 1 or 2");
|
||||
}
|
||||
{
|
||||
auto bad = good;
|
||||
bad[jss::public_key] = 1;
|
||||
expectError(bad, R"("public_key" and "manifest" must be strings)");
|
||||
}
|
||||
{
|
||||
auto bad = good;
|
||||
bad[jss::manifest] = "AAAA";
|
||||
expectError(bad, "\"manifest\" does not deserialize and verify");
|
||||
}
|
||||
{
|
||||
SigningKeys revoked(KeyType::Ed25519);
|
||||
auto bad = good;
|
||||
bad[jss::manifest] = revoked.revoke();
|
||||
bad[jss::public_key] = strHex(revoked.publicKey());
|
||||
expectError(bad, "the publisher's master key is revoked");
|
||||
}
|
||||
{
|
||||
auto bad = good;
|
||||
bad[jss::blob] = base64Encode("{}");
|
||||
expectError(bad, "blob 0: \"sequence\" must be an integer from 1 to 2147483647");
|
||||
}
|
||||
{
|
||||
auto bad = good;
|
||||
bad[jss::blobs_v2] = json::Value(json::ValueType::Array);
|
||||
expectError(bad, R"(a version 1 list needs "blob" and "signature" and no "blobs_v2")");
|
||||
}
|
||||
|
||||
auto const v2 = signed2();
|
||||
std::string const v2Shape =
|
||||
R"(a version 2 list needs 1 to 5 "blobs_v2" entries and no top-level "blob")";
|
||||
{
|
||||
auto bad = v2;
|
||||
bad[jss::blobs_v2] = json::Value(json::ValueType::Array);
|
||||
expectError(bad, v2Shape);
|
||||
bad = v2;
|
||||
bad[jss::blob] = "x";
|
||||
expectError(bad, v2Shape);
|
||||
}
|
||||
std::string const entryShape =
|
||||
"every \"blobs_v2\" entry needs \"blob\" and \"signature\" strings and an optional "
|
||||
"\"manifest\" string";
|
||||
{
|
||||
auto bad = v2;
|
||||
bad[jss::blobs_v2][0u].removeMember(jss::signature);
|
||||
expectError(bad, entryShape);
|
||||
bad = v2;
|
||||
bad[jss::blobs_v2][0u][jss::manifest] = 5;
|
||||
expectError(bad, entryShape);
|
||||
}
|
||||
{
|
||||
Publisher const other;
|
||||
auto bad = v2;
|
||||
bad[jss::blobs_v2][0u][jss::manifest] = other.token.manifest;
|
||||
expectError(bad, "blob 0: its \"manifest\" is not this publisher's");
|
||||
|
||||
SigningKeys revoked = publisher_.keys;
|
||||
bad[jss::blobs_v2][0u][jss::manifest] = revoked.revoke();
|
||||
expectError(bad, "blob 0: its \"manifest\" revokes the publisher's master key");
|
||||
|
||||
// An entry carrying the publisher's own manifest changes nothing
|
||||
auto fine = v2;
|
||||
fine[jss::blobs_v2][0u][jss::manifest] = publisher_.token.manifest;
|
||||
expectOk(fine);
|
||||
}
|
||||
{
|
||||
// An entry with a newer manifest moves the signing key for that blob and
|
||||
// the ones after it, as a server would.
|
||||
SigningKeys rotated = publisher_.keys;
|
||||
auto const token2 = rotated.createToken(KeyType::Ed25519);
|
||||
auto const manifest2 = required(deserializeManifest(base64Decode(token2.manifest)));
|
||||
auto const later = parseUnsignedList(unsignedListText(validators_, 8, now_ + 300));
|
||||
json::Value entry(json::ValueType::Object);
|
||||
entry[jss::blob] = base64Encode(later.canonical);
|
||||
entry[jss::signature] =
|
||||
signList(later, required(manifest2.signingKey), token2.validationSecret);
|
||||
entry[jss::manifest] = token2.manifest;
|
||||
auto newer = v2;
|
||||
newer[jss::blobs_v2].append(entry);
|
||||
auto const report = verifyList(newer, std::nullopt, std::nullopt, now_);
|
||||
EXPECT_TRUE(report["ok"].asBool()) << to_string(report);
|
||||
EXPECT_EQ(report["manifest_sequence"].asUInt(), 2u);
|
||||
|
||||
// Placed first, it invalidates the blob signed under the older key
|
||||
auto reordered = v2;
|
||||
reordered[jss::blobs_v2] = json::Value(json::ValueType::Array);
|
||||
reordered[jss::blobs_v2].append(entry);
|
||||
reordered[jss::blobs_v2].append(v2[jss::blobs_v2][0u]);
|
||||
expectError(reordered, "blob 1: the signature does not verify under the signing key");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl::tools::test
|
||||
533
src/tests/tools/validator-keys/SigningKeys.cpp
Normal file
533
src/tests/tools/validator-keys/SigningKeys.cpp
Normal file
@@ -0,0 +1,533 @@
|
||||
#include <tools/validator-keys/SigningKeys.h>
|
||||
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <Fixtures.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::tools::test {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<KeyType, 2> kKeyTypes{{KeyType::Ed25519, KeyType::Secp256k1}};
|
||||
constexpr std::uint32_t kMaxSequence = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
std::string const kBadManifest = "Manifest is not properly signed";
|
||||
std::string const kRevoked = "Validator keys have been revoked.";
|
||||
std::string const kExhausted =
|
||||
"Maximum number of tokens have already been generated.\n"
|
||||
"Revoke validator keys if previous token has been compromised.";
|
||||
|
||||
class SigningKeysTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
TempDir dir_;
|
||||
std::filesystem::path keyFile_{dir_.file("validator_keys.json")};
|
||||
|
||||
void
|
||||
writeKeyFile(json::Value const& jv)
|
||||
{
|
||||
writeFile(keyFile_, jv.toStyledString());
|
||||
}
|
||||
|
||||
std::string
|
||||
loadError(json::Value const& jv)
|
||||
{
|
||||
writeKeyFile(jv);
|
||||
return errorOf([&] { SigningKeys::makeSigningKeys(keyFile_); });
|
||||
}
|
||||
|
||||
json::Value
|
||||
keyFileJson()
|
||||
{
|
||||
json::Reader reader;
|
||||
json::Value jv;
|
||||
reader.parse(readFile(keyFile_), jv);
|
||||
return jv;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string
|
||||
invalidField(std::string const& field) const
|
||||
{
|
||||
return "Key file '" + keyFile_.string() + "' contains invalid \"" + field + "\" field";
|
||||
}
|
||||
|
||||
static json::Value
|
||||
baseKeyFile(SecretKey const& secret)
|
||||
{
|
||||
json::Value jv;
|
||||
jv["key_type"] = "ed25519";
|
||||
jv["secret_key"] = toBase58(TokenType::NodePrivate, secret);
|
||||
jv["token_sequence"] = 1;
|
||||
jv["revoked"] = false;
|
||||
return jv;
|
||||
}
|
||||
};
|
||||
|
||||
// The manifest of a token or a revocation, parsed and checked against the
|
||||
// master key that made it.
|
||||
Manifest
|
||||
manifestOf(std::string const& base64, SigningKeys const& keys)
|
||||
{
|
||||
auto m = required(deserializeManifest(base64Decode(base64)));
|
||||
EXPECT_TRUE(m.verify());
|
||||
EXPECT_EQ(m.masterKey, keys.publicKey());
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(SigningKeysTest, key_file_round_trip)
|
||||
{
|
||||
for (auto const keyType : kKeyTypes)
|
||||
{
|
||||
SigningKeys const keys(keyType);
|
||||
keys.writeToFile(keyFile_);
|
||||
EXPECT_TRUE(std::filesystem::exists(keyFile_));
|
||||
EXPECT_FALSE(std::filesystem::exists(keyFile_.string() + ".tmp"));
|
||||
auto const perms = std::filesystem::status(keyFile_).permissions();
|
||||
EXPECT_EQ(
|
||||
perms & (std::filesystem::perms::group_all | std::filesystem::perms::others_all),
|
||||
std::filesystem::perms::none);
|
||||
EXPECT_TRUE(keys == SigningKeys::makeSigningKeys(keyFile_));
|
||||
}
|
||||
|
||||
// A token, a domain and a pending external token all survive the round trip
|
||||
SigningKeys keys(KeyType::Ed25519);
|
||||
keys.domain("validator.example.com");
|
||||
keys.createToken();
|
||||
SigningKeys const signer(KeyType::Ed25519);
|
||||
keys.startToken(KeyType::Ed25519, signer.publicKey());
|
||||
keys.writeToFile(keyFile_);
|
||||
EXPECT_TRUE(keys == SigningKeys::makeSigningKeys(keyFile_));
|
||||
|
||||
// So does a pending token with a generated signing key
|
||||
SigningKeys other(KeyType::Secp256k1);
|
||||
SigningKeys const before = other;
|
||||
other.startToken(KeyType::Secp256k1);
|
||||
other.writeToFile(keyFile_);
|
||||
EXPECT_TRUE(other == SigningKeys::makeSigningKeys(keyFile_));
|
||||
EXPECT_FALSE(other == keys);
|
||||
EXPECT_FALSE(other == before);
|
||||
|
||||
// The same master key with a pending token of the other kind
|
||||
SigningKeys external = before;
|
||||
external.startToken(KeyType::Secp256k1, signer.publicKey());
|
||||
EXPECT_FALSE(other == external);
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, write_to_file_errors)
|
||||
{
|
||||
SigningKeys const keys(KeyType::Ed25519);
|
||||
|
||||
auto const nested = dir_.file("a/b/c/validator_keys.json");
|
||||
keys.writeToFile(nested);
|
||||
EXPECT_TRUE(keys == SigningKeys::makeSigningKeys(nested));
|
||||
EXPECT_EQ(
|
||||
std::filesystem::status(std::filesystem::path(nested).parent_path()).permissions() &
|
||||
(std::filesystem::perms::group_all | std::filesystem::perms::others_all),
|
||||
std::filesystem::perms::none);
|
||||
|
||||
// The parent path is a file
|
||||
auto const blocked = std::filesystem::path(keyFile_.string() + "/keys.json");
|
||||
keys.writeToFile(keyFile_);
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.writeToFile(blocked); }),
|
||||
"Cannot create directory: " + blocked.parent_path().string());
|
||||
|
||||
// The target is a directory
|
||||
auto const directory = std::filesystem::path(dir_.file("dir"));
|
||||
std::filesystem::create_directory(directory);
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.writeToFile(directory); }),
|
||||
"Cannot write key file: " + directory.string());
|
||||
|
||||
// The directory cannot be written to
|
||||
auto const sealed = std::filesystem::path(dir_.file("sealed"));
|
||||
std::filesystem::create_directory(sealed);
|
||||
std::filesystem::permissions(
|
||||
sealed, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec);
|
||||
auto const inSealed = sealed / "keys.json";
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.writeToFile(inSealed); }),
|
||||
"Cannot write key file: " + inSealed.string());
|
||||
std::filesystem::permissions(sealed, std::filesystem::perms::owner_all);
|
||||
|
||||
// The temporary beside the key file is a symlink
|
||||
auto const linked = std::filesystem::path(dir_.file("linked.json"));
|
||||
std::filesystem::create_symlink(dir_.file("elsewhere.json"), linked.string() + ".tmp");
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.writeToFile(linked); }),
|
||||
"Refusing to write through a symlink: " + linked.string() + ".tmp");
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, key_file_fields)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { SigningKeys::makeSigningKeys(keyFile_); }),
|
||||
"Failed to open key file: " + keyFile_.string());
|
||||
|
||||
writeFile(keyFile_, "{{}");
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { SigningKeys::makeSigningKeys(keyFile_); }),
|
||||
"Unable to parse json key file: " + keyFile_.string());
|
||||
|
||||
json::Value jv;
|
||||
jv["dummy"] = "field";
|
||||
for (auto const* field : {"key_type", "secret_key", "token_sequence", "revoked"})
|
||||
{
|
||||
EXPECT_EQ(
|
||||
loadError(jv),
|
||||
"Key file '" + keyFile_.string() + "' is missing \"" + field + "\" field");
|
||||
jv[field] = "dummy";
|
||||
}
|
||||
EXPECT_EQ(loadError(jv), invalidField("key_type"));
|
||||
|
||||
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
|
||||
jv["key_type"] = "ed25519";
|
||||
EXPECT_EQ(loadError(jv), invalidField("token_sequence"));
|
||||
jv["token_sequence"] = -1;
|
||||
EXPECT_EQ(loadError(jv), invalidField("token_sequence"));
|
||||
jv["token_sequence"] = true;
|
||||
EXPECT_EQ(loadError(jv), invalidField("token_sequence"));
|
||||
jv["token_sequence"] = json::UInt(kMaxSequence);
|
||||
EXPECT_EQ(loadError(jv), invalidField("revoked"));
|
||||
jv["revoked"] = false;
|
||||
EXPECT_EQ(loadError(jv), invalidField("secret_key"));
|
||||
jv["secret_key"] = toBase58(TokenType::NodePrivate, kp.second);
|
||||
EXPECT_EQ(loadError(jv), "");
|
||||
|
||||
// Optional fields with the wrong type or value
|
||||
for (auto const* field :
|
||||
{"domain", "manifest", "pending_token_secret", "pending_signing_key", "pending_key_type"})
|
||||
{
|
||||
auto bad = baseKeyFile(kp.second);
|
||||
bad[field] = 1;
|
||||
if (field == std::string("pending_key_type"))
|
||||
{
|
||||
bad["pending_token_secret"] = toBase58(TokenType::NodePrivate, kp.second);
|
||||
}
|
||||
else if (std::string(field).starts_with("pending_"))
|
||||
{
|
||||
bad["pending_key_type"] = "ed25519";
|
||||
}
|
||||
EXPECT_EQ(loadError(bad), invalidField(field)) << field;
|
||||
}
|
||||
for (auto const* field : {"manifest", "pending_token_secret", "pending_signing_key"})
|
||||
{
|
||||
auto bad = baseKeyFile(kp.second);
|
||||
bad[field] = "not valid";
|
||||
bad["pending_key_type"] = "ed25519";
|
||||
EXPECT_EQ(loadError(bad), invalidField(field)) << field;
|
||||
}
|
||||
{
|
||||
auto bad = baseKeyFile(kp.second);
|
||||
bad["manifest"] = "";
|
||||
EXPECT_EQ(loadError(bad), invalidField("manifest"));
|
||||
}
|
||||
{
|
||||
auto bad = baseKeyFile(kp.second);
|
||||
bad["domain"] = "-bad.example";
|
||||
EXPECT_EQ(
|
||||
loadError(bad), "The domain field must use the '[host.][subdomain.]domain.tld' format");
|
||||
}
|
||||
{
|
||||
// Pending fields need a key type and exclude each other
|
||||
auto bad = baseKeyFile(kp.second);
|
||||
bad["pending_token_secret"] = toBase58(TokenType::NodePrivate, kp.second);
|
||||
EXPECT_EQ(
|
||||
loadError(bad),
|
||||
"Key file '" + keyFile_.string() + "' is missing \"pending_key_type\" field");
|
||||
bad["pending_key_type"] = "dummy";
|
||||
EXPECT_EQ(loadError(bad), invalidField("pending_key_type"));
|
||||
bad["pending_key_type"] = "ed25519";
|
||||
bad["pending_signing_key"] = toBase58(TokenType::NodePublic, kp.first);
|
||||
EXPECT_EQ(
|
||||
loadError(bad),
|
||||
"Key file '" + keyFile_.string() +
|
||||
"' has both \"pending_token_secret\" and \"pending_signing_key\"");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, external_key_file_fields)
|
||||
{
|
||||
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
|
||||
json::Value jv;
|
||||
jv["key_type"] = "ed25519";
|
||||
jv["secret_key"] = "external";
|
||||
jv["token_sequence"] = 0;
|
||||
jv["revoked"] = false;
|
||||
EXPECT_EQ(
|
||||
loadError(jv), "Key file '" + keyFile_.string() + "' is missing \"public_key\" field");
|
||||
jv["public_key"] = "dummy public";
|
||||
EXPECT_EQ(loadError(jv), invalidField("public_key"));
|
||||
jv["public_key"] = toBase58(TokenType::NodePublic, kp.first);
|
||||
jv["key_type"] = "secp256k1";
|
||||
EXPECT_EQ(
|
||||
loadError(jv),
|
||||
"Key file '" + keyFile_.string() +
|
||||
"' has a \"key_type\" that does not match \"public_key\"");
|
||||
jv["key_type"] = "ed25519";
|
||||
EXPECT_EQ(loadError(jv), "");
|
||||
|
||||
auto const keys = SigningKeys::makeSigningKeys(keyFile_);
|
||||
EXPECT_FALSE(keys.hasSecret());
|
||||
EXPECT_EQ(keys.publicKey(), kp.first);
|
||||
EXPECT_TRUE(keys == SigningKeys(KeyType::Ed25519, kp.first));
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, create_token)
|
||||
{
|
||||
for (auto const keyType : kKeyTypes)
|
||||
{
|
||||
SigningKeys keys(keyType);
|
||||
std::uint32_t sequence = 0;
|
||||
for (auto const tokenKeyType : kKeyTypes)
|
||||
{
|
||||
auto const token = keys.createToken(tokenKeyType);
|
||||
auto const m = manifestOf(token.manifest, keys);
|
||||
EXPECT_EQ(m.sequence, ++sequence);
|
||||
EXPECT_EQ(keys.sequence(), sequence);
|
||||
ASSERT_TRUE(m.signingKey);
|
||||
EXPECT_EQ(
|
||||
required(m.signingKey), derivePublicKey(tokenKeyType, token.validationSecret));
|
||||
EXPECT_EQ(base64Encode(keys.manifest().data(), keys.manifest().size()), token.manifest);
|
||||
}
|
||||
}
|
||||
|
||||
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
|
||||
{
|
||||
SigningKeys keys(KeyType::Ed25519, kp.second, kMaxSequence - 1);
|
||||
EXPECT_EQ(errorOf([&] { keys.createToken(); }), kExhausted);
|
||||
}
|
||||
{
|
||||
// A key migrated from a publisher whose manifests carried the list
|
||||
// sequence continues from that sequence.
|
||||
SigningKeys keys(KeyType::Ed25519, kp.second, 2026091301);
|
||||
auto const m = manifestOf(keys.createToken(KeyType::Ed25519).manifest, keys);
|
||||
EXPECT_EQ(m.sequence, 2026091302u);
|
||||
}
|
||||
{
|
||||
SigningKeys keys(KeyType::Ed25519);
|
||||
keys.revoke();
|
||||
EXPECT_EQ(errorOf([&] { keys.createToken(); }), kRevoked);
|
||||
}
|
||||
{
|
||||
SigningKeys keys(KeyType::Ed25519, kp.first);
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.createToken(); }), "This key file cannot be used to sign tokens.");
|
||||
EXPECT_EQ(errorOf([&] { keys.revoke(); }), "This key file cannot be used to sign tokens.");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, token_with_domain)
|
||||
{
|
||||
SigningKeys keys(KeyType::Ed25519);
|
||||
keys.domain("validator.example.com");
|
||||
auto const m = manifestOf(keys.createToken().manifest, keys);
|
||||
EXPECT_EQ(m.domain, "validator.example.com");
|
||||
EXPECT_EQ(
|
||||
keys.attestationData(),
|
||||
"[domain-attestation-blob:validator.example.com:" +
|
||||
toBase58(TokenType::NodePublic, keys.publicKey()) + "]");
|
||||
|
||||
for (auto const* bad : {"a.b", "-bad.example", "nodots"})
|
||||
{
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.domain(bad); }),
|
||||
"The domain field must use the '[host.][subdomain.]domain.tld' format")
|
||||
<< bad;
|
||||
}
|
||||
keys.domain("");
|
||||
EXPECT_TRUE(keys.domain().empty());
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, revoke)
|
||||
{
|
||||
for (auto const keyType : kKeyTypes)
|
||||
{
|
||||
SigningKeys keys(keyType);
|
||||
auto const m = manifestOf(keys.revoke(), keys);
|
||||
EXPECT_TRUE(m.revoked());
|
||||
EXPECT_FALSE(m.signingKey);
|
||||
EXPECT_TRUE(keys.revoked());
|
||||
// Revoking again is allowed
|
||||
manifestOf(keys.revoke(), keys);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, sign)
|
||||
{
|
||||
std::map<KeyType, std::string> const expected{
|
||||
{KeyType::Ed25519,
|
||||
"2EE541D6825791BF5454C571D2B363EAB3F01C73159B1F"
|
||||
"237AC6D38663A82B9D5EAD262D5F776B916E68247A1F082090F3BAE7ABC939"
|
||||
"C8F29B0DC759FD712300"},
|
||||
{KeyType::Secp256k1,
|
||||
"3045022100F142C27BF83D8D4541C7A4E759DE64A672"
|
||||
"51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5"
|
||||
"55438D64A229CD0B4BA2AE33402443B20409"}};
|
||||
|
||||
std::string const data = "data to sign";
|
||||
for (auto const keyType : kKeyTypes)
|
||||
{
|
||||
auto const sk = generateSecretKey(keyType, generateSeed("test"));
|
||||
SigningKeys const keys(keyType, sk, 1);
|
||||
EXPECT_EQ(keys.sign(data), expected.at(keyType));
|
||||
EXPECT_EQ(keys.signHex(strHex(data)), expected.at(keyType));
|
||||
auto const sig = required(strUnHex(keys.sign(data)));
|
||||
EXPECT_TRUE(verify(keys.publicKey(), makeSlice(data), makeSlice(sig)));
|
||||
|
||||
SigningKeys const external(keyType, derivePublicKey(keyType, sk));
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { (void)external.sign(data); }), "This key file cannot be used to sign.");
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { (void)external.signHex(strHex(data)); }),
|
||||
"This key file cannot be used to sign.");
|
||||
}
|
||||
SigningKeys const keys(KeyType::Ed25519);
|
||||
EXPECT_EQ(errorOf([&] { (void)keys.signHex("zz"); }), "Could not decode hex string: zz");
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, external_master)
|
||||
{
|
||||
for (auto const keyType : kKeyTypes)
|
||||
{
|
||||
// The signer stands in for the hardware holding the master key
|
||||
SigningKeys const signer(keyType);
|
||||
SigningKeys keys(keyType, signer.publicKey());
|
||||
std::uint32_t sequence = 0;
|
||||
|
||||
for (auto const tokenKeyType : kKeyTypes)
|
||||
{
|
||||
auto const data = keys.startToken(tokenKeyType);
|
||||
keys.writeToFile(keyFile_);
|
||||
auto fileKeys = SigningKeys::makeSigningKeys(keyFile_);
|
||||
EXPECT_TRUE(keys == fileKeys);
|
||||
|
||||
auto const finished = fileKeys.finishToken(required(strUnHex(signer.signHex(data))));
|
||||
ASSERT_TRUE(finished.secret);
|
||||
auto const m = manifestOf(finished.manifest, keys);
|
||||
EXPECT_EQ(m.sequence, ++sequence);
|
||||
EXPECT_EQ(fileKeys.sequence(), sequence);
|
||||
EXPECT_EQ(
|
||||
required(m.signingKey), derivePublicKey(tokenKeyType, required(finished.secret)));
|
||||
|
||||
// A signature over other bytes does not finish the token
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.finishToken(required(strUnHex(signer.sign("foo")))); }),
|
||||
kBadManifest);
|
||||
EXPECT_EQ(
|
||||
errorOf(
|
||||
[&] { keys.finishToken(required(strUnHex(signer.signHex(data))), Blob{}); }),
|
||||
"The pending token's signing key is in this key file; pass one signature");
|
||||
keys.finishToken(required(strUnHex(signer.signHex(data))));
|
||||
}
|
||||
|
||||
// Nothing pending
|
||||
EXPECT_EQ(errorOf([&] { keys.finishToken(Blob{}); }), "No pending token to finish");
|
||||
|
||||
// Revocation: the same bytes each time, so a signature can be kept and reused
|
||||
auto const revocation = keys.startRevoke();
|
||||
EXPECT_EQ(revocation, keys.startRevoke());
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.finishRevoke(required(strUnHex(signer.sign("foo")))); }),
|
||||
kBadManifest);
|
||||
EXPECT_FALSE(keys.revoked());
|
||||
auto const sig = required(strUnHex(signer.signHex(revocation)));
|
||||
manifestOf(keys.finishRevoke(sig), keys);
|
||||
EXPECT_TRUE(keys.revoked());
|
||||
manifestOf(keys.finishRevoke(sig), keys);
|
||||
|
||||
EXPECT_EQ(errorOf([&] { keys.startToken(); }), kRevoked);
|
||||
EXPECT_EQ(errorOf([&] { keys.finishToken(sig); }), kRevoked);
|
||||
}
|
||||
|
||||
SigningKeys exhausted(
|
||||
KeyType::Ed25519, SigningKeys(KeyType::Ed25519).publicKey(), kMaxSequence - 1);
|
||||
EXPECT_EQ(errorOf([&] { exhausted.startToken(); }), kExhausted);
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, external_signing_key)
|
||||
{
|
||||
// The master key is in software here; the signing key is held elsewhere.
|
||||
SigningKeys const signer(KeyType::Ed25519);
|
||||
SigningKeys keys(KeyType::Ed25519);
|
||||
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { keys.startToken(KeyType::Ed25519, keys.publicKey()); }),
|
||||
"The signing key must differ from the master key");
|
||||
|
||||
auto const data = keys.startToken(KeyType::Ed25519, signer.publicKey());
|
||||
keys.writeToFile(keyFile_);
|
||||
auto fileKeys = SigningKeys::makeSigningKeys(keyFile_);
|
||||
EXPECT_TRUE(keys == fileKeys);
|
||||
|
||||
auto const masterSig = required(strUnHex(keys.signHex(data)));
|
||||
auto const signingSig = required(strUnHex(signer.signHex(data)));
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { fileKeys.finishToken(masterSig); }),
|
||||
"The pending token's signing key is external; pass its signature too");
|
||||
EXPECT_EQ(errorOf([&] { fileKeys.finishToken(masterSig, masterSig); }), kBadManifest);
|
||||
|
||||
auto const finished = fileKeys.finishToken(masterSig, signingSig);
|
||||
EXPECT_FALSE(finished.secret);
|
||||
auto const m = manifestOf(finished.manifest, keys);
|
||||
EXPECT_EQ(m.sequence, 1u);
|
||||
EXPECT_EQ(required(m.signingKey), signer.publicKey());
|
||||
EXPECT_EQ(fileKeys.sequence(), 1u);
|
||||
EXPECT_EQ(
|
||||
errorOf([&] { fileKeys.finishToken(masterSig, signingSig); }),
|
||||
"No pending token to finish");
|
||||
}
|
||||
|
||||
TEST_F(SigningKeysTest, stored_manifest_is_checked)
|
||||
{
|
||||
SigningKeys keys(KeyType::Ed25519);
|
||||
keys.createToken(KeyType::Ed25519);
|
||||
keys.writeToFile(keyFile_);
|
||||
auto const good = keyFileJson();
|
||||
EXPECT_EQ(loadError(good), "");
|
||||
|
||||
// A token manifest on revoked keys
|
||||
auto jv = good;
|
||||
jv["revoked"] = true;
|
||||
EXPECT_EQ(loadError(jv), kBadManifest);
|
||||
|
||||
// A revocation manifest on keys that are not revoked
|
||||
SigningKeys revoked(KeyType::Ed25519);
|
||||
revoked.revoke();
|
||||
jv = good;
|
||||
jv["manifest"] = strHex(makeSlice(revoked.manifest()));
|
||||
EXPECT_EQ(loadError(jv), kBadManifest);
|
||||
|
||||
// A manifest of another key
|
||||
jv = good;
|
||||
jv["secret_key"] =
|
||||
toBase58(TokenType::NodePrivate, generateKeyPair(KeyType::Ed25519, randomSeed()).second);
|
||||
EXPECT_EQ(loadError(jv), kBadManifest);
|
||||
}
|
||||
|
||||
} // namespace xrpl::tools::test
|
||||
33
src/tools/validator-keys/CMakeLists.txt
Normal file
33
src/tools/validator-keys/CMakeLists.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
# The validator-keys tool: validator and publisher key files, manifests,
|
||||
# tokens, revocations and validator-list signing. Built only when the
|
||||
# validator_keys option is ON (see cmake/XrplValidatorKeys.cmake).
|
||||
include(GNUInstallDirs)
|
||||
|
||||
add_library(xrpl.validator-keys STATIC)
|
||||
target_sources(
|
||||
xrpl.validator-keys
|
||||
PRIVATE Commands.cpp ListSigning.cpp OwnerOnlyFile.cpp SigningKeys.cpp
|
||||
)
|
||||
target_include_directories(
|
||||
xrpl.validator-keys
|
||||
PUBLIC $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/src>
|
||||
)
|
||||
target_link_libraries(
|
||||
xrpl.validator-keys
|
||||
PUBLIC Xrpl::boost Xrpl::opts Xrpl::libs xrpl.libxrpl
|
||||
)
|
||||
|
||||
add_executable(validator-keys Main.cpp)
|
||||
target_link_libraries(validator-keys PRIVATE xrpl.validator-keys)
|
||||
patch_nix_binary(validator-keys)
|
||||
set_target_properties(
|
||||
validator-keys
|
||||
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/LICENSE"
|
||||
"${CMAKE_BINARY_DIR}/validator-keys-LICENSE"
|
||||
COPYONLY
|
||||
)
|
||||
install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
640
src/tools/validator-keys/Commands.cpp
Normal file
640
src/tools/validator-keys/Commands.cpp
Normal file
@@ -0,0 +1,640 @@
|
||||
#include <tools/validator-keys/Commands.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/core/SemanticVersion.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <boost/preprocessor/stringize.hpp>
|
||||
|
||||
#include <tools/validator-keys/ListSigning.h>
|
||||
#include <tools/validator-keys/OwnerOnlyFile.h>
|
||||
#include <tools/validator-keys/SigningKeys.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::tools {
|
||||
|
||||
namespace {
|
||||
|
||||
// The build version number: edit for each release, in semantic version form.
|
||||
char const* const kVersionString =
|
||||
"0.4.0"
|
||||
|
||||
#if defined(DEBUG) || defined(SANITIZER)
|
||||
"+"
|
||||
#ifdef DEBUG
|
||||
"DEBUG"
|
||||
#ifdef SANITIZER
|
||||
"."
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef SANITIZER
|
||||
BOOST_PP_STRINGIZE(SANITIZER)
|
||||
#endif
|
||||
#endif
|
||||
;
|
||||
|
||||
constexpr std::size_t kMaxDocumentBytes = 4 * 1024 * 1024;
|
||||
constexpr std::size_t kBlockLineLength = 72;
|
||||
|
||||
using Args = std::vector<std::string>;
|
||||
|
||||
struct Context
|
||||
{
|
||||
ToolOptions const& options;
|
||||
std::ostream& out;
|
||||
std::ostream& err;
|
||||
};
|
||||
|
||||
/**
|
||||
* Where a command's result goes: the file named by `--out`, replaced whole
|
||||
* once the command has succeeded, or the output stream. The file is prepared
|
||||
* before the command changes any state so an unwritable path fails first.
|
||||
*/
|
||||
class Output
|
||||
{
|
||||
// Null when the result goes to the output stream.
|
||||
std::unique_ptr<OwnerOnlyFile> file_;
|
||||
std::ostream& out_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @param inputs Files the command reads besides those in the options;
|
||||
* `--out` may not name any input, since the output replaces it.
|
||||
*/
|
||||
// A result that only goes to the output stream.
|
||||
explicit Output(std::ostream& out) : out_(out)
|
||||
{
|
||||
}
|
||||
|
||||
Output(Context const& ctx, std::vector<std::filesystem::path> const& inputs = {})
|
||||
: out_(ctx.out)
|
||||
{
|
||||
auto const& options = ctx.options;
|
||||
if (!options.outFile)
|
||||
return;
|
||||
|
||||
std::vector<std::filesystem::path> read(inputs);
|
||||
read.push_back(options.keyFile);
|
||||
for (auto const& file :
|
||||
{options.tokenFile, options.manifestFile, options.appendFile, options.validatorsFile})
|
||||
{
|
||||
if (file)
|
||||
read.push_back(*file);
|
||||
}
|
||||
for (auto const& file : read)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (std::filesystem::equivalent(file, *options.outFile, ec))
|
||||
throw std::runtime_error("--out names an input file: " + options.outFile->string());
|
||||
}
|
||||
|
||||
file_ = std::make_unique<OwnerOnlyFile>(*options.outFile, "output file");
|
||||
}
|
||||
|
||||
// A config block in 72-character lines.
|
||||
void
|
||||
block(std::string const& section, std::string const& publicKey, std::string const& body)
|
||||
{
|
||||
std::string text = "# validator public key: " + publicKey + "\n\n[" + section + "]\n";
|
||||
for (std::size_t i = 0; i < body.size(); i += kBlockLineLength)
|
||||
text.append(body, i, kBlockLineLength).push_back('\n');
|
||||
|
||||
if (!file_)
|
||||
{
|
||||
out_ << "Update xrpld.cfg file with these values and restart xrpld:\n\n"
|
||||
<< text << std::endl;
|
||||
return;
|
||||
}
|
||||
write(text, "[" + section + "]");
|
||||
}
|
||||
|
||||
void
|
||||
json(json::Value const& jv)
|
||||
{
|
||||
if (!file_)
|
||||
{
|
||||
out_ << jv.toStyledString() << std::endl;
|
||||
return;
|
||||
}
|
||||
write(jv.toStyledString(), "The list");
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
write(std::string const& text, std::string const& what)
|
||||
{
|
||||
file_->write(text);
|
||||
file_->commit();
|
||||
out_ << what << " written to " << file_->target().string() << "\n";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a public key given as base58, hex or base64.
|
||||
*
|
||||
* @throws std::runtime_error if none of the encodings yields a public key
|
||||
*/
|
||||
PublicKey
|
||||
parsePublicKey(std::string const& data)
|
||||
{
|
||||
if (auto const key = parseBase58<PublicKey>(TokenType::NodePublic, data))
|
||||
return *key;
|
||||
if (auto const key = parseHexKey(data))
|
||||
return *key;
|
||||
if (auto const bytes = base64Decode(data); publicKeyType(makeSlice(bytes)))
|
||||
return PublicKey(makeSlice(bytes));
|
||||
throw std::runtime_error("Unable to parse public key: " + data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a signature given as hex or base64. Only using it shows whether it
|
||||
* is right, so any string that decodes is accepted.
|
||||
*/
|
||||
Blob
|
||||
decodeSignature(std::string const& data)
|
||||
{
|
||||
if (auto const bytes = strUnHex(data))
|
||||
return *bytes;
|
||||
// base64Decode returns partial data for invalid input, so require a round trip.
|
||||
if (auto const bytes = base64Decode(data); base64Encode(bytes) == data)
|
||||
return Blob(bytes.begin(), bytes.end());
|
||||
throw std::runtime_error("Invalid signature encoding");
|
||||
}
|
||||
|
||||
json::Value
|
||||
readJsonFile(std::filesystem::path const& file)
|
||||
{
|
||||
std::error_code ec;
|
||||
auto const text = getFileContents(ec, file, kMaxDocumentBytes);
|
||||
if (ec)
|
||||
throw std::runtime_error("Failed to open file: " + file.string());
|
||||
json::Reader reader;
|
||||
json::Value jv;
|
||||
if (!reader.parse(text, jv))
|
||||
throw std::runtime_error("Not a JSON document: " + file.string());
|
||||
return jv;
|
||||
}
|
||||
|
||||
std::string
|
||||
nodePublic(SigningKeys const& keys)
|
||||
{
|
||||
return toBase58(TokenType::NodePublic, keys.publicKey());
|
||||
}
|
||||
|
||||
void
|
||||
refuseExisting(std::filesystem::path const& keyFile)
|
||||
{
|
||||
if (std::filesystem::exists(keyFile))
|
||||
throw std::runtime_error("Refusing to overwrite existing key file: " + keyFile.string());
|
||||
}
|
||||
|
||||
void
|
||||
storedNotice(std::filesystem::path const& keyFile, std::ostream& out)
|
||||
{
|
||||
out << "Validator keys stored in " << keyFile.string()
|
||||
<< "\n\nThis file should be stored securely and not shared.\n\n";
|
||||
}
|
||||
|
||||
SigningKeys
|
||||
loadUnrevoked(std::filesystem::path const& keyFile)
|
||||
{
|
||||
auto keys = SigningKeys::makeSigningKeys(keyFile);
|
||||
if (keys.revoked())
|
||||
throw std::runtime_error("Operation error: The specified master key has been revoked!");
|
||||
return keys;
|
||||
}
|
||||
|
||||
void
|
||||
warnRevocation(SigningKeys const& keys, std::ostream& err)
|
||||
{
|
||||
if (keys.revoked())
|
||||
{
|
||||
err << "WARNING: Validator keys have already been revoked!\n\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
err << "WARNING: This will revoke your validator keys!\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
emitFinished(SigningKeys const& keys, SigningKeys::Finished const& finished, Output& output)
|
||||
{
|
||||
if (finished.secret)
|
||||
{
|
||||
output.block(
|
||||
"validator_token",
|
||||
nodePublic(keys),
|
||||
tokenToBase64(
|
||||
ValidatorToken{
|
||||
.manifest = finished.manifest, .validationSecret = *finished.secret}));
|
||||
}
|
||||
else
|
||||
{
|
||||
output.block("validator_manifest", nodePublic(keys), finished.manifest);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
emitAttestation(SigningKeys const& keys, Context& ctx)
|
||||
{
|
||||
if (keys.domain().empty())
|
||||
{
|
||||
ctx.out << "No attestation is necessary if no domain is specified!\n"
|
||||
"If you have an attestation in your xrp-ledger.toml\n"
|
||||
"you should remove it at this time.\n";
|
||||
return;
|
||||
}
|
||||
if (!keys.hasSecret())
|
||||
{
|
||||
ctx.err << "Sign these bytes with the master key; the hex signature is the\n"
|
||||
"attestation for xrp-ledger.toml.\n\n";
|
||||
ctx.out << strHex(makeSlice(keys.attestationData())) << std::endl;
|
||||
return;
|
||||
}
|
||||
ctx.out << "The domain attestation for validator " << nodePublic(keys) << " is:\n\n"
|
||||
<< "attestation=\"" << keys.sign(keys.attestationData()) << "\"\n\n"
|
||||
<< "You should include it in your xrp-ledger.toml file in the\n"
|
||||
"section for this validator.\n";
|
||||
}
|
||||
|
||||
int
|
||||
cmdCreateKeys(Args const&, Context& ctx)
|
||||
{
|
||||
refuseExisting(ctx.options.keyFile);
|
||||
SigningKeys const keys(KeyType::Ed25519);
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
storedNotice(ctx.options.keyFile, ctx.out);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdCreateExternal(Args const& args, Context& ctx)
|
||||
{
|
||||
refuseExisting(ctx.options.keyFile);
|
||||
auto const publicKey = parsePublicKey(args[0]);
|
||||
auto const keyType = publicKeyType(publicKey);
|
||||
if (!keyType)
|
||||
logicError("create_external: public key without a key type"); // LCOV_EXCL_LINE
|
||||
SigningKeys const keys(*keyType, publicKey);
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
storedNotice(ctx.options.keyFile, ctx.out);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdCreateToken(Args const&, Context& ctx)
|
||||
{
|
||||
Output output(ctx);
|
||||
auto keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
auto const token = keys.createToken(ctx.options.tokenKeyType);
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
output.block("validator_token", nodePublic(keys), tokenToBase64(token));
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdStartToken(Args const&, Context& ctx)
|
||||
{
|
||||
auto keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
auto const data = keys.startToken(ctx.options.tokenKeyType, ctx.options.signingKey);
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
ctx.out << data << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdFinishToken(Args const& args, Context& ctx)
|
||||
{
|
||||
Output output(ctx);
|
||||
auto keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
std::optional<Blob> signingSig;
|
||||
if (args.size() == 2)
|
||||
signingSig = decodeSignature(args[1]);
|
||||
auto const finished = keys.finishToken(decodeSignature(args[0]), signingSig);
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
emitFinished(keys, finished, output);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdRevokeKeys(Args const&, Context& ctx)
|
||||
{
|
||||
auto keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
warnRevocation(keys, ctx.err);
|
||||
auto const revocation = keys.revoke();
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
Output(ctx.out).block("validator_key_revocation", nodePublic(keys), revocation);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdStartRevokeKeys(Args const&, Context& ctx)
|
||||
{
|
||||
auto const keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
warnRevocation(keys, ctx.err);
|
||||
ctx.out << keys.startRevoke() << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdFinishRevokeKeys(Args const& args, Context& ctx)
|
||||
{
|
||||
auto keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
warnRevocation(keys, ctx.err);
|
||||
auto const revocation = keys.finishRevoke(decodeSignature(args[0]));
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
Output(ctx.out).block("validator_key_revocation", nodePublic(keys), revocation);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
setDomain(std::string const& domain, Context& ctx)
|
||||
{
|
||||
Output output(ctx);
|
||||
auto keys = loadUnrevoked(ctx.options.keyFile);
|
||||
|
||||
if (domain == keys.domain())
|
||||
{
|
||||
ctx.out
|
||||
<< (domain.empty() ? "The domain name was already cleared!\n"
|
||||
: "The domain name was already set.\n");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
keys.domain(domain);
|
||||
if (!keys.hasSecret())
|
||||
{
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
ctx.out << (domain.empty() ? "The domain name has been cleared.\n"
|
||||
: "The domain name has been set to: " + domain + "\n")
|
||||
<< "The next token carries it: run start_token and finish_token.\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
auto const token = keys.createToken(ctx.options.tokenKeyType);
|
||||
keys.writeToFile(ctx.options.keyFile);
|
||||
|
||||
ctx.out
|
||||
<< (domain.empty() ? "The domain name has been cleared.\n"
|
||||
: "The domain name has been set to: " + domain + "\n\n");
|
||||
emitAttestation(keys, ctx);
|
||||
ctx.out << "\nYou also need to update the xrpld.cfg file to add a new\n"
|
||||
"validator token and restart xrpld:\n\n";
|
||||
output.block("validator_token", nodePublic(keys), tokenToBase64(token));
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdSetDomain(Args const& args, Context& ctx)
|
||||
{
|
||||
return setDomain(args[0], ctx);
|
||||
}
|
||||
|
||||
int
|
||||
cmdClearDomain(Args const&, Context& ctx)
|
||||
{
|
||||
return setDomain("", ctx);
|
||||
}
|
||||
|
||||
int
|
||||
cmdAttestDomain(Args const&, Context& ctx)
|
||||
{
|
||||
emitAttestation(loadUnrevoked(ctx.options.keyFile), ctx);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
sign(std::string const& data, bool hex, Context& ctx)
|
||||
{
|
||||
if (data.empty())
|
||||
throw std::runtime_error("Syntax error: Must specify data string to sign");
|
||||
auto const keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
if (keys.revoked())
|
||||
ctx.err << "WARNING: Validator keys have been revoked!\n\n";
|
||||
ctx.out << (hex ? keys.signHex(data) : keys.sign(data)) << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdSign(Args const& args, Context& ctx)
|
||||
{
|
||||
return sign(args[0], false, ctx);
|
||||
}
|
||||
|
||||
int
|
||||
cmdSignHex(Args const& args, Context& ctx)
|
||||
{
|
||||
return sign(args[0], true, ctx);
|
||||
}
|
||||
|
||||
int
|
||||
cmdShowManifest(Args const& args, Context& ctx)
|
||||
{
|
||||
auto const keys = SigningKeys::makeSigningKeys(ctx.options.keyFile);
|
||||
auto const& m = keys.manifest();
|
||||
if (m.empty())
|
||||
{
|
||||
ctx.out << "The last manifest generated is unavailable. You can\n"
|
||||
"generate a new one.\n\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if (args[0] == "base64")
|
||||
{
|
||||
ctx.out << "Manifest #" << keys.sequence() << " (Base64):\n"
|
||||
<< base64Encode(m.data(), m.size()) << "\n\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if (args[0] == "hex")
|
||||
{
|
||||
ctx.out << "Manifest #" << keys.sequence() << " (Hex):\n" << strHex(makeSlice(m)) << "\n\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
throw std::runtime_error("Unknown encoding '" + args[0] + "'");
|
||||
}
|
||||
|
||||
// The manifest a list is signed under when the signing key is external, and
|
||||
// the signing key it delegates to.
|
||||
std::pair<Manifest, PublicKey>
|
||||
loadSigningManifest(Context const& ctx, char const* command)
|
||||
{
|
||||
if (!ctx.options.manifestFile)
|
||||
throw std::runtime_error(std::string(command) + " needs --manifest-file");
|
||||
auto manifest = loadManifestFile(*ctx.options.manifestFile);
|
||||
if (manifest.revoked() || !manifest.signingKey)
|
||||
throw std::runtime_error("The manifest is revoked");
|
||||
auto const signingKey = *manifest.signingKey;
|
||||
return {std::move(manifest), signingKey};
|
||||
}
|
||||
|
||||
void
|
||||
emitSignedList(
|
||||
std::string const& manifestBase64,
|
||||
PublicKey const& masterKey,
|
||||
UnsignedList const& list,
|
||||
std::string const& signatureHex,
|
||||
Resigner const& resign,
|
||||
Context& ctx,
|
||||
Output& output)
|
||||
{
|
||||
std::optional<json::Value> append;
|
||||
if (ctx.options.appendFile)
|
||||
append = readJsonFile(*ctx.options.appendFile);
|
||||
output.json(makeSignedList(
|
||||
manifestBase64, masterKey, list, signatureHex, ctx.options.listVersion, append, resign));
|
||||
}
|
||||
|
||||
int
|
||||
cmdSignList(Args const& args, Context& ctx)
|
||||
{
|
||||
if (!ctx.options.tokenFile)
|
||||
throw std::runtime_error("sign_list needs --token-file");
|
||||
Output output(ctx, {args[0]});
|
||||
|
||||
auto const token = loadTokenFile(*ctx.options.tokenFile);
|
||||
auto const manifest = deserializeManifest(base64Decode(token.manifest));
|
||||
if (!manifest || !manifest->verify() || manifest->revoked() || !manifest->signingKey)
|
||||
throw std::runtime_error("The token's manifest is not valid");
|
||||
|
||||
auto const signingKey = *manifest->signingKey;
|
||||
auto const keyType = publicKeyType(signingKey);
|
||||
if (!keyType || derivePublicKey(*keyType, token.validationSecret) != signingKey)
|
||||
throw std::runtime_error("The token's secret does not match its manifest");
|
||||
|
||||
auto const list = loadUnsignedList(args[0]);
|
||||
auto const resign = [&](std::string const& blobBytes) {
|
||||
return strHex(xrpl::sign(signingKey, token.validationSecret, makeSlice(blobBytes)));
|
||||
};
|
||||
emitSignedList(
|
||||
token.manifest, manifest->masterKey, list, resign(list.canonical), resign, ctx, output);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdStartSignList(Args const& args, Context& ctx)
|
||||
{
|
||||
loadSigningManifest(ctx, "start_sign_list");
|
||||
auto const list = loadUnsignedList(args[0]);
|
||||
ctx.out << strHex(makeSlice(list.canonical)) << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdFinishSignList(Args const& args, Context& ctx)
|
||||
{
|
||||
auto const [manifest, signingKey] = loadSigningManifest(ctx, "finish_sign_list");
|
||||
Output output(ctx, {args[1]});
|
||||
|
||||
auto const list = loadUnsignedList(args[1]);
|
||||
auto const sig = decodeSignature(args[0]);
|
||||
if (!verify(signingKey, makeSlice(list.canonical), makeSlice(sig)))
|
||||
throw std::runtime_error("The signature does not verify under the manifest's signing key");
|
||||
|
||||
emitSignedList(
|
||||
base64Encode(manifest.serialized), manifest.masterKey, list, strHex(sig), {}, ctx, output);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int
|
||||
cmdVerifyList(Args const& args, Context& ctx)
|
||||
{
|
||||
std::optional<UnsignedList> roster;
|
||||
if (ctx.options.validatorsFile)
|
||||
roster = loadUnsignedList(*ctx.options.validatorsFile);
|
||||
|
||||
auto const report =
|
||||
verifyList(readJsonFile(args[0]), roster, ctx.options.expectedKey, netClockNow());
|
||||
ctx.out << report.toStyledString() << std::endl;
|
||||
return report["ok"].asBool() ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
|
||||
struct Command
|
||||
{
|
||||
char const* name;
|
||||
std::size_t minArgs;
|
||||
std::size_t maxArgs;
|
||||
int (*run)(Args const&, Context&);
|
||||
};
|
||||
|
||||
constexpr std::array<Command, 18> kCommands{{
|
||||
{.name = "create_keys", .minArgs = 0, .maxArgs = 0, .run = cmdCreateKeys},
|
||||
{.name = "create_external", .minArgs = 1, .maxArgs = 1, .run = cmdCreateExternal},
|
||||
{.name = "create_token", .minArgs = 0, .maxArgs = 0, .run = cmdCreateToken},
|
||||
{.name = "start_token", .minArgs = 0, .maxArgs = 0, .run = cmdStartToken},
|
||||
{.name = "finish_token", .minArgs = 1, .maxArgs = 2, .run = cmdFinishToken},
|
||||
{.name = "revoke_keys", .minArgs = 0, .maxArgs = 0, .run = cmdRevokeKeys},
|
||||
{.name = "start_revoke_keys", .minArgs = 0, .maxArgs = 0, .run = cmdStartRevokeKeys},
|
||||
{.name = "finish_revoke_keys", .minArgs = 1, .maxArgs = 1, .run = cmdFinishRevokeKeys},
|
||||
{.name = "set_domain", .minArgs = 1, .maxArgs = 1, .run = cmdSetDomain},
|
||||
{.name = "clear_domain", .minArgs = 0, .maxArgs = 0, .run = cmdClearDomain},
|
||||
{.name = "attest_domain", .minArgs = 0, .maxArgs = 0, .run = cmdAttestDomain},
|
||||
{.name = "sign", .minArgs = 1, .maxArgs = 1, .run = cmdSign},
|
||||
{.name = "sign_hex", .minArgs = 1, .maxArgs = 1, .run = cmdSignHex},
|
||||
{.name = "show_manifest", .minArgs = 1, .maxArgs = 1, .run = cmdShowManifest},
|
||||
{.name = "sign_list", .minArgs = 1, .maxArgs = 1, .run = cmdSignList},
|
||||
{.name = "start_sign_list", .minArgs = 1, .maxArgs = 1, .run = cmdStartSignList},
|
||||
{.name = "finish_sign_list", .minArgs = 2, .maxArgs = 2, .run = cmdFinishSignList},
|
||||
{.name = "verify_list", .minArgs = 1, .maxArgs = 1, .run = cmdVerifyList},
|
||||
}};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string const&
|
||||
getVersionString()
|
||||
{
|
||||
static std::string const kValue = [] {
|
||||
std::string const s = kVersionString;
|
||||
beast::SemanticVersion v;
|
||||
if (!v.parse(s) || v.print() != s)
|
||||
throw std::logic_error(s + ": Bad version string"); // LCOV_EXCL_LINE
|
||||
return s;
|
||||
}();
|
||||
return kValue;
|
||||
}
|
||||
|
||||
int
|
||||
runCommand(
|
||||
std::string const& command,
|
||||
std::vector<std::string> const& args,
|
||||
ToolOptions const& options,
|
||||
std::ostream& out,
|
||||
std::ostream& err)
|
||||
{
|
||||
auto const it =
|
||||
std::ranges::find_if(kCommands, [&](Command const& c) { return command == c.name; });
|
||||
if (it == kCommands.end())
|
||||
throw std::runtime_error("Unknown command: " + command);
|
||||
if (args.size() < it->minArgs || args.size() > it->maxArgs)
|
||||
throw std::runtime_error("Syntax error: Wrong number of arguments");
|
||||
|
||||
Context ctx{.options = options, .out = out, .err = err};
|
||||
return it->run(args, ctx);
|
||||
}
|
||||
|
||||
} // namespace xrpl::tools
|
||||
64
src/tools/validator-keys/Commands.h
Normal file
64
src/tools/validator-keys/Commands.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::tools {
|
||||
|
||||
/**
|
||||
* The command-line options every command may read.
|
||||
*/
|
||||
struct ToolOptions
|
||||
{
|
||||
// The master key file.
|
||||
std::filesystem::path keyFile;
|
||||
// Key type of a token's signing key. xrpld loads only secp256k1 validator
|
||||
// tokens; ed25519 is for a publisher's signing key.
|
||||
KeyType tokenKeyType = KeyType::Secp256k1;
|
||||
// External signing key a token delegates to.
|
||||
std::optional<PublicKey> signingKey;
|
||||
// File holding a [validator_token] block.
|
||||
std::optional<std::filesystem::path> tokenFile;
|
||||
// File holding a base64 manifest.
|
||||
std::optional<std::filesystem::path> manifestFile;
|
||||
// File to write a token or a signed list to instead of stdout.
|
||||
std::optional<std::filesystem::path> outFile;
|
||||
// Version of the signed list document.
|
||||
unsigned listVersion = 1;
|
||||
// Version 2 list to add a blob to.
|
||||
std::optional<std::filesystem::path> appendFile;
|
||||
// Unsigned list whose validators a published list must carry.
|
||||
std::optional<std::filesystem::path> validatorsFile;
|
||||
// Master key a published list must be signed under.
|
||||
std::optional<PublicKey> expectedKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* The tool's version, checked to be a semantic version.
|
||||
*/
|
||||
std::string const&
|
||||
getVersionString();
|
||||
|
||||
/**
|
||||
* Runs one command. Results go to @p out, warnings and notes to @p err.
|
||||
*
|
||||
* @return The process exit code
|
||||
*
|
||||
* @throws std::runtime_error naming what went wrong; nothing has been written
|
||||
* to a key file or an output file when it throws before that point
|
||||
*/
|
||||
int
|
||||
runCommand(
|
||||
std::string const& command,
|
||||
std::vector<std::string> const& args,
|
||||
ToolOptions const& options,
|
||||
std::ostream& out,
|
||||
std::ostream& err);
|
||||
|
||||
} // namespace xrpl::tools
|
||||
77
src/tools/validator-keys/LICENSE
Normal file
77
src/tools/validator-keys/LICENSE
Normal file
@@ -0,0 +1,77 @@
|
||||
The accompanying files under various copyrights.
|
||||
|
||||
Copyright (c) 2016 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
The accompanying files incorporate work covered by the following copyright
|
||||
and previous license notice:
|
||||
|
||||
Copyright (c) 2011 Arthur Britto, David Schwartz, Jed McCaleb,
|
||||
Vinnie Falco, Bob Way, Eric Lombrozo, Nikolaos D. Bougalis, Howard Hinnant
|
||||
|
||||
Some code from Raw Material Software, Ltd., provided under the terms of the
|
||||
ISC License. See the corresponding source files for more details.
|
||||
Copyright (c) 2013 - Raw Material Software Ltd.
|
||||
Please visit http://www.juce.com
|
||||
|
||||
Some code from ASIO examples:
|
||||
// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
|
||||
Some code from Bitcoin:
|
||||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2011 The Bitcoin developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
Some code from Tom Wu:
|
||||
This software is covered under the following copyright:
|
||||
|
||||
/*
|
||||
* Copyright (c) 2003-2005 Tom Wu
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
|
||||
* WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*
|
||||
* IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
|
||||
* INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
|
||||
* RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF
|
||||
* THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT
|
||||
* OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*
|
||||
* In addition, the following condition applies:
|
||||
*
|
||||
* All redistributions must retain an intact copy of this copyright notice
|
||||
* and disclaimer.
|
||||
*/
|
||||
|
||||
Address all questions regarding this license to:
|
||||
|
||||
Tom Wu
|
||||
tjw@cs.Stanford.EDU
|
||||
558
src/tools/validator-keys/ListSigning.cpp
Normal file
558
src/tools/validator-keys/ListSigning.cpp
Normal file
@@ -0,0 +1,558 @@
|
||||
#include <tools/validator-keys/ListSigning.h>
|
||||
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
// A version 2 document holds at most this many blobs.
|
||||
constexpr std::size_t kMaxBlobs = 5;
|
||||
// The largest value a server reads for sequence, expiration and effective.
|
||||
constexpr std::int64_t kMaxListInteger = 2147483647;
|
||||
constexpr std::size_t kMaxListBytes = 4 * 1024 * 1024;
|
||||
constexpr std::size_t kMaxKeyMaterialBytes = 64 * 1024;
|
||||
|
||||
std::string
|
||||
readFile(std::filesystem::path const& file, std::size_t maxSize)
|
||||
{
|
||||
std::error_code ec;
|
||||
auto text = getFileContents(ec, file, maxSize);
|
||||
if (ec)
|
||||
throw std::runtime_error("Failed to open file: " + file.string());
|
||||
return text;
|
||||
}
|
||||
|
||||
// The base64 lines of a config-style block, without its section header or
|
||||
// comment lines.
|
||||
std::vector<std::string>
|
||||
base64Lines(std::string const& text)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
boost::split(lines, text, boost::is_any_of("\n"));
|
||||
for (auto& line : lines)
|
||||
boost::trim(line);
|
||||
std::erase_if(lines, [](std::string const& line) {
|
||||
return line.empty() || line.front() == '#' || line.front() == '[';
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
std::optional<Manifest>
|
||||
parseManifest(std::string const& base64)
|
||||
{
|
||||
auto m = deserializeManifest(base64Decode(base64));
|
||||
if (!m || !m->verify())
|
||||
return std::nullopt;
|
||||
return m;
|
||||
}
|
||||
|
||||
// An integer field as a server reads it: type Int, so at most 2147483647.
|
||||
std::optional<std::uint32_t>
|
||||
listInteger(json::Value const& obj, char const* name)
|
||||
{
|
||||
if (!obj.isMember(name) || !obj[name].isInt() || obj[name].asInt() < 0)
|
||||
return std::nullopt;
|
||||
return obj[name].asUInt();
|
||||
}
|
||||
|
||||
std::string
|
||||
integerError(char const* name, std::int64_t least)
|
||||
{
|
||||
return std::string("\"") + name + "\" must be an integer from " + std::to_string(least) +
|
||||
" to " + std::to_string(kMaxListInteger);
|
||||
}
|
||||
|
||||
// Field checks over parsed text whose canonical form is already known.
|
||||
UnsignedList
|
||||
checkedList(std::string canonical, json::Value const& jv)
|
||||
{
|
||||
UnsignedList list;
|
||||
list.canonical = std::move(canonical);
|
||||
|
||||
auto const sequence = listInteger(jv, jss::sequence);
|
||||
if (!sequence || *sequence == 0)
|
||||
throw std::runtime_error(integerError(jss::sequence, 1));
|
||||
list.sequence = *sequence;
|
||||
|
||||
// A server takes a missing effective time as 0 and needs expiration after it.
|
||||
auto const expiration = listInteger(jv, jss::expiration);
|
||||
if (!expiration || *expiration == 0)
|
||||
throw std::runtime_error(integerError(jss::expiration, 1));
|
||||
list.expiration = *expiration;
|
||||
|
||||
if (jv.isMember(jss::effective))
|
||||
{
|
||||
auto const effective = listInteger(jv, jss::effective);
|
||||
if (!effective)
|
||||
throw std::runtime_error(integerError(jss::effective, 0));
|
||||
if (*effective >= list.expiration)
|
||||
throw std::runtime_error(R"("effective" must be earlier than "expiration")");
|
||||
list.effective = effective;
|
||||
}
|
||||
|
||||
if (!jv.isMember(jss::validators) || !jv[jss::validators].isArray() ||
|
||||
jv[jss::validators].size() == 0)
|
||||
throw std::runtime_error("\"validators\" must be a non-empty array");
|
||||
|
||||
for (auto const& entry : jv[jss::validators])
|
||||
{
|
||||
if (!entry.isObject() || !entry.isMember(jss::validation_public_key) ||
|
||||
!entry[jss::validation_public_key].isString())
|
||||
throw std::runtime_error("every validator needs a \"validation_public_key\" string");
|
||||
|
||||
auto const keyText = entry[jss::validation_public_key].asString();
|
||||
auto const key = parseHexKey(keyText);
|
||||
if (!key)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"\"validation_public_key\" is not a hex public key: " + keyText);
|
||||
}
|
||||
|
||||
if (entry.isMember(jss::manifest))
|
||||
{
|
||||
if (!entry[jss::manifest].isString())
|
||||
throw std::runtime_error("\"manifest\" must be a base64 string for " + keyText);
|
||||
auto const m = parseManifest(entry[jss::manifest].asString());
|
||||
if (!m)
|
||||
throw std::runtime_error("\"manifest\" does not verify for " + keyText);
|
||||
if (m->masterKey != *key)
|
||||
throw std::runtime_error("\"manifest\" belongs to another key than " + keyText);
|
||||
}
|
||||
|
||||
if (std::ranges::find(list.validators, *key) != list.validators.end())
|
||||
throw std::runtime_error("\"validators\" lists " + keyText + " more than once");
|
||||
list.validators.push_back(*key);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
json::Value
|
||||
parseObject(std::string const& text)
|
||||
{
|
||||
json::Reader reader;
|
||||
json::Value jv;
|
||||
if (!reader.parse(text, jv) || !jv.isObject())
|
||||
throw std::runtime_error("Not a JSON object");
|
||||
return jv;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<PublicKey>
|
||||
parseHexKey(std::string const& hex)
|
||||
{
|
||||
auto const bytes = strUnHex(hex);
|
||||
if (!bytes)
|
||||
return std::nullopt;
|
||||
auto const slice = makeSlice(*bytes);
|
||||
if (!publicKeyType(slice))
|
||||
return std::nullopt;
|
||||
return PublicKey(slice);
|
||||
}
|
||||
|
||||
ValidatorToken
|
||||
loadTokenFile(std::filesystem::path const& tokenFile)
|
||||
{
|
||||
auto const token = loadValidatorToken(base64Lines(readFile(tokenFile, kMaxKeyMaterialBytes)));
|
||||
if (!token)
|
||||
throw std::runtime_error("Not a validator token: " + tokenFile.string());
|
||||
return *token;
|
||||
}
|
||||
|
||||
Manifest
|
||||
loadManifestFile(std::filesystem::path const& manifestFile)
|
||||
{
|
||||
auto const lines = base64Lines(readFile(manifestFile, kMaxKeyMaterialBytes));
|
||||
auto m = parseManifest(boost::join(lines, ""));
|
||||
if (!m)
|
||||
throw std::runtime_error("Not a valid manifest: " + manifestFile.string());
|
||||
return std::move(*m);
|
||||
}
|
||||
|
||||
std::string
|
||||
canonicalJson(std::string const& text)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(text.size());
|
||||
bool inString = false;
|
||||
bool escaped = false;
|
||||
for (std::size_t i = 0; i < text.size(); ++i)
|
||||
{
|
||||
char const c = text[i];
|
||||
if (inString)
|
||||
{
|
||||
out += c;
|
||||
if (escaped)
|
||||
{
|
||||
escaped = false;
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
escaped = true;
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < text.size() && text[i + 1] == '/')
|
||||
{
|
||||
i = text.find('\n', i);
|
||||
if (i == std::string::npos)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < text.size() && text[i + 1] == '*')
|
||||
{
|
||||
i = text.find("*/", i + 2);
|
||||
if (i == std::string::npos)
|
||||
break;
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
switch (c)
|
||||
{
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '\n':
|
||||
case '\r':
|
||||
break;
|
||||
case '"':
|
||||
inString = true;
|
||||
out += c;
|
||||
break;
|
||||
case ',':
|
||||
out += ", ";
|
||||
break;
|
||||
case ':':
|
||||
out += ": ";
|
||||
break;
|
||||
default:
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
parseObject(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
UnsignedList
|
||||
parseUnsignedList(std::string const& text)
|
||||
{
|
||||
auto canonical = canonicalJson(text);
|
||||
auto const jv = parseObject(canonical);
|
||||
return checkedList(std::move(canonical), jv);
|
||||
}
|
||||
|
||||
UnsignedList
|
||||
loadUnsignedList(std::filesystem::path const& file)
|
||||
{
|
||||
return parseUnsignedList(readFile(file, kMaxListBytes));
|
||||
}
|
||||
|
||||
std::string
|
||||
signList(UnsignedList const& list, PublicKey const& signingKey, SecretKey const& signingSecret)
|
||||
{
|
||||
return strHex(sign(signingKey, signingSecret, makeSlice(list.canonical)));
|
||||
}
|
||||
|
||||
json::Value
|
||||
makeSignedList(
|
||||
std::string const& manifestBase64,
|
||||
PublicKey const& masterKey,
|
||||
UnsignedList const& list,
|
||||
std::string const& signatureHex,
|
||||
unsigned version,
|
||||
std::optional<json::Value> const& append,
|
||||
Resigner const& resign)
|
||||
{
|
||||
if (version != 1 && version != 2)
|
||||
throw std::runtime_error("Unsupported list version");
|
||||
if (version == 1 && append)
|
||||
throw std::runtime_error("A version 1 list holds one blob; use version 2 to append");
|
||||
|
||||
json::Value jv(json::ValueType::Object);
|
||||
if (append)
|
||||
{
|
||||
auto const& existing = *append;
|
||||
if (!existing.isObject() || !existing.isMember(jss::version) ||
|
||||
!existing[jss::version].isInt() || existing[jss::version].asInt() != 2 ||
|
||||
!existing.isMember(jss::blobs_v2) || !existing[jss::blobs_v2].isArray() ||
|
||||
!existing.isMember(jss::manifest) || !existing[jss::manifest].isString())
|
||||
throw std::runtime_error("The list to append to is not a version 2 list");
|
||||
if (!existing.isMember(jss::public_key) || !existing[jss::public_key].isString() ||
|
||||
!boost::iequals(existing[jss::public_key].asString(), strHex(masterKey)))
|
||||
throw std::runtime_error("The list to append to belongs to another master key");
|
||||
if (existing[jss::blobs_v2].size() >= kMaxBlobs)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"The list to append to already holds " + std::to_string(kMaxBlobs) + " blobs");
|
||||
}
|
||||
|
||||
jv[jss::blobs_v2] = existing[jss::blobs_v2];
|
||||
if (existing[jss::manifest].asString() != manifestBase64)
|
||||
{
|
||||
if (!resign)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"The list to append to was signed under another manifest and its blobs "
|
||||
"need signing again");
|
||||
}
|
||||
for (auto& entry : jv[jss::blobs_v2])
|
||||
{
|
||||
if (!entry.isObject() || !entry.isMember(jss::blob) || !entry[jss::blob].isString())
|
||||
throw std::runtime_error("The list to append to holds an invalid blob");
|
||||
entry[jss::signature] = resign(base64Decode(entry[jss::blob].asString()));
|
||||
entry.removeMember(jss::manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (version == 2)
|
||||
{
|
||||
jv[jss::blobs_v2] = json::Value(json::ValueType::Array);
|
||||
}
|
||||
|
||||
jv[jss::manifest] = manifestBase64;
|
||||
jv[jss::public_key] = strHex(masterKey);
|
||||
jv[jss::version] = static_cast<int>(version);
|
||||
|
||||
auto const blob = base64Encode(list.canonical);
|
||||
if (version == 1)
|
||||
{
|
||||
jv[jss::blob] = blob;
|
||||
jv[jss::signature] = signatureHex;
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value entry(json::ValueType::Object);
|
||||
entry[jss::blob] = blob;
|
||||
entry[jss::signature] = signatureHex;
|
||||
jv[jss::blobs_v2].append(entry);
|
||||
return jv;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
netClockNow()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto const since1970 = duration_cast<seconds>(system_clock::now().time_since_epoch());
|
||||
return static_cast<std::uint32_t>((since1970 - kEpochOffset).count());
|
||||
}
|
||||
|
||||
json::Value
|
||||
verifyList(
|
||||
json::Value const& list,
|
||||
std::optional<UnsignedList> const& expectedRoster,
|
||||
std::optional<PublicKey> const& expectedKey,
|
||||
std::uint32_t now)
|
||||
{
|
||||
json::Value report(json::ValueType::Object);
|
||||
report["ok"] = true;
|
||||
report["errors"] = json::Value(json::ValueType::Array);
|
||||
auto fail = [&report](std::string const& error) {
|
||||
report["ok"] = false;
|
||||
report["errors"].append(error);
|
||||
};
|
||||
|
||||
if (!list.isObject())
|
||||
{
|
||||
fail("the list is not a JSON object");
|
||||
return report;
|
||||
}
|
||||
|
||||
auto const version = listInteger(list, jss::version);
|
||||
if (!version || (*version != 1 && *version != 2))
|
||||
{
|
||||
fail("\"version\" must be 1 or 2");
|
||||
return report;
|
||||
}
|
||||
report[jss::version] = *version;
|
||||
|
||||
if (!list.isMember(jss::public_key) || !list[jss::public_key].isString() ||
|
||||
!list.isMember(jss::manifest) || !list[jss::manifest].isString())
|
||||
{
|
||||
fail(R"("public_key" and "manifest" must be strings)");
|
||||
return report;
|
||||
}
|
||||
|
||||
auto manifest = parseManifest(list[jss::manifest].asString());
|
||||
if (!manifest)
|
||||
{
|
||||
fail("\"manifest\" does not deserialize and verify");
|
||||
return report;
|
||||
}
|
||||
report[jss::public_key] = strHex(manifest->masterKey);
|
||||
report["manifest_sequence"] = manifest->sequence;
|
||||
|
||||
if (manifest->revoked() || !manifest->signingKey)
|
||||
{
|
||||
fail("the publisher's master key is revoked");
|
||||
return report;
|
||||
}
|
||||
report["signing_key"] = strHex(*manifest->signingKey);
|
||||
|
||||
{
|
||||
auto const declared = parseHexKey(list[jss::public_key].asString());
|
||||
if (!declared || *declared != manifest->masterKey)
|
||||
fail("\"public_key\" is not the manifest's master key");
|
||||
}
|
||||
if (expectedKey && *expectedKey != manifest->masterKey)
|
||||
fail("the master key is not the expected key");
|
||||
|
||||
struct Entry
|
||||
{
|
||||
std::string blob;
|
||||
std::string signature;
|
||||
std::optional<std::string> manifest;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
if (*version == 1)
|
||||
{
|
||||
if (!list.isMember(jss::blob) || !list[jss::blob].isString() ||
|
||||
!list.isMember(jss::signature) || !list[jss::signature].isString() ||
|
||||
list.isMember(jss::blobs_v2))
|
||||
{
|
||||
fail(R"(a version 1 list needs "blob" and "signature" and no "blobs_v2")");
|
||||
return report;
|
||||
}
|
||||
entries.push_back(
|
||||
{.blob = list[jss::blob].asString(),
|
||||
.signature = list[jss::signature].asString(),
|
||||
.manifest = {}});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!list.isMember(jss::blobs_v2) || !list[jss::blobs_v2].isArray() ||
|
||||
list[jss::blobs_v2].size() == 0 || list[jss::blobs_v2].size() > kMaxBlobs ||
|
||||
list.isMember(jss::blob) || list.isMember(jss::signature))
|
||||
{
|
||||
fail(
|
||||
"a version 2 list needs 1 to " + std::to_string(kMaxBlobs) +
|
||||
R"( "blobs_v2" entries and no top-level "blob")");
|
||||
return report;
|
||||
}
|
||||
for (auto const& entry : list[jss::blobs_v2])
|
||||
{
|
||||
if (!entry.isObject() || !entry.isMember(jss::blob) || !entry[jss::blob].isString() ||
|
||||
!entry.isMember(jss::signature) || !entry[jss::signature].isString() ||
|
||||
(entry.isMember(jss::manifest) && !entry[jss::manifest].isString()))
|
||||
{
|
||||
fail(
|
||||
"every \"blobs_v2\" entry needs \"blob\" and \"signature\" strings and "
|
||||
"an optional \"manifest\" string");
|
||||
return report;
|
||||
}
|
||||
std::optional<std::string> entryManifest;
|
||||
if (entry.isMember(jss::manifest))
|
||||
entryManifest = entry[jss::manifest].asString();
|
||||
entries.push_back(
|
||||
{.blob = entry[jss::blob].asString(),
|
||||
.signature = entry[jss::signature].asString(),
|
||||
.manifest = entryManifest});
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::set<PublicKey>> want;
|
||||
if (expectedRoster)
|
||||
want.emplace(expectedRoster->validators.begin(), expectedRoster->validators.end());
|
||||
|
||||
report["blobs"] = json::Value(json::ValueType::Array);
|
||||
std::size_t index = 0;
|
||||
for (auto const& entry : entries)
|
||||
{
|
||||
auto const where = "blob " + std::to_string(index++);
|
||||
json::Value found(json::ValueType::Object);
|
||||
|
||||
// A server applies an entry's manifest before checking the blob and
|
||||
// keeps the newest manifest it has seen for the publisher.
|
||||
if (entry.manifest)
|
||||
{
|
||||
auto m = parseManifest(*entry.manifest);
|
||||
if (!m || m->masterKey != manifest->masterKey)
|
||||
{
|
||||
fail(where + ": its \"manifest\" is not this publisher's");
|
||||
}
|
||||
else if (m->revoked() || !m->signingKey)
|
||||
{
|
||||
fail(where + ": its \"manifest\" revokes the publisher's master key");
|
||||
}
|
||||
else if (m->sequence > manifest->sequence)
|
||||
{
|
||||
manifest = std::move(m);
|
||||
}
|
||||
}
|
||||
|
||||
auto const sig = strUnHex(entry.signature);
|
||||
auto const data = base64Decode(entry.blob);
|
||||
if (!sig || !verify(*manifest->signingKey, makeSlice(data), makeSlice(*sig)))
|
||||
fail(where + ": the signature does not verify under the signing key");
|
||||
|
||||
std::optional<UnsignedList> parsed;
|
||||
try
|
||||
{
|
||||
parsed = parseUnsignedList(data);
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
fail(where + ": " + e.what());
|
||||
}
|
||||
|
||||
if (parsed)
|
||||
{
|
||||
found[jss::sequence] = parsed->sequence;
|
||||
if (parsed->effective)
|
||||
found[jss::effective] = *parsed->effective;
|
||||
found[jss::expiration] = parsed->expiration;
|
||||
found[jss::validators] = json::UInt(parsed->validators.size());
|
||||
found["expired"] = parsed->expiration <= now;
|
||||
|
||||
if (parsed->expiration <= now)
|
||||
fail(where + ": expired");
|
||||
|
||||
if (want)
|
||||
{
|
||||
std::set<PublicKey> const have(
|
||||
parsed->validators.begin(), parsed->validators.end());
|
||||
if (have != *want)
|
||||
fail(where + ": the validators differ from the expected list");
|
||||
}
|
||||
}
|
||||
|
||||
report["blobs"].append(found);
|
||||
}
|
||||
|
||||
report["signing_key"] = strHex(*manifest->signingKey);
|
||||
report["manifest_sequence"] = manifest->sequence;
|
||||
return report;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
153
src/tools/validator-keys/ListSigning.h
Normal file
153
src/tools/validator-keys/ListSigning.h
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Parses a public key given as hex: 33 bytes with a type byte a server
|
||||
* accepts.
|
||||
*/
|
||||
std::optional<PublicKey>
|
||||
parseHexKey(std::string const& hex);
|
||||
|
||||
/**
|
||||
* Reads a token from a file holding the [validator_token] block as
|
||||
* `create_token` prints it. The section header and `#` comment lines are
|
||||
* ignored and the base64 lines are joined.
|
||||
*
|
||||
* @throws std::runtime_error if the file cannot be read or is not a token
|
||||
*/
|
||||
ValidatorToken
|
||||
loadTokenFile(std::filesystem::path const& tokenFile);
|
||||
|
||||
/**
|
||||
* Reads a base64 manifest from a file. Comment lines and line breaks are
|
||||
* ignored.
|
||||
*
|
||||
* @throws std::runtime_error if the file cannot be read or the manifest does
|
||||
* not deserialize and verify
|
||||
*/
|
||||
Manifest
|
||||
loadManifestFile(std::filesystem::path const& manifestFile);
|
||||
|
||||
/**
|
||||
* A validator list before it is signed: the canonical bytes the signing key
|
||||
* signs, and the fields checked before signing.
|
||||
*/
|
||||
struct UnsignedList
|
||||
{
|
||||
// Canonical JSON text: compact, `, ` and `: ` separators, key order as
|
||||
// written.
|
||||
std::string canonical;
|
||||
std::uint32_t sequence = 0;
|
||||
std::optional<std::uint32_t> effective;
|
||||
std::uint32_t expiration = 0;
|
||||
// Master keys of the listed validators.
|
||||
std::vector<PublicKey> validators;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the canonical form of a JSON document: comments and whitespace
|
||||
* outside strings removed, one space after each `,` and `:`, key order
|
||||
* preserved.
|
||||
*
|
||||
* @throws std::runtime_error if the text is not a JSON object
|
||||
*/
|
||||
std::string
|
||||
canonicalJson(std::string const& text);
|
||||
|
||||
/**
|
||||
* Parses an unsigned list and checks its fields the way a server does:
|
||||
* `sequence`, `expiration` and an optional `effective` are integers no
|
||||
* greater than 2147483647, `effective` is earlier than `expiration`, and
|
||||
* every entry of `validators` has a `validation_public_key` that is a hex
|
||||
* public key and, if present, a `manifest` for that key.
|
||||
*
|
||||
* @throws std::runtime_error naming the first failed check
|
||||
*/
|
||||
UnsignedList
|
||||
parseUnsignedList(std::string const& text);
|
||||
|
||||
/**
|
||||
* Reads and parses an unsigned list file.
|
||||
*
|
||||
* @throws std::runtime_error if the file cannot be read or fails a check
|
||||
*/
|
||||
UnsignedList
|
||||
loadUnsignedList(std::filesystem::path const& file);
|
||||
|
||||
/**
|
||||
* Returns the hex signature of the list's canonical bytes.
|
||||
*/
|
||||
std::string
|
||||
signList(UnsignedList const& list, PublicKey const& signingKey, SecretKey const& signingSecret);
|
||||
|
||||
/**
|
||||
* Signs the bytes of an already published blob; returns the hex signature.
|
||||
*/
|
||||
using Resigner = std::function<std::string(std::string const& blobBytes)>;
|
||||
|
||||
/**
|
||||
* Builds the document a publisher serves.
|
||||
*
|
||||
* Version 1 is `{blob, manifest, public_key, signature, version}`. Version 2
|
||||
* carries the blob and signature inside `blobs_v2`; when @p append is given it
|
||||
* must be a version 2 document for the same master key and the new blob is
|
||||
* added to it. A server verifies every blob under the newest manifest it has
|
||||
* seen for the publisher, so when @p append was signed under another manifest
|
||||
* its blobs are signed again with @p resign.
|
||||
*
|
||||
* @throws std::runtime_error if @p append is not a version 2 document for
|
||||
* @p masterKey, already holds the maximum number of blobs, or needs
|
||||
* re-signing and @p resign is empty
|
||||
*/
|
||||
json::Value
|
||||
makeSignedList(
|
||||
std::string const& manifestBase64,
|
||||
PublicKey const& masterKey,
|
||||
UnsignedList const& list,
|
||||
std::string const& signatureHex,
|
||||
unsigned version,
|
||||
std::optional<json::Value> const& append,
|
||||
Resigner const& resign);
|
||||
|
||||
/**
|
||||
* Seconds since the XRP Ledger epoch, now.
|
||||
*/
|
||||
std::uint32_t
|
||||
netClockNow();
|
||||
|
||||
/**
|
||||
* Checks a published list the way a server does before trusting it: the
|
||||
* manifest verifies and names the `public_key`, every blob's signature
|
||||
* verifies under the newest signing key seen for the publisher, every blob
|
||||
* parses, and none has expired at @p now.
|
||||
*
|
||||
* @param list The document as served
|
||||
* @param expectedRoster When set, every blob must list exactly these master
|
||||
* keys
|
||||
* @param expectedKey When set, the manifest's master key must be this key
|
||||
* @param now Seconds since the XRP Ledger epoch
|
||||
*
|
||||
* @return A report with `ok`, `errors`, the keys and sequences found, and one
|
||||
* entry per blob
|
||||
*/
|
||||
json::Value
|
||||
verifyList(
|
||||
json::Value const& list,
|
||||
std::optional<UnsignedList> const& expectedRoster,
|
||||
std::optional<PublicKey> const& expectedKey,
|
||||
std::uint32_t now);
|
||||
|
||||
} // namespace xrpl
|
||||
203
src/tools/validator-keys/Main.cpp
Normal file
203
src/tools/validator-keys/Main.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
#include <tools/validator-keys/Commands.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// LCOV_EXCL_START
|
||||
namespace {
|
||||
|
||||
std::string
|
||||
getEnvVar(char const* name)
|
||||
{
|
||||
auto const v = std::getenv(name);
|
||||
return v == nullptr ? std::string() : std::string(v);
|
||||
}
|
||||
|
||||
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"
|
||||
" sign_hex <data> Decode and sign hex 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"
|
||||
"Commands for signing externally: \n"
|
||||
" create_external <public key> Generate validator keys without "
|
||||
"a secret.\n"
|
||||
" start_token Print the bytes a token's "
|
||||
"signatures cover; --signing-key delegates to an external key.\n"
|
||||
" finish_token <master sig> [<signing sig>]\n"
|
||||
" Finish the token with the "
|
||||
"external signature(s).\n"
|
||||
" start_revoke_keys Print the bytes a revocation's "
|
||||
"signature covers.\n"
|
||||
" finish_revoke_keys <sig> Finish the revocation with the "
|
||||
"external signature.\n"
|
||||
"Commands for validator lists: \n"
|
||||
" sign_list <unsigned list> Sign a list with --token-file.\n"
|
||||
" start_sign_list <unsigned list>\n"
|
||||
" Print the bytes to sign with an "
|
||||
"external signing key; needs --manifest-file.\n"
|
||||
" finish_sign_list <sig> <unsigned list>\n"
|
||||
" Assemble the signed list from an "
|
||||
"external signature; needs --manifest-file.\n"
|
||||
" verify_list <list> Check a published list; "
|
||||
"--validators and --expected-key add checks.\n";
|
||||
}
|
||||
|
||||
xrpl::PublicKey
|
||||
publicKeyOption(std::string const& value)
|
||||
{
|
||||
if (auto const key = xrpl::parseBase58<xrpl::PublicKey>(xrpl::TokenType::NodePublic, value))
|
||||
return *key;
|
||||
if (auto const bytes = xrpl::strUnHex(value);
|
||||
bytes && xrpl::publicKeyType(xrpl::makeSlice(*bytes)))
|
||||
return xrpl::PublicKey(xrpl::makeSlice(*bytes));
|
||||
throw std::runtime_error("Unable to parse public key: " + value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
namespace po = boost::program_options;
|
||||
using namespace xrpl::tools;
|
||||
|
||||
po::options_description general("General Options");
|
||||
general.add_options()("help,h", "Display this message.")(
|
||||
"keyfile", po::value<std::string>(), "Specify the key file.")(
|
||||
"token-key-type",
|
||||
po::value<std::string>(),
|
||||
"Key type of a token's signing key: secp256k1 (default; the only type xrpld loads "
|
||||
"from [validator_token]) or ed25519 (for a publisher's signing key).")(
|
||||
"signing-key",
|
||||
po::value<std::string>(),
|
||||
"External signing key a token delegates to (start_token).")(
|
||||
"token-file", po::value<std::string>(), "File holding a [validator_token] block.")(
|
||||
"manifest-file", po::value<std::string>(), "File holding a base64 manifest.")(
|
||||
"out", po::value<std::string>(), "Write the token or signed list to this file.")(
|
||||
"list-version", po::value<unsigned>(), "Signed list version: 1 (default) or 2.")(
|
||||
"append", po::value<std::string>(), "Version 2 list to add the new blob to.")(
|
||||
"validators",
|
||||
po::value<std::string>(),
|
||||
"Unsigned list whose validators a published list must carry (verify_list).")(
|
||||
"expected-key",
|
||||
po::value<std::string>(),
|
||||
"Master key a published list must be signed under (verify_list).")(
|
||||
"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 positional;
|
||||
positional.add("command", 1).add("arguments", -1);
|
||||
|
||||
po::options_description all;
|
||||
all.add(general).add(hidden);
|
||||
|
||||
po::variables_map vm;
|
||||
try
|
||||
{
|
||||
po::store(
|
||||
po::command_line_parser(argc, argv).options(all).positional(positional).run(), vm);
|
||||
po::notify(vm);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (vm.contains("version"))
|
||||
{
|
||||
std::cout << "validator-keys version " << getVersionString() << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (vm.contains("help") || !vm.contains("command"))
|
||||
{
|
||||
printHelp(general);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
std::string const homeDir = getEnvVar("HOME");
|
||||
std::string const defaultKeyFile =
|
||||
(homeDir.empty() ? std::filesystem::current_path().string() : homeDir) +
|
||||
"/.ripple/validator-keys.json";
|
||||
|
||||
try
|
||||
{
|
||||
ToolOptions options;
|
||||
options.keyFile = vm.contains("keyfile") ? vm["keyfile"].as<std::string>() : defaultKeyFile;
|
||||
|
||||
if (vm.contains("token-key-type"))
|
||||
{
|
||||
auto const keyType = xrpl::keyTypeFromString(vm["token-key-type"].as<std::string>());
|
||||
if (!keyType)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Unknown key type: " + vm["token-key-type"].as<std::string>());
|
||||
}
|
||||
options.tokenKeyType = *keyType;
|
||||
}
|
||||
if (vm.contains("signing-key"))
|
||||
options.signingKey = publicKeyOption(vm["signing-key"].as<std::string>());
|
||||
if (vm.contains("token-file"))
|
||||
options.tokenFile = vm["token-file"].as<std::string>();
|
||||
if (vm.contains("manifest-file"))
|
||||
options.manifestFile = vm["manifest-file"].as<std::string>();
|
||||
if (vm.contains("out"))
|
||||
options.outFile = vm["out"].as<std::string>();
|
||||
if (vm.contains("list-version"))
|
||||
options.listVersion = vm["list-version"].as<unsigned>();
|
||||
if (vm.contains("append"))
|
||||
options.appendFile = vm["append"].as<std::string>();
|
||||
if (vm.contains("validators"))
|
||||
options.validatorsFile = vm["validators"].as<std::string>();
|
||||
if (vm.contains("expected-key"))
|
||||
options.expectedKey = publicKeyOption(vm["expected-key"].as<std::string>());
|
||||
|
||||
return runCommand(
|
||||
vm["command"].as<std::string>(),
|
||||
vm["arguments"].as<std::vector<std::string>>(),
|
||||
options,
|
||||
std::cout,
|
||||
std::cerr);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
std::cerr << e.what() << "\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
70
src/tools/validator-keys/OwnerOnlyFile.cpp
Normal file
70
src/tools/validator-keys/OwnerOnlyFile.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <tools/validator-keys/OwnerOnlyFile.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <initializer_list>
|
||||
#include <ios>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
OwnerOnlyFile::OwnerOnlyFile(fs::path target, std::string what)
|
||||
: target_(std::move(target)), temp_(target_.string() + ".tmp"), what_(std::move(what))
|
||||
{
|
||||
for (auto const& path : {target_, temp_})
|
||||
{
|
||||
if (fs::is_symlink(path))
|
||||
throw std::runtime_error("Refusing to write through a symlink: " + path.string());
|
||||
}
|
||||
|
||||
stream_.open(temp_, std::ios_base::trunc);
|
||||
if (stream_.fail())
|
||||
throw std::runtime_error("Cannot write " + what_ + ": " + target_.string());
|
||||
|
||||
std::error_code ec;
|
||||
fs::permissions(temp_, fs::perms::owner_read | fs::perms::owner_write, ec);
|
||||
if (ec)
|
||||
{
|
||||
stream_.close(); // LCOV_EXCL_LINE
|
||||
fs::remove(temp_, ec); // LCOV_EXCL_LINE
|
||||
throw std::runtime_error( // LCOV_EXCL_LINE
|
||||
"Cannot write " + what_ + ": " + target_.string()); // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
OwnerOnlyFile::~OwnerOnlyFile()
|
||||
{
|
||||
if (!committed_)
|
||||
{
|
||||
stream_.close();
|
||||
std::error_code ec;
|
||||
fs::remove(temp_, ec);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
OwnerOnlyFile::write(std::string const& text)
|
||||
{
|
||||
stream_ << text;
|
||||
}
|
||||
|
||||
void
|
||||
OwnerOnlyFile::commit()
|
||||
{
|
||||
stream_.close();
|
||||
std::error_code ec;
|
||||
if (!stream_.fail())
|
||||
fs::rename(temp_, target_, ec);
|
||||
if (stream_.fail() || ec)
|
||||
{
|
||||
fs::remove(temp_, ec);
|
||||
throw std::runtime_error("Cannot write " + what_ + ": " + target_.string());
|
||||
}
|
||||
committed_ = true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
59
src/tools/validator-keys/OwnerOnlyFile.h
Normal file
59
src/tools/validator-keys/OwnerOnlyFile.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* A file written for its owner only and replaced whole.
|
||||
*
|
||||
* The content goes to a temporary beside the target, restricted to the owner
|
||||
* before the first byte, and the target is replaced in one step by `commit`.
|
||||
* Without `commit` the temporary is removed, so a failed command leaves the
|
||||
* previous target as it was.
|
||||
*/
|
||||
class OwnerOnlyFile
|
||||
{
|
||||
std::filesystem::path target_;
|
||||
std::filesystem::path temp_;
|
||||
// Names the file in errors: "key file", "output file".
|
||||
std::string what_;
|
||||
std::ofstream stream_;
|
||||
bool committed_ = false;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Opens the temporary.
|
||||
*
|
||||
* @throws std::runtime_error if the target or the temporary is a symlink,
|
||||
* or the temporary cannot be opened or restricted
|
||||
*/
|
||||
OwnerOnlyFile(std::filesystem::path target, std::string what);
|
||||
~OwnerOnlyFile();
|
||||
|
||||
OwnerOnlyFile(OwnerOnlyFile const&) = delete;
|
||||
OwnerOnlyFile&
|
||||
operator=(OwnerOnlyFile const&) = delete;
|
||||
|
||||
void
|
||||
write(std::string const& text);
|
||||
|
||||
/**
|
||||
* Replaces the target with what was written.
|
||||
*
|
||||
* @throws std::runtime_error if the content could not be written or the
|
||||
* target could not be replaced
|
||||
*/
|
||||
void
|
||||
commit();
|
||||
|
||||
[[nodiscard]] std::filesystem::path const&
|
||||
target() const
|
||||
{
|
||||
return target_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
24
src/tools/validator-keys/README.md
Normal file
24
src/tools/validator-keys/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# validator-keys
|
||||
|
||||
Command-line tool for the keys behind an XRP Ledger validator or a validator-list
|
||||
publisher: the key file, the manifest that delegates from the master key to a
|
||||
signing key, the `[validator_token]` for `xrpld.cfg`, key revocation, domain
|
||||
attestation, and signing and verifying validator lists.
|
||||
|
||||
It is built as a separate target of this repository and ships in the `xrpld`
|
||||
packages as `/usr/bin/validator-keys`.
|
||||
|
||||
## Build
|
||||
|
||||
Configure with `-Dvalidator_keys=ON` and build the `validator-keys` target. With
|
||||
`-Dtests=ON` the gtest suites under `src/tests/tools/validator-keys` build as
|
||||
`validator_keys_tests`:
|
||||
|
||||
```
|
||||
cmake --build . --target validator-keys validator_keys_tests
|
||||
./validator_keys_tests
|
||||
```
|
||||
|
||||
## Guide
|
||||
|
||||
[Validator Keys Tool Guide](doc/validator-keys-tool-guide.md)
|
||||
518
src/tools/validator-keys/SigningKeys.cpp
Normal file
518
src/tools/validator-keys/SigningKeys.cpp
Normal file
@@ -0,0 +1,518 @@
|
||||
#include <tools/validator-keys/SigningKeys.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <tools/validator-keys/OwnerOnlyFile.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
// Key files are small; anything larger is not one.
|
||||
constexpr std::size_t kMaxKeyFileBytes = 64 * 1024;
|
||||
|
||||
char const* const kRevokedError = "Validator keys have been revoked.";
|
||||
char const* const kExhaustedError =
|
||||
"Maximum number of tokens have already been generated.\n"
|
||||
"Revoke validator keys if previous token has been compromised.";
|
||||
char const* const kNoSecretError = "This key file cannot be used to sign.";
|
||||
char const* const kBadManifestError = "Manifest is not properly signed";
|
||||
|
||||
bool
|
||||
sameSecret(SecretKey const& a, SecretKey const& b)
|
||||
{
|
||||
return std::equal(a.begin(), a.end(), b.begin());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string
|
||||
tokenToBase64(ValidatorToken const& token)
|
||||
{
|
||||
json::Value jv;
|
||||
jv["validation_secret_key"] = strHex(token.validationSecret);
|
||||
jv["manifest"] = token.manifest;
|
||||
|
||||
return base64Encode(to_string(jv));
|
||||
}
|
||||
|
||||
SigningKeys::SigningKeys(KeyType const& keyType)
|
||||
: keyType_(keyType)
|
||||
, keys_(generateKeyPair(keyType_, randomSeed()))
|
||||
, tokenSequence_(0)
|
||||
, revoked_(false)
|
||||
{
|
||||
}
|
||||
|
||||
SigningKeys::SigningKeys(
|
||||
KeyType const& keyType,
|
||||
SecretKey const& secretKey,
|
||||
std::uint32_t tokenSequence,
|
||||
bool revoked)
|
||||
: keyType_(keyType)
|
||||
, keys_({derivePublicKey(keyType_, secretKey), secretKey})
|
||||
, tokenSequence_(tokenSequence)
|
||||
, revoked_(revoked)
|
||||
{
|
||||
}
|
||||
|
||||
SigningKeys::SigningKeys(
|
||||
KeyType const& keyType,
|
||||
PublicKey const& publicKey,
|
||||
std::uint32_t tokenSequence,
|
||||
bool revoked)
|
||||
: keyType_(keyType), keys_(publicKey), tokenSequence_(tokenSequence), revoked_(revoked)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
SigningKeys::operator==(SigningKeys const& rhs) const
|
||||
{
|
||||
if (keyType_ != rhs.keyType_ || keys_.publicKey != rhs.keys_.publicKey ||
|
||||
keys_.secretKey.has_value() != rhs.keys_.secretKey.has_value() ||
|
||||
(keys_.secretKey && !sameSecret(*keys_.secretKey, *rhs.keys_.secretKey)))
|
||||
return false;
|
||||
if (tokenSequence_ != rhs.tokenSequence_ || revoked_ != rhs.revoked_ ||
|
||||
domain_ != rhs.domain_ || manifest_ != rhs.manifest_ ||
|
||||
pending_.has_value() != rhs.pending_.has_value())
|
||||
return false;
|
||||
if (!pending_)
|
||||
return true;
|
||||
auto const& pending = *pending_;
|
||||
auto const& other = *rhs.pending_;
|
||||
if (pending.signingKey != other.signingKey ||
|
||||
pending.generated.has_value() != other.generated.has_value())
|
||||
return false;
|
||||
if (!pending.generated || !other.generated)
|
||||
return true;
|
||||
return pending.generated->keyType == other.generated->keyType &&
|
||||
sameSecret(pending.generated->secretKey, other.generated->secretKey);
|
||||
}
|
||||
|
||||
SigningKeys
|
||||
SigningKeys::makeSigningKeys(std::filesystem::path const& keyFile)
|
||||
{
|
||||
std::error_code ec;
|
||||
auto const text = getFileContents(ec, keyFile, kMaxKeyFileBytes);
|
||||
if (ec)
|
||||
throw std::runtime_error("Failed to open key file: " + keyFile.string());
|
||||
|
||||
json::Reader reader;
|
||||
json::Value jKeys;
|
||||
if (!reader.parse(text, jKeys) || !jKeys.isObject())
|
||||
throw std::runtime_error("Unable to parse json key file: " + keyFile.string());
|
||||
|
||||
static constexpr std::array<char const*, 4> kRequiredFields{
|
||||
{"key_type", "secret_key", "token_sequence", "revoked"}};
|
||||
|
||||
for (auto const* field : kRequiredFields)
|
||||
{
|
||||
if (!jKeys.isMember(field))
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() + "' is missing \"" + field + "\" field");
|
||||
}
|
||||
}
|
||||
|
||||
// The value is not repeated: it may be a secret.
|
||||
auto const invalidField = [&keyFile](std::string const& field) {
|
||||
return std::runtime_error(
|
||||
"Key file '" + keyFile.string() + "' contains invalid \"" + field + "\" field");
|
||||
};
|
||||
|
||||
auto const keyType = keyTypeFromString(jKeys["key_type"].asString());
|
||||
if (!keyType)
|
||||
throw invalidField("key_type");
|
||||
|
||||
if (!jKeys["token_sequence"].isIntegral() || jKeys["token_sequence"].isBool() ||
|
||||
(jKeys["token_sequence"].isInt() && jKeys["token_sequence"].asInt() < 0))
|
||||
throw invalidField("token_sequence");
|
||||
auto const tokenSequence = jKeys["token_sequence"].asUInt();
|
||||
|
||||
if (!jKeys["revoked"].isBool())
|
||||
throw invalidField("revoked");
|
||||
auto const revoked = jKeys["revoked"].asBool();
|
||||
|
||||
auto keys = [&]() {
|
||||
if (jKeys["secret_key"].asString() == "external")
|
||||
{
|
||||
if (!jKeys.isMember("public_key"))
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() + "' is missing \"public_key\" field");
|
||||
}
|
||||
auto const pubKey =
|
||||
parseBase58<PublicKey>(TokenType::NodePublic, jKeys["public_key"].asString());
|
||||
if (!pubKey)
|
||||
throw invalidField("public_key");
|
||||
if (*keyType != *publicKeyType(*pubKey))
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
R"(' has a "key_type" that does not match "public_key")");
|
||||
}
|
||||
return SigningKeys(*keyType, *pubKey, tokenSequence, revoked);
|
||||
}
|
||||
auto const secret =
|
||||
parseBase58<SecretKey>(TokenType::NodePrivate, jKeys["secret_key"].asString());
|
||||
if (!secret)
|
||||
throw invalidField("secret_key");
|
||||
return SigningKeys(*keyType, *secret, tokenSequence, revoked);
|
||||
}();
|
||||
|
||||
if (jKeys.isMember("domain"))
|
||||
{
|
||||
if (!jKeys["domain"].isString())
|
||||
throw invalidField("domain");
|
||||
keys.domain(jKeys["domain"].asString());
|
||||
}
|
||||
|
||||
if (jKeys.isMember("manifest"))
|
||||
{
|
||||
if (!jKeys["manifest"].isString())
|
||||
throw invalidField("manifest");
|
||||
auto bytes = strUnHex(jKeys["manifest"].asString());
|
||||
if (!bytes || bytes->empty())
|
||||
throw invalidField("manifest");
|
||||
keys.manifest_ = std::move(*bytes);
|
||||
keys.checkManifest();
|
||||
}
|
||||
|
||||
bool const hasSecret = jKeys.isMember("pending_token_secret");
|
||||
bool const hasSigningKey = jKeys.isMember("pending_signing_key");
|
||||
if (hasSecret || hasSigningKey)
|
||||
{
|
||||
if (hasSecret && hasSigningKey)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
R"(' has both "pending_token_secret" and "pending_signing_key")");
|
||||
}
|
||||
if (hasSecret)
|
||||
{
|
||||
if (!jKeys.isMember("pending_key_type"))
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() + "' is missing \"pending_key_type\" field");
|
||||
}
|
||||
auto const pendingKeyType = keyTypeFromString(jKeys["pending_key_type"].asString());
|
||||
if (!pendingKeyType)
|
||||
throw invalidField("pending_key_type");
|
||||
if (!jKeys["pending_token_secret"].isString())
|
||||
throw invalidField("pending_token_secret");
|
||||
auto const secret = parseBase58<SecretKey>(
|
||||
TokenType::NodePrivate, jKeys["pending_token_secret"].asString());
|
||||
if (!secret)
|
||||
throw invalidField("pending_token_secret");
|
||||
keys.pending_ = Pending{
|
||||
.signingKey = derivePublicKey(*pendingKeyType, *secret),
|
||||
.generated = GeneratedKey{.keyType = *pendingKeyType, .secretKey = *secret}};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!jKeys["pending_signing_key"].isString())
|
||||
throw invalidField("pending_signing_key");
|
||||
auto const signingKey = parseBase58<PublicKey>(
|
||||
TokenType::NodePublic, jKeys["pending_signing_key"].asString());
|
||||
if (!signingKey)
|
||||
throw invalidField("pending_signing_key");
|
||||
keys.pending_ = Pending{.signingKey = *signingKey, .generated = std::nullopt};
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
void
|
||||
SigningKeys::writeToFile(std::filesystem::path const& keyFile) const
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
json::Value jv;
|
||||
jv["key_type"] = to_string(keyType_);
|
||||
jv["public_key"] = toBase58(TokenType::NodePublic, keys_.publicKey);
|
||||
jv["secret_key"] =
|
||||
keys_.secretKey ? toBase58(TokenType::NodePrivate, *keys_.secretKey) : "external";
|
||||
jv["token_sequence"] = json::UInt(tokenSequence_);
|
||||
jv["revoked"] = revoked_;
|
||||
if (!domain_.empty())
|
||||
jv["domain"] = domain_;
|
||||
if (!manifest_.empty())
|
||||
jv["manifest"] = strHex(makeSlice(manifest_));
|
||||
if (pending_)
|
||||
{
|
||||
if (auto const& generated = pending_->generated)
|
||||
{
|
||||
jv["pending_key_type"] = to_string(generated->keyType);
|
||||
jv["pending_token_secret"] = toBase58(TokenType::NodePrivate, generated->secretKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
jv["pending_signing_key"] = toBase58(TokenType::NodePublic, pending_->signingKey);
|
||||
}
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
if (auto const parent = keyFile.parent_path(); !parent.empty())
|
||||
{
|
||||
// A directory made here is the owner's alone.
|
||||
if (fs::create_directories(parent, ec) && !ec)
|
||||
fs::permissions(parent, fs::perms::owner_all, ec);
|
||||
if (ec || !fs::is_directory(parent))
|
||||
throw std::runtime_error("Cannot create directory: " + parent.string());
|
||||
}
|
||||
|
||||
OwnerOnlyFile file(keyFile, "key file");
|
||||
file.write(jv.toStyledString());
|
||||
file.commit();
|
||||
}
|
||||
|
||||
STObject
|
||||
SigningKeys::partialManifest(std::uint32_t sequence, PublicKey const& signingKey) const
|
||||
{
|
||||
return makeManifestFields(keys_.publicKey, signingKey, sequence, domain_);
|
||||
}
|
||||
|
||||
STObject
|
||||
SigningKeys::partialRevocation() const
|
||||
{
|
||||
return makeRevocationFields(keys_.publicKey);
|
||||
}
|
||||
|
||||
void
|
||||
SigningKeys::checkManifest() const
|
||||
{
|
||||
auto const m = deserializeManifest(manifest_);
|
||||
if (!m || !m->verify() || m->masterKey != keys_.publicKey || m->revoked() != revoked_)
|
||||
throw std::runtime_error(kBadManifestError);
|
||||
}
|
||||
|
||||
void
|
||||
SigningKeys::storeManifest(STObject const& st)
|
||||
{
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
auto const previous = std::exchange(manifest_, std::vector<std::uint8_t>(s.begin(), s.end()));
|
||||
try
|
||||
{
|
||||
checkManifest();
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
manifest_ = previous;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::startToken(KeyType const& keyType, std::optional<PublicKey> const& externalSigningKey)
|
||||
{
|
||||
if (externalSigningKey)
|
||||
{
|
||||
if (*externalSigningKey == keys_.publicKey)
|
||||
throw std::runtime_error("The signing key must differ from the master key");
|
||||
return strHex(
|
||||
startPending(Pending{.signingKey = *externalSigningKey, .generated = std::nullopt}));
|
||||
}
|
||||
|
||||
auto const secret = generateSecretKey(keyType, randomSeed());
|
||||
return strHex(startPending(
|
||||
Pending{
|
||||
.signingKey = derivePublicKey(keyType, secret),
|
||||
.generated = GeneratedKey{.keyType = keyType, .secretKey = secret}}));
|
||||
}
|
||||
|
||||
Blob
|
||||
SigningKeys::startPending(Pending const& pending)
|
||||
{
|
||||
if (revoked_)
|
||||
throw std::runtime_error(kRevokedError);
|
||||
if (tokenSequence_ >= std::numeric_limits<std::uint32_t>::max() - 1)
|
||||
throw std::runtime_error(kExhaustedError);
|
||||
|
||||
pending_ = pending;
|
||||
return manifestSigningData(partialManifest(tokenSequence_ + 1, pending.signingKey));
|
||||
}
|
||||
|
||||
SigningKeys::Finished
|
||||
SigningKeys::finishToken(Blob const& masterSig, std::optional<Blob> const& signingSig)
|
||||
{
|
||||
if (revoked_)
|
||||
throw std::runtime_error(kRevokedError);
|
||||
if (!pending_)
|
||||
throw std::runtime_error("No pending token to finish");
|
||||
|
||||
auto const pending = *pending_;
|
||||
std::optional<SecretKey> secret;
|
||||
if (pending.generated)
|
||||
secret = pending.generated->secretKey;
|
||||
return Finished{.manifest = finishPending(pending, masterSig, signingSig), .secret = secret};
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::finishPending(
|
||||
Pending const& pending,
|
||||
Blob const& masterSig,
|
||||
std::optional<Blob> const& signingSig)
|
||||
{
|
||||
STObject st = partialManifest(tokenSequence_ + 1, pending.signingKey);
|
||||
if (auto const& generated = pending.generated)
|
||||
{
|
||||
if (signingSig)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"The pending token's signing key is in this key file; pass one signature");
|
||||
}
|
||||
xrpl::sign(st, HashPrefix::Manifest, generated->keyType, generated->secretKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!signingSig)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"The pending token's signing key is external; pass its signature too");
|
||||
}
|
||||
st[sfSignature] = makeSlice(*signingSig);
|
||||
}
|
||||
st[sfMasterSignature] = makeSlice(masterSig);
|
||||
|
||||
storeManifest(st);
|
||||
++tokenSequence_;
|
||||
pending_.reset();
|
||||
|
||||
return base64Encode(manifest_.data(), manifest_.size());
|
||||
}
|
||||
|
||||
ValidatorToken
|
||||
SigningKeys::createToken(KeyType const& keyType)
|
||||
{
|
||||
if (!keys_.secretKey)
|
||||
throw std::runtime_error("This key file cannot be used to sign tokens.");
|
||||
|
||||
auto const secret = generateSecretKey(keyType, randomSeed());
|
||||
Pending const pending{
|
||||
.signingKey = derivePublicKey(keyType, secret),
|
||||
.generated = GeneratedKey{.keyType = keyType, .secretKey = secret}};
|
||||
auto const data = startPending(pending);
|
||||
return ValidatorToken{
|
||||
.manifest = finishPending(pending, masterSign(makeSlice(data)), std::nullopt),
|
||||
.validationSecret = secret};
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::startRevoke() const
|
||||
{
|
||||
return strHex(manifestSigningData(partialRevocation()));
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::finishRevoke(Blob const& masterSig)
|
||||
{
|
||||
STObject st = partialRevocation();
|
||||
st[sfMasterSignature] = makeSlice(masterSig);
|
||||
|
||||
auto const wasRevoked = std::exchange(revoked_, true);
|
||||
try
|
||||
{
|
||||
storeManifest(st);
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
revoked_ = wasRevoked;
|
||||
throw;
|
||||
}
|
||||
pending_.reset();
|
||||
|
||||
return base64Encode(manifest_.data(), manifest_.size());
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::revoke()
|
||||
{
|
||||
if (!keys_.secretKey)
|
||||
throw std::runtime_error("This key file cannot be used to sign tokens.");
|
||||
|
||||
return finishRevoke(masterSign(makeSlice(manifestSigningData(partialRevocation()))));
|
||||
}
|
||||
|
||||
Blob
|
||||
SigningKeys::masterSign(Slice const& data) const
|
||||
{
|
||||
if (!keys_.secretKey)
|
||||
throw std::runtime_error(kNoSecretError);
|
||||
|
||||
auto const sig = xrpl::sign(keys_.publicKey, *keys_.secretKey, data);
|
||||
return Blob(sig.begin(), sig.end());
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::sign(std::string const& data) const
|
||||
{
|
||||
return strHex(masterSign(makeSlice(data)));
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::signHex(std::string data) const
|
||||
{
|
||||
boost::algorithm::trim(data);
|
||||
auto const blob = strUnHex(data);
|
||||
if (!blob)
|
||||
throw std::runtime_error("Could not decode hex string: " + data);
|
||||
return strHex(masterSign(makeSlice(*blob)));
|
||||
}
|
||||
|
||||
std::string
|
||||
SigningKeys::attestationData() const
|
||||
{
|
||||
return "[domain-attestation-blob:" + domain_ + ":" +
|
||||
toBase58(TokenType::NodePublic, keys_.publicKey) + "]";
|
||||
}
|
||||
|
||||
void
|
||||
SigningKeys::domain(std::string d)
|
||||
{
|
||||
if (!d.empty() && !isProperlyFormedTomlDomain(d))
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"The domain field must use the '[host.][subdomain.]domain.tld' format");
|
||||
}
|
||||
|
||||
domain_ = std::move(d);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
316
src/tools/validator-keys/SigningKeys.h
Normal file
316
src/tools/validator-keys/SigningKeys.h
Normal file
@@ -0,0 +1,316 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Returns the token as the base64 JSON object written to [validator_token].
|
||||
*/
|
||||
std::string
|
||||
tokenToBase64(ValidatorToken const& token);
|
||||
|
||||
/**
|
||||
* The master key of a validator or a validator-list publisher, as stored in
|
||||
* the key file, with the manifests, tokens and revocations the master key
|
||||
* signs.
|
||||
*
|
||||
* A manifest is made in two steps so the master signature can come from a
|
||||
* signer outside this process: `startToken` fixes the manifest's contents and
|
||||
* returns the bytes to sign, `finishToken` takes the signature back. When the
|
||||
* master secret is in the key file, `createToken` does both. A revocation
|
||||
* follows the same two steps.
|
||||
*
|
||||
* The secret key is optional. When it is absent the key file was created with
|
||||
* `create_external` and the master key never signs inside this process.
|
||||
*/
|
||||
class SigningKeys
|
||||
{
|
||||
private:
|
||||
struct Keys
|
||||
{
|
||||
PublicKey publicKey;
|
||||
// Unset when the master key is held by an external signer.
|
||||
std::optional<SecretKey> secretKey;
|
||||
|
||||
Keys() = delete;
|
||||
Keys(std::pair<PublicKey, SecretKey> const& p) : publicKey(p.first), secretKey(p.second)
|
||||
{
|
||||
}
|
||||
Keys(PublicKey const& pub) : publicKey(pub), secretKey(std::nullopt)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// A signing key generated here, kept until its token is finished.
|
||||
struct GeneratedKey
|
||||
{
|
||||
KeyType keyType;
|
||||
SecretKey secretKey;
|
||||
};
|
||||
|
||||
// A token started and not yet finished: its signing key and, unless an
|
||||
// external signer holds that key, the key generated here.
|
||||
struct Pending
|
||||
{
|
||||
PublicKey signingKey;
|
||||
std::optional<GeneratedKey> generated;
|
||||
};
|
||||
|
||||
KeyType const keyType_;
|
||||
Keys const keys_;
|
||||
std::vector<std::uint8_t> manifest_;
|
||||
std::uint32_t tokenSequence_;
|
||||
bool revoked_;
|
||||
std::string domain_;
|
||||
std::optional<Pending> pending_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* The result of finishing a token: the manifest and, when the signing key
|
||||
* was generated here, its secret.
|
||||
*/
|
||||
struct Finished
|
||||
{
|
||||
std::string manifest;
|
||||
std::optional<SecretKey> secret;
|
||||
};
|
||||
|
||||
explicit SigningKeys(KeyType const& keyType);
|
||||
|
||||
SigningKeys(
|
||||
KeyType const& keyType,
|
||||
SecretKey const& secretKey,
|
||||
std::uint32_t tokenSequence,
|
||||
bool revoked = false);
|
||||
|
||||
/**
|
||||
* Creates keys whose secret is held by an external signer.
|
||||
*
|
||||
* The key file is written with `"secret_key": "external"`.
|
||||
*/
|
||||
SigningKeys(
|
||||
KeyType const& keyType,
|
||||
PublicKey const& publicKey,
|
||||
std::uint32_t tokenSequence = 0,
|
||||
bool revoked = false);
|
||||
|
||||
/**
|
||||
* Returns SigningKeys constructed from a JSON key file.
|
||||
*
|
||||
* @param keyFile Path to JSON key file
|
||||
*
|
||||
* @throws std::runtime_error if file content is invalid or the stored
|
||||
* manifest is not a valid manifest for this key
|
||||
*/
|
||||
static SigningKeys
|
||||
makeSigningKeys(std::filesystem::path const& keyFile);
|
||||
|
||||
~SigningKeys() = default;
|
||||
SigningKeys(SigningKeys const&) = default;
|
||||
SigningKeys&
|
||||
operator=(SigningKeys const&) = delete;
|
||||
|
||||
bool
|
||||
operator==(SigningKeys const& rhs) const;
|
||||
|
||||
/**
|
||||
* Writes the keys to a JSON key file readable by its owner only. The file
|
||||
* is replaced whole, so a failed write leaves the previous content.
|
||||
*
|
||||
* @param keyFile Path to file to write
|
||||
*
|
||||
* @throws std::runtime_error if the file cannot be written
|
||||
*/
|
||||
void
|
||||
writeToFile(std::filesystem::path const& keyFile) const;
|
||||
|
||||
/**
|
||||
* Starts a token: fixes the next manifest's contents and returns the
|
||||
* bytes both its signatures cover, as hex.
|
||||
*
|
||||
* With @p externalSigningKey the token delegates to that key and its
|
||||
* signature must come from outside too; otherwise a signing key of
|
||||
* @p keyType is generated and kept pending until `finishToken`.
|
||||
*
|
||||
* @throws std::runtime_error if the keys are revoked, the sequence is
|
||||
* exhausted, or the external signing key is the master key
|
||||
*/
|
||||
std::string
|
||||
startToken(
|
||||
KeyType const& keyType = KeyType::Secp256k1,
|
||||
std::optional<PublicKey> const& externalSigningKey = std::nullopt);
|
||||
|
||||
/**
|
||||
* Finishes the pending token with the master signature and, when the
|
||||
* signing key is external, the signing key's signature over the same
|
||||
* bytes.
|
||||
*
|
||||
* @throws std::runtime_error if no token is pending, a needed signature is
|
||||
* missing, or the manifest does not verify
|
||||
*/
|
||||
Finished
|
||||
finishToken(Blob const& masterSig, std::optional<Blob> const& signingSig = std::nullopt);
|
||||
|
||||
/**
|
||||
* Makes a token signed with the master secret in this key file.
|
||||
*
|
||||
* @param keyType Key type of the token's signing key
|
||||
*
|
||||
* @throws std::runtime_error if the master key is external, the keys are
|
||||
* revoked, or the sequence is exhausted
|
||||
*/
|
||||
ValidatorToken
|
||||
createToken(KeyType const& keyType = KeyType::Secp256k1);
|
||||
|
||||
/**
|
||||
* Returns the bytes a master signature over a revocation covers, as hex.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
startRevoke() const;
|
||||
|
||||
/**
|
||||
* Records the revocation and returns the base64 revocation manifest.
|
||||
*
|
||||
* @throws std::runtime_error if the signature does not verify
|
||||
*/
|
||||
std::string
|
||||
finishRevoke(Blob const& masterSig);
|
||||
|
||||
/**
|
||||
* Revokes the keys with the master secret in this key file.
|
||||
*
|
||||
* @throws std::runtime_error if the master key is external
|
||||
*/
|
||||
std::string
|
||||
revoke();
|
||||
|
||||
/**
|
||||
* Signs a string with the master key.
|
||||
*
|
||||
* @return The hex signature
|
||||
*
|
||||
* @throws std::runtime_error if the master key is external
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
sign(std::string const& data) const;
|
||||
|
||||
/**
|
||||
* Signs hex-encoded bytes with the master key.
|
||||
*
|
||||
* @return The hex signature
|
||||
*
|
||||
* @throws std::runtime_error if the data is not hex or the master key is
|
||||
* external
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
signHex(std::string data) const;
|
||||
|
||||
/**
|
||||
* The string a domain attestation signs, for the domain of this key.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
attestationData() const;
|
||||
|
||||
[[nodiscard]] PublicKey const&
|
||||
publicKey() const
|
||||
{
|
||||
return keys_.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the master secret is in the key file.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
hasSecret() const
|
||||
{
|
||||
return keys_.secretKey.has_value();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
revoked() const
|
||||
{
|
||||
return revoked_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string const&
|
||||
domain() const
|
||||
{
|
||||
return domain_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the domain the next manifest carries.
|
||||
*
|
||||
* @throws std::runtime_error if the domain is not well formed
|
||||
*/
|
||||
void
|
||||
domain(std::string d);
|
||||
|
||||
/**
|
||||
* The last manifest generated, serialized; empty if none.
|
||||
*/
|
||||
[[nodiscard]] std::vector<std::uint8_t> const&
|
||||
manifest() const
|
||||
{
|
||||
return manifest_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t
|
||||
sequence() const
|
||||
{
|
||||
return tokenSequence_;
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] STObject
|
||||
partialManifest(std::uint32_t sequence, PublicKey const& signingKey) const;
|
||||
|
||||
[[nodiscard]] STObject
|
||||
partialRevocation() const;
|
||||
|
||||
/**
|
||||
* Fixes the next manifest for @p pending and returns the bytes both its
|
||||
* signatures cover.
|
||||
*/
|
||||
Blob
|
||||
startPending(Pending const& pending);
|
||||
|
||||
/**
|
||||
* Assembles and stores the manifest of @p pending from its signatures and
|
||||
* returns it as base64.
|
||||
*/
|
||||
std::string
|
||||
finishPending(
|
||||
Pending const& pending,
|
||||
Blob const& masterSig,
|
||||
std::optional<Blob> const& signingSig);
|
||||
|
||||
/**
|
||||
* Signs bytes with the master key.
|
||||
*
|
||||
* @throws std::runtime_error if the master key is external
|
||||
*/
|
||||
[[nodiscard]] Blob
|
||||
masterSign(Slice const& data) const;
|
||||
|
||||
void
|
||||
storeManifest(STObject const& st);
|
||||
|
||||
void
|
||||
checkManifest() const;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
229
src/tools/validator-keys/doc/validator-keys-tool-guide.md
Normal file
229
src/tools/validator-keys/doc/validator-keys-tool-guide.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Validator Keys Tool Guide
|
||||
|
||||
This guide explains how to set up a validator so its public key does not have to
|
||||
change if the xrpld config and/or server are compromised.
|
||||
|
||||
A validator uses a public/private key pair. The validator is identified by the
|
||||
public key. The private key should be tightly controlled. It is used to:
|
||||
|
||||
- sign tokens authorizing an xrpld server to run as the validator identified
|
||||
by this public key.
|
||||
- sign revocations indicating that the private key has been compromised and
|
||||
the validator public key should no longer be trusted.
|
||||
|
||||
Each new token invalidates all previous tokens for the validator public key.
|
||||
The current token needs to be present in the xrpld config file.
|
||||
|
||||
Servers that trust the validator will adapt automatically when the token
|
||||
changes.
|
||||
|
||||
## Validator Keys
|
||||
|
||||
When first setting up a validator, use the `validator-keys` tool to generate
|
||||
its key pair:
|
||||
|
||||
```
|
||||
$ validator-keys create_keys
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
Validator keys stored in /home/ubuntu/.ripple/validator-keys.json
|
||||
```
|
||||
|
||||
Keep the key file in a secure but recoverable location, such as an encrypted
|
||||
USB flash drive. Do not modify its contents.
|
||||
|
||||
## Validator Token
|
||||
|
||||
After first creating the [validator keys](#validator-keys) or if the previous
|
||||
token has been compromised, use the `validator-keys` tool to create a new
|
||||
validator token:
|
||||
|
||||
```
|
||||
$ validator-keys create_token
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
Update xrpld.cfg file with these values:
|
||||
|
||||
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
|
||||
|
||||
[validator_token]
|
||||
<base64 token, in 72-character lines>
|
||||
```
|
||||
|
||||
For a new validator, add the [validator_token] value to the xrpld config file.
|
||||
For a pre-existing validator, replace the old [validator_token] value with the
|
||||
newly generated one. A valid config file may only contain one [validator_token]
|
||||
value. After the config is updated, restart xrpld.
|
||||
|
||||
There is a hard limit of 4,294,967,294 tokens that can be generated for a given
|
||||
validator key pair.
|
||||
|
||||
## Key Revocation
|
||||
|
||||
If a validator private key is compromised, the key must be revoked permanently.
|
||||
To revoke the validator key, use the `validator-keys` tool to generate a
|
||||
revocation, which indicates to other servers that the key is no longer valid:
|
||||
|
||||
```
|
||||
$ validator-keys revoke_keys
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
WARNING: This will revoke your validator keys!
|
||||
|
||||
Update xrpld.cfg file with these values and restart xrpld:
|
||||
|
||||
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
|
||||
|
||||
[validator_key_revocation]
|
||||
<base64 revocation, in 72-character lines>
|
||||
```
|
||||
|
||||
Add the `[validator_key_revocation]` value to this validator's config and
|
||||
restart xrpld. Rename the old key file and generate new [validator keys](#validator-keys) and
|
||||
a corresponding [validator token](#validator-token).
|
||||
|
||||
## Signing
|
||||
|
||||
The `validator-keys` tool can be used to sign arbitrary data with the validator
|
||||
key.
|
||||
|
||||
```
|
||||
$ validator-keys sign "your data to sign"
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
B91B73536235BBA028D344B81DBCBECF19C1E0034AC21FB51C2351A138C9871162F3193D7C41A49FB7AABBC32BC2B116B1D5701807BE462D8800B5AEA4F0550D
|
||||
```
|
||||
|
||||
## External Signing
|
||||
|
||||
The master key can live in a hardware signer instead of the key file. The tool
|
||||
then produces the bytes to sign, the hardware signs them, and the tool
|
||||
assembles the result.
|
||||
|
||||
One-time setup, with the hardware key's public key in base58 (`nHB...`), hex
|
||||
(`ED...`) or base64:
|
||||
|
||||
```
|
||||
$ validator-keys create_external <public key>
|
||||
```
|
||||
|
||||
The key file is written with `"secret_key": "external"`.
|
||||
|
||||
To create a token, print the bytes to sign, sign them externally, and pass the
|
||||
hex or base64 signature back:
|
||||
|
||||
```
|
||||
$ validator-keys start_token
|
||||
$ validator-keys finish_token <signature>
|
||||
```
|
||||
|
||||
The output is the same `[validator_token]` block `create_token` prints. A
|
||||
revocation works the same way with `start_revoke_keys` and
|
||||
`finish_revoke_keys <signature>`.
|
||||
|
||||
When the signing key is also held by the hardware, name it when starting the
|
||||
token. The bytes are then signed twice, once by each key, and the result is a
|
||||
manifest without a secret:
|
||||
|
||||
```
|
||||
$ validator-keys start_token --signing-key <signing public key>
|
||||
$ validator-keys finish_token <master signature> <signing signature>
|
||||
```
|
||||
|
||||
A secp256k1 signature from an external signer must be in the fully canonical
|
||||
form a server accepts (a low `S` value in the DER encoding); the tool rejects
|
||||
any other form. An ed25519 signature has one form.
|
||||
|
||||
`set_domain` on an external master key stores the domain for the next token
|
||||
made with `start_token` and `finish_token`, and `attest_domain` then prints the
|
||||
bytes of the attestation for the external signer; the hex signature is the
|
||||
attestation.
|
||||
|
||||
For testing without a hardware signer, a second key file can stand in for it:
|
||||
|
||||
```
|
||||
$ validator-keys --keyfile other-keys.json sign_hex <bytes>
|
||||
```
|
||||
|
||||
## Validator List Publishers
|
||||
|
||||
A publisher's keys are a master key and a signing key bound to it by a
|
||||
manifest, exactly like a validator's. The token `create_token` prints holds
|
||||
that manifest and the signing key, so a publisher's setup is:
|
||||
|
||||
```
|
||||
$ validator-keys create_keys
|
||||
$ validator-keys create_token --token-key-type ed25519 --out publisher-token.txt
|
||||
```
|
||||
|
||||
`--token-key-type ed25519` matches what hardware signers support; it is for a
|
||||
publisher's signing key only, since xrpld loads secp256k1 tokens from
|
||||
`[validator_token]` and no other. `--out` writes the token to a file readable
|
||||
only by its owner instead of printing it.
|
||||
|
||||
The manifest's sequence is `token_sequence` in the key file. A server keeps
|
||||
the highest sequence it has seen for a master key, so a key migrated from
|
||||
another tool must start `token_sequence` above the sequence of the manifest
|
||||
currently published.
|
||||
|
||||
### Signing a list
|
||||
|
||||
The unsigned list is a JSON file with the fields a server reads:
|
||||
|
||||
```json
|
||||
{
|
||||
"sequence": 2026091301,
|
||||
"expiration": 843955200,
|
||||
"validators": [
|
||||
{ "validation_public_key": "ED...", "manifest": "<base64 manifest>" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`sequence` must rise with every published list, `expiration` and the optional
|
||||
`effective` are seconds since the XRP Ledger epoch, and each validator's
|
||||
`manifest` must belong to its `validation_public_key`. Whitespace is removed
|
||||
and one space is placed after each `,` and `:` before signing.
|
||||
|
||||
```
|
||||
$ validator-keys sign_list unsigned.json --token-file publisher-token.txt --out vl.json
|
||||
```
|
||||
|
||||
`vl.json` is the version 1 document a server fetches: `blob`, `manifest`,
|
||||
`public_key`, `signature`, `version`. `--list-version 2` writes the blob into
|
||||
`blobs_v2` instead, and `--append <existing.json>` adds it to a version 2
|
||||
document that already holds up to four blobs, so a list can be published
|
||||
alongside the one it will replace.
|
||||
|
||||
When the signing key is held by a hardware signer, the list is signed in two
|
||||
steps against the manifest from `finish_token`:
|
||||
|
||||
```
|
||||
$ validator-keys start_sign_list unsigned.json --manifest-file manifest.txt
|
||||
$ validator-keys finish_sign_list <signature> unsigned.json --manifest-file manifest.txt --out vl.json
|
||||
```
|
||||
|
||||
### Checking a list
|
||||
|
||||
```
|
||||
$ validator-keys verify_list vl.json --validators unsigned.json --expected-key <master public key>
|
||||
```
|
||||
|
||||
The checks are the ones a server makes before trusting the list: the manifest
|
||||
verifies and names `public_key`, every blob's signature verifies under the
|
||||
manifest's signing key, every blob parses, and none has expired. `--validators`
|
||||
requires every blob to list exactly the keys in the unsigned list, and
|
||||
`--expected-key` requires the master key to be the one given. The result is
|
||||
printed as JSON and the exit code is 0 only if every check passed.
|
||||
Reference in New Issue
Block a user