Compare commits

..

2 Commits

Author SHA1 Message Date
Michael Legleux
06337079f0 DROP: limit CI to validator keys package checks 2026-06-16 10:47:25 -07:00
Michael Legleux
bd8351c653 ci: test validator keys tool as libxrpl consumer 2026-06-16 10:47:06 -07:00
72 changed files with 2944 additions and 1111 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -133,9 +133,9 @@ endif()
include(XrplCore)
include(XrplProtocolAutogen)
include(XrplValidatorKeys)
include(XrplInstall)
include(XrplPackaging)
include(XrplValidatorKeys)
if(tests)
include(CTest)

View File

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

View File

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

View 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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1,2 @@
README.md
LICENSE.md

View File

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

View File

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

View 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

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Daily xrpld update check
[Timer]
OnCalendar=*-*-* 00:00:00
RandomizedDelaySec=4h
Persistent=true
[Install]
WantedBy=timers.target

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,2 @@
# Conan test_package build output (cmake_layout)
/build/

View File

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

View File

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

View File

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

View 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

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

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

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

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

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

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

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

View 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

View 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

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

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

View 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

View 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

View 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