Compare commits

...

18 Commits

Author SHA1 Message Date
Denis Angell
fa789d95fd style: drop includes the test publisher server no longer uses 2026-09-14 18:31:58 -04:00
Denis Angell
3dc27ca77d fix: replace output files whole and refuse an output that names an input 2026-09-14 17:32:39 -04:00
Denis Angell
5cda41fad0 refactor: sign manifests and revocations in libxrpl 2026-09-14 17:14:23 -04:00
Denis Angell
88a9bc76bc fix: name xrp-ledger.toml correctly in the attest_domain hint 2026-09-14 14:52:49 -04:00
Denis Angell
5e7da075fa fix: refuse symlinked key file temporaries and keep secrets out of errors 2026-09-14 14:35:34 -04:00
Denis Angell
b51d8c1140 refactor: satisfy clang-tidy in validator-keys 2026-09-14 13:46:16 -04:00
Denis Angell
82f95502ac docs: article the rename check expects in the validator-keys guide 2026-09-14 13:03:36 -04:00
Denis Angell
0467fd7144 fix: reject duplicate validators and a zero expiration; restrict output files before writing 2026-09-14 12:51:59 -04:00
Denis Angell
3ffba1678e refactor: one manifest pipeline, server-faithful list checks, gtest suites for validator-keys 2026-09-14 12:47:53 -04:00
Denis Angell
9d2428967d test: cover the tool's sources; appended blobs verify under their own manifest 2026-09-13 20:39:36 -04:00
Denis Angell
7bd7ea79dd ci: lint validator-keys in the clang-tidy job 2026-09-13 19:56:36 -04:00
Denis Angell
728db41016 test: manifest sequence continues from a migrated token_sequence 2026-09-13 19:56:34 -04:00
Denis Angell
ff5aa537e1 feat: vendor validator-keys with external signing and validator-list signing 2026-09-13 19:52:40 -04:00
Sergey Kuznetsov
9403736199 ci: Exclude Rust unit tests from code coverage (#8203) 2026-09-10 11:38:25 +00:00
Mayukha Vadari
028783661d feat: Apply .macro changes from ripple/smart-escrow (#8157)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-09 20:05:58 +00:00
Vito Tumas
21890d9daf feat: Register featureLendingProtocolV1_2 amendment (#8185)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-09-09 10:01:32 +00:00
Sergey Kuznetsov
060957ed39 ci: Add nightly Rust toolchain to the CI image (#8182)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-09-09 01:24:28 +00:00
Ayaz Salikhov
1381483c7a build: Fix test installation on debian:11 due to EOL (#8200) 2026-09-08 22:49:16 +00:00
75 changed files with 6067 additions and 372 deletions

View File

@@ -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: "*"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,5 @@
{
"image_tag": "sha-473fe44",
"image_tag": "sha-060957e",
"configs": {
"ubuntu": [
{

View File

@@ -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"

View File

@@ -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.

View File

@@ -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

View File

@@ -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' }}

View File

@@ -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 \
..

View File

@@ -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:

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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).

View File

@@ -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`.

View File

@@ -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

View 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())

View File

@@ -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()

View File

@@ -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

View File

@@ -8,3 +8,6 @@ crate-type = ["staticlib"]
[dependencies]
cxx.workspace = true
[lints]
workspace = true

View File

@@ -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")
}
}

View File

@@ -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,
};
//------------------------------------------------------------------------------

View File

@@ -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)

View File

@@ -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},

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)
{

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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/

View File

@@ -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
View 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
View 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
];
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -59,6 +59,10 @@ STValidation::validationFormat()
{sfBaseFeeDrops, SoeOptional},
{sfReserveBaseDrops, SoeOptional},
{sfReserveIncrementDrops, SoeOptional},
// featureSmartEscrow
{sfGasLimit, SoeOptional},
{sfBytecodeSizeLimit, SoeOptional},
{sfGasPrice, SoeOptional},
};
// clang-format on

View File

@@ -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."),

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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

View File

@@ -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:

View File

@@ -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) ==

View File

@@ -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,

View File

@@ -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);

View File

@@ -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());

View File

@@ -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());

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View 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)

View 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

View 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

View 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

View 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

View 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})

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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)

View 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

View 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

View 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.