Compare commits

..

8 Commits

Author SHA1 Message Date
Bart
70cb8c27a8 Merge branch 'develop' into bthomee/dispatch 2026-06-15 11:41:49 -04:00
Bart
49da582a32 Revert dispatch to post due to deadlock 2026-06-15 10:03:16 -04:00
Bart
e9c21a8d08 Do not capture this 2026-06-10 09:41:40 -04:00
Bart
e57d5ac775 Review feedback 2026-06-09 13:44:05 -04:00
Bart
7e9c12b360 refactor: Use dispatch instead of post 2026-06-09 13:28:15 -04:00
Bart
d0761744b4 Use dispatch instead of post 2026-06-09 13:27:33 -04:00
Bart
85f6358a79 Merge branch 'develop' into bthomee/peer_charge 2026-06-09 13:04:17 -04:00
Bart
2a73e11f51 fix: Always charge peer on strand 2026-06-06 17:52:00 -04:00
58 changed files with 878 additions and 3379 deletions

View File

@@ -1,12 +1,56 @@
{
"image_tag": "sha-fe4c8ae",
"image_tag": "sha-63ffdc3",
"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"],
"extra_cmake_args": "-Dvalidator_keys=ON"
"arch": ["amd64"]
}
],
@@ -14,8 +58,7 @@
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"extra_cmake_args": "-Dvalidator_keys=ON"
"arch": ["amd64"]
}
]
},
@@ -25,7 +68,7 @@
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745"
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-63ffdc3"
}
],
@@ -34,7 +77,7 @@
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745"
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-63ffdc3"
}
]
}

View File

@@ -9,20 +9,12 @@ on:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "bin/check-tools.sh"
- "bin/install-sanitizer-libs.sh"
pull_request:
paths:
- ".github/workflows/build-nix-images.yml"
- "flake.nix"
- "flake.lock"
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "bin/check-tools.sh"
- "bin/install-sanitizer-libs.sh"
workflow_dispatch:
concurrency:

View File

