mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-17 15:47:02 +00:00
Compare commits
2 Commits
develop
...
legleux/va
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06337079f0 | ||
|
|
bd8351c653 |
@@ -153,7 +153,6 @@ Checks: "-*,
|
||||
readability-use-std-min-max
|
||||
"
|
||||
# ---
|
||||
# bugprone-narrowing-conversions, # this will break a lot of code but we should enable it in the future because it can eliminate a lot of bugs
|
||||
# readability-inconsistent-declaration-parameter-name, # In this codebase this check will break a lot of arg names
|
||||
# readability-static-accessed-through-instance, # this check is probably unnecessary. It makes the code less readable
|
||||
# ---
|
||||
|
||||
@@ -14,6 +14,7 @@ libxrpl.ledger > xrpl.json
|
||||
libxrpl.ledger > xrpl.ledger
|
||||
libxrpl.ledger > xrpl.nodestore
|
||||
libxrpl.ledger > xrpl.protocol
|
||||
libxrpl.ledger > xrpl.server
|
||||
libxrpl.ledger > xrpl.shamap
|
||||
libxrpl.net > xrpl.basics
|
||||
libxrpl.net > xrpl.net
|
||||
@@ -220,6 +221,7 @@ xrpl.core > xrpl.protocol
|
||||
xrpl.json > xrpl.basics
|
||||
xrpl.ledger > xrpl.basics
|
||||
xrpl.ledger > xrpl.protocol
|
||||
xrpl.ledger > xrpl.server
|
||||
xrpl.ledger > xrpl.shamap
|
||||
xrpl.net > xrpl.basics
|
||||
xrpl.nodestore > xrpl.basics
|
||||
|
||||
2
.github/scripts/strategy-matrix/generate.py
vendored
2
.github/scripts/strategy-matrix/generate.py
vendored
@@ -20,6 +20,8 @@ _SANITIZER_SUFFIX: dict[str, str] = {
|
||||
def get_cmake_args(build_type: str, extra_args: str) -> str:
|
||||
"""Get the full list of CMake arguments for a config."""
|
||||
args = _BASE_CMAKE_ARGS.copy()
|
||||
if build_type == "Release":
|
||||
args.append("-Dassert=ON")
|
||||
if extra_args:
|
||||
args.extend(extra_args.split())
|
||||
return " ".join(args)
|
||||
|
||||
51
.github/scripts/strategy-matrix/linux.json
vendored
51
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,56 +1,12 @@
|
||||
{
|
||||
"image_tag": "sha-fe4c8ae",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
"compiler": ["gcc", "clang"],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"arch": ["amd64", "arm64"]
|
||||
},
|
||||
|
||||
{
|
||||
"compiler": ["gcc", "clang"],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"arch": ["amd64"],
|
||||
"sanitizers": ["address", "undefinedbehavior"]
|
||||
},
|
||||
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "coverage",
|
||||
"extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0"
|
||||
},
|
||||
{
|
||||
"compiler": ["clang"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "voidstar",
|
||||
"extra_cmake_args": "-Dvoidstar=ON"
|
||||
},
|
||||
{
|
||||
"compiler": ["clang"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "reffee",
|
||||
"extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=1000"
|
||||
},
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "unity",
|
||||
"extra_cmake_args": "-Dunity=ON",
|
||||
"exclude_event_types": ["pull_request"]
|
||||
}
|
||||
],
|
||||
|
||||
"debian": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"]
|
||||
"arch": ["amd64"],
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -58,7 +14,8 @@
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"]
|
||||
"arch": ["amd64"],
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
61
.github/workflows/on-pr.yml
vendored
61
.github/workflows/on-pr.yml
vendored
@@ -67,6 +67,7 @@ jobs:
|
||||
.github/workflows/reusable-package.yml
|
||||
.github/workflows/reusable-strategy-matrix.yml
|
||||
.github/workflows/reusable-test.yml
|
||||
.github/workflows/reusable-test-conan-package.yml
|
||||
.github/workflows/reusable-upload-recipe.yml
|
||||
.clang-tidy
|
||||
.codecov.yml
|
||||
@@ -77,6 +78,7 @@ jobs:
|
||||
include/**
|
||||
src/**
|
||||
tests/**
|
||||
validator-keys-tool/**
|
||||
CMakeLists.txt
|
||||
conanfile.py
|
||||
conan.lock
|
||||
@@ -103,27 +105,6 @@ jobs:
|
||||
outputs:
|
||||
go: ${{ steps.go.outputs.go == 'true' }}
|
||||
|
||||
check-levelization:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-levelization.yml
|
||||
|
||||
check-rename:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-rename.yml
|
||||
|
||||
clang-tidy:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-clang-tidy.yml
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
with:
|
||||
check_only_changed: true
|
||||
create_issue_on_failure: false
|
||||
|
||||
build-test:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
@@ -131,7 +112,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [linux, macos, windows]
|
||||
os: [linux]
|
||||
with:
|
||||
# Enable ccache only for events targeting the XRPLF repository, since
|
||||
# other accounts will not have access to our remote cache storage.
|
||||
@@ -145,43 +126,17 @@ jobs:
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-package.yml
|
||||
|
||||
upload-recipe:
|
||||
needs:
|
||||
- should-run
|
||||
- build-test
|
||||
# Only run when committing to a PR that targets a release branch.
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
|
||||
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
|
||||
|
||||
notify-clio:
|
||||
needs: upload-recipe
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Notify the Clio repository about the newly proposed release version, so
|
||||
# it can be checked for compatibility before the release is actually made.
|
||||
- name: Notify Clio
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CLIO_NOTIFY_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \
|
||||
-F "client_payload[ref]=${{ needs.upload-recipe.outputs.recipe_ref }}" \
|
||||
-F "client_payload[pr_url]=${PR_URL}"
|
||||
test-conan-package:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-test-conan-package.yml
|
||||
|
||||
passed:
|
||||
if: failure() || cancelled()
|
||||
needs:
|
||||
- check-levelization
|
||||
- check-rename
|
||||
- clang-tidy
|
||||
- build-test
|
||||
- package
|
||||
- upload-recipe
|
||||
- notify-clio
|
||||
- test-conan-package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fail
|
||||
|
||||
5
.github/workflows/on-tag.yml
vendored
5
.github/workflows/on-tag.yml
vendored
@@ -16,8 +16,13 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
test-conan-package:
|
||||
if: ${{ github.repository == 'XRPLF/rippled' }}
|
||||
uses: ./.github/workflows/reusable-test-conan-package.yml
|
||||
|
||||
upload-recipe:
|
||||
if: ${{ github.repository == 'XRPLF/rippled' }}
|
||||
needs: test-conan-package
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
|
||||
|
||||
23
.github/workflows/on-trigger.yml
vendored
23
.github/workflows/on-trigger.yml
vendored
@@ -24,6 +24,7 @@ on:
|
||||
- ".github/workflows/reusable-package.yml"
|
||||
- ".github/workflows/reusable-strategy-matrix.yml"
|
||||
- ".github/workflows/reusable-test.yml"
|
||||
- ".github/workflows/reusable-test-conan-package.yml"
|
||||
- ".github/workflows/reusable-upload-recipe.yml"
|
||||
- ".clang-tidy"
|
||||
- ".codecov.yml"
|
||||
@@ -34,6 +35,7 @@ on:
|
||||
- "include/**"
|
||||
- "src/**"
|
||||
- "tests/**"
|
||||
- "validator-keys-tool/**"
|
||||
- "CMakeLists.txt"
|
||||
- "conanfile.py"
|
||||
- "conan.lock"
|
||||
@@ -65,21 +67,12 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
clang-tidy:
|
||||
uses: ./.github/workflows/reusable-clang-tidy.yml
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
with:
|
||||
check_only_changed: false
|
||||
create_issue_on_failure: ${{ github.event_name == 'schedule' }}
|
||||
|
||||
build-test:
|
||||
uses: ./.github/workflows/reusable-build-test.yml
|
||||
strategy:
|
||||
fail-fast: ${{ github.event_name == 'merge_group' }}
|
||||
matrix:
|
||||
os: [linux, macos, windows]
|
||||
os: [linux]
|
||||
with:
|
||||
# Enable ccache only for events targeting the XRPLF repository, since
|
||||
# other accounts will not have access to our remote cache storage.
|
||||
@@ -91,14 +84,8 @@ jobs:
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
upload-recipe:
|
||||
needs: build-test
|
||||
# Only run when pushing to the develop branch.
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
|
||||
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
|
||||
test-conan-package:
|
||||
uses: ./.github/workflows/reusable-test-conan-package.yml
|
||||
|
||||
package:
|
||||
needs: build-test
|
||||
|
||||
14
.github/workflows/reusable-build-test-config.yml
vendored
14
.github/workflows/reusable-build-test-config.yml
vendored
@@ -235,6 +235,9 @@ jobs:
|
||||
run: |
|
||||
loader="$(/tmp/loader-path.sh)"
|
||||
patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/xrpld"
|
||||
if [ -x "${{ env.BUILD_DIR }}/validator-keys" ]; then
|
||||
patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/validator-keys"
|
||||
fi
|
||||
|
||||
# We're only running aarch64 Linux builds in Ubuntu-based images, so this is kept simple
|
||||
- name: Install libatomic (Linux aarch64)
|
||||
@@ -253,12 +256,21 @@ jobs:
|
||||
curl ${CCACHE_REMOTE_STORAGE%|*}/status || true
|
||||
fi
|
||||
|
||||
- name: Stage binary artifacts (Linux)
|
||||
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
mkdir -p "${BUILD_DIR}/artifacts"
|
||||
cp "${BUILD_DIR}/xrpld" "${BUILD_DIR}/artifacts/xrpld"
|
||||
if [ -x "${BUILD_DIR}/validator-keys" ]; then
|
||||
cp "${BUILD_DIR}/validator-keys" "${BUILD_DIR}/artifacts/validator-keys"
|
||||
fi
|
||||
|
||||
- name: Upload the binary (Linux)
|
||||
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: xrpld-${{ inputs.config_name }}
|
||||
path: ${{ env.BUILD_DIR }}/xrpld
|
||||
path: ${{ env.BUILD_DIR }}/artifacts/*
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
23
.github/workflows/reusable-clang-tidy.yml
vendored
23
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -20,12 +20,9 @@ env:
|
||||
BUILD_DIR: build
|
||||
BUILD_TYPE: Debug # Debug so that ASSERTS and such participate in clang-tidy check
|
||||
|
||||
OUTPUT_FILE: /tmp/clang-tidy-output.txt
|
||||
FILTERED_OUTPUT_FILE: /tmp/clang-tidy-filtered-output.txt
|
||||
DIFF_FILE: /tmp/clang-tidy-git-diff.txt
|
||||
ISSUE_FILE: /tmp/clang-tidy-issue.md
|
||||
|
||||
COMPILER: clang
|
||||
OUTPUT_FILE: clang-tidy-output.txt
|
||||
DIFF_FILE: clang-tidy-git-diff.txt
|
||||
ISSUE_FILE: clang-tidy-issue.md
|
||||
|
||||
jobs:
|
||||
determine-files:
|
||||
@@ -62,7 +59,7 @@ jobs:
|
||||
- name: Set compiler environment
|
||||
uses: ./.github/actions/set-compiler-env
|
||||
with:
|
||||
compiler: ${{ env.COMPILER }}
|
||||
compiler: clang
|
||||
|
||||
- name: Setup Conan
|
||||
uses: ./.github/actions/setup-conan
|
||||
@@ -153,21 +150,21 @@ jobs:
|
||||
run: |
|
||||
if [ -f "${OUTPUT_FILE}" ]; then
|
||||
# Extract lines containing 'error:', 'warning:', or 'note:'
|
||||
grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true
|
||||
grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >filtered-output.txt || true
|
||||
|
||||
# If filtered output is empty, use original (might be a different error format)
|
||||
if [ ! -s "${FILTERED_OUTPUT_FILE}" ]; then
|
||||
cp "${OUTPUT_FILE}" "${FILTERED_OUTPUT_FILE}"
|
||||
if [ ! -s filtered-output.txt ]; then
|
||||
cp "${OUTPUT_FILE}" filtered-output.txt
|
||||
fi
|
||||
|
||||
# Truncate if too large
|
||||
head -c 60000 "${FILTERED_OUTPUT_FILE}" >>"${ISSUE_FILE}"
|
||||
if [ "$(wc -c <"${FILTERED_OUTPUT_FILE}")" -gt 60000 ]; then
|
||||
head -c 60000 filtered-output.txt >>"${ISSUE_FILE}"
|
||||
if [ "$(wc -c <filtered-output.txt)" -gt 60000 ]; then
|
||||
echo "" >>"${ISSUE_FILE}"
|
||||
echo "... (output truncated, see artifacts for full output)" >>"${ISSUE_FILE}"
|
||||
fi
|
||||
|
||||
rm "${FILTERED_OUTPUT_FILE}"
|
||||
rm filtered-output.txt
|
||||
else
|
||||
echo "No output file found" >>"${ISSUE_FILE}"
|
||||
fi
|
||||
|
||||
4
.github/workflows/reusable-package.yml
vendored
4
.github/workflows/reusable-package.yml
vendored
@@ -77,8 +77,8 @@ jobs:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Make binary executable
|
||||
run: chmod +x "${BUILD_DIR}/xrpld"
|
||||
- name: Make binaries executable
|
||||
run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys"
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
|
||||
35
.github/workflows/reusable-test-conan-package.yml
vendored
Normal file
35
.github/workflows/reusable-test-conan-package.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Build the Conan package and run the consumer test package.
|
||||
name: Test Conan package
|
||||
|
||||
# This workflow can only be triggered by other workflows.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
test-conan-package:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Set up Conan
|
||||
uses: ./.github/actions/setup-conan
|
||||
|
||||
- name: Export Conan package under test
|
||||
run: conan export . --version=head
|
||||
|
||||
- name: Run Conan package test
|
||||
working-directory: tests/conan
|
||||
run: |
|
||||
conan test . xrpl/head \
|
||||
--profile:all ci \
|
||||
--build=missing \
|
||||
--settings:all build_type=Release \
|
||||
--conf:all tools.build:jobs="$(nproc)"
|
||||
@@ -133,9 +133,9 @@ endif()
|
||||
|
||||
include(XrplCore)
|
||||
include(XrplProtocolAutogen)
|
||||
include(XrplValidatorKeys)
|
||||
include(XrplInstall)
|
||||
include(XrplPackaging)
|
||||
include(XrplValidatorKeys)
|
||||
|
||||
if(tests)
|
||||
include(CTest)
|
||||
|
||||
@@ -25,6 +25,19 @@ if(NOT (RPMBUILD_EXECUTABLE OR DPKG_BUILDPACKAGE_EXECUTABLE))
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT TARGET xrpld)
|
||||
message(STATUS "xrpld=ON is required; 'package' target not available")
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT TARGET validator-keys)
|
||||
message(
|
||||
STATUS
|
||||
"validator_keys=ON is required; 'package' target not available"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(package_env
|
||||
SRC_DIR=${CMAKE_SOURCE_DIR}
|
||||
BUILD_DIR=${CMAKE_BINARY_DIR}
|
||||
@@ -38,7 +51,7 @@ add_custom_target(
|
||||
${CMAKE_COMMAND} -E env ${package_env}
|
||||
${CMAKE_SOURCE_DIR}/package/build_pkg.sh
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS xrpld
|
||||
DEPENDS xrpld validator-keys
|
||||
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
option(
|
||||
validator_keys
|
||||
"Enables building of validator-keys tool as a separate target (imported via FetchContent)"
|
||||
"Enables building of the vendored validator-keys tool as a separate target"
|
||||
OFF
|
||||
)
|
||||
|
||||
if(validator_keys)
|
||||
git_branch(current_branch)
|
||||
# default to tracking VK master branch unless we are on release
|
||||
if(NOT (current_branch STREQUAL "release"))
|
||||
set(current_branch "master")
|
||||
endif()
|
||||
message(STATUS "Tracking ValidatorKeys branch: ${current_branch}")
|
||||
include(GNUInstallDirs)
|
||||
|
||||
FetchContent_Declare(
|
||||
validator_keys
|
||||
GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git
|
||||
GIT_TAG "${current_branch}"
|
||||
add_subdirectory(
|
||||
"${CMAKE_SOURCE_DIR}/validator-keys-tool"
|
||||
"${CMAKE_BINARY_DIR}/validator-keys-tool"
|
||||
)
|
||||
FetchContent_MakeAvailable(validator_keys)
|
||||
set_target_properties(
|
||||
validator-keys
|
||||
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(
|
||||
TARGETS validator-keys
|
||||
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime
|
||||
)
|
||||
endif()
|
||||
|
||||
29
include/xrpl/basics/rocksdb.h
Normal file
29
include/xrpl/basics/rocksdb.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#if XRPL_ROCKSDB_AVAILABLE
|
||||
// #include <rocksdb2/port/port_posix.h>
|
||||
#include <rocksdb/cache.h>
|
||||
#include <rocksdb/compaction_filter.h>
|
||||
#include <rocksdb/comparator.h>
|
||||
#include <rocksdb/convenience.h>
|
||||
#include <rocksdb/db.h>
|
||||
#include <rocksdb/env.h>
|
||||
#include <rocksdb/filter_policy.h>
|
||||
#include <rocksdb/flush_block_policy.h>
|
||||
#include <rocksdb/iterator.h>
|
||||
#include <rocksdb/memtablerep.h>
|
||||
#include <rocksdb/merge_operator.h>
|
||||
#include <rocksdb/options.h>
|
||||
#include <rocksdb/perf_context.h>
|
||||
#include <rocksdb/slice.h>
|
||||
#include <rocksdb/slice_transform.h>
|
||||
#include <rocksdb/statistics.h>
|
||||
#include <rocksdb/status.h>
|
||||
#include <rocksdb/table.h>
|
||||
#include <rocksdb/table_properties.h>
|
||||
#include <rocksdb/transaction_log.h>
|
||||
#include <rocksdb/types.h>
|
||||
#include <rocksdb/universal_compaction.h>
|
||||
#include <rocksdb/write_batch.h>
|
||||
|
||||
#endif
|
||||
@@ -4,7 +4,7 @@
|
||||
/*
|
||||
ASAN flags some false positives with sudden jumps in control flow, like
|
||||
exceptions, or when encountering coroutine stack switches. This macro can be used to disable ASAN
|
||||
instrumentation for specific functions.
|
||||
intrumentation for specific functions.
|
||||
*/
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define XRPL_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address", "hwaddress")))
|
||||
|
||||
49
include/xrpl/ledger/BookListeners.h
Normal file
49
include/xrpl/ledger/BookListeners.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/MultiApiJson.h>
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Listen to public/subscribe messages from a book. */
|
||||
class BookListeners
|
||||
{
|
||||
public:
|
||||
using pointer = std::shared_ptr<BookListeners>;
|
||||
|
||||
BookListeners() = default;
|
||||
|
||||
/** Add a new subscription for this book
|
||||
*/
|
||||
void
|
||||
addSubscriber(InfoSub::ref sub);
|
||||
|
||||
/** Stop publishing to a subscriber
|
||||
*/
|
||||
void
|
||||
removeSubscriber(std::uint64_t sub);
|
||||
|
||||
/** Publish a transaction to subscribers
|
||||
|
||||
Publish a transaction to clients subscribed to changes on this book.
|
||||
Uses havePublished to prevent sending duplicate transactions to clients
|
||||
that have subscribed to multiple books.
|
||||
|
||||
@param jvObj JSON transaction data to publish
|
||||
@param havePublished InfoSub sequence numbers that have already
|
||||
published this transaction.
|
||||
|
||||
*/
|
||||
void
|
||||
publish(MultiApiJson const& jvObj, hash_set<std::uint64_t>& havePublished);
|
||||
|
||||
private:
|
||||
std::recursive_mutex lock_;
|
||||
|
||||
hash_map<std::uint64_t, InfoSub::wptr> listeners_;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/AcceptedLedgerTx.h>
|
||||
#include <xrpl/ledger/BookListeners.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/MultiApiJson.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <memory>
|
||||
@@ -77,24 +77,34 @@ public:
|
||||
*/
|
||||
virtual bool
|
||||
isBookToXRP(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
|
||||
|
||||
/**
|
||||
* Process a transaction for order book tracking.
|
||||
* @param ledger The ledger the transaction was applied to
|
||||
* @param alTx The transaction to process
|
||||
* @param jvObj The JSON object of the transaction
|
||||
*/
|
||||
virtual void
|
||||
processTxn(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
AcceptedLedgerTx const& alTx,
|
||||
MultiApiJson const& jvObj) = 0;
|
||||
|
||||
/**
|
||||
* Get the book listeners for a book.
|
||||
* @param book The book to get the listeners for
|
||||
* @return The book listeners for the book
|
||||
*/
|
||||
virtual BookListeners::pointer
|
||||
getBookListeners(Book const&) = 0;
|
||||
|
||||
/**
|
||||
* Create a new book listeners for a book.
|
||||
* @param book The book to create the listeners for
|
||||
* @return The new book listeners for the book
|
||||
*/
|
||||
virtual BookListeners::pointer
|
||||
makeBookListeners(Book const&) = 0;
|
||||
};
|
||||
|
||||
/** Extract the set of books affected by a transaction.
|
||||
*
|
||||
* Walks the transaction's metadata nodes and collects every order book
|
||||
* whose offers were created, modified, or deleted. Used by NetworkOPs to
|
||||
* fan transaction notifications out to book subscribers.
|
||||
*
|
||||
* @param alTx The accepted ledger transaction to inspect.
|
||||
* @param j Journal used to log per-node parsing failures. Inspecting an
|
||||
* offer node can throw if a required field is missing; in that
|
||||
* case the bad node is skipped and a warn-level message is
|
||||
* emitted via @p j. Other affected books in the same transaction
|
||||
* are still returned.
|
||||
* @return The set of books whose offers were created, modified, or
|
||||
* deleted. May be empty for non-offer transactions.
|
||||
*/
|
||||
hash_set<Book>
|
||||
affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -36,13 +36,13 @@ checkFields(STTx const& tx, beast::Journal j);
|
||||
TER
|
||||
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j);
|
||||
|
||||
// Check if subject has any credential matching the given domain. If you call it
|
||||
// Check if subject has any credential maching the given domain. If you call it
|
||||
// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in
|
||||
// doApply. This will ensure that expired credentials are deleted.
|
||||
TER
|
||||
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject);
|
||||
|
||||
// This function is only called when we are about to return tecNO_PERMISSION
|
||||
// This function is only called when we about to return tecNO_PERMISSION
|
||||
// because all the checks for the DepositPreauth authorization failed.
|
||||
TER
|
||||
authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst);
|
||||
@@ -58,7 +58,7 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
|
||||
|
||||
} // namespace credentials
|
||||
|
||||
// Check expired credentials and for credentials matching DomainID of the ledger
|
||||
// Check expired credentials and for credentials maching DomainID of the ledger
|
||||
// object
|
||||
TER
|
||||
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);
|
||||
|
||||
@@ -5,45 +5,9 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Close a payment channel and return its remaining funds to the channel owner.
|
||||
*
|
||||
* @param slep The SLE for the PayChannel object to close.
|
||||
* @param view The apply view in which ledger state modifications are made.
|
||||
* @param key The ledger key identifying the PayChannel entry.
|
||||
* @param j Journal used for fatal-level diagnostic messages.
|
||||
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
|
||||
* fails; tefINTERNAL if the source account SLE cannot be found.
|
||||
*/
|
||||
TER
|
||||
closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j);
|
||||
|
||||
/** Add two uint32_t values with saturation at UINT32_MAX.
|
||||
*
|
||||
* @param rules The current ledger rules used to check amendment status.
|
||||
* @param lhs Left-hand operand.
|
||||
* @param rhs Right-hand operand.
|
||||
* @return @p lhs + @p rhs, saturated at UINT32_MAX when the amendment
|
||||
* is active.
|
||||
*/
|
||||
uint32_t
|
||||
saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs);
|
||||
|
||||
/** Determine whether a payment channel time field represents an expired time.
|
||||
*
|
||||
* @param view The apply view providing the parent close time and rules.
|
||||
* @param timeField The optional expiry timestamp (seconds since the XRP
|
||||
* Ledger epoch). If empty, the function returns false.
|
||||
* @return @c true if @p timeField is set and the indicated time is
|
||||
* in the past relative to the view's parent close time;
|
||||
* @c false otherwise.
|
||||
*/
|
||||
bool
|
||||
isChannelExpired(ApplyView const& view, std::optional<std::uint32_t> timeField);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -102,32 +102,25 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled)
|
||||
json::Value const maxVersion(
|
||||
betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion);
|
||||
|
||||
if (!jv.isObject() || !jv.isMember(jss::api_version))
|
||||
return RPC::kApiVersionIfUnspecified;
|
||||
|
||||
try
|
||||
if (jv.isObject())
|
||||
{
|
||||
auto const& rawVersion = jv[jss::api_version];
|
||||
switch (rawVersion.type())
|
||||
if (jv.isMember(jss::api_version))
|
||||
{
|
||||
case json::ValueType::Int:
|
||||
if (rawVersion.asInt() < 0)
|
||||
return RPC::kApiInvalidVersion;
|
||||
[[fallthrough]];
|
||||
case json::ValueType::UInt: {
|
||||
auto const apiVersion = rawVersion.asUInt();
|
||||
if (apiVersion < kMinVersion || apiVersion > maxVersion)
|
||||
return RPC::kApiInvalidVersion;
|
||||
return apiVersion;
|
||||
}
|
||||
default:
|
||||
auto const specifiedVersion = jv[jss::api_version];
|
||||
if (!specifiedVersion.isInt() && !specifiedVersion.isUInt())
|
||||
{
|
||||
return RPC::kApiInvalidVersion;
|
||||
}
|
||||
auto const specifiedVersionInt = specifiedVersion.asInt();
|
||||
if (specifiedVersionInt < kMinVersion || specifiedVersionInt > maxVersion)
|
||||
{
|
||||
return RPC::kApiInvalidVersion;
|
||||
}
|
||||
return specifiedVersionInt;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return RPC::kApiInvalidVersion;
|
||||
}
|
||||
|
||||
return RPC::kApiVersionIfUnspecified;
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
@@ -27,19 +26,6 @@ public:
|
||||
};
|
||||
|
||||
/** Manages a client's subscription to data feeds.
|
||||
*
|
||||
* An InfoSub holds a non-owning reference to its `Source` (typically the
|
||||
* process-wide `NetworkOPsImp`). The destructor reaches back into the
|
||||
* `Source` to remove this subscriber from every server-side subscription
|
||||
* map.
|
||||
*
|
||||
* @note Lifetime contract: every `InfoSub` instance MUST be destroyed
|
||||
* before the backing `Source`. NetworkOPsImp shutdown drops all
|
||||
* subscriber strong refs before its own teardown to satisfy this.
|
||||
* @note Thread-safety: per-instance state is guarded by `lock_`. The
|
||||
* destructor reads tracking sets without taking `lock_` because
|
||||
* the strong-pointer ref-count is zero at destruction time, so
|
||||
* no other thread can be calling the public mutators.
|
||||
*/
|
||||
class InfoSub : public CountedObject<InfoSub>
|
||||
{
|
||||
@@ -131,43 +117,8 @@ public:
|
||||
|
||||
virtual bool
|
||||
subBook(ref ispListener, Book const&) = 0;
|
||||
|
||||
/**
|
||||
* Remove a book subscription for a live subscriber.
|
||||
*
|
||||
* Clears the book from the subscriber's own tracking set
|
||||
* (InfoSub::bookSubscriptions_) and then removes the server-side
|
||||
* entry from subBook_. Call this from RPC unsubscribe handlers.
|
||||
*
|
||||
* @param ispListener The subscriber requesting removal.
|
||||
* @param book The order book to unsubscribe from.
|
||||
* @return true if the entry was present and removed, false if the
|
||||
* subscriber was not subscribed to @p book.
|
||||
*
|
||||
* @note Thread-safety: acquires subLock_ internally.
|
||||
* @note Do NOT call from ~InfoSub(). Use unsubBookInternal instead
|
||||
* to avoid a redundant write-back to bookSubscriptions_ on a
|
||||
* partially-destroyed object.
|
||||
*/
|
||||
virtual bool
|
||||
unsubBook(ref ispListener, Book const&) = 0;
|
||||
|
||||
/**
|
||||
* Remove a book subscription during InfoSub teardown.
|
||||
*
|
||||
* Removes only the server-side entry from subBook_. Does NOT touch
|
||||
* InfoSub::bookSubscriptions_ because the InfoSub is being destroyed.
|
||||
* Called by ~InfoSub() for each book in bookSubscriptions_.
|
||||
*
|
||||
* @param uListener The sequence number of the subscriber being torn down.
|
||||
* @param book The order book entry to remove.
|
||||
* @return true if the entry was present and removed, false otherwise
|
||||
* (e.g., already removed by a concurrent RPC unsubscribe).
|
||||
*
|
||||
* @note Thread-safety: acquires subLock_ internally.
|
||||
*/
|
||||
virtual bool
|
||||
unsubBookInternal(std::uint64_t uListener, Book const&) = 0;
|
||||
unsubBook(std::uint64_t uListener, Book const&) = 0;
|
||||
|
||||
virtual bool
|
||||
subTransactions(ref ispListener) = 0;
|
||||
@@ -207,13 +158,6 @@ public:
|
||||
addRpcSub(std::string const& strUrl, ref rspEntry) = 0;
|
||||
virtual bool
|
||||
tryRemoveRpcSub(std::string const& strUrl) = 0;
|
||||
|
||||
/** Journal used by InfoSub for diagnostics that occur after the
|
||||
* owning subsystem (e.g. application-level Logs) is the only
|
||||
* surviving sink — primarily destructor-time cleanup failures.
|
||||
*/
|
||||
[[nodiscard]] virtual beast::Journal const&
|
||||
journal() const = 0;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -240,31 +184,6 @@ public:
|
||||
void
|
||||
deleteSubAccountInfo(AccountID const& account, bool rt);
|
||||
|
||||
/** Record that this subscriber is following @p book.
|
||||
*
|
||||
* Called by NetworkOPsImp::subBook so that ~InfoSub() can issue a
|
||||
* matching unsubBook for every book this subscriber is tracking,
|
||||
* keeping per-subscriber state symmetric with the server-side map.
|
||||
*
|
||||
* @param book The order book this subscriber has just subscribed to.
|
||||
* @note Idempotent: re-inserting an already-tracked book is a no-op.
|
||||
* @note Thread-safe: takes InfoSub::lock_.
|
||||
*/
|
||||
void
|
||||
insertBookSubscription(Book const& book);
|
||||
|
||||
/** Stop tracking @p book for this subscriber.
|
||||
*
|
||||
* Called by the unsubscribe RPC handler so that the book is not
|
||||
* re-unsubscribed by ~InfoSub(). Pairs with insertBookSubscription.
|
||||
*
|
||||
* @param book The order book to forget.
|
||||
* @note No-op if @p book was not previously inserted.
|
||||
* @note Thread-safe: takes InfoSub::lock_.
|
||||
*/
|
||||
void
|
||||
deleteBookSubscription(Book const& book);
|
||||
|
||||
// return false if already subscribed to this account
|
||||
bool
|
||||
insertSubAccountHistory(AccountID const& account);
|
||||
@@ -298,7 +217,6 @@ private:
|
||||
std::shared_ptr<InfoSubRequest> request_;
|
||||
std::uint64_t seq_;
|
||||
hash_set<AccountID> accountHistorySubscriptions_;
|
||||
hash_set<Book> bookSubscriptions_;
|
||||
unsigned int apiVersion_ = 0;
|
||||
|
||||
static int
|
||||
|
||||
@@ -249,19 +249,6 @@ public:
|
||||
|
||||
virtual void
|
||||
stateAccounting(json::Value& obj) = 0;
|
||||
|
||||
/** Total number of (book, subscriber) entries currently tracked.
|
||||
*
|
||||
* Counts every weak_ptr stored across every book in subBook_, NOT the
|
||||
* number of distinct subscribers and NOT the number of distinct
|
||||
* books: a single subscriber following N books contributes N entries.
|
||||
*
|
||||
* @note Diagnostic accessor; intended for tests and operator visibility
|
||||
* into per-book subscription state. The returned value is a
|
||||
* snapshot under the subscription lock.
|
||||
*/
|
||||
virtual std::size_t
|
||||
getBookSubscribersCount() = 0;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -19,10 +19,8 @@ in
|
||||
gh
|
||||
git
|
||||
git-cliff
|
||||
git-lfs
|
||||
gnumake
|
||||
gnupg # needed for signing commits & codecov/codecov-action
|
||||
graphviz
|
||||
llvmPackages_22.clang-tools
|
||||
less # needed for git diff
|
||||
mold
|
||||
@@ -35,6 +33,5 @@ in
|
||||
python3
|
||||
runClangTidy
|
||||
vim
|
||||
zip
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Linux Packaging
|
||||
|
||||
This directory contains all files needed to build RPM and Debian packages for `xrpld`.
|
||||
This directory contains all files needed to build RPM and Debian packages for
|
||||
`xrpld`. The packages also include the `validator-keys` utility.
|
||||
|
||||
## Directory layout
|
||||
|
||||
@@ -15,6 +16,7 @@ package/
|
||||
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
|
||||
xrpld.tmpfiles tmpfiles.d config (used by both RPM and DEB)
|
||||
xrpld.logrotate logrotate config (installed to /etc/logrotate.d/xrpld)
|
||||
update-xrpld auto-update script (installed to /usr/libexec/xrpld/, run by update-xrpld.timer)
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
@@ -48,16 +50,17 @@ To print the exact image tags for the current `linux.json`:
|
||||
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
|
||||
`reusable-strategy-matrix.yml` with `mode: packaging` to generate the matrix of
|
||||
`{artifact_name, os}` entries, then fan out to
|
||||
`reusable-package.yml` per entry. That workflow downloads the pre-built `xrpld`
|
||||
binary artifact, detects the package format from the container, and calls
|
||||
`build_pkg.sh` directly — no CMake configure or build step is needed inside
|
||||
the packaging job.
|
||||
`reusable-package.yml` per entry. That workflow downloads the pre-built binary
|
||||
artifact containing `xrpld` and `validator-keys`, detects the package format
|
||||
from the container, and calls `build_pkg.sh` directly — no CMake configure or
|
||||
build step is needed inside the packaging job.
|
||||
|
||||
### Locally (mirrors CI)
|
||||
|
||||
With an `xrpld` binary already built at `build/xrpld`, run the packaging step
|
||||
inside the same container CI uses. The image tag is derived from `linux.json`
|
||||
so you don't need to hardcode a SHA.
|
||||
With `xrpld` and `validator-keys` binaries already built at `build/xrpld` and
|
||||
`build/validator-keys`, run the packaging step inside the same container CI
|
||||
uses. The image tag is derived from `linux.json` so you don't need to hardcode a
|
||||
SHA.
|
||||
|
||||
```bash
|
||||
# From the repo root. Pick any image flagged with `"package": true` in
|
||||
@@ -91,6 +94,7 @@ needed, but the host toolchain replaces the pinned CI image:
|
||||
```bash
|
||||
cmake \
|
||||
-Dxrpld=ON \
|
||||
-Dvalidator_keys=ON \
|
||||
-Dxrpld_version=2.4.0-local \
|
||||
-Dtests=OFF \
|
||||
..
|
||||
@@ -110,13 +114,13 @@ to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of
|
||||
environment variable. Flags override env vars; env vars override the built-in
|
||||
defaults. Run `./package/build_pkg.sh --help` for the same table:
|
||||
|
||||
| Flag | Env var | Default | Purpose |
|
||||
| -------------------------- | ------------------- | ----------------------------- | ----------------------------------- |
|
||||
| `--src-dir DIR` | `SRC_DIR` | `$PWD` | repo root |
|
||||
| `--build-dir DIR` | `BUILD_DIR` | `$PWD/build` | directory holding pre-built `xrpld` |
|
||||
| `--pkg-version STR` | `PKG_VERSION` | parsed from `xrpld --version` | version string, e.g. `3.2.0-b1` |
|
||||
| `--pkg-release N` | `PKG_RELEASE` | `1` | package release number |
|
||||
| `--source-date-epoch SECS` | `SOURCE_DATE_EPOCH` | latest git commit ctime | reproducibility timestamp |
|
||||
| Flag | Env var | Default | Purpose |
|
||||
| -------------------------- | ------------------- | ----------------------------- | ------------------------------------ |
|
||||
| `--src-dir DIR` | `SRC_DIR` | `$PWD` | repo root |
|
||||
| `--build-dir DIR` | `BUILD_DIR` | `$PWD/build` | directory holding pre-built binaries |
|
||||
| `--pkg-version STR` | `PKG_VERSION` | parsed from `xrpld --version` | version string, e.g. `3.2.0-b1` |
|
||||
| `--pkg-release N` | `PKG_RELEASE` | `1` | package release number |
|
||||
| `--source-date-epoch SECS` | `SOURCE_DATE_EPOCH` | latest git commit ctime | reproducibility timestamp |
|
||||
|
||||
The package format (`deb` or `rpm`) is inferred from the host's package
|
||||
manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). Hosts without one of those
|
||||
@@ -141,7 +145,7 @@ into the staging area, and invokes the platform build tool.
|
||||
### DEB
|
||||
|
||||
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
|
||||
2. Stages the binary, configs, `README.md`, and `LICENSE.md`.
|
||||
2. Stages the binaries, configs, `README.md`, and `LICENSE.md`.
|
||||
3. Copies `package/debian/` control files into `debbuild/source/debian/`.
|
||||
4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically.
|
||||
5. Generates a minimal `debian/changelog` (pre-release versions use `~` instead of `-`).
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build an RPM or Debian package from a pre-built xrpld binary.
|
||||
# Build an RPM or Debian package from pre-built xrpld and validator-keys
|
||||
# binaries.
|
||||
#
|
||||
# Flags override env vars; env vars override defaults. Env vars are intended
|
||||
# for CMake/systemd/CI integration; flags are for explicit invocation.
|
||||
@@ -12,7 +13,7 @@ Usage: build_pkg.sh [options]
|
||||
|
||||
Options (each can also be set via the env var shown):
|
||||
--src-dir DIR repo root [SRC_DIR; default: $PWD]
|
||||
--build-dir DIR directory holding xrpld [BUILD_DIR; default: $PWD/build]
|
||||
--build-dir DIR directory holding binaries [BUILD_DIR; default: $PWD/build]
|
||||
--pkg-version STR version, e.g. 3.2.0-b1 [PKG_VERSION; default: parsed from xrpld --version]
|
||||
--pkg-release N package release number [PKG_RELEASE; default: 1]
|
||||
--source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; default: latest git commit ctime]
|
||||
@@ -114,11 +115,10 @@ VER_BASE="${VERSION%%-*}"
|
||||
VER_SUFFIX="${VERSION#*-}"
|
||||
[[ "${VER_SUFFIX}" == "${VERSION}" ]] && VER_SUFFIX=""
|
||||
|
||||
# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). Neither an
|
||||
# RPM Version nor a Debian upstream version may contain '-' (it's the NVR /
|
||||
# version-revision separator), and the convention here is single-token
|
||||
# suffixes like b1 or rc2. Fail early with a clear message rather than letting
|
||||
# the package tooling blow up or silently mangle dashes.
|
||||
# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). The RPM
|
||||
# Release field forbids '-', and the convention here is single-token suffixes
|
||||
# like b1 or rc2. Fail early with a clear message rather than letting either
|
||||
# rpmbuild blow up or silently mangling dashes into dots.
|
||||
if [[ "${VER_SUFFIX}" == *-* ]]; then
|
||||
echo "build_pkg.sh: multi-segment pre-release in VERSION='${VERSION}' (suffix '${VER_SUFFIX}')." >&2
|
||||
echo "Use single-token suffixes like 3.2.0-b1 or 3.2.0-rc2." >&2
|
||||
@@ -134,6 +134,7 @@ stage_common() {
|
||||
mkdir -p "${dest}"
|
||||
|
||||
cp "${BUILD_DIR}/xrpld" "${dest}/xrpld"
|
||||
cp "${BUILD_DIR}/validator-keys" "${dest}/validator-keys"
|
||||
cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg"
|
||||
cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt"
|
||||
cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md"
|
||||
@@ -143,6 +144,9 @@ stage_common() {
|
||||
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
|
||||
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
|
||||
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
|
||||
cp "${SHARED}/update-xrpld" "${dest}/update-xrpld"
|
||||
cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service"
|
||||
cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer"
|
||||
cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset"
|
||||
}
|
||||
|
||||
@@ -154,18 +158,20 @@ build_rpm() {
|
||||
cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec"
|
||||
stage_common "${topdir}/SOURCES"
|
||||
|
||||
# Pre-releases use the modern rpm '~' convention (rpm >= 4.10): the suffix
|
||||
# goes in Version (e.g. 3.2.0~b1), which rpmvercmp sorts *before* the final
|
||||
# 3.2.0 — identical semantics to Debian's '~'. Release is just the package
|
||||
# release number. This replaces the older "0.<release>.<suffix>" Release
|
||||
# hack and keeps the RPM and DEB version strings symmetric.
|
||||
local rpm_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}"
|
||||
# RPM Version can't contain '-'. A pre-release goes in Release with a
|
||||
# leading "0." so 3.2.0-b1 sorts before the final 3.2.0-<pkg_release>.
|
||||
# The order is "0.<pkg_release>.<suffix>" (e.g. 0.1.b6) — the Fedora/EPEL
|
||||
# convention. Reversing to "0.<suffix>.<pkg_release>" (e.g. 0.b6.1) breaks
|
||||
# rpmvercmp against the former because numeric segments outrank alphabetic
|
||||
# ones, so "0.1.b5" would sort newer than "0.b6.1".
|
||||
local rpm_release="${PKG_RELEASE}"
|
||||
[[ -n "${VER_SUFFIX}" ]] && rpm_release="0.${PKG_RELEASE}.${VER_SUFFIX}"
|
||||
|
||||
set -x
|
||||
rpmbuild -bb \
|
||||
--define "_topdir ${topdir}" \
|
||||
--define "xrpld_version ${rpm_version}" \
|
||||
--define "xrpld_release ${PKG_RELEASE}" \
|
||||
--define "xrpld_version ${VER_BASE}" \
|
||||
--define "xrpld_release ${rpm_release}" \
|
||||
"${topdir}/SPECS/xrpld.spec"
|
||||
}
|
||||
|
||||
@@ -177,10 +183,13 @@ build_deb() {
|
||||
stage_common "${staging}"
|
||||
cp -r "${DEBIAN_DIR}" "${staging}/debian"
|
||||
|
||||
# Debhelper auto-discovers these only from debian/.
|
||||
cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service"
|
||||
cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers"
|
||||
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
|
||||
cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate"
|
||||
cp "${staging}/update-xrpld.service" "${staging}/debian/xrpld.update-xrpld.service"
|
||||
cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer"
|
||||
|
||||
# Debian '~' marks a pre-release; 3.2.0~b1 sorts before 3.2.0.
|
||||
local deb_full_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}-${PKG_RELEASE}"
|
||||
|
||||
@@ -20,4 +20,5 @@ Depends:
|
||||
Description: XRP Ledger daemon
|
||||
Reference implementation of the XRP Ledger protocol.
|
||||
Participates in the peer-to-peer network, processes transactions,
|
||||
and maintains a local ledger copy.
|
||||
and maintains a local ledger copy. Includes validator-keys for
|
||||
validator key management.
|
||||
|
||||
@@ -10,6 +10,7 @@ override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
|
||||
|
||||
override_dh_installsystemd:
|
||||
dh_installsystemd --no-stop-on-upgrade xrpld.service
|
||||
dh_installsystemd --name=update-xrpld --no-enable --no-start update-xrpld.service update-xrpld.timer
|
||||
|
||||
execute_before_dh_installtmpfiles:
|
||||
dh_installsysusers
|
||||
@@ -18,8 +19,10 @@ override_dh_installsysusers:
|
||||
|
||||
override_dh_install:
|
||||
install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld
|
||||
install -D -m 0755 validator-keys debian/xrpld/usr/bin/validator-keys
|
||||
install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg
|
||||
install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt
|
||||
install -D -m 0755 update-xrpld debian/xrpld/usr/libexec/xrpld/update-xrpld
|
||||
|
||||
override_dh_dwz:
|
||||
@:
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
README.md
|
||||
LICENSE.md
|
||||
|
||||
@@ -21,6 +21,8 @@ BuildRequires: systemd-rpm-macros
|
||||
xrpld is the reference implementation of the XRP Ledger protocol. It
|
||||
participates in the peer-to-peer XRP Ledger network, processes
|
||||
transactions, and maintains the ledger database.
|
||||
This package also includes the validator-keys tool for validator key
|
||||
management.
|
||||
|
||||
%prep
|
||||
:
|
||||
@@ -30,11 +32,14 @@ transactions, and maintains the ledger database.
|
||||
|
||||
%install
|
||||
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{name}
|
||||
install -Dm0755 %{_sourcedir}/validator-keys %{buildroot}%{_bindir}/validator-keys
|
||||
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{name}/xrpld.cfg
|
||||
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{name}/validators.txt
|
||||
|
||||
# systemd units, sysusers, tmpfiles, preset
|
||||
install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
|
||||
install -Dm0644 %{_sourcedir}/update-xrpld.service %{buildroot}%{_unitdir}/update-xrpld.service
|
||||
install -Dm0644 %{_sourcedir}/update-xrpld.timer %{buildroot}%{_unitdir}/update-xrpld.timer
|
||||
install -Dm0644 %{_sourcedir}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
|
||||
install -Dm0644 %{_sourcedir}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
|
||||
install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50-xrpld.preset
|
||||
@@ -42,6 +47,9 @@ install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50-
|
||||
# Logrotate config
|
||||
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name}
|
||||
|
||||
# Update helper
|
||||
install -Dm0755 %{_sourcedir}/update-xrpld %{buildroot}%{_libexecdir}/%{name}/update-xrpld
|
||||
|
||||
# Docs
|
||||
install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md
|
||||
install -Dm0644 %{_sourcedir}/README.md %{buildroot}%{_docdir}/%{name}/README.md
|
||||
@@ -56,10 +64,10 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
|
||||
|
||||
%post
|
||||
systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
%systemd_post xrpld.service
|
||||
%systemd_post xrpld.service update-xrpld.timer
|
||||
|
||||
%preun
|
||||
%systemd_preun xrpld.service
|
||||
%systemd_preun xrpld.service update-xrpld.timer
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart xrpld.service
|
||||
@@ -69,20 +77,27 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
|
||||
%doc %{_docdir}/%{name}/README.md
|
||||
|
||||
%dir %{_sysconfdir}/%{name}
|
||||
%dir %{_libexecdir}/%{name}
|
||||
|
||||
%{_bindir}/%{name}
|
||||
%{_bindir}/validator-keys
|
||||
|
||||
%config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg
|
||||
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt
|
||||
%config(noreplace) %{_sysconfdir}/logrotate.d/%{name}
|
||||
|
||||
%{_libexecdir}/%{name}/update-xrpld
|
||||
|
||||
%{_unitdir}/xrpld.service
|
||||
%{_unitdir}/update-xrpld.service
|
||||
%{_unitdir}/update-xrpld.timer
|
||||
%{_presetdir}/50-xrpld.preset
|
||||
%{_sysusersdir}/xrpld.conf
|
||||
%{_tmpfilesdir}/xrpld.conf
|
||||
%ghost %dir /var/lib/xrpld
|
||||
%ghost %dir /var/log/xrpld
|
||||
|
||||
%ghost %dir /var/lib/%{name}
|
||||
%ghost %dir /var/log/%{name}
|
||||
|
||||
|
||||
# Legacy compatibility for pre-FHS package layouts.
|
||||
# TODO: remove after rippled fully deprecated.
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# /usr/lib/systemd/system-preset/50-xrpld.preset
|
||||
enable xrpld.service
|
||||
# Don't enable automatic updates
|
||||
disable update-xrpld.timer
|
||||
|
||||
152
package/shared/update-xrpld
Executable file
152
package/shared/update-xrpld
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Optional: also write logs to a legacy file in addition to journald.
|
||||
# By default, this script logs to systemd/journald, viewable via:
|
||||
# journalctl -t update-xrpld
|
||||
#
|
||||
# Uncomment the line below if you need a flat file for compatibility with
|
||||
# external tooling, manual inspection, or environments where journald logs
|
||||
# are not persisted or easily accessible.
|
||||
#
|
||||
# Note: This duplicates all output (stdout/stderr) to both journald and the file.
|
||||
# It is generally not needed on modern systems and may cause log file growth
|
||||
# if left enabled long-term.
|
||||
#
|
||||
# Requires /var/log/xrpld/ to exist and be writable by the service (root).
|
||||
#
|
||||
# exec > >(tee -a /var/log/xrpld/update.log) 2>&1
|
||||
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
PKG_NAME=${PKG_NAME:-xrpld}
|
||||
|
||||
log() {
|
||||
# If running under systemd/journald, let it handle timestamps.
|
||||
if [[ -n "${JOURNAL_STREAM:-}" ]]; then
|
||||
printf '%s\n' "$*"
|
||||
else
|
||||
printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"
|
||||
fi
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
|
||||
log "RESULT: failed reason=not-root"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
get_installed_version() {
|
||||
if command -v dpkg-query >/dev/null 2>&1; then
|
||||
dpkg-query -W -f='${Version}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
|
||||
elif command -v rpm >/dev/null 2>&1; then
|
||||
rpm -q --qf '%{VERSION}-%{RELEASE}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
|
||||
else
|
||||
printf 'unknown'
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'log "RESULT: failed reason=script-error exit_code=$?"' ERR
|
||||
|
||||
apt_can_update() {
|
||||
apt-get update -qq
|
||||
apt-get -s --only-upgrade install "$PKG_NAME" 2>/dev/null | grep -q "^Inst ${PKG_NAME}\b"
|
||||
}
|
||||
|
||||
apt_apply_update() {
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
||||
-o Dpkg::Options::="--force-confdef" \
|
||||
-o Dpkg::Options::="--force-confold" \
|
||||
"$PKG_NAME"
|
||||
}
|
||||
|
||||
get_rpm_pm() {
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
printf 'dnf\n'
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
printf 'yum\n'
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
rpm_refresh_metadata() {
|
||||
local pm=$1
|
||||
if [[ "$pm" == "dnf" ]]; then
|
||||
dnf makecache --refresh -q >/dev/null
|
||||
else
|
||||
yum clean expire-cache -q >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
rpm_can_update() {
|
||||
local pm=$1
|
||||
|
||||
rpm_refresh_metadata "$pm"
|
||||
local rc=0
|
||||
set +e
|
||||
"$pm" check-update -q "$PKG_NAME" >/dev/null 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
if [[ $rc -eq 100 ]]; then
|
||||
return 0
|
||||
elif [[ $rc -eq 0 ]]; then
|
||||
return 1
|
||||
else
|
||||
log "$pm check-update failed with exit code ${rc}."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
rpm_apply_update() {
|
||||
local pm=$1
|
||||
"$pm" update -y "$PKG_NAME"
|
||||
}
|
||||
|
||||
restart_service() {
|
||||
# Preserve the operator's prior service state: if xrpld was intentionally
|
||||
# stopped before the update, don't bring it back up just because the
|
||||
# auto-update timer fired.
|
||||
if systemctl is-active --quiet "${PKG_NAME}.service"; then
|
||||
systemctl restart "${PKG_NAME}.service"
|
||||
log "${PKG_NAME} service restarted successfully."
|
||||
else
|
||||
log "${PKG_NAME} service was not running; skipping restart to preserve prior state."
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
log "Checking for ${PKG_NAME} updates via apt"
|
||||
if apt_can_update; then
|
||||
log "Update available; installing."
|
||||
apt_apply_update
|
||||
restart_service
|
||||
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
|
||||
else
|
||||
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
local rpm_pm=""
|
||||
if rpm_pm="$(get_rpm_pm)"; then
|
||||
log "Checking for ${PKG_NAME} updates via ${rpm_pm}"
|
||||
if rpm_can_update "$rpm_pm"; then
|
||||
log "Update available; installing"
|
||||
rpm_apply_update "$rpm_pm"
|
||||
restart_service
|
||||
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
|
||||
else
|
||||
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
log "RESULT: failed reason=no-package-manager"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
16
package/shared/update-xrpld.service
Normal file
16
package/shared/update-xrpld.service
Normal file
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Check for and install xrpld package updates
|
||||
Documentation=man:systemd.service(5)
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
ConditionPathExists=/usr/libexec/xrpld/update-xrpld
|
||||
ConditionPathExists=/usr/bin/xrpld
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/flock -n /run/lock/xrpld-update.lock /usr/libexec/xrpld/update-xrpld
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=update-xrpld
|
||||
TimeoutStartSec=30min
|
||||
PrivateTmp=true
|
||||
10
package/shared/update-xrpld.timer
Normal file
10
package/shared/update-xrpld.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Daily xrpld update check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 00:00:00
|
||||
RandomizedDelaySec=4h
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -18,11 +18,6 @@ PrivateTmp=true
|
||||
User=xrpld
|
||||
Group=xrpld
|
||||
LimitNOFILE=65536
|
||||
SystemCallArchitectures=native
|
||||
|
||||
# Uncomment both lines to allow xrpld to bind to privileged ports (<1024)
|
||||
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
#AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
55
src/libxrpl/ledger/BookListeners.cpp
Normal file
55
src/libxrpl/ledger/BookListeners.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#include <xrpl/ledger/BookListeners.h>
|
||||
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/MultiApiJson.h>
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
BookListeners::addSubscriber(InfoSub::ref sub)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
listeners_[sub->getSeq()] = sub;
|
||||
}
|
||||
|
||||
void
|
||||
BookListeners::removeSubscriber(std::uint64_t seq)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
listeners_.erase(seq);
|
||||
}
|
||||
|
||||
void
|
||||
BookListeners::publish(MultiApiJson const& jvObj, hash_set<std::uint64_t>& havePublished)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
auto it = listeners_.cbegin();
|
||||
|
||||
while (it != listeners_.cend())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
// Only publish jvObj if this is the first occurrence
|
||||
if (havePublished.emplace(p->getSeq()).second)
|
||||
{
|
||||
jvObj.visit(
|
||||
p->getApiVersion(), //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
}
|
||||
++it;
|
||||
}
|
||||
else
|
||||
{
|
||||
it = listeners_.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -5,20 +5,13 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
TER
|
||||
@@ -66,28 +59,4 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs)
|
||||
{
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
static constexpr auto kUint32Max =
|
||||
static_cast<uint64_t>(std::numeric_limits<uint32_t>::max());
|
||||
uint64_t const saturatedResult = std::min(uint64_t{lhs} + rhs, kUint32Max);
|
||||
return static_cast<uint32_t>(saturatedResult);
|
||||
}
|
||||
|
||||
return lhs + rhs;
|
||||
}
|
||||
|
||||
bool
|
||||
isChannelExpired(ApplyView const& view, std::optional<uint32_t> timeField)
|
||||
{
|
||||
if (!timeField)
|
||||
return false;
|
||||
if (view.rules().enabled(fixCleanup3_2_0))
|
||||
return after(view.header().parentCloseTime, *timeField);
|
||||
return view.header().parentCloseTime.time_since_epoch().count() >= *timeField;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -45,10 +45,8 @@ ManagerImp::missingBackend()
|
||||
// the Factory classes is an undefined behaviour.
|
||||
void
|
||||
registerNuDBFactory(Manager& manager);
|
||||
#if XRPL_ROCKSDB_AVAILABLE
|
||||
void
|
||||
registerRocksDBFactory(Manager& manager);
|
||||
#endif
|
||||
void
|
||||
registerNullFactory(Manager& manager);
|
||||
void
|
||||
@@ -57,9 +55,7 @@ registerMemoryFactory(Manager& manager);
|
||||
ManagerImp::ManagerImp()
|
||||
{
|
||||
registerNuDBFactory(*this);
|
||||
#if XRPL_ROCKSDB_AVAILABLE
|
||||
registerRocksDBFactory(*this);
|
||||
#endif
|
||||
registerNullFactory(*this);
|
||||
registerMemoryFactory(*this);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
#if XRPL_ROCKSDB_AVAILABLE
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
#include <xrpl/beast/core/CurrentThreadName.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/nodestore/Backend.h>
|
||||
#include <xrpl/nodestore/Factory.h>
|
||||
#include <xrpl/nodestore/Manager.h>
|
||||
#include <xrpl/nodestore/NodeObject.h>
|
||||
#include <xrpl/nodestore/Scheduler.h>
|
||||
#include <xrpl/nodestore/Types.h>
|
||||
#include <xrpl/nodestore/detail/BatchWriter.h>
|
||||
#include <xrpl/nodestore/detail/DecodedBlob.h>
|
||||
#include <xrpl/nodestore/detail/EncodedBlob.h>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
@@ -35,14 +25,26 @@
|
||||
#include <rocksdb/table.h>
|
||||
#include <rocksdb/write_batch.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#if XRPL_ROCKSDB_AVAILABLE
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
#include <xrpl/beast/core/CurrentThreadName.h>
|
||||
#include <xrpl/nodestore/Factory.h>
|
||||
#include <xrpl/nodestore/Manager.h>
|
||||
#include <xrpl/nodestore/detail/BatchWriter.h>
|
||||
#include <xrpl/nodestore/detail/DecodedBlob.h>
|
||||
#include <xrpl/nodestore/detail/EncodedBlob.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::NodeStore {
|
||||
|
||||
class RocksDBEnv : public rocksdb::EnvWrapper
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.3.0-b0"
|
||||
char const* const versionString = "3.2.0-rc3"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -1,47 +1,15 @@
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
// Wraps a Source teardown call so that an exception from one cleanup
|
||||
// step does not prevent the subsequent steps from running. Source methods
|
||||
// acquire a lock and can throw std::system_error; a throw out of ~InfoSub
|
||||
// during stack unwinding would terminate the process. Failures are
|
||||
// reported through the Source's Journal so they reach the configured log
|
||||
// sinks; JLOG itself cannot throw, so the noexcept guarantee holds.
|
||||
template <typename F>
|
||||
void
|
||||
safeUnsub(std::uint64_t seq, F&& f, beast::Journal j) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
f();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: " << e.what();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: unknown exception";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// This is the primary interface into the "client" portion of the program.
|
||||
// Code that wants to do normal operations on the network such as
|
||||
// creating and monitoring accounts, creating transactions, and so on
|
||||
@@ -64,44 +32,25 @@ InfoSub::InfoSub(Source& source, Consumer consumer)
|
||||
|
||||
InfoSub::~InfoSub()
|
||||
{
|
||||
// Each Source teardown call below acquires a server-side lock and
|
||||
// can throw. Wrap each independent call so partial failure does not
|
||||
// skip the remaining teardown steps.
|
||||
|
||||
auto const& j = source_.journal();
|
||||
|
||||
safeUnsub(seq_, [&] { source_.unsubTransactions(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubRTTransactions(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubLedger(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubManifests(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubServer(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubValidations(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j);
|
||||
safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j);
|
||||
source_.unsubTransactions(seq_);
|
||||
source_.unsubRTTransactions(seq_);
|
||||
source_.unsubLedger(seq_);
|
||||
source_.unsubManifests(seq_);
|
||||
source_.unsubServer(seq_);
|
||||
source_.unsubValidations(seq_);
|
||||
source_.unsubPeerStatus(seq_);
|
||||
source_.unsubConsensus(seq_);
|
||||
|
||||
// Use the internal unsubscribe so that it won't call
|
||||
// back to us and modify its own parameter
|
||||
if (!realTimeSubscriptions_.empty())
|
||||
{
|
||||
safeUnsub(
|
||||
seq_, [&] { source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true); }, j);
|
||||
}
|
||||
source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true);
|
||||
|
||||
if (!normalSubscriptions_.empty())
|
||||
{
|
||||
safeUnsub(
|
||||
seq_, [&] { source_.unsubAccountInternal(seq_, normalSubscriptions_, false); }, j);
|
||||
}
|
||||
source_.unsubAccountInternal(seq_, normalSubscriptions_, false);
|
||||
|
||||
for (auto const& account : accountHistorySubscriptions_)
|
||||
{
|
||||
safeUnsub(seq_, [&] { source_.unsubAccountHistoryInternal(seq_, account, false); }, j);
|
||||
}
|
||||
|
||||
for (auto const& book : bookSubscriptions_)
|
||||
{
|
||||
safeUnsub(seq_, [&] { source_.unsubBookInternal(seq_, book); }, j);
|
||||
}
|
||||
source_.unsubAccountHistoryInternal(seq_, account, false);
|
||||
}
|
||||
|
||||
Resource::Consumer&
|
||||
@@ -165,20 +114,6 @@ InfoSub::deleteSubAccountHistory(AccountID const& account)
|
||||
accountHistorySubscriptions_.erase(account);
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::insertBookSubscription(Book const& book)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
bookSubscriptions_.insert(book);
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::deleteBookSubscription(Book const& book)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
bookSubscriptions_.erase(book);
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::clearRequest()
|
||||
{
|
||||
|
||||
@@ -42,9 +42,6 @@ PaymentChannelClaim::getFlagsMask(PreflightContext const&)
|
||||
NotTEC
|
||||
PaymentChannelClaim::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero)
|
||||
return temMALFORMED;
|
||||
|
||||
auto const bal = ctx.tx[~sfBalance];
|
||||
if (bal && (!isXRP(*bal) || *bal <= beast::kZero))
|
||||
return temBAD_AMOUNT;
|
||||
@@ -119,10 +116,12 @@ PaymentChannelClaim::doApply()
|
||||
AccountID const txAccount = ctx_.tx[sfAccount];
|
||||
|
||||
auto const curExpiration = (*slep)[~sfExpiration];
|
||||
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
|
||||
isChannelExpired(ctx_.view(), curExpiration))
|
||||
{
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
auto const cancelAfter = (*slep)[~sfCancelAfter];
|
||||
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
|
||||
if ((cancelAfter && closeTime >= *cancelAfter) ||
|
||||
(curExpiration && closeTime >= *curExpiration))
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
}
|
||||
|
||||
if (txAccount != src && txAccount != dst)
|
||||
@@ -135,19 +134,13 @@ PaymentChannelClaim::doApply()
|
||||
auto const reqBalance = ctx_.tx[sfBalance].xrp();
|
||||
|
||||
if (txAccount == dst && !ctx_.tx[~sfSignature])
|
||||
{
|
||||
return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION}
|
||||
: TER{temBAD_SIGNATURE};
|
||||
}
|
||||
return temBAD_SIGNATURE;
|
||||
|
||||
if (ctx_.tx[~sfSignature])
|
||||
{
|
||||
PublicKey const pk((*slep)[sfPublicKey]);
|
||||
if (ctx_.tx[sfPublicKey] != pk)
|
||||
{
|
||||
return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION}
|
||||
: TER{temBAD_SIGNER};
|
||||
}
|
||||
return temBAD_SIGNER;
|
||||
}
|
||||
|
||||
if (reqBalance > chanFunds)
|
||||
@@ -191,10 +184,9 @@ PaymentChannelClaim::doApply()
|
||||
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
|
||||
auto const settleExpiration = saturatingAdd(
|
||||
ctx_.view().rules(),
|
||||
ctx_.view().header().parentCloseTime.time_since_epoch().count(),
|
||||
(*slep)[sfSettleDelay]);
|
||||
auto const settleExpiration =
|
||||
ctx_.view().header().parentCloseTime.time_since_epoch().count() +
|
||||
(*slep)[sfSettleDelay];
|
||||
|
||||
if (!curExpiration || *curExpiration > settleExpiration)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
@@ -30,9 +29,6 @@ PaymentChannelFund::makeTxConsequences(PreflightContext const& ctx)
|
||||
NotTEC
|
||||
PaymentChannelFund::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero)
|
||||
return temMALFORMED;
|
||||
|
||||
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
@@ -49,12 +45,13 @@ PaymentChannelFund::doApply()
|
||||
|
||||
AccountID const src = (*slep)[sfAccount];
|
||||
auto const txAccount = ctx_.tx[sfAccount];
|
||||
auto const curExpiration = (*slep)[~sfExpiration];
|
||||
auto const expiration = (*slep)[~sfExpiration];
|
||||
|
||||
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
|
||||
isChannelExpired(ctx_.view(), curExpiration))
|
||||
{
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
auto const cancelAfter = (*slep)[~sfCancelAfter];
|
||||
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
|
||||
if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration))
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
}
|
||||
|
||||
if (src != txAccount)
|
||||
@@ -63,21 +60,16 @@ PaymentChannelFund::doApply()
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (auto newExpiration = ctx_.tx[~sfExpiration])
|
||||
if (auto extend = ctx_.tx[~sfExpiration])
|
||||
{
|
||||
auto minExpiration = saturatingAdd(
|
||||
ctx_.view().rules(),
|
||||
ctx_.view().header().parentCloseTime.time_since_epoch().count(),
|
||||
(*slep)[sfSettleDelay]);
|
||||
if (curExpiration && *curExpiration < minExpiration)
|
||||
minExpiration = *curExpiration;
|
||||
auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() +
|
||||
(*slep)[sfSettleDelay];
|
||||
if (expiration && *expiration < minExpiration)
|
||||
minExpiration = *expiration;
|
||||
|
||||
if (*newExpiration < minExpiration)
|
||||
{
|
||||
return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION}
|
||||
: TER{temBAD_EXPIRATION};
|
||||
}
|
||||
(*slep)[~sfExpiration] = *newExpiration;
|
||||
if (*extend < minExpiration)
|
||||
return temBAD_EXPIRATION;
|
||||
(*slep)[~sfExpiration] = *extend;
|
||||
ctx_.view().update(slep);
|
||||
}
|
||||
|
||||
|
||||
@@ -1990,10 +1990,7 @@ public:
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
// fixCleanup3_2_0 changes payment-channel error codes (tem* -> tec*)
|
||||
// and channel-closing semantics. This suite asserts the
|
||||
// pre-amendment behavior, so run it with the amendment disabled.
|
||||
FeatureBitset const all{testableAmendments() - fixCleanup3_2_0};
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
testWithFeats(all);
|
||||
testDepositAuthCreds();
|
||||
testMetaAndOwnership(all - fixIncludeKeyletFields);
|
||||
|
||||
@@ -21,16 +21,11 @@
|
||||
#include <nudb/file.hpp>
|
||||
#include <nudb/native_file.hpp>
|
||||
#include <nudb/xxhasher.hpp>
|
||||
|
||||
#if XRPL_ROCKSDB_AVAILABLE
|
||||
|
||||
#include <rocksdb/db.h>
|
||||
#include <rocksdb/iterator.h>
|
||||
#include <rocksdb/options.h>
|
||||
#include <rocksdb/status.h>
|
||||
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
@@ -100,17 +100,6 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite
|
||||
return lastSentMessage_;
|
||||
}
|
||||
|
||||
// Synchronous test access to the JobQueue-dispatched processor.
|
||||
// The production path runs this on JtLedgerReq; tests need a
|
||||
// synchronous entry point to inspect the reply via send().
|
||||
// PeerImp::processGetObjectByHash is `protected` so the derived
|
||||
// test subclass can call it directly.
|
||||
void
|
||||
runProcessGetObjectByHash(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
|
||||
{
|
||||
processGetObjectByHash(m);
|
||||
}
|
||||
|
||||
static void
|
||||
resetId()
|
||||
{
|
||||
@@ -190,10 +179,6 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite
|
||||
/**
|
||||
* Test that reply is limited to hardMaxReplyNodes when more objects
|
||||
* are requested than the limit allows.
|
||||
*
|
||||
* `onMessage(TMGetObjectByHash)` dispatches the generic-query path
|
||||
* to the JobQueue, so tests invoke the synchronous processor
|
||||
* directly via `runProcessGetObjectByHash`.
|
||||
*/
|
||||
void
|
||||
testReplyLimit(size_t const numObjects, int const expectedReplySize)
|
||||
@@ -206,7 +191,8 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite
|
||||
auto peer = createPeer(env);
|
||||
|
||||
auto request = createRequest(numObjects, env);
|
||||
peer->runProcessGetObjectByHash(request);
|
||||
// Call the onMessage handler
|
||||
peer->onMessage(request);
|
||||
|
||||
// Verify that a reply was sent
|
||||
auto sentMessage = peer->getLastSentMessage();
|
||||
|
||||
@@ -9,17 +9,19 @@
|
||||
#include <xrpl/core/JobQueue.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/AcceptedLedgerTx.h>
|
||||
#include <xrpl/ledger/BookListeners.h>
|
||||
#include <xrpl/ledger/OrderBookDB.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MultiApiJson.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
#include <xrpl/shamap/SHAMapMissingNode.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -305,10 +307,55 @@ OrderBookDBImpl::isBookToXRP(Asset const& asset, std::optional<Domain> const& do
|
||||
return xrpBooks_.contains(asset);
|
||||
}
|
||||
|
||||
hash_set<Book>
|
||||
affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j)
|
||||
BookListeners::pointer
|
||||
OrderBookDBImpl::makeBookListeners(Book const& book)
|
||||
{
|
||||
hash_set<Book> result;
|
||||
std::scoped_lock const sl(lock_);
|
||||
auto ret = getBookListeners(book);
|
||||
|
||||
if (!ret)
|
||||
{
|
||||
ret = std::make_shared<BookListeners>();
|
||||
|
||||
listeners_[book] = ret;
|
||||
XRPL_ASSERT(
|
||||
getBookListeners(book) == ret,
|
||||
"xrpl::OrderBookDB::makeBookListeners : result roundtrip "
|
||||
"lookup");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
BookListeners::pointer
|
||||
OrderBookDBImpl::getBookListeners(Book const& book)
|
||||
{
|
||||
BookListeners::pointer ret;
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
auto it0 = listeners_.find(book);
|
||||
if (it0 != listeners_.end())
|
||||
ret = it0->second;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Based on the meta, send the meta to the streams that are listening.
|
||||
// We need to determine which streams a given meta effects.
|
||||
void
|
||||
OrderBookDBImpl::processTxn(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
AcceptedLedgerTx const& alTx,
|
||||
MultiApiJson const& jvObj)
|
||||
{
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
// For this particular transaction, maintain the set of unique
|
||||
// subscriptions that have already published it. This prevents sending
|
||||
// the transaction multiple times if it touches multiple ltOFFER
|
||||
// entries for the same book, or if it touches multiple books and a
|
||||
// single client has subscribed to those books.
|
||||
hash_set<std::uint64_t> havePublished;
|
||||
|
||||
for (auto const& node : alTx.getMeta().getNodes())
|
||||
{
|
||||
@@ -316,41 +363,40 @@ affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j)
|
||||
{
|
||||
if (node.getFieldU16(sfLedgerEntryType) == ltOFFER)
|
||||
{
|
||||
auto extract = [&](SField const& field) {
|
||||
auto process = [&, this](SField const& field) {
|
||||
if (auto data = dynamic_cast<STObject const*>(node.peekAtPField(field)); data &&
|
||||
data->isFieldPresent(sfTakerPays) && data->isFieldPresent(sfTakerGets))
|
||||
{
|
||||
result.emplace(
|
||||
data->getFieldAmount(sfTakerGets).asset(),
|
||||
data->getFieldAmount(sfTakerPays).asset(),
|
||||
(*data)[~sfDomainID]);
|
||||
auto listeners = getBookListeners(
|
||||
{data->getFieldAmount(sfTakerGets).asset(),
|
||||
data->getFieldAmount(sfTakerPays).asset(),
|
||||
(*data)[~sfDomainID]});
|
||||
if (listeners)
|
||||
listeners->publish(jvObj, havePublished);
|
||||
}
|
||||
};
|
||||
|
||||
// We need a field that contains the TakerGets and TakerPays
|
||||
// parameters.
|
||||
if (node.getFName() == sfModifiedNode)
|
||||
{
|
||||
extract(sfPreviousFields);
|
||||
process(sfPreviousFields);
|
||||
}
|
||||
else if (node.getFName() == sfCreatedNode)
|
||||
{
|
||||
extract(sfNewFields);
|
||||
process(sfNewFields);
|
||||
}
|
||||
else if (node.getFName() == sfDeletedNode)
|
||||
{
|
||||
extract(sfFinalFields);
|
||||
process(sfFinalFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
// The bad node is skipped; other affected books in the same
|
||||
// transaction are still returned. Logged at warn so a malformed
|
||||
// offer node is visible to operators.
|
||||
JLOG(j.warn()) << "affectedBooks: skipping malformed node (" << ex.what() << ")";
|
||||
JLOG(j_.info()) << "processTxn: field not found (" << ex.what() << ")";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/AcceptedLedgerTx.h>
|
||||
#include <xrpl/ledger/BookListeners.h>
|
||||
#include <xrpl/ledger/OrderBookDB.h>
|
||||
#include <xrpl/protocol/MultiApiJson.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <mutex>
|
||||
@@ -51,6 +54,18 @@ public:
|
||||
void
|
||||
update(std::shared_ptr<ReadView const> const& ledger);
|
||||
|
||||
// see if this txn effects any orderbook
|
||||
void
|
||||
processTxn(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
AcceptedLedgerTx const& alTx,
|
||||
MultiApiJson const& jvObj) override;
|
||||
|
||||
BookListeners::pointer
|
||||
getBookListeners(Book const&) override;
|
||||
BookListeners::pointer
|
||||
makeBookListeners(Book const&) override;
|
||||
|
||||
private:
|
||||
std::reference_wrapper<ServiceRegistry> registry_;
|
||||
int const pathSearchMax_;
|
||||
@@ -69,6 +84,10 @@ private:
|
||||
|
||||
std::recursive_mutex lock_;
|
||||
|
||||
using BookToListenersMap = hash_map<Book, BookListeners::pointer>;
|
||||
|
||||
BookToListenersMap listeners_;
|
||||
|
||||
std::atomic<std::uint32_t> seq_;
|
||||
|
||||
beast::Journal const j_;
|
||||
|
||||
@@ -527,8 +527,6 @@ public:
|
||||
updateLocalTx(ReadView const& view) override;
|
||||
std::size_t
|
||||
getLocalTxCount() override;
|
||||
std::size_t
|
||||
getBookSubscribersCount() override;
|
||||
|
||||
//
|
||||
// Monitoring: publisher side.
|
||||
@@ -588,9 +586,7 @@ public:
|
||||
bool
|
||||
subBook(InfoSub::ref ispListener, Book const&) override;
|
||||
bool
|
||||
unsubBook(InfoSub::ref ispListener, Book const&) override;
|
||||
bool
|
||||
unsubBookInternal(std::uint64_t uListener, Book const&) override;
|
||||
unsubBook(std::uint64_t uListener, Book const&) override;
|
||||
|
||||
bool
|
||||
subManifests(InfoSub::ref ispListener) override;
|
||||
@@ -633,12 +629,6 @@ public:
|
||||
bool
|
||||
tryRemoveRpcSub(std::string const& strUrl) override;
|
||||
|
||||
beast::Journal const&
|
||||
journal() const override
|
||||
{
|
||||
return journal_;
|
||||
}
|
||||
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
@@ -715,32 +705,6 @@ private:
|
||||
AcceptedLedgerTx const& transaction,
|
||||
bool last);
|
||||
|
||||
/**
|
||||
* Fan transaction notifications out to all book subscribers.
|
||||
*
|
||||
* Extracts the set of order books affected by @p transaction, then
|
||||
* delivers @p jvObj to every live subscriber of those books.
|
||||
*
|
||||
* Uses a two-pass design to keep subLock_ hold time short:
|
||||
* 1. Under subLock_, collect strong InfoSub pointers for all live
|
||||
* subscribers and prune any expired weak_ptrs encountered.
|
||||
* 2. Release subLock_, then call send() on each collected pointer.
|
||||
*
|
||||
* @param transaction The accepted ledger transaction to inspect.
|
||||
* @param jvObj JSON representation of the transaction to deliver.
|
||||
*
|
||||
* @note Thread-safety: acquires subLock_ for the collection pass only.
|
||||
* send() is intentionally called outside the lock to avoid blocking
|
||||
* all other sub/unsub/publish paths while I/O is in progress.
|
||||
* @note Contention: subLock_ is shared with all other subscription types.
|
||||
* On high-throughput nodes processing multi-hop payments that touch
|
||||
* many offer nodes, this pass holds subLock_ longer than the old
|
||||
* per-book BookListeners locks did. This is an accepted trade-off
|
||||
* for lock-domain simplicity.
|
||||
*/
|
||||
void
|
||||
pubBookTransaction(AcceptedLedgerTx const& transaction, MultiApiJson const& jvObj);
|
||||
|
||||
void
|
||||
pubProposedAccountTransaction(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
@@ -838,19 +802,8 @@ private:
|
||||
|
||||
LedgerMaster& ledgerMaster_;
|
||||
|
||||
/** Maps each order book to its current set of subscribers.
|
||||
* Outer key: the Book (currency pair + optional domain).
|
||||
* Inner key: InfoSub::seq (unique per connection).
|
||||
* Inner value: weak_ptr so that a dropped connection does not prevent
|
||||
* the InfoSub from being destroyed; expired entries are pruned lazily
|
||||
* by pubBookTransaction and eagerly by unsubBookInternal (~InfoSub path).
|
||||
* Guarded by subLock_.
|
||||
*/
|
||||
using SubBookMapType = hash_map<Book, SubMapType>;
|
||||
|
||||
SubInfoMapType subAccount_;
|
||||
SubInfoMapType subRTAccount_;
|
||||
SubBookMapType subBook_; ///< Guarded by subLock_.
|
||||
|
||||
subRpcMapType rpcSubMap_;
|
||||
|
||||
@@ -3239,16 +3192,6 @@ NetworkOPsImp::getLocalTxCount()
|
||||
return localTX_->size();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
NetworkOPsImp::getBookSubscribersCount()
|
||||
{
|
||||
std::scoped_lock const sl(subLock_);
|
||||
std::size_t total = 0;
|
||||
for (auto const& [_, subs] : subBook_)
|
||||
total += subs.size();
|
||||
return total;
|
||||
}
|
||||
|
||||
// This routine should only be used to publish accepted or validated
|
||||
// transactions.
|
||||
MultiApiJson
|
||||
@@ -3410,89 +3353,11 @@ NetworkOPsImp::pubValidatedTransaction(
|
||||
}
|
||||
|
||||
if (transaction.getResult() == tesSUCCESS)
|
||||
pubBookTransaction(transaction, jvObj);
|
||||
registry_.get().getOrderBookDB().processTxn(ledger, transaction, jvObj);
|
||||
|
||||
pubAccountTransaction(ledger, transaction, last);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson const& jvObj)
|
||||
{
|
||||
auto const books = affectedBooks(alTx, journal_);
|
||||
if (books.empty())
|
||||
return;
|
||||
|
||||
// Two-pass design:
|
||||
//
|
||||
// 1. Under subLock_, walk subBook_, collect a strong pointer for each
|
||||
// unique listener (and prune any expired weak_ptrs we encounter).
|
||||
// 2. Release subLock_, then send to each collected listener.
|
||||
//
|
||||
// Reasoning:
|
||||
// * send() can be slow / blocking, so holding subLock_ across it would
|
||||
// stall every other sub/unsub/pub path on this server (see the matching
|
||||
// TODO above pubServer at line ~2275).
|
||||
// * A strong pointer destructed while subLock_ is held risks running
|
||||
// ~InfoSub() in-line, which re-enters unsubBook() and mutates the very
|
||||
// subBook_/SubMapType being iterated -> dangling iterator UB.
|
||||
//
|
||||
// Releasing subLock_ before any InfoSub::pointer can decay solves both.
|
||||
// ~InfoSub() reacquires subLock_ via unsubBook() on its own and serializes
|
||||
// safely with concurrent traffic.
|
||||
|
||||
std::vector<InfoSub::pointer> listeners;
|
||||
hash_set<std::uint64_t> seen;
|
||||
|
||||
// Sized for the common case where every affected book has at most
|
||||
// one subscriber. Multi-subscriber books trigger reallocation, but
|
||||
// that is rare and the upper-bound estimate (sum of per-book sizes)
|
||||
// would itself require walking subBook_ twice.
|
||||
listeners.reserve(books.size());
|
||||
seen.reserve(books.size());
|
||||
|
||||
{
|
||||
std::scoped_lock const sl(subLock_);
|
||||
|
||||
for (auto const& book : books)
|
||||
{
|
||||
auto it = subBook_.find(book);
|
||||
if (it == subBook_.end())
|
||||
continue;
|
||||
|
||||
for (auto sit = it->second.begin(); sit != it->second.end();)
|
||||
{
|
||||
if (auto p = sit->second.lock())
|
||||
{
|
||||
// Defensive: subBook_ entries are normally cleared by
|
||||
// ~InfoSub() -> unsubBook(), so we rarely see expired
|
||||
// weak_ptrs here. The else branch covers the narrow race
|
||||
// where the last strong ref is dropped between insertion
|
||||
// and our lock() call.
|
||||
if (seen.emplace(p->getSeq()).second)
|
||||
listeners.emplace_back(std::move(p));
|
||||
++sit;
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(journal_.debug())
|
||||
<< "pubBookTransaction: pruning expired weak_ptr for seq=" << sit->first;
|
||||
sit = it->second.erase(sit);
|
||||
}
|
||||
}
|
||||
|
||||
if (it->second.empty())
|
||||
subBook_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& p : listeners)
|
||||
{
|
||||
jvObj.visit(p->getApiVersion(), [&](json::Value const& jv) { p->send(jv, true); });
|
||||
}
|
||||
// listeners destructs here, outside subLock_; ~InfoSub (if any fires)
|
||||
// will reacquire subLock_ via unsubBook with no iterator hazard.
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::pubAccountTransaction(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
@@ -4146,39 +4011,26 @@ NetworkOPsImp::unsubAccountHistoryInternal(
|
||||
bool
|
||||
NetworkOPsImp::subBook(InfoSub::ref isrListener, Book const& book)
|
||||
{
|
||||
// Server-side insert first, then InfoSub bookkeeping. If the InfoSub-side
|
||||
// insert throws, the orphan in subBook_ is cleared by the expired-weak_ptr
|
||||
// prune in pubBookTransaction. With the reverse ordering, ~InfoSub would
|
||||
// call unsubBookInternal for a key that was never inserted server-side.
|
||||
if (auto listeners = registry_.get().getOrderBookDB().makeBookListeners(book))
|
||||
{
|
||||
std::scoped_lock const sl(subLock_);
|
||||
subBook_[book].try_emplace(isrListener->getSeq(), isrListener);
|
||||
listeners->addSubscriber(isrListener);
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::NetworkOPsImp::subBook : null book listeners");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
isrListener->insertBookSubscription(book);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
NetworkOPsImp::unsubBook(InfoSub::ref isrListener, Book const& book)
|
||||
NetworkOPsImp::unsubBook(std::uint64_t uSeq, Book const& book)
|
||||
{
|
||||
// Mirrors unsubAccount: clear the per-subscriber tracking set first so
|
||||
// ~InfoSub does not re-issue an unsubBookInternal for a book the caller
|
||||
// already removed, then erase the server-side entry.
|
||||
isrListener->deleteBookSubscription(book);
|
||||
return unsubBookInternal(isrListener->getSeq(), book);
|
||||
}
|
||||
if (auto listeners = registry_.get().getOrderBookDB().getBookListeners(book))
|
||||
listeners->removeSubscriber(uSeq);
|
||||
|
||||
bool
|
||||
NetworkOPsImp::unsubBookInternal(std::uint64_t uSeq, Book const& book)
|
||||
{
|
||||
std::scoped_lock const sl(subLock_);
|
||||
auto it = subBook_.find(book);
|
||||
if (it == subBook_.end())
|
||||
return false;
|
||||
bool const erased = it->second.erase(uSeq) != 0u;
|
||||
if (it->second.empty())
|
||||
subBook_.erase(it);
|
||||
return erased;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <xrpld/peerfinder/PeerfinderManager.h>
|
||||
#include <xrpld/peerfinder/Slot.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
@@ -82,7 +81,6 @@
|
||||
#include <xrpl.pb.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -395,22 +393,12 @@ void
|
||||
PeerImp::charge(Resource::Charge const& fee, std::string const& context)
|
||||
{
|
||||
dispatch(strand_, [this, self = shared_from_this(), fee, context]() {
|
||||
if ((usage_.charge(fee, context) == Resource::Disposition::Drop) &&
|
||||
if (usage_.charge(fee, context) == Resource::Disposition::Drop &&
|
||||
usage_.disconnect(pJournal_))
|
||||
{
|
||||
// Idempotent: only the first worker to observe Drop counts the
|
||||
// metric and posts fail(). Without the guard, several queued
|
||||
// workers can all see Drop before fail() lands on the strand,
|
||||
// overcounting peerDisconnectsCharges_ and posting duplicate
|
||||
// shutdowns. fail(std::string const&) self-posts to strand_
|
||||
// when invoked off-strand.
|
||||
bool expected = false;
|
||||
if (chargeDisconnectFired_.compare_exchange_strong(
|
||||
expected, true, std::memory_order_acq_rel))
|
||||
{
|
||||
overlay_.incPeerDisconnectCharges();
|
||||
fail("charge: Resources");
|
||||
}
|
||||
// Sever the connection.
|
||||
overlay_.incPeerDisconnectCharges();
|
||||
fail("charge: Resources");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2046,7 +2034,7 @@ PeerImp::checkTracking(std::uint32_t validationSeq)
|
||||
void
|
||||
PeerImp::checkTracking(std::uint32_t seq1, std::uint32_t seq2)
|
||||
{
|
||||
std::uint32_t const diff = std::max(seq1, seq2) - std::min(seq1, seq2);
|
||||
int const diff = std::max(seq1, seq2) - std::min(seq1, seq2);
|
||||
|
||||
if (diff < Tuning::kConvergedLedgerLimit)
|
||||
{
|
||||
@@ -2487,63 +2475,63 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
|
||||
return;
|
||||
}
|
||||
|
||||
protocol::TMGetObjectByHash reply;
|
||||
|
||||
reply.set_query(false);
|
||||
|
||||
reply.set_type(packet.type());
|
||||
|
||||
if (packet.has_ledgerhash())
|
||||
{
|
||||
if (!stringIsUInt256Sized(packet.ledgerhash()))
|
||||
{
|
||||
JLOG(pJournal_.debug()) << "GetObj: malformed ledgerhash from peer " << id_;
|
||||
fee_.update(Resource::kFeeMalformedRequest, "get object ledger hash");
|
||||
fee_.update(Resource::kFeeMalformedRequest, "ledger hash");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Reject oversized requests before touching the NodeStore.
|
||||
// The legitimate upper bound (InboundLedger::getNeededHashes())
|
||||
// is 8 hashes; anything beyond kHardMaxReplyNodes is non-conforming.
|
||||
if (packet.objects_size() > Tuning::kHardMaxReplyNodes)
|
||||
{
|
||||
JLOG(pJournal_.warn())
|
||||
<< "GetObj: oversized request from peer " << id_ << " (" << packet.objects_size()
|
||||
<< " > " << Tuning::kHardMaxReplyNodes << ")";
|
||||
fee_.update(Resource::kFeeInvalidData, "oversized get object request");
|
||||
return;
|
||||
|
||||
reply.set_ledgerhash(packet.ledgerhash());
|
||||
}
|
||||
|
||||
// Dispatch heavy synchronous NodeStore lookups off the peer's
|
||||
// I/O strand and onto the bounded job queue, mirroring the pattern
|
||||
// used by processLedgerRequest.
|
||||
std::weak_ptr<PeerImp> const weak = shared_from_this();
|
||||
bool const queued = app_.getJobQueue().addJob(JtLedgerReq, "RcvGetObjByHash", [weak, m]() {
|
||||
auto peer = weak.lock();
|
||||
if (!peer)
|
||||
return;
|
||||
try
|
||||
{
|
||||
peer->processGetObjectByHash(m);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
// Surface backend failures (NodeStore I/O, allocation)
|
||||
// back through the resource model so a misbehaving peer
|
||||
// is still accountable rather than silently dropped.
|
||||
JLOG(peer->pJournal_.warn()) << "GetObj: handler threw: " << e.what();
|
||||
peer->charge(Resource::kFeeRequestNoReply, "get object handler exception");
|
||||
}
|
||||
});
|
||||
if (!queued)
|
||||
fee_.update(Resource::kFeeModerateBurdenPeer, " received a get object by hash request");
|
||||
|
||||
// This is a very minimal implementation
|
||||
for (int i = 0; i < packet.objects_size(); ++i)
|
||||
{
|
||||
// The JobQueue is no longer accepting new work (typically
|
||||
// because it is shutting down / has been joined).
|
||||
JLOG(pJournal_.warn()) << "GetObj: job queue refused request from peer " << id_;
|
||||
return;
|
||||
auto const& obj = packet.objects(i);
|
||||
if (obj.has_hash() && stringIsUInt256Sized(obj.hash()))
|
||||
{
|
||||
uint256 const hash = uint256::fromRaw(obj.hash());
|
||||
// VFALCO TODO Move this someplace more sensible so we dont
|
||||
// need to inject the NodeStore interfaces.
|
||||
std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0};
|
||||
auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)};
|
||||
if (nodeObject)
|
||||
{
|
||||
protocol::TMIndexedObject& newObj = *reply.add_objects();
|
||||
newObj.set_hash(hash.begin(), hash.size());
|
||||
newObj.set_data(&nodeObject->getData().front(), nodeObject->getData().size());
|
||||
|
||||
if (obj.has_nodeid())
|
||||
newObj.set_index(obj.nodeid());
|
||||
if (obj.has_ledgerseq())
|
||||
newObj.set_ledgerseq(obj.ledgerseq());
|
||||
|
||||
// Check if by adding this object, reply has reached its
|
||||
// limit
|
||||
if (reply.objects_size() >= Tuning::kHardMaxReplyNodes)
|
||||
{
|
||||
fee_.update(
|
||||
Resource::kFeeModerateBurdenPeer,
|
||||
"Reply limit reached. Truncating reply.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Admission-time charge: a peer that floods enqueues would
|
||||
// otherwise be billed only the trivial onMessageEnd fee per
|
||||
// message until the JobQueue catches up, re-creating an
|
||||
// uncharged DoS window. Charge the base burden up-front (after
|
||||
// a successful enqueue); the per-lookup differential is added
|
||||
// in the worker.
|
||||
fee_.update(Resource::kFeeModerateBurdenPeer, "received a get object by hash request");
|
||||
JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of "
|
||||
<< packet.objects_size();
|
||||
send(std::make_shared<Message>(reply, protocol::mtGET_OBJECTS));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2599,69 +2587,6 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerImp::processGetObjectByHash(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
|
||||
{
|
||||
protocol::TMGetObjectByHash const& packet = *m;
|
||||
|
||||
protocol::TMGetObjectByHash reply;
|
||||
reply.set_query(false);
|
||||
reply.set_type(packet.type());
|
||||
|
||||
if (packet.has_ledgerhash())
|
||||
{
|
||||
reply.set_ledgerhash(packet.ledgerhash());
|
||||
}
|
||||
|
||||
// Defense in depth: caller (onMessage) already validates cheap
|
||||
// structural properties of the request before dispatching here:
|
||||
// - objects_size() <= kHardMaxReplyNodes (oversize gate)
|
||||
// - if has_ledgerhash() then ledgerhash is uint256-sized
|
||||
// The iteration cap below mirrors the oversize gate so this method
|
||||
// remains safe if invoked directly by tests or future callers, and
|
||||
// a peer cannot drive unbounded NodeStore lookups by sending
|
||||
// non-existent hashes.
|
||||
int const requested = packet.objects_size();
|
||||
int const iterLimit = std::min(requested, Tuning::kHardMaxReplyNodes);
|
||||
|
||||
for (int i = 0; i < iterLimit; ++i)
|
||||
{
|
||||
auto const& obj = packet.objects(i);
|
||||
if (!obj.has_hash() || !stringIsUInt256Sized(obj.hash()))
|
||||
continue;
|
||||
|
||||
uint256 const hash = uint256::fromRaw(obj.hash());
|
||||
// VFALCO TODO Move this someplace more sensible so we don't
|
||||
// need to inject the NodeStore interfaces.
|
||||
std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0};
|
||||
auto const nodeObject = app_.getNodeStore().fetchNodeObject(hash, seq);
|
||||
if (!nodeObject)
|
||||
continue;
|
||||
|
||||
protocol::TMIndexedObject& newObj = *reply.add_objects();
|
||||
newObj.set_hash(hash.begin(), hash.size());
|
||||
auto const& data = nodeObject->getData();
|
||||
newObj.set_data(data.data(), data.size());
|
||||
if (obj.has_nodeid())
|
||||
newObj.set_index(obj.nodeid());
|
||||
if (obj.has_ledgerseq())
|
||||
newObj.set_ledgerseq(obj.ledgerseq());
|
||||
}
|
||||
|
||||
// Apply work-proportional charge. `charge()` posts the disconnect
|
||||
// step (if any) back to strand_, so it is safe to call from this
|
||||
// JobQueue worker thread.
|
||||
charge(
|
||||
// We pass `requested` directly here, instead of actual lookups done. Which could be
|
||||
// std::min(packet.objects_size(), static_cast<int>(Tuning::kHardMaxReplyNodes));
|
||||
// Because we want to charge as per the request size, to discourage large requests.
|
||||
computeGetObjectByHashFee(requested, reply.objects_size()),
|
||||
"processed get object by hash request");
|
||||
|
||||
JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " << requested;
|
||||
send(std::make_shared<Message>(reply, protocol::mtGET_OBJECTS));
|
||||
}
|
||||
|
||||
void
|
||||
PeerImp::onMessage(std::shared_ptr<protocol::TMHaveTransactions> const& m)
|
||||
{
|
||||
@@ -3489,53 +3414,6 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
|
||||
send(std::make_shared<Message>(ledgerData, protocol::mtLEDGER_DATA));
|
||||
}
|
||||
|
||||
// Differential pricing helper. Returns only the *dynamic* component
|
||||
// of the per-message charge — the base `kFeeModerateBurdenPeer` is
|
||||
// applied at admission time in `onMessage(TMGetObjectByHash)` so a
|
||||
// high traffic client pays for the message regardless of when (or
|
||||
// whether) the worker runs.
|
||||
//
|
||||
// Dynamic charge model:
|
||||
//
|
||||
// billable = max(0, requested - kFreeObjectsPerRequest)
|
||||
// missed = max(0, requested - found)
|
||||
// billableMisses = min(missed, billable) // misses billed first
|
||||
// billableHits = billable - billableMisses
|
||||
// sizeBand = (requested > kBandMediumMax) ? kCostBandLarge
|
||||
// : (requested > kBandSmallMax) ? kCostBandMedium
|
||||
// : kCostBandSmall
|
||||
// dynamic = billableHits * kCostPerLookupHit
|
||||
// + billableMisses * kCostPerLookupMiss
|
||||
// + sizeBand
|
||||
//
|
||||
// Misses are billed first against the billable budget because a node store
|
||||
// seek dominates a cache hit and because invalid hashes are ~100% miss by construction.
|
||||
Resource::Charge
|
||||
PeerImp::computeGetObjectByHashFee(int const requested, int const found)
|
||||
{
|
||||
int const billable = std::max(0, requested - static_cast<int>(Tuning::kFreeObjectsPerRequest));
|
||||
// Clamp `missed` so a future caller passing found > requested cannot
|
||||
// produce a negative value that flips the hits/misses split.
|
||||
int const missed = std::max(0, requested - found);
|
||||
int const billableMisses = std::min(missed, billable);
|
||||
int const billableHits = billable - billableMisses;
|
||||
|
||||
int sizeBand = Tuning::kCostBandSmall;
|
||||
if (requested > Tuning::kBandMediumMax)
|
||||
{
|
||||
sizeBand = Tuning::kCostBandLarge;
|
||||
}
|
||||
else if (requested > Tuning::kBandSmallMax)
|
||||
{
|
||||
sizeBand = Tuning::kCostBandMedium;
|
||||
}
|
||||
|
||||
int const dynamic = (billableHits * Tuning::kCostPerLookupHit) +
|
||||
(billableMisses * Tuning::kCostPerLookupMiss) + sizeBand;
|
||||
|
||||
return Resource::Charge(dynamic, "GetObject differential");
|
||||
}
|
||||
|
||||
int
|
||||
PeerImp::getScore(bool haveItem) const
|
||||
{
|
||||
|
||||
@@ -147,12 +147,6 @@ private:
|
||||
protocol::TMStatusChange lastStatus_;
|
||||
Resource::Consumer usage_;
|
||||
ChargeWithContext fee_;
|
||||
|
||||
// One-shot guard so concurrent JobQueue workers cannot double-count
|
||||
// the per-connection peer-disconnect-by-charge metric (and cannot
|
||||
// post duplicate fail() calls) when several queued requests cross
|
||||
// kDropThreshold before the first fail() lands on the strand.
|
||||
std::atomic<bool> chargeDisconnectFired_{false};
|
||||
std::shared_ptr<PeerFinder::Slot> const slot_;
|
||||
boost::beast::multi_buffer readBuffer_;
|
||||
http_request_type request_;
|
||||
@@ -630,67 +624,6 @@ private:
|
||||
|
||||
void
|
||||
processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m);
|
||||
|
||||
protected:
|
||||
// Kept `protected` so test subclasses (see
|
||||
// TMGetObjectByHash_test) can drive the
|
||||
// synchronous processor and the differential-pricing helper without
|
||||
// routing through the JobQueue or going through `friend` plumbing.
|
||||
// Production callers reach these members only via
|
||||
// `onMessage(TMGetObjectByHash)` → JobQueue → `processGetObjectByHash`.
|
||||
|
||||
/** Process a generic-query TMGetObjectByHash message.
|
||||
|
||||
Dispatched from `onMessage(TMGetObjectByHash)` to the JobQueue
|
||||
(`JtLedgerReq`) so synchronous NodeStore lookups do not block the
|
||||
peer's I/O strand. Caps iteration at `Tuning::kHardMaxReplyNodes`
|
||||
regardless of hit/miss outcome and applies differential pricing
|
||||
via `computeGetObjectByHashFee()` after the fetch loop completes.
|
||||
|
||||
@param m The protocol message containing requested object hashes.
|
||||
*/
|
||||
void
|
||||
processGetObjectByHash(std::shared_ptr<protocol::TMGetObjectByHash> const& m);
|
||||
|
||||
/** Compute the per-message resource charge for a TMGetObjectByHash
|
||||
request based on how much work was actually performed.
|
||||
|
||||
The charge has three components on top of the base
|
||||
`Resource::kFeeModerateBurdenPeer`:
|
||||
- per-hit lookup cost (cheap; usually served from cache)
|
||||
- per-miss lookup cost (expensive node store seeks)
|
||||
- request-size band surcharge (escalates abusive batch sizes)
|
||||
|
||||
The first `Tuning::kFreeObjectsPerRequest` objects are free so
|
||||
that legitimate `InboundLedger::getNeededHashes()` traffic
|
||||
(at most 8 objects) is unaffected.
|
||||
|
||||
@param requested Number of objects requested by the message. This
|
||||
value is used for request-size pricing and may
|
||||
exceed `Tuning::kHardMaxReplyNodes` when this
|
||||
helper is called directly, even though processing
|
||||
caps the iterations to `Tuning::kHardMaxReplyNodes`.
|
||||
@param found Number of objects successfully returned in the
|
||||
reply.
|
||||
@return A `Resource::Charge` whose cost reflects the work performed.
|
||||
*/
|
||||
static Resource::Charge
|
||||
computeGetObjectByHashFee(int const requested, int const found);
|
||||
|
||||
/** Read-only accessor for the accumulated peer-message charge.
|
||||
|
||||
Exposed at `protected` scope so test subclasses can verify the
|
||||
oversized-request rejection path (Layer 1) without invoking the
|
||||
full JobQueue handler. Production callers should never read this back —
|
||||
the value is consumed by `charge()`/`disconnect()` internally.
|
||||
|
||||
@return The current `Resource::Charge` accumulated on `fee_`.
|
||||
*/
|
||||
Resource::Charge
|
||||
currentFeeCharge() const
|
||||
{
|
||||
return fee_.fee;
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
#pragma once
|
||||
#include <xrpl/shamap/SHAMapInnerNode.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl::Tuning {
|
||||
|
||||
/** How many ledgers off a server can be and we will
|
||||
still consider it converged */
|
||||
static constexpr std::uint32_t kConvergedLedgerLimit = 24;
|
||||
static constexpr auto kConvergedLedgerLimit = 24;
|
||||
|
||||
/** How many ledgers off a server has to be before we
|
||||
consider it diverged */
|
||||
static constexpr std::uint32_t kDivergedLedgerLimit = 128;
|
||||
static constexpr auto kDivergedLedgerLimit = 128;
|
||||
|
||||
/** The soft cap on the number of ledger entries in a single reply. */
|
||||
static constexpr auto kSoftMaxReplyNodes = 8192;
|
||||
@@ -41,92 +37,4 @@ static constexpr auto kMaxQueryDepth = 3;
|
||||
/** Size of buffer used to read from the socket. */
|
||||
constexpr std::size_t kReadBufferBytes = 16384;
|
||||
|
||||
/** TMGetObjectByHash differential pricing.
|
||||
|
||||
Honest peers ask for at most 8 hashes per call (the header, or up to
|
||||
4 state + 4 tx hashes from `InboundLedger::getNeededHashes()`). The
|
||||
free tier covers them at zero cost. Beyond that, each lookup is billed:
|
||||
'misses' cost much more than 'hits' because a miss does a node store seek
|
||||
while a hit is usually served from cache. On top of that, a size-band
|
||||
surcharge kicks in for larger requests so an attacker who crams a
|
||||
single message with thousands of hashes blows past
|
||||
`Resource::kDropThreshold` and gets disconnected.
|
||||
|
||||
The numbers below are picked to keep three things true given
|
||||
`kDropThreshold = 25000`:
|
||||
|
||||
- Honest traffic (<= 8 objects per request) is free.
|
||||
- A single all-miss request at `kHardMaxReplyNodes` (12288) costs
|
||||
more than the drop threshold, so an attacker gets dropped in one
|
||||
message.
|
||||
- A peer spamming 1024-object hit-only requests gets dropped in
|
||||
~19 messages — fast enough to be useful, slow enough that an
|
||||
honest peer momentarily sending oversized requests has time to
|
||||
back off. */
|
||||
|
||||
/** How many objects a request can ask for before per-lookup billing
|
||||
begins?
|
||||
Twice the honest peak (8) so a peer that occasionally retries a hash
|
||||
never trips pricing. Same value as `SHAMapInnerNode::kBranchFactor`;
|
||||
that's a coincidence, not a requirement. */
|
||||
static constexpr auto kFreeObjectsPerRequest = 16;
|
||||
|
||||
/** Cost of one cache-hit lookup. The unit; everything else is a
|
||||
multiple of this. */
|
||||
static constexpr auto kCostPerLookupHit = 1;
|
||||
|
||||
/** Cost of one node-store miss, in units of `kCostPerLookupHit`.
|
||||
|
||||
A miss does a node store disk seek; a hit usually comes from cache.
|
||||
The 8x ratio is an order-of-magnitude guess at the latency gap on
|
||||
SSD-backed nodes, not a measured number. The math only requires this
|
||||
to be at least 2 — any smaller and a full-miss request at the hard
|
||||
cap wouldn't trip the drop threshold. 8 leaves headroom: if
|
||||
`kDropThreshold` goes up or `kHardMaxReplyNodes` comes down, the
|
||||
drop-on-attack property still holds without a code change. */
|
||||
static constexpr auto kCostPerLookupMiss = 8;
|
||||
|
||||
/** Size-band surcharges. Whichever band a request's size falls into,
|
||||
its surcharge is added once on top of the per-lookup cost.
|
||||
|
||||
The job of the surcharge is to make crossing a band edge feel like
|
||||
a step, not a slope. With these values, the cost roughly doubles or triples at each cliff:
|
||||
|
||||
n=64: costs 48 => n=65 costs 149 (~3x jump)
|
||||
n=1024: costs 1108 => n=1025 costs 2009 (~2x jump)
|
||||
|
||||
The 10x step between medium and large mirrors the ~16x step
|
||||
between the band edges (64 -> 1024) so the cliff feels comparable
|
||||
at both scales.
|
||||
*/
|
||||
static constexpr auto kCostBandSmall = 0;
|
||||
static constexpr auto kCostBandMedium = 100;
|
||||
static constexpr auto kCostBandLarge = 1000;
|
||||
|
||||
/** How many hashes per type an honest peer asks for at a time.
|
||||
|
||||
Matches the `4` passed to `neededStateHashes(4)` and
|
||||
`neededTxHashes(4)` in `InboundLedger::getNeededHashes()`. Kept here
|
||||
instead of imported from the ledger module so overlay stays
|
||||
self-contained; if that `4` ever changes, update this in lockstep or
|
||||
the band thresholds below will start charging honest peers. */
|
||||
static constexpr auto kLegitHashesPerType = 4;
|
||||
|
||||
/** Cutoffs that decide which size band a request falls into.
|
||||
|
||||
A SHAMap inner node has 16 children; an honest peer asks for 4
|
||||
hashes per type. So:
|
||||
|
||||
kBandSmallMax = 4 * 16 = 64 // one inner node's worth
|
||||
kBandMediumMax = 4 * 16^2 = 1024 // a depth-2 subtree's worth
|
||||
|
||||
A request up to 64 objects is small (no surcharge); up to 1024 is
|
||||
medium; anything larger is large. The bounds are inclusive: a
|
||||
request of exactly 64 is small, 65 is medium. Anything past 1024 is
|
||||
well beyond what the honest sync path produces, so it's billed at
|
||||
the large rate to drive attack-shaped traffic over the drop
|
||||
threshold quickly. */
|
||||
static constexpr auto kBandSmallMax = kLegitHashesPerType * SHAMapInnerNode::kBranchFactor;
|
||||
static constexpr auto kBandMediumMax = kBandSmallMax * SHAMapInnerNode::kBranchFactor;
|
||||
|
||||
} // namespace xrpl::Tuning
|
||||
|
||||
@@ -186,23 +186,13 @@ doUnsubscribe(RPC::JsonContext& context)
|
||||
book.domain = domain;
|
||||
}
|
||||
|
||||
if (!context.netOps.unsubBook(ispSub, book))
|
||||
{
|
||||
JLOG(context.j.debug())
|
||||
<< "doUnsubscribe: book not subscribed (no-op for seq=" << ispSub->getSeq()
|
||||
<< ")";
|
||||
}
|
||||
context.netOps.unsubBook(ispSub->getSeq(), book);
|
||||
|
||||
// both_sides is deprecated.
|
||||
if ((jv.isMember(jss::both) && jv[jss::both].asBool()) ||
|
||||
(jv.isMember(jss::both_sides) && jv[jss::both_sides].asBool()))
|
||||
{
|
||||
if (!context.netOps.unsubBook(ispSub, reversed(book)))
|
||||
{
|
||||
JLOG(context.j.debug())
|
||||
<< "doUnsubscribe: reversed book not subscribed (no-op for seq="
|
||||
<< ispSub->getSeq() << ")";
|
||||
}
|
||||
context.netOps.unsubBook(ispSub->getSeq(), reversed(book));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
tests/conan/.gitignore
vendored
Normal file
2
tests/conan/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Conan test_package build output (cmake_layout)
|
||||
/build/
|
||||
@@ -1,12 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
set(name example)
|
||||
set(name validator-keys-conan-test)
|
||||
set(version 0.1.0)
|
||||
|
||||
project(${name} VERSION ${version} LANGUAGES CXX)
|
||||
|
||||
find_package(xrpl CONFIG REQUIRED)
|
||||
|
||||
add_executable(example)
|
||||
target_sources(example PRIVATE src/example.cpp)
|
||||
target_link_libraries(example PRIVATE xrpl::libxrpl)
|
||||
# Build the in-repo validator-keys-tool source instead of fetching it from
|
||||
# GitHub. Keep it out of the default build; the test recipe builds the target
|
||||
# explicitly.
|
||||
add_subdirectory(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../validator-keys-tool
|
||||
${CMAKE_BINARY_DIR}/validator-keys-tool
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
|
||||
set_target_properties(
|
||||
validator-keys
|
||||
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from conan.tools.build import can_run
|
||||
from conan.tools.cmake import CMake, cmake_layout
|
||||
@@ -6,15 +6,13 @@ from conan.tools.cmake import CMake, cmake_layout
|
||||
from conan import ConanFile
|
||||
|
||||
|
||||
class Example(ConanFile):
|
||||
name = "example"
|
||||
class ValidatorKeysConanTest(ConanFile):
|
||||
name = "validator-keys-conan-test"
|
||||
license = "ISC"
|
||||
author = "John Freeman <jfreeman08@gmail.com>, Michael Legleux <mlegleux@ripple.com"
|
||||
|
||||
settings = "os", "compiler", "build_type", "arch"
|
||||
|
||||
requires = ["xrpl/head"]
|
||||
|
||||
default_options = {
|
||||
"xrpl/*:xrpld": False,
|
||||
}
|
||||
@@ -25,19 +23,20 @@ class Example(ConanFile):
|
||||
if self.version is None:
|
||||
self.version = "0.1.0"
|
||||
|
||||
def requirements(self):
|
||||
# Test whatever reference is being created/tested rather than a
|
||||
# hardcoded version, so this test_package works for any xrpl version.
|
||||
self.requires(self.tested_reference_str)
|
||||
|
||||
def layout(self):
|
||||
cmake_layout(self)
|
||||
|
||||
def build(self):
|
||||
cmake = CMake(self)
|
||||
cmake.configure()
|
||||
cmake.build()
|
||||
|
||||
def package(self):
|
||||
cmake = CMake(self)
|
||||
cmake.install()
|
||||
cmake.build(target="validator-keys")
|
||||
|
||||
def test(self):
|
||||
if can_run(self):
|
||||
cmd_path = Path(self.build_folder) / self.cpp.build.bindir / "example"
|
||||
self.run(cmd_path, env="conanrun")
|
||||
cmd = os.path.join(self.cpp.build.bindir, "validator-keys")
|
||||
self.run(f'"{cmd}" --unittest', env="conanrun")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
int
|
||||
main(int argc, char const** argv)
|
||||
{
|
||||
std::printf("%s\n", xrpl::BuildInfo::getVersionString().c_str());
|
||||
return 0;
|
||||
}
|
||||
4
validator-keys-tool/.git-blame-ignore-revs
Normal file
4
validator-keys-tool/.git-blame-ignore-revs
Normal file
@@ -0,0 +1,4 @@
|
||||
# This feature requires Git >= 2.24
|
||||
# To use it by default in git blame:
|
||||
# git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
8ae260cb466d4cd0d4db378e5ce0acb8e4432f7c
|
||||
34
validator-keys-tool/CMakeLists.txt
Normal file
34
validator-keys-tool/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
cmake_minimum_required(VERSION 3.11)
|
||||
project(validator-keys-tool)
|
||||
|
||||
#[===========================================[
|
||||
This project is built as part of the rippled
|
||||
repository's Conan test package. The parent
|
||||
project calls find_package(xrpl) and adds this
|
||||
directory, providing the xrpl::libxrpl target.
|
||||
#]===========================================]
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
if(NOT TARGET xrpl::libxrpl)
|
||||
find_package(xrpl CONFIG REQUIRED)
|
||||
endif()
|
||||
|
||||
include(KeysSanity)
|
||||
include(KeysCov)
|
||||
include(KeysInterface)
|
||||
|
||||
add_executable(
|
||||
validator-keys
|
||||
src/ValidatorKeys.cpp
|
||||
src/ValidatorKeysTool.cpp
|
||||
# UNIT TESTS:
|
||||
src/test/ValidatorKeys_test.cpp
|
||||
src/test/ValidatorKeysTool_test.cpp
|
||||
)
|
||||
target_include_directories(validator-keys PRIVATE src)
|
||||
target_link_libraries(validator-keys xrpl::libxrpl Keys::opts)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_test(test validator-keys --unittest)
|
||||
endif()
|
||||
27
validator-keys-tool/README.md
Normal file
27
validator-keys-tool/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# validator-keys-tool
|
||||
|
||||
Rippled validator key generation tool
|
||||
|
||||
## Build
|
||||
|
||||
If you do not have package `xrpl` in your local Conan cache, it can be added by following the instructions in the [BUILD.md](https://github.com/XRPLF/rippled/blob/master/BUILD.md#patched-recipes) file in the rippled GitHub repository.
|
||||
|
||||
The build requirements and commands are the exact same as
|
||||
[those](https://github.com/XRPLF/rippled/blob/develop/BUILD.md) for rippled.
|
||||
In short:
|
||||
|
||||
```
|
||||
mkdir .build
|
||||
cd .build
|
||||
conan install .. --output-folder . --build missing
|
||||
cmake -DCMAKE_POLICY_DEFAULT_CMP0091=NEW \
|
||||
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=conan_toolchain.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
..
|
||||
cmake --build .
|
||||
./validator-keys --unittest # or ctest --test-dir .
|
||||
```
|
||||
|
||||
## Guide
|
||||
|
||||
[Validator Keys Tool Guide](doc/validator-keys-tool-guide.md)
|
||||
25
validator-keys-tool/RELEASENOTES.md
Normal file
25
validator-keys-tool/RELEASENOTES.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Release Notes
|
||||
|
||||
# Change Log
|
||||
|
||||
# Releases
|
||||
|
||||
## Version 0.3.2
|
||||
|
||||
This release overhauls the Travis CI configuration to cover more cases more robustly, and fixes a Windows build error introduced in 0.3.1.
|
||||
|
||||
### New and Improved Features
|
||||
|
||||
- Restructure Travis CI builds to use rippled's infrastructure [[#16](https://github.com/ripple/validator-keys-tool/pull/16)].
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Restores the windows.h include removed in 0.3.1, which is required for Windows builds.
|
||||
|
||||
## Version 0.3.1
|
||||
|
||||
This version brings the code up to date with the rippled code base's internal APIs and structures.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Update includes paths [[#14](https://github.com/ripple/validator-keys-tool/pull/14)].
|
||||
137
validator-keys-tool/cmake/KeysCov.cmake
Normal file
137
validator-keys-tool/cmake/KeysCov.cmake
Normal file
@@ -0,0 +1,137 @@
|
||||
#[===================================================================[
|
||||
coverage report target
|
||||
|
||||
Copied from rippled https://github.com/ripple/rippled/blob/develop/Builds/CMake/RippledCov.cmake
|
||||
#]===================================================================]
|
||||
|
||||
# cspell: words xcrun
|
||||
|
||||
if(coverage)
|
||||
if(is_clang)
|
||||
if(APPLE)
|
||||
execute_process(
|
||||
COMMAND xcrun -f llvm-profdata
|
||||
OUTPUT_VARIABLE LLVM_PROFDATA
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
else()
|
||||
find_program(LLVM_PROFDATA llvm-profdata)
|
||||
endif()
|
||||
if(NOT LLVM_PROFDATA)
|
||||
message(
|
||||
WARNING
|
||||
"unable to find llvm-profdata - skipping coverage_report target"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
execute_process(
|
||||
COMMAND xcrun -f llvm-cov
|
||||
OUTPUT_VARIABLE LLVM_COV
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
else()
|
||||
find_program(LLVM_COV llvm-cov)
|
||||
endif()
|
||||
if(NOT LLVM_COV)
|
||||
message(
|
||||
WARNING
|
||||
"unable to find llvm-cov - skipping coverage_report target"
|
||||
)
|
||||
endif()
|
||||
|
||||
set(extract_pattern "")
|
||||
if(coverage_core_only)
|
||||
set(extract_pattern "${CMAKE_CURRENT_SOURCE_DIR}/src/")
|
||||
endif()
|
||||
|
||||
if(LLVM_COV AND LLVM_PROFDATA)
|
||||
add_custom_target(
|
||||
coverage_report
|
||||
USES_TERMINAL
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E echo
|
||||
"Generating coverage - results will be in ${CMAKE_BINARY_DIR}/coverage/index.html."
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Running validator-keys tests."
|
||||
COMMAND
|
||||
validator-keys
|
||||
--unittest$<$<BOOL:${coverage_test}>:=${coverage_test}>
|
||||
COMMAND
|
||||
${LLVM_PROFDATA} merge -sparse default.profraw -o
|
||||
rip.profdata
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:"
|
||||
COMMAND
|
||||
${LLVM_COV} report -instr-profile=rip.profdata
|
||||
$<TARGET_FILE:validator-keys> ${extract_pattern}
|
||||
# generate html report
|
||||
COMMAND
|
||||
${LLVM_COV} show -format=html
|
||||
-output-dir=${CMAKE_BINARY_DIR}/coverage
|
||||
-instr-profile=rip.profdata $<TARGET_FILE:validator-keys>
|
||||
${extract_pattern}
|
||||
BYPRODUCTS coverage/index.html
|
||||
)
|
||||
endif()
|
||||
elseif(is_gcc)
|
||||
find_program(LCOV lcov)
|
||||
if(NOT LCOV)
|
||||
message(
|
||||
WARNING
|
||||
"unable to find lcov - skipping coverage_report target"
|
||||
)
|
||||
endif()
|
||||
|
||||
find_program(GENHTML genhtml)
|
||||
if(NOT GENHTML)
|
||||
message(
|
||||
WARNING
|
||||
"unable to find genhtml - skipping coverage_report target"
|
||||
)
|
||||
endif()
|
||||
|
||||
set(extract_pattern "*")
|
||||
if(coverage_core_only)
|
||||
set(extract_pattern "*/src/*")
|
||||
endif()
|
||||
|
||||
if(LCOV AND GENHTML)
|
||||
add_custom_target(
|
||||
coverage_report
|
||||
USES_TERMINAL
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E echo
|
||||
"Generating coverage- results will be in ${CMAKE_BINARY_DIR}/coverage/index.html."
|
||||
# create baseline info file
|
||||
COMMAND
|
||||
${LCOV} --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d
|
||||
. -i -o baseline.info | grep -v
|
||||
"ignoring data for external file"
|
||||
# run tests
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E echo
|
||||
"Running validator-keys tests for coverage report."
|
||||
COMMAND
|
||||
validator-keys
|
||||
--unittest$<$<BOOL:${coverage_test}>:=${coverage_test}>
|
||||
# Create test coverage data file
|
||||
COMMAND
|
||||
${LCOV} --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d
|
||||
. -o tests.info | grep -v "ignoring data for external file"
|
||||
# Combine baseline and test coverage data
|
||||
COMMAND ${LCOV} -a baseline.info -a tests.info -o lcov-all.info
|
||||
# extract our files
|
||||
COMMAND
|
||||
${LCOV} -e lcov-all.info "${extract_pattern}" -o lcov.info
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:"
|
||||
COMMAND ${LCOV} --summary lcov.info
|
||||
# generate HTML report
|
||||
COMMAND ${GENHTML} -o ${CMAKE_BINARY_DIR}/coverage lcov.info
|
||||
BYPRODUCTS coverage/index.html
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Coverage: neither clang nor gcc")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Coverage disabled")
|
||||
endif()
|
||||
87
validator-keys-tool/cmake/KeysInterface.cmake
Normal file
87
validator-keys-tool/cmake/KeysInterface.cmake
Normal file
@@ -0,0 +1,87 @@
|
||||
#[===================================================================[
|
||||
rippled compile options/settings via an interface library
|
||||
#]===================================================================]
|
||||
|
||||
# cspell: words Wsuggest fprofile ftest
|
||||
|
||||
add_library(keys_opts INTERFACE)
|
||||
add_library(Keys::opts ALIAS keys_opts)
|
||||
target_compile_definitions(
|
||||
keys_opts
|
||||
INTERFACE
|
||||
BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
|
||||
$<$<BOOL:${boost_show_deprecated}>:
|
||||
BOOST_ASIO_NO_DEPRECATED
|
||||
BOOST_FILESYSTEM_NO_DEPRECATED
|
||||
>
|
||||
$<$<NOT:$<BOOL:${boost_show_deprecated}>>:
|
||||
BOOST_COROUTINES_NO_DEPRECATION_WARNING
|
||||
BOOST_BEAST_ALLOW_DEPRECATED
|
||||
BOOST_FILESYSTEM_DEPRECATED
|
||||
>
|
||||
$<$<BOOL:${beast_hashers}>:
|
||||
USE_BEAST_HASHER
|
||||
>
|
||||
$<$<BOOL:${beast_no_unit_test_inline}>:BEAST_NO_UNIT_TEST_INLINE=1>
|
||||
$<$<BOOL:${beast_disable_autolink}>:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1>
|
||||
$<$<BOOL:${single_io_service_thread}>:RIPPLE_SINGLE_IO_SERVICE_THREAD=1>
|
||||
)
|
||||
target_compile_options(
|
||||
keys_opts
|
||||
INTERFACE
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<COMPILE_LANGUAGE:CXX>>:-Wsuggest-override>
|
||||
$<$<BOOL:${perf}>:-fno-omit-frame-pointer>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-fprofile-arcs
|
||||
-ftest-coverage>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-fprofile-instr-generate
|
||||
-fcoverage-mapping>
|
||||
$<$<BOOL:${profile}>:-pg>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
keys_opts
|
||||
INTERFACE
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-fprofile-arcs
|
||||
-ftest-coverage>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-fprofile-instr-generate
|
||||
-fcoverage-mapping>
|
||||
$<$<BOOL:${profile}>:-pg>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>
|
||||
)
|
||||
|
||||
if(jemalloc)
|
||||
if(static)
|
||||
set(JEMALLOC_USE_STATIC ON CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
find_package(jemalloc REQUIRED)
|
||||
target_compile_definitions(keys_opts INTERFACE PROFILE_JEMALLOC)
|
||||
target_include_directories(
|
||||
keys_opts
|
||||
SYSTEM
|
||||
INTERFACE ${JEMALLOC_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(keys_opts INTERFACE ${JEMALLOC_LIBRARIES})
|
||||
get_filename_component(JEMALLOC_LIB_PATH ${JEMALLOC_LIBRARIES} DIRECTORY)
|
||||
## TODO see if we can use the BUILD_RPATH target property (is it transitive?)
|
||||
set(CMAKE_BUILD_RPATH ${CMAKE_BUILD_RPATH} ${JEMALLOC_LIB_PATH})
|
||||
endif()
|
||||
if(san)
|
||||
target_compile_options(
|
||||
keys_opts
|
||||
INTERFACE
|
||||
# sanitizers recommend minimum of -O1 for reasonable performance
|
||||
$<$<CONFIG:Debug>:-O1>
|
||||
${SAN_FLAG}
|
||||
-fno-omit-frame-pointer
|
||||
)
|
||||
target_compile_definitions(
|
||||
keys_opts
|
||||
INTERFACE
|
||||
$<$<STREQUAL:${san},address>:SANITIZER=ASAN>
|
||||
$<$<STREQUAL:${san},thread>:SANITIZER=TSAN>
|
||||
$<$<STREQUAL:${san},memory>:SANITIZER=MSAN>
|
||||
$<$<STREQUAL:${san},undefined>:SANITIZER=UBSAN>
|
||||
)
|
||||
target_link_libraries(keys_opts INTERFACE ${SAN_FLAG} ${SAN_LIB})
|
||||
endif()
|
||||
103
validator-keys-tool/cmake/KeysSanity.cmake
Normal file
103
validator-keys-tool/cmake/KeysSanity.cmake
Normal file
@@ -0,0 +1,103 @@
|
||||
#[===================================================================[
|
||||
convenience variables and sanity checks
|
||||
#]===================================================================]
|
||||
|
||||
if(NOT ep_procs)
|
||||
include(ProcessorCount)
|
||||
ProcessorCount(ep_procs)
|
||||
if(ep_procs GREATER 1)
|
||||
# never use more than half of cores for EP builds
|
||||
math(EXPR ep_procs "${ep_procs} / 2")
|
||||
message(STATUS "Using ${ep_procs} cores for ExternalProject builds.")
|
||||
endif()
|
||||
endif()
|
||||
get_property(is_multiconfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(is_multiconfig STREQUAL "NOTFOUND")
|
||||
if(
|
||||
${CMAKE_GENERATOR} STREQUAL "Xcode"
|
||||
OR ${CMAKE_GENERATOR} MATCHES "^Visual Studio"
|
||||
)
|
||||
set(is_multiconfig TRUE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE)
|
||||
if(NOT is_multiconfig)
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
message(STATUS "Build type not specified - defaulting to Release")
|
||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "build type" FORCE)
|
||||
elseif(
|
||||
NOT (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL Release)
|
||||
)
|
||||
# for simplicity, these are the only two config types we care about. Limiting
|
||||
# the build types simplifies dealing with external project builds especially
|
||||
message(
|
||||
FATAL_ERROR
|
||||
" *** Only Debug or Release build types are currently supported ***"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
get_directory_property(has_parent PARENT_DIRECTORY)
|
||||
if(has_parent)
|
||||
set(is_root_project OFF)
|
||||
else()
|
||||
set(is_root_project ON)
|
||||
endif()
|
||||
|
||||
if("${CMAKE_CXX_COMPILER_ID}" MATCHES ".*Clang") # both Clang and AppleClang
|
||||
set(is_clang TRUE)
|
||||
if(
|
||||
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang"
|
||||
AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0
|
||||
)
|
||||
message(FATAL_ERROR "This project requires clang 7 or later")
|
||||
endif()
|
||||
# TODO min AppleClang version check ?
|
||||
elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
set(is_gcc TRUE)
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0)
|
||||
message(FATAL_ERROR "This project requires GCC 7 or later")
|
||||
endif()
|
||||
endif()
|
||||
if(CMAKE_GENERATOR STREQUAL "Xcode")
|
||||
set(is_xcode TRUE)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(is_linux TRUE)
|
||||
else()
|
||||
set(is_linux FALSE)
|
||||
endif()
|
||||
|
||||
if("$ENV{CI}" STREQUAL "true" OR "$ENV{CONTINUOUS_INTEGRATION}" STREQUAL "true")
|
||||
set(is_ci TRUE)
|
||||
else()
|
||||
set(is_ci FALSE)
|
||||
endif()
|
||||
|
||||
# check for in-source build and fail
|
||||
if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Builds (in-source) are not allowed in "
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}. Please remove CMakeCache.txt and the CMakeFiles "
|
||||
"directory from ${CMAKE_CURRENT_SOURCE_DIR} and try building in a separate directory."
|
||||
)
|
||||
endif()
|
||||
|
||||
if(MSVC AND CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
|
||||
message(FATAL_ERROR "Visual Studio 32-bit build is not supported.")
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Rippled requires a 64 bit target architecture.\n"
|
||||
"The most likely cause of this warning is trying to build rippled with a 32-bit OS."
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE AND NOT HOMEBREW)
|
||||
find_program(HOMEBREW brew)
|
||||
endif()
|
||||
117
validator-keys-tool/doc/validator-keys-tool-guide.md
Normal file
117
validator-keys-tool/doc/validator-keys-tool-guide.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Validator Keys Tool Guide
|
||||
|
||||
<!-- cspell: words Iiwib hvssbqmgz -->
|
||||
|
||||
This guide explains how to set up a validator so its public key does not have to
|
||||
change if the rippled config and/or server are compromised.
|
||||
|
||||
A validator uses a public/private key pair. The validator is identified by the
|
||||
public key. The private key should be tightly controlled. It is used to:
|
||||
|
||||
- sign tokens authorizing a rippled server to run as the validator identified
|
||||
by this public key.
|
||||
- sign revocations indicating that the private key has been compromised and
|
||||
the validator public key should no longer be trusted.
|
||||
|
||||
Each new token invalidates all previous tokens for the validator public key.
|
||||
The current token needs to be present in the rippled config file.
|
||||
|
||||
Servers that trust the validator will adapt automatically when the token
|
||||
changes.
|
||||
|
||||
## Validator Keys
|
||||
|
||||
When first setting up a validator, use the `validator-keys` tool to generate
|
||||
its key pair:
|
||||
|
||||
```
|
||||
$ validator-keys create_keys
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
Validator keys stored in /home/ubuntu/.ripple/validator-keys.json
|
||||
```
|
||||
|
||||
Keep the key file in a secure but recoverable location, such as an encrypted
|
||||
USB flash drive. Do not modify its contents.
|
||||
|
||||
## Validator Token
|
||||
|
||||
After first creating the [validator keys](#validator-keys) or if the previous
|
||||
token has been compromised, use the `validator-keys` tool to create a new
|
||||
validator token:
|
||||
|
||||
```
|
||||
$ validator-keys create_token
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
Update rippled.cfg file with these values:
|
||||
|
||||
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
|
||||
|
||||
[validator_token]
|
||||
eyJ2YWxpZGF0aW9uX3NlY3J|dF9rZXkiOiI5ZWQ0NWY4NjYyNDFjYzE4YTI3NDdiNT
|
||||
QzODdjMDYyNTkwNzk3MmY0ZTcxOTAyMzFmYWE5Mzc0NTdmYT|kYWY2IiwibWFuaWZl
|
||||
c3QiOiJKQUFBQUFGeEllMUZ0d21pbXZHdEgyaUNjTUpxQzlnVkZLaWxHZncxL3ZDeE
|
||||
hYWExwbGMyR25NaEFrRTFhZ3FYeEJ3RHdEYklENk9NU1l1TTBGREFscEFnTms4U0tG
|
||||
bjdNTzJmZGtjd1JRSWhBT25ndTlzQUtxWFlvdUorbDJWMFcrc0FPa1ZCK1pSUzZQU2
|
||||
hsSkFmVXNYZkFpQnNWSkdlc2FhZE9KYy9hQVpva1MxdnltR21WcmxIUEtXWDNZeXd1
|
||||
NmluOEhBU1FLUHVnQkQ2N2tNYVJGR3ZtcEFUSGxHS0pkdkRGbFdQWXk1QXFEZWRGdj
|
||||
VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0ifQ==
|
||||
```
|
||||
|
||||
For a new validator, add the [validator_token] value to the rippled config file.
|
||||
For a pre-existing validator, replace the old [validator_token] value with the
|
||||
newly generated one. A valid config file may only contain one [validator_token]
|
||||
value. After the config is updated, restart rippled.
|
||||
|
||||
There is a hard limit of 4,294,967,293 tokens that can be generated for a given
|
||||
validator key pair.
|
||||
|
||||
## Key Revocation
|
||||
|
||||
If a validator private key is compromised, the key must be revoked permanently.
|
||||
To revoke the validator key, use the `validator-keys` tool to generate a
|
||||
revocation, which indicates to other servers that the key is no longer valid:
|
||||
|
||||
```
|
||||
$ validator-keys revoke_keys
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
WARNING: This will revoke your validator keys!
|
||||
|
||||
Update rippled.cfg file with these values and restart rippled:
|
||||
|
||||
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
|
||||
|
||||
[validator_key_revocation]
|
||||
JP////9xIe0hvssbqmgzFH4/NDp1z|3ShkmCtFXuC5A0IUocppHopnASQN2MuMD1Puoyjvnr
|
||||
jQ2KJSO/2tsjRhjO6q0QQHppslQsKNSXWxjGQNIEa6nPisBOKlDDcJVZAMP4QcIyNCadzgM=
|
||||
```
|
||||
|
||||
Add the `[validator_key_revocation]` value to this validator's config and
|
||||
restart rippled. Rename the old key file and generate new [validator keys](#validator-keys) and
|
||||
a corresponding [validator token](#validator-token).
|
||||
|
||||
## Signing
|
||||
|
||||
The `validator-keys` tool can be used to sign arbitrary data with the validator
|
||||
key.
|
||||
|
||||
```
|
||||
$ validator-keys sign "your data to sign"
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
B91B73536235BBA028D344B81DBCBECF19C1E0034AC21FB51C2351A138C9871162F3193D7C41A49FB7AABBC32BC2B116B1D5701807BE462D8800B5AEA4F0550D
|
||||
```
|
||||
276
validator-keys-tool/src/ValidatorKeys.cpp
Normal file
276
validator-keys-tool/src/ValidatorKeys.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
#include <ValidatorKeys.h>
|
||||
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
#include <boost/algorithm/clamp.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
std::string
|
||||
ValidatorToken::toString() const
|
||||
{
|
||||
json::Value jv;
|
||||
jv["validation_secret_key"] = strHex(secretKey);
|
||||
jv["manifest"] = manifest;
|
||||
|
||||
return xrpl::base64Encode(to_string(jv));
|
||||
}
|
||||
|
||||
ValidatorKeys::ValidatorKeys(KeyType const& keyType)
|
||||
: keyType_(keyType)
|
||||
, tokenSequence_(0)
|
||||
, revoked_(false)
|
||||
, keys_(generateKeyPair(keyType_, randomSeed()))
|
||||
{
|
||||
}
|
||||
|
||||
ValidatorKeys::ValidatorKeys(
|
||||
KeyType const& keyType,
|
||||
SecretKey const& secretKey,
|
||||
std::uint32_t tokenSequence,
|
||||
bool revoked)
|
||||
: keyType_(keyType)
|
||||
, tokenSequence_(tokenSequence)
|
||||
, revoked_(revoked)
|
||||
, keys_({derivePublicKey(keyType_, secretKey), secretKey})
|
||||
{
|
||||
}
|
||||
|
||||
ValidatorKeys
|
||||
ValidatorKeys::make_ValidatorKeys(boost::filesystem::path const& keyFile)
|
||||
{
|
||||
std::ifstream ifsKeys(keyFile.c_str(), std::ios::in);
|
||||
|
||||
if (!ifsKeys)
|
||||
throw std::runtime_error("Failed to open key file: " + keyFile.string());
|
||||
|
||||
json::Reader reader;
|
||||
json::Value jKeys;
|
||||
if (!reader.parse(ifsKeys, jKeys))
|
||||
{
|
||||
throw std::runtime_error("Unable to parse json key file: " + keyFile.string());
|
||||
}
|
||||
|
||||
static std::array<std::string, 4> const requiredFields{
|
||||
{"key_type", "secret_key", "token_sequence", "revoked"}};
|
||||
|
||||
for (auto field : requiredFields)
|
||||
{
|
||||
if (!jKeys.isMember(field))
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() + "' is missing \"" + field + "\" field");
|
||||
}
|
||||
}
|
||||
|
||||
auto const keyType = keyTypeFromString(jKeys["key_type"].asString());
|
||||
if (!keyType)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
"' contains invalid \"key_type\" field: " + jKeys["key_type"].toStyledString());
|
||||
}
|
||||
|
||||
auto const secret =
|
||||
parseBase58<SecretKey>(TokenType::NodePrivate, jKeys["secret_key"].asString());
|
||||
|
||||
if (!secret)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
"' contains invalid \"secret_key\" field: " + jKeys["secret_key"].toStyledString());
|
||||
}
|
||||
|
||||
std::uint32_t tokenSequence;
|
||||
try
|
||||
{
|
||||
if (!jKeys["token_sequence"].isIntegral())
|
||||
throw std::runtime_error("");
|
||||
|
||||
tokenSequence = jKeys["token_sequence"].asUInt();
|
||||
}
|
||||
catch (std::runtime_error&)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() + "' contains invalid \"token_sequence\" field: " +
|
||||
jKeys["token_sequence"].toStyledString());
|
||||
}
|
||||
|
||||
if (!jKeys["revoked"].isBool())
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
"' contains invalid \"revoked\" field: " + jKeys["revoked"].toStyledString());
|
||||
|
||||
ValidatorKeys vk(*keyType, *secret, tokenSequence, jKeys["revoked"].asBool());
|
||||
|
||||
if (jKeys.isMember("domain"))
|
||||
{
|
||||
if (!jKeys["domain"].isString())
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
"' contains invalid \"domain\" field: " + jKeys["domain"].toStyledString());
|
||||
|
||||
vk.domain(jKeys["domain"].asString());
|
||||
}
|
||||
|
||||
if (jKeys.isMember("manifest"))
|
||||
{
|
||||
if (!jKeys["manifest"].isString())
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
"' contains invalid \"manifest\" field: " + jKeys["manifest"].toStyledString());
|
||||
|
||||
auto ret = strUnHex(jKeys["manifest"].asString());
|
||||
|
||||
if (!ret || ret->size() == 0)
|
||||
throw std::runtime_error(
|
||||
"Key file '" + keyFile.string() +
|
||||
"' contains invalid \"manifest\" field: " + jKeys["manifest"].toStyledString());
|
||||
|
||||
vk.manifest_.clear();
|
||||
vk.manifest_.reserve(ret->size());
|
||||
std::copy(ret->begin(), ret->end(), std::back_inserter(vk.manifest_));
|
||||
}
|
||||
|
||||
return vk;
|
||||
}
|
||||
|
||||
void
|
||||
ValidatorKeys::writeToFile(boost::filesystem::path const& keyFile) const
|
||||
{
|
||||
using namespace boost::filesystem;
|
||||
|
||||
json::Value jv;
|
||||
jv["key_type"] = to_string(keyType_);
|
||||
jv["public_key"] = toBase58(TokenType::NodePublic, keys_.publicKey);
|
||||
jv["secret_key"] = toBase58(TokenType::NodePrivate, keys_.secretKey);
|
||||
jv["token_sequence"] = json::UInt(tokenSequence_);
|
||||
jv["revoked"] = revoked_;
|
||||
if (!domain_.empty())
|
||||
jv["domain"] = domain_;
|
||||
if (!manifest_.empty())
|
||||
jv["manifest"] = strHex(makeSlice(manifest_));
|
||||
|
||||
if (!keyFile.parent_path().empty())
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
if (!exists(keyFile.parent_path()))
|
||||
boost::filesystem::create_directories(keyFile.parent_path(), ec);
|
||||
|
||||
if (ec || !is_directory(keyFile.parent_path()))
|
||||
throw std::runtime_error("Cannot create directory: " + keyFile.parent_path().string());
|
||||
}
|
||||
|
||||
std::ofstream o(keyFile.string(), std::ios_base::trunc);
|
||||
if (o.fail())
|
||||
throw std::runtime_error("Cannot open key file: " + keyFile.string());
|
||||
|
||||
o << jv.toStyledString();
|
||||
}
|
||||
|
||||
boost::optional<ValidatorToken>
|
||||
ValidatorKeys::createValidatorToken(KeyType const& keyType)
|
||||
{
|
||||
if (revoked() || std::numeric_limits<std::uint32_t>::max() - 1 <= tokenSequence_)
|
||||
return boost::none;
|
||||
|
||||
++tokenSequence_;
|
||||
|
||||
auto const tokenSecret = generateSecretKey(keyType, randomSeed());
|
||||
auto const tokenPublic = derivePublicKey(keyType, tokenSecret);
|
||||
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = tokenSequence_;
|
||||
st[sfPublicKey] = keys_.publicKey;
|
||||
st[sfSigningPubKey] = tokenPublic;
|
||||
|
||||
if (!domain_.empty())
|
||||
st[sfDomain] = makeSlice(domain_);
|
||||
|
||||
xrpl::sign(st, HashPrefix::Manifest, keyType, tokenSecret);
|
||||
xrpl::sign(st, HashPrefix::Manifest, keyType_, keys_.secretKey, sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
manifest_.clear();
|
||||
manifest_.reserve(s.size());
|
||||
std::copy(s.begin(), s.end(), std::back_inserter(manifest_));
|
||||
|
||||
return ValidatorToken{xrpl::base64Encode(manifest_.data(), manifest_.size()), tokenSecret};
|
||||
}
|
||||
|
||||
std::string
|
||||
ValidatorKeys::revoke()
|
||||
{
|
||||
revoked_ = true;
|
||||
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
||||
st[sfPublicKey] = keys_.publicKey;
|
||||
|
||||
xrpl::sign(st, HashPrefix::Manifest, keyType_, keys_.secretKey, sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
manifest_.clear();
|
||||
manifest_.reserve(s.size());
|
||||
std::copy(s.begin(), s.end(), std::back_inserter(manifest_));
|
||||
|
||||
return xrpl::base64Encode(manifest_.data(), manifest_.size());
|
||||
}
|
||||
|
||||
std::string
|
||||
ValidatorKeys::sign(std::string const& data) const
|
||||
{
|
||||
return strHex(xrpl::sign(keys_.publicKey, keys_.secretKey, makeSlice(data)));
|
||||
}
|
||||
|
||||
void
|
||||
ValidatorKeys::domain(std::string d)
|
||||
{
|
||||
if (!d.empty())
|
||||
{
|
||||
// A valid domain for a validator must be at least 4 characters
|
||||
// long, should contain at least one . and should not be longer
|
||||
// that 128 characters.
|
||||
if (d.size() < 4 || d.size() > 128)
|
||||
throw std::runtime_error("The domain must be between 4 and 128 characters long.");
|
||||
|
||||
// This regular expression should do a decent job of weeding out
|
||||
// obviously wrong domain names but it isn't perfect. It does not
|
||||
// really support IDNs. If this turns out to be an issue, a more
|
||||
// thorough regex can be used or this check can just be removed.
|
||||
static boost::regex const re(
|
||||
"^" // Beginning of line
|
||||
"(" // Hostname or domain name
|
||||
"(?!-)" // - must not begin with '-'
|
||||
"[a-zA-Z0-9-]{1,63}" // - only alphanumeric and '-'
|
||||
"(?<!-)" // - must not end with '-'
|
||||
"\\." // segment separator
|
||||
")+" // 1 or more segments
|
||||
"[A-Za-z]{2,63}" // TLD
|
||||
"$" // End of line
|
||||
,
|
||||
boost::regex_constants::optimize);
|
||||
|
||||
if (!boost::regex_match(d, re))
|
||||
throw std::runtime_error(
|
||||
"The domain field must use the '[host.][subdomain.]domain.tld' "
|
||||
"format");
|
||||
}
|
||||
|
||||
domain_ = std::move(d);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
158
validator-keys-tool/src/ValidatorKeys.h
Normal file
158
validator-keys-tool/src/ValidatorKeys.h
Normal file
@@ -0,0 +1,158 @@
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace boost {
|
||||
namespace filesystem {
|
||||
class path;
|
||||
}
|
||||
} // namespace boost
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
struct ValidatorToken
|
||||
{
|
||||
std::string const manifest;
|
||||
SecretKey const secretKey;
|
||||
|
||||
/// Returns base64-encoded JSON object
|
||||
std::string
|
||||
toString() const;
|
||||
};
|
||||
|
||||
class ValidatorKeys
|
||||
{
|
||||
private:
|
||||
KeyType keyType_;
|
||||
|
||||
// struct used to contain both public and secret keys
|
||||
struct Keys
|
||||
{
|
||||
PublicKey publicKey;
|
||||
SecretKey secretKey;
|
||||
|
||||
Keys() = delete;
|
||||
Keys(std::pair<PublicKey, SecretKey> p) : publicKey(p.first), secretKey(p.second)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> manifest_;
|
||||
std::uint32_t tokenSequence_;
|
||||
bool revoked_;
|
||||
std::string domain_;
|
||||
Keys keys_;
|
||||
|
||||
public:
|
||||
explicit ValidatorKeys(KeyType const& keyType);
|
||||
|
||||
ValidatorKeys(
|
||||
KeyType const& keyType,
|
||||
SecretKey const& secretKey,
|
||||
std::uint32_t sequence,
|
||||
bool revoked = false);
|
||||
|
||||
/** Returns ValidatorKeys constructed from JSON file
|
||||
|
||||
@param keyFile Path to JSON key file
|
||||
|
||||
@throws std::runtime_error if file content is invalid
|
||||
*/
|
||||
static ValidatorKeys
|
||||
make_ValidatorKeys(boost::filesystem::path const& keyFile);
|
||||
|
||||
~ValidatorKeys() = default;
|
||||
ValidatorKeys(ValidatorKeys const&) = default;
|
||||
ValidatorKeys&
|
||||
operator=(ValidatorKeys const&) = default;
|
||||
|
||||
inline bool
|
||||
operator==(ValidatorKeys const& rhs) const
|
||||
{
|
||||
// SecretKey::operator== is deleted to discourage non-constant-time
|
||||
// comparison. The public key is derived deterministically from the
|
||||
// secret key, so comparing public keys is equivalent here.
|
||||
return revoked_ == rhs.revoked_ && keyType_ == rhs.keyType_ &&
|
||||
tokenSequence_ == rhs.tokenSequence_ && keys_.publicKey == rhs.keys_.publicKey;
|
||||
}
|
||||
|
||||
/** Write keys to JSON file
|
||||
|
||||
@param keyFile Path to file to write
|
||||
|
||||
@note Overwrites existing key file
|
||||
|
||||
@throws std::runtime_error if unable to create parent directory
|
||||
*/
|
||||
void
|
||||
writeToFile(boost::filesystem::path const& keyFile) const;
|
||||
|
||||
/** Returns validator token for current sequence
|
||||
|
||||
@param keyType Key type for the token keys
|
||||
*/
|
||||
boost::optional<ValidatorToken>
|
||||
createValidatorToken(KeyType const& keyType = KeyType::Secp256k1);
|
||||
|
||||
/** Revokes validator keys
|
||||
|
||||
@return base64-encoded key revocation
|
||||
*/
|
||||
std::string
|
||||
revoke();
|
||||
|
||||
/** Signs string with validator key
|
||||
|
||||
@param data String to sign
|
||||
|
||||
@return hex-encoded signature
|
||||
*/
|
||||
std::string
|
||||
sign(std::string const& data) const;
|
||||
|
||||
/** Returns the public key. */
|
||||
PublicKey const&
|
||||
publicKey() const
|
||||
{
|
||||
return keys_.publicKey;
|
||||
}
|
||||
|
||||
/** Returns true if keys are revoked. */
|
||||
bool
|
||||
revoked() const
|
||||
{
|
||||
return revoked_;
|
||||
}
|
||||
|
||||
/** Returns the domain associated with this key, if any */
|
||||
std::string
|
||||
domain() const
|
||||
{
|
||||
return domain_;
|
||||
}
|
||||
|
||||
/** Sets the domain associated with this key */
|
||||
void
|
||||
domain(std::string d);
|
||||
|
||||
/** Returns the last manifest we generated for this domain, if available. */
|
||||
std::vector<std::uint8_t>
|
||||
manifest() const
|
||||
{
|
||||
return manifest_;
|
||||
}
|
||||
|
||||
/** Returns the sequence number of the last manifest generated. */
|
||||
std::uint32_t
|
||||
sequence() const
|
||||
{
|
||||
return tokenSequence_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
455
validator-keys-tool/src/ValidatorKeysTool.cpp
Normal file
455
validator-keys-tool/src/ValidatorKeysTool.cpp
Normal file
@@ -0,0 +1,455 @@
|
||||
#include <ValidatorKeysTool.h>
|
||||
|
||||
// cspell: words STRINGIZE
|
||||
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/beast/core/SemanticVersion.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/preprocessor/stringize.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
#include <ValidatorKeys.h>
|
||||
|
||||
#ifdef BOOST_MSVC
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// The build version number. You must edit this for each release
|
||||
// and follow the format described at http://semver.org/
|
||||
//--------------------------------------------------------------------------
|
||||
char const* const versionString =
|
||||
"0.3.2"
|
||||
|
||||
#if defined(DEBUG) || defined(SANITIZER)
|
||||
"+"
|
||||
#ifdef DEBUG
|
||||
"DEBUG"
|
||||
#ifdef SANITIZER
|
||||
"."
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef SANITIZER
|
||||
BOOST_PP_STRINGIZE(SANITIZER)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
;
|
||||
|
||||
static int
|
||||
runUnitTests()
|
||||
{
|
||||
using namespace beast::unit_test;
|
||||
reporter r;
|
||||
bool const anyFailed = r.runEach(globalSuites());
|
||||
if (anyFailed)
|
||||
return EXIT_FAILURE; // LCOV_EXCL_LINE
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
createKeyFile(boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
if (exists(keyFile))
|
||||
throw std::runtime_error("Refusing to overwrite existing key file: " + keyFile.string());
|
||||
|
||||
ValidatorKeys const keys(KeyType::Ed25519);
|
||||
keys.writeToFile(keyFile);
|
||||
|
||||
std::cout << "Validator keys stored in " << keyFile.string()
|
||||
<< "\n\nThis file should be stored securely and not shared.\n\n";
|
||||
}
|
||||
|
||||
void
|
||||
createToken(boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
|
||||
if (keys.revoked())
|
||||
throw std::runtime_error("Validator keys have been revoked.");
|
||||
|
||||
auto const token = keys.createValidatorToken();
|
||||
|
||||
if (!token)
|
||||
throw std::runtime_error(
|
||||
"Maximum number of tokens have already been generated.\n"
|
||||
"Revoke validator keys if previous token has been compromised.");
|
||||
|
||||
// Update key file with new token sequence
|
||||
keys.writeToFile(keyFile);
|
||||
|
||||
std::cout << "Update rippled.cfg file with these values and restart xrpld:\n\n";
|
||||
std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey())
|
||||
<< "\n\n";
|
||||
std::cout << "[validator_token]\n";
|
||||
|
||||
auto const tokenStr = token->toString();
|
||||
auto const len = 72;
|
||||
for (auto i = 0; i < tokenStr.size(); i += len)
|
||||
std::cout << tokenStr.substr(i, len) << std::endl;
|
||||
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
void
|
||||
createRevocation(boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
|
||||
if (keys.revoked())
|
||||
std::cout << "WARNING: Validator keys have already been revoked!\n\n";
|
||||
else
|
||||
std::cout << "WARNING: This will revoke your validator keys!\n\n";
|
||||
|
||||
auto const revocation = keys.revoke();
|
||||
|
||||
// Update key file with new token sequence
|
||||
keys.writeToFile(keyFile);
|
||||
|
||||
std::cout << "Update rippled.cfg file with these values and restart xrpld:\n\n";
|
||||
std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey())
|
||||
<< "\n\n";
|
||||
std::cout << "[validator_key_revocation]\n";
|
||||
|
||||
auto const len = 72;
|
||||
for (auto i = 0; i < revocation.size(); i += len)
|
||||
std::cout << revocation.substr(i, len) << std::endl;
|
||||
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
void
|
||||
attestDomain(xrpl::ValidatorKeys const& keys)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
if (keys.domain().empty())
|
||||
{
|
||||
std::cout << "No attestation is necessary if no domain is specified!\n";
|
||||
std::cout << "If you have an attestation in your xrpl-ledger.toml\n";
|
||||
std::cout << "you should remove it at this time.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "The domain attestation for validator "
|
||||
<< toBase58(TokenType::NodePublic, keys.publicKey()) << " is:\n\n";
|
||||
|
||||
std::cout << "attestation=\""
|
||||
<< keys.sign(
|
||||
"[domain-attestation-blob:" + keys.domain() + ":" +
|
||||
toBase58(TokenType::NodePublic, keys.publicKey()) + "]")
|
||||
<< "\"\n\n";
|
||||
|
||||
std::cout << "You should include it in your xrp-ledger.toml file in the\n";
|
||||
std::cout << "section for this validator.\n";
|
||||
}
|
||||
|
||||
void
|
||||
attestDomain(boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
|
||||
if (keys.revoked())
|
||||
throw std::runtime_error("Operation error: The specified master key has been revoked!");
|
||||
|
||||
attestDomain(keys);
|
||||
}
|
||||
|
||||
void
|
||||
setDomain(std::string const& domain, boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
|
||||
if (keys.revoked())
|
||||
throw std::runtime_error("Operation error: The specified master key has been revoked!");
|
||||
|
||||
if (domain == keys.domain())
|
||||
{
|
||||
if (domain.empty())
|
||||
std::cout << "The domain name was already cleared!\n";
|
||||
else
|
||||
std::cout << "The domain name was already set.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the domain and generate a new token
|
||||
keys.domain(domain);
|
||||
auto const token = keys.createValidatorToken();
|
||||
if (!token)
|
||||
throw std::runtime_error(
|
||||
"Maximum number of tokens have already been generated.\n"
|
||||
"Revoke validator keys if previous token has been compromised.");
|
||||
|
||||
// Flush to disk
|
||||
keys.writeToFile(keyFile);
|
||||
|
||||
if (domain.empty())
|
||||
std::cout << "The domain name has been cleared.\n";
|
||||
else
|
||||
std::cout << "The domain name has been set to: " << domain << "\n\n";
|
||||
attestDomain(keys);
|
||||
|
||||
std::cout << "\n";
|
||||
std::cout << "You also need to update the rippled.cfg file to add a new\n";
|
||||
std::cout << "validator token and restart xrpld:\n\n";
|
||||
std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey())
|
||||
<< "\n\n";
|
||||
std::cout << "[validator_token]\n";
|
||||
|
||||
auto const tokenStr = token->toString();
|
||||
auto const len = 72;
|
||||
for (auto i = 0; i < tokenStr.size(); i += len)
|
||||
std::cout << tokenStr.substr(i, len) << std::endl;
|
||||
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
void
|
||||
signData(std::string const& data, boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
if (data.empty())
|
||||
throw std::runtime_error("Syntax error: Must specify data string to sign");
|
||||
|
||||
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
|
||||
if (keys.revoked())
|
||||
std::cout << "WARNING: Validator keys have been revoked!\n\n";
|
||||
|
||||
std::cout << keys.sign(data) << std::endl;
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
void
|
||||
generateManifest(std::string const& type, boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace xrpl;
|
||||
|
||||
auto keys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
|
||||
auto const m = keys.manifest();
|
||||
|
||||
if (m.empty())
|
||||
{
|
||||
std::cout << "The last manifest generated is unavailable. You can\n";
|
||||
std::cout << "generate a new one.\n\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "base64")
|
||||
{
|
||||
std::cout << "Manifest #" << keys.sequence() << " (Base64):\n";
|
||||
std::cout << base64Encode(m.data(), m.size()) << "\n\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "hex")
|
||||
{
|
||||
std::cout << "Manifest #" << keys.sequence() << " (Hex):\n";
|
||||
std::cout << strHex(makeSlice(m)) << "\n\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Unknown encoding '" << type << "'\n";
|
||||
}
|
||||
|
||||
int
|
||||
runCommand(
|
||||
std::string const& command,
|
||||
std::vector<std::string> const& args,
|
||||
boost::filesystem::path const& keyFile)
|
||||
{
|
||||
using namespace std;
|
||||
|
||||
static map<string, vector<string>::size_type> const commandArgs = {
|
||||
{"create_keys", 0},
|
||||
{"create_token", 0},
|
||||
{"revoke_keys", 0},
|
||||
{"set_domain", 1},
|
||||
{"clear_domain", 0},
|
||||
{"attest_domain", 0},
|
||||
{"show_manifest", 1},
|
||||
{"sign", 1},
|
||||
};
|
||||
|
||||
auto const iArgs = commandArgs.find(command);
|
||||
|
||||
if (iArgs == commandArgs.end())
|
||||
throw std::runtime_error("Unknown command: " + command);
|
||||
|
||||
if (args.size() != iArgs->second)
|
||||
throw std::runtime_error("Syntax error: Wrong number of arguments");
|
||||
|
||||
if (command == "create_keys")
|
||||
createKeyFile(keyFile);
|
||||
else if (command == "create_token")
|
||||
createToken(keyFile);
|
||||
else if (command == "revoke_keys")
|
||||
createRevocation(keyFile);
|
||||
else if (command == "set_domain")
|
||||
setDomain(args[0], keyFile);
|
||||
else if (command == "clear_domain")
|
||||
setDomain("", keyFile);
|
||||
else if (command == "attest_domain")
|
||||
attestDomain(keyFile);
|
||||
else if (command == "sign")
|
||||
signData(args[0], keyFile);
|
||||
else if (command == "show_manifest")
|
||||
generateManifest(args[0], keyFile);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
static std::string
|
||||
getEnvVar(char const* name)
|
||||
{
|
||||
std::string value;
|
||||
|
||||
auto const v = getenv(name);
|
||||
|
||||
if (v != nullptr)
|
||||
value = v;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void
|
||||
printHelp(boost::program_options::options_description const& desc)
|
||||
{
|
||||
std::cerr << "validator-keys [options] <command> [<argument> ...]\n"
|
||||
<< desc << std::endl
|
||||
<< "Commands: \n"
|
||||
" create_keys Generate validator keys.\n"
|
||||
" create_token Generate validator token.\n"
|
||||
" revoke_keys Revoke validator keys.\n"
|
||||
" sign <data> Sign string with validator "
|
||||
"key.\n"
|
||||
" show_manifest [hex|base64] Displays the last generated "
|
||||
"manifest\n"
|
||||
" set_domain <domain> Associate a domain with the "
|
||||
"validator key.\n"
|
||||
" clear_domain Disassociate a domain from a "
|
||||
"validator key.\n"
|
||||
" attest_domain Produce the attestation string "
|
||||
"for a domain.\n";
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
std::string const&
|
||||
getVersionString()
|
||||
{
|
||||
static std::string const value = [] {
|
||||
std::string const s = versionString;
|
||||
beast::SemanticVersion v;
|
||||
if (!v.parse(s) || v.print() != s)
|
||||
throw std::logic_error(s + ": Bad version string"); // LCOV_EXCL_LINE
|
||||
return s;
|
||||
}();
|
||||
return value;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
namespace po = boost::program_options;
|
||||
|
||||
po::variables_map vm;
|
||||
|
||||
// Set up option parsing.
|
||||
//
|
||||
po::options_description general("General Options");
|
||||
general.add_options()("help,h", "Display this message.")(
|
||||
"keyfile", po::value<std::string>(), "Specify the key file.")(
|
||||
"unittest,u", "Perform unit tests.")("version", "Display the build version.");
|
||||
|
||||
po::options_description hidden("Hidden options");
|
||||
hidden.add_options()("command", po::value<std::string>(), "Command.")(
|
||||
"arguments",
|
||||
po::value<std::vector<std::string>>()->default_value(std::vector<std::string>(), "empty"),
|
||||
"Arguments.");
|
||||
po::positional_options_description p;
|
||||
p.add("command", 1).add("arguments", -1);
|
||||
|
||||
po::options_description cmdline_options;
|
||||
cmdline_options.add(general).add(hidden);
|
||||
|
||||
// Parse options, if no error.
|
||||
try
|
||||
{
|
||||
po::store(
|
||||
po::command_line_parser(argc, argv)
|
||||
.options(cmdline_options) // Parse options.
|
||||
.positional(p)
|
||||
.run(),
|
||||
vm);
|
||||
po::notify(vm); // Invoke option notify functions.
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
catch (std::exception const&)
|
||||
{
|
||||
std::cerr << "validator-keys: Incorrect command line syntax." << std::endl;
|
||||
std::cerr << "Use '--help' for a list of options." << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
// Run the unit tests if requested.
|
||||
// The unit tests will exit the application with an appropriate return code.
|
||||
if (vm.count("unittest"))
|
||||
return runUnitTests();
|
||||
|
||||
// LCOV_EXCL_START
|
||||
if (vm.count("version"))
|
||||
{
|
||||
std::cout << "validator-keys version " << getVersionString() << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (vm.count("help") || !vm.count("command"))
|
||||
{
|
||||
printHelp(general);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
std::string const homeDir = getEnvVar("HOME");
|
||||
std::string const defaultKeyFile =
|
||||
(homeDir.empty() ? boost::filesystem::current_path().string() : homeDir) +
|
||||
"/.ripple/validator-keys.json";
|
||||
|
||||
try
|
||||
{
|
||||
using namespace boost::filesystem;
|
||||
path keyFile = vm.count("keyfile") ? vm["keyfile"].as<std::string>() : defaultKeyFile;
|
||||
|
||||
return runCommand(
|
||||
vm["command"].as<std::string>(),
|
||||
vm["arguments"].as<std::vector<std::string>>(),
|
||||
keyFile);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
std::cerr << e.what() << "\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
30
validator-keys-tool/src/ValidatorKeysTool.h
Normal file
30
validator-keys-tool/src/ValidatorKeysTool.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace boost {
|
||||
namespace filesystem {
|
||||
class path;
|
||||
}
|
||||
} // namespace boost
|
||||
|
||||
std::string const&
|
||||
getVersionString();
|
||||
|
||||
void
|
||||
createKeyFile(boost::filesystem::path const& keyFile);
|
||||
|
||||
void
|
||||
createToken(boost::filesystem::path const& keyFile);
|
||||
|
||||
void
|
||||
createRevocation(boost::filesystem::path const& keyFile);
|
||||
|
||||
void
|
||||
signData(std::string const& data, boost::filesystem::path const& keyFile);
|
||||
|
||||
int
|
||||
runCommand(
|
||||
std::string const& command,
|
||||
std::vector<std::string> const& arg,
|
||||
boost::filesystem::path const& keyFile);
|
||||
58
validator-keys-tool/src/test/KeyFileGuard.h
Normal file
58
validator-keys-tool/src/test/KeyFileGuard.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
Write a key file dir and remove when done.
|
||||
*/
|
||||
class KeyFileGuard
|
||||
{
|
||||
private:
|
||||
using path = boost::filesystem::path;
|
||||
path subDir_;
|
||||
beast::unit_test::Suite& test_;
|
||||
|
||||
auto
|
||||
rmDir(path const& toRm)
|
||||
{
|
||||
if (is_directory(toRm))
|
||||
remove_all(toRm);
|
||||
else
|
||||
test_.log << "Expected " << toRm.string() << " to be an existing directory."
|
||||
<< std::endl;
|
||||
};
|
||||
|
||||
public:
|
||||
KeyFileGuard(beast::unit_test::Suite& test, std::string const& subDir)
|
||||
: subDir_(subDir), test_(test)
|
||||
{
|
||||
using namespace boost::filesystem;
|
||||
|
||||
if (!exists(subDir_))
|
||||
create_directory(subDir_);
|
||||
else
|
||||
// Cannot run the test. Someone created a file or directory
|
||||
// where we want to put our directory
|
||||
throw std::runtime_error("Cannot create directory: " + subDir_.string());
|
||||
}
|
||||
~KeyFileGuard()
|
||||
{
|
||||
try
|
||||
{
|
||||
using namespace boost::filesystem;
|
||||
|
||||
rmDir(subDir_);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
// if we throw here, just let it die.
|
||||
test_.log << "Error in ~KeyFileGuard: " << e.what() << std::endl;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
287
validator-keys-tool/src/test/ValidatorKeysTool_test.cpp
Normal file
287
validator-keys-tool/src/test/ValidatorKeysTool_test.cpp
Normal file
@@ -0,0 +1,287 @@
|
||||
#include <test/KeyFileGuard.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <ValidatorKeys.h>
|
||||
#include <ValidatorKeysTool.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace tests {
|
||||
|
||||
class ValidatorKeysTool_test : public beast::unit_test::Suite
|
||||
{
|
||||
private:
|
||||
// Allow cout to be redirected. Destructor restores old cout streambuf.
|
||||
class CoutRedirect
|
||||
{
|
||||
public:
|
||||
CoutRedirect(std::stringstream& sStream) : old_(std::cout.rdbuf(sStream.rdbuf()))
|
||||
{
|
||||
}
|
||||
|
||||
~CoutRedirect()
|
||||
{
|
||||
std::cout.rdbuf(old_);
|
||||
}
|
||||
|
||||
private:
|
||||
std::streambuf* const old_;
|
||||
};
|
||||
|
||||
void
|
||||
testCreateKeyFile()
|
||||
{
|
||||
testcase("Create Key File");
|
||||
|
||||
std::stringstream coutCapture;
|
||||
CoutRedirect coutRedirect{coutCapture};
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
path const subdir = "test_key_file";
|
||||
KeyFileGuard const g(*this, subdir.string());
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
|
||||
createKeyFile(keyFile);
|
||||
BEAST_EXPECT(exists(keyFile));
|
||||
|
||||
std::string const expectedError =
|
||||
"Refusing to overwrite existing key file: " + keyFile.string();
|
||||
std::string error;
|
||||
try
|
||||
{
|
||||
createKeyFile(keyFile);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
BEAST_EXPECT(error == expectedError);
|
||||
}
|
||||
|
||||
void
|
||||
testCreateToken()
|
||||
{
|
||||
testcase("Create Token");
|
||||
|
||||
std::stringstream coutCapture;
|
||||
CoutRedirect coutRedirect{coutCapture};
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
path const subdir = "test_key_file";
|
||||
KeyFileGuard const g(*this, subdir.string());
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
|
||||
auto testToken = [this](path const& keyFile, std::string const& expectedError) {
|
||||
try
|
||||
{
|
||||
createToken(keyFile);
|
||||
BEAST_EXPECT(expectedError.empty());
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BEAST_EXPECT(e.what() == expectedError);
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
std::string const expectedError = "Failed to open key file: " + keyFile.string();
|
||||
testToken(keyFile, expectedError);
|
||||
}
|
||||
|
||||
createKeyFile(keyFile);
|
||||
|
||||
{
|
||||
std::string const expectedError = "";
|
||||
testToken(keyFile, expectedError);
|
||||
}
|
||||
{
|
||||
auto const keyType = KeyType::Ed25519;
|
||||
auto const kp = generateKeyPair(keyType, randomSeed());
|
||||
|
||||
auto keys =
|
||||
ValidatorKeys(keyType, kp.second, std::numeric_limits<std::uint32_t>::max() - 1);
|
||||
|
||||
keys.writeToFile(keyFile);
|
||||
std::string const expectedError =
|
||||
"Maximum number of tokens have already been generated.\n"
|
||||
"Revoke validator keys if previous token has been compromised.";
|
||||
testToken(keyFile, expectedError);
|
||||
}
|
||||
{
|
||||
createRevocation(keyFile);
|
||||
std::string const expectedError = "Validator keys have been revoked.";
|
||||
testToken(keyFile, expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreateRevocation()
|
||||
{
|
||||
testcase("Create Revocation");
|
||||
|
||||
std::stringstream coutCapture;
|
||||
CoutRedirect coutRedirect{coutCapture};
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
path const subdir = "test_key_file";
|
||||
KeyFileGuard const g(*this, subdir.string());
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
|
||||
auto expectedError = "Failed to open key file: " + keyFile.string();
|
||||
std::string error;
|
||||
try
|
||||
{
|
||||
createRevocation(keyFile);
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
BEAST_EXPECT(error == expectedError);
|
||||
|
||||
createKeyFile(keyFile);
|
||||
BEAST_EXPECT(exists(keyFile));
|
||||
|
||||
createRevocation(keyFile);
|
||||
createRevocation(keyFile);
|
||||
}
|
||||
|
||||
void
|
||||
testSign()
|
||||
{
|
||||
testcase("Sign");
|
||||
|
||||
std::stringstream coutCapture;
|
||||
CoutRedirect coutRedirect{coutCapture};
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
auto testSign =
|
||||
[this](std::string const& data, path const& keyFile, std::string const& expectedError) {
|
||||
try
|
||||
{
|
||||
signData(data, keyFile);
|
||||
BEAST_EXPECT(expectedError.empty());
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BEAST_EXPECT(e.what() == expectedError);
|
||||
}
|
||||
};
|
||||
|
||||
std::string const data = "data to sign";
|
||||
|
||||
path const subdir = "test_key_file";
|
||||
KeyFileGuard const g(*this, subdir.string());
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
|
||||
{
|
||||
std::string const expectedError = "Failed to open key file: " + keyFile.string();
|
||||
testSign(data, keyFile, expectedError);
|
||||
}
|
||||
|
||||
createKeyFile(keyFile);
|
||||
BEAST_EXPECT(exists(keyFile));
|
||||
|
||||
{
|
||||
std::string const emptyData = "";
|
||||
std::string const expectedError = "Syntax error: Must specify data string to sign";
|
||||
testSign(emptyData, keyFile, expectedError);
|
||||
}
|
||||
{
|
||||
std::string const expectedError = "";
|
||||
testSign(data, keyFile, expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRunCommand()
|
||||
{
|
||||
testcase("Run Command");
|
||||
|
||||
std::stringstream coutCapture;
|
||||
CoutRedirect coutRedirect{coutCapture};
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
path const subdir = "test_key_file";
|
||||
KeyFileGuard g(*this, subdir.string());
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
|
||||
auto testCommand = [this](
|
||||
std::string const& command,
|
||||
std::vector<std::string> const& args,
|
||||
path const& keyFile,
|
||||
std::string const& expectedError) {
|
||||
try
|
||||
{
|
||||
runCommand(command, args, keyFile);
|
||||
BEAST_EXPECT(expectedError.empty());
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BEAST_EXPECT(e.what() == expectedError);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::string> const noArgs;
|
||||
std::vector<std::string> const oneArg = {"some data"};
|
||||
std::vector<std::string> const twoArgs = {"data", "more data"};
|
||||
std::string const noError = "";
|
||||
std::string const argError = "Syntax error: Wrong number of arguments";
|
||||
{
|
||||
std::string const command = "unknown";
|
||||
std::string const expectedError = "Unknown command: " + command;
|
||||
testCommand(command, noArgs, keyFile, expectedError);
|
||||
testCommand(command, oneArg, keyFile, expectedError);
|
||||
testCommand(command, twoArgs, keyFile, expectedError);
|
||||
}
|
||||
{
|
||||
std::string const command = "create_keys";
|
||||
testCommand(command, noArgs, keyFile, noError);
|
||||
testCommand(command, oneArg, keyFile, argError);
|
||||
testCommand(command, twoArgs, keyFile, argError);
|
||||
}
|
||||
{
|
||||
std::string const command = "create_token";
|
||||
testCommand(command, noArgs, keyFile, noError);
|
||||
testCommand(command, oneArg, keyFile, argError);
|
||||
testCommand(command, twoArgs, keyFile, argError);
|
||||
}
|
||||
{
|
||||
std::string const command = "revoke_keys";
|
||||
testCommand(command, noArgs, keyFile, noError);
|
||||
testCommand(command, oneArg, keyFile, argError);
|
||||
testCommand(command, twoArgs, keyFile, argError);
|
||||
}
|
||||
{
|
||||
std::string const command = "sign";
|
||||
testCommand(command, noArgs, keyFile, argError);
|
||||
testCommand(command, oneArg, keyFile, noError);
|
||||
testCommand(command, twoArgs, keyFile, argError);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
getVersionString();
|
||||
|
||||
testCreateKeyFile();
|
||||
testCreateToken();
|
||||
testCreateRevocation();
|
||||
testSign();
|
||||
testRunCommand();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ValidatorKeysTool, keys, xrpl);
|
||||
|
||||
} // namespace tests
|
||||
|
||||
} // namespace xrpl
|
||||
374
validator-keys-tool/src/test/ValidatorKeys_test.cpp
Normal file
374
validator-keys-tool/src/test/ValidatorKeys_test.cpp
Normal file
@@ -0,0 +1,374 @@
|
||||
#include <test/KeyFileGuard.h>
|
||||
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
#include <ValidatorKeys.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace tests {
|
||||
|
||||
class ValidatorKeys_test : public beast::unit_test::Suite
|
||||
{
|
||||
private:
|
||||
void
|
||||
testKeyFile(
|
||||
boost::filesystem::path const& keyFile,
|
||||
json::Value const& jv,
|
||||
std::string const& expectedError)
|
||||
{
|
||||
{
|
||||
std::ofstream o(keyFile.string(), std::ios_base::trunc);
|
||||
o << jv.toStyledString();
|
||||
o.close();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
BEAST_EXPECT(expectedError.empty());
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
BEAST_EXPECT(e.what() == expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
std::array<KeyType, 2> const keyTypes{{KeyType::Ed25519, KeyType::Secp256k1}};
|
||||
|
||||
void
|
||||
testMakeValidatorKeys()
|
||||
{
|
||||
testcase("Make Validator Keys");
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
path const subdir = "test_key_file";
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
|
||||
for (auto const keyType : keyTypes)
|
||||
{
|
||||
ValidatorKeys const keys(keyType);
|
||||
|
||||
KeyFileGuard const g(*this, subdir.string());
|
||||
|
||||
keys.writeToFile(keyFile);
|
||||
BEAST_EXPECT(exists(keyFile));
|
||||
|
||||
auto const keys2 = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
BEAST_EXPECT(keys == keys2);
|
||||
}
|
||||
{
|
||||
// Require expected fields
|
||||
KeyFileGuard g(*this, subdir.string());
|
||||
|
||||
auto expectedError = "Failed to open key file: " + keyFile.string();
|
||||
std::string error;
|
||||
try
|
||||
{
|
||||
ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
BEAST_EXPECT(error == expectedError);
|
||||
|
||||
expectedError = "Unable to parse json key file: " + keyFile.string();
|
||||
|
||||
{
|
||||
std::ofstream o(keyFile.string(), std::ios_base::trunc);
|
||||
o << "{{}";
|
||||
o.close();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
BEAST_EXPECT(error == expectedError);
|
||||
|
||||
json::Value jv;
|
||||
jv["dummy"] = "field";
|
||||
expectedError = "Key file '" + keyFile.string() + "' is missing \"key_type\" field";
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["key_type"] = "dummy keytype";
|
||||
expectedError = "Key file '" + keyFile.string() + "' is missing \"secret_key\" field";
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["secret_key"] = "dummy secret";
|
||||
expectedError =
|
||||
"Key file '" + keyFile.string() + "' is missing \"token_sequence\" field";
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["token_sequence"] = "dummy sequence";
|
||||
expectedError = "Key file '" + keyFile.string() + "' is missing \"revoked\" field";
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["revoked"] = "dummy revoked";
|
||||
expectedError = "Key file '" + keyFile.string() +
|
||||
"' contains invalid \"key_type\" field: " + jv["key_type"].toStyledString();
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
auto const keyType = KeyType::Ed25519;
|
||||
jv["key_type"] = to_string(keyType);
|
||||
expectedError = "Key file '" + keyFile.string() +
|
||||
"' contains invalid \"secret_key\" field: " + jv["secret_key"].toStyledString();
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
ValidatorKeys const keys(keyType);
|
||||
{
|
||||
auto const kp = generateKeyPair(keyType, randomSeed());
|
||||
jv["secret_key"] = toBase58(TokenType::NodePrivate, kp.second);
|
||||
}
|
||||
expectedError = "Key file '" + keyFile.string() +
|
||||
"' contains invalid \"token_sequence\" field: " +
|
||||
jv["token_sequence"].toStyledString();
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["token_sequence"] = -1;
|
||||
expectedError = "Key file '" + keyFile.string() +
|
||||
"' contains invalid \"token_sequence\" field: " +
|
||||
jv["token_sequence"].toStyledString();
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["token_sequence"] = json::UInt(std::numeric_limits<std::uint32_t>::max());
|
||||
expectedError = "Key file '" + keyFile.string() +
|
||||
"' contains invalid \"revoked\" field: " + jv["revoked"].toStyledString();
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["revoked"] = false;
|
||||
expectedError = "";
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
|
||||
jv["revoked"] = true;
|
||||
testKeyFile(keyFile, jv, expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreateValidatorToken()
|
||||
{
|
||||
testcase("Create Validator Token");
|
||||
|
||||
for (auto const keyType : keyTypes)
|
||||
{
|
||||
ValidatorKeys keys(keyType);
|
||||
std::uint32_t sequence = 0;
|
||||
|
||||
for (auto const tokenKeyType : keyTypes)
|
||||
{
|
||||
auto const token = keys.createValidatorToken(tokenKeyType);
|
||||
|
||||
if (!BEAST_EXPECT(token))
|
||||
continue;
|
||||
|
||||
auto const tokenPublicKey = derivePublicKey(tokenKeyType, token->secretKey);
|
||||
|
||||
STObject st(sfGeneric);
|
||||
auto const manifest = xrpl::base64Decode(token->manifest);
|
||||
SerialIter sit(manifest.data(), manifest.size());
|
||||
st.set(sit);
|
||||
|
||||
auto const seq = get(st, sfSequence);
|
||||
BEAST_EXPECT(seq);
|
||||
BEAST_EXPECT(*seq == ++sequence);
|
||||
|
||||
auto const tpk = get<PublicKey>(st, sfSigningPubKey);
|
||||
BEAST_EXPECT(tpk);
|
||||
BEAST_EXPECT(*tpk == tokenPublicKey);
|
||||
BEAST_EXPECT(verify(st, HashPrefix::Manifest, tokenPublicKey));
|
||||
|
||||
auto const pk = get<PublicKey>(st, sfPublicKey);
|
||||
BEAST_EXPECT(pk);
|
||||
BEAST_EXPECT(*pk == keys.publicKey());
|
||||
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
|
||||
}
|
||||
}
|
||||
|
||||
auto const keyType = KeyType::Ed25519;
|
||||
auto const kp = generateKeyPair(keyType, randomSeed());
|
||||
|
||||
auto keys =
|
||||
ValidatorKeys(keyType, kp.second, std::numeric_limits<std::uint32_t>::max() - 1);
|
||||
|
||||
BEAST_EXPECT(!keys.createValidatorToken(keyType));
|
||||
|
||||
keys.revoke();
|
||||
BEAST_EXPECT(!keys.createValidatorToken(keyType));
|
||||
}
|
||||
|
||||
void
|
||||
testRevoke()
|
||||
{
|
||||
testcase("Revoke");
|
||||
|
||||
for (auto const keyType : keyTypes)
|
||||
{
|
||||
ValidatorKeys keys(keyType);
|
||||
|
||||
auto const revocation = keys.revoke();
|
||||
|
||||
STObject st(sfGeneric);
|
||||
auto const manifest = xrpl::base64Decode(revocation);
|
||||
SerialIter sit(manifest.data(), manifest.size());
|
||||
st.set(sit);
|
||||
|
||||
auto const seq = get(st, sfSequence);
|
||||
BEAST_EXPECT(seq);
|
||||
BEAST_EXPECT(*seq == std::numeric_limits<std::uint32_t>::max());
|
||||
|
||||
auto const pk = get(st, sfPublicKey);
|
||||
BEAST_EXPECT(pk);
|
||||
BEAST_EXPECT(*pk == keys.publicKey());
|
||||
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSign()
|
||||
{
|
||||
testcase("Sign");
|
||||
|
||||
std::map<KeyType, std::string> expected(
|
||||
{{KeyType::Ed25519,
|
||||
"2EE541D6825791BF5454C571D2B363EAB3F01C73159B1F"
|
||||
"237AC6D38663A82B9D5EAD262D5F776B916E68247A1F082090F3BAE7ABC939"
|
||||
"C8F29B0DC759FD712300"},
|
||||
{KeyType::Secp256k1,
|
||||
"3045022100F142C27BF83D8D4541C7A4E759DE64A672"
|
||||
"51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5"
|
||||
"55438D64A229CD0B4BA2AE33402443B20409"}});
|
||||
|
||||
std::string const data = "data to sign";
|
||||
|
||||
for (auto const keyType : keyTypes)
|
||||
{
|
||||
auto const sk = generateSecretKey(keyType, generateSeed("test"));
|
||||
ValidatorKeys keys(keyType, sk, 1);
|
||||
|
||||
auto const signature = keys.sign(data);
|
||||
BEAST_EXPECT(expected[keyType] == signature);
|
||||
|
||||
auto const ret = strUnHex(signature);
|
||||
BEAST_EXPECT(ret);
|
||||
BEAST_EXPECT(ret->size());
|
||||
BEAST_EXPECT(verify(keys.publicKey(), makeSlice(data), makeSlice(*ret)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWriteToFile()
|
||||
{
|
||||
testcase("Write to File");
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
auto const keyType = KeyType::Ed25519;
|
||||
ValidatorKeys keys(keyType);
|
||||
|
||||
{
|
||||
path const subdir = "test_key_file";
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
KeyFileGuard g(*this, subdir.string());
|
||||
|
||||
keys.writeToFile(keyFile);
|
||||
BEAST_EXPECT(exists(keyFile));
|
||||
|
||||
auto fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
BEAST_EXPECT(keys == fileKeys);
|
||||
|
||||
// Overwrite file with new sequence
|
||||
keys.createValidatorToken(KeyType::Secp256k1);
|
||||
keys.writeToFile(keyFile);
|
||||
|
||||
fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
BEAST_EXPECT(keys == fileKeys);
|
||||
}
|
||||
{
|
||||
// Write to key file in current relative directory
|
||||
path const keyFile = "test_validator_keys.json";
|
||||
if (!exists(keyFile))
|
||||
{
|
||||
keys.writeToFile(keyFile);
|
||||
remove(keyFile.string());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cannot run the test. Someone created a file
|
||||
// where we want to put our key file
|
||||
Throw<std::runtime_error>("Cannot create key file: " + keyFile.string());
|
||||
}
|
||||
}
|
||||
{
|
||||
// Create key file directory
|
||||
path const subdir = "test_key_file";
|
||||
path const keyFile = subdir / "directories/to/create/validator_keys.json";
|
||||
KeyFileGuard g(*this, subdir.string());
|
||||
|
||||
keys.writeToFile(keyFile);
|
||||
BEAST_EXPECT(exists(keyFile));
|
||||
|
||||
auto const fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile);
|
||||
BEAST_EXPECT(keys == fileKeys);
|
||||
}
|
||||
{
|
||||
// Fail if file cannot be opened for write
|
||||
path const subdir = "test_key_file";
|
||||
KeyFileGuard g(*this, subdir.string());
|
||||
|
||||
path const badKeyFile = subdir / ".";
|
||||
auto expectedError = "Cannot open key file: " + badKeyFile.string();
|
||||
std::string error;
|
||||
try
|
||||
{
|
||||
keys.writeToFile(badKeyFile);
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
BEAST_EXPECT(error == expectedError);
|
||||
|
||||
// Fail if parent directory is existing file
|
||||
path const keyFile = subdir / "validator_keys.json";
|
||||
keys.writeToFile(keyFile);
|
||||
path const conflictingPath = keyFile / "validators_keys.json";
|
||||
expectedError = "Cannot create directory: " + conflictingPath.parent_path().string();
|
||||
try
|
||||
{
|
||||
keys.writeToFile(conflictingPath);
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
error = e.what();
|
||||
}
|
||||
BEAST_EXPECT(error == expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testMakeValidatorKeys();
|
||||
testCreateValidatorToken();
|
||||
testRevoke();
|
||||
testSign();
|
||||
testWriteToFile();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ValidatorKeys, keys, xrpl);
|
||||
|
||||
} // namespace tests
|
||||
|
||||
} // namespace xrpl
|
||||
Reference in New Issue
Block a user