@@ -67,7 +67,6 @@ 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
@@ -78,7 +77,6 @@ jobs:
include/**
src/**
tests/**
validator-keys-tool/**
CMakeLists.txt
conanfile.py
conan.lock
@@ -105,6 +103,27 @@ 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' }}
@@ -112,7 +131,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [linux]
os: [linux, macos, windows]
with:
# Enable ccache only for events targeting the XRPLF repository, since
# other accounts will not have access to our remote cache storage.
@@ -126,17 +145,43 @@ jobs:
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-package.yml
test-conan-package:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-test-conan-package.yml
upload-recipe:
needs:
- should-run
- build-test
# 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}"
passed:
if: failure() || cancelled()
needs:
- check-levelization
- check-rename
- clang-tidy
- build-test
- package
- test-conan-package
- upload-recipe
- notify-clio
runs-on: ubuntu-latest
steps:
- name: Fail

View File

@@ -16,13 +16,8 @@ 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,7 +24,6 @@ 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"
@@ -35,7 +34,6 @@ on:
- "include/**"
- "src/**"
- "tests/**"
- "validator-keys-tool/**"
- "CMakeLists.txt"
- "conanfile.py"
- "conan.lock"
@@ -67,12 +65,21 @@ 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]
os: [linux, macos, windows]
with:
# Enable ccache only for events targeting the XRPLF repository, since
# other accounts will not have access to our remote cache storage.
@@ -84,8 +91,14 @@ jobs:
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-conan-package:
uses: ./.github/workflows/reusable-test-conan-package.yml
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 }}
package:
needs: build-test

View File

@@ -41,7 +41,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -121,11 +121,6 @@ jobs:
if: ${{ inputs.ccache_enabled && runner.debug == '1' }}
run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >>"${GITHUB_ENV}"
- name: Check tools
env:
CHECK_TOOLS_SKIP_CLONE: "1"
run: ./bin/check-tools.sh
- name: Print build environment
uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574
@@ -169,27 +164,6 @@ jobs:
${CMAKE_ARGS} \
..
# Export the sanitizer options before any instrumented binary runs. The
# protocol code-gen and build steps below invoke instrumented dependency
# tools (protoc, grpc), so setting UBSAN_OPTIONS here lets the UBSan
# suppression list silence their diagnostics too, not just at test time.
# GITHUB_WORKSPACE (not the github.workspace context) is used so the path
# resolves correctly inside the container job.
- name: Set sanitizer options
if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }}
env:
CONFIG_NAME: ${{ inputs.config_name }}
run: |
SUPP="${GITHUB_WORKSPACE}/sanitizers/suppressions"
ASAN_OPTS="include=${SUPP}/runtime-asan-options.txt:suppressions=${SUPP}/asan.supp"
if [[ "${CONFIG_NAME}" == *gcc* ]]; then
ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0"
fi
echo "ASAN_OPTIONS=${ASAN_OPTS}" >>${GITHUB_ENV}
echo "TSAN_OPTIONS=include=${SUPP}/runtime-tsan-options.txt:suppressions=${SUPP}/tsan.supp" >>${GITHUB_ENV}
echo "UBSAN_OPTIONS=include=${SUPP}/runtime-ubsan-options.txt:suppressions=${SUPP}/ubsan.supp" >>${GITHUB_ENV}
echo "LSAN_OPTIONS=include=${SUPP}/runtime-lsan-options.txt:suppressions=${SUPP}/lsan.supp" >>${GITHUB_ENV}
- name: Check protocol autogen files are up-to-date
working-directory: ${{ env.BUILD_DIR }}
env:
@@ -235,9 +209,6 @@ 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)
@@ -256,21 +227,12 @@ 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 }}/artifacts/*
path: ${{ env.BUILD_DIR }}/xrpld
retention-days: 3
if-no-files-found: error
@@ -317,6 +279,20 @@ jobs:
run: |
./xrpld --version | grep libvoidstar
- name: Set sanitizer options
if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }}
env:
CONFIG_NAME: ${{ inputs.config_name }}
run: |
ASAN_OPTS="include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-asan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp"
if [[ "${CONFIG_NAME}" == *gcc* ]]; then
ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0"
fi
echo "ASAN_OPTIONS=${ASAN_OPTS}" >>${GITHUB_ENV}
echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >>${GITHUB_ENV}
echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >>${GITHUB_ENV}
echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >>${GITHUB_ENV}
- name: Run the separate tests
if: ${{ !inputs.build_only }}
working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}

View File

@@ -36,7 +36,7 @@ jobs:
needs: [determine-files]
if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-fe4c8ae"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-63ffdc3"
permissions:
contents: read
issues: write

View File

@@ -77,8 +77,8 @@ jobs:
name: ${{ matrix.artifact_name }}
path: ${{ env.BUILD_DIR }}
- name: Make binaries executable
run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys"
- name: Make binary executable
run: chmod +x "${BUILD_DIR}/xrpld"
- name: Build package
env:

View File

@@ -1,35 +0,0 @@
# 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

@@ -40,7 +40,7 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

391
BUILD.md
View File

@@ -1,57 +1,26 @@
| :warning: **WARNING** :warning: |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
| :warning: **WARNING** :warning: |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md). |
## Minimum Requirements
> These instructions also assume a basic familiarity with Conan and CMake.
> If you are unfamiliar with Conan, you can read our
> [crash course](./docs/build/conan.md) or the official [Getting Started][3]
> walkthrough.
See [System Requirements](https://xrpl.org/system-requirements.html).
## Branches
Building xrpld generally requires Git, Python, Conan, CMake, and a C++
compiler.
- [Python](https://www.python.org/downloads/)
- [Conan](https://conan.io/downloads.html)
- [CMake](https://cmake.org/download/)
You can verify that the required tools are installed and runnable with:
For a stable release, choose the `master` branch or one of the [tagged
releases](https://github.com/XRPLF/rippled/releases).
```bash
./bin/check-tools.sh
git checkout master
```
`xrpld` is written in the C++23 dialect. The [tested compiler versions][cpp23-support] are:
For the latest release candidate, choose the `release` branch.
| Compiler | Version |
| ----------- | --------------- |
| GCC | 15.2 |
| Clang | 22 |
| Apple Clang | 17 |
| MSVC | 19.44[^windows] |
## Operating Systems
Please see the [environment setup guide](./docs/build/environment.md) for detailed instructions for all platforms.
### Linux
The Ubuntu Linux distribution has received the highest level of quality
assurance, testing, and support. We also support Red Hat and use Debian
internally.
Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them.
### macOS
Many `xrpld` engineers use macOS for development.
### Windows
Windows is used by some engineers for development only.
[^windows]: Windows is not recommended for production use.
## Steps
### Branches
```bash
git checkout release
```
For the latest set of untested features, or to contribute, choose the `develop`
branch.
@@ -60,15 +29,55 @@ branch.
git checkout develop
```
For a release candidate, choose the relevant release branch, e.g.
`release/3.2.x`.
## Minimum Requirements
```bash
git checkout release/3.2.x
```
See [System Requirements](https://xrpl.org/system-requirements.html).
For a stable release, choose one of the [tagged
releases](https://github.com/XRPLF/rippled/releases).
Building xrpld generally requires git, Python, Conan, CMake, and a C++
compiler. Some guidance on setting up such a [C++ development environment can be
found here](./docs/build/environment.md).
- [Python 3.11](https://www.python.org/downloads/), or higher
- [Conan 2.17](https://conan.io/downloads.html)[^1], or higher
- [CMake 3.22](https://cmake.org/download/), or higher
[^1]:
It is possible to build with Conan 1.60+, but the instructions are
significantly different, which is why we are not recommending it.
`xrpld` is written in the C++23 dialect and includes the `<concepts>` header.
The [tested compiler versions][2] are:
| Compiler | Version |
| ----------- | --------- |
| GCC | 15 |
| Clang | 22 |
| Apple Clang | 17 |
| MSVC | 19.44[^3] |
### Linux
The Ubuntu Linux distribution has received the highest level of quality
assurance, testing, and support. We also support Red Hat and use Debian
internally.
Here are [sample instructions for setting up a C++ development environment on
Linux](./docs/build/environment.md#linux).
### Mac
Many xrpld engineers use macOS for development.
Here are [sample instructions for setting up a C++ development environment on
macOS](./docs/build/environment.md#macos).
### Windows
Windows is used by some engineers for development only.
[^3]: Windows is not recommended for production use.
## Steps
### Set Up Conan
@@ -77,11 +86,18 @@ Conan, CMake, and a C++ compiler, you may need to set up your Conan profile.
These instructions assume a basic familiarity with Conan and CMake. If you are
unfamiliar with Conan, then please read [this crash course](./docs/build/conan.md) or the official
[Getting Started][conan-getting-started] walkthrough.
[Getting Started][3] walkthrough.
#### Profiles
#### Conan lockfile
We recommend that you install our Conan profiles:
To achieve reproducible dependencies, we use a [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html),
which has to be updated every time dependencies change.
Please see the [instructions on how to regenerate the lockfile](conan/lockfile/README.md).
#### Default profile
We recommend that you import the provided `conan/profiles/default` profile:
```bash
conan config install conan/profiles/ -tf $(conan config home)/profiles/
@@ -93,15 +109,222 @@ You can check your Conan profile by running:
conan profile show
```
If the default profile is not suitable for your environment, you can create a custom profile and pass it to Conan.
More information on customizing Conan can be found in the [Advanced Conan configuration](./docs/build/advanced_conan.md).
#### Custom profile
#### Add xrplf remote
Run the following command to add the `xrplf` remote, which hosts some of our dependencies:
If the default profile does not work for you and you do not yet have a Conan
profile, you can create one by running:
```bash
conan remote add --index 0 --force xrplf https://conan.ripplex.io
conan profile detect
```
You may need to make changes to the profile to suit your environment. You can
refer to the provided `conan/profiles/default` profile for inspiration, and you
may also need to apply the required [tweaks](#conan-profile-tweaks) to this
default profile.
### Patched recipes
Occasionally, we need patched recipes or recipes not present in Conan Center.
We maintain a fork of the Conan Center Index
[here](https://github.com/XRPLF/conan-center-index/) containing the modified and newly added recipes.
To ensure our patched recipes are used, you must add our Conan remote at a
higher index than the default Conan Center remote, so it is consulted first. You
can do this by running:
```bash
conan remote add --index 0 xrplf https://conan.ripplex.io
```
Alternatively, you can pull our recipes from the repository and export them locally:
```bash
# Define which recipes to export.
recipes=('abseil' 'ed25519' 'mpt-crypto' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi')
# Selectively check out the recipes from our CCI fork.
cd external
mkdir -p conan-center-index
cd conan-center-index
git init
git remote add origin git@github.com:XRPLF/conan-center-index.git
git sparse-checkout init
for recipe in "${recipes[@]}"; do
echo "Checking out recipe '${recipe}'..."
git sparse-checkout add recipes/${recipe}
done
git fetch origin master
git checkout master
./export_all.sh
cd ../../
```
In the case we switch to a newer version of a dependency that still requires a
patch or add a new dependency, it will be necessary for you to pull in the changes and re-export the
updated dependencies with the newer version. However, if we switch to a newer
version that no longer requires a patch, no action is required on your part, as
the new recipe will be automatically pulled from the official Conan Center.
> [!NOTE]
> You might need to add `--lockfile=""` to your `conan install` command
> to avoid automatic use of the existing `conan.lock` file when you run
> `conan export` manually on your machine
>
> This is not recommended though, as you might end up using different revisions of recipes.
### Conan profile tweaks
#### Missing compiler version
If you see an error similar to the following after running `conan profile show`:
```text
ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value.
Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1',
'9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15',
'15.0', '16', '16.0']
Read "http://docs.conan.io/2/knowledge/faq.html#error-invalid-setting"
```
you need to add your compiler to the list of compiler versions in
`$(conan config home)/settings_user.yml`, by adding the required version number(s)
to the `version` array specific for your compiler. For example:
```yaml
compiler:
apple-clang:
version: ["17.0"]
```
#### Multiple compilers
If you have multiple compilers installed, make sure to select the one to use in
your default Conan configuration **before** running `conan profile detect`, by
setting the `CC` and `CXX` environment variables.
For example, if you are running MacOS and have [homebrew
LLVM@18](https://formulae.brew.sh/formula/llvm@18), and want to use it as a
compiler in the new Conan profile:
```bash
export CC=$(brew --prefix llvm@18)/bin/clang
export CXX=$(brew --prefix llvm@18)/bin/clang++
conan profile detect
```
You should also explicitly set the path to the compiler in the profile file,
which helps to avoid errors when `CC` and/or `CXX` are set and disagree with the
selected Conan profile. For example:
```text
[conf]
tools.build:compiler_executables={'c':'/usr/bin/gcc','cpp':'/usr/bin/g++'}
```
#### Multiple profiles
You can manage multiple Conan profiles in the directory
`$(conan config home)/profiles`, for example renaming `default` to a different
name and then creating a new `default` profile for a different compiler.
#### Select language
The default profile created by Conan will typically select different C++ dialect
than C++23 used by this project. You should set `23` in the profile line
starting with `compiler.cppstd=`. For example:
```bash
sed -i.bak -e 's|^compiler\.cppstd=.*$|compiler.cppstd=23|' $(conan config home)/profiles/default
```
#### Select standard library in Linux
**Linux** developers will commonly have a default Conan [profile][] that
compiles with GCC and links with libstdc++. If you are linking with libstdc++
(see profile setting `compiler.libcxx`), then you will need to choose the
`libstdc++11` ABI:
```bash
sed -i.bak -e 's|^compiler\.libcxx=.*$|compiler.libcxx=libstdc++11|' $(conan config home)/profiles/default
```
#### Select architecture and runtime in Windows
**Windows** developers may need to use the x64 native build tools. An easy way
to do that is to run the shortcut "x64 Native Tools Command Prompt" for the
version of Visual Studio that you have installed.
Windows developers must also build `xrpld` and its dependencies for the x64
architecture:
```bash
sed -i.bak -e 's|^arch=.*$|arch=x86_64|' $(conan config home)/profiles/default
```
**Windows** developers also must select static runtime:
```bash
sed -i.bak -e 's|^compiler\.runtime=.*$|compiler.runtime=static|' $(conan config home)/profiles/default
```
#### Clang workaround for grpc
If your compiler is clang, version 19 or later, or apple-clang, version 17 or
later, you may encounter a compilation error while building the `grpc`
dependency:
```text
In file included from .../lib/promise/try_seq.h:26:
.../lib/promise/detail/basic_seq.h:499:38: error: a template argument list is expected after a name prefixed by the template keyword [-Wmissing-template-arg-list-after-template-kw]
499 | Traits::template CallSeqFactory(f_, *cur_, std::move(arg)));
| ^
```
The workaround for this error is to add two lines to profile:
```text
[conf]
tools.build:cxxflags=['-Wno-missing-template-arg-list-after-template-kw']
```
#### Workaround for gcc 12
If your compiler is gcc, version 12, and you have enabled `werr` option, you may
encounter a compilation error such as:
```text
/usr/include/c++/12/bits/char_traits.h:435:56: error: 'void* __builtin_memcpy(void*, const void*, long unsigned int)' accessing 9223372036854775810 or more bytes at offsets [2, 9223372036854775807] and 1 may overlap up to 9223372036854775813 bytes at offset -3 [-Werror=restrict]
435 | return static_cast<char_type*>(__builtin_memcpy(__s1, __s2, __n));
| ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
cc1plus: all warnings being treated as errors
```
The workaround for this error is to add two lines to your profile:
```text
[conf]
tools.build:cxxflags=['-Wno-restrict']
```
#### Workaround for clang 16
If your compiler is clang, version 16, you may encounter compilation error such
as:
```text
In file included from .../boost/beast/websocket/stream.hpp:2857:
.../boost/beast/websocket/impl/read.hpp:695:17: error: call to 'async_teardown' is ambiguous
async_teardown(impl.role, impl.stream(),
^~~~~~~~~~~~~~
```
The workaround for this error is to add two lines to your profile:
```text
[conf]
tools.build:cxxflags=['-DBOOST_ASIO_DISABLE_CONCEPTS']
```
### Set Up Ccache
@@ -110,7 +333,14 @@ To speed up repeated compilations, we recommend that you install
[ccache](https://ccache.dev), a tool that wraps your compiler so that it can
cache build objects locally.
On Linux and macOS, `ccache` is included in the [Nix development shell](./docs/build/nix.md).
#### Linux
You can install it using the package manager, e.g. `sudo apt install ccache`
(Ubuntu) or `sudo dnf install ccache` (RHEL).
#### macOS
You can install it using Homebrew, i.e. `brew install ccache`.
#### Windows
@@ -319,7 +549,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details.
| Option | Default Value | Description |
| ---------- | ------------- | -------------------------------------------------------------- |
| `assert` | OFF | Force enabling assertions. |
| `assert` | OFF | Enable assertions. |
| `coverage` | OFF | Prepare the coverage report. |
| `tests` | OFF | Build tests. |
| `unity` | OFF | Configure a unity build. |
@@ -327,7 +557,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details.
| `werr` | OFF | Treat compilation warnings as errors |
| `wextra` | OFF | Enable additional compilation warnings |
[Unity builds][unity-build] may be faster for the first build (at the cost of much more
[Unity builds][5] may be faster for the first build (at the cost of much more
memory) since they concatenate sources into fewer translation units. Non-unity
builds may be faster for incremental builds, and can be helpful for detecting
`#include` omissions.
@@ -353,14 +583,14 @@ After any updates or changes to dependencies, you may need to do the following:
conan remove '*'
```
3. Re-run [conan export](./docs/build/advanced_conan.md#patched-recipes) if needed.
4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile).
3. Re-run [conan export](#patched-recipes) if needed.
4. [Regenerate lockfile](#conan-lockfile).
5. Re-run [conan install](#build-and-test).
#### ERROR: Package not resolved
If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`,
please [add `xrplf` remote](#add-xrplf-remote) or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes).
please add `xrplf` remote or re-run `conan export` for [patched recipes](#patched-recipes).
### `protobuf/port_def.inc` file not found
@@ -380,9 +610,28 @@ For example, if you want to build Debug:
1. For conan install, pass `--settings build_type=Debug`
2. For cmake, pass `-DCMAKE_BUILD_TYPE=Debug`
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23
[conan-getting-started]: https://docs.conan.io/en/latest/getting_started.html
[unity-build]: https://en.wikipedia.org/wiki/Unity_build
## Add a Dependency
If you want to experiment with a new package, follow these steps:
1. Search for the package on [Conan Center](https://conan.io/center/).
2. Modify [`conanfile.py`](./conanfile.py):
- Add a version of the package to the `requires` property.
- Change any default options for the package by adding them to the
`default_options` property (with syntax `'$package:$option': $value`).
3. Modify [`CMakeLists.txt`](./CMakeLists.txt):
- Add a call to `find_package($package REQUIRED)`.
- Link a library from the package to the target `xrpl_libs`
(search for the existing call to `target_link_libraries(xrpl_libs INTERFACE ...)`).
4. Start coding! Don't forget to include whatever headers you need from the package.
[1]: https://github.com/conan-io/conan-center-index/issues/13168
[2]: https://en.cppreference.com/w/cpp/compiler_support/20
[3]: https://docs.conan.io/en/latest/getting_started.html
[5]: https://en.wikipedia.org/wiki/Unity_build
[6]: https://github.com/boostorg/beast/issues/2648
[7]: https://github.com/boostorg/beast/issues/2661
[gcovr]: https://gcovr.com/en/stable/getting-started.html
[python-pip]: https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/
[build_type]: https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html
[profile]: https://docs.conan.io/en/latest/reference/profiles.html

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

@@ -14,9 +14,9 @@ The following branches exist in the main project repository:
- `develop`: The latest set of unreleased features, and the most common
starting point for contributions.
- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line,
holding the latest release candidate, or stable release for that line.
Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases).
- `release`: The latest beta release or release candidate.
- `master`: The latest stable release.
- `gh-pages`: The documentation for this project, built by Doxygen.
The tip of each branch must be signed. In order for GitHub to sign a
squashed commit that it builds from your pull request, GitHub must know
@@ -130,9 +130,11 @@ tl;dr
## Pull requests
In general, pull requests use `develop` as the base branch.
The exceptions are
The exceptions are fixes, improvements, and hotfixes for an existing release,
which use that release's branch (e.g. `release/3.2.x`) as the base.
- Fixes and improvements to a release candidate use `release` as the
base.
- Hotfixes use `master` as the base.
If your changes are not quite ready, but you want to make it easily available
for preliminary examination or review, you can create a "Draft" pull request.
@@ -214,7 +216,7 @@ coherent rather than a set of _thou shalt not_ commandments.
## Formatting
All code must conform to `clang-format` version 22,
All code must conform to `clang-format` version 21,
according to the settings in [`.clang-format`](./.clang-format),
unless the result would be unreasonably difficult to read or maintain.
To demarcate lines that should be left as-is, surround them with comments like
@@ -259,7 +261,7 @@ This ensures that configuration changes don't introduce new warnings across the
### Installing clang-tidy
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for platform-specific installation instructions.
### Running clang-tidy locally

View File

@@ -1,158 +0,0 @@
#!/usr/bin/env bash
#
# check-tools.sh — verify the xrpld development tooling is present and runnable.
#
# Works on Linux, macOS, and Windows (Git Bash / MSYS). For every expected tool
# it runs a version probe, collecting anything that is missing or fails to run,
# and prints a summary at the end (exiting non-zero if anything is missing).
#
# The tool set is platform-aware:
# - Linux: the full Nix CI environment (see nix/packages.nix, nix/ci-env.nix),
# with GCC, Clang and the sanitizer/coverage tooling. This script is
# run during the Nix Docker image build (nix/docker/Dockerfile), so
# the Linux list is kept in sync with that environment.
# - macOS: the same tooling, minus GCC/g++/gcov/mold
# - Windows: the core build tools only (CMake, Conan, Git, Python).
# MSVC is expected to be provided separately and is not checked here.
#
# Some tools (clang-format, doxygen, gcovr, gh, git-cliff, gpg, pre-commit,
# run-clang-tidy) are present in our Linux CI images and in local development
# setups, but not in the macOS CI environment. They are checked everywhere
# except when running in CI on macOS.
#
# Environment variables:
# CI if set, skip the tools above when on macOS.
# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check.
set -uo pipefail
missing=()
checked=0
# check <name> [probe-command...]
# Runs the probe (default: "<name> --version") quietly. Records <name> as
# missing if the command is not found or exits non-zero.
check() {
local name="$1"
shift
local -a probe=("$@")
if [ "${#probe[@]}" -eq 0 ]; then
probe=("${name}" --version)
fi
echo "Checking ${name}..."
checked=$((checked + 1))
if "${probe[@]}" | head -n 1; then
printf ' [ ok ] %s\n' "${name}"
else
printf ' [MISS] %s\n' "${name}"
missing+=("${name}")
fi
}
case "$(uname -s)" in
Linux*) os=linux ;;
Darwin*) os=macos ;;
MINGW* | MSYS* | CYGWIN*) os=windows ;;
*)
echo "Unknown OS: $(uname -s)" >&2
exit 1
;;
esac
echo "Detected OS: ${os} ($(uname -s) $(uname -m))"
echo
echo "Core build tools:"
check cmake
check conan
check git
if [ "${os}" = "windows" ]; then
check python python --version
else
check python3
fi
# The full development toolchain. Available from Nix on Linux and macOS; on
# Windows these are typically not installed, so they are skipped.
if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
echo
echo "Development tooling:"
check ccache
check clang
check clang++
check ClangBuildAnalyzer
check curl
check file
check less
check make
check netstat which netstat
check ninja
check perl
check pkg-config
check vim
# These tools are present in our Linux CI images and in local development
# setups, but not in the macOS CI environment. So check them everywhere
# except when running in CI on macOS.
if [ "${os}" = "linux" ] || [ -z "${CI:-}" ]; then
check clang-format
check doxygen
check gcovr
check gh
check git-cliff
check gpg
# pre-commit, or its alternative implementation prek
check pre-commit sh -c 'pre-commit --version || prek --version'
check run-clang-tidy run-clang-tidy --help
fi
fi
# GCC is the default compiler on Linux. macOS uses the system Apple Clang
# instead, so GCC/g++/gcov are not expected there.
if [ "${os}" = "linux" ]; then
echo
echo "GCC toolchain:"
check gcc
check g++
check gcov
echo
echo "Mold:"
check mold
fi
if [ "${os}" = "windows" ]; then
echo
echo "Note: on Windows the C++ compiler is MSVC, which is provided"
echo " separately (e.g. via Visual Studio) and is not checked here."
fi
# A simple test to verify that git can clone a repository over HTTPS
# (i.e. the CA bundle is wired up). Clone to a temp dir and clean up.
if [ -n "${CHECK_TOOLS_SKIP_CLONE:-}" ]; then
echo
echo "Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set)."
else
echo
echo "Connectivity check:"
checked=$((checked + 1))
tmp_clone="$(mktemp -d)"
if git clone --depth 1 https://github.com/XRPLF/actions.git "${tmp_clone}/actions" >/dev/null 2>&1; then
printf ' [ ok ] git clone over HTTPS\n'
else
printf ' [MISS] git clone over HTTPS\n'
missing+=("git-https-clone")
fi
rm -rf "${tmp_clone}"
fi
echo
if [ "${#missing[@]}" -eq 0 ]; then
echo "All ${checked} checked tools are present and runnable."
else
echo "Missing or non-functional tools (${#missing[@]} of ${checked}):" >&2
for tool in "${missing[@]}"; do
echo " - ${tool}" >&2
done
exit 1
fi

View File

@@ -25,19 +25,6 @@ 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}
@@ -51,7 +38,7 @@ add_custom_target(
${CMAKE_COMMAND} -E env ${package_env}
${CMAKE_SOURCE_DIR}/package/build_pkg.sh
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld validator-keys
DEPENDS xrpld
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
VERBATIM
)

View File

@@ -1,22 +1,26 @@
option(
validator_keys
"Enables building of the vendored validator-keys tool as a separate target"
"Enables building of validator-keys tool as a separate target (imported via FetchContent)"
OFF
)
if(validator_keys)
include(GNUInstallDirs)
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}")
add_subdirectory(
"${CMAKE_SOURCE_DIR}/validator-keys-tool"
"${CMAKE_BINARY_DIR}/validator-keys-tool"
FetchContent_Declare(
validator_keys
GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git
GIT_TAG "${current_branch}"
)
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}" COMPONENT runtime
)
install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()

View File

@@ -109,7 +109,6 @@ words:
- enabled
- enablerepo
- endmacro
- envrc
- exceptioned
- EXPECT_STREQ
- Falco

View File

@@ -1,193 +0,0 @@
# Advanced Conan configuration
This document provides advanced instructions for setting up and configuring Conan for `xrpld` development: custom profiles, the lockfile, patched recipes, and profile tweaks.
## Custom profile
If the default profile does not work for you and you do not yet have a Conan
profile, you can create one by running:
```bash
conan profile detect
```
You may need to make changes to the profile to suit your environment. You can
refer to the provided `conan/profiles/default` profile for inspiration, and you
may also need to apply the required [tweaks](#conan-profile-tweaks) to this
default profile.
## Conan lockfile
To achieve reproducible dependencies, we use a [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html),
which has to be updated every time dependencies change.
Please see the [instructions on how to regenerate the lockfile](../../conan/lockfile/README.md).
## Patched recipes
Occasionally, we need patched recipes or recipes not present in Conan Center.
We maintain a fork of the Conan Center Index
[here](https://github.com/XRPLF/conan-center-index/) containing the modified and newly added recipes.
To ensure our patched recipes are used, you must add our Conan remote at a
higher index than the default Conan Center remote, so it is consulted first. You
can do this by running:
```bash
conan remote add --index 0 --force xrplf https://conan.ripplex.io
```
Alternatively, you can pull our recipes from the repository and export them locally:
```bash
# Define which recipes to export.
recipes=('abseil' 'ed25519' 'mpt-crypto' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi')
# Selectively check out the recipes from our CCI fork.
cd external
mkdir -p conan-center-index
cd conan-center-index
git init
git remote add origin git@github.com:XRPLF/conan-center-index.git
git sparse-checkout init
for recipe in "${recipes[@]}"; do
echo "Checking out recipe '${recipe}'..."
git sparse-checkout add recipes/${recipe}
done
git fetch origin master
git checkout master
./export_all.sh
cd ../../
```
In the case we switch to a newer version of a dependency that still requires a
patch or add a new dependency, it will be necessary for you to pull in the changes and re-export the
updated dependencies with the newer version. However, if we switch to a newer
version that no longer requires a patch, no action is required on your part, as
the new recipe will be automatically pulled from the official Conan Center.
> [!NOTE]
> You might need to add `--lockfile=""` to your `conan install` command
> to avoid automatic use of the existing `conan.lock` file when you run
> `conan export` manually on your machine
>
> This is not recommended though, as you might end up using different revisions of recipes.
## Conan profile tweaks
### Missing compiler version
If you see an error similar to the following after running `conan profile show`:
```text
ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value.
Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1',
'9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15',
'15.0', '16', '16.0']
Read "http://docs.conan.io/2/knowledge/faq.html#error-invalid-setting"
```
you need to create `$(conan config home)/settings_user.yml` file if it doesn't exist and add the required version number(s)
to the `version` array specific for your compiler. For example:
```yaml
compiler:
apple-clang:
version: ["17.0"]
```
### Multiple compilers
If you have multiple compilers installed, make sure to select the one to use in
your default Conan configuration **before** running `conan profile detect`, by
setting the `CC` and `CXX` environment variables.
For example, if you are running MacOS and have [homebrew
LLVM@18](https://formulae.brew.sh/formula/llvm@18), and want to use it as a
compiler in the new Conan profile:
```bash
export CC=$(brew --prefix llvm@18)/bin/clang
export CXX=$(brew --prefix llvm@18)/bin/clang++
conan profile detect
```
You should also explicitly set the path to the compiler in the profile file,
which helps to avoid errors when `CC` and/or `CXX` are set and disagree with the
selected Conan profile. For example:
```text
[conf]
tools.build:compiler_executables={'c':'/usr/bin/gcc','cpp':'/usr/bin/g++'}
```
### Multiple profiles
You can manage multiple Conan profiles in the directory
`$(conan config home)/profiles`, for example renaming `default` to a different
name and then creating a new `default` profile for a different compiler.
### Select language
The default profile created by Conan will typically select different C++ dialect
than C++23 used by this project. You should set `23` in the profile line
starting with `compiler.cppstd=`. For example:
```bash
sed -i.bak -e 's|^compiler\.cppstd=.*$|compiler.cppstd=23|' $(conan config home)/profiles/default
```
### Select standard library in Linux
**Linux** developers will commonly have a default Conan [profile][] that
compiles with GCC and links with libstdc++. If you are linking with libstdc++
(see profile setting `compiler.libcxx`), then you will need to choose the
`libstdc++11` ABI:
```bash
sed -i.bak -e 's|^compiler\.libcxx=.*$|compiler.libcxx=libstdc++11|' $(conan config home)/profiles/default
```
### Select architecture and runtime in Windows
**Windows** developers may need to use the x64 native build tools. An easy way
to do that is to run the shortcut "x64 Native Tools Command Prompt" for the
version of Visual Studio that you have installed.
Windows developers must also build `xrpld` and its dependencies for the x64
architecture:
```bash
sed -i.bak -e 's|^arch=.*$|arch=x86_64|' $(conan config home)/profiles/default
```
**Windows** developers also must select static runtime:
```bash
sed -i.bak -e 's|^compiler\.runtime=.*$|compiler.runtime=static|' $(conan config home)/profiles/default
```
## Add a Dependency
If you want to experiment with a new package, follow these steps:
1. Search for the package on [Conan Center](https://conan.io/center/).
2. Modify [`conanfile.py`](../../conanfile.py):
- Add a version of the package to the `requires` property.
- Change any default options for the package by adding them to the
`default_options` property (with syntax `'$package:$option': $value`).
3. Regenerate the [Conan lockfile](../../conan/lockfile/README.md) so the new
dependency is captured:
```bash
./conan/lockfile/regenerate.sh
```
4. Modify [`CMakeLists.txt`](../../CMakeLists.txt):
- Add a call to `find_package($package REQUIRED)`.
- Link a library from the package to the target `xrpl_libs`
(search for the existing call to `target_link_libraries(xrpl_libs INTERFACE ...)`).
5. Start coding! Don't forget to include whatever headers you need from the package.
[profile]: https://docs.conan.io/2/reference/config_files/profiles.html

2
docs/build/conan.md vendored
View File

@@ -115,7 +115,7 @@ By default, Conan will use the profile named "default".
[find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
[pcf]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-configuration-file
[prefix_path]: https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html
[profile]: https://docs.conan.io/2/reference/config_files/profiles.html
[profile]: https://docs.conan.io/en/latest/reference/profiles.html
[pvf]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-version-file
[runtime]: https://cmake.org/cmake/help/latest/variable/CMAKE_MSVC_RUNTIME_LIBRARY.html
[search]: https://cmake.org/cmake/help/latest/command/find_package.html#search-procedure

View File

@@ -1,73 +1,69 @@
Our [build instructions][BUILD.md] assume you have a C++ development
environment complete with Git, Python, Conan, CMake, and a C++ compiler.
This document explains how to set one up.
This document exists to help readers set one up on any of the Big Three
platforms: Linux, macOS, or Windows.
As an alternative to system packages, the Nix development shell can be used to provide a development environment. See [using nix development shell](./nix.md) for more details.
[BUILD.md]: ../../BUILD.md
## Tested compiler versions
## Linux
`xrpld` is built in the **C++23** dialect by default.
Make sure your toolchain is recent enough — the compiler versions currently tested in CI are:
Package ecosystems vary across Linux distributions,
so there is no one set of instructions that will work for every Linux user.
The instructions below are written for Debian 12 (Bookworm).
| Compiler | Version |
| ----------- | ------- |
| GCC | 15.2 |
| Clang | 22 |
| Apple Clang | 17 |
| MSVC | 19.44 |
```
export GCC_RELEASE=12
sudo apt update
sudo apt install --yes gcc-${GCC_RELEASE} g++-${GCC_RELEASE} python3-pip \
python-is-python3 python3-venv python3-dev curl wget ca-certificates \
git build-essential cmake ninja-build libc6-dev
sudo pip install --break-system-packages conan
LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22.
Older compilers may fail to build the latest `develop` code: the codebase now
relies on C++23 features and has been adjusted for `clang-tidy`.
If the latest code doesn't build for you, update your build toolchain first.
## Linux and macOS
The **recommended way** to get a development environment on Linux and macOS is
the Nix development shell. It provides the exact tooling used in CI — `git`,
`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else —
with a single command and without installing anything system-wide:
```bash
nix --experimental-features 'nix-command flakes' develop
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-${GCC_RELEASE} 999
sudo update-alternatives --install \
/usr/bin/gcc gcc /usr/bin/gcc-${GCC_RELEASE} 100 \
--slave /usr/bin/g++ g++ /usr/bin/g++-${GCC_RELEASE} \
--slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-${GCC_RELEASE} \
--slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-${GCC_RELEASE} \
--slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-${GCC_RELEASE} \
--slave /usr/bin/gcov gcov /usr/bin/gcov-${GCC_RELEASE} \
--slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-${GCC_RELEASE} \
--slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-${GCC_RELEASE} \
--slave /usr/bin/lto-dump lto-dump /usr/bin/lto-dump-${GCC_RELEASE}
sudo update-alternatives --auto cc
sudo update-alternatives --auto gcc
```
On **Linux**, Nix also provides the compiler (GCC). On **macOS**, the shell uses
your **system-wide Apple Clang** as the compiler, so you still need to manage
its version (see below).
If you use different Linux distribution, hope the instruction above can guide
you in the right direction. We try to maintain compatibility with all recent
compiler releases, so if you use a rolling distribution like e.g. Arch or CentOS
then there is a chance that everything will "just work".
See [Using the Nix development shell](./nix.md) for installation and usage
details, including how to select a different compiler.
## macOS
> [!NOTE]
> Using Nix is not mandatory. Any custom environment (Homebrew packages or
> anything else) will continue to work, but then it is up to you to keep it in
> sync with the environment used in CI. Nix unifies the development environment
> for everyone and synchronizes updates, which is why we recommend it.
Open a Terminal and enter the below command to bring up a dialog to install
the command line developer tools.
Once it is finished, this command should return a version greater than the
minimum required (see [BUILD.md][]).
### macOS: managing the Apple Clang version
Because the Nix shell uses the system-wide Apple Clang on macOS, the compiler
version is whatever your installed Xcode (or Command Line Tools) provides. The
following command should return a version greater than or equal to the
[minimum required](#tested-compiler-versions):
```bash
```
clang --version
```
If you develop other applications using Xcode, you might be consistently
updating to the newest version of Apple Clang, which will likely cause issues
building xrpld. You may want to install and pin a specific version of Xcode:
### Install Xcode Specific Version (Optional)
If you develop other applications using XCode you might be consistently updating to the newest version of Apple Clang.
This will likely cause issues building xrpld. You may want to install a specific version of Xcode:
1. **Download Xcode**
- Visit [Apple Developer Downloads](https://developer.apple.com/download/more/)
- Sign in with your Apple Developer account
- Search for an Xcode version that includes the expected Apple Clang version
- Search for an Xcode version that includes **Apple Clang (Expected Version)**
- Download the `.xip` file
2. **Install and configure Xcode**
2. **Install and Configure Xcode**
```bash
# Extract the .xip file and rename for version management
@@ -83,28 +79,62 @@ building xrpld. You may want to install and pin a specific version of Xcode:
export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
```
## Windows
The command line developer tools should include Git too:
Nix is not available on Windows, so the required tools have to be installed
manually:
```
git --version
```
- [Visual Studio 2022](https://visualstudio.microsoft.com/) with the
**"Desktop development with C++"** workload — this provides MSVC and the
"x64 Native Tools Command Prompt".
- [Git for Windows](https://git-scm.com/download/win)
- [Python 3.11](https://www.python.org/downloads/), or higher
- [Conan 2.17](https://conan.io/downloads.html), or higher
- [CMake 3.22](https://cmake.org/download/), or higher
Install [Homebrew][],
use it to install [pyenv][],
use it to install Python,
and use it to install Conan:
> [!NOTE]
> Windows is used for development only and is not recommended for production.
[Homebrew]: https://brew.sh/
[pyenv]: https://github.com/pyenv/pyenv
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew update
brew install xz
brew install pyenv
pyenv install 3.11
pyenv global 3.11
eval "$(pyenv init -)"
pip install 'conan'
```
Install CMake with Homebrew too:
```
brew install cmake
```
## Clang-tidy
`clang-tidy` is required to run static analysis checks locally (see
[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the
project. This project currently uses `clang-tidy` version 22.
Clang-tidy is required to run static analysis checks locally (see [CONTRIBUTING.md](../../CONTRIBUTING.md)).
It is not required to build the project. Currently this project uses clang-tidy version 21.
On Linux and macOS, the [Nix development shell](./nix.md) provides `clang-tidy`
22 out of the box — run it via `run-clang-tidy`. No separate installation is
needed.
### Linux
LLVM 21 is not available in the default Debian 12 (Bookworm) repositories.
Install it using the official LLVM apt installer:
```
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt install --yes clang-tidy-21
```
Then use `run-clang-tidy-21` when running clang-tidy locally.
### macOS
Install LLVM 21 via Homebrew:
```
brew install llvm@21
```
Then use `run-clang-tidy` from the LLVM 21 Homebrew prefix when running clang-tidy locally.

45
docs/build/nix.md vendored
View File

@@ -2,12 +2,9 @@
This guide explains how to use Nix to set up a reproducible development environment for xrpld. Using Nix eliminates the need to manually install utilities and ensures consistent tooling across different machines.
**The Nix development shell is the recommended way to develop xrpld.** It unifies the development environment for everyone and synchronizes updates: the same tooling and compiler versions are used both here and in CI. Any custom environment (Homebrew packages or anything else) will continue to work, but then it is up to you to keep it in sync with the environment used in CI.
## Benefits of Using Nix
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment
- **No system pollution**: Dependencies are isolated and don't affect your system packages
- **Multiple compiler versions**: Easily switch between different GCC and Clang versions
- **Quick setup**: Get started with a single command
@@ -31,22 +28,11 @@ This will:
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
- Configure the appropriate compiler for your platform:
- **Linux**: GCC 15.2 (provided by Nix)
- **macOS**: Apple Clang (your system compiler)
- **macOS**: Apple Clang (default system compiler)
- **Linux**: GCC 15
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
### Platform notes
- **Linux**: `nix develop` gives you a shell with all the tooling necessary to
develop xrpld and with GCC 15.2 (also provided by Nix). There are no caveats.
- **macOS**: `nix develop` gives you a full environment too. The compiler is
your system-wide Apple Clang, while every other tool — including Conan — is
provided by Nix. Conan has no binary in the Nix cache for macOS, so it is
built from source the first time you enter the shell, which makes the initial
setup slower (this is handled automatically; see
[`nix/devshell.nix`](../../nix/devshell.nix)).
> [!TIP]
> To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`:
>
@@ -65,7 +51,7 @@ The first time you run this command, it will take a few minutes to download and
A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`.
Use `nix flake show` to see all the available development shells.
Use `nix develop .#no-compiler` to use the compiler from your system.
Use `nix develop .#no_compiler` to use the compiler from your system.
### Example Usage
@@ -82,28 +68,12 @@ nix develop
### Using a different shell
`nix develop` opens bash by default. To use another shell, pass it with the `-c` flag — this works with any shell, e.g. `zsh` or `fish`:
`nix develop` opens bash by default. If you want to use another shell this could be done by adding `-c` flag. For example:
```bash
# Use zsh
nix develop -c zsh
# Use fish
nix develop -c fish
# Use your login shell
nix develop -c "$SHELL"
```
> [!WARNING]
> Your shell's interactive startup files (e.g. `config.fish`, `.zshrc`) may prepend other directories — most commonly Homebrew — to `$PATH`, which can shadow the tools provided by the Nix shell. After entering, verify that tools resolve into the Nix store:
>
> ```bash
> command -v cmake # should print a /nix/store/... path
> ```
>
> If it doesn't, either adjust your shell configuration so it doesn't override `$PATH`, or use [direnv](#automatic-activation-with-direnv) (below), which loads the environment _after_ your shell config and so takes precedence regardless of the shell you use.
## Building xrpld with Nix
Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.).
@@ -112,8 +82,6 @@ Once inside the Nix development shell, follow the standard [build instructions](
[direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory.
This is also the most robust way to use the environment from **any shell** (bash, zsh, fish, …): direnv stays in your current shell and loads the environment _after_ your shell's startup files have run, so the Nix-provided tools take precedence over anything your shell configuration adds to `$PATH`. To use it, install direnv for your shell, then add an `.envrc` containing `use flake` at the repository root and run `direnv allow`.
## Conan and Prebuilt Packages
Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source:
@@ -125,8 +93,3 @@ conan install .. --output-folder . --build '*' --settings build_type=Release
## Updating `flake.lock` file
To update `flake.lock` to the latest revision use `nix flake update` command.
## Troubleshooting
See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues,
such as `nix develop` failing inside Git worktrees.

View File

@@ -1,61 +0,0 @@
# Troubleshooting Nix problems
Common issues encountered when using the [Nix development shell](./nix.md), and
how to resolve them.
## Git worktrees
If `nix develop` fails with an error like:
```
error:
… while fetching the input 'git+file:///path/to/rippled'
error: opening Git repository "/path/to/rippled": unsupported extension name extensions.relativeworktrees (libgit2 error code = 6)
```
then your Nix is linked against a libgit2 older than **1.9.4**. Git 2.48+ writes
the `extensions.relativeWorktrees` config entry when a worktree is created with
relative paths (`git worktree add --relative-paths`, or with
`worktree.useRelativePaths=true`), and older libgit2 versions refuse to open a
repository that uses it. Nix uses libgit2 to read the flake, so evaluation
fails.
> [!IMPORTANT]
> This entry is written to the **shared** repository config, so once any
> relative worktree exists, `nix develop` fails in the main checkout too — not
> just inside the worktree.
### Workarounds
These work today, with any Nix version:
- bypass libgit2 with a `path:` flakeref: `nix develop "path:$PWD"`
(note: this copies the working tree to the store and ignores `.gitignore`); or
- create worktrees with absolute paths (omit `--relative-paths`); or
- clear the extension if you don't need relative worktrees:
`git config --unset extensions.relativeWorktrees`.
### Permanent fix
The fix is in [libgit2 1.9.4](https://github.com/libgit2/libgit2/releases/tag/v1.9.4),
so the real solution is a Nix that links against libgit2 `1.9.4` or newer. Check
which version yours links against:
```bash
nix-store -qR "$(readlink -f "$(command -v nix)")" | grep libgit2
```
> [!WARNING]
> `nix upgrade-nix` does **not** help yet. It installs the build from the
> official [`nix-fallback-paths`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/tools/nix-fallback-paths.nix),
> which is still linked against libgit2 `1.9.2` — there is no new upstream Nix
> release with the fix. (On some systems that build is even the exact store path
> you already have, making the upgrade a no-op.)
nixpkgs has already rebuilt Nix against the fixed libgit2 (e.g. `nix-2.34.7+1`),
so the cleanest path is to reinstall Nix using your usual installation method
once it picks up that rebuild, then re-run the `grep libgit2` check above to
confirm it reports `1.9.4` or newer.
Until then, prefer the workarounds above.

View File

@@ -71,7 +71,7 @@ if [ ! -e "${target}" ]; then
fi
EOF
COPY bin/check-tools.sh /tmp/check-tools.sh
COPY nix/docker/check-tools.sh /tmp/check-tools.sh
RUN /tmp/check-tools.sh
# Sanity-check that the g++/clang++ are able to build binaries, including sanitizer-instrumented ones.
@@ -93,7 +93,7 @@ RUN if echo "${BASE_IMAGE}" | grep -qiE 'nixos'; then \
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
# Sanity-check that the built binaries run correctly in the vanilla base image, with the necessary sanitizer runtime libraries installed.
COPY bin/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh
COPY nix/docker/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh
COPY nix/docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh
COPY --from=final /tmp/bins /tmp/bins

View File

@@ -1,90 +0,0 @@
# Nix CI Docker images
This directory builds the Docker images used by xrpld's Linux CI. Each image
bundles the **exact same toolchain that the Nix development shell provides**
(see [`docs/build/nix.md`](../../docs/build/nix.md)), so what runs in CI matches
what developers get locally from `nix develop`.
The toolchain (CMake, Ninja, Conan, GCC, Clang, clang-tidy, the
sanitizer/coverage tools, …) is defined in [`nix/packages.nix`](../packages.nix)
and assembled for CI by [`nix/ci-env.nix`](../ci-env.nix). The Docker build
turns that Nix environment into an ordinary container image layered on top of a
conventional base image (Ubuntu, Debian, RHEL, or `nixos/nix`).
## Images
The images are built by the [`build-nix-images.yml`](../../.github/workflows/build-nix-images.yml)
workflow and pushed to `ghcr.io/xrplf/xrpld/nix-<distro>`. The `<distro>` is
selected through the `BASE_IMAGE` build argument; the base images are the
**oldest supported version** of each distribution we target:
| Image | `BASE_IMAGE` | Notes |
| ------------ | -------------------------------------------- | -------------------------------------------------- |
| `nix-nixos` | `nixos/nix:latest` | Build/lint only; binaries are not run (see below). |
| `nix-ubuntu` | `ubuntu:20.04` | Oldest supported Ubuntu (glibc 2.31). |
| `nix-debian` | `debian:bookworm` | |
| `nix-rhel` | `registry.access.redhat.com/ubi9/ubi:latest` | |
All images carry the full toolchain on `PATH` (via `/nix/ci-env/bin`) plus the
CA bundle shipped in the Nix environment, so HTTPS clients (git, curl, Conan)
work without `ca-certificates` being installed in the base image.
## Build stages
[`Dockerfile`](./Dockerfile) is a multi-stage build:
1. **`builder`** — On a `nixos/nix` builder, evaluate the flake and build the
CI environment (`nix/ci-env.nix`). The resulting Nix store closure (the
complete set of store paths the toolchain depends on) is copied into a
staging directory.
2. **`final`** — Start from `BASE_IMAGE`, copy in the Nix store closure and the
`ci-env` symlink tree, and wire up `PATH` and the CA bundle. It then:
- installs the dynamic linker if the base image lacks one (see
[How libc is handled](#how-libc-is-handled)),
- runs [`bin/check-tools.sh`](../../bin/check-tools.sh) to verify every
expected tool is present and runnable, and
- compiles the C++ test programs in
[`test_files/`](./test_files) with both `g++` and `clang++`, and sanitizers.
3. **`tester`** — Start again from a clean `BASE_IMAGE` (no Nix toolchain),
install only the sanitizer runtime libraries
([`install-sanitizer-libs.sh`](./install-sanitizer-libs.sh)), and run the
binaries compiled in `final`. This proves the binaries built with the Nix
toolchain actually run on a vanilla base image. On `nixos/nix` this step is
skipped (the binaries are patched for a conventional FHS loader).
4. **Output** — The final image is gated on the tester succeeding: it copies a
sentinel file out of `tester`, so a failed test run fails the whole build.
## How libc is handled
The goal is for binaries built in these images to run on the **oldest supported
base image** (Ubuntu 20.04, glibc 2.31) and newer — without the developer's Nix
toolchain being present at runtime. Two pieces make that work:
- **Compilers linked against an old glibc.** The Nix CI environment does not use
nixpkgs' current glibc. Instead it pins a 2020 nixpkgs snapshot whose primary
glibc is **2.31** (matching Ubuntu 20.04), via the `nixpkgs-custom-glibc`
flake input. GCC, Clang, binutils and compiler-rt are all rebuilt/wrapped
against this custom glibc (see [`nix/ci-env.nix`](../ci-env.nix)). As a result
the libraries they emit (`libstdc++`, `libgcc_s`, the sanitizer runtimes)
reference only symbols available in glibc 2.31.
- **An expected dynamic linker in the image.**
Binaries built in Nix environments reference a dynamic linker from Nix store paths, which won't be present in the base image. However,
[`loader-path.sh`](./loader-path.sh) reports the expected loader path for the
current architecture, so we can patch the binaries to use the correct loader.
The build then verifies all of this end to end: the test programs in
`test_files/` (a regular binary plus ASan/TSan/UBSan variants) are compiled in
`final`, their `PT_INTERP` is patched to the target loader, and they are run in
the clean `tester` stage to confirm each emits the expected sanitizer
diagnostic on a stock base image.
## Files
| File | Purpose |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`./test_files/`](./test_files) | C++ sources and scripts to compile and run the sanitizer smoke tests. |
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |

38
nix/docker/check-tools.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Verify that every tool expected in the Nix CI env is present and runnable.
set -euo pipefail
ccache --version
clang --version
clang++ --version
clang-format --version
cmake --version
conan --version
curl --version
doxygen --version
file --version
g++ --version
gcc --version
gcov --version
gcovr --version
gh --version
git --version
git-cliff --version
gpg --version
less --version
make --version
mold --version
netstat --version
ninja --version
perl --version
pkg-config --version
pre-commit --version
python3 --version
run-clang-tidy --help
vim --version
# A simple test to verify that git can clone a repository over HTTPS
# (i.e. the CA bundle is wired up). Clone to a temp dir and clean up.
tmp_clone="$(mktemp -d)"
git clone --depth 1 https://github.com/XRPLF/actions.git "${tmp_clone}/actions"
rm -rf "${tmp_clone}"

View File

@@ -9,7 +9,6 @@ in
{
commonPackages = with pkgs; [
ccache
clangbuildanalyzer
cmake
conan
curlMinimal # needed for codecov/codecov-action

View File

@@ -1,7 +1,6 @@
# Linux Packaging
This directory contains all files needed to build RPM and Debian packages for
`xrpld`. The packages also include the `validator-keys` utility.
This directory contains all files needed to build RPM and Debian packages for `xrpld`.
## Directory layout
@@ -50,17 +49,16 @@ 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 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.
`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.
### Locally (mirrors CI)
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.
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.
```bash
# From the repo root. Pick any image flagged with `"package": true` in
@@ -94,7 +92,6 @@ 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 \
..
@@ -114,13 +111,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 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 |
| 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 |
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
@@ -145,7 +142,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 binaries, configs, `README.md`, and `LICENSE.md`.
2. Stages the binary, 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,8 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# Build an RPM or Debian package from pre-built xrpld and validator-keys
# binaries.
# Build an RPM or Debian package from a pre-built xrpld binary.
#
# Flags override env vars; env vars override defaults. Env vars are intended
# for CMake/systemd/CI integration; flags are for explicit invocation.
@@ -13,7 +12,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 binaries [BUILD_DIR; default: $PWD/build]
--build-dir DIR directory holding xrpld [BUILD_DIR; default: $PWD/build]
--pkg-version STR version, e.g. 3.2.0-b1 [PKG_VERSION; default: parsed from xrpld --version]
--pkg-release N package release number [PKG_RELEASE; default: 1]
--source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; default: latest git commit ctime]
@@ -134,7 +133,6 @@ 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"

View File

@@ -20,5 +20,4 @@ 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. Includes validator-keys for
validator key management.
and maintains a local ledger copy.

View File

@@ -19,7 +19,6 @@ 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

View File

@@ -21,8 +21,6 @@ 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
:
@@ -32,7 +30,6 @@ management.
%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
@@ -80,7 +77,6 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%dir %{_libexecdir}/%{name}
%{_bindir}/%{name}
%{_bindir}/validator-keys
%config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt

View File

@@ -1 +1 @@
halt_on_error=true
halt_on_error=false

View File

@@ -72,7 +72,7 @@ vptr:boost
# Google protobuf - intentional overflows in hash functions
undefined:protobuf
unsigned-integer-overflow:protobuf
unsigned-integer-overflow:google/protobuf/stubs/stringpiece.h
# gRPC intentional overflows in timer calculations
unsigned-integer-overflow:grpc
@@ -102,103 +102,47 @@ undefined:nudb
# Snappy compression library intentional overflows
unsigned-integer-overflow:snappy.cc
# Abseil intentional overflows in hashing, RNG and time arithmetic.
# Matched at library scope (like boost above): the wraparound is by design
# across many absl files (hash mixing, raw_hash_set probing, duration math,
# int128, uniform_int_distribution), so listing individual files just churns.
unsigned-integer-overflow:absl
# Abseil intentional overflows
unsigned-integer-overflow:absl/strings/numbers.cc
unsigned-integer-overflow:absl/strings/internal/cord_rep_flat.h
unsigned-integer-overflow:absl/base/internal/low_level_alloc.cc
unsigned-integer-overflow:absl/hash/internal/hash.h
unsigned-integer-overflow:absl/container/internal/raw_hash_set.h
# Standard library intentional overflows
unsigned-integer-overflow:basic_string.h
unsigned-integer-overflow:bits/align.h
unsigned-integer-overflow:bits/basic_string.tcc
unsigned-integer-overflow:bits/chrono.h
unsigned-integer-overflow:bits/random.h
unsigned-integer-overflow:bits/random.tcc
unsigned-integer-overflow:bits/stl_algobase.h
unsigned-integer-overflow:bits/string_view.tcc
unsigned-integer-overflow:bits/uniform_int_dist.h
unsigned-integer-overflow:string_view
unsigned-integer-overflow:__random/seed_seq.h
unsigned-integer-overflow:__charconv/traits.h
unsigned-integer-overflow:__chrono/duration.h
# libstdc++ <bit> (std::__bit_ceil etc.) negates an unsigned width; <bit> is a
# distinct header from the bits/ directory so it needs its own entry.
unsigned-integer-overflow:include/c++/*/bit
# =============================================================================
# Rippled code suppressions
# =============================================================================
# These suppressions are keyed by SOURCE FILE, not function name. This UBSan
# build runs without symbol information, so the runtime only knows the
# file:line of each report, never the enclosing function — function-name
# patterns silently never match. Each entry below is therefore scoped to the
# file whose arithmetic is intentional; the comment names the specific
# construct.
# Signed integer negation (-value) in amount types.
# INT64_MIN cannot occur in practice due to domain invariants (mantissa ranges
# are well within int64_t bounds), but UBSan flags the pattern as potential
# signed overflow. Narrowed to operator- to avoid suppressing unrelated
# overflows anywhere in a stack trace containing these type names.
signed-integer-overflow:operator-*IOUAmount*
signed-integer-overflow:operator-*XRPAmount*
signed-integer-overflow:operator-*MPTAmount*
signed-integer-overflow:operator-*STAmount*
# STAmount amount-type arithmetic. Unary negation of the mantissa in xrp()/
# iou()/mpt()/canonicalize() and getInt64Value, plus bounded +/- on amounts:
# INT64_MIN cannot occur because canonicalize() keeps the mantissa well within
# int64_t, and operands are bounded by total supply (~10^17 XRP, ~10^18 MPT).
signed-integer-overflow:protocol/STAmount.cpp
# STAmount::operator+ signed addition — operands are bounded by total supply
# (~10^17 for XRP, ~10^18 for MPT) so overflow cannot occur in practice.
signed-integer-overflow:operator+*STAmount*
# nft::cipheredTaxon uses intentional uint32 wraparound (LCG permutation);
# the helper lives in the generated protocol header nft.h.
unsigned-integer-overflow:protocol/nft.h
# STAmount::getRate uses unsigned shift and addition
unsigned-integer-overflow:*STAmount*getRate*
# STAmount::serialize uses unsigned bitwise operations
unsigned-integer-overflow:*STAmount*serialize*
# STPathElement::getHash multiplies/adds accumulators (non-secure, speed-first).
unsigned-integer-overflow:protocol/STPathSet.cpp
# beast XorShiftEngine PRNG and murmurhash3 mixing wrap by design.
unsigned-integer-overflow:beast/xor_shift_engine.h
# Number::normalizeToRange multiplies the mantissa by powers of ten; the result
# is intentionally allowed to wrap while searching for the in-range value.
unsigned-integer-overflow:basics/Number.h
# Counter / sequence arithmetic with intentional unsigned wraparound, each
# guarded by an explicit overflow or domain check at the call site:
# base_uint operator++/-- wrap by definition;
# ApplyView::insertPage ++page is asserted to wrap to 0 (page exhaustion);
# confineOwnerCount documents "overflow is well defined on unsigned";
# NFTokenMint checks tokenSeq + 1u == 0u; AmendmentTable does (seq - 1) / 256.
unsigned-integer-overflow:basics/base_uint.h
unsigned-integer-overflow:ledger/ApplyView.cpp
unsigned-integer-overflow:ledger/helpers/AccountRootHelpers.cpp
unsigned-integer-overflow:tx/transactors/nft/NFTokenMint.cpp
unsigned-integer-overflow:app/misc/detail/AmendmentTable.cpp
# Sentinel / bounded subtractions that wrap by design (loop counters, reverse
# iteration, "not found" sentinels, balance math bounded by issuance invariants,
# base58/base64 codec index math, hash-router and role bit math).
unsigned-integer-overflow:shamap/SHAMap.cpp
unsigned-integer-overflow:protocol/Permissions.cpp
unsigned-integer-overflow:protocol/tokens.cpp
unsigned-integer-overflow:basics/base64.cpp
unsigned-integer-overflow:json/json_value.cpp
unsigned-integer-overflow:app/misc/NetworkOPs.cpp
unsigned-integer-overflow:rpc/detail/Role.cpp
unsigned-integer-overflow:tx/transactors/oracle/OracleSet.cpp
unsigned-integer-overflow:ledger/helpers/MPTokenHelpers.cpp
unsigned-integer-overflow:crypto/RFC1751.cpp
unsigned-integer-overflow:tx/paths/detail/StrandFlow.h
unsigned-integer-overflow:protocol/STObject.h
# GetAggregatePrice negates an unsigned trim count to step a reverse iterator;
# trimCount is bounded by the price set size.
unsigned-integer-overflow:rpc/handlers/orderbook/GetAggregatePrice.cpp
# Test-only intentional overflow/underflow in fixture and unit-test arithmetic.
unsigned-integer-overflow:tests/libxrpl/basics/RangeSet.cpp
unsigned-integer-overflow:test/app/Batch_test.cpp
unsigned-integer-overflow:test/app/Invariants_test.cpp
unsigned-integer-overflow:test/app/Loan_test.cpp
unsigned-integer-overflow:test/app/NFToken_test.cpp
unsigned-integer-overflow:test/app/OfferMPT_test.cpp
unsigned-integer-overflow:test/app/Offer_test.cpp
unsigned-integer-overflow:test/app/Path_test.cpp
unsigned-integer-overflow:test/jtx/impl/acctdelete.cpp
unsigned-integer-overflow:test/ledger/SkipList_test.cpp
unsigned-integer-overflow:test/rpc/Subscribe_test.cpp
signed-integer-overflow:test/basics/XRPAmount_test.cpp
# nft::cipheredTaxon uses intentional uint32 wraparound (LCG permutation)
unsigned-integer-overflow:cipheredTaxon

View File

@@ -1091,13 +1091,10 @@ AMMWithdraw::singleWithdrawEPrice(
// t = T*(T + A*E*(f - 2))/(T*f - A*E)
Number const ae = amountBalance * ePrice;
auto const f = getFee(tfee);
auto const denom = lptAMMBalance * f - ae;
// fixCleanup3_3_0: guard against division by zero
// when ePrice == lptAMMBalance*f/amountBalance
if (view.rules().enabled(fixCleanup3_3_0) && denom == beast::kZero)
return {tecAMM_FAILED, STAmount{}};
auto tokNoRoundCb = [&] { return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / denom; };
auto tokProdCb = [&] { return (lptAMMBalance + ae * (f - 2)) / denom; };
auto tokNoRoundCb = [&] {
return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae);
};
auto tokProdCb = [&] { return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae); };
auto const tokensAdj =
getRoundedLPTokens(view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No);
if (tokensAdj <= beast::kZero)

View File

@@ -2229,31 +2229,6 @@ private:
ammAlice.withdraw(alice_, XRPAmount{9'999'999'999});
BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100}));
});
// singleWithdrawEPrice: crafted ePrice = lptAMMBalance*f/amountBalance
// makes the denominator (T*f - A*E) exactly zero.
// Pre-fixCleanup3_3_0: std::overflow_error escapes to the
// transactor backstop and is returned as tefEXCEPTION.
// Post-fixCleanup3_3_0: denominator check returns tecAMM_FAILED.
//
// Pool: USD(100)/EUR(100), baseFee=1000 (1%).
// Alice is the creator so her discounted fee is 100 (0.1%), f=0.001.
// ePrice = lptAMMBalance(100) * f(0.001) / amountBalance(100) = 0.001
testAMM(
[&](AMM& ammAlice, Env& env) {
auto const err =
env.enabled(fixCleanup3_3_0) ? Ter(tecAMM_FAILED) : Ter(tefEXCEPTION);
ammAlice.withdraw(
WithdrawArg{
.account = alice_,
.asset1Out = USD(0),
.maxEP = IOUAmount{1, -3}, // ePrice=0.001 → denom=0
.err = err});
},
{{USD(100), EUR(100)}},
1000,
std::nullopt,
{all - fixCleanup3_3_0, all});
}
void

View File

@@ -196,78 +196,74 @@ stringIsUInt256Sized(std::string const& pBuffStr)
void
PeerImp::run()
{
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::run, shared_from_this()));
return;
}
// Must use post, not dispatch. Callers (onHandoff, addActive) hold
// overlay_.mutex_ and dispatch can run inline on an io_context thread,
// which would cause doAccept() -> overlay_.activate() to deadlock on
// the same mutex.
post(strand_, [self = shared_from_this()]() {
auto parseLedgerHash = [](std::string_view value) -> std::optional<uint256> {
if (uint256 ret; ret.parseHex(value))
return ret;
auto parseLedgerHash = [](std::string_view value) -> std::optional<uint256> {
if (uint256 ret; ret.parseHex(value))
return ret;
if (auto const s = base64Decode(value); s.size() == uint256::size())
return uint256::fromRaw(s);
if (auto const s = base64Decode(value); s.size() == uint256::size())
return uint256::fromRaw(s);
return std::nullopt;
};
return std::nullopt;
};
std::optional<uint256> closed;
std::optional<uint256> previous;
std::optional<uint256> closed;
std::optional<uint256> previous;
if (auto const iter = self->headers_.find("Closed-Ledger"); iter != self->headers_.end())
{
closed = parseLedgerHash(iter->value());
if (auto const iter = headers_.find("Closed-Ledger"); iter != headers_.end())
{
closed = parseLedgerHash(iter->value());
if (!closed)
self->fail("Malformed handshake data (1)");
}
if (!closed)
fail("Malformed handshake data (1)");
}
if (auto const iter = self->headers_.find("Previous-Ledger"); iter != self->headers_.end())
{
previous = parseLedgerHash(iter->value());
if (auto const iter = headers_.find("Previous-Ledger"); iter != headers_.end())
{
previous = parseLedgerHash(iter->value());
if (!previous)
self->fail("Malformed handshake data (2)");
}
if (!previous)
fail("Malformed handshake data (2)");
}
if (previous && !closed)
self->fail("Malformed handshake data (3)");
if (previous && !closed)
fail("Malformed handshake data (3)");
{
std::scoped_lock const sl(self->recentLock_);
if (closed)
self->closedLedgerHash_ = *closed;
if (previous)
self->previousLedgerHash_ = *previous;
}
{
std::scoped_lock const sl(recentLock_);
if (closed)
closedLedgerHash_ = *closed;
if (previous)
previousLedgerHash_ = *previous;
}
if (self->inbound_)
{
self->doAccept();
}
else
{
self->doProtocolStart();
}
if (inbound_)
{
doAccept();
}
else
{
doProtocolStart();
}
// Anything else that needs to be done with the connection should be
// done in doProtocolStart
// Anything else that needs to be done with the connection should be
// done in doProtocolStart
});
}
void
PeerImp::stop()
{
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::stop, shared_from_this()));
return;
}
dispatch(strand_, [self = shared_from_this()]() {
if (!self->socket_.is_open())
return;
if (!socket_.is_open())
return;
close();
self->close();
});
}
//------------------------------------------------------------------------------
@@ -275,130 +271,115 @@ PeerImp::stop()
void
PeerImp::send(std::shared_ptr<Message> const& m)
{
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::send, shared_from_this(), m));
return;
}
if (gracefulClose_)
return;
if (detaching_)
return;
if (!socket_.is_open())
return;
dispatch(strand_, [self = shared_from_this(), m]() {
if (self->gracefulClose_)
return;
if (self->detaching_)
return;
if (!self->socket_.is_open())
return;
auto validator = m->getValidatorKey();
if (validator && !squelch_.expireSquelch(*validator))
{
overlay_.reportOutboundTraffic(
TrafficCount::Category::SquelchSuppressed,
static_cast<int>(m->getBuffer(compressionEnabled_).size()));
return;
}
auto validator = m->getValidatorKey();
if (validator && !self->squelch_.expireSquelch(*validator))
{
self->overlay_.reportOutboundTraffic(
TrafficCount::Category::SquelchSuppressed,
static_cast<int>(m->getBuffer(self->compressionEnabled_).size()));
return;
}
// report categorized outgoing traffic
overlay_.reportOutboundTraffic(
safeCast<TrafficCount::Category>(m->getCategory()),
static_cast<int>(m->getBuffer(compressionEnabled_).size()));
// report categorized outgoing traffic
self->overlay_.reportOutboundTraffic(
safeCast<TrafficCount::Category>(m->getCategory()),
static_cast<int>(m->getBuffer(self->compressionEnabled_).size()));
// report total outgoing traffic
overlay_.reportOutboundTraffic(
TrafficCount::Category::Total, static_cast<int>(m->getBuffer(compressionEnabled_).size()));
// report total outgoing traffic
self->overlay_.reportOutboundTraffic(
TrafficCount::Category::Total,
static_cast<int>(m->getBuffer(self->compressionEnabled_).size()));
auto sendqSize = sendQueue_.size();
auto sendqSize = self->sendQueue_.size();
if (sendqSize < Tuning::kTargetSendQueue)
{
// To detect a peer that does not read from their
// side of the connection, we expect a peer to have
// a small senq periodically
largeSendq_ = 0;
}
else if (auto sink = journal_.debug(); sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0)
{
std::string const n = name();
sink << n << " sendq: " << sendqSize;
}
if (sendqSize < Tuning::kTargetSendQueue)
{
// To detect a peer that does not read from their
// side of the connection, we expect a peer to have
// a small sendq periodically
self->largeSendq_ = 0;
}
else if (
auto sink = self->journal_.debug();
sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0)
{
std::string const n = self->name();
sink << n << " sendq: " << sendqSize;
}
sendQueue_.push(m);
self->sendQueue_.push(m);
if (sendqSize != 0)
return;
if (sendqSize != 0)
return;
boost::asio::async_write(
stream_,
boost::asio::buffer(sendQueue_.front()->getBuffer(compressionEnabled_)),
bind_executor(
strand_,
std::bind(
&PeerImp::onWriteMessage,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2)));
boost::asio::async_write(
self->stream_,
boost::asio::buffer(self->sendQueue_.front()->getBuffer(self->compressionEnabled_)),
bind_executor(
self->strand_,
std::bind(
&PeerImp::onWriteMessage, self, std::placeholders::_1, std::placeholders::_2)));
});
}
void
PeerImp::sendTxQueue()
{
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::sendTxQueue, shared_from_this()));
return;
}
if (!txQueue_.empty())
{
protocol::TMHaveTransactions ht;
std::ranges::for_each(
txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); });
JLOG(pJournal_.trace()) << "sendTxQueue " << txQueue_.size();
txQueue_.clear();
send(std::make_shared<Message>(ht, protocol::mtHAVE_TRANSACTIONS));
}
dispatch(strand_, [self = shared_from_this()]() {
if (!self->txQueue_.empty())
{
protocol::TMHaveTransactions ht;
std::ranges::for_each(
self->txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); });
JLOG(self->pJournal_.trace()) << "sendTxQueue " << self->txQueue_.size();
self->txQueue_.clear();
self->send(std::make_shared<Message>(ht, protocol::mtHAVE_TRANSACTIONS));
}
});
}
void
PeerImp::addTxQueue(uint256 const& hash)
{
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::addTxQueue, shared_from_this(), hash));
return;
}
dispatch(strand_, [self = shared_from_this(), hash]() {
if (self->txQueue_.size() == reduce_relay::kMaxTxQueueSize)
{
JLOG(self->pJournal_.warn()) << "addTxQueue exceeds the cap";
self->sendTxQueue();
}
if (txQueue_.size() == reduce_relay::kMaxTxQueueSize)
{
JLOG(pJournal_.warn()) << "addTxQueue exceeds the cap";
sendTxQueue();
}
txQueue_.insert(hash);
JLOG(pJournal_.trace()) << "addTxQueue " << txQueue_.size();
self->txQueue_.insert(hash);
JLOG(self->pJournal_.trace()) << "addTxQueue " << self->txQueue_.size();
});
}
void
PeerImp::removeTxQueue(uint256 const& hash)
{
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::removeTxQueue, shared_from_this(), hash));
return;
}
auto removed = txQueue_.erase(hash);
JLOG(pJournal_.trace()) << "removeTxQueue " << removed;
dispatch(strand_, [self = shared_from_this(), hash]() {
auto removed = self->txQueue_.erase(hash);
JLOG(self->pJournal_.trace()) << "removeTxQueue " << removed;
});
}
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 &&
usage_.disconnect(pJournal_))
dispatch(strand_, [self = shared_from_this(), fee, context]() {
if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) &&
self->usage_.disconnect(self->pJournal_))
{
// Sever the connection.
overlay_.incPeerDisconnectCharges();
fail("charge: Resources");
// Sever the connection
self->overlay_.incPeerDisconnectCharges();
self->fail("charge: Resources");
}
});
}
@@ -628,20 +609,14 @@ PeerImp::close()
void
PeerImp::fail(std::string const& reason)
{
if (!strand_.running_in_this_thread())
{
post(
strand_,
std::bind(
(void (Peer::*)(std::string const&))&PeerImp::fail, shared_from_this(), reason));
return;
}
if (journal_.active(beast::Severity::Warning) && socket_.is_open())
{
std::string const n = name();
JLOG(journal_.warn()) << n << " failed: " << reason;
}
close();
dispatch(strand_, [self = shared_from_this(), reason]() {
if (self->journal_.active(beast::Severity::Warning) && self->socket_.is_open())
{
std::string const n = self->name();
JLOG(self->journal_.warn()) << n << " failed: " << reason;
}
self->close();
});
}
void
@@ -2677,45 +2652,42 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMTransactions> const& m)
void
PeerImp::onMessage(std::shared_ptr<protocol::TMSquelch> const& m)
{
using on_message_fn = void (PeerImp::*)(std::shared_ptr<protocol::TMSquelch> const&);
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind((on_message_fn)&PeerImp::onMessage, shared_from_this(), m));
return;
}
dispatch(strand_, [self = shared_from_this(), m]() {
if (!m->has_validatorpubkey())
{
self->fee_.update(Resource::kFeeInvalidData, "squelch no pubkey");
return;
}
auto validator = m->validatorpubkey();
auto const slice{makeSlice(validator)};
if (!publicKeyType(slice))
{
self->fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey");
return;
}
PublicKey const key(slice);
if (!m->has_validatorpubkey())
{
fee_.update(Resource::kFeeInvalidData, "squelch no pubkey");
return;
}
auto validator = m->validatorpubkey();
auto const slice{makeSlice(validator)};
if (!publicKeyType(slice))
{
fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey");
return;
}
PublicKey const key(slice);
// Ignore the squelch for validator's own messages.
if (key == self->app_.getValidationPublicKey())
{
JLOG(self->pJournal_.debug())
<< "onMessage: TMSquelch discarding validator's squelch " << slice;
return;
}
// Ignore the squelch for validator's own messages.
if (key == app_.getValidationPublicKey())
{
JLOG(pJournal_.debug()) << "onMessage: TMSquelch discarding validator's squelch " << slice;
return;
}
std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0;
if (!m->squelch())
{
self->squelch_.removeSquelch(key);
}
else if (!self->squelch_.addSquelch(key, std::chrono::seconds{duration}))
{
self->fee_.update(Resource::kFeeInvalidData, "squelch duration");
}
std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0;
if (!m->squelch())
{
squelch_.removeSquelch(key);
}
else if (!squelch_.addSquelch(key, std::chrono::seconds{duration}))
{
fee_.update(Resource::kFeeInvalidData, "squelch duration");
}
JLOG(pJournal_.debug()) << "onMessage: TMSquelch " << slice << " " << id() << " " << duration;
JLOG(self->pJournal_.debug())
<< "onMessage: TMSquelch " << slice << " " << self->id() << " " << duration;
});
}
//--------------------------------------------------------------------------

View File

@@ -30,7 +30,6 @@
#include <expected>
#include <limits>
#include <memory>
#include <string>
#include <utility>
namespace xrpl {
@@ -350,15 +349,13 @@ doLedgerGrpc(RPC::GRPCContext<org::xrpl::rpc::v1::GetLedgerRequest>& context)
auto end = std::chrono::system_clock::now();
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() * 1.0;
// Guard the per-item rates: an empty ledger has zero objects and/or zero
// transactions, and dividing by zero is undefined for these doubles.
auto const numObjects = response.ledger_objects().objects_size();
auto const numTxns = response.transactions_list().transactions_size();
std::string const msPerObj = numObjects > 0 ? std::to_string(duration / numObjects) : "n/a";
std::string const msPerTxn = numTxns > 0 ? std::to_string(duration / numTxns) : "n/a";
JLOG(context.j.warn()) << __func__ << " - Extract time = " << duration
<< " - num objects = " << numObjects << " - num txns = " << numTxns
<< " - ms per obj " << msPerObj << " - ms per txn " << msPerTxn;
<< " - num objects = " << response.ledger_objects().objects_size()
<< " - num txns = " << response.transactions_list().transactions_size()
<< " - ms per obj "
<< duration / response.ledger_objects().objects_size()
<< " - ms per txn "
<< duration / response.transactions_list().transactions_size();
return {response, status};
}

View File

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

View File

@@ -1,22 +1,12 @@
cmake_minimum_required(VERSION 3.21)
set(name validator-keys-conan-test)
set(name example)
set(version 0.1.0)
project(${name} VERSION ${version} LANGUAGES CXX)
find_package(xrpl CONFIG REQUIRED)
# 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}"
)
add_executable(example)
target_sources(example PRIVATE src/example.cpp)
target_link_libraries(example PRIVATE xrpl::libxrpl)

View File

@@ -1,4 +1,4 @@
import os
from pathlib import Path
from conan.tools.build import can_run
from conan.tools.cmake import CMake, cmake_layout
@@ -6,13 +6,15 @@ from conan.tools.cmake import CMake, cmake_layout
from conan import ConanFile
class ValidatorKeysConanTest(ConanFile):
name = "validator-keys-conan-test"
class Example(ConanFile):
name = "example"
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,
}
@@ -23,20 +25,19 @@ class ValidatorKeysConanTest(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(target="validator-keys")
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()
def test(self):
if can_run(self):
cmd = os.path.join(self.cpp.build.bindir, "validator-keys")
self.run(f'"{cmd}" --unittest', env="conanrun")
cmd_path = Path(self.build_folder) / self.cpp.build.bindir / "example"
self.run(cmd_path, env="conanrun")

View File

@@ -0,0 +1,10 @@
#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

@@ -1,4 +0,0 @@
# 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

@@ -1,34 +0,0 @@
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

@@ -1,27 +0,0 @@
# 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

@@ -1,25 +0,0 @@
# Release Notes
# Change Log
# Releases
## Version 0.3.2
This release overhauls the Travis CI configuration to cover more cases more robustly, and fixes a Windows build error introduced in 0.3.1.
### New and Improved Features
- Restructure Travis CI builds to use 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

@@ -1,137 +0,0 @@
#[===================================================================[
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

@@ -1,87 +0,0 @@
#[===================================================================[
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

@@ -1,103 +0,0 @@
#[===================================================================[
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

@@ -1,117 +0,0 @@
# 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

@@ -1,276 +0,0 @@
#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

@@ -1,158 +0,0 @@
#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

@@ -1,455 +0,0 @@
#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

@@ -1,30 +0,0 @@
#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

@@ -1,58 +0,0 @@
#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

@@ -1,287 +0,0 @@
#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

@@ -1,374 +0,0 @@
#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