Compare commits

...

2 Commits

Author SHA1 Message Date
Mayukha Vadari
744355c1d9 fix: Keep GFM alert markers on their own line
proseWrap:never collapsed each `> [!NOTE]`-style blockquote onto its
body line, which breaks GitHub/GitLab's alert-callout rendering (the
marker must be alone on the blockquote's first line). Mark each one
with a prettier-ignore comment so prettier leaves it split across two
lines, and fix two more instances the review missed (CONTRIBUTING.md,
docs/build/advanced_conan.md).
2026-09-01 17:33:45 -04:00
Mayukha Vadari
9dde6c907f style: Collapse hard-wrapped Markdown prose to one line per paragraph
Set prettier's proseWrap to never for Markdown files so paragraphs are
written and formatted as a single line rather than manually wrapped at
a fixed column, matching the convention already used in AGENTS.md.
2026-09-01 17:22:18 -04:00
49 changed files with 1083 additions and 3896 deletions

View File

@@ -1,34 +1,19 @@
# Levelization
Levelization is the term used to describe efforts to prevent xrpld from
having or creating cyclic dependencies.
Levelization is the term used to describe efforts to prevent xrpld from having or creating cyclic dependencies.
xrpld code is organized into directories under `src/xrpld`, `src/libxrpl` (and
`src/test`) representing modules. The modules are intended to be
organized into "tiers" or "levels" such that a module from one level can
only include code from lower levels. Additionally, a module
in one level should never include code in an `impl` or `detail` folder of any level
other than its own.
xrpld code is organized into directories under `src/xrpld`, `src/libxrpl` (and `src/test`) representing modules. The modules are intended to be organized into "tiers" or "levels" such that a module from one level can only include code from lower levels. Additionally, a module in one level should never include code in an `impl` or `detail` folder of any level other than its own.
The codebase is split into two main areas:
- **libxrpl** (`src/libxrpl`, `include/xrpl`): Reusable library modules with public interfaces
- **xrpld** (`src/xrpld`): Application-specific implementation code
Unfortunately, over time, enforcement of levelization has been
inconsistent, so the current state of the code doesn't necessarily
reflect these rules. Whenever possible, developers should refactor any
levelization violations they find (by moving files or individual
classes). At the very least, don't make things worse.
Unfortunately, over time, enforcement of levelization has been inconsistent, so the current state of the code doesn't necessarily reflect these rules. Whenever possible, developers should refactor any levelization violations they find (by moving files or individual classes). At the very least, don't make things worse.
The table below summarizes the _desired_ division of modules, based on the current
state of the xrpld code. The levels are numbered from
the bottom up with the lower level, lower numbered, more independent
modules listed first, and the higher level, higher numbered modules with
more dependencies listed later.
The table below summarizes the _desired_ division of modules, based on the current state of the xrpld code. The levels are numbered from the bottom up with the lower level, lower numbered, more independent modules listed first, and the higher level, higher numbered modules with more dependencies listed later.
**tl;dr:** The modules listed first are more independent than the modules
listed later.
**tl;dr:** The modules listed first are more independent than the modules listed later.
## libxrpl Modules (Reusable Libraries)
@@ -55,80 +40,37 @@ listed later.
## Test Modules
| Level / Tier | Module(s) |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| 11 | test/jtx test/beast test/csf |
| 12 | test/unit_test |
| 13 | test/crypto test/conditions test/json test/resource test/shamap test/peerfinder test/basics test/overlay |
| 14 | test |
| 15 | test/net test/protocol test/ledger test/consensus test/core test/server test/nodestore |
| 16 | test/rpc test/app |
| Level / Tier | Module(s) |
| --- | --- |
| 11 | test/jtx test/beast test/csf |
| 12 | test/unit_test |
| 13 | test/crypto test/conditions test/json test/resource test/shamap test/peerfinder test/basics test/overlay |
| 14 | test |
| 15 | test/net test/protocol test/ledger test/consensus test/core test/server test/nodestore |
| 16 | test/rpc test/app |
(Note that `test` levelization is _much_ less important and _much_ less
strictly enforced than `xrpl`/`xrpld` levelization, other than the requirement
that `test` code should _never_ be included in `xrpl` or `xrpld` code.)
(Note that `test` levelization is _much_ less important and _much_ less strictly enforced than `xrpl`/`xrpld` levelization, other than the requirement that `test` code should _never_ be included in `xrpl` or `xrpld` code.)
## Validation
The [levelization](generate.py) script takes no parameters,
reads no environment variables, and can be run from any directory,
as long as it is in the expected location in the xrpld repo.
It can be run at any time from within a checked out repo, and will
do an analysis of all the `#include`s in
the xrpld source. The only caveat is that it runs much slower
under Windows than in Linux. It hasn't yet been tested under MacOS.
It generates many files of [results](results):
The [levelization](generate.py) script takes no parameters, reads no environment variables, and can be run from any directory, as long as it is in the expected location in the xrpld repo. It can be run at any time from within a checked out repo, and will do an analysis of all the `#include`s in the xrpld source. The only caveat is that it runs much slower under Windows than in Linux. It hasn't yet been tested under MacOS. It generates many files of [results](results):
- `rawincludes.txt`: The raw dump of the `#includes`
- `paths.txt`: A second dump grouping the source module
to the destination module, de-duped, and with frequency counts.
- `includes/`: A directory where each file represents a module and
contains a list of modules and counts that the module _includes_.
- `included_by/`: Similar to `includes/`, but the other way around. Each
file represents a module and contains a list of modules and counts
that _include_ the module.
- [`loops.txt`](results/loops.txt): A list of direct loops detected
between modules as they actually exist, as opposed to how they are
desired as described above. In a perfect repo, this file will be
empty.
This file is committed to the repo, and is used by the [levelization
Github workflow](../../workflows/reusable-check-levelization.yml) to validate
that nothing changed.
- [`ordering.txt`](results/ordering.txt): A list showing relationships
between modules where there are no loops as they actually exist, as
opposed to how they are desired as described above.
This file is committed to the repo, and is used by the [levelization
Github workflow](../../workflows/reusable-check-levelization.yml) to validate
that nothing changed.
- [`levelization.yml`](../../workflows/reusable-check-levelization.yml)
Github Actions workflow to test that levelization loops haven't
changed. Unfortunately, if changes are detected, it can't tell if
they are improvements or not, so if you have resolved any issues or
done anything else to improve levelization, run `generate.py`,
and commit the updated results.
- `paths.txt`: A second dump grouping the source module to the destination module, de-duped, and with frequency counts.
- `includes/`: A directory where each file represents a module and contains a list of modules and counts that the module _includes_.
- `included_by/`: Similar to `includes/`, but the other way around. Each file represents a module and contains a list of modules and counts that _include_ the module.
- [`loops.txt`](results/loops.txt): A list of direct loops detected between modules as they actually exist, as opposed to how they are desired as described above. In a perfect repo, this file will be empty. This file is committed to the repo, and is used by the [levelization Github workflow](../../workflows/reusable-check-levelization.yml) to validate that nothing changed.
- [`ordering.txt`](results/ordering.txt): A list showing relationships between modules where there are no loops as they actually exist, as opposed to how they are desired as described above. This file is committed to the repo, and is used by the [levelization Github workflow](../../workflows/reusable-check-levelization.yml) to validate that nothing changed.
- [`levelization.yml`](../../workflows/reusable-check-levelization.yml) Github Actions workflow to test that levelization loops haven't changed. Unfortunately, if changes are detected, it can't tell if they are improvements or not, so if you have resolved any issues or done anything else to improve levelization, run `generate.py`, and commit the updated results.
The `loops.txt` and `ordering.txt` files relate the modules
using comparison signs, which indicate the number of times each
module is included in the other.
The `loops.txt` and `ordering.txt` files relate the modules using comparison signs, which indicate the number of times each module is included in the other.
- `A > B` means that A should probably be at a higher level than B,
because B is included in A significantly more than A is included in B.
These results can be included in both `loops.txt` and `ordering.txt`.
Because `ordering.txt`only includes relationships where B is not
included in A at all, it will only include these types of results.
- `A ~= B` means that A and B are included in each other a different
number of times, but the values are so close that the script can't
definitively say that one should be above the other. These results
will only be included in `loops.txt`.
- `A == B` means that A and B include each other the same number of
times, so the script has no clue which should be higher. These results
will only be included in `loops.txt`.
- `A > B` means that A should probably be at a higher level than B, because B is included in A significantly more than A is included in B. These results can be included in both `loops.txt` and `ordering.txt`. Because `ordering.txt`only includes relationships where B is not included in A at all, it will only include these types of results.
- `A ~= B` means that A and B are included in each other a different number of times, but the values are so close that the script can't definitively say that one should be above the other. These results will only be included in `loops.txt`.
- `A == B` means that A and B include each other the same number of times, so the script has no clue which should be higher. These results will only be included in `loops.txt`.
The committed files hide the detailed values intentionally, to
prevent false alarms and merging issues, and because it's easy to
get those details locally.
The committed files hide the detailed values intentionally, to prevent false alarms and merging issues, and because it's easy to get those details locally.
1. Run `generate.py`
2. Grep the modules in `paths.txt`.
- For example, if a cycle is found `A ~= B`, simply `grep -w
A .github/scripts/levelization/results/paths.txt | grep -w B`
- For example, if a cycle is found `A ~= B`, simply `grep -w A .github/scripts/levelization/results/paths.txt | grep -w B`

View File

@@ -1,41 +1,20 @@
## Renaming ripple(d) to xrpl(d)
In the initial phases of development of the XRPL, the open source codebase was
called "rippled" and it remains with that name even today. Today, over 1000
nodes run the application, and code contributions have been submitted by
developers located around the world. The XRPL community is larger than ever.
In light of the decentralized and diversified nature of XRPL, we will rename any
references to `ripple` and `rippled` to `xrpl` and `xrpld`, when appropriate.
In the initial phases of development of the XRPL, the open source codebase was called "rippled" and it remains with that name even today. Today, over 1000 nodes run the application, and code contributions have been submitted by developers located around the world. The XRPL community is larger than ever. In light of the decentralized and diversified nature of XRPL, we will rename any references to `ripple` and `rippled` to `xrpl` and `xrpld`, when appropriate.
See [here](https://xls.xrpl.org/xls/XLS-0095-rename-rippled-to-xrpld.html) for
more information.
See [here](https://xls.xrpl.org/xls/XLS-0095-rename-rippled-to-xrpld.html) for more information.
### Scripts
To facilitate this transition, there will be multiple scripts that developers
can run on their own PRs and forks to minimize conflicts. Each script should be
run from the repository root.
To facilitate this transition, there will be multiple scripts that developers can run on their own PRs and forks to minimize conflicts. Each script should be run from the repository root.
1. `.github/scripts/rename/definitions.sh`: This script will rename all
definitions, such as include guards, from `RIPPLE_XXX` and `RIPPLED_XXX` to
`XRPL_XXX`.
2. `.github/scripts/rename/copyright.sh`: This script will remove superfluous
copyright notices.
3. `.github/scripts/rename/cmake.sh`: This script will rename all CMake files
from `RippleXXX.cmake` or `RippledXXX.cmake` to `XrplXXX.cmake`, and any
references to `ripple` and `rippled` (with or without capital letters) to
`xrpl` and `xrpld`, respectively. The name of the binary will remain as-is,
and will only be renamed to `xrpld` by a later script.
4. `.github/scripts/rename/binary.sh`: This script will rename the binary from
`rippled` to `xrpld`, and reverses the symlink so that `rippled` points to
the `xrpld` binary.
5. `.github/scripts/rename/namespace.sh`: This script will rename the C++
namespaces from `ripple` to `xrpl`.
6. `.github/scripts/rename/config.sh`: This script will rename the config from
`rippled.cfg` to `xrpld.cfg`, and updating the code accordingly. The old
filename will still be accepted.
7. `.github/scripts/rename/docs.sh`: This script will rename any lingering
references of `ripple(d)` to `xrpl(d)` in code, comments, and documentation.
1. `.github/scripts/rename/definitions.sh`: This script will rename all definitions, such as include guards, from `RIPPLE_XXX` and `RIPPLED_XXX` to `XRPL_XXX`.
2. `.github/scripts/rename/copyright.sh`: This script will remove superfluous copyright notices.
3. `.github/scripts/rename/cmake.sh`: This script will rename all CMake files from `RippleXXX.cmake` or `RippledXXX.cmake` to `XrplXXX.cmake`, and any references to `ripple` and `rippled` (with or without capital letters) to `xrpl` and `xrpld`, respectively. The name of the binary will remain as-is, and will only be renamed to `xrpld` by a later script.
4. `.github/scripts/rename/binary.sh`: This script will rename the binary from `rippled` to `xrpld`, and reverses the symlink so that `rippled` points to the `xrpld` binary.
5. `.github/scripts/rename/namespace.sh`: This script will rename the C++ namespaces from `ripple` to `xrpl`.
6. `.github/scripts/rename/config.sh`: This script will rename the config from `rippled.cfg` to `xrpld.cfg`, and updating the code accordingly. The old filename will still be accepted.
7. `.github/scripts/rename/docs.sh`: This script will rename any lingering references of `ripple(d)` to `xrpl(d)` in code, comments, and documentation.
You can run all these scripts from the repository root as follows:

7
.prettierrc.yaml Normal file
View File

@@ -0,0 +1,7 @@
# Collapse hard-wrapped Markdown prose to one line per paragraph instead of
# preserving manual line breaks (the prettier default). Scoped to Markdown
# only, since proseWrap also reflows YAML block scalars.
overrides:
- files: "*.md"
options:
proseWrap: never

View File

@@ -28,8 +28,7 @@ This section contains changes targeting a future version.
### Additions
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`.
When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present.
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`. When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present.
- `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681))
- `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)):
- `TRANSACTION_FORMATS`: Describes the fields and their optionality for each transaction type, including common fields shared across all transactions.
@@ -208,9 +207,7 @@ This release contains bug fixes only and no API changes.
- Adds `AMMDelete` transaction type to delete `AMM` instance.
- Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`.
- Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account.
- Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`,
`tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination
for `AMMDeposit` and `AMMWithdraw` transactions.
- Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`, `tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination for `AMMDeposit` and `AMMWithdraw` transactions.
- Adds new transaction result codes:
- tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision.
- tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances.
@@ -253,14 +250,11 @@ This release contains bug fixes only and no API changes.
## XRP Ledger server version 1.10.0
[Version 1.10.0](https://github.com/XRPLF/rippled/releases/tag/1.10.0)
was released on Mar 14, 2023.
[Version 1.10.0](https://github.com/XRPLF/rippled/releases/tag/1.10.0) was released on Mar 14, 2023.
### Breaking changes in 1.10
- If the `XRPFees` feature is enabled, the `fee_ref` field will be
removed from the [ledger subscription stream](https://xrpl.org/subscribe.html#ledger-stream), because it will no longer
have any meaning.
- If the `XRPFees` feature is enabled, the `fee_ref` field will be removed from the [ledger subscription stream](https://xrpl.org/subscribe.html#ledger-stream), because it will no longer have any meaning.
# Unit tests for API changes

View File

@@ -13,8 +13,7 @@ In API version 2, the following deprecated methods are no longer available: ([#4
## Modifications to JSON transaction element in API version 2
In API version 2, JSON elements for transaction output have been changed and made consistent for all methods which output transactions. ([#4775](https://github.com/XRPLF/rippled/pull/4775))
This helps to unify the JSON serialization format of transactions. ([clio#722](https://github.com/XRPLF/clio/issues/722), [#4727](https://github.com/XRPLF/rippled/issues/4727))
In API version 2, JSON elements for transaction output have been changed and made consistent for all methods which output transactions. ([#4775](https://github.com/XRPLF/rippled/pull/4775)) This helps to unify the JSON serialization format of transactions. ([clio#722](https://github.com/XRPLF/clio/issues/722), [#4727](https://github.com/XRPLF/rippled/issues/4727))
- JSON transaction element is named `tx_json`
- Binary transaction element is named `tx_blob`

218
BUILD.md
View File

@@ -1,91 +1,70 @@
| :warning: **WARNING** :warning: |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| :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. |
## Minimum Requirements
For the hardware needed to run a node, see
[System Requirements](https://xrpl.org/system-requirements.html).
For the hardware needed to run a node, see [System Requirements](https://xrpl.org/system-requirements.html).
For the software needed to build xrpld, see the
[environment setup guide](./docs/build/environment.md).
For the software needed to build xrpld, see the [environment setup guide](./docs/build/environment.md).
## Operating Systems
### 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.
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.
The minimum supported version is macOS 15 (Sequoia).
CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
Many `xrpld` engineers use macOS for development. The minimum supported version is macOS 15 (Sequoia). CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
### Windows
Windows is used by some engineers for development only, and is not recommended
for production use.
Windows is used by some engineers for development only, and is not recommended for production use.
## Steps
### Branches
For the latest set of untested features, or to contribute, choose the `develop`
branch.
For the latest set of untested features, or to contribute, choose the `develop` branch.
```bash
git checkout develop
```
For a release candidate, choose the relevant release branch, e.g.
`release/3.2.x`.
For a release candidate, choose the relevant release branch, e.g. `release/3.2.x`.
```bash
git checkout release/3.2.x
```
For a stable release, choose one of the [tagged
releases](https://github.com/XRPLF/rippled/releases).
For a stable release, choose one of the [tagged releases](https://github.com/XRPLF/rippled/releases).
### Set Up Conan
Once your [development environment](./docs/build/environment.md) is ready, set
Conan up for this repository:
Once your [development environment](./docs/build/environment.md) is ready, set Conan up for this repository:
```bash
./conan/init.sh
```
That installs our [`global.conf`](./conan/global.conf), our Conan
[profiles](./conan/profiles), and the `xrplf` remote that hosts some of our
dependencies. It honours `CONAN_HOME` and never deletes an existing Conan home,
so it is safe to re-run — it only overwrites the files it manages.
That installs our [`global.conf`](./conan/global.conf), our Conan [profiles](./conan/profiles), and the `xrplf` remote that hosts some of our dependencies. It honours `CONAN_HOME` and never deletes an existing Conan home, so it is safe to re-run — it only overwrites the files it manages.
<!-- prettier-ignore -->
> [!TIP]
> In the [Nix development shell](./docs/build/nix.md#conan-configuration) this is
> already done for you: the script runs on entry.
> In the [Nix development shell](./docs/build/nix.md#conan-configuration) this is already done for you: the script runs on entry.
You can inspect the resulting profile with `conan profile show`. If it is not
suitable for your environment, create a custom profile and pass it to Conan — see
[Advanced Conan configuration](./docs/build/advanced_conan.md).
You can inspect the resulting profile with `conan profile show`. If it is not suitable for your environment, create a custom profile and pass it to Conan — see [Advanced Conan configuration](./docs/build/advanced_conan.md).
### Set Up Ccache
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.
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).
#### Windows
You can install it using Chocolatey, i.e. `choco install ccache`. If you already
have Ccache installed, then `choco upgrade ccache` will update it to the latest
version. However, if you see an error such as:
You can install it using Chocolatey, i.e. `choco install ccache`. If you already have Ccache installed, then `choco upgrade ccache` will update it to the latest version. However, if you see an error such as:
```
terminate called after throwing an instance of 'std::bad_alloc'
@@ -93,8 +72,7 @@ terminate called after throwing an instance of 'std::bad_alloc'
C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Microsoft.CppCommon.targets(617,5): error MSB6006: "cl.exe" exited with code 3.
```
then please install a specific version of Ccache that we know works, via: `choco
install ccache --version 4.11.3 --allow-downgrade`.
then please install a specific version of Ccache that we know works, via: `choco install ccache --version 4.11.3 --allow-downgrade`.
### Build and Test
@@ -105,14 +83,9 @@ install ccache --version 4.11.3 --allow-downgrade`.
cd .build
```
You can use any directory name. Conan treats your working directory as an
install folder and generates files with implementation details.
You don't need to worry about these files, but make sure to change
your working directory to your build directory before calling Conan.
You can use any directory name. Conan treats your working directory as an install folder and generates files with implementation details. You don't need to worry about these files, but make sure to change your working directory to your build directory before calling Conan.
**Note:** You can specify a directory for the installation files by adding
the `install-folder` or `-if` option to every `conan install` command
in the next step.
**Note:** You can specify a directory for the installation files by adding the `install-folder` or `-if` option to every `conan install` command in the next step.
2. Use conan to generate CMake files for every configuration you want to build:
@@ -123,25 +96,15 @@ install ccache --version 4.11.3 --allow-downgrade`.
To build Debug, in the next step, be sure to set `-DCMAKE_BUILD_TYPE=Debug`
For a single-configuration generator, e.g. `Unix Makefiles` or `Ninja`,
you only need to run this command once.
For a multi-configuration generator, e.g. `Visual Studio`, you may want to
run it more than once.
For a single-configuration generator, e.g. `Unix Makefiles` or `Ninja`, you only need to run this command once. For a multi-configuration generator, e.g. `Visual Studio`, you may want to run it more than once.
Each of these commands should also have a different `build_type` setting.
A second command with the same `build_type` setting will overwrite the files
generated by the first. You can pass the build type on the command line with
`--settings build_type=$BUILD_TYPE` or in the profile itself,
under the section `[settings]` with the key `build_type`.
Each of these commands should also have a different `build_type` setting. A second command with the same `build_type` setting will overwrite the files generated by the first. You can pass the build type on the command line with `--settings build_type=$BUILD_TYPE` or in the profile itself, under the section `[settings]` with the key `build_type`.
3. Configure CMake and pass the toolchain file generated by Conan, located at
`$OUTPUT_FOLDER/build/generators/conan_toolchain.cmake`.
3. Configure CMake and pass the toolchain file generated by Conan, located at `$OUTPUT_FOLDER/build/generators/conan_toolchain.cmake`.
Single-config generators:
Pass the CMake variable [`CMAKE_BUILD_TYPE`][build_type]
and make sure it matches the one of the `build_type` settings
you chose in the previous step.
Pass the CMake variable [`CMAKE_BUILD_TYPE`][build_type] and make sure it matches the one of the `build_type` settings you chose in the previous step.
For example, to build Debug, in the next command, replace "Release" with "Debug"
@@ -159,9 +122,7 @@ install ccache --version 4.11.3 --allow-downgrade`.
4. Build `xrpld`.
For a single-configuration generator, it will build whatever configuration
you passed for `CMAKE_BUILD_TYPE`. For a multi-configuration generator, you
must pass the option `--config` to select the build configuration.
For a single-configuration generator, it will build whatever configuration you passed for `CMAKE_BUILD_TYPE`. For a multi-configuration generator, you must pass the option `--config` to select the build configuration.
Single-config generators:
@@ -176,8 +137,7 @@ install ccache --version 4.11.3 --allow-downgrade`.
cmake --build . --config Debug --parallel N
```
Replace the `--parallel` parameter N with the desired number of parallel jobs. A common starting point is half of the number of available CPU
cores.
Replace the `--parallel` parameter N with the desired number of parallel jobs. A common starting point is half of the number of available CPU cores.
5. Test xrpld.
@@ -194,28 +154,20 @@ install ccache --version 4.11.3 --allow-downgrade`.
./Debug/xrpld --unittest --unittest-jobs N
```
Replace the `--unittest-jobs` parameter N with the desired unit tests
concurrency. Recommended setting is half of the number of available CPU
cores.
Replace the `--unittest-jobs` parameter N with the desired unit tests concurrency. Recommended setting is half of the number of available CPU cores.
The location of `xrpld` binary in your build directory depends on your
CMake generator. Pass `--help` to see the rest of the command line options.
The location of `xrpld` binary in your build directory depends on your CMake generator. Pass `--help` to see the rest of the command line options.
## Code generation
The protocol wrapper classes in `include/xrpl/protocol_autogen/` are generated
from macro definition files in `include/xrpl/protocol/detail/`. If you modify
the macro files (e.g. `transactions.macro`, `ledger_entries.macro`) or the
generation scripts/templates in `cmake/scripts/codegen/`, you need to regenerate the
files:
The protocol wrapper classes in `include/xrpl/protocol_autogen/` are generated from macro definition files in `include/xrpl/protocol/detail/`. If you modify the macro files (e.g. `transactions.macro`, `ledger_entries.macro`) or the generation scripts/templates in `cmake/scripts/codegen/`, you need to regenerate the files:
```
cmake --build . --target setup_code_gen # create venv and install dependencies (once)
cmake --build . --target code_gen # regenerate code
```
The same targets are also available as a standalone project, which does not
need the dependencies to be configured first:
The same targets are also available as a standalone project, which does not need the dependencies to be configured first:
```
cmake -S cmake/codegen -B build/codegen
@@ -223,15 +175,11 @@ cmake --build build/codegen --target setup_code_gen
cmake --build build/codegen --target code_gen
```
The regenerated files should be committed alongside your changes. CI verifies
that they are up-to-date.
The regenerated files should be committed alongside your changes. CI verifies that they are up-to-date.
## Coverage report
The coverage report is intended for developers using compilers GCC
or Clang (including Apple Clang). It is generated by the build target `coverage`,
which is only enabled when the `coverage` option is set, e.g. with
`--options coverage=True` in `conan` or `-Dcoverage=ON` variable in `cmake`
The coverage report is intended for developers using compilers GCC or Clang (including Apple Clang). It is generated by the build target `coverage`, which is only enabled when the `coverage` option is set, e.g. with `--options coverage=True` in `conan` or `-Dcoverage=ON` variable in `cmake`
Prerequisites for the coverage report:
@@ -239,34 +187,19 @@ Prerequisites for the coverage report:
- `gcov` for GCC or `llvm-cov` for Clang, usually installed with the compiler
- `Debug` build type
<!-- prettier-ignore -->
> [!NOTE]
> Clang coverage is not available in the [Nix development shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell):
> its `clang` shells do not ship `llvm-cov`. Use a `gcc` shell instead (`.#gcc`,
> or `.#gcc-plain` on Linux), which provides a `gcov` matching its compiler.
> Clang coverage is not available in the [Nix development shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell): its `clang` shells do not ship `llvm-cov`. Use a `gcc` shell instead (`.#gcc`, or `.#gcc-plain` on Linux), which provides a `gcov` matching its compiler.
A coverage report is created when the following steps are completed, in order:
1. `xrpld` binary built with instrumentation data, enabled by the `coverage`
option mentioned above
1. `xrpld` binary built with instrumentation data, enabled by the `coverage` option mentioned above
2. completed one or more run of the unit tests, which populates coverage capture data
3. completed run of the `gcovr` tool (which internally invokes either `gcov` or `llvm-cov`)
to assemble both instrumentation data and the coverage capture data into a coverage report
3. completed run of the `gcovr` tool (which internally invokes either `gcov` or `llvm-cov`) to assemble both instrumentation data and the coverage capture data into a coverage report
The last step of the above is automated into a single target `coverage`. The instrumented
`xrpld` binary can also be used for regular development or testing work, at
the cost of extra disk space utilization and a small performance hit
(to store coverage capture data). Since `xrpld` binary is simply a dependency of the
coverage report target, it is possible to re-run the `coverage` target without
rebuilding the `xrpld` binary. Note, running of the unit tests before the `coverage`
target is left to the developer. Each such run will append to the coverage data
collected in the build directory.
The last step of the above is automated into a single target `coverage`. The instrumented `xrpld` binary can also be used for regular development or testing work, at the cost of extra disk space utilization and a small performance hit (to store coverage capture data). Since `xrpld` binary is simply a dependency of the coverage report target, it is possible to re-run the `coverage` target without rebuilding the `xrpld` binary. Note, running of the unit tests before the `coverage` target is left to the developer. Each such run will append to the coverage data collected in the build directory.
The default coverage report format is `html-details`, but the user
can override it to any of the formats listed in `Builds/CMake/CodeCoverage.cmake`
by setting the `coverage_format` variable in `cmake`. It is also possible
to generate more than one format at a time by setting the `coverage_extra_args`
variable in `cmake`. The specific command line used to run the `gcovr` tool will be
displayed if the `CODE_COVERAGE_VERBOSE` variable is set.
The default coverage report format is `html-details`, but the user can override it to any of the formats listed in `Builds/CMake/CodeCoverage.cmake` by setting the `coverage_format` variable in `cmake`. It is also possible to generate more than one format at a time by setting the `coverage_extra_args` variable in `cmake`. The specific command line used to run the `gcovr` tool will be displayed if the `CODE_COVERAGE_VERBOSE` variable is set.
Example use with some cmake variables set:
@@ -277,16 +210,14 @@ cmake -DCMAKE_BUILD_TYPE=Debug -Dcoverage=ON -Dxrpld=ON -Dtests=ON -Dcoverage_te
cmake --build . --target coverage
```
After the `coverage` target is completed, the generated coverage report will be
stored inside the build directory, as either of:
After the `coverage` target is completed, the generated coverage report will be stored inside the build directory, as either of:
- file named `coverage.`_extension_, with a suitable extension for the report format, or
- directory named `coverage`, with the `index.html` and other files inside, for the `html-details` or `html-nested` report formats.
## Sanitizers
To build dependencies and xrpld with sanitizer instrumentation, set the
`SANITIZERS` environment variable when running `conan install` and use the `sanitizers` profile:
To build dependencies and xrpld with sanitizer instrumentation, set the `SANITIZERS` environment variable when running `conan install` and use the `sanitizers` profile:
```bash
export SANITIZERS=address,undefinedbehavior
@@ -300,42 +231,27 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details.
## Options
| Option | Default Value | Description |
| ---------------- | ------------- | ----------------------------------------------------------------------------- |
| `assert` | OFF | Force enabling assertions. |
| `coverage` | OFF | Prepare the coverage report. |
| `rust` | OFF | Build the Rust crates and the C++ code that depends on them. |
| `tests` | OFF | Build tests. |
| `unity` | OFF | Configure a unity build. |
| `verify_headers` | ON | Make the `verify-headers` target available to compile each header on its own. |
| `xrpld` | OFF | Build the xrpld application, and not just the libxrpl library. |
| `werr` | OFF | Treat compilation warnings as errors |
| `wextra` | OFF | Enable additional compilation warnings |
| Option | Default Value | Description |
| --- | --- | --- |
| `assert` | OFF | Force enabling assertions. |
| `coverage` | OFF | Prepare the coverage report. |
| `rust` | OFF | Build the Rust crates and the C++ code that depends on them. |
| `tests` | OFF | Build tests. |
| `unity` | OFF | Configure a unity build. |
| `verify_headers` | ON | Make the `verify-headers` target available to compile each header on its own. |
| `xrpld` | OFF | Build the xrpld application, and not just the libxrpl library. |
| `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
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.
[Unity builds][unity-build] 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.
### Rust crates
The Rust crates in `crates/` are only part of the build when `rust` is ON. With
`-Drust=OFF` (the default) the `crates` directory is not added to the build, no
cxxbridge bindings are generated, and the C++ tests that exercise the Rust
interop are not compiled — so no Rust toolchain is needed. CI builds always pass
`-Drust=ON`.
The Rust crates in `crates/` are only part of the build when `rust` is ON. With `-Drust=OFF` (the default) the `crates` directory is not added to the build, no cxxbridge bindings are generated, and the C++ tests that exercise the Rust interop are not compiled — so no Rust toolchain is needed. CI builds always pass `-Drust=ON`.
With `-Drust=ON` you need one extra dependency: a Rust toolchain (`cargo`,
`rustc`) matching the channel pinned in
[`rust-toolchain.toml`](./rust-toolchain.toml), which compiles the crates and
generates the cxxbridge bindings. It is provided by the
[Nix development shell](./docs/build/nix.md), so `-Drust=ON` works there without
any extra setup; otherwise install it as described in
[Rust](./docs/build/environment.md#rust).
With `-Drust=ON` you need one extra dependency: a Rust toolchain (`cargo`, `rustc`) matching the channel pinned in [`rust-toolchain.toml`](./rust-toolchain.toml), which compiles the crates and generates the cxxbridge bindings. It is provided by the [Nix development shell](./docs/build/nix.md), so `-Drust=ON` works there without any extra setup; otherwise install it as described in [Rust](./docs/build/environment.md#rust).
The crates also have their own Rust unit tests. Those are run with `cargo` and
need only the Rust toolchain, independently of CMake and of the `rust` option
(CI runs them with `cargo nextest`):
The crates also have their own Rust unit tests. Those are run with `cargo` and need only the Rust toolchain, independently of CMake and of the `rust` option (CI runs them with `cargo nextest`):
```bash
cargo test --manifest-path crates/Cargo.toml --workspace
@@ -343,22 +259,13 @@ cargo test --manifest-path crates/Cargo.toml --workspace
### Verifying headers
The regular build only compiles `.cpp` files, so a header is only ever checked
through whatever translation unit happens to include it. A header that forgets
an `#include` is not caught as long as every `.cpp` that uses it includes its
missing dependency first. The `verify_headers` option (ON by default) adds a
`verify-headers` target that compiles every header on its own, which fails if a
header is not self-contained:
The regular build only compiles `.cpp` files, so a header is only ever checked through whatever translation unit happens to include it. A header that forgets an `#include` is not caught as long as every `.cpp` that uses it includes its missing dependency first. The `verify_headers` option (ON by default) adds a `verify-headers` target that compiles every header on its own, which fails if a header is not self-contained:
```bash
cmake --build . --target verify-headers
```
The per-header objects are excluded from the `all` target, so a normal build
never compiles them; they are built only through `verify-headers`. The generated
translation units do appear in `compile_commands.json`, so clang-tidy (and
clangd and IDEs) can lint each header on its own. Pass `-Dverify_headers=OFF` to
omit them entirely.
The per-header objects are excluded from the `all` target, so a normal build never compiles them; they are built only through `verify-headers`. The generated translation units do appear in `compile_commands.json`, so clang-tidy (and clangd and IDEs) can lint each header on its own. Pass `-Dverify_headers=OFF` to omit them entirely.
## Troubleshooting
@@ -385,20 +292,15 @@ After any updates or changes to dependencies, you may need to do the following:
4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile).
5. Re-run [conan install](#build-and-test).
If you are using the Nix development shell, whether prebuilt Conan binaries apply
depends on your platform — see
[Prebuilt packages](./docs/build/nix.md#prebuilt-packages).
If you are using the Nix development shell, whether prebuilt Conan binaries apply depends on your platform — see [Prebuilt packages](./docs/build/nix.md#prebuilt-packages).
#### 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 [set Conan up](#set-up-conan) so the `xrplf` remote is configured, or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes).
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 [set Conan up](#set-up-conan) so the `xrplf` remote is configured, or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes).
### `protobuf/port_def.inc` file not found
If `cmake --build .` results in an error due to a missing a protobuf file, then
you might have generated CMake files for a different `build_type` than the
`CMAKE_BUILD_TYPE` you passed to Conan.
If `cmake --build .` results in an error due to a missing a protobuf file, then you might have generated CMake files for a different `build_type` than the `CMAKE_BUILD_TYPE` you passed to Conan.
```
/xrpld/.build/pb-xrpl.libpb/xrpl/proto/xrpl.pb.h:10:10: fatal error: 'google/protobuf/port_def.inc' file not found

View File

@@ -1,31 +1,19 @@
The XRP Ledger has many and diverse stakeholders, and everyone deserves
a chance to contribute meaningful changes to the code that runs the
XRPL.
The XRP Ledger has many and diverse stakeholders, and everyone deserves a chance to contribute meaningful changes to the code that runs the XRPL.
# Contributing
We assume you are familiar with the general practice of [making
contributions on GitHub][contrib]. This file includes only special
instructions specific to this project.
We assume you are familiar with the general practice of [making contributions on GitHub][contrib]. This file includes only special instructions specific to this project.
## Before you start
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).
- `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).
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
your verifying key. Please set up [signature verification][signing].
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 your verifying key. Please set up [signature verification][signing].
In general, external contributions should be developed in your personal
[fork][forking]. Contributions from developers with write permissions
should be done in [the main repository][xrpld] in a branch with
a permitted prefix. Permitted prefixes are:
In general, external contributions should be developed in your personal [fork][forking]. Contributions from developers with write permissions should be done in [the main repository][xrpld] in a branch with a permitted prefix. Permitted prefixes are:
- XLS-[a-zA-Z0-9]+/.+
- e.g. XLS-0033d/mpt-clarify-STEitherAmount
@@ -34,87 +22,47 @@ a permitted prefix. Permitted prefixes are:
- [Organization name]/.+
- e.g. ripple/antithesis
Regardless of where the branch is created, please open a _draft_ pull
request as soon as possible after pushing the branch to Github, to
increase visibility, and ease feedback during the development process.
Regardless of where the branch is created, please open a _draft_ pull request as soon as possible after pushing the branch to Github, to increase visibility, and ease feedback during the development process.
## Major contributions
If your contribution is a major feature or breaking change, then you
must first write an XRP Ledger Standard (XLS) describing it. Go to
[XRPL-Standards](https://github.com/XRPLF/XRPL-Standards/discussions),
choose the next available standard number, and open a discussion with an
appropriate title to propose your draft standard.
If your contribution is a major feature or breaking change, then you must first write an XRP Ledger Standard (XLS) describing it. Go to [XRPL-Standards](https://github.com/XRPLF/XRPL-Standards/discussions), choose the next available standard number, and open a discussion with an appropriate title to propose your draft standard.
When you submit a pull request, please link the corresponding XLS in the
description. An XLS still in `Draft` status is considered a
work-in-progress and open for discussion. Please allow time for
questions, suggestions, and changes to the XLS draft. It is the
responsibility of the XLS author to update the draft to match the final
implementation when its corresponding pull request is merged, unless the
author delegates that responsibility to others.
When you submit a pull request, please link the corresponding XLS in the description. An XLS still in `Draft` status is considered a work-in-progress and open for discussion. Please allow time for questions, suggestions, and changes to the XLS draft. It is the responsibility of the XLS author to update the draft to match the final implementation when its corresponding pull request is merged, unless the author delegates that responsibility to others.
Any amendment or major RPC change requires either a new XLS or an update
to an existing XLS. Neither change will be released (in an amendment's
case, marked as `Supported::yes`) until the corresponding XLS's status
is `Final`.
Any amendment or major RPC change requires either a new XLS or an update to an existing XLS. Neither change will be released (in an amendment's case, marked as `Supported::yes`) until the corresponding XLS's status is `Final`.
## Before making a pull request
(Or marking a draft pull request as ready.)
Changes that alter transaction processing must be guarded by an
[Amendment](https://xrpl.org/amendments.html).
All other changes that maintain the existing behavior do not need an
Amendment.
Changes that alter transaction processing must be guarded by an [Amendment](https://xrpl.org/amendments.html). All other changes that maintain the existing behavior do not need an Amendment.
Ensure that your code compiles according to the build instructions in
[`BUILD.md`](./BUILD.md).
Ensure that your code compiles according to the build instructions in [`BUILD.md`](./BUILD.md).
Please write tests for your code.
If your test can be run offline, in under 60 seconds, then it can be an
automatic test run by `xrpld --unittest`.
Otherwise, it must be a manual test.
Please write tests for your code. If your test can be run offline, in under 60 seconds, then it can be an automatic test run by `xrpld --unittest`. Otherwise, it must be a manual test.
If you create new source files, they must be organized as follows:
- If the files are in any of the `libxrpl` modules, the headers (`.h`) must go
under `include/xrpl`, and source (`.cpp`) files must go under
`src/libxrpl`.
- If the files are in any of the `libxrpl` modules, the headers (`.h`) must go under `include/xrpl`, and source (`.cpp`) files must go under `src/libxrpl`.
- All other non-test files must go under `src/xrpld`.
- All test source files must go under `src/test`.
- All benchmark source files must go under `src/benchmarks`.
The source must be formatted according to the style guide below. The easiest
way to satisfy this is to install the [`pre-commit`](#pre-commit-hooks) hooks,
which format and lint your changes automatically on every commit.
The source must be formatted according to the style guide below. The easiest way to satisfy this is to install the [`pre-commit`](#pre-commit-hooks) hooks, which format and lint your changes automatically on every commit.
Header includes must be [levelized](.github/scripts/levelization).
Changes should be usually squashed down into a single commit.
Some larger or more complicated change sets make more sense,
and are easier to review if organized into multiple logical commits.
Either way, all commits should fit the following criteria:
Changes should be usually squashed down into a single commit. Some larger or more complicated change sets make more sense, and are easier to review if organized into multiple logical commits. Either way, all commits should fit the following criteria:
- Changes should be presented in a single commit or a logical
sequence of commits.
Specifically, chronological commits that simply
reflect the history of how the author implemented
the change, "warts and all", are not useful to
reviewers.
- Every commit should have a [good message](#good-commit-messages).
to explain a specific aspects of the change.
- Changes should be presented in a single commit or a logical sequence of commits. Specifically, chronological commits that simply reflect the history of how the author implemented the change, "warts and all", are not useful to reviewers.
- Every commit should have a [good message](#good-commit-messages). to explain a specific aspects of the change.
- Every commit should be signed.
- Every commit should be well-formed (builds successfully,
unit tests passing), as this helps to resolve merge
conflicts, and makes it easier to use `git bisect`
to find bugs.
- Every commit should be well-formed (builds successfully, unit tests passing), as this helps to resolve merge conflicts, and makes it easier to use `git bisect` to find bugs.
### Good commit messages
Refer to
["How to Write a Git Commit Message"](https://cbea.ms/git-commit/)
for general rules on writing a good commit message.
Refer to ["How to Write a Git Commit Message"](https://cbea.ms/git-commit/) for general rules on writing a good commit message.
tl;dr
@@ -124,9 +72,7 @@ tl;dr
> 3. Capitalize the subject line.
> 4. Do not end the subject line with a period.
> 5. Use the imperative mood in the subject line.
> - A properly formed Git commit subject line should always be able
> to complete the following sentence: "If applied, this commit will
> _your subject line here_".
> - A properly formed Git commit subject line should always be able to complete the following sentence: "If applied, this commit will _your subject line here_".
> 6. Wrap the body at 72 characters.
> 7. Use the body to explain what and why vs. how.
@@ -134,32 +80,15 @@ tl;dr
In general, pull requests use `develop` as the base branch.
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.
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.
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.
While a pull request is marked as a "Draft", you can rebase or reorganize the
commits in the pull request as desired.
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. While a pull request is marked as a "Draft", you can rebase or reorganize the commits in the pull request as desired.
Github pull requests are created as "Ready" by default, or you can mark
a "Draft" pull request as "Ready".
Once a pull request is marked as "Ready",
any changes must be added as new commits. Do not
force-push to a branch in a pull request under review.
(This includes rebasing your branch onto the updated base branch.
Use a merge operation, instead or hit the "Update branch" button
at the bottom of the Github PR page.)
This preserves the ability for reviewers to filter changes since their last
review.
Github pull requests are created as "Ready" by default, or you can mark a "Draft" pull request as "Ready". Once a pull request is marked as "Ready", any changes must be added as new commits. Do not force-push to a branch in a pull request under review. (This includes rebasing your branch onto the updated base branch. Use a merge operation, instead or hit the "Update branch" button at the bottom of the Github PR page.) This preserves the ability for reviewers to filter changes since their last review.
A pull request must obtain **approvals from at least two reviewers**
before it can be considered for merge by a Maintainer.
Maintainers retain discretion to require more approvals if they feel the
credibility of the existing approvals is insufficient.
A pull request must obtain **approvals from at least two reviewers** before it can be considered for merge by a Maintainer. Maintainers retain discretion to require more approvals if they feel the credibility of the existing approvals is insufficient.
Pull requests must be merged by [squash-and-merge][squash]
to preserve a linear history for the `develop` branch.
Pull requests must be merged by [squash-and-merge][squash] to preserve a linear history for the `develop` branch.
### Type of Change
@@ -180,54 +109,26 @@ First letter after the type prefix should be capitalized, and the type prefix sh
### "Ready to merge"
A pull request should only have the "Ready to merge" label added when it
meets a few criteria:
A pull request should only have the "Ready to merge" label added when it meets a few criteria:
1. It must have two approving reviews [as described
above](#pull-requests). (Exception: PRs that are deemed "trivial"
only need one approval.)
2. All CI checks must be complete and passed. (One-off failures may
be acceptable if they are related to a known issue.)
1. It must have two approving reviews [as described above](#pull-requests). (Exception: PRs that are deemed "trivial" only need one approval.)
2. All CI checks must be complete and passed. (One-off failures may be acceptable if they are related to a known issue.)
3. The PR must have a [good commit message](#good-commit-messages).
- If the PR started with a good commit message, and it doesn't
need to be updated, the author can indicate that in a comment.
- Any contributor, preferably the author, can leave a comment
suggesting a commit message.
- If the author squashes and rebases the code in preparation for
merge, they should also ensure the commit message(s) are updated
as well.
4. The PR branch must be up to date with the base branch (usually
`develop`). This is usually accomplished by merging the base branch
into the feature branch, but if the other criteria are met, the
changes can be squashed and rebased on top of the base branch.
5. Finally, and most importantly, the author of the PR must
positively indicate that the PR is ready to merge. That can be
accomplished by adding the "Ready to merge" label if their role
allows, or by leaving a comment to the effect that the PR is ready to
merge.
- If the PR started with a good commit message, and it doesn't need to be updated, the author can indicate that in a comment.
- Any contributor, preferably the author, can leave a comment suggesting a commit message.
- If the author squashes and rebases the code in preparation for merge, they should also ensure the commit message(s) are updated as well.
4. The PR branch must be up to date with the base branch (usually `develop`). This is usually accomplished by merging the base branch into the feature branch, but if the other criteria are met, the changes can be squashed and rebased on top of the base branch.
5. Finally, and most importantly, the author of the PR must positively indicate that the PR is ready to merge. That can be accomplished by adding the "Ready to merge" label if their role allows, or by leaving a comment to the effect that the PR is ready to merge.
Once the "Ready to merge" label is added, a maintainer may merge the PR
at any time, so don't use it lightly.
Once the "Ready to merge" label is added, a maintainer may merge the PR at any time, so don't use it lightly.
# Style guide
This is a non-exhaustive list of recommended style guidelines. These are
not always strictly enforced and serve as a way to keep the codebase
coherent rather than a set of _thou shalt not_ commandments.
This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent rather than a set of _thou shalt not_ commandments.
## Pre-commit hooks
We use the [`pre-commit`](https://pre-commit.com/) framework to run the
formatting and linting tools that keep the codebase consistent. `pre-commit`
runs each tool configured in
[`.pre-commit-config.yaml`](./.pre-commit-config.yaml) in its own isolated
environment, so you don't need to install most of the individual tools
yourself. The version of each hook sourced from an external repository
(`clang-format`, `gersemi`, etc.) is pinned in that file, so running the hooks
locally uses exactly the same versions as CI. A few `local` hooks — most notably
`clang-tidy` and `cargo fmt` — run tools from your own environment; see
[Installing clang-tidy](#installing-clang-tidy) and
[Rust](./docs/build/environment.md#rust) for how to get those.
We use the [`pre-commit`](https://pre-commit.com/) framework to run the formatting and linting tools that keep the codebase consistent. `pre-commit` runs each tool configured in [`.pre-commit-config.yaml`](./.pre-commit-config.yaml) in its own isolated environment, so you don't need to install most of the individual tools yourself. The version of each hook sourced from an external repository (`clang-format`, `gersemi`, etc.) is pinned in that file, so running the hooks locally uses exactly the same versions as CI. A few `local` hooks — most notably `clang-tidy` and `cargo fmt` — run tools from your own environment; see [Installing clang-tidy](#installing-clang-tidy) and [Rust](./docs/build/environment.md#rust) for how to get those.
To get started, install `pre-commit` and enable the git hook scripts:
@@ -236,8 +137,7 @@ pip install pre-commit
pre-commit install
```
Once installed, the hooks run automatically on your staged files every time you
`git commit`. You can also run them on demand:
Once installed, the hooks run automatically on your staged files every time you `git commit`. You can also run them on demand:
```bash
# Run all hooks against only the staged files
@@ -260,18 +160,11 @@ The hooks configured in this repository include, among others:
- `prettier`, `black`, `shfmt` — formatting for JavaScript/JSON/Markdown, Python, and shell
- `cspell` — spell checking
The same hooks run in CI on every pull request, so running them locally before
you push helps you avoid CI failures.
The same hooks run in CI on every pull request, so running them locally before you push helps you avoid CI failures.
## Formatting
All code must conform to `clang-format`, according to the settings in
[`.clang-format`](./.clang-format), unless the result would be unreasonably
difficult to read or maintain. The `clang-format` version is pinned in
[`.pre-commit-config.yaml`](./.pre-commit-config.yaml), so the
[`pre-commit`](#pre-commit-hooks) hook always formats with the same version as
CI. To demarcate lines that should be left as-is, surround them with comments
like this:
All code must conform to `clang-format`, according to the settings in [`.clang-format`](./.clang-format), unless the result would be unreasonably difficult to read or maintain. The `clang-format` version is pinned in [`.pre-commit-config.yaml`](./.pre-commit-config.yaml), so the [`pre-commit`](#pre-commit-hooks) hook always formats with the same version as CI. To demarcate lines that should be left as-is, surround them with comments like this:
```
// clang-format off
@@ -279,20 +172,17 @@ like this:
// clang-format on
```
The easiest way to format your changes is to let the `pre-commit` hook run
automatically on commit, or to run it manually:
The easiest way to format your changes is to let the `pre-commit` hook run automatically on commit, or to run it manually:
```bash
pre-commit run clang-format --all-files
```
You can also format individual files in place by running `clang-format -i <file>...`
from any directory within this project.
You can also format individual files in place by running `clang-format -i <file>...` from any directory within this project.
<!-- prettier-ignore -->
> [!NOTE]
> This uses whatever `clang-format` version is installed locally, which may
> differ from the pinned version used by `pre-commit` and CI, so the results
> can vary.
> This uses whatever `clang-format` version is installed locally, which may differ from the pinned version used by `pre-commit` and CI, so the results can vary.
There is a Continuous Integration job that runs clang-format on pull requests. If the code doesn't comply, a patch file that corrects auto-fixable formatting issues is generated.
@@ -353,8 +243,7 @@ Then run clang-tidy on your local changes:
run-clang-tidy -p build -allow-no-checks src tests
```
This will check all source files in the `src`, `include` and `tests` directories using the compile commands from your `build` directory.
If you wish to automatically fix whatever clang-tidy finds _and_ is capable of fixing, add `-fix -format` to the above command:
This will check all source files in the `src`, `include` and `tests` directories using the compile commands from your `build` directory. If you wish to automatically fix whatever clang-tidy finds _and_ is capable of fixing, add `-fix -format` to the above command:
```
run-clang-tidy -p build -quiet -fix -format -allow-no-checks src tests
@@ -364,27 +253,11 @@ run-clang-tidy -p build -quiet -fix -format -allow-no-checks src tests
## Contracts and instrumentation
We are using [Antithesis](https://antithesis.com/) for continuous fuzzing,
and keep a copy of [Antithesis C++ SDK](https://github.com/antithesishq/antithesis-sdk-cpp/)
in `external/antithesis-sdk`. One of the aims of fuzzing is to identify bugs
by finding external conditions which cause contracts violations inside `xrpld`.
The contracts are expressed as `XRPL_ASSERT` or `UNREACHABLE` (defined in
`include/xrpl/beast/utility/instrumentation.h`), which are effectively (outside
of Antithesis) wrappers for `assert(...)` with added name. The purpose of name
is to provide contracts with stable identity which does not rely on line numbers.
We are using [Antithesis](https://antithesis.com/) for continuous fuzzing, and keep a copy of [Antithesis C++ SDK](https://github.com/antithesishq/antithesis-sdk-cpp/) in `external/antithesis-sdk`. One of the aims of fuzzing is to identify bugs by finding external conditions which cause contracts violations inside `xrpld`. The contracts are expressed as `XRPL_ASSERT` or `UNREACHABLE` (defined in `include/xrpl/beast/utility/instrumentation.h`), which are effectively (outside of Antithesis) wrappers for `assert(...)` with added name. The purpose of name is to provide contracts with stable identity which does not rely on line numbers.
When `xrpld` is built with the Antithesis instrumentation enabled
(using `voidstar` CMake option) and ran on the Antithesis platform, the
contracts become
[test properties](https://antithesis.com/docs/using_antithesis/properties.html);
otherwise they are just like a regular `assert`.
To learn more about Antithesis, see
[How Antithesis Works](https://antithesis.com/docs/introduction/how_antithesis_works.html)
and [C++ SDK](https://antithesis.com/docs/using_antithesis/sdk/cpp/overview.html#)
When `xrpld` is built with the Antithesis instrumentation enabled (using `voidstar` CMake option) and ran on the Antithesis platform, the contracts become [test properties](https://antithesis.com/docs/using_antithesis/properties.html); otherwise they are just like a regular `assert`. To learn more about Antithesis, see [How Antithesis Works](https://antithesis.com/docs/introduction/how_antithesis_works.html) and [C++ SDK](https://antithesis.com/docs/using_antithesis/sdk/cpp/overview.html#)
We continue to use the old style `assert` or `assert(false)` in certain
locations, where the reporting of contract violations on the Antithesis
platform is either not possible or not useful.
We continue to use the old style `assert` or `assert(false)` in certain locations, where the reporting of contract violations on the Antithesis platform is either not possible or not useful.
For this reason:
@@ -392,38 +265,17 @@ For this reason:
- `constexpr` functions
- unit tests i.e. files under `src/test`
- unit tests-related modules (files under `beast/test` and `beast/unit_test`)
- Outside of the listed locations, do not use `assert`; use `XRPL_ASSERT` instead,
giving it unique name, with the short description of the contract.
- Outside of the listed locations, do not use `assert(false)`; use
`UNREACHABLE` instead, giving it unique name, with the description of the
condition being violated
- The contract name should start with a full name (including scope) of the
function, optionally a named lambda, followed by a colon `:` and a brief
(typically at most five words) description. `UNREACHABLE` contracts
can use slightly longer descriptions. If there are multiple overloads of the
function, use common sense to balance both brevity and unambiguity of the
function name. NOTE: the purpose of name is to provide stable means of
unique identification of every contract; for this reason try to avoid elements
which can change in some obvious refactors or when reinforcing the condition.
- Contract description typically (except for `UNREACHABLE`) should describe the
_expected_ condition, as in "I assert that _expected_ is true".
- Contract description for `UNREACHABLE` should describe the _unexpected_
situation which caused the line to have been reached.
- Example good name for an
`UNREACHABLE` macro `"json::operator==(Value, Value) : invalid type"`; example
good name for an `XRPL_ASSERT` macro `"json::Value::asCString : valid type"`.
- Example **bad** name
`"RFC1751::insert(char* s, int x, int start, int length) : length is greater than or equal zero"`
(missing namespace, unnecessary full function signature, description too verbose).
Good name: `"xrpl::RFC1751::insert : minimum length"`.
- In **few** well-justified cases a non-standard name can be used, in which case a
comment should be placed to explain the rationale (example in `contract.cpp`)
- Do **not** rename a contract without a good reason (e.g. the name no longer
reflects the location or the condition being checked)
- Outside of the listed locations, do not use `assert`; use `XRPL_ASSERT` instead, giving it unique name, with the short description of the contract.
- Outside of the listed locations, do not use `assert(false)`; use `UNREACHABLE` instead, giving it unique name, with the description of the condition being violated
- The contract name should start with a full name (including scope) of the function, optionally a named lambda, followed by a colon `:` and a brief (typically at most five words) description. `UNREACHABLE` contracts can use slightly longer descriptions. If there are multiple overloads of the function, use common sense to balance both brevity and unambiguity of the function name. NOTE: the purpose of name is to provide stable means of unique identification of every contract; for this reason try to avoid elements which can change in some obvious refactors or when reinforcing the condition.
- Contract description typically (except for `UNREACHABLE`) should describe the _expected_ condition, as in "I assert that _expected_ is true".
- Contract description for `UNREACHABLE` should describe the _unexpected_ situation which caused the line to have been reached.
- Example good name for an `UNREACHABLE` macro `"json::operator==(Value, Value) : invalid type"`; example good name for an `XRPL_ASSERT` macro `"json::Value::asCString : valid type"`.
- Example **bad** name `"RFC1751::insert(char* s, int x, int start, int length) : length is greater than or equal zero"` (missing namespace, unnecessary full function signature, description too verbose). Good name: `"xrpl::RFC1751::insert : minimum length"`.
- In **few** well-justified cases a non-standard name can be used, in which case a comment should be placed to explain the rationale (example in `contract.cpp`)
- Do **not** rename a contract without a good reason (e.g. the name no longer reflects the location or the condition being checked)
- Do not use `std::unreachable`
- Do not put contracts where they can be violated by an external condition
(e.g. timing, data payload before mandatory validation etc.) as this creates
bogus bug reports (and causes crashes of Debug builds)
- Do not put contracts where they can be violated by an external condition (e.g. timing, data payload before mandatory validation etc.) as this creates bogus bug reports (and causes crashes of Debug builds)
## Unit Tests
@@ -431,8 +283,7 @@ To execute all unit tests:
`xrpld --unittest --unittest-jobs=<number of cores>`
(Note: Using multiple cores on a Mac M1 can cause spurious test failures. The
cause is still under investigation. If you observe this problem, try specifying fewer jobs.)
(Note: Using multiple cores on a Mac M1 can cause spurious test failures. The cause is still under investigation. If you observe this problem, try specifying fewer jobs.)
To run a specific set of test suites:
@@ -440,11 +291,7 @@ To run a specific set of test suites:
xrpld --unittest TestSuiteName
```
Note: In this example, all tests with prefix `TestSuiteName` will be run, so if
`TestSuiteName1` and `TestSuiteName2` both exist, then both tests will run.
Alternatively, if the unit test name finds an exact match, it will stop
doing partial matches, i.e. if a unit test with a title of `TestSuiteName`
exists, then no other unit test will be executed, apart from `TestSuiteName`.
Note: In this example, all tests with prefix `TestSuiteName` will be run, so if `TestSuiteName1` and `TestSuiteName2` both exist, then both tests will run. Alternatively, if the unit test name finds an exact match, it will stop doing partial matches, i.e. if a unit test with a title of `TestSuiteName` exists, then no other unit test will be executed, apart from `TestSuiteName`.
## Avoid
@@ -454,44 +301,27 @@ exists, then no other unit test will be executed, apart from `TestSuiteName`.
4. Unmanaged memory allocation and raw pointers.
5. Macros and non-trivial templates (unless they add significant value).
6. Lambda patterns (unless these add significant value).
7. CPU or architecture-specific code unless there is a good reason to
include it, and where it is used, guard it with macros and provide
explanatory comments.
7. CPU or architecture-specific code unless there is a good reason to include it, and where it is used, guard it with macros and provide explanatory comments.
8. Importing new libraries unless there is a very good reason to do so.
## Seek to
9. Extend functionality of existing code rather than creating new code.
10. Prefer readability over terseness where important logic is
concerned.
11. Inline functions that are not used or are not likely to be used
elsewhere in the codebase.
12. Use clear and self-explanatory names for functions, variables,
structs and classes.
13. Use TitleCase for classes, structs and filenames, camelCase for
function and variable names, lower case for namespaces and folders.
14. Provide as many comments as you feel that a competent programmer
would need to understand what your code does.
10. Prefer readability over terseness where important logic is concerned.
11. Inline functions that are not used or are not likely to be used elsewhere in the codebase.
12. Use clear and self-explanatory names for functions, variables, structs and classes.
13. Use TitleCase for classes, structs and filenames, camelCase for function and variable names, lower case for namespaces and folders.
14. Provide as many comments as you feel that a competent programmer would need to understand what your code does.
# Maintainers
Maintainers are ecosystem participants with elevated access to the repository.
They are able to push new code, make decisions on when a release should be
made, etc.
Maintainers are ecosystem participants with elevated access to the repository. They are able to push new code, make decisions on when a release should be made, etc.
## Adding and removing
New maintainers can be proposed by two existing maintainers, subject to a vote
by a quorum of the existing maintainers.
A minimum of 50% support and a 50% participation is required.
In the event of a tie vote, the addition of the new maintainer will be
rejected.
New maintainers can be proposed by two existing maintainers, subject to a vote by a quorum of the existing maintainers. A minimum of 50% support and a 50% participation is required. In the event of a tie vote, the addition of the new maintainer will be rejected.
Existing maintainers can resign, or be subject to a vote for removal at the
behest of two existing maintainers.
A minimum of 60% agreement and 50% participation are required.
The XRP Ledger Foundation will have the ability, for cause, to remove an
existing maintainer without a vote.
Existing maintainers can resign, or be subject to a vote for removal at the behest of two existing maintainers. A minimum of 60% agreement and 50% participation are required. The XRP Ledger Foundation will have the ability, for cause, to remove an existing maintainer without a vote.
## Current Maintainers
@@ -507,8 +337,7 @@ Maintainers are users with maintain or admin access to the repo.
## Current Code Reviewers
Code Reviewers are developers who have the ability to review, approve, and
in some cases merge source code changes.
Code Reviewers are developers who have the ability to review, approve, and in some cases merge source code changes.
- [a1q123456](https://github.com/a1q123456) (Ripple)
- [Bronek](https://github.com/Bronek) (Ripple)
@@ -529,14 +358,11 @@ in some cases merge source code changes.
- [Tapanito](https://github.com/Tapanito) (Ripple)
- [ximinez](https://github.com/ximinez) (Ripple)
Developers not on this list are able and encouraged to submit feedback
on pending code changes (open pull requests).
Developers not on this list are able and encouraged to submit feedback on pending code changes (open pull requests).
## Instructions for maintainers
These instructions assume you have your git upstream remotes configured
to avoid accidental pushes to the main repo, and a remote group
specifying both of them. e.g.
These instructions assume you have your git upstream remotes configured to avoid accidental pushes to the main repo, and a remote group specifying both of them. e.g.
```
$ git remote -v | grep upstream
@@ -561,104 +387,58 @@ $ git config user.signingkey
### When and how to merge pull requests
The maintainer should double-check that the PR has met all the
necessary criteria, and can request additional information from the
owner, or additional reviews, and can always feel free to remove the
"Ready to merge" label if appropriate. The maintainer has final say on
whether a PR gets merged, and are encouraged to communicate and issues
or concerns to other maintainers.
The maintainer should double-check that the PR has met all the necessary criteria, and can request additional information from the owner, or additional reviews, and can always feel free to remove the "Ready to merge" label if appropriate. The maintainer has final say on whether a PR gets merged, and are encouraged to communicate and issues or concerns to other maintainers.
#### Most pull requests: "Squash and merge"
Most pull requests don't need special handling, and can simply be
merged using the "Squash and merge" button on the Github UI. Update
the suggested commit message, or modify it as needed.
Most pull requests don't need special handling, and can simply be merged using the "Squash and merge" button on the Github UI. Update the suggested commit message, or modify it as needed.
#### Slightly more complicated pull requests
Some pull requests need to be pushed to `develop` as more than one
commit. A PR author may _request_ to merge as separate commits. They
must _justify_ why separate commits are needed, and _specify_ how they
would like the commits to be merged. If you disagree with the author,
discuss it with them directly.
Some pull requests need to be pushed to `develop` as more than one commit. A PR author may _request_ to merge as separate commits. They must _justify_ why separate commits are needed, and _specify_ how they would like the commits to be merged. If you disagree with the author, discuss it with them directly.
If the process is reasonable, follow it. The simplest option is to do a
fast forward only merge (`--ff-only`) on the command line and push to
`develop`.
If the process is reasonable, follow it. The simplest option is to do a fast forward only merge (`--ff-only`) on the command line and push to `develop`.
Some examples of when separate commits are worthwhile are:
1. PRs where source files are reorganized in multiple steps.
2. PRs where the commits are mostly independent and _could_ be separate
PRs, but are pulled together into one PR under a commit theme or
issue.
3. PRs that are complicated enough that `git bisect` would not be much
help if it determined this PR introduced a problem.
2. PRs where the commits are mostly independent and _could_ be separate PRs, but are pulled together into one PR under a commit theme or issue.
3. PRs that are complicated enough that `git bisect` would not be much help if it determined this PR introduced a problem.
Either way, check that:
- The commits are based on the current tip of `develop`.
- The commits are clean: No merge commits (except when reverse
merging), no "[FOLD]" or "fixup!" messages.
- All commits are signed. If the commits are not signed by the author, use
`git commit --amend -S` to sign them yourself.
- At least one (but preferably all) of the commits has the PR number
in the commit message.
- The commits are clean: No merge commits (except when reverse merging), no "[FOLD]" or "fixup!" messages.
- All commits are signed. If the commits are not signed by the author, use `git commit --amend -S` to sign them yourself.
- At least one (but preferably all) of the commits has the PR number in the commit message.
The "Create a merge commit" and "Rebase and merge" options should be
disabled in the Github UI, but if you ever find them available **Do not
use them!**
The "Create a merge commit" and "Rebase and merge" options should be disabled in the Github UI, but if you ever find them available **Do not use them!**
### Releases
All releases, including release candidates and betas, are handled
differently from typical PRs. Most importantly, never use
the Github UI to merge a release.
All releases, including release candidates and betas, are handled differently from typical PRs. Most importantly, never use the Github UI to merge a release.
Xrpld uses a linear workflow model that can be summarized as:
1. In between releases, developers work against the `develop` branch.
2. Periodically, a maintainer will build and tag a beta version from
`develop`, which is pushed to `release`.
- Betas are usually released every two to three weeks, though that
schedule can vary depending on progress, availability, and other
factors.
3. When the changes in `develop` are considered stable and mature enough
to be ready to release, a release candidate (RC) is built and tagged
from `develop`, and merged to `release`.
- Further development for that release (primarily fixes) then
continues against `release`, while other development continues on
`develop`. Effectively, `release` is forked from `develop`. Changes
to `release` must be reverse merged to `develop`.
4. When the candidate has passed testing and is ready for release, the
final release is merged to `master`.
5. If any issues are found post-release, a hotfix / point release may be
created, which is merged to `master`, and then reverse merged to
`develop`.
2. Periodically, a maintainer will build and tag a beta version from `develop`, which is pushed to `release`.
- Betas are usually released every two to three weeks, though that schedule can vary depending on progress, availability, and other factors.
3. When the changes in `develop` are considered stable and mature enough to be ready to release, a release candidate (RC) is built and tagged from `develop`, and merged to `release`.
- Further development for that release (primarily fixes) then continues against `release`, while other development continues on `develop`. Effectively, `release` is forked from `develop`. Changes to `release` must be reverse merged to `develop`.
4. When the candidate has passed testing and is ready for release, the final release is merged to `master`.
5. If any issues are found post-release, a hotfix / point release may be created, which is merged to `master`, and then reverse merged to `develop`.
#### Betas, and the first release candidate
##### Preparing the `develop` branch
1. Optimally, the `develop` branch will be ready to go, with all
relevant PRs already merged.
1. Optimally, the `develop` branch will be ready to go, with all relevant PRs already merged.
2. If there are any PRs pending, merge them **BEFORE** preparing the beta.
1. If only one or two PRs need to be merged, merge those PRs [as
normal](#when-and-how-to-merge-pull-requests), updating the second
one, and waiting for CI to finish in between.
2. If there are several pending PRs, do not use the Github UI,
because the delays waiting for CI in between each merge will be
unnecessarily onerous. (Incidentally, this process can also be
used to merge if the Github UI has issues.) Merge each PR branch
directly to a `release-next` on your local machine and create a single
PR, then push your branch to `develop`.
1. Squash the changes from each PR, one commit each (unless more
are needed), being sure to sign each commit and update the
commit message to include the PR number. You may be able to use
a fast-forward merge for the first PR.
1. If only one or two PRs need to be merged, merge those PRs [as normal](#when-and-how-to-merge-pull-requests), updating the second one, and waiting for CI to finish in between.
2. If there are several pending PRs, do not use the Github UI, because the delays waiting for CI in between each merge will be unnecessarily onerous. (Incidentally, this process can also be used to merge if the Github UI has issues.) Merge each PR branch directly to a `release-next` on your local machine and create a single PR, then push your branch to `develop`.
1. Squash the changes from each PR, one commit each (unless more are needed), being sure to sign each commit and update the commit message to include the PR number. You may be able to use a fast-forward merge for the first PR.
2. Push your branch.
3. Continue to [Making the release](#making-the-release) to update
the version number, etc.
3. Continue to [Making the release](#making-the-release) to update the version number, etc.
The workflow may look something like:
@@ -691,17 +471,13 @@ git push --set-upstream origin
You can also use the [squash-branches] script.
You may also need to manually close the open PRs after the changes are
merged to `develop`. Be sure to include the commit ID.
You may also need to manually close the open PRs after the changes are merged to `develop`. Be sure to include the commit ID.
##### Making the release
This includes, betas, and the first release candidate (RC).
1. If you didn't create one [preparing the `develop`
branch](#preparing-the-develop-branch), Ensure there is no old
`release-next` branch hanging around. Then make a `release-next`
branch that only changes the version number. e.g.
1. If you didn't create one [preparing the `develop` branch](#preparing-the-develop-branch), Ensure there is no old `release-next` branch hanging around. Then make a `release-next` branch that only changes the version number. e.g.
```
git fetch upstreams
@@ -724,8 +500,7 @@ git fetch upstreams
git branch --set-upstream-to=upstream/release-next
```
You can also use the [update-version] script. 2. Create a Pull Request for `release-next` with **`develop`** as
the base branch.
You can also use the [update-version] script. 2. Create a Pull Request for `release-next` with **`develop`** as the base branch.
1. Use the title "[TRIVIAL] Set version to X.X.X-bX".
2. Instead of the default description template, use the following:
@@ -737,27 +512,17 @@ This PR only changes the version number. It will be merged as
soon as Github CI actions successfully complete.
```
3. Wait for CI to successfully complete, and get someone to approve
the PR. (It is safe to ignore known CI issues.)
4. Push the updated `develop` branch using your `release-next`
branch. **Do not use the Github UI. It's important to preserve
commit IDs.**
3. Wait for CI to successfully complete, and get someone to approve the PR. (It is safe to ignore known CI issues.)
4. Push the updated `develop` branch using your `release-next` branch. **Do not use the Github UI. It's important to preserve commit IDs.**
```
git push upstream-push release-next:develop
```
5. In the unlikely event that the push fails because someone has merged
something else in the meantime, rebase your branch onto the updated
`develop` branch, push again, and go back to step 3.
6. Ensure that your PR against `develop` is closed. Github should do it
automatically.
7. Once this is done, forward progress on `develop` can continue
(other PRs may be merged).
8. Now create a Pull Request for `release-next` with **`release`** as
the base branch. Instead of the default template, reuse and update
the message from the previous release. Include the following verbiage
somewhere in the description:
5. In the unlikely event that the push fails because someone has merged something else in the meantime, rebase your branch onto the updated `develop` branch, push again, and go back to step 3.
6. Ensure that your PR against `develop` is closed. Github should do it automatically.
7. Once this is done, forward progress on `develop` can continue (other PRs may be merged).
8. Now create a Pull Request for `release-next` with **`release`** as the base branch. Instead of the default template, reuse and update the message from the previous release. Include the following verbiage somewhere in the description:
```
The base branch is `release`. [All releases (including
@@ -766,12 +531,8 @@ go in `release`. This PR branch will be pushed directly to `release` (not
squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, simply abandon the
release. It's easy to start a new release, it should be easy to
abandon one. **DO NOT REUSE THE VERSION NUMBER.** e.g. If you
abandon 2.4.0-b1, the next attempt will be 2.4.0-b2.
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, simply abandon the release. It's easy to start a new release, it should be easy to abandon one. **DO NOT REUSE THE VERSION NUMBER.** e.g. If you abandon 2.4.0-b1, the next attempt will be 2.4.0-b2.
8. Once everything is ready to go, push to `release`.
```
@@ -808,44 +569,25 @@ git push upstream-push <version number>
git push --delete upstream-push release-next
```
11. Finally [create a new release on
Github](https://github.com/XRPLF/rippled/releases).
11. Finally [create a new release on Github](https://github.com/XRPLF/rippled/releases).
#### Release candidates after the first
Once the first release candidate is [merged into
release](#making-the-release), then `release` and `develop` _are allowed
to diverge_.
Once the first release candidate is [merged into release](#making-the-release), then `release` and `develop` _are allowed to diverge_.
If a bug or issue is discovered in a version that has a release
candidate being tested, any fix and new version will need to be applied
against `release`, then reverse-merged to `develop`. This helps keep git
history as linear as possible.
If a bug or issue is discovered in a version that has a release candidate being tested, any fix and new version will need to be applied against `release`, then reverse-merged to `develop`. This helps keep git history as linear as possible.
A `release-next` branch will be created from `release`, and any further
work for that release must be based on `release-next`. Specifically,
PRs must use `release-next` as the base, and those PRs will be merged
directly to `release-next` when approved. Changes should be restricted
to bug fixes, but other changes may be necessary from time to time.
A `release-next` branch will be created from `release`, and any further work for that release must be based on `release-next`. Specifically, PRs must use `release-next` as the base, and those PRs will be merged directly to `release-next` when approved. Changes should be restricted to bug fixes, but other changes may be necessary from time to time.
1. Open any PRs for the pending release using `release-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`release-next` can be thrown away and recreated if necessary.
2. Once a new release candidate is ready, create a version commit as in
step 1 [above](#making-the-release) on `release-next`. You can use
the [update-version] script for this, too.
3. Jump to step 8 ("Now create a Pull Request for `release-next` with
**`release`** as the base") from the process
[above](#making-the-release) to merge `release-next` into `release`.
1. Open any PRs for the pending release using `release-next` as the base, so they can be merged directly in to it. Unlike `develop`, though, `release-next` can be thrown away and recreated if necessary.
2. Once a new release candidate is ready, create a version commit as in step 1 [above](#making-the-release) on `release-next`. You can use the [update-version] script for this, too.
3. Jump to step 8 ("Now create a Pull Request for `release-next` with **`release`** as the base") from the process [above](#making-the-release) to merge `release-next` into `release`.
##### Follow up: reverse merge
Once the RC is merged and tagged, it needs to be reverse merged into
`develop` as soon as possible.
Once the RC is merged and tagged, it needs to be reverse merged into `develop` as soon as possible.
1. Create a branch, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNNrcN".
E.g. For release A.B.C-rcD, use `mergeABCrcD`.
1. Create a branch, based on `upstream/develop`. The branch name is not important, but could include "mergeNNNrcN". E.g. For release A.B.C-rcD, use `mergeABCrcD`.
```
git fetch upstreams
@@ -861,24 +603,15 @@ git checkout --no-track -b mergeABCrcD upstream/develop
git merge upstream/release
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo (or `upstream` if you have permission),
and open a normal PR against `develop`. The "High level overview" can
simply indicate that this is a merge of the RC. The "Context" should
summarize the changes from the RC. Include the following text
prominently:
3. `BuildInfo.cpp` will have a conflict with the version number. Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo (or `upstream` if you have permission), and open a normal PR against `develop`. The "High level overview" can simply indicate that this is a merge of the RC. The "Context" should summarize the changes from the RC. Include the following text prominently:
```
This PR must be merged manually using a push. Do not use the Github UI.
```
5. Depending on the complexity of the changes, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
5. Depending on the complexity of the changes, and/or merge conflicts, the PR may need a thorough review, or just a sign-off that the merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge `develop` back into your branch. Instead rebase preserving merges, or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
@@ -906,30 +639,14 @@ Development on `develop` can proceed as normal.
A final release is any release that is not a beta or RC, such as 2.2.0.
Only code that has already been tested and vetted across all three
platforms should be included in a final release. Most of the time, that
means that the commit immediately preceding the commit setting the
version number will be an RC. Occasionally, there may be last-minute bug
fixes included as well. If so, those bug fixes must have been tested
internally as if they were RCs (at minimum, ensuring unit tests pass,
and the app starts, syncs, and stops cleanly across all three
platforms.)
Only code that has already been tested and vetted across all three platforms should be included in a final release. Most of the time, that means that the commit immediately preceding the commit setting the version number will be an RC. Occasionally, there may be last-minute bug fixes included as well. If so, those bug fixes must have been tested internally as if they were RCs (at minimum, ensuring unit tests pass, and the app starts, syncs, and stops cleanly across all three platforms.)
_If in doubt, make an RC first._
The process for building a final release is very similar to [the process
for building a beta](#making-the-release), except the code will be
moving from `release` to `master` instead of from `develop` to
`release`, and both branches will be pushed at the same time.
The process for building a final release is very similar to [the process for building a beta](#making-the-release), except the code will be moving from `release` to `master` instead of from `develop` to `release`, and both branches will be pushed at the same time.
1. Ensure there is no old `master-next` branch hanging around.
Then make a `master-next` branch that only changes the version
number. As above, or using the
[update-version] script.
2. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
1. Ensure there is no old `master-next` branch hanging around. Then make a `master-next` branch that only changes the version number. As above, or using the [update-version] script.
2. Create a Pull Request for `master-next` with **`master`** as the base branch. Instead of the default template, reuse and update the message from the previous final release. Include the following verbiage somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
@@ -937,11 +654,8 @@ The base branch is `master`. This PR branch will be pushed directly to
GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, close the PR, delete
`master-next`, and move development back to `release`, [issuing
more RCs as necessary](#release-candidates-after-the-first)
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, close the PR, delete `master-next`, and move development back to `release`, [issuing more RCs as necessary](#release-candidates-after-the-first)
8. Once everything is ready to go, push to `release` and `master`.
```
@@ -980,31 +694,19 @@ git push upstream-push <version number>
git push --delete upstream-push master-next
```
11. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
11. [Create a new release on Github](https://github.com/XRPLF/rippled/releases). Be sure that "Set as the latest release" is checked.
12. Open a PR to update the [API-CHANGELOG](API-CHANGELOG.md) and `API-VERSION-[n].md` with the changes for this release (if any are missing).
13. Finally, [reverse merge the release into `develop`](#follow-up-reverse-merge).
#### Special cases: point releases, hotfixes, etc.
On occasion, a bug or issue is discovered in a version that already
had a final release. Most of the time, development will have started
on the next version, and will usually have changes in `develop`
and often in `release`.
On occasion, a bug or issue is discovered in a version that already had a final release. Most of the time, development will have started on the next version, and will usually have changes in `develop` and often in `release`.
Because git history is kept as linear as possible, any fix and new
version will need to be applied against `master`.
Because git history is kept as linear as possible, any fix and new version will need to be applied against `master`.
The process for building a hotfix release is very similar to [the
process for building release candidates after the
first](#release-candidates-after-the-first) and [for building a final
release](#final-releases), except the changes will be done against
`master` instead of `release`.
The process for building a hotfix release is very similar to [the process for building release candidates after the first](#release-candidates-after-the-first) and [for building a final release](#final-releases), except the changes will be done against `master` instead of `release`.
If there is only a single issue for the hotfix, the work can be done in
any branch. When it's ready to merge, jump to step 3 using your branch
instead of `master-next`.
If there is only a single issue for the hotfix, the work can be done in any branch. When it's ready to merge, jump to step 3 using your branch instead of `master-next`.
1. Create a `master-next` branch from `master`.
@@ -1014,27 +716,17 @@ git push upstream-push
git fetch upstreams
```
2. Open any PRs for the pending hotfix using `master-next` as the base,
so they can be merged directly in to it. Unlike `develop`, though,
`master-next` can be thrown away and recreated if necessary.
3. Once the hotfix is ready, create a version commit using the same
steps as above, or use the
[update-version] script.
4. Create a Pull Request for `master-next` with **`master`** as
the base branch. Instead of the default template, reuse and update
the message from the previous final release. Include the following verbiage
somewhere in the description:
2. Open any PRs for the pending hotfix using `master-next` as the base, so they can be merged directly in to it. Unlike `develop`, though, `master-next` can be thrown away and recreated if necessary.
3. Once the hotfix is ready, create a version commit using the same steps as above, or use the [update-version] script.
4. Create a Pull Request for `master-next` with **`master`** as the base branch. Instead of the default template, reuse and update the message from the previous final release. Include the following verbiage somewhere in the description:
```
The base branch is `master`. This PR branch will be pushed directly to
`master` (not squashed or rebased, and not using the GitHub UI).
```
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur
offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, update `master-next` as
needed, but ensure that the changes are properly squashed, and the
version setting commit remains last
7. Sign-offs for the three platforms (Linux, Mac, Windows) usually occur offline, but at least one approval will be needed on the PR.
- If issues are discovered during testing, update `master-next` as needed, but ensure that the changes are properly squashed, and the version setting commit remains last
8. Once everything is ready to go, push to `master` **only**.
```
@@ -1071,17 +763,11 @@ git push upstream-push <version number>
git push --delete upstream-push master-next
```
10. [Create a new release on
Github](https://github.com/XRPLF/rippled/releases). Be sure that
"Set as the latest release" is checked.
10. [Create a new release on Github](https://github.com/XRPLF/rippled/releases). Be sure that "Set as the latest release" is checked.
Once the hotfix is released, it needs to be reverse merged into
`develop` as soon as possible. It may also need to be merged into
`release` if a release candidate is under development.
Once the hotfix is released, it needs to be reverse merged into `develop` as soon as possible. It may also need to be merged into `release` if a release candidate is under development.
1. Create a branch in your own repo, based on `upstream/develop`.
The branch name is not important, but could include "mergeNNN".
E.g. For release 2.2.3, use `merge223`.
1. Create a branch in your own repo, based on `upstream/develop`. The branch name is not important, but could include "mergeNNN". E.g. For release 2.2.3, use `merge223`.
```
git fetch upstreams
@@ -1097,24 +783,15 @@ git checkout --no-track -b merge223 upstream/develop
git merge upstream/master
```
3. `BuildInfo.cpp` will have a conflict with the version number.
Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo, and open a normal PR against
`develop`. The "High level overview" can simply indicate that this
is a merge of the hotfix version. The "Context" should summarize
the changes from the hotfix. Include the following text
prominently:
3. `BuildInfo.cpp` will have a conflict with the version number. Resolve it with the version from `develop` - the higher version.
4. Push your branch to your repo, and open a normal PR against `develop`. The "High level overview" can simply indicate that this is a merge of the hotfix version. The "Context" should summarize the changes from the hotfix. Include the following text prominently:
```
This PR must be merged manually using a --ff-only merge. Do not use the Github UI.
```
5. Depending on the complexity of the hotfix, and/or merge conflicts,
the PR may need a thorough review, or just a sign-off that the
merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge
`develop` back into your branch. Instead rebase preserving merges,
or do the merge again. (See also the `rerere` git config setting.)
5. Depending on the complexity of the hotfix, and/or merge conflicts, the PR may need a thorough review, or just a sign-off that the merge was done correctly.
6. If `develop` is updated before this PR is merged, do not merge `develop` back into your branch. Instead rebase preserving merges, or do the merge again. (See also the `rerere` git config setting.)
```
git rebase --rebase-merges upstream/develop
@@ -1134,22 +811,13 @@ git log --show-signature "upstream/develop..HEAD"
git push upstream-push HEAD:develop
```
Development on `develop` can proceed as normal. It is recommended to
create a beta (or RC) immediately to ensure that everything worked as
expected.
Development on `develop` can proceed as normal. It is recommended to create a beta (or RC) immediately to ensure that everything worked as expected.
##### An even rarer scenario: A hotfix on an old release
Historically, once a final release is tagged and packages are released,
versions older than the latest final release are no longer supported.
However, there is a possibility that a very high severity bug may occur
in a non-amendment blocked version that is still being run by
a significant fraction of users, which would necessitate a hotfix / point
release to that version as well as any later versions.
Historically, once a final release is tagged and packages are released, versions older than the latest final release are no longer supported. However, there is a possibility that a very high severity bug may occur in a non-amendment blocked version that is still being run by a significant fraction of users, which would necessitate a hotfix / point release to that version as well as any later versions.
This scenario would follow the same basic procedure as above,
except that _none_ of `develop`, `release`, or `master`
would be touched during the release process.
This scenario would follow the same basic procedure as above, except that _none_ of `develop`, `release`, or `master` would be touched during the release process.
In this example, consider if version 2.1.1 needed to be patched.
@@ -1169,20 +837,9 @@ git push upstream-push
git fetch upstreams
```
2. Work continues as above, except using `master-2.1.2`as
the base branch for any merging, packaging, etc.
3. After the release is tagged and packages are built, you could
potentially delete both branches, e.g. `master-2.1.2` and
`master212-next`. However, it may be useful to keep `master-2.1.2`
around indefinitely for reference.
4. Assuming that a hotfix is also released for the latest
version in parallel with this one, or if the issue is
already fixed in the latest version, do no do any
reverse merges. However, if it is not, it probably makes
sense to reverse merge `master-2.1.2` into `master`,
release a hotfix for _that_ version, then reverse merge
from `master` to `develop`. (Please don't do this unless absolutely
necessary.)
2. Work continues as above, except using `master-2.1.2`as the base branch for any merging, packaging, etc.
3. After the release is tagged and packages are built, you could potentially delete both branches, e.g. `master-2.1.2` and `master212-next`. However, it may be useful to keep `master-2.1.2` around indefinitely for reference.
4. Assuming that a hotfix is also released for the latest version in parallel with this one, or if the issue is already fixed in the latest version, do no do any reverse merges. However, if it is not, it probably makes sense to reverse merge `master-2.1.2` into `master`, release a hotfix for _that_ version, then reverse merge from `master` to `develop`. (Please don't do this unless absolutely necessary.)
[contrib]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
[squash]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits

View File

@@ -1,16 +1,7 @@
ISC License
Copyright (c) 2011, Arthur Britto, David Schwartz, Jed McCaleb, Vinnie Falco, Bob Way, Eric Lombrozo, Nikolaos D. Bougalis, Howard Hinnant.
Copyright (c) 2012-present, the XRP Ledger developers.
Copyright (c) 2011, Arthur Britto, David Schwartz, Jed McCaleb, Vinnie Falco, Bob Way, Eric Lombrozo, Nikolaos D. Bougalis, Howard Hinnant. Copyright (c) 2012-present, the XRP Ledger developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -56,8 +56,7 @@ Here are some good places to start learning the source code:
| `./src` | Source code. |
| `./crates` | Rust source code. |
Some of the directories under `src` are external repositories included using
git-subtree. See those directories' README files for more details.
Some of the directories under `src` are external repositories included using git-subtree. See those directories' README files for more details.
## Additional Documentation

View File

@@ -2,11 +2,8 @@
To achieve reproducible dependencies, we use a [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html).
The `conan.lock` file in the repository contains a "snapshot" of the current
dependencies. It is implicitly used when running `conan` commands, so you don't
need to specify it.
The `conan.lock` file in the repository contains a "snapshot" of the current dependencies. It is implicitly used when running `conan` commands, so you don't need to specify it.
You have to update this file every time you add a new dependency or change a
revision or version of an existing dependency.
You have to update this file every time you add a new dependency or change a revision or version of an existing dependency.
To update a lockfile, run from the repository root: `./conan/lockfile/regenerate.sh`

View File

@@ -2,61 +2,21 @@
## The Problem Statement
The moment-to-moment health of the XRP Ledger network depends on the health and
connectivity of a small number of computers (nodes). The most important nodes
are validators, specifically ones listed on the unique node list
([UNL](#Question-What-are-UNLs)). Ripple publishes a recommended UNL that most
network nodes use to determine which peers in the network are trusted. Although
most validators use the same list, they are not required to. The XRP Ledger
network progresses to the next ledger when enough validators reach agreement
(above the minimum quorum of 80%) about what transactions to include in the next
ledger.
The moment-to-moment health of the XRP Ledger network depends on the health and connectivity of a small number of computers (nodes). The most important nodes are validators, specifically ones listed on the unique node list ([UNL](#Question-What-are-UNLs)). Ripple publishes a recommended UNL that most network nodes use to determine which peers in the network are trusted. Although most validators use the same list, they are not required to. The XRP Ledger network progresses to the next ledger when enough validators reach agreement (above the minimum quorum of 80%) about what transactions to include in the next ledger.
As an example, if there are 10 validators on the UNL, at least 8 validators have
to agree with the latest ledger for it to become validated. But what if enough
of those validators are offline to drop the network below the 80% quorum? The
XRP Ledger network favors safety/correctness over advancing the ledger. Which
means if enough validators are offline, the network will not be able to validate
ledgers.
As an example, if there are 10 validators on the UNL, at least 8 validators have to agree with the latest ledger for it to become validated. But what if enough of those validators are offline to drop the network below the 80% quorum? The XRP Ledger network favors safety/correctness over advancing the ledger. Which means if enough validators are offline, the network will not be able to validate ledgers.
Unfortunately validators can go offline at any time for many different reasons.
Power outages, network connectivity issues, and hardware failures are just a few
scenarios where a validator would appear "offline". Given that most of these
events are temporary, it would make sense to temporarily remove that validator
from the UNL. But the UNL is updated infrequently and not every node uses the
same UNL. So instead of removing the unreliable validator from the Ripple
recommended UNL, we can create a second negative UNL which is stored directly on
the ledger (so the entire network has the same view). This will help the network
see which validators are **currently** unreliable, and adjust their quorum
calculation accordingly.
Unfortunately validators can go offline at any time for many different reasons. Power outages, network connectivity issues, and hardware failures are just a few scenarios where a validator would appear "offline". Given that most of these events are temporary, it would make sense to temporarily remove that validator from the UNL. But the UNL is updated infrequently and not every node uses the same UNL. So instead of removing the unreliable validator from the Ripple recommended UNL, we can create a second negative UNL which is stored directly on the ledger (so the entire network has the same view). This will help the network see which validators are **currently** unreliable, and adjust their quorum calculation accordingly.
_Improving the liveness of the network is the main motivation for the negative UNL._
### Targeted Faults
In order to determine which validators are unreliable, we need clearly define
what kind of faults to measure and analyze. We want to deal with the faults we
frequently observe in the production network. Hence we will only monitor for
validators that do not reliably respond to network messages or send out
validations disagreeing with the locally generated validations. We will not
target other byzantine faults.
In order to determine which validators are unreliable, we need clearly define what kind of faults to measure and analyze. We want to deal with the faults we frequently observe in the production network. Hence we will only monitor for validators that do not reliably respond to network messages or send out validations disagreeing with the locally generated validations. We will not target other byzantine faults.
To track whether or not a validator is responding to the network, we could
monitor them with a “heartbeat” protocol. Instead of creating a new heartbeat
protocol, we can leverage some existing protocol messages to mimic the
heartbeat. We picked validation messages because validators should send one and
only one validation message per ledger. In addition, we only count the
validation messages that agree with the local node's validations.
To track whether or not a validator is responding to the network, we could monitor them with a “heartbeat” protocol. Instead of creating a new heartbeat protocol, we can leverage some existing protocol messages to mimic the heartbeat. We picked validation messages because validators should send one and only one validation message per ledger. In addition, we only count the validation messages that agree with the local node's validations.
With the negative UNL, the network could keep making forward progress safely
even if the number of remaining validators gets to 60%. Say we have a network
with 10 validators on the UNL and everything is operating correctly. The quorum
required for this network would be 8 (80% of 10). When validators fail, the
quorum required would be as low as 6 (60% of 10), which is the absolute
**_minimum quorum_**. We need the absolute minimum quorum to be strictly greater
than 50% of the original UNL so that there cannot be two partitions of
well-behaved nodes headed in different directions. We arbitrarily choose 60% as
the minimum quorum to give a margin of safety.
With the negative UNL, the network could keep making forward progress safely even if the number of remaining validators gets to 60%. Say we have a network with 10 validators on the UNL and everything is operating correctly. The quorum required for this network would be 8 (80% of 10). When validators fail, the quorum required would be as low as 6 (60% of 10), which is the absolute **_minimum quorum_**. We need the absolute minimum quorum to be strictly greater than 50% of the original UNL so that there cannot be two partitions of well-behaved nodes headed in different directions. We arbitrarily choose 60% as the minimum quorum to give a margin of safety.
Consider these events in the absence of negative UNL:
@@ -65,10 +25,7 @@ Consider these events in the absence of negative UNL:
1. 5:00pm - validator3 fails, votes vs. quorum: 7 < 8, we don’t have quorum
- **network cannot validate new ledgers with 3 failed validators**
We're below 80% agreement, so new ledgers cannot be validated. This is how the
XRP Ledger operates today, but if the negative UNL was enabled, the events would
happen as follows. (Please note that the events below are from a simplified
version of our protocol.)
We're below 80% agreement, so new ledgers cannot be validated. This is how the XRP Ledger operates today, but if the negative UNL was enabled, the events would happen as follows. (Please note that the events below are from a simplified version of our protocol.)
1. 1:00pm - validator1 fails, votes vs. quorum: 9 >= 8, we have quorum
1. 1:40pm - network adds validator1 to negative UNL, quorum changes to ceil(9 \* 0.8), or 8
@@ -88,8 +45,7 @@ This proposal will:
1. add a new pseudo-transaction type
1. add the negative UNL to the ledger data structure.
Any tools or systems that rely on the format of this data will have to be
updated.
Any tools or systems that rely on the format of this data will have to be updated.
### Amendment
@@ -105,122 +61,70 @@ This section discusses the following topics about the Negative UNL design:
- [Negative UNL maintenance](#Negative-UNL-Maintenance)
- [Quorum size calculation](#Quorum-Size-Calculation)
- [Filter validation messages](#Filter-Validation-Messages)
- [High level sequence diagram of code
changes](#High-Level-Sequence-Diagram-of-Code-Changes)
- [High level sequence diagram of code changes](#High-Level-Sequence-Diagram-of-Code-Changes)
### Negative UNL Protocol Overview
Every ledger stores a list of zero or more unreliable validators. Updates to the
list must be approved by the validators using the consensus mechanism that
validators use to agree on the set of transactions. The list is used only when
checking if a ledger is fully validated. If a validator V is in the list, nodes
with V in their UNL adjust the quorum and V’s validation message is not counted
when verifying if a ledger is fully validated. V’s flow of messages and network
interactions, however, will remain the same.
Every ledger stores a list of zero or more unreliable validators. Updates to the list must be approved by the validators using the consensus mechanism that validators use to agree on the set of transactions. The list is used only when checking if a ledger is fully validated. If a validator V is in the list, nodes with V in their UNL adjust the quorum and V’s validation message is not counted when verifying if a ledger is fully validated. V’s flow of messages and network interactions, however, will remain the same.
We define the **\*effective UNL** = original UNL - negative UNL\*, and the
**_effective quorum_** as the quorum of the _effective UNL_. And we set
_effective quorum = Ceiling(80% _ effective UNL)\*.
We define the **\*effective UNL** = original UNL - negative UNL\*, and the **_effective quorum_** as the quorum of the _effective UNL_. And we set _effective quorum = Ceiling(80% _ effective UNL)\*.
### Validator Reliability Measurement
A node only measures the reliability of validators on its own UNL, and only
proposes based on local observations. There are many metrics that a node can
measure about its validators, but we have chosen ledger validation messages.
This is because every validator shall send one and only one signed validation
message per ledger. This keeps the measurement simple and removes
timing/clock-sync issues. A node will measure the percentage of agreeing
validation messages (_PAV_) received from each validator on the node's UNL. Note
that the node will only count the validation messages that agree with its own
validations.
A node only measures the reliability of validators on its own UNL, and only proposes based on local observations. There are many metrics that a node can measure about its validators, but we have chosen ledger validation messages. This is because every validator shall send one and only one signed validation message per ledger. This keeps the measurement simple and removes timing/clock-sync issues. A node will measure the percentage of agreeing validation messages (_PAV_) received from each validator on the node's UNL. Note that the node will only count the validation messages that agree with its own validations.
We define the **PAV** as the Percentage of Agreed Validation
messages received for the last N ledgers, where N = 256 by default.
We define the **PAV** as the Percentage of Agreed Validation messages received for the last N ledgers, where N = 256 by default.
When the PAV drops below the **_low-water mark_**, the validator is considered
unreliable, and is a candidate to be disabled by being added to the negative
UNL. A validator must have a PAV higher than the **_high-water mark_** to be
re-enabled. The validator is re-enabled by removing it from the negative UNL. In
the implementation, we plan to set the low-water mark as 50% and the high-water
mark as 80%.
When the PAV drops below the **_low-water mark_**, the validator is considered unreliable, and is a candidate to be disabled by being added to the negative UNL. A validator must have a PAV higher than the **_high-water mark_** to be re-enabled. The validator is re-enabled by removing it from the negative UNL. In the implementation, we plan to set the low-water mark as 50% and the high-water mark as 80%.
### Format Changes
The negative UNL component in a ledger contains three fields.
- **_NegativeUNL_**: The current negative UNL, a list of unreliable validators.
- **_ToDisable_**: The validator to be added to the negative UNL on the next
flag ledger.
- **_ToReEnable_**: The validator to be removed from the negative UNL on the
next flag ledger.
- **_ToDisable_**: The validator to be added to the negative UNL on the next flag ledger.
- **_ToReEnable_**: The validator to be removed from the negative UNL on the next flag ledger.
All three fields are optional. When the _ToReEnable_ field exists, the
_NegativeUNL_ field cannot be empty.
All three fields are optional. When the _ToReEnable_ field exists, the _NegativeUNL_ field cannot be empty.
A new pseudo-transaction, **_UNLModify_**, is added. It has three fields
- **_Disabling_**: A flag indicating whether the modification is to disable or
to re-enable a validator.
- **_Disabling_**: A flag indicating whether the modification is to disable or to re-enable a validator.
- **_Seq_**: The ledger sequence number.
- **_Validator_**: The validator to be disabled or re-enabled.
There would be at most one _disable_ `UNLModify` and one _re-enable_ `UNLModify`
transaction per flag ledger. The full machinery is described further on.
There would be at most one _disable_ `UNLModify` and one _re-enable_ `UNLModify` transaction per flag ledger. The full machinery is described further on.
### Negative UNL Maintenance
The negative UNL can only be modified on the flag ledgers. If a validator's
reliability status changes, it takes two flag ledgers to modify the negative
UNL. Let's see an example of the algorithm:
The negative UNL can only be modified on the flag ledgers. If a validator's reliability status changes, it takes two flag ledgers to modify the negative UNL. Let's see an example of the algorithm:
- Ledger seq = 100: A validator V goes offline.
- Ledger seq = 256: This is a flag ledger, and V's reliability measurement _PAV_
is lower than the low-water mark. Other validators add `UNLModify`
pseudo-transactions `{true, 256, V}` to the transaction set which goes through
the consensus. Then the pseudo-transaction is applied to the negative UNL
ledger component by setting `ToDisable = V`.
- Ledger seq = 257 ~ 511: The negative UNL ledger component is copied from the
parent ledger.
- Ledger seq=512: This is a flag ledger, and the negative UNL is updated
`NegativeUNL = NegativeUNL + ToDisable`.
- Ledger seq = 256: This is a flag ledger, and V's reliability measurement _PAV_ is lower than the low-water mark. Other validators add `UNLModify` pseudo-transactions `{true, 256, V}` to the transaction set which goes through the consensus. Then the pseudo-transaction is applied to the negative UNL ledger component by setting `ToDisable = V`.
- Ledger seq = 257 ~ 511: The negative UNL ledger component is copied from the parent ledger.
- Ledger seq=512: This is a flag ledger, and the negative UNL is updated `NegativeUNL = NegativeUNL + ToDisable`.
The negative UNL may have up to `MaxNegativeListed = floor(original UNL * 25%)`
validators. The 25% is because of 75% \* 80% = 60%, where 75% = 100% - 25%, 80%
is the quorum of the effective UNL, and 60% is the absolute minimum quorum of
the original UNL. Adding more than 25% validators to the negative UNL does not
improve the liveness of the network, because adding more validators to the
negative UNL cannot lower the effective quorum.
The negative UNL may have up to `MaxNegativeListed = floor(original UNL * 25%)` validators. The 25% is because of 75% \* 80% = 60%, where 75% = 100% - 25%, 80% is the quorum of the effective UNL, and 60% is the absolute minimum quorum of the original UNL. Adding more than 25% validators to the negative UNL does not improve the liveness of the network, because adding more validators to the negative UNL cannot lower the effective quorum.
The following is the detailed algorithm:
- **If** the ledger seq = x is a flag ledger
1. Compute `NegativeUNL = NegativeUNL + ToDisable - ToReEnable` if they
exist in the parent ledger
1. Compute `NegativeUNL = NegativeUNL + ToDisable - ToReEnable` if they exist in the parent ledger
1. Try to find a candidate to disable if `sizeof NegativeUNL < MaxNegativeListed`
1. Find a validator V that has a _PAV_ lower than the low-water
mark, but is not in `NegativeUNL`.
1. Find a validator V that has a _PAV_ lower than the low-water mark, but is not in `NegativeUNL`.
1. If two or more are found, their public keys are XORed with the hash
of the parent ledger and the one with the lowest XOR result is chosen.
1. If V is found, create a `UNLModify` pseudo-transaction
`TxDisableValidator = {true, x, V}`
1. If two or more are found, their public keys are XORed with the hash of the parent ledger and the one with the lowest XOR result is chosen.
1. If V is found, create a `UNLModify` pseudo-transaction `TxDisableValidator = {true, x, V}`
1. Try to find a candidate to re-enable if `sizeof NegativeUNL > 0`:
1. Find a validator U that is in `NegativeUNL` and has a _PAV_ higher
than the high-water mark.
1. If U is not found, try to find one in `NegativeUNL` but not in the
local _UNL_.
1. If two or more are found, their public keys are XORed with the hash
of the parent ledger and the one with the lowest XOR result is chosen.
1. If U is found, create a `UNLModify` pseudo-transaction
`TxReEnableValidator = {false, x, U}`
1. Find a validator U that is in `NegativeUNL` and has a _PAV_ higher than the high-water mark.
1. If U is not found, try to find one in `NegativeUNL` but not in the local _UNL_.
1. If two or more are found, their public keys are XORed with the hash of the parent ledger and the one with the lowest XOR result is chosen.
1. If U is found, create a `UNLModify` pseudo-transaction `TxReEnableValidator = {false, x, U}`
1. If any `UNLModify` pseudo-transactions are created, add them to the
transaction set. The transaction set goes through the consensus algorithm.
1. If have enough support, the `UNLModify` pseudo-transactions remain in the
transaction set agreed by the validators. Then the pseudo-transactions are
applied to the ledger:
1. If any `UNLModify` pseudo-transactions are created, add them to the transaction set. The transaction set goes through the consensus algorithm.
1. If have enough support, the `UNLModify` pseudo-transactions remain in the transaction set agreed by the validators. Then the pseudo-transactions are applied to the ledger:
1. If have `TxDisableValidator`, set `ToDisable=TxDisableValidator.V`.
Else clear `ToDisable`.
@@ -231,61 +135,33 @@ The following is the detailed algorithm:
- **Else** (not a flag ledger)
1. Copy the negative UNL ledger component from the parent ledger
The negative UNL is stored on each ledger because we don't know when a validator
may reconnect to the network. If the negative UNL was stored only on every flag
ledger, then a new validator would have to wait until it acquires the latest
flag ledger to know the negative UNL. So any new ledgers created that are not
flag ledgers copy the negative UNL from the parent ledger.
The negative UNL is stored on each ledger because we don't know when a validator may reconnect to the network. If the negative UNL was stored only on every flag ledger, then a new validator would have to wait until it acquires the latest flag ledger to know the negative UNL. So any new ledgers created that are not flag ledgers copy the negative UNL from the parent ledger.
Note that when we have a validator to disable and a validator to re-enable at
the same flag ledger, we create two separate `UNLModify` pseudo-transactions. We
want either one or the other or both to make it into the ledger on their own
merits.
Note that when we have a validator to disable and a validator to re-enable at the same flag ledger, we create two separate `UNLModify` pseudo-transactions. We want either one or the other or both to make it into the ledger on their own merits.
Readers may have noticed that we defined several rules of creating the
`UNLModify` pseudo-transactions but did not describe how to enforce the rules.
The rules are actually enforced by the existing consensus algorithm. Unless
enough validators propose the same pseudo-transaction it will not be included in
the transaction set of the ledger.
Readers may have noticed that we defined several rules of creating the `UNLModify` pseudo-transactions but did not describe how to enforce the rules. The rules are actually enforced by the existing consensus algorithm. Unless enough validators propose the same pseudo-transaction it will not be included in the transaction set of the ledger.
### Quorum Size Calculation
The effective quorum is 80% of the effective UNL. Note that because at most 25%
of the original UNL can be on the negative UNL, the quorum should not be lower
than the absolute minimum quorum (i.e. 60%) of the original UNL. However,
considering that different nodes may have different UNLs, to be safe we compute
`quorum = Ceiling(max(60% * original UNL, 80% * effective UNL))`.
The effective quorum is 80% of the effective UNL. Note that because at most 25% of the original UNL can be on the negative UNL, the quorum should not be lower than the absolute minimum quorum (i.e. 60%) of the original UNL. However, considering that different nodes may have different UNLs, to be safe we compute `quorum = Ceiling(max(60% * original UNL, 80% * effective UNL))`.
### Filter Validation Messages
If a validator V is in the negative UNL, it still participates in consensus
sessions in the same way, i.e. V still follows the protocol and publishes
proposal and validation messages. The messages from V are still stored the same
way by everyone, used to calculate the new PAV for V, and could be used in
future consensus sessions if needed. However V's ledger validation message is
not counted when checking if the ledger is fully validated.
If a validator V is in the negative UNL, it still participates in consensus sessions in the same way, i.e. V still follows the protocol and publishes proposal and validation messages. The messages from V are still stored the same way by everyone, used to calculate the new PAV for V, and could be used in future consensus sessions if needed. However V's ledger validation message is not counted when checking if the ledger is fully validated.
### High Level Sequence Diagram of Code Changes
The diagram below is the sequence of one round of consensus. Classes and
components with non-trivial changes are colored green.
The diagram below is the sequence of one round of consensus. Classes and components with non-trivial changes are colored green.
- The `ValidatorList` class is modified to compute the quorum of the effective
UNL.
- The `ValidatorList` class is modified to compute the quorum of the effective UNL.
- The `Validations` class provides an interface for querying the validation
messages from trusted validators.
- The `Validations` class provides an interface for querying the validation messages from trusted validators.
- The `ConsensusAdaptor` component:
- The `RCLConsensus::Adaptor` class is modified for creating `UNLModify`
Pseudo-Transactions.
- The `Change` class is modified for applying `UNLModify`
Pseudo-Transactions.
- The `Ledger` class is modified for creating and adjusting the negative UNL
ledger component.
- The `LedgerMaster` class is modified for filtering out validation messages
from negative UNL validators when verifying if a ledger is fully
validated.
- The `RCLConsensus::Adaptor` class is modified for creating `UNLModify` Pseudo-Transactions.
- The `Change` class is modified for applying `UNLModify` Pseudo-Transactions.
- The `Ledger` class is modified for creating and adjusting the negative UNL ledger component.
- The `LedgerMaster` class is modified for filtering out validation messages from negative UNL validators when verifying if a ledger is fully validated.
![Sequence diagram](./negativeUNL_highLevel_sequence.png?raw=true "Negative UNL
Changes")
@@ -294,63 +170,29 @@ Changes")
### Use a Mechanism Like Fee Voting to Process UNLModify Pseudo-Transactions
The previous version of the negative UNL specification used the same mechanism
as the [fee voting](https://xrpl.org/fee-voting.html#voting-process.) for
creating the negative UNL, and used the negative UNL as soon as the ledger was
fully validated. However the timing of fully validation can differ among nodes,
so different negative UNLs could be used, resulting in different effective UNLs
and different quorums for the same ledger. As a result, the network's safety is
impacted.
The previous version of the negative UNL specification used the same mechanism as the [fee voting](https://xrpl.org/fee-voting.html#voting-process.) for creating the negative UNL, and used the negative UNL as soon as the ledger was fully validated. However the timing of fully validation can differ among nodes, so different negative UNLs could be used, resulting in different effective UNLs and different quorums for the same ledger. As a result, the network's safety is impacted.
This updated version does not impact safety though operates a bit more slowly.
The negative UNL modifications in the _UNLModify_ pseudo-transaction approved by
the consensus will take effect at the next flag ledger. The extra time of the
256 ledgers should be enough for nodes to be in sync of the negative UNL
modifications.
This updated version does not impact safety though operates a bit more slowly. The negative UNL modifications in the _UNLModify_ pseudo-transaction approved by the consensus will take effect at the next flag ledger. The extra time of the 256 ledgers should be enough for nodes to be in sync of the negative UNL modifications.
### Use an Expiration Approach to Re-enable Validators
After a validator disabled by the negative UNL becomes reliable, other
validators explicitly vote for re-enabling it. An alternative approach to
re-enable a validator is the expiration approach, which was considered in the
previous version of the specification. In the expiration approach, every entry
in the negative UNL has a fixed expiration time. One flag ledger interval was
chosen as the expiration interval. Once expired, the other validators must
continue voting to keep the unreliable validator on the negative UNL. The
advantage of this approach is its simplicity. But it has a requirement. The
negative UNL protocol must be able to vote multiple unreliable validators to be
disabled at the same flag ledger. In this version of the specification, however,
only one unreliable validator can be disabled at a flag ledger. So the
expiration approach cannot be simply applied.
After a validator disabled by the negative UNL becomes reliable, other validators explicitly vote for re-enabling it. An alternative approach to re-enable a validator is the expiration approach, which was considered in the previous version of the specification. In the expiration approach, every entry in the negative UNL has a fixed expiration time. One flag ledger interval was chosen as the expiration interval. Once expired, the other validators must continue voting to keep the unreliable validator on the negative UNL. The advantage of this approach is its simplicity. But it has a requirement. The negative UNL protocol must be able to vote multiple unreliable validators to be disabled at the same flag ledger. In this version of the specification, however, only one unreliable validator can be disabled at a flag ledger. So the expiration approach cannot be simply applied.
### Validator Reliability Measurement and Flag Ledger Frequency
If the ledger time is about 4.5 seconds and the low-water mark is 50%, then in
the worst case, it takes 48 minutes _((0.5 _ 256 + 256 + 256) _ 4.5 / 60 = 48)_
to put an offline validator on the negative UNL. We considered lowering the flag
ledger frequency so that the negative UNL can be more responsive. We also
considered decoupling the reliability measurement and flag ledger frequency to
be more flexible. In practice, however, their benefits are not clear.
If the ledger time is about 4.5 seconds and the low-water mark is 50%, then in the worst case, it takes 48 minutes _((0.5 _ 256 + 256 + 256) _ 4.5 / 60 = 48)_ to put an offline validator on the negative UNL. We considered lowering the flag ledger frequency so that the negative UNL can be more responsive. We also considered decoupling the reliability measurement and flag ledger frequency to be more flexible. In practice, however, their benefits are not clear.
## New Attack Vectors
A group of malicious validators may try to frame a reliable validator and put it
on the negative UNL. But they cannot succeed. Because:
A group of malicious validators may try to frame a reliable validator and put it on the negative UNL. But they cannot succeed. Because:
1. A reliable validator sends a signed validation message every ledger. A
sufficient peer-to-peer network will propagate the validation messages to other
validators. The validators will decide if another validator is reliable or not
only by its local observation of the validation messages received. So an honest
validator’s vote on another validator’s reliability is accurate.
1. A reliable validator sends a signed validation message every ledger. A sufficient peer-to-peer network will propagate the validation messages to other validators. The validators will decide if another validator is reliable or not only by its local observation of the validation messages received. So an honest validator’s vote on another validator’s reliability is accurate.
1. Given the votes are accurate, and one vote per validator, an honest validator
will not create a UNLModify transaction of a reliable validator.
1. Given the votes are accurate, and one vote per validator, an honest validator will not create a UNLModify transaction of a reliable validator.
1. A validator can be added to a negative UNL only through a UNLModify
transaction.
1. A validator can be added to a negative UNL only through a UNLModify transaction.
Assuming the group of malicious validators is less than the quorum, they cannot
frame a reliable validator.
Assuming the group of malicious validators is less than the quorum, they cannot frame a reliable validator.
## Summary
@@ -358,16 +200,13 @@ The bullet points below briefly summarize the current proposal:
- The motivation of the negative UNL is to improve the liveness of the network.
- The targeted faults are the ones frequently observed in the production
network.
- The targeted faults are the ones frequently observed in the production network.
- Validators propose negative UNL candidates based on their local measurements.
- The absolute minimum quorum is 60% of the original UNL.
- The format of the ledger is changed, and a new _UNLModify_ pseudo-transaction
is added. Any tools or systems that rely on the format of these data will have
to be updated.
- The format of the ledger is changed, and a new _UNLModify_ pseudo-transaction is added. Any tools or systems that rely on the format of these data will have to be updated.
- The negative UNL can only be modified on the flag ledgers.
@@ -375,59 +214,39 @@ The bullet points below briefly summarize the current proposal:
- At most one validator can be removed from the negative UNL at a flag ledger.
- If a validator's reliability status changes, it takes two flag ledgers to
modify the negative UNL.
- If a validator's reliability status changes, it takes two flag ledgers to modify the negative UNL.
- The quorum is the larger of 80% of the effective UNL and 60% of the original
UNL.
- The quorum is the larger of 80% of the effective UNL and 60% of the original UNL.
- If a validator is on the negative UNL, its validation messages are ignored
when the local node verifies if a ledger is fully validated.
- If a validator is on the negative UNL, its validation messages are ignored when the local node verifies if a ledger is fully validated.
## FAQ
### Question: What are UNLs?
Quote from the [Technical FAQ](https://xrpl.org/technical-faq.html): "They are
the lists of transaction validators a given participant believes will not
conspire to defraud them."
Quote from the [Technical FAQ](https://xrpl.org/technical-faq.html): "They are the lists of transaction validators a given participant believes will not conspire to defraud them."
### Question: How does the negative UNL proposal affect network liveness?
The network can make forward progress when more than a quorum of the trusted
validators agree with the progress. The lower the quorum size is, the easier for
the network to progress. If the quorum is too low, however, the network is not
safe because nodes may have different results. So the quorum size used in the
consensus protocol is a balance between the safety and the liveness of the
network. The negative UNL reduces the size of the effective UNL, resulting in a
lower quorum size while keeping the network safe.
The network can make forward progress when more than a quorum of the trusted validators agree with the progress. The lower the quorum size is, the easier for the network to progress. If the quorum is too low, however, the network is not safe because nodes may have different results. So the quorum size used in the consensus protocol is a balance between the safety and the liveness of the network. The negative UNL reduces the size of the effective UNL, resulting in a lower quorum size while keeping the network safe.
<h3> Question: How does a validator get into the negative UNL? How is a
validator removed from the negative UNL? </h3>
A validator’s reliability is measured by other validators. If a validator
becomes unreliable, at a flag ledger, other validators propose _UNLModify_
pseudo-transactions which vote the validator to add to the negative UNL during
the consensus session. If agreed, the validator is added to the negative UNL at
the next flag ledger. The mechanism of removing a validator from the negative
UNL is the same.
A validator’s reliability is measured by other validators. If a validator becomes unreliable, at a flag ledger, other validators propose _UNLModify_ pseudo-transactions which vote the validator to add to the negative UNL during the consensus session. If agreed, the validator is added to the negative UNL at the next flag ledger. The mechanism of removing a validator from the negative UNL is the same.
### Question: Given a negative UNL, what happens if the UNL changes?
Answer: Let’s consider the cases:
1. A validator is added to the UNL, and it is already in the negative UNL. This
case could happen when not all the nodes have the same UNL. Note that the
negative UNL on the ledger lists unreliable nodes that are not necessarily the
validators for everyone.
1. A validator is added to the UNL, and it is already in the negative UNL. This case could happen when not all the nodes have the same UNL. Note that the negative UNL on the ledger lists unreliable nodes that are not necessarily the validators for everyone.
In this case, the liveness is affected negatively. Because the minimum
quorum could be larger but the usable validators are not increased.
1. A validator is removed from the UNL, and it is in the negative UNL.
In this case, the liveness is affected positively. Because the quorum could
be smaller but the usable validators are not reduced.
In this case, the liveness is affected positively. Because the quorum could be smaller but the usable validators are not reduced.
1. A validator is added to the UNL, and it is not in the negative UNL.
1. A validator is removed from the UNL, and it is not in the negative UNL.
@@ -438,59 +257,23 @@ Answer: Let’s consider the cases:
Answer: No, because the negative UNL approach is safer.
First let’s compare the two approaches intuitively, (1) the _negative UNL_
approach, and (2) _lower quorum_: simply lowering the quorum from 80% to 60%
without the negative UNL. The negative UNL approach uses consensus to come up
with a list of unreliable validators, which are then removed from the effective
UNL temporarily. With this approach, the list of unreliable validators is agreed
to by a quorum of validators and will be used by every node in the network to
adjust its UNL. The quorum is always 80% of the effective UNL. The lower quorum
approach is a tradeoff between safety and liveness and against our principle of
preferring safety over liveness. Note that different validators don't have to
agree on which validation sources they are ignoring.
First let’s compare the two approaches intuitively, (1) the _negative UNL_ approach, and (2) _lower quorum_: simply lowering the quorum from 80% to 60% without the negative UNL. The negative UNL approach uses consensus to come up with a list of unreliable validators, which are then removed from the effective UNL temporarily. With this approach, the list of unreliable validators is agreed to by a quorum of validators and will be used by every node in the network to adjust its UNL. The quorum is always 80% of the effective UNL. The lower quorum approach is a tradeoff between safety and liveness and against our principle of preferring safety over liveness. Note that different validators don't have to agree on which validation sources they are ignoring.
Next we compare the two approaches quantitatively with examples, and apply
Theorem 8 of [Analysis of the XRP Ledger Consensus
Protocol](https://arxiv.org/abs/1802.07242) paper:
Next we compare the two approaches quantitatively with examples, and apply Theorem 8 of [Analysis of the XRP Ledger Consensus Protocol](https://arxiv.org/abs/1802.07242) paper:
_XRP LCP guarantees fork safety if **O<sub>i,j</sub> > n<sub>j</sub> / 2 +
n<sub>i</sub> − q<sub>i</sub> + t<sub>i,j</sub>** for every pair of nodes
P<sub>i</sub>, P<sub>j</sub>,_
_XRP LCP guarantees fork safety if **O<sub>i,j</sub> > n<sub>j</sub> / 2 + n<sub>i</sub> − q<sub>i</sub> + t<sub>i,j</sub>** for every pair of nodes P<sub>i</sub>, P<sub>j</sub>,_
where _O<sub>i,j</sub>_ is the overlapping requirement, n<sub>j</sub> and
n<sub>i</sub> are UNL sizes, q<sub>i</sub> is the quorum size of P<sub>i</sub>,
_t<sub>i,j</sub> = min(t<sub>i</sub>, t<sub>j</sub>, O<sub>i,j</sub>)_, and
t<sub>i</sub> and t<sub>j</sub> are the number of faults can be tolerated by
P<sub>i</sub> and P<sub>j</sub>.
where _O<sub>i,j</sub>_ is the overlapping requirement, n<sub>j</sub> and n<sub>i</sub> are UNL sizes, q<sub>i</sub> is the quorum size of P<sub>i</sub>, _t<sub>i,j</sub> = min(t<sub>i</sub>, t<sub>j</sub>, O<sub>i,j</sub>)_, and t<sub>i</sub> and t<sub>j</sub> are the number of faults can be tolerated by P<sub>i</sub> and P<sub>j</sub>.
We denote _UNL<sub>i</sub>_ as _P<sub>i</sub>'s UNL_, and _|UNL<sub>i</sub>|_ as
the size of _P<sub>i</sub>'s UNL_.
We denote _UNL<sub>i</sub>_ as _P<sub>i</sub>'s UNL_, and _|UNL<sub>i</sub>|_ as the size of _P<sub>i</sub>'s UNL_.
Assuming _|UNL<sub>i</sub>| = |UNL<sub>j</sub>|_, let's consider the following
three cases:
Assuming _|UNL<sub>i</sub>| = |UNL<sub>j</sub>|_, let's consider the following three cases:
1. With 80% quorum and 20% faults, _O<sub>i,j</sub> > 100% / 2 + 100% - 80% +
20% = 90%_. I.e. fork safety requires > 90% UNL overlaps. This is one of the
results in the analysis paper.
1. With 80% quorum and 20% faults, _O<sub>i,j</sub> > 100% / 2 + 100% - 80% + 20% = 90%_. I.e. fork safety requires > 90% UNL overlaps. This is one of the results in the analysis paper.
1. If the quorum is 60%, the relationship between the overlapping requirement
and the faults that can be tolerated is _O<sub>i,j</sub> > 90% +
t<sub>i,j</sub>_. Under the same overlapping condition (i.e. 90%), to guarantee
the fork safety, the network cannot tolerate any faults. So under the same
overlapping condition, if the quorum is simply lowered, the network can tolerate
fewer faults.
1. If the quorum is 60%, the relationship between the overlapping requirement and the faults that can be tolerated is _O<sub>i,j</sub> > 90% + t<sub>i,j</sub>_. Under the same overlapping condition (i.e. 90%), to guarantee the fork safety, the network cannot tolerate any faults. So under the same overlapping condition, if the quorum is simply lowered, the network can tolerate fewer faults.
1. With the negative UNL approach, we want to argue that the inequation
_O<sub>i,j</sub> > n<sub>j</sub> / 2 + n<sub>i</sub> − q<sub>i</sub> +
t<sub>i,j</sub>_ is always true to guarantee fork safety, while the negative UNL
protocol runs, i.e. the effective quorum is lowered without weakening the
network's fault tolerance. To make the discussion easier, we rewrite the
inequation as _O<sub>i,j</sub> > n<sub>j</sub> / 2 + (n<sub>i</sub> −
q<sub>i</sub>) + min(t<sub>i</sub>, t<sub>j</sub>)_, where O<sub>i,j</sub> is
dropped from the definition of t<sub>i,j</sub> because _O<sub>i,j</sub> >
min(t<sub>i</sub>, t<sub>j</sub>)_ always holds under the parameters we will
use. Assuming a validator V is added to the negative UNL, now let's consider the
4 cases:
1. With the negative UNL approach, we want to argue that the inequation _O<sub>i,j</sub> > n<sub>j</sub> / 2 + n<sub>i</sub> − q<sub>i</sub> + t<sub>i,j</sub>_ is always true to guarantee fork safety, while the negative UNL protocol runs, i.e. the effective quorum is lowered without weakening the network's fault tolerance. To make the discussion easier, we rewrite the inequation as _O<sub>i,j</sub> > n<sub>j</sub> / 2 + (n<sub>i</sub> − q<sub>i</sub>) + min(t<sub>i</sub>, t<sub>j</sub>)_, where O<sub>i,j</sub> is dropped from the definition of t<sub>i,j</sub> because _O<sub>i,j</sub> > min(t<sub>i</sub>, t<sub>j</sub>)_ always holds under the parameters we will use. Assuming a validator V is added to the negative UNL, now let's consider the 4 cases:
1. V is not on UNL<sub>i</sub> nor UNL<sub>j</sub>
@@ -526,64 +309,35 @@ three cases:
<h3> Question: We have observed that occasionally a validator wanders off on its
own chain. How is this case handled by the negative UNL algorithm? </h3>
Answer: The case that a validator wanders off on its own chain can be measured
with the validations agreement. Because the validations by this validator must
be different from other validators' validations of the same sequence numbers.
When there are enough disagreed validations, other validators will vote this
validator onto the negative UNL.
Answer: The case that a validator wanders off on its own chain can be measured with the validations agreement. Because the validations by this validator must be different from other validators' validations of the same sequence numbers. When there are enough disagreed validations, other validators will vote this validator onto the negative UNL.
In general by measuring the agreement of validations, we also measured the
"sanity". If two validators have too many disagreements, one of them could be
insane. When enough validators think a validator is insane, that validator is
put on the negative UNL.
In general by measuring the agreement of validations, we also measured the "sanity". If two validators have too many disagreements, one of them could be insane. When enough validators think a validator is insane, that validator is put on the negative UNL.
<h3> Question: Why would there be at most one disable UNLModify and one
re-enable UNLModify transaction per flag ledger? </h3>
Answer: It is a design choice so that the effective UNL does not change too
quickly. A typical targeted scenario is several validators go offline slowly
during a long weekend. The current design can handle this kind of cases well
without changing the effective UNL too quickly.
Answer: It is a design choice so that the effective UNL does not change too quickly. A typical targeted scenario is several validators go offline slowly during a long weekend. The current design can handle this kind of cases well without changing the effective UNL too quickly.
## Appendix
### Confidence Test
We will use two test networks, a single machine test network with multiple IP
addresses and the QE test network with multiple machines. The single machine
network will be used to test all the test cases and to debug. The QE network
will be used after that. We want to see the test cases still pass with real
network delay. A test case specifies:
We will use two test networks, a single machine test network with multiple IP addresses and the QE test network with multiple machines. The single machine network will be used to test all the test cases and to debug. The QE network will be used after that. We want to see the test cases still pass with real network delay. A test case specifies:
1. a UNL with different number of validators for different test cases,
1. a network with zero or more non-validator nodes,
1. a sequence of validator reliability change events (by killing/restarting
nodes, or by running modified xrpld that does not send all validation
messages),
1. a sequence of validator reliability change events (by killing/restarting nodes, or by running modified xrpld that does not send all validation messages),
1. the correct outcomes.
For all the test cases, the correct outcomes are verified by examining logs. We
will grep the log to see if the correct negative UNLs are generated, and whether
or not the network is making progress when it should be. The ripdtop tool will
be helpful for monitoring validators' states and ledger progress. Some of the
timing parameters of xrpld will be changed to have faster ledger time. Most if
not all test cases do not need client transactions.
For all the test cases, the correct outcomes are verified by examining logs. We will grep the log to see if the correct negative UNLs are generated, and whether or not the network is making progress when it should be. The ripdtop tool will be helpful for monitoring validators' states and ledger progress. Some of the timing parameters of xrpld will be changed to have faster ledger time. Most if not all test cases do not need client transactions.
For example, the test cases for the prototype:
1. A 10-validator UNL.
1. The network does not have other nodes.
1. The validators will be started from the genesis. Once they start to produce
ledgers, we kill five validators, one every flag ledger interval. Then we
will restart them one by one.
1. A sequence of events (or the lack of events) such as a killed validator is
added to the negative UNL.
1. The validators will be started from the genesis. Once they start to produce ledgers, we kill five validators, one every flag ledger interval. Then we will restart them one by one.
1. A sequence of events (or the lack of events) such as a killed validator is added to the negative UNL.
#### Roads Not Taken: Test with Extended CSF
We considered testing with the current unit test framework, specifically the
[Consensus Simulation
Framework](https://github.com/XRPLF/rippled/blob/develop/src/test/csf/README.md)
(CSF). However, the CSF currently can only test the generic consensus algorithm
as in the paper: [Analysis of the XRP Ledger Consensus
Protocol](https://arxiv.org/abs/1802.07242).
We considered testing with the current unit test framework, specifically the [Consensus Simulation Framework](https://github.com/XRPLF/rippled/blob/develop/src/test/csf/README.md) (CSF). However, the CSF currently can only test the generic consensus algorithm as in the paper: [Analysis of the XRP Ledger Consensus Protocol](https://arxiv.org/abs/1802.07242).

View File

@@ -1,63 +1,20 @@
# Ledger Replay
`LedgerReplayer` is a new `Stoppable` for replaying ledgers.
Patterned after two other `Stoppable`s under `JobQueue`---`InboundLedgers`
and `InboundTransactions`---it acts like a factory for creating
state-machine workers, and a network message demultiplexer for those workers.
Think of these workers like asynchronous functions.
Like functions, they each take a set of parameters.
The `Stoppable` memoizes these functions. It maintains a table for each
worker type, mapping sets of arguments to the worker currently working
on that argument set.
Whenever the `Stoppable` is asked to construct a worker, it first searches its
table to see if there is an existing worker with the same or overlapping
argument set.
If one exists, then it is used. If not, then a new one is created,
initialized, and added to the table.
`LedgerReplayer` is a new `Stoppable` for replaying ledgers. Patterned after two other `Stoppable`s under `JobQueue`---`InboundLedgers` and `InboundTransactions`---it acts like a factory for creating state-machine workers, and a network message demultiplexer for those workers. Think of these workers like asynchronous functions. Like functions, they each take a set of parameters. The `Stoppable` memoizes these functions. It maintains a table for each worker type, mapping sets of arguments to the worker currently working on that argument set. Whenever the `Stoppable` is asked to construct a worker, it first searches its table to see if there is an existing worker with the same or overlapping argument set. If one exists, then it is used. If not, then a new one is created, initialized, and added to the table.
For `LedgerReplayer`, there are three worker types: `LedgerReplayTask`,
`SkipListAcquire`, and `LedgerDeltaAcquire`.
Each is derived from `TimeoutCounter` to give it a timeout.
For `LedgerReplayTask`, the parameter set
is {reason, finish ledger ID, number of ledgers}. For `SkipListAcquire` and
`LedgerDeltaAcquire`, there is just one parameter: a ledger ID.
For `LedgerReplayer`, there are three worker types: `LedgerReplayTask`, `SkipListAcquire`, and `LedgerDeltaAcquire`. Each is derived from `TimeoutCounter` to give it a timeout. For `LedgerReplayTask`, the parameter set is {reason, finish ledger ID, number of ledgers}. For `SkipListAcquire` and `LedgerDeltaAcquire`, there is just one parameter: a ledger ID.
Each `Stoppable` has an entry point. For `LedgerReplayer`, it is `replay`.
`replay` creates two workers: a `LedgerReplayTask` and a `SkipListAcquire`.
`LedgerDeltaAcquire`s are created in the callback for when the skip list
returns.
Each `Stoppable` has an entry point. For `LedgerReplayer`, it is `replay`. `replay` creates two workers: a `LedgerReplayTask` and a `SkipListAcquire`. `LedgerDeltaAcquire`s are created in the callback for when the skip list returns.
For `SkipListAcquire` and `LedgerDeltaAcquire`, initialization fires off the
underlying asynchronous network request and starts the timeout. The argument
set identifying the worker is included in the network request, and copied to
the network response. `SkipListAcquire` sends a request for a proof path for
the skip list of the desired ledger. `LedgerDeltaAcquire` sends a request for
the transaction set of the desired ledger.
For `SkipListAcquire` and `LedgerDeltaAcquire`, initialization fires off the underlying asynchronous network request and starts the timeout. The argument set identifying the worker is included in the network request, and copied to the network response. `SkipListAcquire` sends a request for a proof path for the skip list of the desired ledger. `LedgerDeltaAcquire` sends a request for the transaction set of the desired ledger.
`LedgerReplayer` is also a network message demultiplexer.
When a response arrives for a request that was sent by a `SkipListAcquire` or
`LedgerDeltaAcquire` worker, the `Peer` object knows to send it to the
`LedgerReplayer`, which looks up the worker waiting for that response based on
the identifying argument set included in the response.
`LedgerReplayer` is also a network message demultiplexer. When a response arrives for a request that was sent by a `SkipListAcquire` or `LedgerDeltaAcquire` worker, the `Peer` object knows to send it to the `LedgerReplayer`, which looks up the worker waiting for that response based on the identifying argument set included in the response.
`LedgerReplayTask` may ask `InboundLedgers` to send requests to acquire
the start ledger, but there is no way to attach a callback or be notified when
the `InboundLedger` worker completes. All the responses for its messages will
be directed to `InboundLedgers`, not `LedgerReplayer`. Instead,
`LedgerReplayTask` checks whether the start ledger has arrived every time its
timeout expires.
`LedgerReplayTask` may ask `InboundLedgers` to send requests to acquire the start ledger, but there is no way to attach a callback or be notified when the `InboundLedger` worker completes. All the responses for its messages will be directed to `InboundLedgers`, not `LedgerReplayer`. Instead, `LedgerReplayTask` checks whether the start ledger has arrived every time its timeout expires.
Like a promise, each worker keeps track of whether it is pending (`!isDone()`)
or whether it has resolved successfully (`complete_ == true`) or unsuccessfully
(`failed_ == true`). It will never exist in both resolved states at once, nor
will it return to a pending state after reaching a resolved state.
Like a promise, each worker keeps track of whether it is pending (`!isDone()`) or whether it has resolved successfully (`complete_ == true`) or unsuccessfully (`failed_ == true`). It will never exist in both resolved states at once, nor will it return to a pending state after reaching a resolved state.
Like promises, some workers can accept continuations to be called when they
reach a resolved state, or immediately if they are already resolved.
`SkipListAcquire` and `LedgerDeltaAcquire` both accept continuations of a type
specific to their payload, both via a method named `addDataCallback()`. Continuations
cannot be removed explicitly, but they are held by `std::weak_ptr` so they can
be removed implicitly.
Like promises, some workers can accept continuations to be called when they reach a resolved state, or immediately if they are already resolved. `SkipListAcquire` and `LedgerDeltaAcquire` both accept continuations of a type specific to their payload, both via a method named `addDataCallback()`. Continuations cannot be removed explicitly, but they are held by `std::weak_ptr` so they can be removed implicitly.
`LedgerReplayTask` is simultaneously:
@@ -73,13 +30,7 @@ Each of these roles corresponds to different entry points:
1. the callback added to `LedgerDeltaAcquire`, which calls `deltaReady(...)` or `cancel()`
1. `onTimer()`
Each of these entry points does something unique to that entry point. They
either (a) transition `LedgerReplayTask` to a terminal failed resolved state
(`cancel()` and `onTimer()`) or (b) try to make progress toward the successful
resolved state. `init()` and `updateSkipList(...)` call `trigger()` while
`deltaReady(...)` calls `tryAdvance()`. There's a similarity between this
pattern and the way coroutines are implemented, where every yield saves the spot
in the code where it left off and every resume jumps back to that spot.
Each of these entry points does something unique to that entry point. They either (a) transition `LedgerReplayTask` to a terminal failed resolved state (`cancel()` and `onTimer()`) or (b) try to make progress toward the successful resolved state. `init()` and `updateSkipList(...)` call `trigger()` while `deltaReady(...)` calls `tryAdvance()`. There's a similarity between this pattern and the way coroutines are implemented, where every yield saves the spot in the code where it left off and every resume jumps back to that spot.
### Sequence Diagram

View File

@@ -1,13 +1,10 @@
# Coding Standards
Coding standards used here gradually evolve and propagate through
code reviews. Some aspects are enforced more strictly than others.
Coding standards used here gradually evolve and propagate through code reviews. Some aspects are enforced more strictly than others.
## Rules
These rules only apply to our own code. We can't enforce any sort of
style on the external repositories and libraries we include. The best
guideline is to maintain the standards that are used in those libraries.
These rules only apply to our own code. We can't enforce any sort of style on the external repositories and libraries we include. The best guideline is to maintain the standards that are used in those libraries.
- Tab inserts 4 spaces. No tab characters.
- Braces are indented in the [Allman style][1].
@@ -16,67 +13,37 @@ guideline is to maintain the standards that are used in those libraries.
## Guidelines
If you want to do something contrary to these guidelines, understand
why you're doing it. Think, use common sense, and consider that these
changes will probably need to be maintained long after you've
moved on to other projects.
If you want to do something contrary to these guidelines, understand why you're doing it. Think, use common sense, and consider that these changes will probably need to be maintained long after you've moved on to other projects.
- Use white space and blank lines to guide the eye and keep your intent clear.
- Put private data members at the top of a class, and the 6 public special
members immediately after, in the following order:
- Put private data members at the top of a class, and the 6 public special members immediately after, in the following order:
- Destructor
- Default constructor
- Copy constructor
- Copy assignment
- Move constructor
- Move assignment
- Don't over-inline by defining large functions within the class
declaration, not even for template classes.
- Don't over-inline by defining large functions within the class declaration, not even for template classes.
## Formatting
The goal of source code formatting should always be to make things as easy to
read as possible. White space is used to guide the eye so that details are not
overlooked. Blank lines are used to separate code into "paragraphs."
The goal of source code formatting should always be to make things as easy to read as possible. White space is used to guide the eye so that details are not overlooked. Blank lines are used to separate code into "paragraphs."
- Always place a space before and after all binary operators,
especially assignments (`operator=`).
- Always place a space before and after all binary operators, especially assignments (`operator=`).
- The `!` operator should be preceded by a space, but not followed by one.
- The `~` operator should be preceded by a space, but not followed by one.
- The `++` and `--` operators should have no spaces between the operator and
the operand.
- The `++` and `--` operators should have no spaces between the operator and the operand.
- A space never appears before a comma, and always appears after a comma.
- Don't put spaces after a parenthesis. A typical member function call might
look like this: `foobar (1, 2, 3);`
- Don't put spaces after a parenthesis. A typical member function call might look like this: `foobar (1, 2, 3);`
- In general, leave a blank line before an `if` statement.
- In general, leave a blank line after a closing brace `}`.
- Do not place code on the same line as any opening or
closing brace.
- Do not write `if` statements all-on-one-line. The exception to this is when
you've got a sequence of similar `if` statements, and are aligning them all
vertically to highlight their similarities.
- In an `if-else` statement, if you surround one half of the statement with
braces, you also need to put braces around the other half, to match.
- When writing a pointer type, use this spacing: `SomeObject* myObject`.
Technically, a more correct spacing would be `SomeObject *myObject`, but
it makes more sense for the asterisk to be grouped with the type name,
since being a pointer is part of the type, not the variable name. The only
time that this can lead to any problems is when you're declaring multiple
pointers of the same type in the same statement - which leads on to the next
rule:
- When declaring multiple pointers, never do so in a single statement, e.g.
`SomeObject* p1, *p2;` - instead, always split them out onto separate lines
and write the type name again, to make it quite clear what's going on, and
avoid the danger of missing out any vital asterisks.
- The previous point also applies to references, so always put the `&` next to
the type rather than the variable, e.g. `void foo (Thing const& thing)`. And
don't put a space on both sides of the `*` or `&` - always put a space after
it, but never before it.
- The word `const` should be placed to the right of the thing that it modifies,
for consistency. For example `int const` refers to an int which is const.
`int const*` is a pointer to an int which is const. `int *const` is a const
pointer to an int.
- Always place a space in between the template angle brackets and the type
name. Template code is already hard enough to read!
- Do not place code on the same line as any opening or closing brace.
- Do not write `if` statements all-on-one-line. The exception to this is when you've got a sequence of similar `if` statements, and are aligning them all vertically to highlight their similarities.
- In an `if-else` statement, if you surround one half of the statement with braces, you also need to put braces around the other half, to match.
- When writing a pointer type, use this spacing: `SomeObject* myObject`. Technically, a more correct spacing would be `SomeObject *myObject`, but it makes more sense for the asterisk to be grouped with the type name, since being a pointer is part of the type, not the variable name. The only time that this can lead to any problems is when you're declaring multiple pointers of the same type in the same statement - which leads on to the next rule:
- When declaring multiple pointers, never do so in a single statement, e.g. `SomeObject* p1, *p2;` - instead, always split them out onto separate lines and write the type name again, to make it quite clear what's going on, and avoid the danger of missing out any vital asterisks.
- The previous point also applies to references, so always put the `&` next to the type rather than the variable, e.g. `void foo (Thing const& thing)`. And don't put a space on both sides of the `*` or `&` - always put a space after it, but never before it.
- The word `const` should be placed to the right of the thing that it modifies, for consistency. For example `int const` refers to an int which is const. `int const*` is a pointer to an int which is const. `int *const` is a const pointer to an int.
- Always place a space in between the template angle brackets and the type name. Template code is already hard enough to read!
[1]: http://en.wikipedia.org/wiki/Indent_style#Allman_style

View File

@@ -1,33 +1,18 @@
## Heap profiling of xrpld with jemalloc
The jemalloc library provides a good API for doing heap analysis,
including a mechanism to dump a description of the heap from within the
running application via a function call. Details on how to perform this
activity in general, as well as how to acquire the software, are available on
the jemalloc site:
[https://github.com/jemalloc/jemalloc/wiki/Use-Case:-Heap-Profiling](https://github.com/jemalloc/jemalloc/wiki/Use-Case:-Heap-Profiling)
The jemalloc library provides a good API for doing heap analysis, including a mechanism to dump a description of the heap from within the running application via a function call. Details on how to perform this activity in general, as well as how to acquire the software, are available on the jemalloc site: [https://github.com/jemalloc/jemalloc/wiki/Use-Case:-Heap-Profiling](https://github.com/jemalloc/jemalloc/wiki/Use-Case:-Heap-Profiling)
jemalloc is acquired separately from xrpld, and is not affiliated
with Ripple Labs. If you compile and install jemalloc from the
source release with default options, it will install the library and header
under `/usr/local/lib` and `/usr/local/include`, respectively. Heap
profiling has been tested with xrpld on a Linux platform. It should
work on platforms on which both xrpld and jemalloc are available.
jemalloc is acquired separately from xrpld, and is not affiliated with Ripple Labs. If you compile and install jemalloc from the source release with default options, it will install the library and header under `/usr/local/lib` and `/usr/local/include`, respectively. Heap profiling has been tested with xrpld on a Linux platform. It should work on platforms on which both xrpld and jemalloc are available.
To link xrpld with jemalloc, the argument
`profile-jemalloc=<jemalloc_dir>` is provided after the optional target.
The `<jemalloc_dir>` argument should be the same as that of the
`--prefix` parameter passed to the jemalloc configure script when building.
To link xrpld with jemalloc, the argument `profile-jemalloc=<jemalloc_dir>` is provided after the optional target. The `<jemalloc_dir>` argument should be the same as that of the `--prefix` parameter passed to the jemalloc configure script when building.
## Examples:
Build xrpld with jemalloc library under /usr/local/lib and
header under /usr/local/include:
Build xrpld with jemalloc library under /usr/local/lib and header under /usr/local/include:
$ scons profile-jemalloc=/usr/local
Build xrpld using clang with the jemalloc library under /opt/local/lib
and header under /opt/local/include:
Build xrpld using clang with the jemalloc library under /opt/local/lib and header under /opt/local/include:
$ scons clang profile-jemalloc=/opt/local
@@ -35,10 +20,7 @@ and header under /opt/local/include:
## Using the jemalloc library from within the code
The `profile-jemalloc` parameter enables a macro definition called
`PROFILE_JEMALLOC`. Include the jemalloc header file as
well as the api call(s) that you wish to make within preprocessor
conditional groups, such as:
The `profile-jemalloc` parameter enables a macro definition called `PROFILE_JEMALLOC`. Include the jemalloc header file as well as the api call(s) that you wish to make within preprocessor conditional groups, such as:
In global scope:
@@ -52,11 +34,6 @@ And later, within a function scope:
mallctl("prof.dump", NULL, NULL, NULL, 0);
#endif
Fuller descriptions of how to acquire and use jemalloc's api to do memory
analysis are available at the [jemalloc
site.](http://www.canonware.com/jemalloc/)
Fuller descriptions of how to acquire and use jemalloc's api to do memory analysis are available at the [jemalloc site.](http://www.canonware.com/jemalloc/)
Linking against the jemalloc library will override
the system's default `malloc()` and related functions with jemalloc's
implementation. This is the case even if the code is not instrumented
to use jemalloc's specific API.
Linking against the jemalloc library will override the system's default `malloc()` and related functions with jemalloc's implementation. This is the case even if the code is not instrumented to use jemalloc's specific API.

View File

@@ -4,34 +4,25 @@ This document provides advanced instructions for setting up and configuring Cona
## 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:
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.
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.
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.
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:
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.xrplf.org/repository/conan/
@@ -61,16 +52,11 @@ git checkout master
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.
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.
<!-- prettier-ignore -->
> [!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
> 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.
@@ -88,8 +74,7 @@ Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1',
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:
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:
@@ -99,13 +84,9 @@ compiler:
### 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.
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:
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
@@ -113,9 +94,7 @@ 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:
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]
@@ -124,15 +103,11 @@ 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.
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:
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
@@ -140,10 +115,7 @@ sed -i.bak -e 's|^compiler\.cppstd=.*$|compiler.cppstd=23|' $(conan config home)
### 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:
**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
@@ -151,12 +123,9 @@ sed -i.bak -e 's|^compiler\.libcxx=.*$|compiler.libcxx=libstdc++11|' $(conan con
### 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 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:
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
@@ -175,10 +144,8 @@ 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:
- 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
@@ -186,8 +153,7 @@ If you want to experiment with a new package, follow these steps:
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 ...)`).
- 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

103
docs/build/conan.md vendored
View File

@@ -1,115 +1,44 @@
## A crash course in CMake and Conan
To better understand how to use Conan,
we should first understand _why_ we use Conan,
and to understand that,
we need to understand how we use CMake.
To better understand how to use Conan, we should first understand _why_ we use Conan, and to understand that, we need to understand how we use CMake.
### CMake
Technically, you don't need CMake to build this project.
You could manually compile every translation unit into an object file,
using the right compiler options,
and then manually link all those objects together,
using the right linker options.
However, that is very tedious and error-prone,
which is why we lean on tools like CMake.
Technically, you don't need CMake to build this project. You could manually compile every translation unit into an object file, using the right compiler options, and then manually link all those objects together, using the right linker options. However, that is very tedious and error-prone, which is why we lean on tools like CMake.
We have written CMake configuration files
([`CMakeLists.txt`](./CMakeLists.txt) and friends)
for this project so that CMake can be used to correctly compile and link
all of the translation units in it.
Or rather, CMake will generate files for a separate build system
(e.g. Make, Ninja, Visual Studio, Xcode, etc.)
that compile and link all of the translation units.
Even then, CMake has parameters, some of which are platform-specific.
In CMake's parlance, parameters are specially-named **variables** like
[`CMAKE_BUILD_TYPE`][build_type] or
[`CMAKE_MSVC_RUNTIME_LIBRARY`][runtime].
Parameters include:
We have written CMake configuration files ([`CMakeLists.txt`](./CMakeLists.txt) and friends) for this project so that CMake can be used to correctly compile and link all of the translation units in it. Or rather, CMake will generate files for a separate build system (e.g. Make, Ninja, Visual Studio, Xcode, etc.) that compile and link all of the translation units. Even then, CMake has parameters, some of which are platform-specific. In CMake's parlance, parameters are specially-named **variables** like [`CMAKE_BUILD_TYPE`][build_type] or [`CMAKE_MSVC_RUNTIME_LIBRARY`][runtime]. Parameters include:
- what build system to generate files for
- where to find the compiler and linker
- where to find dependencies, e.g. libraries and headers
- how to link dependencies, e.g. any special compiler or linker flags that
need to be used with them, including preprocessor definitions
- how to compile translation units, e.g. with optimizations, debug symbols,
position-independent code, etc.
- how to link dependencies, e.g. any special compiler or linker flags that need to be used with them, including preprocessor definitions
- how to compile translation units, e.g. with optimizations, debug symbols, position-independent code, etc.
- on Windows, which runtime library to link with
For some of these parameters, like the build system and compiler,
CMake goes through a complicated search process to choose default values.
For others, like the dependencies,
_we_ had written in the CMake configuration files of this project
our own complicated process to choose defaults.
For most developers, things "just worked"... until they didn't, and then
you were left trying to debug one of these complicated processes, instead of
choosing and manually passing the parameter values yourself.
For some of these parameters, like the build system and compiler, CMake goes through a complicated search process to choose default values. For others, like the dependencies, _we_ had written in the CMake configuration files of this project our own complicated process to choose defaults. For most developers, things "just worked"... until they didn't, and then you were left trying to debug one of these complicated processes, instead of choosing and manually passing the parameter values yourself.
You can pass every parameter to CMake on the command line,
but writing out these parameters every time we want to configure CMake is
a pain.
Most humans prefer to put them into a configuration file, once, that
CMake can read every time it is configured.
For CMake, that file is a [toolchain file][toolchain].
You can pass every parameter to CMake on the command line, but writing out these parameters every time we want to configure CMake is a pain. Most humans prefer to put them into a configuration file, once, that CMake can read every time it is configured. For CMake, that file is a [toolchain file][toolchain].
### Conan
These next few paragraphs on Conan are going to read much like the ones above
for CMake.
These next few paragraphs on Conan are going to read much like the ones above for CMake.
Technically, you don't need Conan to build this project.
You could manually download, configure, build, and install all of the
dependencies yourself, and then pass all of the parameters necessary for
CMake to link to those dependencies.
To guarantee ABI compatibility, you must be sure to use the same set of
compiler and linker options for all dependencies _and_ this project.
However, that is very tedious and error-prone, which is why we lean on tools
like Conan.
Technically, you don't need Conan to build this project. You could manually download, configure, build, and install all of the dependencies yourself, and then pass all of the parameters necessary for CMake to link to those dependencies. To guarantee ABI compatibility, you must be sure to use the same set of compiler and linker options for all dependencies _and_ this project. However, that is very tedious and error-prone, which is why we lean on tools like Conan.
We have written a Conan configuration file ([`conanfile.py`](../../conanfile.py))
so that Conan can be used to correctly download, configure, build, and install
all of the dependencies for this project,
using a single set of compiler and linker options for all of them.
It generates files that contain almost all of the parameters that CMake
expects.
Those files include:
We have written a Conan configuration file ([`conanfile.py`](../../conanfile.py)) so that Conan can be used to correctly download, configure, build, and install all of the dependencies for this project, using a single set of compiler and linker options for all of them. It generates files that contain almost all of the parameters that CMake expects. Those files include:
- A single toolchain file.
- For every dependency, a CMake [package configuration file][pcf],
[package version file][pvf], and for every build type, a package
targets file.
Together, these files implement version checking and define `IMPORTED`
targets for the dependencies.
- For every dependency, a CMake [package configuration file][pcf], [package version file][pvf], and for every build type, a package targets file. Together, these files implement version checking and define `IMPORTED` targets for the dependencies.
The toolchain file itself amends the search path
([`CMAKE_PREFIX_PATH`][prefix_path]) so that [`find_package()`][find_package]
will [discover][search] the generated package configuration files.
The toolchain file itself amends the search path ([`CMAKE_PREFIX_PATH`][prefix_path]) so that [`find_package()`][find_package] will [discover][search] the generated package configuration files.
**Nearly all we must do to properly configure CMake is pass the toolchain
file.**
What CMake parameters are left out?
You'll still need to pick a build system generator,
and if you choose a single-configuration generator,
you'll need to pass the `CMAKE_BUILD_TYPE`,
which should match the `build_type` setting you gave to Conan.
**Nearly all we must do to properly configure CMake is pass the toolchain file.** What CMake parameters are left out? You'll still need to pick a build system generator, and if you choose a single-configuration generator, you'll need to pass the `CMAKE_BUILD_TYPE`, which should match the `build_type` setting you gave to Conan.
Even then, Conan has parameters, some of which are platform-specific.
In Conan's parlance, parameters are either settings or options.
**Settings** are shared by all packages, e.g. the build type.
**Options** are specific to a given package, e.g. whether to build and link
OpenSSL as a shared library.
Even then, Conan has parameters, some of which are platform-specific. In Conan's parlance, parameters are either settings or options. **Settings** are shared by all packages, e.g. the build type. **Options** are specific to a given package, e.g. whether to build and link OpenSSL as a shared library.
For settings, Conan goes through a complicated search process to choose
defaults.
For options, each package recipe defines its own defaults.
For settings, Conan goes through a complicated search process to choose defaults. For options, each package recipe defines its own defaults.
You can pass every parameter to Conan on the command line,
but it is more convenient to put them in a configuration file, once, that
Conan can read every time it is configured.
For Conan, that file is a [profile][].
**All we must do to properly configure Conan is edit and pass the profile.**
By default, Conan will use the profile named "default".
You can pass every parameter to Conan on the command line, but it is more convenient to put them in a configuration file, once, that Conan can read every time it is configured. For Conan, that file is a [profile][]. **All we must do to properly configure Conan is edit and pass the profile.** By default, Conan will use the profile named "default".
[build_type]: https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html
[find_package]: https://cmake.org/cmake/help/latest/command/find_package.html

19
docs/build/depend.md vendored
View File

@@ -1,12 +1,8 @@
We recommend two different methods to depend on libxrpl in your own [CMake][]
project.
Both methods add a CMake library target named `xrpl::libxrpl`.
We recommend two different methods to depend on libxrpl in your own [CMake][] project. Both methods add a CMake library target named `xrpl::libxrpl`.
## Conan requirement
The first method adds libxrpl as a [Conan][] requirement.
With this method, there is no need for a Git [submodule][].
It is good for when you just need a dependency on libxrpl as-is.
The first method adds libxrpl as a [Conan][] requirement. With this method, there is no need for a Git [submodule][]. It is good for when you just need a dependency on libxrpl as-is.
```
# This conanfile.txt is just an example.
@@ -49,16 +45,7 @@ cmake --build . --parallel
## CMake subdirectory
The second method adds the [xrpld][] project as a CMake
[subdirectory][add_subdirectory].
This method works well when you keep the xrpld project as a Git
[submodule][].
It's good for when you want to make changes to libxrpl as part of your own
project.
Be careful, though.
Your project will inherit all of the same CMake options,
so watch out for name collisions.
We still recommend using [Conan][] to download, build, and connect dependencies.
The second method adds the [xrpld][] project as a CMake [subdirectory][add_subdirectory]. This method works well when you keep the xrpld project as a Git [submodule][]. It's good for when you want to make changes to libxrpl as part of your own project. Be careful, though. Your project will inherit all of the same CMake options, so watch out for name collisions. We still recommend using [Conan][] to download, build, and connect dependencies.
```
# Add the project as a Git submodule.

View File

@@ -1,14 +1,10 @@
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.
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.
[BUILD.md]: ../../BUILD.md
## Tested compiler versions
`xrpld` is built in the **C++23** dialect by default, so your toolchain has to
support it — see [compiler support for C++23][cpp23-support].
The versions currently tested in CI are:
`xrpld` is built in the **C++23** dialect by default, so your toolchain has to support it — see [compiler support for C++23][cpp23-support]. The versions currently tested in CI are:
| Compiler | Version |
| ----------- | ------------------ |
@@ -21,16 +17,9 @@ LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22.
### Older compilers
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.
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.
If updating isn't an option for you, we do accept pull requests that fix builds
on older compilers, as long as the change is small and doesn't make the code
harder to read. What we can't promise is that older compilers will keep working:
only the versions in the table above are tested in CI, and we won't hold back
the use of C++23 features or add invasive workarounds to keep an untested
compiler building. Treat support for anything outside the table as best-effort.
If updating isn't an option for you, we do accept pull requests that fix builds on older compilers, as long as the change is small and doesn't make the code harder to read. What we can't promise is that older compilers will keep working: only the versions in the table above are tested in CI, and we won't hold back the use of C++23 features or add invasive workarounds to keep an untested compiler building. Treat support for anything outside the table as best-effort.
## Required tools
@@ -43,11 +32,9 @@ Besides a compiler, building `xrpld` requires:
| [Conan](https://conan.io/downloads.html) | 2.17 |
| [CMake](https://cmake.org/download/) | 3.16 |
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
(see below). On Windows they have to be installed manually.
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them (see below). On Windows they have to be installed manually.
Building with `-Drust=ON` additionally requires a Rust toolchain, see
[Rust](#rust). A default build does not, so it is not in the table above.
Building with `-Drust=ON` additionally requires a Rust toolchain, see [Rust](#rust). A default build does not, so it is not in the table above.
Once they are in place, verify that everything is installed and runnable with:
@@ -57,38 +44,25 @@ Once they are in place, verify that everything is installed and runnable with:
## 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:
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
```
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides
Clang. If you instead opt to use your system-wide Apple Clang (via
`nix develop .#apple-clang`), you need to manage its version yourself (see
below).
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides Clang. If you instead opt to use your system-wide Apple Clang (via `nix develop .#apple-clang`), you need to manage its version yourself (see below).
See [Using the Nix development shell](./nix.md) for installation and usage
details, including how to select a different compiler and why we recommend Nix
over a hand-maintained environment.
See [Using the Nix development shell](./nix.md) for installation and usage details, including how to select a different compiler and why we recommend Nix over a hand-maintained environment.
### macOS: managing the Apple Clang version
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`),
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 [tested one](#tested-compiler-versions):
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`), 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 [tested one](#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:
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:
1. **Download Xcode**
- Visit [Apple Developer Downloads](https://developer.apple.com/download/more/)
@@ -114,45 +88,25 @@ building xrpld. You may want to install and pin a specific version of Xcode:
## Windows
Nix is not available on Windows, so the required tools have to be installed
manually:
Nix is not available on Windows, so the required tools have to be installed manually:
- [Visual Studio 2026](https://visualstudio.microsoft.com/) with the
**"Desktop development with C++"** workload — this provides MSVC and the
"x64 Native Tools Command Prompt". CI configures CMake with the
`Visual Studio 18 2026` generator.
- [Visual Studio 2026](https://visualstudio.microsoft.com/) with the **"Desktop development with C++"** workload — this provides MSVC and the "x64 Native Tools Command Prompt". CI configures CMake with the `Visual Studio 18 2026` generator.
- [Git for Windows](https://git-scm.com/download/win)
- Python, Conan, and CMake, at the versions listed in
[Required tools](#required-tools).
- a [Rust toolchain](https://rustup.rs) — only needed to build with
`-Drust=ON`, see [Rust](#rust)
- Python, Conan, and CMake, at the versions listed in [Required tools](#required-tools).
- a [Rust toolchain](https://rustup.rs) — only needed to build with `-Drust=ON`, see [Rust](#rust)
## Rust
The repository contains a Rust workspace in [`crates/`](../../crates), whose
crates are exposed to C++ through [cxx](https://cxx.rs) bindings. It is **not**
part of a default build: the CMake `rust` option is OFF by default, and with it
off no Rust toolchain is needed. It is only required when configuring with
`-Drust=ON` (which is what CI does), see [Options](../../BUILD.md#options).
The repository contains a Rust workspace in [`crates/`](../../crates), whose crates are exposed to C++ through [cxx](https://cxx.rs) bindings. It is **not** part of a default build: the CMake `rust` option is OFF by default, and with it off no Rust toolchain is needed. It is only required when configuring with `-Drust=ON` (which is what CI does), see [Options](../../BUILD.md#options).
The toolchain (`cargo`, `rustc`) is pinned to the channel in
[`rust-toolchain.toml`](../../rust-toolchain.toml) at the repository root. If
you install Rust with [rustup](https://rustup.rs), that file is picked up
automatically, and `cargo`/`rustc` in the repository will use the pinned
version.
The toolchain (`cargo`, `rustc`) is pinned to the channel in [`rust-toolchain.toml`](../../rust-toolchain.toml) at the repository root. If you install Rust with [rustup](https://rustup.rs), that file is picked up automatically, and `cargo`/`rustc` in the repository will use the pinned version.
Everything else the Rust build needs on the CMake side comes from Conan along
with the rest of the dependencies, so there is nothing further to install.
Everything else the Rust build needs on the CMake side comes from Conan along with the rest of the dependencies, so there is nothing further to install.
## 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. The version this project uses is listed in
[Tested compiler versions](#tested-compiler-versions).
`clang-tidy` is required to run static analysis checks locally (see [CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the project. The version this project uses is listed in [Tested compiler versions](#tested-compiler-versions).
On Linux and macOS, the [Nix development shell](./nix.md) provides that exact
version out of the box — run it via `run-clang-tidy`. No separate installation
is needed.
On Linux and macOS, the [Nix development shell](./nix.md) provides that exact version out of the box — run it via `run-clang-tidy`. No separate installation is needed.
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23

115
docs/build/nix.md vendored
View File

@@ -38,17 +38,10 @@ The first time you run this command, it will take a few minutes to download and
### Platform notes
- **Linux**: `nix develop` gives you a shell with all the tooling necessary to develop xrpld
and with the same GCC/glibc toolchain that Nix builds for CI.
See [Choosing a different compiler](#choosing-a-different-compiler)
for the custom-vs-plain toolchain trade-off.
- **macOS**: `nix develop` gives you a full environment too, with Clang (and
every other tool, including Conan) provided by Nix. To use your system-wide
Apple Clang instead, enter `nix develop .#apple-clang`. 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)).
- **Linux**: `nix develop` gives you a shell with all the tooling necessary to develop xrpld and with the same GCC/glibc toolchain that Nix builds for CI. See [Choosing a different compiler](#choosing-a-different-compiler) for the custom-vs-plain toolchain trade-off.
- **macOS**: `nix develop` gives you a full environment too, with Clang (and every other tool, including Conan) provided by Nix. To use your system-wide Apple Clang instead, enter `nix develop .#apple-clang`. 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)).
<!-- prettier-ignore -->
> [!TIP]
> To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`:
>
@@ -59,6 +52,7 @@ The first time you run this command, it will take a few minutes to download and
>
> After this, you can simply use `nix develop` instead.
<!-- prettier-ignore -->
> [!NOTE]
> The examples below assume you've enabled flakes in your config. If you haven't, add `--experimental-features 'nix-command flakes'` after each `nix` command.
@@ -66,14 +60,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 .#clang`.
On Linux, `.#gcc` and `.#clang` provide the exact toolchain CI uses:
the compiler (pinned in [`nix/packages.nix`](../../nix/packages.nix))
rebuilt against the pinned custom glibc (see [`nix/linux.nix`](../../nix/linux.nix)).
Building that toolchain the first time is slow unless it is fetched from a Nix binary cache.
If you don't need the custom glibc, the Linux-only `.#gcc-plain` and `.#clang-plain`
give you the stock nixpkgs compilers of the same versions.
On macOS there is no custom glibc, so `.#gcc` and `.#clang` are already the plain nixpkgs toolchain,
and the `-plain` variants do not exist.
On Linux, `.#gcc` and `.#clang` provide the exact toolchain CI uses: the compiler (pinned in [`nix/packages.nix`](../../nix/packages.nix)) rebuilt against the pinned custom glibc (see [`nix/linux.nix`](../../nix/linux.nix)). Building that toolchain the first time is slow unless it is fetched from a Nix binary cache. If you don't need the custom glibc, the Linux-only `.#gcc-plain` and `.#clang-plain` give you the stock nixpkgs compilers of the same versions. On macOS there is no custom glibc, so `.#gcc` and `.#clang` are already the plain nixpkgs toolchain, and the `-plain` variants do not exist.
Use `nix flake show` to see all the available development shells.
@@ -111,6 +98,7 @@ nix develop -c fish
nix develop -c "$SHELL"
```
<!-- prettier-ignore -->
> [!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:
>
@@ -124,80 +112,40 @@ nix develop -c "$SHELL"
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.).
Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Linux):
each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one.
The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage.
Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Linux): each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one. The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage.
Builds of the Rust crates (`-Drust=ON`) also work out of the box: every shell
provides the Rust toolchain pinned in
[`rust-toolchain.toml`](../../rust-toolchain.toml) (see
[Rust](./environment.md#rust)), plus the `cargo-audit`, `cargo-llvm-cov` and
`cargo-nextest` plugins.
Builds of the Rust crates (`-Drust=ON`) also work out of the box: every shell provides the Rust toolchain pinned in [`rust-toolchain.toml`](../../rust-toolchain.toml) (see [Rust](./environment.md#rust)), plus the `cargo-audit`, `cargo-llvm-cov` and `cargo-nextest` plugins.
## Conan configuration
The shell runs [`conan/init.sh`](../../conan/init.sh) on entry, so
[Set Up Conan](../../BUILD.md#set-up-conan) is already done for you. It installs
into the shell's own Conan home: `CONAN_HOME=~/.conan2-nix`.
The shell runs [`conan/init.sh`](../../conan/init.sh) on entry, so [Set Up Conan](../../BUILD.md#set-up-conan) is already done for you. It installs into the shell's own Conan home: `CONAN_HOME=~/.conan2-nix`.
### Prebuilt packages
On **Linux**, the binaries on the `xrplf` remote are built in this same Nix
environment — CI runs in Docker images that bundle the dev shell's toolchain (see
[`nix/docker`](../../nix/docker)) — so `.#gcc` and `.#clang` can reuse them. The
`-plain` shells do not match that toolchain's glibc, so binaries from the remote
are not a reliable match there.
On **Linux**, the binaries on the `xrplf` remote are built in this same Nix environment — CI runs in Docker images that bundle the dev shell's toolchain (see [`nix/docker`](../../nix/docker)) — so `.#gcc` and `.#clang` can reuse them. The `-plain` shells do not match that toolchain's glibc, so binaries from the remote are not a reliable match there.
On **macOS**, CI also builds in this Nix environment, in Debug and Release (the
`macos-arm64-*-nix` configurations — Debug because the profile defaults to it).
The Nix build resolves to `compiler=clang`, so it gets its own package IDs,
separate from the Apple Clang ones. The
[dependency upload](../../.github/workflows/upload-conan-deps.yml) publishes them
on pushes to `develop` and on manual runs — its nightly run rebuilds everything
from source but uploads nothing — so once a set has been published `nix develop`
can reuse it instead of compiling every dependency locally. These configurations
run outside the reduced pull-request matrix, so label a PR `Full CI build` when it
touches `flake.lock` or `nix/`.
On **macOS**, CI also builds in this Nix environment, in Debug and Release (the `macos-arm64-*-nix` configurations — Debug because the profile defaults to it). The Nix build resolves to `compiler=clang`, so it gets its own package IDs, separate from the Apple Clang ones. The [dependency upload](../../.github/workflows/upload-conan-deps.yml) publishes them on pushes to `develop` and on manual runs — its nightly run rebuilds everything from source but uploads nothing — so once a set has been published `nix develop` can reuse it instead of compiling every dependency locally. These configurations run outside the reduced pull-request matrix, so label a PR `Full CI build` when it touches `flake.lock` or `nix/`.
To compile everything from source, add `--build '*'` to the `conan install`
command.
To compile everything from source, add `--build '*'` to the `conan install` command.
### Why the nixpkgs revision is not part of the package ID
A Conan package ID records the compiler and its major version, but nothing about
the nixpkgs revision the toolchain came from — and `flake.lock` moves far more
often than the toolchain meaningfully changes, so folding it in would rebuild
every dependency on every bump for nothing.
A Conan package ID records the compiler and its major version, but nothing about the nixpkgs revision the toolchain came from — and `flake.lock` moves far more often than the toolchain meaningfully changes, so folding it in would rebuild every dependency on every bump for nothing.
That is safe as long as no cached artifact resolves a `/nix/store` path at run
time, because store paths change on every update and the old ones disappear with
`nix-collect-garbage`. With the `clang` toolchain macOS CI and the dev shell use,
they do not: it links against `/usr/lib/libc++` and `/usr/lib/libSystem`, and
store paths reach the `.a` files only through debug info, which nothing resolves
at link or run time.
That is safe as long as no cached artifact resolves a `/nix/store` path at run time, because store paths change on every update and the old ones disappear with `nix-collect-garbage`. With the `clang` toolchain macOS CI and the dev shell use, they do not: it links against `/usr/lib/libc++` and `/usr/lib/libSystem`, and store paths reach the `.a` files only through debug info, which nothing resolves at link or run time.
<!-- prettier-ignore -->
> [!WARNING]
> This does not hold for `nix develop .#gcc` on macOS. There is no system
> libstdc++, so GCC links its own from the store and every binary keeps a
> `/nix/store` reference. That shell is fine for tooling, but it is not a build
> configuration CI covers, and no dependency binaries are published for it.
> This does not hold for `nix develop .#gcc` on macOS. There is no system libstdc++, so GCC links its own from the store and every binary keeps a `/nix/store` reference. That shell is fine for tooling, but it is not a build configuration CI covers, and no dependency binaries are published for it.
This is checked rather than assumed.
[`bin/check-nix-store-refs.sh`](../../bin/check-nix-store-refs.sh) takes one file
or directory and fails if a binary under it resolves a store path at run time.
CI runs it over the build output and the Conan cache, and again in the upload job
before anything is published. You can run it yourself:
This is checked rather than assumed. [`bin/check-nix-store-refs.sh`](../../bin/check-nix-store-refs.sh) takes one file or directory and fails if a binary under it resolves a store path at run time. CI runs it over the build output and the Conan cache, and again in the upload job before anything is published. You can run it yourself:
```bash
bin/check-nix-store-refs.sh build
bin/check-nix-store-refs.sh ~/.conan2-nix
```
It works on Linux too, but asserts something narrower there: the toolchain always
writes the store into `PT_INTERP` and `RUNPATH`, and CI builds inside an image
whose store is fixed for its lifetime, so that is fine. Only the binaries
[`PatchNixBinary.cmake`](../../cmake/PatchNixBinary.cmake) retargets to the
system loader have to be clean, and those are what CI checks:
It works on Linux too, but asserts something narrower there: the toolchain always writes the store into `PT_INTERP` and `RUNPATH`, and CI builds inside an image whose store is fixed for its lifetime, so that is fine. Only the binaries [`PatchNixBinary.cmake`](../../cmake/PatchNixBinary.cmake) retargets to the system loader have to be clean, and those are what CI checks:
```bash
bin/check-nix-store-refs.sh build/xrpld
@@ -205,22 +153,11 @@ bin/check-nix-store-refs.sh build/xrpld
### The libresolv stub
This is not hypothetical: `xrpld` used to be caught by it. The c-ares package
tells the linker to pass `-lresolv`, and nixpkgs keeps `libresolv` out of the
macOS SDK and ships it as an ordinary store dylib — so every Nix-built `xrpld`
recorded a `/nix/store/…-libresolv-93/lib/libresolv.9.dylib` load command and
stopped running once that path was collected. Nothing in the link uses a single
symbol from it.
This is not hypothetical: `xrpld` used to be caught by it. The c-ares package tells the linker to pass `-lresolv`, and nixpkgs keeps `libresolv` out of the macOS SDK and ships it as an ordinary store dylib — so every Nix-built `xrpld` recorded a `/nix/store/…-libresolv-93/lib/libresolv.9.dylib` load command and stopped running once that path was collected. Nothing in the link uses a single symbol from it.
Both environments now put a stub on the linker search path
(`libresolvSystemStub` in [`nix/darwin.nix`](../../nix/darwin.nix)): the
same library with its install name set to `/usr/lib/libresolv.9.dylib`, which is
exactly the load command the Apple Clang build records.
Both environments now put a stub on the linker search path (`libresolvSystemStub` in [`nix/darwin.nix`](../../nix/darwin.nix)): the same library with its install name set to `/usr/lib/libresolv.9.dylib`, which is exactly the load command the Apple Clang build records.
Package IDs did not change, so Conan keeps serving anything built before the
stub landed. If a binary fails to start with `Library not loaded: /nix/store/…`,
see [that entry](./nix_troubleshooting.md#library-not-loaded-nixstore-from-a-binary-that-used-to-work)
in the troubleshooting guide.
Package IDs did not change, so Conan keeps serving anything built before the stub landed. If a binary fails to start with `Library not loaded: /nix/store/…`, see [that entry](./nix_troubleshooting.md#library-not-loaded-nixstore-from-a-binary-that-used-to-work) in the troubleshooting guide.
## Automatic Activation with direnv
@@ -233,6 +170,7 @@ The repository already ships an `.envrc` at its root that activates the Nix flak
1. [Install direnv](https://direnv.net/docs/installation.html) and [hook it into your shell](https://direnv.net/docs/hook.html) (bash, zsh, fish, …). Installing [nix-direnv](https://github.com/nix-community/nix-direnv) as well is recommended: it caches the shell so that activation is near-instant after the first run.
2. Run `direnv allow` once in the repository root. direnv will then load (and reload) the Nix development shell automatically whenever you enter the directory.
<!-- prettier-ignore -->
> [!NOTE]
> direnv only caches the `.direnv` directory (already listed in `.gitignore`); no other repository files are affected.
@@ -242,13 +180,8 @@ To update `flake.lock` to the latest revision use `nix flake update` command.
## Tooling snapshots
The tool versions in each Nix environment are recorded in
[`nix/check-tools/`](../../nix/check-tools) and verified by CI. If you change the
environment (bump the CI image tag, update `flake.lock`, or edit the tool list in
`bin/check-tools.sh`), CI fails until you regenerate and commit the affected
snapshot — see [`nix/check-tools/README.md`](../../nix/check-tools/README.md).
The tool versions in each Nix environment are recorded in [`nix/check-tools/`](../../nix/check-tools) and verified by CI. If you change the environment (bump the CI image tag, update `flake.lock`, or edit the tool list in `bin/check-tools.sh`), CI fails until you regenerate and commit the affected snapshot — see [`nix/check-tools/README.md`](../../nix/check-tools/README.md).
## Troubleshooting
See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues,
such as `nix develop` failing inside Git worktrees.
See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues, such as `nix develop` failing inside Git worktrees.

View File

@@ -1,7 +1,6 @@
# Troubleshooting Nix problems
Common issues encountered when using the [Nix development shell](./nix.md), and
how to resolve them.
Common issues encountered when using the [Nix development shell](./nix.md), and how to resolve them.
## `command not found: nix` after a macOS update
@@ -12,8 +11,7 @@ $ nix develop
zsh: command not found: nix
```
then Nix is almost certainly still installed — only the shell hook that puts it
on your `PATH` is gone. Confirm that first:
then Nix is almost certainly still installed — only the shell hook that puts it on your `PATH` is gone. Confirm that first:
```bash
ls -l /nix/var/nix/profiles/default/bin/nix
@@ -23,8 +21,7 @@ If that exists, the installation is fine and this is purely a `PATH` problem.
### Why it happens
The installer does not touch your dotfiles. Instead it sources a setup script
from the Nix store by editing **system-wide** rc files:
The installer does not touch your dotfiles. Instead it sources a setup script from the Nix store by editing **system-wide** rc files:
| Shell | File the installer edits |
| ----- | ------------------------------------- |
@@ -32,17 +29,13 @@ from the Nix store by editing **system-wide** rc files:
| zsh | `/etc/zshrc` |
| fish | `$__fish_sysconf_dir/conf.d/nix.fish` |
macOS manages `/etc/zshrc`, so an OS update can replace it with the vendor copy
and silently drop the Nix block. `/etc/bashrc` and the fish file usually survive,
which is why the breakage often shows up in zsh only. You can verify this by
diffing against the backup the installer left behind:
macOS manages `/etc/zshrc`, so an OS update can replace it with the vendor copy and silently drop the Nix block. `/etc/bashrc` and the fish file usually survive, which is why the breakage often shows up in zsh only. You can verify this by diffing against the backup the installer left behind:
```bash
diff /etc/zshrc /etc/zshrc.backup-before-nix
```
If they are identical, the Nix snippet was wiped. This is upstream issue
[NixOS/nix#3616](https://github.com/NixOS/nix/issues/3616).
If they are identical, the Nix snippet was wiped. This is upstream issue [NixOS/nix#3616](https://github.com/NixOS/nix/issues/3616).
### Fix
@@ -52,8 +45,7 @@ To unblock the current shell:
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
```
For a permanent fix, add the snippet to your **user** rc file rather than
restoring `/etc/zshrc` — user dotfiles are not clobbered by OS updates:
For a permanent fix, add the snippet to your **user** rc file rather than restoring `/etc/zshrc` — user dotfiles are not clobbered by OS updates:
```bash
cat >>~/.zshrc <<'EOF'
@@ -66,14 +58,11 @@ fi
EOF
```
The scripts guard against double-sourcing via `__ETC_PROFILE_NIX_SOURCED`, so
this is safe even if a system-wide hook is later restored.
The scripts guard against double-sourcing via `__ETC_PROFILE_NIX_SOURCED`, so this is safe even if a system-wide hook is later restored.
<!-- prettier-ignore -->
> [!NOTE]
> `/etc/zshrc` and `~/.zshrc` are only read by **interactive** zsh. If the
> snippet is present but `zsh -c '…'`, a script, or an IDE terminal still can't
> find `nix`, that shell is non-interactive — put the snippet in `~/.zshenv`
> instead.
> `/etc/zshrc` and `~/.zshrc` are only read by **interactive** zsh. If the snippet is present but `zsh -c '…'`, a script, or an IDE terminal still can't find `nix`, that shell is non-interactive — put the snippet in `~/.zshenv` instead.
## Git worktrees
@@ -86,56 +75,39 @@ error:
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.
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.
<!-- prettier-ignore -->
> [!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.
> 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
- 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`.
- 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:
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
```
<!-- prettier-ignore -->
> [!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.)
> `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.
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.
## `wint_t` / `uint32_t` errors from the Nix libc++ headers
A build that mixes the Nix toolchain with the system SDK fails in libc++ itself,
with errors that look nothing like your code:
A build that mixes the Nix toolchain with the system SDK fails in libc++ itself, with errors that look nothing like your code:
```
/nix/store/...-libcxx-.../include/c++/v1/cwchar:136:9: error: target of using declaration conflicts with declaration already in scope
@@ -145,41 +117,31 @@ with errors that look nothing like your code:
error: use of undeclared identifier 'UINT32_C'
```
The give-away is the second path: Nix's libc++ headers are being combined with
the **Xcode Command Line Tools** SDK instead of the Nix one.
The give-away is the second path: Nix's libc++ headers are being combined with the **Xcode Command Line Tools** SDK instead of the Nix one.
### Why it happens
`SDKROOT` and `DEVELOPER_DIR` are what point the toolchain at the Nix SDK, and
they are not baked into the compiler — a dev shell gets them from the
`apple-sdk` setup hook. CMake, finding neither, asks `xcrun`, which answers with
the system SDK. Nix's `libc++` and Apple's headers then declare the same types
twice.
`SDKROOT` and `DEVELOPER_DIR` are what point the toolchain at the Nix SDK, and they are not baked into the compiler — a dev shell gets them from the `apple-sdk` setup hook. CMake, finding neither, asks `xcrun`, which answers with the system SDK. Nix's `libc++` and Apple's headers then declare the same types twice.
### Fix
Run the build from inside the dev shell (`nix develop`), or from an environment
that exports both variables. To confirm which SDK a configured build is using:
Run the build from inside the dev shell (`nix develop`), or from an environment that exports both variables. To confirm which SDK a configured build is using:
```bash
grep -o '\-isysroot [^ ]*' build/compile_commands.json | sort -u
```
It should print a `/nix/store/...-apple-sdk-*` path. If it prints
`/Library/Developer/CommandLineTools/...`, re-configure from within the shell —
CMake caches the sysroot, so an existing `build/` directory keeps the wrong one.
It should print a `/nix/store/...-apple-sdk-*` path. If it prints `/Library/Developer/CommandLineTools/...`, re-configure from within the shell — CMake caches the sysroot, so an existing `build/` directory keeps the wrong one.
## `Library not loaded: /nix/store/…` from a binary that used to work
A binary stops starting after a `nix flake update`, or after
`nix-collect-garbage` removes the paths the previous toolchain used:
A binary stops starting after a `nix flake update`, or after `nix-collect-garbage` removes the paths the previous toolchain used:
```
dyld[57271]: Library not loaded: /nix/store/…-libresolv-93/lib/libresolv.9.dylib
```
[`bin/check-nix-store-refs.sh`](../../bin/check-nix-store-refs.sh) finds the same
thing without having to run anything, and names the file:
[`bin/check-nix-store-refs.sh`](../../bin/check-nix-store-refs.sh) finds the same thing without having to run anything, and names the file:
```
$ bin/check-nix-store-refs.sh ~/.conan2-nix
@@ -189,9 +151,7 @@ $ bin/check-nix-store-refs.sh ~/.conan2-nix
/Users/you/.conan2-nix: checked 135, skipped 2495, 1 with Nix store references.
```
Conan's cache folders are named after a truncated package name plus a hash, so
ask Conan which package the offending one belongs to — pass the folder holding
the hash, not the file itself:
Conan's cache folders are named after a truncated package name plus a hash, so ask Conan which package the offending one belongs to — pass the folder holding the hash, not the file itself:
```
$ conan cache ref ~/.conan2-nix/p/b/c-area24ded30c388c
@@ -200,17 +160,9 @@ c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650:dab5992496abe6d219defb7986ecbf367
### Why it happens
The binary records a store path that no longer exists. Nothing we build should:
see [Prebuilt packages](./nix.md#prebuilt-packages) for why, and
`libresolvSystemStub` in [`nix/darwin.nix`](../../nix/darwin.nix) for the one
dependency that needed help to comply.
The binary records a store path that no longer exists. Nothing we build should: see [Prebuilt packages](./nix.md#prebuilt-packages) for why, and `libresolvSystemStub` in [`nix/darwin.nix`](../../nix/darwin.nix) for the one dependency that needed help to comply.
A Conan package ID does not encode the nixpkgs revision, so a package built
before that stub existed stays in your local cache and keeps being reused. The
dev shell is also what tends to produce one: it is a slightly _less_ isolated
build environment than CI's, because `mkShell` puts every tool's headers and
libraries on the compiler's search path — which is how c-ares found the Nix
`libresolv` in the first place.
A Conan package ID does not encode the nixpkgs revision, so a package built before that stub existed stays in your local cache and keeps being reused. The dev shell is also what tends to produce one: it is a slightly _less_ isolated build environment than CI's, because `mkShell` puts every tool's headers and libraries on the compiler's search path — which is how c-ares found the Nix `libresolv` in the first place.
### Fix

View File

@@ -1,11 +1,10 @@
# Sanitizer Configuration for Xrpld
This document explains how to properly configure and run sanitizers (`AddressSanitizer`, `UndefinedBehaviorSanitizer`, `ThreadSanitizer`) with the xrpld project.
Corresponding suppression files are located in the `sanitizers/suppressions` directory.
This document explains how to properly configure and run sanitizers (`AddressSanitizer`, `UndefinedBehaviorSanitizer`, `ThreadSanitizer`) with the xrpld project. Corresponding suppression files are located in the `sanitizers/suppressions` directory.
<!-- prettier-ignore -->
> [!CAUTION]
> Do not mix Address and Thread sanitizers - they are incompatible.
> Also, we don't yet support MSVC sanitizers, so this is only for Clang/GCC builds.
> Do not mix Address and Thread sanitizers - they are incompatible. Also, we don't yet support MSVC sanitizers, so this is only for Clang/GCC builds.
- [Sanitizer Configuration for Xrpld](#sanitizer-configuration-for-xrpld)
- [Building with Sanitizers](#building-with-sanitizers)
@@ -35,10 +34,10 @@ Corresponding suppression files are located in the `sanitizers/suppressions` dir
Follow the same instructions as mentioned in [BUILD.md](../../BUILD.md) but with the following changes:
1. Make sure you have a clean build directory.
2. Set the `SANITIZERS` environment variable before calling `conan install`. Only set it once.
Example: `export SANITIZERS=address,undefinedbehavior`
2. Set the `SANITIZERS` environment variable before calling `conan install`. Only set it once. Example: `export SANITIZERS=address,undefinedbehavior`
3. Use `--profile:all sanitizers` with Conan to build dependencies with sanitizer instrumentation.
<!-- prettier-ignore -->
> [!NOTE]
> Building with sanitizer-instrumented dependencies is slower but produces fewer false positives.

View File

@@ -2,277 +2,116 @@
**This section is a work in progress!!**
Consensus is the task of reaching agreement within a distributed system in the
presence of faulty or even malicious participants. This document outlines the
[XRP Ledger Consensus Algorithm](https://arxiv.org/abs/1802.07242)
as implemented in [xrpld](https://github.com/XRPLF/rippled), but
focuses on its utility as a generic consensus algorithm independent of the
detailed mechanics of the XRPL consensus Ledger. Most notably, the algorithm
does not require fully synchronous communication between all nodes in the
network, or even a fixed network topology, but instead achieves consensus via
collectively trusted subnetworks.
Consensus is the task of reaching agreement within a distributed system in the presence of faulty or even malicious participants. This document outlines the [XRP Ledger Consensus Algorithm](https://arxiv.org/abs/1802.07242) as implemented in [xrpld](https://github.com/XRPLF/rippled), but focuses on its utility as a generic consensus algorithm independent of the detailed mechanics of the XRPL consensus Ledger. Most notably, the algorithm does not require fully synchronous communication between all nodes in the network, or even a fixed network topology, but instead achieves consensus via collectively trusted subnetworks.
## Distributed Agreement
A challenge for distributed systems is reaching agreement on changes in shared
state. For the XRPL network, the shared state is the current ledger--account
information, account balances, order books and other financial data. We will
refer to shared distributed state as a /ledger/ throughout the remainder of this
document.
A challenge for distributed systems is reaching agreement on changes in shared state. For the XRPL network, the shared state is the current ledger--account information, account balances, order books and other financial data. We will refer to shared distributed state as a /ledger/ throughout the remainder of this document.
![Ledger Chain](images/consensus/ledger_chain.png "Ledger Chain")
As shown above, new ledgers are made by applying a set of transactions to the
prior ledger. For the XRPL network, transactions include payments,
modification of account settings, updates to offers and more.
As shown above, new ledgers are made by applying a set of transactions to the prior ledger. For the XRPL network, transactions include payments, modification of account settings, updates to offers and more.
In a centralized system, generating the next ledger is trivial since there is a
single unique arbiter of which transactions to include and how to apply them to
a ledger. For decentralized systems, participants must resolve disagreements on
the set of transactions to include, the order to apply those transactions, and
even the resulting ledger after applying the transactions. This is even more
difficult when some participants are faulty or malicious.
In a centralized system, generating the next ledger is trivial since there is a single unique arbiter of which transactions to include and how to apply them to a ledger. For decentralized systems, participants must resolve disagreements on the set of transactions to include, the order to apply those transactions, and even the resulting ledger after applying the transactions. This is even more difficult when some participants are faulty or malicious.
The XRPL network is a decentralized and **trust-full** network. Anyone is free
to join and participants are free to choose a subset of peers that are
collectively trusted to not collude in an attempt to defraud the participant.
Leveraging this network of trust, the XRPL algorithm has two main components.
The XRPL network is a decentralized and **trust-full** network. Anyone is free to join and participants are free to choose a subset of peers that are collectively trusted to not collude in an attempt to defraud the participant. Leveraging this network of trust, the XRPL algorithm has two main components.
- _Consensus_ in which network participants agree on the transactions to apply
to a prior ledger, based on the positions of their chosen peers.
- _Validation_ in which network participants agree on what ledger was
generated, based on the ledgers generated by chosen peers.
- _Consensus_ in which network participants agree on the transactions to apply to a prior ledger, based on the positions of their chosen peers.
- _Validation_ in which network participants agree on what ledger was generated, based on the ledgers generated by chosen peers.
These phases are continually repeated to process transactions submitted to the
network, generating successive ledgers and giving rise to the blockchain ledger
history depicted below. In this diagram, time is flowing to the right, but
links between ledgers point backward to the parent. Also note the alternate
Ledger 2 that was generated by some participants, but which failed validation
and was abandoned.
These phases are continually repeated to process transactions submitted to the network, generating successive ledgers and giving rise to the blockchain ledger history depicted below. In this diagram, time is flowing to the right, but links between ledgers point backward to the parent. Also note the alternate Ledger 2 that was generated by some participants, but which failed validation and was abandoned.
![Block Chain](images/consensus/block_chain.png "Block Chain")
The remainder of this section describes the Consensus and Validation algorithms
in more detail and is meant as a companion guide to understanding the generic
implementation in `xrpld`. The document **does not** discuss correctness,
fault-tolerance or liveness properties of the algorithms or the full details of
how they integrate within `xrpld` to support the XRPL consensus Ledger.
The remainder of this section describes the Consensus and Validation algorithms in more detail and is meant as a companion guide to understanding the generic implementation in `xrpld`. The document **does not** discuss correctness, fault-tolerance or liveness properties of the algorithms or the full details of how they integrate within `xrpld` to support the XRPL consensus Ledger.
## Consensus Overview
### Definitions
- The _ledger_ is the shared distributed state. Each ledger has a unique ID to
distinguish it from all other ledgers. During consensus, the _previous_,
_prior_ or _last-closed_ ledger is the most recent ledger seen by consensus
and is the basis upon which it will build the next ledger.
- A _transaction_ is an instruction for an atomic change in the ledger state. A
unique ID distinguishes a transaction from other transactions.
- A _transaction set_ is a set of transactions under consideration by consensus.
The goal of consensus is to reach agreement on this set. The generic
consensus algorithm does not rely on an ordering of transactions within the
set, nor does it specify how to apply a transaction set to a ledger to
generate a new ledger. A unique ID distinguishes a set of transactions from
all other sets of transactions.
- A _node_ is one of the distributed actors running the consensus algorithm. It
has a unique ID to distinguish it from all other nodes.
- A _peer_ of a node is another node that it has chosen to follow and which it
believes will not collude with other chosen peers. The choice of peers is not
symmetric, since participants can decide on their chosen sets independently.
- A /position/ is the current belief of the next ledger's transaction set and
close time. Position can refer to the node's own position or the position of a
peer.
- A _proposal_ is one of a sequence of positions a node shares during consensus.
An initial proposal contains the starting position taken by a node before it
considers any peer positions. If a node subsequently updates its position in
response to its peers, it will issue an updated proposal. A proposal is
uniquely identified by the ID of the proposing node, the ID of the position
taken, the ID of the prior ledger the proposal is for, and the sequence number
of the proposal.
- A _dispute_ is a transaction that is either not part of a node's position or
not in a peer's position. During consensus, the node will add or remove
disputed transactions from its position based on that transaction's support
amongst its peers.
- The _ledger_ is the shared distributed state. Each ledger has a unique ID to distinguish it from all other ledgers. During consensus, the _previous_, _prior_ or _last-closed_ ledger is the most recent ledger seen by consensus and is the basis upon which it will build the next ledger.
- A _transaction_ is an instruction for an atomic change in the ledger state. A unique ID distinguishes a transaction from other transactions.
- A _transaction set_ is a set of transactions under consideration by consensus. The goal of consensus is to reach agreement on this set. The generic consensus algorithm does not rely on an ordering of transactions within the set, nor does it specify how to apply a transaction set to a ledger to generate a new ledger. A unique ID distinguishes a set of transactions from all other sets of transactions.
- A _node_ is one of the distributed actors running the consensus algorithm. It has a unique ID to distinguish it from all other nodes.
- A _peer_ of a node is another node that it has chosen to follow and which it believes will not collude with other chosen peers. The choice of peers is not symmetric, since participants can decide on their chosen sets independently.
- A /position/ is the current belief of the next ledger's transaction set and close time. Position can refer to the node's own position or the position of a peer.
- A _proposal_ is one of a sequence of positions a node shares during consensus. An initial proposal contains the starting position taken by a node before it considers any peer positions. If a node subsequently updates its position in response to its peers, it will issue an updated proposal. A proposal is uniquely identified by the ID of the proposing node, the ID of the position taken, the ID of the prior ledger the proposal is for, and the sequence number of the proposal.
- A _dispute_ is a transaction that is either not part of a node's position or not in a peer's position. During consensus, the node will add or remove disputed transactions from its position based on that transaction's support amongst its peers.
Note that most types have an ID as a lightweight identifier of instances of that
type. Consensus often operates on the IDs directly since the underlying type is
potentially expensive to share over the network. For example, proposal's only
contain the ID of the position of a peer. Since many peers likely have the same
position, this reduces the need to send the full transaction set multiple times.
Instead, a node can request the transaction set from the network if necessary.
Note that most types have an ID as a lightweight identifier of instances of that type. Consensus often operates on the IDs directly since the underlying type is potentially expensive to share over the network. For example, proposal's only contain the ID of the position of a peer. Since many peers likely have the same position, this reduces the need to send the full transaction set multiple times. Instead, a node can request the transaction set from the network if necessary.
### Overview
![Consensus Overview](images/consensus/consensus_overview.png "Consensus Overview")
The diagram above is an overview of the consensus process from the perspective
of a single participant. Recall that during a single consensus round, a node is
trying to agree with its peers on which transactions to apply to its prior
ledger when generating the next ledger. It also attempts to agree on the
[network time when the ledger closed](#effective_close_time). There are
3 main phases to a consensus round:
The diagram above is an overview of the consensus process from the perspective of a single participant. Recall that during a single consensus round, a node is trying to agree with its peers on which transactions to apply to its prior ledger when generating the next ledger. It also attempts to agree on the [network time when the ledger closed](#effective_close_time). There are 3 main phases to a consensus round:
- A call to `startRound` places the node in the `Open` phase. In this phase,
the node is waiting for transactions to include in its open ledger.
- At some point, the node will `Close` the open ledger and transition to the
`Establish` phase. In this phase, the node shares/receives peer proposals on
which transactions should be accepted in the closed ledger.
- At some point, the node determines it has reached consensus with its peers on
which transactions to include. It transitions to the `Accept` phase. In this
phase, the node works on applying the transactions to the prior ledger to
generate a new closed ledger. Once the new ledger is completed, the node shares
the validated ledger hash with the network and makes a call to `startRound` to
start the cycle again for the next ledger.
- A call to `startRound` places the node in the `Open` phase. In this phase, the node is waiting for transactions to include in its open ledger.
- At some point, the node will `Close` the open ledger and transition to the `Establish` phase. In this phase, the node shares/receives peer proposals on which transactions should be accepted in the closed ledger.
- At some point, the node determines it has reached consensus with its peers on which transactions to include. It transitions to the `Accept` phase. In this phase, the node works on applying the transactions to the prior ledger to generate a new closed ledger. Once the new ledger is completed, the node shares the validated ledger hash with the network and makes a call to `startRound` to start the cycle again for the next ledger.
Throughout, a heartbeat timer calls `timerEntry` at a regular frequency to drive
the process forward. Although the `startRound` call occurs at arbitrary times
based on when the initial round began and the time it takes to apply
transactions, the transitions from `Open` to `Establish` and `Establish` to
`Accept` only occur during calls to `timerEntry`. Similarly, transactions can
arrive at arbitrary times, independent of the heartbeat timer. Transactions
received after the `Open` to `Close` transition and not part of peer proposals
won't be considered until the next consensus round. They are represented above
by the light green triangles.
Throughout, a heartbeat timer calls `timerEntry` at a regular frequency to drive the process forward. Although the `startRound` call occurs at arbitrary times based on when the initial round began and the time it takes to apply transactions, the transitions from `Open` to `Establish` and `Establish` to `Accept` only occur during calls to `timerEntry`. Similarly, transactions can arrive at arbitrary times, independent of the heartbeat timer. Transactions received after the `Open` to `Close` transition and not part of peer proposals won't be considered until the next consensus round. They are represented above by the light green triangles.
Peer proposals are issued by a node during a `timerEntry` call, but since peers
do not synchronize `timerEntry` calls, they are received by other peers at
arbitrary times. Peer proposals are only considered if received prior to the
`Establish` to `Accept` transition, and only if the peer is working on the same
prior ledger. Peer proposals received after consensus is reached will not be
meaningful and are represented above by the circle with the X in it. Only
proposals from chosen peers are considered.
Peer proposals are issued by a node during a `timerEntry` call, but since peers do not synchronize `timerEntry` calls, they are received by other peers at arbitrary times. Peer proposals are only considered if received prior to the `Establish` to `Accept` transition, and only if the peer is working on the same prior ledger. Peer proposals received after consensus is reached will not be meaningful and are represented above by the circle with the X in it. Only proposals from chosen peers are considered.
### Effective Close Time ### {#effective_close_time}
In addition to agreeing on a transaction set, each consensus round tries to
agree on the time the ledger closed. Each node calculates its own close time
when it closes the open ledger. This exact close time is rounded to the nearest
multiple of the current _effective close time resolution_. It is this
_effective close time_ that nodes seek to agree on. This allows servers to
derive a common time for a ledger without the need for perfectly synchronized
clocks. As depicted below, the 3 pink arrows represent exact close times from 3
consensus nodes that round to the same effective close time given the current
resolution. The purple arrow represents a peer whose estimate rounds to a
different effective close time given the current resolution.
In addition to agreeing on a transaction set, each consensus round tries to agree on the time the ledger closed. Each node calculates its own close time when it closes the open ledger. This exact close time is rounded to the nearest multiple of the current _effective close time resolution_. It is this _effective close time_ that nodes seek to agree on. This allows servers to derive a common time for a ledger without the need for perfectly synchronized clocks. As depicted below, the 3 pink arrows represent exact close times from 3 consensus nodes that round to the same effective close time given the current resolution. The purple arrow represents a peer whose estimate rounds to a different effective close time given the current resolution.
![Effective Close Time](images/consensus/EffCloseTime.png "Effective Close Time")
The effective close time is part of the node's position and is shared with peers
in its proposals. Just like the position on the consensus transaction set, a
node will update its close time position in response to its peers' effective
close time positions. Peers can agree to disagree on the close time, in which
case the effective close time is taken as 1 second past the prior close.
The effective close time is part of the node's position and is shared with peers in its proposals. Just like the position on the consensus transaction set, a node will update its close time position in response to its peers' effective close time positions. Peers can agree to disagree on the close time, in which case the effective close time is taken as 1 second past the prior close.
The close time resolution is itself dynamic, decreasing (coarser) resolution in
subsequent consensus rounds if nodes are unable to reach consensus on an
effective close time and increasing (finer) resolution if nodes consistently
reach close time consensus.
The close time resolution is itself dynamic, decreasing (coarser) resolution in subsequent consensus rounds if nodes are unable to reach consensus on an effective close time and increasing (finer) resolution if nodes consistently reach close time consensus.
### Modes
Internally, a node operates under one of the following consensus modes. Either
of the first two modes may be chosen when a consensus round starts.
Internally, a node operates under one of the following consensus modes. Either of the first two modes may be chosen when a consensus round starts.
- _Proposing_ indicates the node is a full-fledged consensus participant. It
takes on positions and sends proposals to its peers.
- _Observing_ indicates the node is a passive consensus participant. It
maintains a position internally, but does not propose that position to its
peers. Instead, it receives peer proposals and updates its position
to track the majority of its peers. This may be preferred if the node is only
being used to track the state of the network or during a start-up phase while
it is still synchronizing with the network.
- _Proposing_ indicates the node is a full-fledged consensus participant. It takes on positions and sends proposals to its peers.
- _Observing_ indicates the node is a passive consensus participant. It maintains a position internally, but does not propose that position to its peers. Instead, it receives peer proposals and updates its position to track the majority of its peers. This may be preferred if the node is only being used to track the state of the network or during a start-up phase while it is still synchronizing with the network.
The other two modes are set internally during the consensus round when the node
believes it is no longer working on the dominant ledger chain based on peer
validations. It checks this on every call to `timerEntry`.
The other two modes are set internally during the consensus round when the node believes it is no longer working on the dominant ledger chain based on peer validations. It checks this on every call to `timerEntry`.
- _Wrong Ledger_ indicates the node is not working on the correct prior ledger
and does not have it available. It requests that ledger from the network, but
continues to work towards consensus this round while waiting. If it had been
_proposing_, it will send a special "bow-out" proposal to its peers to indicate
its change in mode for the rest of this round. For the duration of the round,
it defers to peer positions for determining the consensus outcome as if it
were just _observing_.
- _Switch Ledger_ indicates that the node has acquired the correct prior ledger
from the network. Although it now has the correct prior ledger, the fact that
it had the wrong one at some point during this round means it is likely behind
and should defer to peer positions for determining the consensus outcome.
- _Wrong Ledger_ indicates the node is not working on the correct prior ledger and does not have it available. It requests that ledger from the network, but continues to work towards consensus this round while waiting. If it had been _proposing_, it will send a special "bow-out" proposal to its peers to indicate its change in mode for the rest of this round. For the duration of the round, it defers to peer positions for determining the consensus outcome as if it were just _observing_.
- _Switch Ledger_ indicates that the node has acquired the correct prior ledger from the network. Although it now has the correct prior ledger, the fact that it had the wrong one at some point during this round means it is likely behind and should defer to peer positions for determining the consensus outcome.
![Consensus Modes](images/consensus/consensus_modes.png "Consensus Modes")
Once either wrong ledger or switch ledger are reached, the node cannot
return to proposing or observing until the next consensus round. However,
the node could change its view of the correct prior ledger, so going from
switch ledger to wrong ledger and back again is possible.
Once either wrong ledger or switch ledger are reached, the node cannot return to proposing or observing until the next consensus round. However, the node could change its view of the correct prior ledger, so going from switch ledger to wrong ledger and back again is possible.
The distinction between the wrong and switched ledger modes arises because a
ledger's unique identifier may be known by a node before the ledger itself. This
reflects that fact that the data corresponding to a ledger may be large and take
time to share over the network, whereas the smaller ID could be shared in a peer
validation much more quickly. Distinguishing the two states allows the node to
decide how best to generate the next ledger once it declares consensus.
The distinction between the wrong and switched ledger modes arises because a ledger's unique identifier may be known by a node before the ledger itself. This reflects that fact that the data corresponding to a ledger may be large and take time to share over the network, whereas the smaller ID could be shared in a peer validation much more quickly. Distinguishing the two states allows the node to decide how best to generate the next ledger once it declares consensus.
### Phases
As depicted in the overview diagram, consensus is best viewed as a progression
through 3 phases. There are 4 public methods of the generic consensus algorithm
that determine this progression
As depicted in the overview diagram, consensus is best viewed as a progression through 3 phases. There are 4 public methods of the generic consensus algorithm that determine this progression
- `startRound` begins a consensus round.
- `timerEntry` is called at a regular frequency (`LEDGER_MIN_CLOSE`) and is the
only call to consensus that can change the phase from `Open` to `Establish`
or `Accept`.
- `peerProposal` is called whenever a peer proposal is received and is what
allows a node to update its position in a subsequent `timerEntry` call.
- `gotTxSet` is called when a transaction set is received from the network. This
is typically in response to a prior request from the node to acquire the
transaction set corresponding to a disagreeing peer's position.
- `timerEntry` is called at a regular frequency (`LEDGER_MIN_CLOSE`) and is the only call to consensus that can change the phase from `Open` to `Establish` or `Accept`.
- `peerProposal` is called whenever a peer proposal is received and is what allows a node to update its position in a subsequent `timerEntry` call.
- `gotTxSet` is called when a transaction set is received from the network. This is typically in response to a prior request from the node to acquire the transaction set corresponding to a disagreeing peer's position.
The following subsections describe each consensus phase in more detail and what
actions are taken in response to these calls.
The following subsections describe each consensus phase in more detail and what actions are taken in response to these calls.
#### Open
The `Open` phase is a quiescent period to allow transactions to build up in the
node's open ledger. The duration is a trade-off between latency and throughput.
A shorter window reduces the latency to generating the next ledger, but also
reduces transaction throughput due to fewer transactions accepted into the
ledger.
The `Open` phase is a quiescent period to allow transactions to build up in the node's open ledger. The duration is a trade-off between latency and throughput. A shorter window reduces the latency to generating the next ledger, but also reduces transaction throughput due to fewer transactions accepted into the ledger.
A call to `startRound` would forcibly begin the next consensus round, skipping
completion of the current round. This is not expected during normal operation.
Calls to `peerProposal` or `gotTxSet` simply store the proposal or transaction
set for use in the coming `Establish` phase.
A call to `startRound` would forcibly begin the next consensus round, skipping completion of the current round. This is not expected during normal operation. Calls to `peerProposal` or `gotTxSet` simply store the proposal or transaction set for use in the coming `Establish` phase.
A call to `timerEntry` first checks that the node is working on the correct
prior ledger. If not, it will update the mode and request the correct ledger.
Otherwise, the node checks whether to switch to the `Establish` phase and close
the ledger.
A call to `timerEntry` first checks that the node is working on the correct prior ledger. If not, it will update the mode and request the correct ledger. Otherwise, the node checks whether to switch to the `Establish` phase and close the ledger.
##### Ledger Close
Under normal circumstances, the open ledger period ends when one of the following
is true
Under normal circumstances, the open ledger period ends when one of the following is true
- if there are transactions in the open ledger and more than `LEDGER_MIN_CLOSE`
have elapsed. This is the typical behavior.
- if there are no open transactions and a suitably longer idle interval has
elapsed. This increases the opportunity to get some transaction into
the next ledger and avoids doing useless work closing an empty ledger.
- if more than half the number of prior round peers have already closed or finished
this round. This indicates the node is falling behind and needs to catch up.
- if there are transactions in the open ledger and more than `LEDGER_MIN_CLOSE` have elapsed. This is the typical behavior.
- if there are no open transactions and a suitably longer idle interval has elapsed. This increases the opportunity to get some transaction into the next ledger and avoids doing useless work closing an empty ledger.
- if more than half the number of prior round peers have already closed or finished this round. This indicates the node is falling behind and needs to catch up.
When closing the ledger, the node takes its initial position based on the
transactions in the open ledger and uses the current time as
its initial close time estimate. If in the proposing mode, the node shares its
initial position with peers. Now that the node has taken a position, it will
consider any peer positions for this round that arrived earlier. The node
generates disputed transactions for each transaction not in common with a peer's
position. The node also records the vote of each peer for each disputed
transaction.
When closing the ledger, the node takes its initial position based on the transactions in the open ledger and uses the current time as its initial close time estimate. If in the proposing mode, the node shares its initial position with peers. Now that the node has taken a position, it will consider any peer positions for this round that arrived earlier. The node generates disputed transactions for each transaction not in common with a peer's position. The node also records the vote of each peer for each disputed transaction.
In the example below, we suppose our node has closed with transactions 1,2 and 3. It creates disputes
for transactions 2,3 and 4, since at least one peer position differs on each.
In the example below, we suppose our node has closed with transactions 1,2 and 3. It creates disputes for transactions 2,3 and 4, since at least one peer position differs on each.
##### disputes ##### {#disputes_image}
@@ -280,119 +119,55 @@ for transactions 2,3 and 4, since at least one peer position differs on each.
#### Establish
The establish phase is the active period of consensus in which the node
exchanges proposals with peers in an attempt to reach agreement on the consensus
transactions and effective close time.
The establish phase is the active period of consensus in which the node exchanges proposals with peers in an attempt to reach agreement on the consensus transactions and effective close time.
A call to `startRound` would forcibly begin the next consensus round, skipping
completion of the current round. This is not expected during normal operation.
Calls to `peerProposal` or `gotTxSet` that reflect new positions will generate
disputed transactions for any new disagreements and will update the peer's vote
for all disputed transactions.
A call to `startRound` would forcibly begin the next consensus round, skipping completion of the current round. This is not expected during normal operation. Calls to `peerProposal` or `gotTxSet` that reflect new positions will generate disputed transactions for any new disagreements and will update the peer's vote for all disputed transactions.
A call to `timerEntry` first checks that the node is working from the correct
prior ledger. If not, the node will update the mode and request the correct
ledger. Otherwise, the node updates the node's position and considers whether
to switch to the `Accepted` phase and declare consensus reached. However, at
least `LEDGER_MIN_CONSENSUS` time must have elapsed before doing either. This
allows peers an opportunity to take an initial position and share it.
A call to `timerEntry` first checks that the node is working from the correct prior ledger. If not, the node will update the mode and request the correct ledger. Otherwise, the node updates the node's position and considers whether to switch to the `Accepted` phase and declare consensus reached. However, at least `LEDGER_MIN_CONSENSUS` time must have elapsed before doing either. This allows peers an opportunity to take an initial position and share it.
##### Update Position
In order to achieve consensus, the node is looking for a transaction set that is
supported by a super-majority of peers. The node works towards this set by
adding or removing disputed transactions from its position based on an
increasing threshold for inclusion.
In order to achieve consensus, the node is looking for a transaction set that is supported by a super-majority of peers. The node works towards this set by adding or removing disputed transactions from its position based on an increasing threshold for inclusion.
![Threshold](images/consensus/threshold.png "Threshold")
By starting with a lower threshold, a node initially allows a wide set of
transactions into its position. If the establish round continues and the node is
"stuck", a higher threshold can focus on accepting transactions with the most
support. The constants that define the thresholds and durations at which the
thresholds change are given by `AV_XXX_CONSENSUS_PCT` and
`AV_XXX_CONSENSUS_TIME` respectively, where `XXX` is `INIT`,`MID`,`LATE` and
`STUCK`. The effective close time position is updated using the same
thresholds.
By starting with a lower threshold, a node initially allows a wide set of transactions into its position. If the establish round continues and the node is "stuck", a higher threshold can focus on accepting transactions with the most support. The constants that define the thresholds and durations at which the thresholds change are given by `AV_XXX_CONSENSUS_PCT` and `AV_XXX_CONSENSUS_TIME` respectively, where `XXX` is `INIT`,`MID`,`LATE` and `STUCK`. The effective close time position is updated using the same thresholds.
Given the [example disputes above](#disputes_image) and an initial threshold
of 50%, our node would retain its position since transaction 1 was not in
dispute and transactions 2 and 3 have 75% support. Since its position did not
change, it would not need to send a new proposal to peers. Peer C would not
change either. Peer A would add transaction 3 to its position and Peer B would
remove transaction 4 from its position; both would then send an updated
position.
Given the [example disputes above](#disputes_image) and an initial threshold of 50%, our node would retain its position since transaction 1 was not in dispute and transactions 2 and 3 have 75% support. Since its position did not change, it would not need to send a new proposal to peers. Peer C would not change either. Peer A would add transaction 3 to its position and Peer B would remove transaction 4 from its position; both would then send an updated position.
Conversely, if the diagram reflected a later call to =timerEntry= that occurs in
the stuck region with a threshold of say 95%, our node would remove transactions
2 and 3 from its candidate set and send an updated position. Likewise, all the
other peers would end up with only transaction 1 in their position.
Conversely, if the diagram reflected a later call to =timerEntry= that occurs in the stuck region with a threshold of say 95%, our node would remove transactions 2 and 3 from its candidate set and send an updated position. Likewise, all the other peers would end up with only transaction 1 in their position.
Lastly, if our node were not in the proposing mode, it would not include its own
vote and just take the majority (>50%) position of its peers. In this example,
our node would maintain its position of transactions 1, 2 and 3.
Lastly, if our node were not in the proposing mode, it would not include its own vote and just take the majority (>50%) position of its peers. In this example, our node would maintain its position of transactions 1, 2 and 3.
##### Checking Consensus
After updating its position, the node checks for supermajority agreement with
its peers on its current position. This agreement is of the exact transaction
set, not just the support of individual transactions. That is, if our position
is a subset of a peer's position, that counts as a disagreement. Also recall
that effective close time agreement allows a supermajority of participants
agreeing to disagree.
After updating its position, the node checks for supermajority agreement with its peers on its current position. This agreement is of the exact transaction set, not just the support of individual transactions. That is, if our position is a subset of a peer's position, that counts as a disagreement. Also recall that effective close time agreement allows a supermajority of participants agreeing to disagree.
Consensus is declared when the following 3 clauses are true:
- `LEDGER_MIN_CONSENSUS` time has elapsed in the establish phase
- At least 75% of the prior round proposers have proposed OR this establish
phase is `LEDGER_MIN_CONSENSUS` longer than the last round's establish phase
- At least 75% of the prior round proposers have proposed OR this establish phase is `LEDGER_MIN_CONSENSUS` longer than the last round's establish phase
- `minimumConsensusPercentage` of ourself and our peers share the same position
The middle condition ensures slower peers have a chance to share positions, but
prevents waiting too long on peers that have disconnected. Additionally, a node
can declare that consensus has moved on if `minimumConsensusPercentage` peers
have sent validations and moved on to the next ledger. This outcome indicates
the node has fallen behind its peers and needs to catch up.
The middle condition ensures slower peers have a chance to share positions, but prevents waiting too long on peers that have disconnected. Additionally, a node can declare that consensus has moved on if `minimumConsensusPercentage` peers have sent validations and moved on to the next ledger. This outcome indicates the node has fallen behind its peers and needs to catch up.
If a node is not proposing, it does not include its own position when
calculating the percent of agreeing participants but otherwise follows the above
logic.
If a node is not proposing, it does not include its own position when calculating the percent of agreeing participants but otherwise follows the above logic.
##### Accepting Consensus
Once consensus is reached (or moved on), the node switches to the `Accept` phase
and signals to the implementing code that the round is complete. That code is
responsible for using the consensus transaction set to generate the next ledger
and calling `startRound` to begin the next round. The implementation has total
freedom on ordering transactions, deciding what to do if consensus moved on,
determining whether to retry or abandon local transactions that did not make the
consensus set and updating any internal state based on the consensus progress.
Once consensus is reached (or moved on), the node switches to the `Accept` phase and signals to the implementing code that the round is complete. That code is responsible for using the consensus transaction set to generate the next ledger and calling `startRound` to begin the next round. The implementation has total freedom on ordering transactions, deciding what to do if consensus moved on, determining whether to retry or abandon local transactions that did not make the consensus set and updating any internal state based on the consensus progress.
#### Accept
The `Accept` phase is the terminal phase of the consensus algorithm. Calls to
`timerEntry`, `peerProposal` and `gotTxSet` will not change the internal
consensus state while in the accept phase. The expectation is that the
application specific code is working to generate the new ledger based on the
consensus outcome. Once complete, that code should make a call to `startRound`
to kick off the next consensus round. The `startRound` call includes the new
prior ledger, prior ledger ID and whether the round should begin in the
proposing or observing mode. After setting some initial state, the phase
transitions to `Open`. The node will also check if the provided prior ledger
and ID are correct, updating the mode and requesting the proper ledger from the
network if necessary.
The `Accept` phase is the terminal phase of the consensus algorithm. Calls to `timerEntry`, `peerProposal` and `gotTxSet` will not change the internal consensus state while in the accept phase. The expectation is that the application specific code is working to generate the new ledger based on the consensus outcome. Once complete, that code should make a call to `startRound` to kick off the next consensus round. The `startRound` call includes the new prior ledger, prior ledger ID and whether the round should begin in the proposing or observing mode. After setting some initial state, the phase transitions to `Open`. The node will also check if the provided prior ledger and ID are correct, updating the mode and requesting the proper ledger from the network if necessary.
## Consensus Type Requirements
The consensus type requirements are given below as minimal implementation stubs.
Actual implementations would augment these stubs with members appropriate for
managing the details of transactions and ledgers within the larger application
framework.
The consensus type requirements are given below as minimal implementation stubs. Actual implementations would augment these stubs with members appropriate for managing the details of transactions and ledgers within the larger application framework.
### Transaction
The transaction type `Tx` encapsulates a single transaction under consideration
by consensus.
The transaction type `Tx` encapsulates a single transaction under consideration by consensus.
```{.cpp}
struct Tx
@@ -406,10 +181,7 @@ struct Tx
### Transaction Set
The transaction set type `TxSet` represents a set of `Tx`s that are collectively
under consideration by consensus. A `TxSet` can be compared against other `TxSet`s
(typically from peers) and can be modified to add or remove transactions via
the mutable subtype.
The transaction set type `TxSet` represents a set of `Tx`s that are collectively under consideration by consensus. A `TxSet` can be compared against other `TxSet`s (typically from peers) and can be modified to add or remove transactions via the mutable subtype.
```{.cpp}
struct TxSet
@@ -446,12 +218,7 @@ struct TxSet
### Ledger
The `Ledger` type represents the state shared amongst the
distributed participants. Notice that the details of how the next ledger is
generated from the prior ledger and the consensus accepted transaction set is
not part of the interface. Within the generic code, this type is primarily used
to know that peers are working on the same tip of the ledger chain and to
provide some basic timing data for consensus.
The `Ledger` type represents the state shared amongst the distributed participants. Notice that the details of how the next ledger is generated from the prior ledger and the consensus accepted transaction set is not part of the interface. Within the generic code, this type is primarily used to know that peers are working on the same tip of the ledger chain and to provide some basic timing data for consensus.
```{.cpp}
struct Ledger
@@ -485,9 +252,7 @@ struct Ledger
### PeerProposal
The `PeerProposal` type represents the signed position taken
by a peer during consensus. The only type requirement is owning an instance of a
generic `ConsensusProposal`.
The `PeerProposal` type represents the signed position taken by a peer during consensus. The only type requirement is owning an instance of a generic `ConsensusProposal`.
```{.cpp}
// Represents our proposed position or a peer's proposed position
@@ -508,11 +273,7 @@ struct PeerPosition
### Generic Consensus Interface
The generic `Consensus` relies on `Adaptor` template class to implement a set
of helper functions that plug the consensus algorithm into a specific application.
The `Adaptor` class also defines the types above needed by the algorithm. Below
are excerpts of the generic consensus implementation and of helper types that will
interact with the concrete implementing class.
The generic `Consensus` relies on `Adaptor` template class to implement a set of helper functions that plug the consensus algorithm into a specific application. The `Adaptor` class also defines the types above needed by the algorithm. Below are excerpts of the generic consensus implementation and of helper types that will interact with the concrete implementing class.
```{.cpp}
// Represents a transaction under dispute this round
@@ -653,26 +414,11 @@ struct Adaptor
};
```
The implementing class hides many details of the peer communication
model from the generic code.
The implementing class hides many details of the peer communication model from the generic code.
- The `share` member functions are responsible for sharing the given type with a
node's peers, but are agnostic to the mechanism. Ideally, messages are delivered
faster than `LEDGER_GRANULARITY`.
- The generic code does not specify how transactions are submitted by clients,
propagated through the network or stored in the open ledger. Indeed, the open
ledger is only conceptual from the perspective of the generic code---the
initial position and transaction set are opaquely generated in a
`Consensus::Result` instance returned from the `onClose` callback.
- The calls to `acquireLedger` and `acquireTxSet` only have non-trivial return
if the ledger or transaction set of interest is available. The implementing
class is free to block while acquiring, or return the empty option while
servicing the request asynchronously. Due to legacy reasons, the two calls
are not symmetric. `acquireTxSet` requires the host application to call
`gotTxSet` when an asynchronous `acquire` completes. Conversely,
`acquireLedger` will be called again later by the consensus code if it still
desires the ledger with the hope that the asynchronous acquisition is
complete.
- The `share` member functions are responsible for sharing the given type with a node's peers, but are agnostic to the mechanism. Ideally, messages are delivered faster than `LEDGER_GRANULARITY`.
- The generic code does not specify how transactions are submitted by clients, propagated through the network or stored in the open ledger. Indeed, the open ledger is only conceptual from the perspective of the generic code---the initial position and transaction set are opaquely generated in a `Consensus::Result` instance returned from the `onClose` callback.
- The calls to `acquireLedger` and `acquireTxSet` only have non-trivial return if the ledger or transaction set of interest is available. The implementing class is free to block while acquiring, or return the empty option while servicing the request asynchronously. Due to legacy reasons, the two calls are not symmetric. `acquireTxSet` requires the host application to call `gotTxSet` when an asynchronous `acquire` completes. Conversely, `acquireLedger` will be called again later by the consensus code if it still desires the ledger with the hope that the asynchronous acquisition is complete.
## Validation

View File

@@ -1,29 +1,20 @@
# Installing xrpld 3.3.0 and earlier
<!-- prettier-ignore -->
> [!IMPORTANT]
> These instructions apply to xrpld 3.3.0 and earlier, published to
> repos.ripple.com.
> For later releases see [install.md](./install.md).
> These instructions apply to xrpld 3.3.0 and earlier, published to repos.ripple.com. For later releases see [install.md](./install.md).
This document contains instructions for installing xrpld.
The APT package manager is common on Debian-based Linux distributions like
Ubuntu,
while the YUM package manager is common on Red Hat-based Linux distributions
like CentOS.
Installing from source is an option for all platforms,
and the only supported option for installing custom builds.
This document contains instructions for installing xrpld. The APT package manager is common on Debian-based Linux distributions like Ubuntu, while the YUM package manager is common on Red Hat-based Linux distributions like CentOS. Installing from source is an option for all platforms, and the only supported option for installing custom builds.
## From source
From a source build, you can install xrpld and libxrpl using CMake's
`--install` mode:
From a source build, you can install xrpld and libxrpl using CMake's `--install` mode:
```
cmake --install . --prefix /opt/local
```
The default [prefix][1] is typically `/usr/local` on Linux and macOS and
`C:/Program Files/xrpld` on Windows.
The default [prefix][1] is typically `/usr/local` on Linux and macOS and `C:/Program Files/xrpld` on Windows.
[1]: https://cmake.org/cmake/help/latest/variable/CMAKE_INSTALL_PREFIX.html

View File

@@ -1,13 +1,10 @@
# Installing xrpld
<!-- prettier-ignore -->
> [!NOTE]
> These instructions apply to packages published from 2026-08-19 onwards.
> For xrpld 3.3.0 and earlier see [install-legacy.md](./install-legacy.md).
> These instructions apply to packages published from 2026-08-19 onwards. For xrpld 3.3.0 and earlier see [install-legacy.md](./install-legacy.md).
`xrpld` is published as DEB and RPM packages for 64-bit x86 Linux.
Use APT on Debian-based distributions such as Debian and Ubuntu,
and YUM on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux.
To build from source instead, see [BUILD.md](../BUILD.md).
`xrpld` is published as DEB and RPM packages for 64-bit x86 Linux. Use APT on Debian-based distributions such as Debian and Ubuntu, and YUM on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux. To build from source instead, see [BUILD.md](../BUILD.md).
## Release channels
@@ -20,13 +17,11 @@ Packages are published to four channels:
See [Publishing packages](../package/README.md#publishing-packages) for how channels are produced.
The instructions below use `stable`.
To follow another channel, replace `stable` with its name
wherever it appears in the repository configuration.
The instructions below use `stable`. To follow another channel, replace `stable` with its name wherever it appears in the repository configuration.
<!-- prettier-ignore -->
> [!WARNING]
> Channels other than `stable` may be broken at any time.
> Do not use them for production servers.
> Channels other than `stable` may be broken at any time. Do not use them for production servers.
## Install the xrpld package
@@ -103,8 +98,7 @@ wherever it appears in the repository configuration.
REPOFILE
```
`gpgcheck=1` verifies each package against the key above.
`repo_gpgcheck=1` verifies the repository metadata, which the server signs with the same key.
`gpgcheck=1` verifies each package against the key above. `repo_gpgcheck=1` verifies the repository metadata, which the server signs with the same key.
3. Install the `xrpld` package:
@@ -114,8 +108,7 @@ wherever it appears in the repository configuration.
## The xrpld service
Both package managers install a systemd unit and enable it, so `xrpld` starts on boot.
Check whether it is already running:
Both package managers install a systemd unit and enable it, so `xrpld` starts on boot. Check whether it is already running:
```bash
systemctl status xrpld.service
@@ -129,8 +122,7 @@ sudo systemctl start xrpld.service
### Optional: binding to privileged ports
To serve incoming API requests on port 80 or 443, grant the service the capability to bind them.
You must also update the config file's port settings.
To serve incoming API requests on port 80 or 443, grant the service the capability to bind them. You must also update the config file's port settings.
```bash
sudo install -d -m 0755 /etc/systemd/system/xrpld.service.d

View File

@@ -15,8 +15,7 @@ The module xrpl/basics should contain no dependencies on other modules.
- `std::list`
- For ordered containers with inserts and erases to the middle.
- For containers with iterators stable over insert and erase.
- Generally slower and bigger than `std::vector` or `std::deque` except for
those cases.
- Generally slower and bigger than `std::vector` or `std::deque` except for those cases.
- `std::set`
- For sorted containers.
@@ -26,9 +25,7 @@ The module xrpl/basics should contain no dependencies on other modules.
- For "small" sets, `std::set` might be faster and smaller.
- `xrpl::hardened_hash_set`
- For data sets where the key could be manipulated by an attacker
in an attempt to mount an algorithmic complexity attack: see
http://en.wikipedia.org/wiki/Algorithmic_complexity_attack
- For data sets where the key could be manipulated by an attacker in an attempt to mount an algorithmic complexity attack: see http://en.wikipedia.org/wiki/Algorithmic_complexity_attack
The following container is deprecated

View File

@@ -1,8 +1,3 @@
# Consensus
This directory contains the implementation of a
generic consensus algorithm. The implementation
follows a CRTP design, requiring client code to implement
specific functions and types to use consensus in their
application. The interface is undergoing refactoring and
is not yet finalized.
This directory contains the implementation of a generic consensus algorithm. The implementation follows a CRTP design, requiring client code to implement specific functions and types to use consensus in their application. The interface is undergoing refactoring and is not yet finalized.

View File

@@ -1,4 +1,3 @@
# SSLUtil
This module exposes the OpenSSL headers and provides utilities to
operate with OpenSSL / BIGNUM objects.
This module exposes the OpenSSL headers and provides utilities to operate with OpenSSL / BIGNUM objects.

View File

@@ -7,16 +7,11 @@
## Introduction
A `NodeObject` is a simple object that the Ledger uses to store entries. It is
comprised of a type, a hash and a blob. It can be uniquely
identified by the hash, which is a 256 bit hash of the blob. The blob is a
variable length block of serialized data. The type identifies what the blob
contains. The fields are as follows:
A `NodeObject` is a simple object that the Ledger uses to store entries. It is comprised of a type, a hash and a blob. It can be uniquely identified by the hash, which is a 256 bit hash of the blob. The blob is a variable length block of serialized data. The type identifies what the blob contains. The fields are as follows:
- `mType`
An enumeration that determines what the blob holds. There are four
different types of objects stored.
An enumeration that determines what the blob holds. There are four different types of objects stored.
- **ledger**
@@ -50,19 +45,11 @@ A blob containing the payload. Stored in the following format.
---
The `NodeStore` provides an interface that stores, in a persistent database, a
collection of NodeObjects that xrpld uses as its primary representation of
ledger entries. All ledger entries are stored as NodeObjects and as such, need
to be persisted between launches. If a NodeObject is accessed and is not in
memory, it will be retrieved from the database.
The `NodeStore` provides an interface that stores, in a persistent database, a collection of NodeObjects that xrpld uses as its primary representation of ledger entries. All ledger entries are stored as NodeObjects and as such, need to be persisted between launches. If a NodeObject is accessed and is not in memory, it will be retrieved from the database.
## Backend
The `NodeStore` implementation provides the `Backend` abstract interface,
which lets different key/value databases to be chosen at run-time. This allows
experimentation with different engines. Improvements in the performance of the
NodeStore are a constant area of research. The database can be specified in
the configuration file [node_db] section as follows.
The `NodeStore` implementation provides the `Backend` abstract interface, which lets different key/value databases to be chosen at run-time. This allows experimentation with different engines. Improvements in the performance of the NodeStore are a constant area of research. The database can be specified in the configuration file [node_db] section as follows.
One or more lines of key / value pairs
@@ -106,75 +93,30 @@ Choices for 'compression'
# Benchmarks
The `NodeStore.Timing` test is used to execute a set of read/write workloads to
compare current available nodestore backends. It can be executed with:
The `NodeStore.Timing` test is used to execute a set of read/write workloads to compare current available nodestore backends. It can be executed with:
```
$xrpld --unittest=NodeStoreTiming
```
It is also possible to use alternate DB config params by passing config strings
as `--unittest-arg`.
It is also possible to use alternate DB config params by passing config strings as `--unittest-arg`.
## Addendum
The discussion below refers to a `RocksDBQuick` backend that has since been
removed from the code as it was not working and not maintained. That backend
primarily used one of the several rocks `Optimize*` methods to setup the
majority of the DB options/params, whereas the primary RocksDB backend exposes
many of the available config options directly. The code for RocksDBQuick can be
found in versions of this repo 1.2 and earlier if you need to refer back to it.
The conclusions below date from about 2014 and may need revisiting based on
newer versions of RocksDB (TBD).
The discussion below refers to a `RocksDBQuick` backend that has since been removed from the code as it was not working and not maintained. That backend primarily used one of the several rocks `Optimize*` methods to setup the majority of the DB options/params, whereas the primary RocksDB backend exposes many of the available config options directly. The code for RocksDBQuick can be found in versions of this repo 1.2 and earlier if you need to refer back to it. The conclusions below date from about 2014 and may need revisiting based on newer versions of RocksDB (TBD).
## Discussion
RocksDBQuickFactory is intended to provide a testbed for comparing potential
rocksdb performance with the existing recommended configuration in xrpld.cfg.
Through various executions and profiling some conclusions are presented below.
RocksDBQuickFactory is intended to provide a testbed for comparing potential rocksdb performance with the existing recommended configuration in xrpld.cfg. Through various executions and profiling some conclusions are presented below.
- If the write ahead log is enabled, insert speed soon clogs up under load. The
BatchWriter class intends to stop this from blocking the main threads by queuing
up writes and running them in a separate thread. However, rocksdb already has
separate threads dedicated to flushing the memtable to disk and the memtable is
itself an in-memory queue. The result is two queues with a guarantee of
durability in between. However if the memtable was used as the sole queue and
the rocksdb::Flush() call was manually triggered at opportune moments, possibly
just after ledger close, then that would provide similar, but more predictable
guarantees. It would also remove an unneeded thread and unnecessary memory
usage. An alternative point of view is that because there will always be many
other xrpld instances running there is no need for such guarantees. The nodes
will always be available from another peer.
- If the write ahead log is enabled, insert speed soon clogs up under load. The BatchWriter class intends to stop this from blocking the main threads by queuing up writes and running them in a separate thread. However, rocksdb already has separate threads dedicated to flushing the memtable to disk and the memtable is itself an in-memory queue. The result is two queues with a guarantee of durability in between. However if the memtable was used as the sole queue and the rocksdb::Flush() call was manually triggered at opportune moments, possibly just after ledger close, then that would provide similar, but more predictable guarantees. It would also remove an unneeded thread and unnecessary memory usage. An alternative point of view is that because there will always be many other xrpld instances running there is no need for such guarantees. The nodes will always be available from another peer.
- Lookup in a block was previously using binary search. With xrpld's use case
it is highly unlikely that two adjacent key/values will ever be requested one
after the other. Therefore hash indexing of blocks makes much more sense.
Rocksdb has a number of options for hash indexing both memtables and blocks and
these need more testing to find the best choice.
- Lookup in a block was previously using binary search. With xrpld's use case it is highly unlikely that two adjacent key/values will ever be requested one after the other. Therefore hash indexing of blocks makes much more sense. Rocksdb has a number of options for hash indexing both memtables and blocks and these need more testing to find the best choice.
- The current Database implementation has two forms of caching, so the LRU cache
of blocks at Factory level does not make any sense. However, if the hash
indexing and potentially the new [bloom
filter](http://rocksdb.org/blog/1427/new-bloom-filter-format/) can provide
faster lookup for non-existent keys, then potentially the caching could exist at
Factory level.
- The current Database implementation has two forms of caching, so the LRU cache of blocks at Factory level does not make any sense. However, if the hash indexing and potentially the new [bloom filter](http://rocksdb.org/blog/1427/new-bloom-filter-format/) can provide faster lookup for non-existent keys, then potentially the caching could exist at Factory level.
- Multiple runs of the benchmarks can yield surprisingly different results. This
can perhaps be attributed to the asynchronous nature of rocksdb's compaction
process. The benchmarks are artificial and create highly unlikely write load to
create the dataset to measure different read access patterns. Therefore multiple
runs of the benchmarks are required to get a feel for the effectiveness of the
changes. This contrasts sharply with the keyvadb benchmarking were highly
repeatable timings were discovered. Also realistically sized datasets are
required to get a correct insight. The number of 2,000,000 key/values (actually
4,000,000 after the two insert benchmarks complete) is too low to get a full
picture.
- Multiple runs of the benchmarks can yield surprisingly different results. This can perhaps be attributed to the asynchronous nature of rocksdb's compaction process. The benchmarks are artificial and create highly unlikely write load to create the dataset to measure different read access patterns. Therefore multiple runs of the benchmarks are required to get a feel for the effectiveness of the changes. This contrasts sharply with the keyvadb benchmarking were highly repeatable timings were discovered. Also realistically sized datasets are required to get a correct insight. The number of 2,000,000 key/values (actually 4,000,000 after the two insert benchmarks complete) is too low to get a full picture.
- An interesting side effect of running the benchmarks in a profiler was that a
clear pattern of what RocksDB does under the hood was observable. This led to
the decision to trial hash indexing and also the discovery of the native CRC32
instruction not being used.
- An interesting side effect of running the benchmarks in a profiler was that a clear pattern of what RocksDB does under the hood was observable. This led to the decision to trial hash indexing and also the discovery of the native CRC32 instruction not being used.
- Important point to note that is if this factory is tested with an existing set
of sst files none of the old sst files will benefit from indexing changes until
they are compacted at a future point in time.
- Important point to note that is if this factory is tested with an existing set of sst files none of the old sst files will benefit from indexing changes until they are compacted at a future point in time.

View File

@@ -1,4 +1,3 @@
# Proto
This holds protocol buffers source code. The protoc tool stores the output
of the .proto files in the build directory.
This holds protocol buffers source code. The protoc tool stores the output of the .proto files in the build directory.

View File

@@ -1,81 +1,35 @@
# Protocol buffer definitions for gRPC
This folder contains the protocol buffer definitions used by the xrpld gRPC API.
The gRPC API attempts to mimic the JSON/Websocket API as much as possible.
As of April 2020, the gRPC API supports a subset of the full xrpld API:
tx, account_tx, account_info, fee and submit.
This folder contains the protocol buffer definitions used by the xrpld gRPC API. The gRPC API attempts to mimic the JSON/Websocket API as much as possible. As of April 2020, the gRPC API supports a subset of the full xrpld API: tx, account_tx, account_info, fee and submit.
### Making Changes
#### Wire Format and Backwards Compatibility
When making changes to the protocol buffer definitions in this folder, care must
be taken to ensure the changes do not break the wire format, which would break
backwards compatibility. At a high level, do not change any existing fields.
This includes the field's name, type and field number. Do not remove any
existing fields. It is always safe to add fields; just remember to give each of
the new fields a unique field number. The field numbers don't have to be in any
particular order and there can be gaps. More info about what changes break the
wire format can be found
[here](https://developers.google.com/protocol-buffers/docs/proto3#updating).
When making changes to the protocol buffer definitions in this folder, care must be taken to ensure the changes do not break the wire format, which would break backwards compatibility. At a high level, do not change any existing fields. This includes the field's name, type and field number. Do not remove any existing fields. It is always safe to add fields; just remember to give each of the new fields a unique field number. The field numbers don't have to be in any particular order and there can be gaps. More info about what changes break the wire format can be found [here](https://developers.google.com/protocol-buffers/docs/proto3#updating).
#### Conventions
For fields that are reused across different message types, we define the field as a unique
message type in common.proto. The name of the message type is the same as the
field name, with the exception that the field name itself is snake case, whereas
the message type is in Pascal case. The message type has one field, called
`value`. This pattern does not need to be strictly followed across the entire API,
but should be followed for transactions and ledger objects, since there is a high rate
of field reuse across different transactions and ledger objects.
The motivation for this pattern is two-fold. First, we ensure the field has the
same type everywhere that the field is used. Second, wrapping primitive types in
their own message type prevents default initialization of those primitive types.
For example, `uint32` is initialized to `0` if not explicitly set;
there is no way to tell if the client or server set the field to `0` (which may be
a valid value for the field) or the field was default initialized.
For fields that are reused across different message types, we define the field as a unique message type in common.proto. The name of the message type is the same as the field name, with the exception that the field name itself is snake case, whereas the message type is in Pascal case. The message type has one field, called `value`. This pattern does not need to be strictly followed across the entire API, but should be followed for transactions and ledger objects, since there is a high rate of field reuse across different transactions and ledger objects. The motivation for this pattern is two-fold. First, we ensure the field has the same type everywhere that the field is used. Second, wrapping primitive types in their own message type prevents default initialization of those primitive types. For example, `uint32` is initialized to `0` if not explicitly set; there is no way to tell if the client or server set the field to `0` (which may be a valid value for the field) or the field was default initialized.
#### Name Collisions
Each message type must have a unique name. To resolve collisions, add a suffix
to one or more message types. For instance, ledger objects and transaction types
often have the same name (`DepositPreauth` for example). To resolve this, the
`DepositPreauth` ledger object is named `DepositPreauthObject`.
Each message type must have a unique name. To resolve collisions, add a suffix to one or more message types. For instance, ledger objects and transaction types often have the same name (`DepositPreauth` for example). To resolve this, the `DepositPreauth` ledger object is named `DepositPreauthObject`.
#### To add a field or message type
To add a field to a message, define the fields type, name and unique index.
To add a new message type, give the message type a unique name.
Then, add the appropriate C++ code in GRPCHelpers.cpp, or in the handler itself,
to serialize/deserialize the new field or message type.
To add a field to a message, define the fields type, name and unique index. To add a new message type, give the message type a unique name. Then, add the appropriate C++ code in GRPCHelpers.cpp, or in the handler itself, to serialize/deserialize the new field or message type.
#### To add a new gRPC method
To add a new gRPC method, add the gRPC method in xrp_ledger.proto. The method name
should begin with a verb. Define the request and response types in their own
file. The name of the request type should be the method name suffixed with `Request`, and
the response type name should be the method name suffixed with `Response`. For
example, the `GetAccountInfo` method has request type `GetAccountInfoRequest` and
response type `GetAccountInfoResponse`.
To add a new gRPC method, add the gRPC method in xrp_ledger.proto. The method name should begin with a verb. Define the request and response types in their own file. The name of the request type should be the method name suffixed with `Request`, and the response type name should be the method name suffixed with `Response`. For example, the `GetAccountInfo` method has request type `GetAccountInfoRequest` and response type `GetAccountInfoResponse`.
After defining the protobuf messages for the new method, add an instantiation of the
templated `CallData` class in GRPCServerImpl::setupListeners(). The template
parameters should be the request type and the response type.
After defining the protobuf messages for the new method, add an instantiation of the templated `CallData` class in GRPCServerImpl::setupListeners(). The template parameters should be the request type and the response type.
Finally, define the handler itself in the appropriate file under the
src/xrpld/rpc/handlers folder. If the method already has a JSON/Websocket
equivalent, write the gRPC handler in the same file, and abstract common logic
into helper functions (see Tx.cpp or AccountTx.cpp for an example).
Finally, define the handler itself in the appropriate file under the src/xrpld/rpc/handlers folder. If the method already has a JSON/Websocket equivalent, write the gRPC handler in the same file, and abstract common logic into helper functions (see Tx.cpp or AccountTx.cpp for an example).
#### Testing
When modifying an existing gRPC method, be sure to test that modification in the
corresponding, existing unit test. When creating a new gRPC method, create a
client stub with `XRPLedgerAPIService::NewStub` and `grpc::CreateChannel`, and
use it to call the new method. See `GRPCServerTLS_test.cpp` for an example.
The gRPC tests are paired with their JSON counterpart, and the tests should
mirror the JSON test as much as possible.
When modifying an existing gRPC method, be sure to test that modification in the corresponding, existing unit test. When creating a new gRPC method, create a client stub with `XRPLedgerAPIService::NewStub` and `grpc::CreateChannel`, and use it to call the new method. See `GRPCServerTLS_test.cpp` for an example. The gRPC tests are paired with their JSON counterpart, and the tests should mirror the JSON test as much as possible.
Refer to the Protocol Buffers [language
guide](https://developers.google.com/protocol-buffers/docs/proto3)
for more detailed information about Protocol Buffers.
Refer to the Protocol Buffers [language guide](https://developers.google.com/protocol-buffers/docs/proto3) for more detailed information about Protocol Buffers.

View File

@@ -1,42 +1,24 @@
# protocol
Classes and functions for handling data and
values associated with the XRP Ledger protocol.
Classes and functions for handling data and values associated with the XRP Ledger protocol.
## Serialized Objects
Objects transmitted over the network must be
serialized into a canonical format. The prefix "ST" refers
to classes that deal with the serialized format.
Objects transmitted over the network must be serialized into a canonical format. The prefix "ST" refers to classes that deal with the serialized format.
The term "Tx" or "tx" is an abbreviation for "Transaction",
a commonly occurring object type.
The term "Tx" or "tx" is an abbreviation for "Transaction", a commonly occurring object type.
### Optional Fields
Our serialized fields have some "type magic" to make
optional fields easier to read:
Our serialized fields have some "type magic" to make optional fields easier to read:
- The operation `x[sfFoo]` means "return the value of 'Foo'
if it exists, or the default value if it doesn't."
- The operation `x[~sfFoo]` means "return the value of 'Foo'
if it exists, or nothing if it doesn't." This usage of the
tilde/bitwise NOT operator is not standard outside of the
`xrpld` codebase.
- As a consequence of this, `x[~sfFoo] = y[~sfFoo]`
assigns the value of Foo from y to x, including omitting
Foo from x if it doesn't exist in y.
- The operation `x[sfFoo]` means "return the value of 'Foo' if it exists, or the default value if it doesn't."
- The operation `x[~sfFoo]` means "return the value of 'Foo' if it exists, or nothing if it doesn't." This usage of the tilde/bitwise NOT operator is not standard outside of the `xrpld` codebase.
- As a consequence of this, `x[~sfFoo] = y[~sfFoo]` assigns the value of Foo from y to x, including omitting Foo from x if it doesn't exist in y.
Typically, for things that are guaranteed to exist, you use
`x[sfFoo]` and avoid having to deal with a container that may
or may not hold a value. For things not guaranteed to exist,
you use `x[~sfFoo]` because you want such a container. It
avoids having to look something up twice, once just to see if
it exists and a second time to get/set its value.
([Real example](https://github.com/XRPLF/rippled/blob/35f4698aed5dce02f771b34cfbb690495cb5efcc/src/ripple/app/tx/impl/PayChan.cpp#L229-L236))
Typically, for things that are guaranteed to exist, you use `x[sfFoo]` and avoid having to deal with a container that may or may not hold a value. For things not guaranteed to exist, you use `x[~sfFoo]` because you want such a container. It avoids having to look something up twice, once just to see if it exists and a second time to get/set its value. ([Real example](https://github.com/XRPLF/rippled/blob/35f4698aed5dce02f771b34cfbb690495cb5efcc/src/ripple/app/tx/impl/PayChan.cpp#L229-L236))
The source of this "type magic" is in
[SField.h](./SField.h#L296-L302).
The source of this "type magic" is in [SField.h](./SField.h#L296-L302).
### Related Resources

View File

@@ -11,21 +11,16 @@ The files in this directory are generated from macro definition files:
## Generation Process
Generation requires a one-time setup step to create a virtual environment
and install Python dependencies, followed by running the generation target:
Generation requires a one-time setup step to create a virtual environment and install Python dependencies, followed by running the generation target:
```bash
cmake --build . --target setup_code_gen # create venv and install dependencies (once)
cmake --build . --target code_gen # generate code
```
By default, `CODEGEN_VENV_DIR` points to `.venv` in the project root. The
`setup_code_gen` target creates a venv there and installs the required packages.
The `code_gen` target then uses the venv's Python interpreter to run generation.
By default, `CODEGEN_VENV_DIR` points to `.venv` in the project root. The `setup_code_gen` target creates a venv there and installs the required packages. The `code_gen` target then uses the venv's Python interpreter to run generation.
Generation is pure Python, so the same targets are also available as a
standalone project that needs neither the dependencies nor a compiler. This is
what CI uses, and it is handy if you only want to regenerate these files:
Generation is pure Python, so the same targets are also available as a standalone project that needs neither the dependencies nor a compiler. This is what CI uses, and it is handy if you only want to regenerate these files:
```bash
cmake -S cmake/codegen -B build/codegen

View File

@@ -9,69 +9,38 @@ The ResourceManager module has these responsibilities:
## Description
To prevent monopolization of server resources or attacks on servers,
resource consumption is monitored at each endpoint. When consumption
exceeds certain thresholds, costs are imposed. Costs could include charging
additional XRP for transactions, requiring a proof of work to be
performed, or simply disconnecting the endpoint.
To prevent monopolization of server resources or attacks on servers, resource consumption is monitored at each endpoint. When consumption exceeds certain thresholds, costs are imposed. Costs could include charging additional XRP for transactions, requiring a proof of work to be performed, or simply disconnecting the endpoint.
Currently, consumption endpoints include websocket connections used to
service clients, and peer connections used to create the peer to peer
overlay network implementing the XRPL protocol.
Currently, consumption endpoints include websocket connections used to service clients, and peer connections used to create the peer to peer overlay network implementing the XRPL protocol.
The current "balance" of a Consumer represents resource consumption
debt or credit. Debt is accrued when bad loads are imposed. Credit is
granted when good loads are imposed. When the balance crosses heuristic
thresholds, costs are increased on the endpoint. The balance is
represented as a unitless relative quantity. This balance is currently
held by the Entry struct in the impl/Entry.h file.
The current "balance" of a Consumer represents resource consumption debt or credit. Debt is accrued when bad loads are imposed. Credit is granted when good loads are imposed. When the balance crosses heuristic thresholds, costs are increased on the endpoint. The balance is represented as a unitless relative quantity. This balance is currently held by the Entry struct in the impl/Entry.h file.
Costs associated with specific transactions are defined in the
impl/Fees files.
Costs associated with specific transactions are defined in the impl/Fees files.
Although RPC connections consume resources, they are transient and
cannot be rate limited. It is advised not to expose RPC interfaces
to the general public.
Although RPC connections consume resources, they are transient and cannot be rate limited. It is advised not to expose RPC interfaces to the general public.
## Consumer Types
Consumers are placed into three classifications (as identified by the
resource::Kind enumeration):
Consumers are placed into three classifications (as identified by the resource::Kind enumeration):
- InBound,
- OutBound, and
- Admin
Each caller determines for itself the classification of the Consumer it is
creating.
Each caller determines for itself the classification of the Consumer it is creating.
## Resource Loading
It is expected that a client will impose a higher load on the server
when it first connects: the client may need to catch up on transactions
it has missed, or get trust lines, or transfer fees. The Manager must
expect this initial peak load, but not allow that high load to continue
because over the long term that would unduly stress the server.
It is expected that a client will impose a higher load on the server when it first connects: the client may need to catch up on transactions it has missed, or get trust lines, or transfer fees. The Manager must expect this initial peak load, but not allow that high load to continue because over the long term that would unduly stress the server.
If a client places a sustained high load on the server, that client
is initially given a warning message. If that high load continues
the Manager may tell the heavily loaded server to drop the connection
entirely and not allow re-connection for some amount of time.
If a client places a sustained high load on the server, that client is initially given a warning message. If that high load continues the Manager may tell the heavily loaded server to drop the connection entirely and not allow re-connection for some amount of time.
Each load is monitored by capturing peaks and then decaying those peak
values over time: this is implemented by the DecayingSample class.
Each load is monitored by capturing peaks and then decaying those peak values over time: this is implemented by the DecayingSample class.
## Gossip
Each server in a cluster creates a list of IP addresses of end points
that are imposing a significant load. This list is called Gossip, which
is passed to other nodes in that cluster. Gossip helps individual
servers in the cluster identify IP addresses that might be unduly loading
the entire cluster. Again the recourse of the individual servers is to
drop connections to those IP addresses that occur commonly in the gossip.
Each server in a cluster creates a list of IP addresses of end points that are imposing a significant load. This list is called Gossip, which is passed to other nodes in that cluster. Gossip helps individual servers in the cluster identify IP addresses that might be unduly loading the entire cluster. Again the recourse of the individual servers is to drop connections to those IP addresses that occur commonly in the gossip.
## Access
In xrpld, the Application holds a unique instance of resource::Manager,
which may be retrieved by calling the method
`Application::getResourceManager()`.
In xrpld, the Application holds a unique instance of resource::Manager, which may be retrieved by calling the method `Application::getResourceManager()`.

View File

@@ -2,25 +2,18 @@
March 2020
The `SHAMap` is a Merkle tree (http://en.wikipedia.org/wiki/Merkle_tree).
The `SHAMap` is also a radix trie of radix 16
(http://en.wikipedia.org/wiki/Radix_tree).
The `SHAMap` is a Merkle tree (http://en.wikipedia.org/wiki/Merkle_tree). The `SHAMap` is also a radix trie of radix 16 (http://en.wikipedia.org/wiki/Radix_tree).
The Merkle trie data structure is important because subtrees and even the entire
tree can be compared with other trees in O(1) time by simply comparing the hashes.
This makes it very efficient to determine if two `SHAMap`s contain the same set of
transactions or account state modifications.
The Merkle trie data structure is important because subtrees and even the entire tree can be compared with other trees in O(1) time by simply comparing the hashes. This makes it very efficient to determine if two `SHAMap`s contain the same set of transactions or account state modifications.
The radix trie property is helpful in that a key (hash) of a transaction
or account state can be used to navigate the trie.
The radix trie property is helpful in that a key (hash) of a transaction or account state can be used to navigate the trie.
A `SHAMap` is a trie with two node types:
1. SHAMapInnerNode
2. SHAMapLeafNode
Both of these nodes directly inherit from SHAMapTreeNode which holds data
common to both of the node types.
Both of these nodes directly inherit from SHAMapTreeNode which holds data common to both of the node types.
All non-leaf nodes have type SHAMapInnerNode.
@@ -34,20 +27,13 @@ A given `SHAMap` always stores only one of three kinds of data:
- Transactions without metadata, or
- Account states.
So all of the leaf nodes of a particular `SHAMap` will always have a uniform type.
The inner nodes carry no data other than the hash of the nodes beneath them.
So all of the leaf nodes of a particular `SHAMap` will always have a uniform type. The inner nodes carry no data other than the hash of the nodes beneath them.
All nodes are owned by shared_ptrs resident in either other nodes, or in case of
the root node, a shared_ptr in the `SHAMap` itself. The use of shared_ptrs
permits more than one `SHAMap` at a time to share ownership of a node. This
occurs (for example), when a copy of a `SHAMap` is made.
All nodes are owned by shared_ptrs resident in either other nodes, or in case of the root node, a shared_ptr in the `SHAMap` itself. The use of shared_ptrs permits more than one `SHAMap` at a time to share ownership of a node. This occurs (for example), when a copy of a `SHAMap` is made.
Copies are made with the `snapShot` function as opposed to the `SHAMap` copy
constructor. See the section on `SHAMap` creation for more details about
`snapShot`.
Copies are made with the `snapShot` function as opposed to the `SHAMap` copy constructor. See the section on `SHAMap` creation for more details about `snapShot`.
Sequence numbers are used to further customize the node ownership strategy. See
the section on sequence numbers for details on sequence numbers.
Sequence numbers are used to further customize the node ownership strategy. See the section on sequence numbers for details on sequence numbers.
![node diagram](https://user-images.githubusercontent.com/46455409/77350005-1ef12c80-6cf9-11ea-9c8d-56410f442859.png)
@@ -58,145 +44,63 @@ There are two different ways of building and using a `SHAMap`:
1. A mutable `SHAMap` and
2. An immutable `SHAMap`
The distinction here is not of the classic C++ immutable-means-unchanging sense.
An immutable `SHAMap` contains _nodes_ that are immutable. Also, once a node has
been located in an immutable `SHAMap`, that node is guaranteed to persist in that
`SHAMap` for the lifetime of the `SHAMap`.
The distinction here is not of the classic C++ immutable-means-unchanging sense. An immutable `SHAMap` contains _nodes_ that are immutable. Also, once a node has been located in an immutable `SHAMap`, that node is guaranteed to persist in that `SHAMap` for the lifetime of the `SHAMap`.
So, somewhat counter-intuitively, an immutable `SHAMap` may grow as new nodes are
introduced. But an immutable `SHAMap` will never get smaller (until it entirely
evaporates when it is destroyed). Nodes, once introduced to the immutable
`SHAMap`, also never change their location in memory. So nodes in an immutable
`SHAMap` can be handled using raw pointers (if you're careful).
So, somewhat counter-intuitively, an immutable `SHAMap` may grow as new nodes are introduced. But an immutable `SHAMap` will never get smaller (until it entirely evaporates when it is destroyed). Nodes, once introduced to the immutable `SHAMap`, also never change their location in memory. So nodes in an immutable `SHAMap` can be handled using raw pointers (if you're careful).
One consequence of this design is that an immutable `SHAMap` can never be
"trimmed". There is no way to identify unnecessary nodes in an immutable `SHAMap`
that could be removed. Once a node has been brought into the in-memory `SHAMap`,
that node stays in memory for the life of the `SHAMap`.
One consequence of this design is that an immutable `SHAMap` can never be "trimmed". There is no way to identify unnecessary nodes in an immutable `SHAMap` that could be removed. Once a node has been brought into the in-memory `SHAMap`, that node stays in memory for the life of the `SHAMap`.
Most `SHAMap`s are immutable, in the sense that they don't modify or remove their
contained nodes.
Most `SHAMap`s are immutable, in the sense that they don't modify or remove their contained nodes.
An example where a mutable `SHAMap` is required is when we want to apply
transactions to the last closed ledger. To do so we'd make a mutable snapshot
of the state trie and then start applying transactions to it. Because the
snapshot is mutable, changes to nodes in the snapshot will not affect nodes in
other `SHAMap`s.
An example where a mutable `SHAMap` is required is when we want to apply transactions to the last closed ledger. To do so we'd make a mutable snapshot of the state trie and then start applying transactions to it. Because the snapshot is mutable, changes to nodes in the snapshot will not affect nodes in other `SHAMap`s.
An example using a immutable ledger would be when there's an open ledger and
some piece of code wishes to query the state of the ledger. In this case we
don't wish to change the state of the `SHAMap`, so we'd use an immutable snapshot.
An example using a immutable ledger would be when there's an open ledger and some piece of code wishes to query the state of the ledger. In this case we don't wish to change the state of the `SHAMap`, so we'd use an immutable snapshot.
## Sequence numbers
Both `SHAMap`s and their nodes carry a sequence number. This is simply an
unsigned number that indicates ownership or membership, or a non-membership.
Both `SHAMap`s and their nodes carry a sequence number. This is simply an unsigned number that indicates ownership or membership, or a non-membership.
`SHAMap`s sequence numbers normally start out as 1. However when a snap-shot of
a `SHAMap` is made, the copy's sequence number is 1 greater than the original.
`SHAMap`s sequence numbers normally start out as 1. However when a snap-shot of a `SHAMap` is made, the copy's sequence number is 1 greater than the original.
The nodes of a `SHAMap` have their own copy of a sequence number. If the `SHAMap`
is mutable, meaning it can change, then all of its nodes must have the
same sequence number as the `SHAMap` itself. This enforces an invariant that none
of the nodes are shared with other `SHAMap`s.
The nodes of a `SHAMap` have their own copy of a sequence number. If the `SHAMap` is mutable, meaning it can change, then all of its nodes must have the same sequence number as the `SHAMap` itself. This enforces an invariant that none of the nodes are shared with other `SHAMap`s.
When a `SHAMap` needs to have a private copy of a node, not shared by any other
`SHAMap`, it first clones it and then sets the new copy to have a sequence number
equal to the `SHAMap` sequence number. The `unshareNode` is a private utility
which automates the task of first checking if the node is already sharable, and
if so, cloning it and giving it the proper sequence number. An example case
where a private copy is needed is when an inner node needs to have a child
pointer altered. Any modification to a node will require a non-shared node.
When a `SHAMap` needs to have a private copy of a node, not shared by any other `SHAMap`, it first clones it and then sets the new copy to have a sequence number equal to the `SHAMap` sequence number. The `unshareNode` is a private utility which automates the task of first checking if the node is already sharable, and if so, cloning it and giving it the proper sequence number. An example case where a private copy is needed is when an inner node needs to have a child pointer altered. Any modification to a node will require a non-shared node.
When a `SHAMap` decides that it is safe to share a node of its own, it sets the
node's sequence number to 0 (a `SHAMap` never has a sequence number of 0). This
is done for every node in the trie when `SHAMap::walkSubTree` is executed.
When a `SHAMap` decides that it is safe to share a node of its own, it sets the node's sequence number to 0 (a `SHAMap` never has a sequence number of 0). This is done for every node in the trie when `SHAMap::walkSubTree` is executed.
Note that other objects in xrpld also have sequence numbers (e.g. ledgers).
The `SHAMap` and node sequence numbers should not be confused with these other
sequence numbers (no relation).
Note that other objects in xrpld also have sequence numbers (e.g. ledgers). The `SHAMap` and node sequence numbers should not be confused with these other sequence numbers (no relation).
## SHAMap Creation
A `SHAMap` is usually not created from vacuum. Once an initial `SHAMap` is
constructed, later `SHAMap`s are usually created by calling snapShot(bool
isMutable) on the original `SHAMap`. The returned `SHAMap` has the expected
characteristics (mutable or immutable) based on the passed in flag.
A `SHAMap` is usually not created from vacuum. Once an initial `SHAMap` is constructed, later `SHAMap`s are usually created by calling snapShot(bool isMutable) on the original `SHAMap`. The returned `SHAMap` has the expected characteristics (mutable or immutable) based on the passed in flag.
It is cheaper to make an immutable snapshot of a `SHAMap` than to make a mutable
snapshot. If the `SHAMap` snapshot is mutable then sharable nodes must be
copied before they are placed in the mutable map.
It is cheaper to make an immutable snapshot of a `SHAMap` than to make a mutable snapshot. If the `SHAMap` snapshot is mutable then sharable nodes must be copied before they are placed in the mutable map.
A new `SHAMap` is created with each new ledger round. Transactions not executed
in the previous ledger populate the `SHAMap` for the new ledger.
A new `SHAMap` is created with each new ledger round. Transactions not executed in the previous ledger populate the `SHAMap` for the new ledger.
## Storing SHAMap data in the database
When consensus is reached, the ledger is closed. As part of this process, the
`SHAMap` is stored to the database by calling `SHAMap::flushDirty`.
When consensus is reached, the ledger is closed. As part of this process, the `SHAMap` is stored to the database by calling `SHAMap::flushDirty`.
Both `unshare()` and `flushDirty` walk the `SHAMap` by calling
`SHAMap::walkSubTree`. As `unshare()` walks the trie, nodes are not written to
the database, and as `flushDirty` walks the trie nodes are written to the
database. `walkSubTree` visits every node in the trie. This process must ensure
that each node is only owned by this trie, and so "unshares" as it walks each
node (from the root down). This is done in the `preFlushNode` function by
ensuring that the node has a sequence number equal to that of the `SHAMap`. If
the node doesn't, it is cloned.
Both `unshare()` and `flushDirty` walk the `SHAMap` by calling `SHAMap::walkSubTree`. As `unshare()` walks the trie, nodes are not written to the database, and as `flushDirty` walks the trie nodes are written to the database. `walkSubTree` visits every node in the trie. This process must ensure that each node is only owned by this trie, and so "unshares" as it walks each node (from the root down). This is done in the `preFlushNode` function by ensuring that the node has a sequence number equal to that of the `SHAMap`. If the node doesn't, it is cloned.
For each inner node encountered (starting with the root node), each of the
children are inspected (from 1 to 16). For each child, if it has a non-zero
sequence number (unshareable), the child is first copied. Then if the child is
an inner node, we recurse down to that node's children. Otherwise we've found a
leaf node and that node is written to the database. A count of each leaf node
that is visited is kept. The hash of the data in the leaf node is computed at
this time, and the child is reassigned back into the parent inner node just in
case the COW operation created a new pointer to this leaf node.
For each inner node encountered (starting with the root node), each of the children are inspected (from 1 to 16). For each child, if it has a non-zero sequence number (unshareable), the child is first copied. Then if the child is an inner node, we recurse down to that node's children. Otherwise we've found a leaf node and that node is written to the database. A count of each leaf node that is visited is kept. The hash of the data in the leaf node is computed at this time, and the child is reassigned back into the parent inner node just in case the COW operation created a new pointer to this leaf node.
After processing each node, the node is then marked as sharable again by setting
its sequence number to 0.
After processing each node, the node is then marked as sharable again by setting its sequence number to 0.
After all of an inner node's children are processed, then its hash is updated
and the inner node is written to the database. Then this inner node is assigned
back into it's parent node, again in case the COW operation created a new
pointer to it.
After all of an inner node's children are processed, then its hash is updated and the inner node is written to the database. Then this inner node is assigned back into it's parent node, again in case the COW operation created a new pointer to it.
## Walking a SHAMap
The private function `SHAMap::walkTowardsKey` is a good example of _how_ to walk
a `SHAMap`, and the various functions that call `walkTowardsKey` are good examples
of _why_ one would want to walk a `SHAMap` (e.g. `SHAMap::findKey`).
`walkTowardsKey` always starts at the root of the `SHAMap` and traverses down
through the inner nodes, looking for a leaf node along a path in the trie
designated by a `uint256`.
The private function `SHAMap::walkTowardsKey` is a good example of _how_ to walk a `SHAMap`, and the various functions that call `walkTowardsKey` are good examples of _why_ one would want to walk a `SHAMap` (e.g. `SHAMap::findKey`). `walkTowardsKey` always starts at the root of the `SHAMap` and traverses down through the inner nodes, looking for a leaf node along a path in the trie designated by a `uint256`.
As one walks the trie, one can _optionally_ keep a stack of nodes that one has
passed through. This isn't necessary for walking the trie, but many clients
will use the stack after finding the desired node. For example if one is
deleting a node from the trie, the stack is handy for repairing invariants in
the trie after the deletion.
As one walks the trie, one can _optionally_ keep a stack of nodes that one has passed through. This isn't necessary for walking the trie, but many clients will use the stack after finding the desired node. For example if one is deleting a node from the trie, the stack is handy for repairing invariants in the trie after the deletion.
To assist in walking the trie, `SHAMap::walkTowardsKey` uses a `SHAMapNodeID`
that identifies a node by its path from the root and its depth in the trie. The
path is just a "list" of numbers, each in the range [0 .. 15], depicting which
child was chosen at each node starting from the root. Each choice is represented
by 4 bits, and then packed in sequence into a `uint256` (such that the longest
path possible has 256 / 4 = 64 steps). The high 4 bits of the first byte
identify which child of the root is chosen, the lower 4 bits of the first byte
identify the child of that node, and so on. The `SHAMapNodeID` identifying the
root node has an ID of 0 and a depth of 0. See `selectBranch` for details of
how we use a `SHAMapNodeID` to select a "branch" (child) by indexing into a
path at a given depth.
To assist in walking the trie, `SHAMap::walkTowardsKey` uses a `SHAMapNodeID` that identifies a node by its path from the root and its depth in the trie. The path is just a "list" of numbers, each in the range [0 .. 15], depicting which child was chosen at each node starting from the root. Each choice is represented by 4 bits, and then packed in sequence into a `uint256` (such that the longest path possible has 256 / 4 = 64 steps). The high 4 bits of the first byte identify which child of the root is chosen, the lower 4 bits of the first byte identify the child of that node, and so on. The `SHAMapNodeID` identifying the root node has an ID of 0 and a depth of 0. See `selectBranch` for details of how we use a `SHAMapNodeID` to select a "branch" (child) by indexing into a path at a given depth.
While the current node is an inner node, traversing down the trie from the root
continues, unless the path indicates a child that does not exist. And in this
case, `nullptr` is returned to indicate no leaf node along the given path
exists. Otherwise a leaf node is found and a (non-owning) pointer to it is
returned. At each step, if a stack is requested, a
`pair<shared_ptr<SHAMapTreeNode>, SHAMapNodeID>` is pushed onto the stack.
While the current node is an inner node, traversing down the trie from the root continues, unless the path indicates a child that does not exist. And in this case, `nullptr` is returned to indicate no leaf node along the given path exists. Otherwise a leaf node is found and a (non-owning) pointer to it is returned. At each step, if a stack is requested, a `pair<shared_ptr<SHAMapTreeNode>, SHAMapNodeID>` is pushed onto the stack.
When a child node is found by `selectBranch`, the traversal to that node
consists of two steps:
When a child node is found by `selectBranch`, the traversal to that node consists of two steps:
1. Update the `shared_ptr` to the current node.
2. Update the `SHAMapNodeID`.
@@ -207,126 +111,79 @@ The first step consists of several attempts to find the node in various places:
2. In the node cache.
3. In the database.
If the node is not found in the trie, then it is installed into the trie as part
of the traversal process.
If the node is not found in the trie, then it is installed into the trie as part of the traversal process.
## Late-arriving Nodes
As we noted earlier, `SHAMap`s (even immutable ones) may grow. If a `SHAMap` is
searching for a node and runs into an empty spot in the trie, then the `SHAMap`
looks to see if the node exists but has not yet been made part of the map. This
operation is performed in the `SHAMap::fetchNodeNT()` method. The _NT_
is this case stands for 'No Throw'.
As we noted earlier, `SHAMap`s (even immutable ones) may grow. If a `SHAMap` is searching for a node and runs into an empty spot in the trie, then the `SHAMap` looks to see if the node exists but has not yet been made part of the map. This operation is performed in the `SHAMap::fetchNodeNT()` method. The _NT_ is this case stands for 'No Throw'.
The `fetchNodeNT()` method goes through three phases:
1. By calling `cacheLookup()` we attempt to locate the missing node in the
TreeNodeCache. The TreeNodeCache is a cache of immutable SHAMapTreeNodes
that are shared across all `SHAMap`s.
1. By calling `cacheLookup()` we attempt to locate the missing node in the TreeNodeCache. The TreeNodeCache is a cache of immutable SHAMapTreeNodes that are shared across all `SHAMap`s.
Any SHAMapLeafNode that is immutable has a sequence number of zero
(sharable). When a mutable `SHAMap` is created then its SHAMapTreeNodes are
given non-zero sequence numbers (unshareable). But all nodes in the
TreeNodeCache are immutable, so if one is found here, its sequence number
will be 0.
Any SHAMapLeafNode that is immutable has a sequence number of zero (sharable). When a mutable `SHAMap` is created then its SHAMapTreeNodes are given non-zero sequence numbers (unshareable). But all nodes in the TreeNodeCache are immutable, so if one is found here, its sequence number will be 0.
2. If the node is not in the TreeNodeCache, we attempt to locate the node
in the historic data stored by the data base. The call to
`fetchNodeFromDB(hash)` does that work for us.
2. If the node is not in the TreeNodeCache, we attempt to locate the node in the historic data stored by the data base. The call to `fetchNodeFromDB(hash)` does that work for us.
3. Finally if a filter exists, we check if it can supply the node. This is
typically the LedgerMaster which tracks the current ledger and ledgers
in the process of closing.
3. Finally if a filter exists, we check if it can supply the node. This is typically the LedgerMaster which tracks the current ledger and ledgers in the process of closing.
## Canonicalize
`canonicalize()` is called every time a node is introduced into the `SHAMap`.
A call to `canonicalize()` stores the node in the `TreeNodeCache` if it does not
already exist in the `TreeNodeCache`.
A call to `canonicalize()` stores the node in the `TreeNodeCache` if it does not already exist in the `TreeNodeCache`.
The calls to `canonicalize()` make sure that if the resulting node is already in
the `SHAMap`, node `TreeNodeCache` or database, then we don't create duplicates
by favoring the copy already in the `TreeNodeCache`.
The calls to `canonicalize()` make sure that if the resulting node is already in the `SHAMap`, node `TreeNodeCache` or database, then we don't create duplicates by favoring the copy already in the `TreeNodeCache`.
By using `canonicalize()` we manage a thread race condition where two different
threads might both recognize the lack of a SHAMapLeafNode at the same time
(during a fetch). If they both attempt to insert the node into the `SHAMap`, then
`canonicalize` makes sure that the first node in wins and the slower thread
receives back a pointer to the node inserted by the faster thread. Recall
that these two `SHAMap`s will share the same `TreeNodeCache`.
By using `canonicalize()` we manage a thread race condition where two different threads might both recognize the lack of a SHAMapLeafNode at the same time (during a fetch). If they both attempt to insert the node into the `SHAMap`, then `canonicalize` makes sure that the first node in wins and the slower thread receives back a pointer to the node inserted by the faster thread. Recall that these two `SHAMap`s will share the same `TreeNodeCache`.
## `TreeNodeCache`
The `TreeNodeCache` is a `std::unordered_map` keyed on the hash of the
`SHAMap` node. The stored type consists of `shared_ptr<SHAMapTreeNode>`,
`weak_ptr<SHAMapTreeNode>`, and a time point indicating the most recent
access of this node in the cache. The time point is based on
`std::chrono::steady_clock`.
The `TreeNodeCache` is a `std::unordered_map` keyed on the hash of the `SHAMap` node. The stored type consists of `shared_ptr<SHAMapTreeNode>`, `weak_ptr<SHAMapTreeNode>`, and a time point indicating the most recent access of this node in the cache. The time point is based on `std::chrono::steady_clock`.
The container uses a cryptographically secure hash that is randomly seeded.
The `TreeNodeCache` also carries with it various data used for statistics
and logging, and a target age for the contained nodes. When the target age
for a node is exceeded, and there are no more references to the node, the
node is removed from the `TreeNodeCache`.
The `TreeNodeCache` also carries with it various data used for statistics and logging, and a target age for the contained nodes. When the target age for a node is exceeded, and there are no more references to the node, the node is removed from the `TreeNodeCache`.
## `FullBelowCache`
This cache remembers which trie keys have all of their children resident in a
`SHAMap`. This optimizes the process of acquiring a complete trie. This is used
when creating the missing nodes list. Missing nodes are those nodes that a
`SHAMap` refers to but that are not stored in the local database.
This cache remembers which trie keys have all of their children resident in a `SHAMap`. This optimizes the process of acquiring a complete trie. This is used when creating the missing nodes list. Missing nodes are those nodes that a `SHAMap` refers to but that are not stored in the local database.
As a depth-first walk of a `SHAMap` is performed, if an inner node answers true to
`isFullBelow()` then it is known that none of this node's children are missing
nodes, and thus that subtree does not need to be walked. These nodes are stored
in the FullBelowCache. Subsequent walks check the FullBelowCache first when
encountering a node, and ignore that subtree if found.
As a depth-first walk of a `SHAMap` is performed, if an inner node answers true to `isFullBelow()` then it is known that none of this node's children are missing nodes, and thus that subtree does not need to be walked. These nodes are stored in the FullBelowCache. Subsequent walks check the FullBelowCache first when encountering a node, and ignore that subtree if found.
## `SHAMapTreeNode`
This is an abstract base class for the concrete node types. It holds the
following common data:
This is an abstract base class for the concrete node types. It holds the following common data:
1. A hash
2. An identifier used to perform copy-on-write operations
### `SHAMapInnerNode`
`SHAMapInnerNode` publicly inherits directly from `SHAMapTreeNode`. It holds
the following data:
`SHAMapInnerNode` publicly inherits directly from `SHAMapTreeNode`. It holds the following data:
1. Up to 16 child nodes, each held with a shared_ptr.
2. A hash for each child.
3. A bitset to indicate which of the 16 children exist.
4. An identifier used to determine whether the map below this node is
fully populated
4. An identifier used to determine whether the map below this node is fully populated
### `SHAMapLeafNode`
`SHAMapLeafNode` is an abstract class which publicly inherits directly from
`SHAMapTreeNode`. It isIt holds the
following data:
`SHAMapLeafNode` is an abstract class which publicly inherits directly from `SHAMapTreeNode`. It isIt holds the following data:
1. A shared_ptr to a const SHAMapItem.
#### `SHAMapAccountStateLeafNode`
`SHAMapAccountStateLeafNode` is a class which publicly inherits directly from
`SHAMapLeafNode`. It is used to represent entries (i.e. account objects, escrow
objects, trust lines, etc.) in a state map.
`SHAMapAccountStateLeafNode` is a class which publicly inherits directly from `SHAMapLeafNode`. It is used to represent entries (i.e. account objects, escrow objects, trust lines, etc.) in a state map.
#### `SHAMapTxLeafNode`
`SHAMapTxLeafNode` is a class which publicly inherits directly from
`SHAMapLeafNode`. It is used to represent transactions in a state map.
`SHAMapTxLeafNode` is a class which publicly inherits directly from `SHAMapLeafNode`. It is used to represent transactions in a state map.
#### `SHAMapTxPlusMetaLeafNode`
`SHAMapTxPlusMetaLeafNode` is a class which publicly inherits directly from
`SHAMapLeafNode`. It is used to represent transactions along with metadata
associated with this transaction in a state map.
`SHAMapTxPlusMetaLeafNode` is a class which publicly inherits directly from `SHAMapLeafNode`. It is used to represent transactions along with metadata associated with this transaction in a state map.
## SHAMapItem

View File

@@ -1,8 +1,6 @@
# check-tools snapshots
These files capture the output of [`bin/check-tools.sh`](../../bin/check-tools.sh)
— the version and resolved store path of each development tool — in each Nix
environment:
These files capture the output of [`bin/check-tools.sh`](../../bin/check-tools.sh) — the version and resolved store path of each development tool — in each Nix environment:
| File | Environment |
| ---------------------- | ------------------------------------ |
@@ -10,26 +8,15 @@ environment:
| `nix-ubuntu-arm64.txt` | `nix-ubuntu` CI image, `linux/arm64` |
| `macos.txt` | macOS, inside `nix develop` |
The [`check-tools`](../../.github/workflows/check-tools.yml) workflow regenerates
each snapshot in its environment and fails if it differs from the committed file.
So if you change the environment (bump the image tag in
[`linux.json`](../../.github/scripts/strategy-matrix/linux.json), update
`flake.lock`, change the tool list in `check-tools.sh`, …) you must regenerate
and commit the affected snapshots.
The [`check-tools`](../../.github/workflows/check-tools.yml) workflow regenerates each snapshot in its environment and fails if it differs from the committed file. So if you change the environment (bump the image tag in [`linux.json`](../../.github/scripts/strategy-matrix/linux.json), update `flake.lock`, change the tool list in `check-tools.sh`, …) you must regenerate and commit the affected snapshots.
Each snapshot is `check-tools.sh` stdout with the git-clone connectivity check
skipped (`CHECK_TOOLS_SKIP_CLONE=1`), so it is deterministic for a given
environment. On macOS the dev-shell greeting that `nix develop` prints first is
dropped with `sed -n '/^Detected OS:/,$p'`.
Each snapshot is `check-tools.sh` stdout with the git-clone connectivity check skipped (`CHECK_TOOLS_SKIP_CLONE=1`), so it is deterministic for a given environment. On macOS the dev-shell greeting that `nix develop` prints first is dropped with `sed -n '/^Detected OS:/,$p'`.
The store paths carry their derivation hash, so they change whenever a tool is
rebuilt — a `flake.lock` update generally rewrites most of them even when no
version moves. That is deliberate: it makes tooling changes visible in review.
The store paths carry their derivation hash, so they change whenever a tool is rebuilt — a `flake.lock` update generally rewrites most of them even when no version moves. That is deliberate: it makes tooling changes visible in review.
## Regenerating
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible
runtime such as Apple `container`). The image tag is pinned in `linux.json`:
The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible runtime such as Apple `container`). The image tag is pinned in `linux.json`:
```bash
img="ghcr.io/xrplf/xrpld/nix-ubuntu:$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)"
@@ -40,12 +27,9 @@ for arch in amd64 arm64; do
done
```
(With Docker, replace `container run … -a "${arch}"` with
`docker run … --platform "linux/${arch}"`.)
(With Docker, replace `container run … -a "${arch}"` with `docker run … --platform "linux/${arch}"`.)
The macOS snapshot is generated locally. `CI=` is unset so `check-tools.sh`
checks the full dev-shell tool set (it otherwise skips some tools when `CI` is
set):
The macOS snapshot is generated locally. `CI=` is unset so `check-tools.sh` checks the full dev-shell tool set (it otherwise skips some tools when `CI` is set):
```bash
CI= nix develop -c bash -c 'CHECK_TOOLS_SKIP_CLONE=1 bash bin/check-tools.sh' |

View File

@@ -1,101 +1,52 @@
# 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`.
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`).
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:
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` | |
| 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.
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.
- compiles the C++ test programs in
[`test_files/cpp/sources/`](./test_files/cpp/sources) with both `g++` and
`clang++`, and sanitizers, and
- compiles the Rust test programs in
[`test_files/rust/sources/`](./test_files/rust/sources) with `rustc`, and
builds the [`test_files/rust/proc_macro/`](./test_files/rust/proc_macro)
workspace with `cargo` to exercise proc-macro dylib loading.
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.
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.
- compiles the C++ test programs in [`test_files/cpp/sources/`](./test_files/cpp/sources) with both `g++` and `clang++`, and sanitizers, and
- compiles the Rust test programs in [`test_files/rust/sources/`](./test_files/rust/sources) with `rustc`, and builds the [`test_files/rust/proc_macro/`](./test_files/rust/proc_macro) workspace with `cargo` to exercise proc-macro dylib loading.
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:
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.
- **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,
[`bin/default-loader-path.sh`](../../bin/default-loader-path.sh) reports the
expected loader path for the current architecture, so we can patch the binaries
to use the correct loader.
- **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, [`bin/default-loader-path.sh`](../../bin/default-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, and the C++ and Rust programs
go through the same pipeline: each is compiled in `final`, has its `PT_INTERP`
patched to the target loader, and is then run in the clean `tester` stage to
confirm it emits the expected diagnostic on a stock base image. The C++ programs
are in `test_files/cpp/sources/` (a regular binary plus ASan/TSan/UBSan
variants); the Rust programs are in `test_files/rust/sources/` (a hello binary
plus panic and overflow-check variants), plus the `test_files/rust/proc_macro/`
workspace — a crate whose compilation additionally loads a proc-macro dylib, and
whose resulting binary is patched and run like the others.
The build then verifies all of this end to end, and the C++ and Rust programs go through the same pipeline: each is compiled in `final`, has its `PT_INTERP` patched to the target loader, and is then run in the clean `tester` stage to confirm it emits the expected diagnostic on a stock base image. The C++ programs are in `test_files/cpp/sources/` (a regular binary plus ASan/TSan/UBSan variants); the Rust programs are in `test_files/rust/sources/` (a hello binary plus panic and overflow-check variants), plus the `test_files/rust/proc_macro/` workspace — a crate whose compilation additionally loads a proc-macro dylib, and whose resulting binary is patched and run like the others.
## Files
| File | Purpose |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
| [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. |
| [`./test_files/rust/`](./test_files/rust) | Rust smoke test: rustc sources + a cargo proc-macro workspace + compile/run scripts. |
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
| [`/bin/default-loader-path.sh`](../../bin/default-loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |
| File | Purpose |
| --- | --- |
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
| [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. |
| [`./test_files/rust/`](./test_files/rust) | Rust smoke test: rustc sources + a cargo proc-macro workspace + compile/run scripts. |
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
| [`/bin/default-loader-path.sh`](../../bin/default-loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |

View File

@@ -1,8 +1,6 @@
# Linux Packaging
This directory contains all files needed to build RPM and Debian packages for
`xrpld`. The packages also ship the `validator-keys` tool, so packaging requires
a build configured with `-Dvalidator_keys=ON`.
This directory contains all files needed to build RPM and Debian packages for `xrpld`. The packages also ship the `validator-keys` tool, so packaging requires a build configured with `-Dvalidator_keys=ON`.
## Directory layout
@@ -25,22 +23,14 @@ package/
## Prerequisites
Packaging is declared on the build configs themselves, in
[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json):
a config that is also packaged carries a `package` map, so its binaries and its
packaging job cannot drift apart. Today only `linux/amd64` is emitted. The map
pins the full container image in `image` — edit that field to move to a new
image and both CI and local builds pick it up — and names the format that image
builds in `type`, which CI passes to `build_pkg.py` as `--package-type`; the two
have to stay in step.
Packaging is declared on the build configs themselves, in [`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json): a config that is also packaged carries a `package` map, so its binaries and its packaging job cannot drift apart. Today only `linux/amd64` is emitted. The map pins the full container image in `image` — edit that field to move to a new image and both CI and local builds pick it up — and names the format that image builds in `type`, which CI passes to `build_pkg.py` as `--package-type`; the two have to stay in step.
| Package type | Image (`configs.<distro>[].package.image` in `linux.json`) | Tools required |
| ------------ | ---------------------------------------------------------- | --------------------------------------------------- |
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild`, `rpmsign` |
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha>` | `dpkg-buildpackage`, debhelper with compat level 13 |
| Package type | Image (`configs.<distro>[].package.image` in `linux.json`) | Tools required |
| --- | --- | --- |
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild`, `rpmsign` |
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha>` | `dpkg-buildpackage`, debhelper with compat level 13 |
To print the full packaging matrix (artifact names and images) for the current
`linux.json`:
To print the full packaging matrix (artifact names and images) for the current `linux.json`:
```bash
./.github/scripts/strategy-matrix/generate.py --packaging
@@ -50,32 +40,15 @@ To print the full packaging matrix (artifact names and images) for the current
### Via CI
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
`reusable-package.yml`. That workflow generates its own packaging matrix from
the configs that carry a `package` map (via `generate.py --packaging`) and fans
out one job per distro. Each job downloads the pre-built `xrpld` and
`validator-keys` binary artifacts and runs in that distro's container, building
the format `package.type` declares. The packaging script derives the package
version from the downloaded binary's `xrpld --version` output; no CMake
configure or build step is needed inside the packaging job.
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call `reusable-package.yml`. That workflow generates its own packaging matrix from the configs that carry a `package` map (via `generate.py --packaging`) and fans out one job per distro. Each job downloads the pre-built `xrpld` and `validator-keys` binary artifacts and runs in that distro's container, building the format `package.type` declares. The packaging script derives the package version from the downloaded binary's `xrpld --version` output; no CMake configure or build step is needed inside the packaging job.
The binaries come from the `debian` and `rhel` build configs themselves — the
ones carrying the `package` map — which pass `-Dvalidator_keys=ON` so that the
build job produces `validator-keys` next to `xrpld` and uploads it as the
`validator-keys-<config name>` artifact. The packaging matrix names both
artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`) after that
same config, so a packaged config must keep `-Dvalidator_keys=ON`. Those configs
are not `minimal`, so `on-pr.yml` only packages once a PR runs the full matrix.
The binaries come from the `debian` and `rhel` build configs themselves — the ones carrying the `package` map — which pass `-Dvalidator_keys=ON` so that the build job produces `validator-keys` next to `xrpld` and uploads it as the `validator-keys-<config name>` artifact. The packaging matrix names both artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`) after that same config, so a packaged config must keep `-Dvalidator_keys=ON`. Those configs are not `minimal`, so `on-pr.yml` only packages once a PR runs the full matrix.
`validator-keys` is fetched from an exact commit pinned in
[`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given
`xrpld` version always packages the same tool; bump that commit deliberately.
`validator-keys` is fetched from an exact commit pinned in [`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given `xrpld` version always packages the same tool; bump that commit deliberately.
### 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 `xrpld` and `validator-keys` binaries already built at `build/xrpld` and `build/validator-keys`, run the packaging step inside the same container CI uses. The image tag is derived from `linux.json` so you don't need to hardcode a SHA.
```bash
# From the repo root. Each distro's container image is the `package.image` field
@@ -101,9 +74,7 @@ docker run --rm \
### Via CMake (host-side target)
If you run CMake configure on a host that has `rpmbuild` or `dpkg-buildpackage`
installed natively, you can use the CMake target directly — no container
needed, but the host toolchain replaces the pinned CI image:
If you run CMake configure on a host that has `rpmbuild` or `dpkg-buildpackage` installed natively, you can use the CMake target directly — no container needed, but the host toolchain replaces the pinned CI image:
```bash
cmake \
@@ -116,101 +87,49 @@ cmake \
cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL
```
The `cmake/XrplPackaging.cmake` module defines the `package` target only if at
least one of `rpmbuild` / `dpkg-buildpackage` is present and both the `xrpld` and
`validator-keys` targets exist (`-Dxrpld=ON -Dvalidator_keys=ON`); the target
builds both binaries before packaging, passing `--package-type deb` when
`dpkg-buildpackage` is present and `rpm` otherwise, and `--channel UNRELEASED`.
The packaging script installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`,
etc.) regardless of `CMAKE_INSTALL_PREFIX`.
The `cmake/XrplPackaging.cmake` module defines the `package` target only if at least one of `rpmbuild` / `dpkg-buildpackage` is present and both the `xrpld` and `validator-keys` targets exist (`-Dxrpld=ON -Dvalidator_keys=ON`); the target builds both binaries before packaging, passing `--package-type deb` when `dpkg-buildpackage` is present and `rpm` otherwise, and `--channel UNRELEASED`. The packaging script installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of `CMAKE_INSTALL_PREFIX`.
The package version is not a CMake input on this path: `build_pkg.py` derives it
from the just-built `xrpld` binary's `xrpld --version` output. The package
release defaults to 1 and is overridable with `-Dpkg_release=N`.
The package version is not a CMake input on this path: `build_pkg.py` derives it from the just-built `xrpld` binary's `xrpld --version` output. The package release defaults to 1 and is overridable with `-Dpkg_release=N`.
## Publishing packages
Packages are published to the XRPLF repositories on Sonatype Nexus at
`https://packages.xrplf.org`. The `release-info` action decides the channel from
the event, and `publish_pkg.py` maps that channel to its repositories:
Packages are published to the XRPLF repositories on Sonatype Nexus at `https://packages.xrplf.org`. The `release-info` action decides the channel from the event, and `publish_pkg.py` maps that channel to its repositories:
| Event | Version | Channel | DEB repository | RPM upload repository |
| ------------------------ | ----------------- | --------- | -------------- | --------------------- |
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
| Event | Version | Channel | DEB repository | RPM upload repository |
| --- | --- | --- | --- | --- |
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` |
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
Only a tag names a channel — do not extend that to `develop`, where
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
version during a release cycle, which would send develop builds into `stable`.
Versions sort in row order, so moving to a more mature channel never downgrades.
Only a tag names a channel — do not extend that to `develop`, where `BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final version during a release cycle, which would send develop builds into `stable`. Versions sort in row order, so moving to a more mature channel never downgrades.
The action decides the package release number on the same split: a tag's version
is unique, so its packages are release 1, while develop repeats the same version
and takes `<run number>.<commit date>git<commit hash>`, e.g.
`857.20260826gitb6a8995` — the leading run number keeps each push superseding
the last, and the date and hash say which commit a package on
`packages.xrplf.org` came from. Both reach the packaging scripts as arguments,
so neither script derives anything itself.
The action decides the package release number on the same split: a tag's version is unique, so its packages are release 1, while develop repeats the same version and takes `<run number>.<commit date>git<commit hash>`, e.g. `857.20260826gitb6a8995` — the leading run number keeps each push superseding the last, and the date and hash say which commit a package on `packages.xrplf.org` came from. Both reach the packaging scripts as arguments, so neither script derives anything itself.
Publishing is the last step of each packaging job, uploading from the container
that built the packages with the `publish_pkg.py` shipped in the image — the
same copy other repositories run. Without `publish: true` the step is a
`--dry-run`, listing the uploads it would make without needing credentials, so
any run that builds packages also exercises the upload routing. `on-trigger.yml`
passes `publish: true` for develop pushes in `XRPLF/rippled` and `on-tag.yml`
for tags in any `XRPLF` repository, both authenticating with the
`NEXUS_REMOTE_USERNAME` / `NEXUS_REMOTE_PASSWORD` secrets already used for the
Conan remote; `on-pr.yml` never publishes.
Publishing is the last step of each packaging job, uploading from the container that built the packages with the `publish_pkg.py` shipped in the image — the same copy other repositories run. Without `publish: true` the step is a `--dry-run`, listing the uploads it would make without needing credentials, so any run that builds packages also exercises the upload routing. `on-trigger.yml` passes `publish: true` for develop pushes in `XRPLF/rippled` and `on-tag.yml` for tags in any `XRPLF` repository, both authenticating with the `NEXUS_REMOTE_USERNAME` / `NEXUS_REMOTE_PASSWORD` secrets already used for the Conan remote; `on-pr.yml` never publishes.
Nexus owns the repository metadata; nothing here indexes anything. Worth knowing:
- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP
signing keypair configured in Nexus, which rejects one created without a
keypair. Nexus signs the apt metadata with it, never the packages.
- Hosted yum repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
repository sits behind a `rpm-<channel>` yum group repository whose metadata
Nexus signs. Uploads go to the hosted repository; clients point at the group
and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs
themselves, so `sign_rpm.py` signs them before they are uploaded, and clients
verify them with `gpgcheck=1`.
- yum metadata is rebuilt asynchronously, so a successful publish is not
immediately installable.
- Each job uploads only what it built, and uploads are not transactional, so a
failure can leave one format published alone. Re-running is safe: both the apt
POST and the yum PUT replace an existing asset.
- The `develop` repositories gain a package per push, so they need a cleanup
policy to stay bounded; tagged channels publish each version once.
- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP signing keypair configured in Nexus, which rejects one created without a keypair. Nexus signs the apt metadata with it, never the packages.
- Hosted yum repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted` repository sits behind a `rpm-<channel>` yum group repository whose metadata Nexus signs. Uploads go to the hosted repository; clients point at the group and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs themselves, so `sign_rpm.py` signs them before they are uploaded, and clients verify them with `gpgcheck=1`.
- yum metadata is rebuilt asynchronously, so a successful publish is not immediately installable.
- Each job uploads only what it built, and uploads are not transactional, so a failure can leave one format published alone. Re-running is safe: both the apt POST and the yum PUT replace an existing asset.
- The `develop` repositories gain a package per push, so they need a cleanup policy to stay bounded; tagged channels publish each version once.
### Publishing from other repositories
`publish_pkg.py` knows nothing about `xrpld`, so the packaging image
installs it at `/usr/local/bin/publish_pkg.py` for other XRPLF repositories that
build their packages elsewhere.
`publish_pkg.py` knows nothing about `xrpld`, so the packaging image installs it at `/usr/local/bin/publish_pkg.py` for other XRPLF repositories that build their packages elsewhere.
## How `build_pkg.py` works
`build_pkg.py` derives the `xrpld` software version from
`${BUILD_DIR}/xrpld --version` in both package formats.
`build_pkg.py` derives the `xrpld` software version from `${BUILD_DIR}/xrpld --version` in both package formats.
The binary's version is already SemVer-validated by `BuildInfo`.
`build_pkg.py` converts pre-release versions such as `3.2.0-b1` or
`3.2.0-rc1` from `-` to `~` for package metadata so pre-releases sort before
the final release. If that normalized package version still contains `-`,
packaging fails because RPM forbids `-` in `Version`, and Debian uses `-` as
the upstream/revision separator.
The binary's version is already SemVer-validated by `BuildInfo`. `build_pkg.py` converts pre-release versions such as `3.2.0-b1` or `3.2.0-rc1` from `-` to `~` for package metadata so pre-releases sort before the final release. If that normalized package version still contains `-`, packaging fails because RPM forbids `-` in `Version`, and Debian uses `-` as the upstream/revision separator.
`pkg_version` is the normalized package metadata version derived inside
`build_pkg.py` from the binary-reported `xrpld` version (`-` pre-release
separator converted to `~`). It is not a separate user input.
`pkg_version` is the normalized package metadata version derived inside `build_pkg.py` from the binary-reported `xrpld` version (`-` pre-release separator converted to `~`). It is not a separate user input.
`PKG_RELEASE` is a different value: the package release iteration for that
`xrpld` version. RPM receives the normalized `pkg_version` and `PKG_RELEASE` as
the `pkg_version` and `pkg_release` macros for its `Version` and `Release`
values; DEB writes them as `${pkg_version}-${PKG_RELEASE}` in
`debian/changelog`.
`PKG_RELEASE` is a different value: the package release iteration for that `xrpld` version. RPM receives the normalized `pkg_version` and `PKG_RELEASE` as the `pkg_version` and `pkg_release` macros for its `Version` and `Release` values; DEB writes them as `${pkg_version}-${PKG_RELEASE}` in `debian/changelog`.
With `PKG_RELEASE=1`, the package metadata becomes:
@@ -221,70 +140,38 @@ With `PKG_RELEASE=1`, the package metadata becomes:
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
`build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it
from the build host, so the RHEL image can track a newer release without
changing what the packages claim to target.
`build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it from the build host, so the RHEL image can track a newer release without changing what the packages claim to target.
The Debian changelog entry carries the channel passed as `--channel`, which
only accepts the channels in the table above plus `UNRELEASED`, the Debian
convention for a build that targets no channel at all — what local and CMake
builds pass, since nothing publishes them. An unsupported pre-release, and
build metadata on a final release such as `3.2.0+abc123`, are both rejected.
The Debian changelog entry carries the channel passed as `--channel`, which only accepts the channels in the table above plus `UNRELEASED`, the Debian convention for a build that targets no channel at all — what local and CMake builds pass, since nothing publishes them. An unsupported pre-release, and build metadata on a final release such as `3.2.0+abc123`, are both rejected.
The RPM path intentionally uses `~` in `Version`, matching the Debian
pre-release ordering convention, so RPM filenames/NVRs begin with forms like
`xrpld-3.2.0~b1-...` and `xrpld-3.2.0~rc1-...` instead of encoding
pre-releases with an older `0.<release>.<suffix>` RPM `Release` value.
The RPM path intentionally uses `~` in `Version`, matching the Debian pre-release ordering convention, so RPM filenames/NVRs begin with forms like `xrpld-3.2.0~b1-...` and `xrpld-3.2.0~rc1-...` instead of encoding pre-releases with an older `0.<release>.<suffix>` RPM `Release` value.
The package format is `--package-type`, either `deb` or `rpm`. It is required,
so a job never silently builds the wrong format for the image it runs in; the
matching build tool still has to be on PATH.
The package format is `--package-type`, either `deb` or `rpm`. It is required, so a job never silently builds the wrong format for the image it runs in; the matching build tool still has to be on PATH.
Every input is a named argument, and every argument but `--build-dir` and
`--pkg-release` is required. The repository root is not an argument
at all: the script reads it from its own location. Only secrets stay in the
environment, so they never reach the process list -- `PKG_SIGNING_KEY` for
`sign_rpm.py`, and `NEXUS_USERNAME` / `NEXUS_PASSWORD` for `publish_pkg.py`.
Every input is a named argument, and every argument but `--build-dir` and `--pkg-release` is required. The repository root is not an argument at all: the script reads it from its own location. Only secrets stay in the environment, so they never reach the process list -- `PKG_SIGNING_KEY` for `sign_rpm.py`, and `NEXUS_USERNAME` / `NEXUS_PASSWORD` for `publish_pkg.py`.
Signing is not part of this script. `sign_rpm.py` does it in a separate CI step
that only runs when publishing, so a published RPM is always signed and a local
build never needs a key.
Signing is not part of this script. `sign_rpm.py` does it in a separate CI step that only runs when publishing, so a published RPM is always signed and a local build never needs a key.
It resolves the build directory to an absolute path, then calls
`stage_common()` to copy the `xrpld` and `validator-keys` binaries, config files,
and shared support files into the staging area, and invokes the platform build
tool. Both binaries must be present in the build directory and must run in the
packaging environment; a missing or non-runnable one fails early. That runtime
check is what catches a binary still linked against the Nix store's ELF loader (see
`patch_nix_binary` in `cmake/PatchNixBinary.cmake`).
It resolves the build directory to an absolute path, then calls `stage_common()` to copy the `xrpld` and `validator-keys` binaries, config files, and shared support files into the staging area, and invokes the platform build tool. Both binaries must be present in the build directory and must run in the packaging environment; a missing or non-runnable one fails early. That runtime check is what catches a binary still linked against the Nix store's ELF loader (see `patch_nix_binary` in `cmake/PatchNixBinary.cmake`).
### RPM
1. Creates the standard `rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}` tree inside the build directory.
2. Copies `xrpld.spec` and all shared source files (binaries, configs, service files) into `SOURCES/`.
3. Runs `rpmbuild -bb`, passing the normalized package metadata version as the
`pkg_version` RPM macro and `PKG_RELEASE` as the `pkg_release` RPM macro.
The spec uses manual `install` commands to place files, disables `dwz`, and
generates debuginfo packages.
3. Runs `rpmbuild -bb`, passing the normalized package metadata version as the `pkg_version` RPM macro and `PKG_RELEASE` as the `pkg_release` RPM macro. The spec uses manual `install` commands to place files, disables `dwz`, and generates debuginfo packages.
4. Output: `rpmbuild/RPMS/x86_64/xrpld-*.rpm`
RPM upgrades intentionally do not restart a running `xrpld` service. The spec
uses `%systemd_postun`, matching Debian's `dh_installsystemd
--no-stop-on-upgrade` behavior; operators pick up the new binary on the next
service restart.
RPM upgrades intentionally do not restart a running `xrpld` service. The spec uses `%systemd_postun`, matching Debian's `dh_installsystemd --no-stop-on-upgrade` behavior; operators pick up the new binary on the next service restart.
### DEB
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and
`validator-keys-LICENSE`.
2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and `validator-keys-LICENSE`.
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` using `${pkg_version}-${PKG_RELEASE}`,
where `pkg_version` is derived from the binary-reported `xrpld` version.
5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`, where `pkg_version` is derived from the binary-reported `xrpld` version.
6. Runs `dpkg-buildpackage -b --no-sign -d` (`-d` skips the build-dependency check, since the binary is already built). `debian/rules` uses manual `install` commands.
7. Output: `debbuild/*.deb`, the binary package and the `-dbgsym` package.
Debian gives dbgsym packages a `.deb` extension; only Ubuntu uses `.ddeb`.
7. Output: `debbuild/*.deb`, the binary package and the `-dbgsym` package. Debian gives dbgsym packages a `.deb` extension; only Ubuntu uses `.ddeb`.
## Post-build verification
@@ -301,11 +188,7 @@ lintian -I debbuild/*.deb
## Reproducibility
`build_pkg.py` sets `SOURCE_DATE_EPOCH` from the latest git commit time and
exports it; the RPM spec clamps file modification times to it via
`%build_mtime_policy`. The remaining variables
below further improve reproducibility but are _not_ set by the script — export
them yourself if needed:
`build_pkg.py` sets `SOURCE_DATE_EPOCH` from the latest git commit time and exports it; the RPM spec clamps file modification times to it via `%build_mtime_policy`. The remaining variables below further improve reproducibility but are _not_ set by the script — export them yourself if needed:
```bash
export TZ=UTC

View File

@@ -2,28 +2,12 @@
## Running Tests
Unit tests are bundled in the `xrpld` executable and can be executed using the
`--unittest` parameter. Without any arguments to this option, all non-manual
unit tests will be executed. If you want to run one or more manual tests, you
must specify it by suite or full-name (e.g. `xrpl.app.NoRippleCheckLimits` or
just `NoRippleCheckLimits`).
Unit tests are bundled in the `xrpld` executable and can be executed using the `--unittest` parameter. Without any arguments to this option, all non-manual unit tests will be executed. If you want to run one or more manual tests, you must specify it by suite or full-name (e.g. `xrpl.app.NoRippleCheckLimits` or just `NoRippleCheckLimits`).
More than one suite or group of suites can be specified as a comma separated
list via the argument. For example, `--unittest=beast,OversizeMeta` will run
all suites in the `beast` library (root identifier) as well as the test suite
named `OversizeMeta`). All name matches are case sensitive.
More than one suite or group of suites can be specified as a comma separated list via the argument. For example, `--unittest=beast,OversizeMeta` will run all suites in the `beast` library (root identifier) as well as the test suite named `OversizeMeta`). All name matches are case sensitive.
Tests can be executed in parallel using several child processes by specifying
the `--unittest-jobs=N` parameter. The default behavior is to execute serially
using a single process.
Tests can be executed in parallel using several child processes by specifying the `--unittest-jobs=N` parameter. The default behavior is to execute serially using a single process.
The order that suites are executed is determined by the suite priority that
is optionally specified when the suite is declared in the code with one of the
`BEAST_DEFINE_TESTSUITE` macros. By default, suites have a priority of 0, and
other suites can choose to declare an integer priority value to make themselves
execute before or after other suites based on their specified priority value.
The order that suites are executed is determined by the suite priority that is optionally specified when the suite is declared in the code with one of the `BEAST_DEFINE_TESTSUITE` macros. By default, suites have a priority of 0, and other suites can choose to declare an integer priority value to make themselves execute before or after other suites based on their specified priority value.
By default, the framework will emit the name of each testcase/testsuite when it
starts and any messages sent to the suite `log` stream. The `--quiet` option will
suppress both types of messages, but combining `--unittest-log` with `--quiet`
will cause `log` messages to be emitted while suite/case names are suppressed.
By default, the framework will emit the name of each testcase/testsuite when it starts and any messages sent to the suite `log` stream. The `--quiet` option will suppress both types of messages, but combining `--unittest-log` with `--quiet` will cause `log` messages to be emitted while suite/case names are suppressed.

View File

@@ -1,5 +1,3 @@
# Unit tests
This directory contains unit tests for the project. The difference from existing `src/test` folder
is that we switch to 3rd party testing framework (`gtest`). We intend to gradually move existing tests
from our own framework to `gtest` and such tests will be moved to this new folder.
This directory contains unit tests for the project. The difference from existing `src/test` folder is that we switch to 3rd party testing framework (`gtest`). We intend to gradually move existing tests from our own framework to `gtest` and such tests will be moved to this new folder.

View File

@@ -1,59 +1,30 @@
# Consensus Simulation Framework
The Consensus Simulation Framework is a set of software components for
describing, running and analyzing simulations of the consensus algorithm in a
controlled manner. It is also used to unit test the generic XRPL consensus
algorithm implementation. The framework is in its early stages, so the design
and supported features are subject to change.
The Consensus Simulation Framework is a set of software components for describing, running and analyzing simulations of the consensus algorithm in a controlled manner. It is also used to unit test the generic XRPL consensus algorithm implementation. The framework is in its early stages, so the design and supported features are subject to change.
## Overview
The simulation framework focuses on simulating the core consensus and validation
algorithms as a [discrete event
simulation](https://en.wikipedia.org/wiki/Discrete_event_simulation). It is
completely abstracted from the details of the XRP ledger and transactions. In
the simulation, a ledger is simply a set of observed integers and transactions
are single integers. The consensus process works to agree on the set of integers
to include in the next ledger.
The simulation framework focuses on simulating the core consensus and validation algorithms as a [discrete event simulation](https://en.wikipedia.org/wiki/Discrete_event_simulation). It is completely abstracted from the details of the XRP ledger and transactions. In the simulation, a ledger is simply a set of observed integers and transactions are single integers. The consensus process works to agree on the set of integers to include in the next ledger.
![CSF Overview](./csf_overview.png "CSF Overview")
The diagram above gives a stylized overview of the components provided by the
framework. These are combined by the simulation author into the simulation
specification, which defines the configuration of the system and the data to
collect when running the simulation. The specification includes:
The diagram above gives a stylized overview of the components provided by the framework. These are combined by the simulation author into the simulation specification, which defines the configuration of the system and the data to collect when running the simulation. The specification includes:
- A collection of [`Peer`s](./Peer.h) that represent the participants in the
network, with each independently running the consensus algorithm.
- The `Peer` trust relationships as a `TrustGraph`. This is a directed graph
whose edges define what other `Peer`s a given `Peer` trusts. In other words,
the set of out edges for a `Peer` in the graph correspond to the UNL of that
`Peer`.
- The network communication layer as a `BasicNetwork`. This models the overlay
network topology in which messages are routed between `Peer`s. This graph
topology can be configured independently from the `TrustGraph`.
- Transaction `Submitter`s that model the submission of client transactions to
the network.
- `Collector`s that aggregate, filter and analyze data from the simulation.
Typically, this is used to monitor invariants or generate reports.
- A collection of [`Peer`s](./Peer.h) that represent the participants in the network, with each independently running the consensus algorithm.
- The `Peer` trust relationships as a `TrustGraph`. This is a directed graph whose edges define what other `Peer`s a given `Peer` trusts. In other words, the set of out edges for a `Peer` in the graph correspond to the UNL of that `Peer`.
- The network communication layer as a `BasicNetwork`. This models the overlay network topology in which messages are routed between `Peer`s. This graph topology can be configured independently from the `TrustGraph`.
- Transaction `Submitter`s that model the submission of client transactions to the network.
- `Collector`s that aggregate, filter and analyze data from the simulation. Typically, this is used to monitor invariants or generate reports.
Once specified, the simulation runs using a single `Scheduler` that manages the
global clock and sequencing of activity. During the course of simulation,
`Peer`s generate `Ledger`s and `Validation`s as a result of consensus,
eventually fully validating the consensus history of accepted transactions. Each
`Peer` also issues various `Event`s during the simulation, which are analyzed by
the registered `Collector`s.
Once specified, the simulation runs using a single `Scheduler` that manages the global clock and sequencing of activity. During the course of simulation, `Peer`s generate `Ledger`s and `Validation`s as a result of consensus, eventually fully validating the consensus history of accepted transactions. Each `Peer` also issues various `Event`s during the simulation, which are analyzed by the registered `Collector`s.
## Example Simulation
Below is a basic simulation we can walk through to get an understanding of the
framework. This simulation is for a set of 5 validators that aren't directly
connected but rely on a single hub node for communication.
Below is a basic simulation we can walk through to get an understanding of the framework. This simulation is for a set of 5 validators that aren't directly connected but rely on a single hub node for communication.
![Example Sim](./csf_graph.png "Example Sim")
Each Peer has a unique transaction submitted, then runs one round of the
consensus algorithm.
Each Peer has a unique transaction submitted, then runs one round of the consensus algorithm.
```c++
Sim sim;
@@ -96,22 +67,9 @@ center[0]->runAsValidator = false;
```
The simulation code starts by creating a single instance of the [`Sim`
class](./Sim.h). This class is used to manage the overall simulation and
internally owns most other components, including the `Peer`s, `Scheduler`,
`BasicNetwork` and `TrustGraph`. The next two lines create two differ
`PeerGroup`s of size 5 and 1 . A [`PeerGroup`](./PeerGroup.h) is a convenient
way for configuring a set of related peers together and internally has a vector
of pointers to the `Peer`s which are owned by the `Sim`. `PeerGroup`s can be
combined using `+/-` operators to configure more complex relationships of nodes
as shown by `PeerGroup network`. Note that each call to `createGroup` adds that
many new `Peer`s to the simulation, but does not specify any trust or network
relationships for the new `Peer`s.
The simulation code starts by creating a single instance of the [`Sim` class](./Sim.h). This class is used to manage the overall simulation and internally owns most other components, including the `Peer`s, `Scheduler`, `BasicNetwork` and `TrustGraph`. The next two lines create two differ `PeerGroup`s of size 5 and 1 . A [`PeerGroup`](./PeerGroup.h) is a convenient way for configuring a set of related peers together and internally has a vector of pointers to the `Peer`s which are owned by the `Sim`. `PeerGroup`s can be combined using `+/-` operators to configure more complex relationships of nodes as shown by `PeerGroup network`. Note that each call to `createGroup` adds that many new `Peer`s to the simulation, but does not specify any trust or network relationships for the new `Peer`s.
Lastly, the single `Peer` in the size 1 `center` group is switched from running
as a validator (the default) to running as a tracking peer. The [`Peer`
class](./Peer.h) has a variety of configurable parameters that control how it
behaves during the simulation.
Lastly, the single `Peer` in the size 1 `center` group is switched from running as a validator (the default) to running as a tracking peer. The [`Peer` class](./Peer.h) has a variety of configurable parameters that control how it behaves during the simulation.
## `trust` and `connect`
@@ -124,18 +82,9 @@ SimDuration delay = 200ms;
validators.connect(center, delay);
```
Although the `sim` object has accessible instances of
[TrustGraph](./TrustGraph.h) and [BasicNetwork](./BasicNetwork.h), it is more
convenient to manage the graphs via the `PeerGroup`s. The first two lines
create a trust topology in which all `Peer`s trust the 5 validating `Peer`s. Or
in the UNL perspective, all `Peer`s are configured with the same UNL listing the
5 validating `Peer`s. The two lines could've been rewritten as
`network.trust(validators)`.
Although the `sim` object has accessible instances of [TrustGraph](./TrustGraph.h) and [BasicNetwork](./BasicNetwork.h), it is more convenient to manage the graphs via the `PeerGroup`s. The first two lines create a trust topology in which all `Peer`s trust the 5 validating `Peer`s. Or in the UNL perspective, all `Peer`s are configured with the same UNL listing the 5 validating `Peer`s. The two lines could've been rewritten as `network.trust(validators)`.
The next lines create the network communication topology. Each of the validating
`Peer`s connects to the central hub `Peer` with a fixed delay of 200ms. Note
that the network connections are really undirected, but are represented
internally in a directed graph using edge pairs of inbound and outbound connections.
The next lines create the network communication topology. Each of the validating `Peer`s connects to the central hub `Peer` with a fixed delay of 200ms. Note that the network connections are really undirected, but are represented internally in a directed graph using edge pairs of inbound and outbound connections.
## Collectors
@@ -144,20 +93,9 @@ SimDurationCollector simDur;
sim.collectors.add(simDur);
```
The next lines add a single collector to the simulation. The
`SimDurationCollector` is a simple example collector which tracks the total
duration of the simulation. More generally, a collector is any class that
implements `void on(NodeID, SimTime, Event)` for all [Events](./events.h)
emitted by a Peer. Events are arbitrary types used to indicate some action or
change of state of a `Peer`. Other [existing collectors](./collectors.h) measure
latencies of transaction submission to validation or the rate of ledger closing
and monitor any jumps in ledger history.
The next lines add a single collector to the simulation. The `SimDurationCollector` is a simple example collector which tracks the total duration of the simulation. More generally, a collector is any class that implements `void on(NodeID, SimTime, Event)` for all [Events](./events.h) emitted by a Peer. Events are arbitrary types used to indicate some action or change of state of a `Peer`. Other [existing collectors](./collectors.h) measure latencies of transaction submission to validation or the rate of ledger closing and monitor any jumps in ledger history.
Note that the collector lifetime is independent of the simulation and is added
to the simulation by reference. This is intentional, since collectors might be
used across several simulations to collect more complex combinations of data. At
the end of the simulation, we print out the total duration by subtracting
`simDur` members.
Note that the collector lifetime is independent of the simulation and is added to the simulation by reference. This is intentional, since collectors might be used across several simulations to collect more complex combinations of data. At the end of the simulation, we print out the total duration by subtracting `simDur` members.
```c++
std::cout << (simDur.stop - simDur.start).count() << std::endl;
@@ -171,22 +109,10 @@ for (Peer * p : validators)
p->submit(Tx(static_cast<std::uint32_t>(p->id)));
```
In this basic example, we explicitly submit a single transaction to each
validator. For larger simulations, clients can use a [Submitter](./submitters.h)
to send transactions in at fixed or random intervals to fixed or random `Peer`s.
In this basic example, we explicitly submit a single transaction to each validator. For larger simulations, clients can use a [Submitter](./submitters.h) to send transactions in at fixed or random intervals to fixed or random `Peer`s.
## Run
The example has two calls to `sim.run(1)`. This call runs the simulation until
each `Peer` has closed one additional ledger. After closing the additional
ledger, the `Peer` stops participating in consensus. The first call is used to
ensure a more useful prior state of all `Peer`s. After the transaction
submission, the second call to `run` results in one additional ledger that
accepts those transactions.
The example has two calls to `sim.run(1)`. This call runs the simulation until each `Peer` has closed one additional ledger. After closing the additional ledger, the `Peer` stops participating in consensus. The first call is used to ensure a more useful prior state of all `Peer`s. After the transaction submission, the second call to `run` results in one additional ledger that accepts those transactions.
Alternatively, you can specify a duration to run the simulation, e.g.
`sim.run(10s)` which would have `Peer`s continuously run consensus until the
scheduler has elapsed 10 additional seconds. The `sim.scheduler.in` or
`sim.scheduler.at` methods can schedule arbitrary code to execute at a later
time in the simulation, for example removing a network connection or modifying
the trust graph.
Alternatively, you can specify a duration to run the simulation, e.g. `sim.run(10s)` which would have `Peer`s continuously run consensus until the scheduler has elapsed 10 additional seconds. The `sim.scheduler.in` or `sim.scheduler.at` methods can schedule arbitrary code to execute at a later time in the simulation, for example removing a network connection or modifying the trust graph.

View File

@@ -1,12 +1,8 @@
# RCL Consensus
This directory holds the types and classes needed
to connect the generic consensus algorithm to the
xrpld-specific instance of consensus.
This directory holds the types and classes needed to connect the generic consensus algorithm to the xrpld-specific instance of consensus.
- `RCLCxTx` adapts a `SHAMapItem` transaction.
- `RCLCxTxSet` adapts a `SHAMap` to represent a set of transactions.
- `RCLCxLedger` adapts a `Ledger`.
- `RCLConsensus` is implements the requirements of the generic
`Consensus` class by connecting to the rest of the `xrpld`
application.
- `RCLConsensus` is implements the requirements of the generic `Consensus` class by connecting to the rest of the `xrpld` application.

View File

@@ -4,36 +4,15 @@
## Life Cycle
Every server always has an open ledger. All received new transactions are
applied to the open ledger. The open ledger can't close until we reach
a consensus on the previous ledger, and either: there is at least one
transaction in the open ledger, or the ledger's close time has been reached.
Every server always has an open ledger. All received new transactions are applied to the open ledger. The open ledger can't close until we reach a consensus on the previous ledger, and either: there is at least one transaction in the open ledger, or the ledger's close time has been reached.
When the open ledger is closed the transactions in the open ledger become
the initial proposal. Validators will send the proposal (non-validators will
simply not send the proposal). The open ledger contains the set of transactions
the server thinks should go into the next ledger.
When the open ledger is closed the transactions in the open ledger become the initial proposal. Validators will send the proposal (non-validators will simply not send the proposal). The open ledger contains the set of transactions the server thinks should go into the next ledger.
Once the ledger is closed, servers avalanche to consensus on the candidate
transaction set and the close time. When consensus is reached, servers build
a new last closed ledger by starting with the previous last closed ledger and
applying the consensus transaction set. In the normal case, servers will all
agree on both the last closed ledger and the consensus transaction set. The
goal is to give the network the highest chances of arriving at the same
conclusion on all servers.
Once the ledger is closed, servers avalanche to consensus on the candidate transaction set and the close time. When consensus is reached, servers build a new last closed ledger by starting with the previous last closed ledger and applying the consensus transaction set. In the normal case, servers will all agree on both the last closed ledger and the consensus transaction set. The goal is to give the network the highest chances of arriving at the same conclusion on all servers.
At all times during the consensus process the open ledger remains open with the
same transaction set, and has new transactions applied. It will likely have
transactions that are also in the new last closed ledger. Valid transactions
received during the consensus process will only be in the open ledger.
At all times during the consensus process the open ledger remains open with the same transaction set, and has new transactions applied. It will likely have transactions that are also in the new last closed ledger. Valid transactions received during the consensus process will only be in the open ledger.
Validators now publish validations of the new last closed ledger. Servers now
build a new open ledger from the last closed ledger. First, all transactions
that were candidates in the previous consensus round but didn't make it into
the consensus set are applied. Next, transactions in the current open ledger
are applied. This covers transactions received during the previous consensus
round. This is a "rebase": now that we know the real history, the current open
ledger is rebased against the last closed ledger.
Validators now publish validations of the new last closed ledger. Servers now build a new open ledger from the last closed ledger. First, all transactions that were candidates in the previous consensus round but didn't make it into the consensus set are applied. Next, transactions in the current open ledger are applied. This covers transactions received during the previous consensus round. This is a "rebase": now that we know the real history, the current open ledger is rebased against the last closed ledger.
The purpose of the open ledger is as follows:
@@ -42,20 +21,13 @@ The purpose of the open ledger is as follows:
## Byzantine Failures
Byzantine failures are resolved as follows. If there is a supermajority ledger,
then a minority of validators will discover that the consensus round is
proceeding on a different ledger than they thought. These validators will
become desynced, and switch to a strategy of trying to acquire the consensus
ledger.
Byzantine failures are resolved as follows. If there is a supermajority ledger, then a minority of validators will discover that the consensus round is proceeding on a different ledger than they thought. These validators will become desynced, and switch to a strategy of trying to acquire the consensus ledger.
If there is no majority ledger, then starting on the next consensus round there
will not be a consensus on the last closed ledger. Another avalanche process
is started.
If there is no majority ledger, then starting on the next consensus round there will not be a consensus on the last closed ledger. Another avalanche process is started.
## Validators
The only meaningful difference between a validator and a 'regular' server is
that the validator sends its proposals and validations to the network.
The only meaningful difference between a validator and a 'regular' server is that the validator sends its proposals and validations to the network.
---
@@ -68,65 +40,31 @@ There are two ledgers that are the most important for an xrpld server to have:
- The consensus ledger and
- The last validated ledger.
If we need either of those two ledgers they are fetched with the highest
priority. Also, when they arrive, they replace their earlier counterparts
(if they exist).
If we need either of those two ledgers they are fetched with the highest priority. Also, when they arrive, they replace their earlier counterparts (if they exist).
The `LedgerMaster` object tracks
- the last published ledger,
- the last validated ledger, and
- ledger history.
So the `LedgerMaster` is at the center of fetching historical ledger data.
- ledger history. So the `LedgerMaster` is at the center of fetching historical ledger data.
In specific, the `LedgerMaster::doAdvance()` method triggers the code that
fetches historical data and controls the state machine for ledger acquisition.
In specific, the `LedgerMaster::doAdvance()` method triggers the code that fetches historical data and controls the state machine for ledger acquisition.
The server tries to publish an on-going stream of consecutive ledgers to its
clients. After the server has started and caught up with network
activity, say when ledger 500 is being settled, then the server puts its best
effort into publishing validated ledger 500 followed by validated ledger 501
and then 502. This effort continues until the server is shut down.
The server tries to publish an on-going stream of consecutive ledgers to its clients. After the server has started and caught up with network activity, say when ledger 500 is being settled, then the server puts its best effort into publishing validated ledger 500 followed by validated ledger 501 and then 502. This effort continues until the server is shut down.
But loading or network connectivity may sometimes interfere with that ledger
stream. So suppose the server publishes validated ledger 600 and then
receives validated ledger 603. Then the server wants to back fill its ledger
history with ledgers 601 and 602.
But loading or network connectivity may sometimes interfere with that ledger stream. So suppose the server publishes validated ledger 600 and then receives validated ledger 603. Then the server wants to back fill its ledger history with ledgers 601 and 602.
The server prioritizes keeping up with current ledgers. But if it is caught
up on the current ledger, and there are no higher priority demands on the
server, then it will attempt to back fill its historical ledgers. It fills
in the historical ledger data first by attempting to retrieve it from the
local database. If the local database does not have all of the necessary data
then the server requests the remaining information from network peers.
The server prioritizes keeping up with current ledgers. But if it is caught up on the current ledger, and there are no higher priority demands on the server, then it will attempt to back fill its historical ledgers. It fills in the historical ledger data first by attempting to retrieve it from the local database. If the local database does not have all of the necessary data then the server requests the remaining information from network peers.
Suppose the server is missing multiple historical ledgers. Take the previous
example where we have ledgers 603 and 600, but we're missing 601 and 602. In
that case the server requests information for ledger 602 first, before
back-filling ledger 601. We want to expand the contiguous range of
most-recent ledgers that the server has locally. There's also a limit to
how much historical ledger data is useful. So if we're on ledger 603, but
we're missing ledger 4 we may not bother asking for ledger 4.
Suppose the server is missing multiple historical ledgers. Take the previous example where we have ledgers 603 and 600, but we're missing 601 and 602. In that case the server requests information for ledger 602 first, before back-filling ledger 601. We want to expand the contiguous range of most-recent ledgers that the server has locally. There's also a limit to how much historical ledger data is useful. So if we're on ledger 603, but we're missing ledger 4 we may not bother asking for ledger 4.
## Assembling a Ledger
When data for a ledger arrives from a peer, it may take a while before the
server can apply that data. So when ledger data arrives we schedule a job
thread to apply that data. If more data arrives before the job starts we add
that data to the job. We defer requesting more ledger data until all of the
data we have for that ledger has been processed. Once all of that data is
processed we can intelligently request only the additional data that we need
to fill in the ledger. This reduces network traffic and minimizes the load
on peers supplying the data.
When data for a ledger arrives from a peer, it may take a while before the server can apply that data. So when ledger data arrives we schedule a job thread to apply that data. If more data arrives before the job starts we add that data to the job. We defer requesting more ledger data until all of the data we have for that ledger has been processed. Once all of that data is processed we can intelligently request only the additional data that we need to fill in the ledger. This reduces network traffic and minimizes the load on peers supplying the data.
If we receive data for a ledger that is not currently under construction,
we don't just throw the data away. In particular the AccountStateNodes
may be useful, since they can be re-used across ledgers. This data is
stashed in memory (not the database) where the acquire process can find
it.
If we receive data for a ledger that is not currently under construction, we don't just throw the data away. In particular the AccountStateNodes may be useful, since they can be re-used across ledgers. This data is stashed in memory (not the database) where the acquire process can find it.
Peers deliver ledger data in the order in which the data can be validated.
Data arrives in the following order:
Peers deliver ledger data in the order in which the data can be validated. Data arrives in the following order:
1. The hash of the ledger header
2. The ledger header
@@ -134,63 +72,29 @@ Data arrives in the following order:
4. The lower (non-root) nodes of the state tree
5. The lower (non-root) nodes of the transaction tree
Inner-most nodes are supplied before outer nodes. This allows the
requesting server to hook things up (and validate) in the order in which
data arrives.
Inner-most nodes are supplied before outer nodes. This allows the requesting server to hook things up (and validate) in the order in which data arrives.
If this process fails, then a server can also ask for ledger data by hash,
rather than by asking for specific nodes in a ledger. Asking for information
by hash is less efficient, but it allows a peer to return the information
even if the information is not assembled into a tree. All the peer needs is
the raw data.
If this process fails, then a server can also ask for ledger data by hash, rather than by asking for specific nodes in a ledger. Asking for information by hash is less efficient, but it allows a peer to return the information even if the information is not assembled into a tree. All the peer needs is the raw data.
## Which Peer To Ask
Peers go though state transitions as the network goes through its state
transitions. Peer's provide their state to their directly connected peers.
By monitoring the state of each connected peer a server can tell which of
its peers has the information that it needs.
Peers go though state transitions as the network goes through its state transitions. Peer's provide their state to their directly connected peers. By monitoring the state of each connected peer a server can tell which of its peers has the information that it needs.
Therefore if a server suffers a byzantine failure the server can tell which
of its peers did not suffer that same failure. So the server knows which
peer(s) to ask for the missing information.
Therefore if a server suffers a byzantine failure the server can tell which of its peers did not suffer that same failure. So the server knows which peer(s) to ask for the missing information.
Peers also report their contiguous range of ledgers. This is another way that
a server can determine which peer to ask for a particular ledger or piece of
a ledger.
Peers also report their contiguous range of ledgers. This is another way that a server can determine which peer to ask for a particular ledger or piece of a ledger.
There are also indirect peer queries. If there have been timeouts while
acquiring ledger data then a server may issue indirect queries. In that
case the server receiving the indirect query passes the query along to any
of its peers that may have the requested data. This is important if the
network has a byzantine failure. It also helps protect the validation
network. A validator may need to get a peer set from one of the other
validators, and indirect queries improve the likelihood of success with
that.
There are also indirect peer queries. If there have been timeouts while acquiring ledger data then a server may issue indirect queries. In that case the server receiving the indirect query passes the query along to any of its peers that may have the requested data. This is important if the network has a byzantine failure. It also helps protect the validation network. A validator may need to get a peer set from one of the other validators, and indirect queries improve the likelihood of success with that.
## Kinds of Fetch Packs
A FetchPack is the way that peers send partial ledger data to other peers
so the receiving peer can reconstruct a ledger.
A FetchPack is the way that peers send partial ledger data to other peers so the receiving peer can reconstruct a ledger.
A 'normal' FetchPack is a bucket of nodes indexed by hash. The server
building the FetchPack puts information into the FetchPack that the
destination server is likely to need. Normally they contain all of the
missing nodes needed to fill in a ledger.
A 'normal' FetchPack is a bucket of nodes indexed by hash. The server building the FetchPack puts information into the FetchPack that the destination server is likely to need. Normally they contain all of the missing nodes needed to fill in a ledger.
A 'compact' FetchPack, on the other hand, contains only leaf nodes, no
inner nodes. Because there are no inner nodes, the ledger information that
it contains cannot be validated as the ledger is assembled. We have to,
initially, take the accuracy of the FetchPack for granted and assemble the
ledger. Once the entire ledger is assembled the entire ledger can be
validated. But if the ledger does not validate then there's nothing to be
done but throw the entire FetchPack away; there's no way to save a portion
of the FetchPack.
A 'compact' FetchPack, on the other hand, contains only leaf nodes, no inner nodes. Because there are no inner nodes, the ledger information that it contains cannot be validated as the ledger is assembled. We have to, initially, take the accuracy of the FetchPack for granted and assemble the ledger. Once the entire ledger is assembled the entire ledger can be validated. But if the ledger does not validate then there's nothing to be done but throw the entire FetchPack away; there's no way to save a portion of the FetchPack.
The FetchPacks just described could be termed 'reverse FetchPacks.' They
only provide historical data. There may be a use for what could be called a
'forward FetchPack.' A forward FetchPack would contain the information that
is needed to build a new ledger out of the preceding ledger.
The FetchPacks just described could be termed 'reverse FetchPacks.' They only provide historical data. There may be a use for what could be called a 'forward FetchPack.' A forward FetchPack would contain the information that is needed to build a new ledger out of the preceding ledger.
A forward compact FetchPack would need to contain:
@@ -206,50 +110,35 @@ A forward compact FetchPack would need to contain:
## Open Ledger
The open ledger is the ledger that the server applies all new incoming
transactions to.
The open ledger is the ledger that the server applies all new incoming transactions to.
## Last Validated Ledger
The most recent ledger that the server is certain will always remain part
of the permanent, public history.
The most recent ledger that the server is certain will always remain part of the permanent, public history.
## Last Closed Ledger
The most recent ledger that the server believes the network reached consensus
on. Different servers can arrive at a different conclusion about the last
closed ledger. This is a consequence of Byzantanine failure. The purpose of
validations is to resolve the differences between servers and come to a common
conclusion about which last closed ledger is authoritative.
The most recent ledger that the server believes the network reached consensus on. Different servers can arrive at a different conclusion about the last closed ledger. This is a consequence of Byzantanine failure. The purpose of validations is to resolve the differences between servers and come to a common conclusion about which last closed ledger is authoritative.
## Consensus
A distributed agreement protocol. XRPL uses the consensus process to solve
the problem of double-spending.
A distributed agreement protocol. XRPL uses the consensus process to solve the problem of double-spending.
## Validation
A signed statement indicating that it built a particular ledger as a result
of the consensus process.
A signed statement indicating that it built a particular ledger as a result of the consensus process.
## Proposal
A signed statement of which transactions it believes should be included in
the next consensus ledger.
A signed statement of which transactions it believes should be included in the next consensus ledger.
## Ledger Header
The "ledger header" is the chunk of data that hashes to the
ledger's hash. It contains the sequence number, parent hash,
hash of the previous ledger, hash of the root node of the
state tree, and so on.
The "ledger header" is the chunk of data that hashes to the ledger's hash. It contains the sequence number, parent hash, hash of the previous ledger, hash of the root node of the state tree, and so on.
## Ledger Base
The term "ledger base" refers to a particular type of query
and response used in the ledger fetch process that includes
the ledger header but may also contain other information
such as the root node of the state tree.
The term "ledger base" refers to a particular type of query and response used in the ledger fetch process that includes the ledger header but may also contain other information such as the root node of the state tree.
---
@@ -265,32 +154,19 @@ such as the root node of the state tree.
**LedgerEntryType:** "AccountRoot"
**OwnerCount:** The number of items the account owns that are charged to the
account. Offers are charged to the account. Trust lines may be charged to
the account (but not necessarily). The OwnerCount determines the reserve on
the account.
**OwnerCount:** The number of items the account owns that are charged to the account. Offers are charged to the account. Trust lines may be charged to the account (but not necessarily). The OwnerCount determines the reserve on the account.
**PreviousTxnID:** 256-bit index of the previous transaction on this account.
**PreviousTxnLgrSeq:** Ledger number sequence number of the previous
transaction on this account.
**PreviousTxnLgrSeq:** Ledger number sequence number of the previous transaction on this account.
**Sequence:** Must be a value of 1 for the account to process a valid
transaction. The value initially matches the sequence number of the state
tree of the account that signed the transaction. The process of executing
the transaction increments the sequence number. This is how ripple prevents
a transaction from executing more than once.
**Sequence:** Must be a value of 1 for the account to process a valid transaction. The value initially matches the sequence number of the state tree of the account that signed the transaction. The process of executing the transaction increments the sequence number. This is how ripple prevents a transaction from executing more than once.
**index:** 256-bit hash of this AccountRoot.
## Trust Line
The trust line acts as an edge connecting two accounts: the accounts
represented by the HighNode and the LowNode. Which account is "high" and
"low" is determined by the values of the two 160-bit account IDs. The
account with the smaller 160-bit ID is always the low account. This
ordering makes the hash of a trust line between accounts A and B have the
same value as a trust line between accounts B and A.
The trust line acts as an edge connecting two accounts: the accounts represented by the HighNode and the LowNode. Which account is "high" and "low" is determined by the values of the two 160-bit account IDs. The account with the smaller 160-bit ID is always the low account. This ordering makes the hash of a trust line between accounts A and B have the same value as a trust line between accounts B and A.
**Balance:**
@@ -320,8 +196,7 @@ same value as a trust line between accounts B and A.
**PreviousTxnID:** 256-bit hash of the previous transaction on this account.
**PreviousTxnLgrSeq:** Ledger number sequence number of the previous
transaction on this account.
**PreviousTxnLgrSeq:** Ledger number sequence number of the previous transaction on this account.
**index:** 256-bit hash of this RippleState.
@@ -357,28 +232,17 @@ Lists all of the offers and trust lines that are associated with an account.
Lists one or more offers that have the same quality.
If a pair of Currency and Issuer fields are all zeros, then that pair is
dealing in XRP.
If a pair of Currency and Issuer fields are all zeros, then that pair is dealing in XRP.
The code, at the moment, does not recognize that the Currency and Issuer
fields are currencies and issuers. So those values are presented in hex,
rather than as accounts and currencies. That's a bug and should be fixed
at some point.
The code, at the moment, does not recognize that the Currency and Issuer fields are currencies and issuers. So those values are presented in hex, rather than as accounts and currencies. That's a bug and should be fixed at some point.
**ExchangeRate:** A 64-bit value. The first 8-bits is the exponent and the
remaining bits are the mantissa. The format is such that a bigger 64-bit
value always represents a higher exchange rate.
**ExchangeRate:** A 64-bit value. The first 8-bits is the exponent and the remaining bits are the mantissa. The format is such that a bigger 64-bit value always represents a higher exchange rate.
Each type can compute its own hash. The hash of a book directory contains,
as its lowest 64 bits, the exchange rate. This means that if there are
multiple _almost_ identical book directories, but with different exchange
rates, then these book directories will sit together in the ledger. The best
exchange rate will be the first in the sequence of Book Directories.
Each type can compute its own hash. The hash of a book directory contains, as its lowest 64 bits, the exchange rate. This means that if there are multiple _almost_ identical book directories, but with different exchange rates, then these book directories will sit together in the ledger. The best exchange rate will be the first in the sequence of Book Directories.
**Flags:** ???
**Indexes:** 256-bit hashes of offers that match the exchange rate and
currencies described by this BookDirectory.
**Indexes:** 256-bit hashes of offers that match the exchange rate and currencies described by this BookDirectory.
**LedgerEntryType:** "DirectoryNode".
@@ -392,9 +256,7 @@ currencies described by this BookDirectory.
**TakerPaysIssuer:** Issuer of the PaysCurrency.
**index:** A 256-bit hash computed using the TakerGetsCurrency, TakerGetsIssuer,
TakerPaysCurrency, and TakerPaysIssuer in the top 192 bits. The lower 64-bits
are occupied by the exchange rate.
**index:** A 256-bit hash computed using the TakerGetsCurrency, TakerGetsIssuer, TakerPaysCurrency, and TakerPaysIssuer in the top 192 bits. The lower 64-bits are occupied by the exchange rate.
---
@@ -402,32 +264,19 @@ are occupied by the exchange rate.
## Overview
The XRPL server permits clients to subscribe to a continuous stream of
fully-validated ledgers. The publication code maintains this stream.
The XRPL server permits clients to subscribe to a continuous stream of fully-validated ledgers. The publication code maintains this stream.
The server attempts to maintain this continuous stream unless it falls
too far behind, in which case it jumps to the current fully-validated
ledger and then attempts to resume a continuous stream.
The server attempts to maintain this continuous stream unless it falls too far behind, in which case it jumps to the current fully-validated ledger and then attempts to resume a continuous stream.
## Implementation
`LedgerMaster::doAdvance` is invoked when work may need to be done to
publish ledgers to clients. This code loops until it cannot make further
progress.
`LedgerMaster::doAdvance` is invoked when work may need to be done to publish ledgers to clients. This code loops until it cannot make further progress.
`LedgerMaster::findNewLedgersToPublish` is called first. If the last
fully-valid ledger's sequence number is greater than the last published
ledger's sequence number, it attempts to publish those ledgers, retrieving
them if needed.
`LedgerMaster::findNewLedgersToPublish` is called first. If the last fully-valid ledger's sequence number is greater than the last published ledger's sequence number, it attempts to publish those ledgers, retrieving them if needed.
If there are no new ledgers to publish, `doAdvance` determines if it can
backfill history. If the publication is not caught up, backfilling is not
attempted to conserve resources.
If there are no new ledgers to publish, `doAdvance` determines if it can backfill history. If the publication is not caught up, backfilling is not attempted to conserve resources.
If history can be backfilled, the missing ledger with the highest
sequence number is retrieved first. If a historical ledger is retrieved,
and its predecessor is in the database, `tryFill` is invoked to update
the list of resident ledgers.
If history can be backfilled, the missing ledger with the highest sequence number is retrieved first. If a historical ledger is retrieved, and its predecessor is in the database, `tryFill` is invoked to update the list of resident ledgers.
---
@@ -435,41 +284,23 @@ the list of resident ledgers.
## Overview
The ledger cleaner checks and, if necessary, repairs the SQLite ledger and
transaction databases. It can also check for pieces of a ledger that should
be in the node back end but are missing. If it detects this case, it
triggers a fetch of the ledger. The ledger cleaner only operates by manual
request. It is never started automatically.
The ledger cleaner checks and, if necessary, repairs the SQLite ledger and transaction databases. It can also check for pieces of a ledger that should be in the node back end but are missing. If it detects this case, it triggers a fetch of the ledger. The ledger cleaner only operates by manual request. It is never started automatically.
## Operations
The ledger cleaner can operate on a single ledger or a range of ledgers. It
always validates the ledger chain itself, ensuring that the SQLite database
contains a consistent chain of ledgers from the last validated ledger as far
back as the database goes.
The ledger cleaner can operate on a single ledger or a range of ledgers. It always validates the ledger chain itself, ensuring that the SQLite database contains a consistent chain of ledgers from the last validated ledger as far back as the database goes.
If requested, it can additionally repair the SQLite entries for transactions
in each checked ledger. This was primarily intended to repair incorrect
entries created by a bug (since fixed) that could cause transactions from a
ledger other than the fully-validated ledger to appear in the SQLite
databases in addition to the transactions from the correct ledger.
If requested, it can additionally repair the SQLite entries for transactions in each checked ledger. This was primarily intended to repair incorrect entries created by a bug (since fixed) that could cause transactions from a ledger other than the fully-validated ledger to appear in the SQLite databases in addition to the transactions from the correct ledger.
If requested, it can additionally check the ledger for missing entries
in the account state and transaction trees.
If requested, it can additionally check the ledger for missing entries in the account state and transaction trees.
To prevent the ledger cleaner from saturating the available I/O bandwidth
and excessively polluting caches with ancient information, the ledger
cleaner paces itself and does not attempt to get its work done quickly.
To prevent the ledger cleaner from saturating the available I/O bandwidth and excessively polluting caches with ancient information, the ledger cleaner paces itself and does not attempt to get its work done quickly.
## Commands
The ledger cleaner can be controlled and monitored with the **ledger_cleaner**
RPC command. With no parameters, this command reports on the status of the
ledger cleaner. This includes the range of ledgers it has been asked to process,
the checks it is doing, and the number of errors it has found.
The ledger cleaner can be controlled and monitored with the **ledger_cleaner** RPC command. With no parameters, this command reports on the status of the ledger cleaner. This includes the range of ledgers it has been asked to process, the checks it is doing, and the number of errors it has found.
The ledger cleaner can be started, stopped, or have its behavior changed by
the following RPC parameters:
The ledger cleaner can be started, stopped, or have its behavior changed by the following RPC parameters:
**stop**: Stops the ledger cleaner gracefully
@@ -479,11 +310,9 @@ the following RPC parameters:
**full**: Requests all operations to be done on the specified ledger(s)
**fix_txns**: A boolean indicating whether to replace the SQLite transaction
entries unconditionally
**fix_txns**: A boolean indicating whether to replace the SQLite transaction entries unconditionally
**check_nodes**: A boolean indicating whether to check the specified
ledger(s) for missing nodes in the back end node store
**check_nodes**: A boolean indicating whether to check the specified ledger(s) for missing nodes in the back end node store
---

View File

@@ -7,180 +7,70 @@ Xrpld's fee mechanism consists of several interrelated processes:
## Fee Escalation
The guiding principal of fee escalation is that when things are going
smoothly, fees stay low, but as soon as high levels of traffic appear
on the network, fees will grow quickly to extreme levels. This should
dissuade malicious users from abusing the system, while giving
legitimate users the ability to pay a higher fee to get high-priority
transactions into the open ledger, even during unfavorable conditions.
The guiding principal of fee escalation is that when things are going smoothly, fees stay low, but as soon as high levels of traffic appear on the network, fees will grow quickly to extreme levels. This should dissuade malicious users from abusing the system, while giving legitimate users the ability to pay a higher fee to get high-priority transactions into the open ledger, even during unfavorable conditions.
How fees escalate:
1. There is a base [fee level](#fee-level) of 256,
which is the minimum that a typical transaction
is required to pay. For a [reference
transaction](#reference-transaction), that corresponds to the
network base fee, which is currently 10 drops.
2. However, there is a limit on the number of transactions that
can get into an open ledger for that base fee level. The limit
will vary based on the [health](#consensus-health) of the
consensus process, but will be at least [5](#other-constants).
1. There is a base [fee level](#fee-level) of 256, which is the minimum that a typical transaction is required to pay. For a [reference transaction](#reference-transaction), that corresponds to the network base fee, which is currently 10 drops.
2. However, there is a limit on the number of transactions that can get into an open ledger for that base fee level. The limit will vary based on the [health](#consensus-health) of the consensus process, but will be at least [5](#other-constants).
- If consensus stays [healthy](#consensus-health), the limit will
be the max of the number of transactions in the validated ledger
plus [20%](#other-constants) or the current limit until it gets
to [50](#other-constants), at which point, the limit will be the
largest number of transactions plus [20%](#other-constants)
in the last [20](#other-constants) validated ledgers which had
more than [50](#other-constants) transactions. Any time the limit
decreases (i.e. a large ledger is no longer recent), the limit will
decrease to the new largest value by 10% each time the ledger has
more than 50 transactions.
- If consensus does not stay [healthy](#consensus-health),
the limit will clamp down to the smaller of the number of
transactions in the validated ledger minus [50%](#other-constants)
or the previous limit minus [50%](#other-constants).
- The intended effect of these mechanisms is to allow as many base fee
level transactions to get into the ledger as possible while the
network is [healthy](#consensus-health), but to respond quickly to
any condition that makes it [unhealthy](#consensus-health), including,
but not limited to, malicious attacks.
- If consensus stays [healthy](#consensus-health), the limit will be the max of the number of transactions in the validated ledger plus [20%](#other-constants) or the current limit until it gets to [50](#other-constants), at which point, the limit will be the largest number of transactions plus [20%](#other-constants) in the last [20](#other-constants) validated ledgers which had more than [50](#other-constants) transactions. Any time the limit decreases (i.e. a large ledger is no longer recent), the limit will decrease to the new largest value by 10% each time the ledger has more than 50 transactions.
- If consensus does not stay [healthy](#consensus-health), the limit will clamp down to the smaller of the number of transactions in the validated ledger minus [50%](#other-constants) or the previous limit minus [50%](#other-constants).
- The intended effect of these mechanisms is to allow as many base fee level transactions to get into the ledger as possible while the network is [healthy](#consensus-health), but to respond quickly to any condition that makes it [unhealthy](#consensus-health), including, but not limited to, malicious attacks.
3. Once there are more transactions in the open ledger than indicated
by the limit, the required fee level jumps drastically.
3. Once there are more transactions in the open ledger than indicated by the limit, the required fee level jumps drastically.
- The formula is `( lastLedgerMedianFeeLevel *
TransactionsInOpenLedger^2 / limit^2 )`,
and returns a [fee level](#fee-level).
- The formula is `( lastLedgerMedianFeeLevel * TransactionsInOpenLedger^2 / limit^2 )`, and returns a [fee level](#fee-level).
4. That may still be pretty small, but as more transactions get
into the ledger, the fee level increases exponentially.
4. That may still be pretty small, but as more transactions get into the ledger, the fee level increases exponentially.
- For example, if the limit is 6, and the median fee is minimal,
and assuming all [reference transactions](#reference-transaction),
the 8th transaction only requires a [level](#fee-level) of about 174,000
or about 6800 drops,
but the 20th transaction requires a [level](#fee-level) of about
1,283,000 or about 50,000 drops.
- For example, if the limit is 6, and the median fee is minimal, and assuming all [reference transactions](#reference-transaction), the 8th transaction only requires a [level](#fee-level) of about 174,000 or about 6800 drops, but the 20th transaction requires a [level](#fee-level) of about 1,283,000 or about 50,000 drops.
5. Finally, as each ledger closes, the median fee level of that ledger is
computed and used as `lastLedgerMedianFeeLevel` (with a
[minimum value of 128,000](#other-constants))
in the fee escalation formula for the next open ledger.
5. Finally, as each ledger closes, the median fee level of that ledger is computed and used as `lastLedgerMedianFeeLevel` (with a [minimum value of 128,000](#other-constants)) in the fee escalation formula for the next open ledger.
- Continuing the example above, if ledger consensus completes with
only those 20 transactions, and all of those transactions paid the
minimum required fee at each step, the limit will be adjusted from
6 to 24, and the `lastLedgerMedianFeeLevel` will be about 322,000,
which is 12,600 drops for a
[reference transaction](#reference-transaction).
- This will only require 10 drops for the first 25 transactions,
but the 26th transaction will require a level of about 349,150
or about 13,649 drops.
- Continuing the example above, if ledger consensus completes with only those 20 transactions, and all of those transactions paid the minimum required fee at each step, the limit will be adjusted from 6 to 24, and the `lastLedgerMedianFeeLevel` will be about 322,000, which is 12,600 drops for a [reference transaction](#reference-transaction).
- This will only require 10 drops for the first 25 transactions, but the 26th transaction will require a level of about 349,150 or about 13,649 drops.
- This example assumes a cold-start scenario, with a single, possibly
malicious, user willing to pay arbitrary amounts to get transactions
into the open ledger. It ignores the effects of the [Transaction
Queue](#transaction-queue). Any lower fee level transactions submitted
by other users at the same time as this user's transactions will go into
the transaction queue, and will have the first opportunity to be applied
to the _next_ open ledger. The next section describes how that works in
more detail.
- This example assumes a cold-start scenario, with a single, possibly malicious, user willing to pay arbitrary amounts to get transactions into the open ledger. It ignores the effects of the [Transaction Queue](#transaction-queue). Any lower fee level transactions submitted by other users at the same time as this user's transactions will go into the transaction queue, and will have the first opportunity to be applied to the _next_ open ledger. The next section describes how that works in more detail.
## Transaction Queue
An integral part of making fee escalation work for users of the network
is the transaction queue. The queue allows legitimate transactions to be
considered by the network for future ledgers if the escalated open
ledger fee gets too high. This allows users to submit low priority
transactions with a low fee, and wait for high fees to drop. It also
allows legitimate users to continue submitting transactions during high
traffic periods, and give those transactions a much better chance to
succeed.
An integral part of making fee escalation work for users of the network is the transaction queue. The queue allows legitimate transactions to be considered by the network for future ledgers if the escalated open ledger fee gets too high. This allows users to submit low priority transactions with a low fee, and wait for high fees to drop. It also allows legitimate users to continue submitting transactions during high traffic periods, and give those transactions a much better chance to succeed.
1. If an incoming transaction meets both the base [fee
level](#fee-level) and the [load fee](#load-fee) minimum, but does not have a high
enough [fee level](#fee-level) to immediately go into the open ledger,
it is instead put into the queue and broadcast to peers. Each peer will
then make an independent decision about whether to put the transaction
into its open ledger or the queue. In principle, peers with identical
open ledgers will come to identical decisions. Any discrepancies will be
resolved as usual during consensus.
2. When consensus completes, the open ledger limit is adjusted, and
the required [fee level](#fee-level) drops back to the base
[fee level](#fee-level). Before the ledger is made available to
external transactions, transactions are applied from the queue to the
ledger from highest [fee level](#fee-level) to lowest. These transactions
count against the open ledger limit, so the required [fee level](#fee-level)
may start rising during this process.
3. Once the queue is empty, or the required [fee level](#fee-level)
rises too high for the remaining transactions in the queue, the ledger
is opened up for normal transaction processing.
4. A transaction in the queue can stay there indefinitely in principle,
but in practice, either
1. If an incoming transaction meets both the base [fee level](#fee-level) and the [load fee](#load-fee) minimum, but does not have a high enough [fee level](#fee-level) to immediately go into the open ledger, it is instead put into the queue and broadcast to peers. Each peer will then make an independent decision about whether to put the transaction into its open ledger or the queue. In principle, peers with identical open ledgers will come to identical decisions. Any discrepancies will be resolved as usual during consensus.
2. When consensus completes, the open ledger limit is adjusted, and the required [fee level](#fee-level) drops back to the base [fee level](#fee-level). Before the ledger is made available to external transactions, transactions are applied from the queue to the ledger from highest [fee level](#fee-level) to lowest. These transactions count against the open ledger limit, so the required [fee level](#fee-level) may start rising during this process.
3. Once the queue is empty, or the required [fee level](#fee-level) rises too high for the remaining transactions in the queue, the ledger is opened up for normal transaction processing.
4. A transaction in the queue can stay there indefinitely in principle, but in practice, either
- it will eventually get applied to the ledger,
- it will attempt to apply to the ledger and fail,
- it will attempt to apply to the ledger and retry [10
times](#other-constants),
- it will attempt to apply to the ledger and retry [10 times](#other-constants),
- its last ledger sequence number will expire,
- the user will replace it by submitting another transaction with the same
sequence number and at least a [25% higher fee](#other-constants), or
- it will get dropped when the queue fills up with more valuable transactions.
The size limit is computed dynamically, and can hold transactions for
the next [20 ledgers](#other-constants) (restricted to a minimum of
[2000 transactions](#other-constants)). The lower the transaction's
fee, the more likely that it will get dropped if the network is busy.
- the user will replace it by submitting another transaction with the same sequence number and at least a [25% higher fee](#other-constants), or
- it will get dropped when the queue fills up with more valuable transactions. The size limit is computed dynamically, and can hold transactions for the next [20 ledgers](#other-constants) (restricted to a minimum of [2000 transactions](#other-constants)). The lower the transaction's fee, the more likely that it will get dropped if the network is busy.
If a transaction is submitted for an account with one or more transactions
already in the queue, and a sequence number that is sequential with the other
transactions in the queue for that account, it will be considered
for the queue if it meets these additional criteria:
If a transaction is submitted for an account with one or more transactions already in the queue, and a sequence number that is sequential with the other transactions in the queue for that account, it will be considered for the queue if it meets these additional criteria:
- the account has fewer than [10](#other-constants) transactions
already in the queue.
- all other queued transactions for that account, in the case where
they spend the maximum possible XRP, leave enough XRP balance to pay
the fee,
- the total fees for the other queued transactions are less than both
the network's minimum reserve and the account's XRP balance, and
- none of the prior queued transactions affect the ability of subsequent
transactions to claim a fee.
- the account has fewer than [10](#other-constants) transactions already in the queue.
- all other queued transactions for that account, in the case where they spend the maximum possible XRP, leave enough XRP balance to pay the fee,
- the total fees for the other queued transactions are less than both the network's minimum reserve and the account's XRP balance, and
- none of the prior queued transactions affect the ability of subsequent transactions to claim a fee.
Currently, there is an additional restriction that the queue cannot work with
transactions using the `sfPreviousTxnID` or `sfAccountTxnID` fields.
`sfPreviousTxnID` is deprecated and shouldn't be used anyway. Future
development will make the queue aware of `sfAccountTxnID` mechanisms.
Currently, there is an additional restriction that the queue cannot work with transactions using the `sfPreviousTxnID` or `sfAccountTxnID` fields. `sfPreviousTxnID` is deprecated and shouldn't be used anyway. Future development will make the queue aware of `sfAccountTxnID` mechanisms.
## Technical Details
### Fee Level
"Fee level" is used to allow the cost of different types of transactions
to be compared directly. For a [reference
transaction](#reference-transaction), the base fee
level is 256. If a transaction is submitted with a higher `Fee` field,
the fee level is scaled appropriately.
"Fee level" is used to allow the cost of different types of transactions to be compared directly. For a [reference transaction](#reference-transaction), the base fee level is 256. If a transaction is submitted with a higher `Fee` field, the fee level is scaled appropriately.
Examples, assuming a [reference transaction](#reference-transaction)
base fee of 10 drops:
Examples, assuming a [reference transaction](#reference-transaction) base fee of 10 drops:
1. A single-signed [reference transaction](#reference-transaction)
with `Fee=20` will have a fee level of
`20 drop fee * 256 fee level / 10 drop base fee = 512 fee level`.
2. A multi-signed [reference transaction](#reference-transaction) with
3 signatures (base fee = 40 drops) and `Fee=60` will have a fee level of
`60 drop fee * 256 fee level / ((1tx + 3sigs) * 10 drop base fee) = 384
fee level`.
3. A hypothetical future non-reference transaction with a base
fee of 15 drops multi-signed with 5 signatures and `Fee=90` will
have a fee level of
`90 drop fee * 256 fee level / ((1tx + 5sigs) * 15 drop base fee) = 256
fee level`.
1. A single-signed [reference transaction](#reference-transaction) with `Fee=20` will have a fee level of `20 drop fee * 256 fee level / 10 drop base fee = 512 fee level`.
2. A multi-signed [reference transaction](#reference-transaction) with 3 signatures (base fee = 40 drops) and `Fee=60` will have a fee level of `60 drop fee * 256 fee level / ((1tx + 3sigs) * 10 drop base fee) = 384 fee level`.
3. A hypothetical future non-reference transaction with a base fee of 15 drops multi-signed with 5 signatures and `Fee=90` will have a fee level of `90 drop fee * 256 fee level / ((1tx + 5sigs) * 15 drop base fee) = 256 fee level`.
This demonstrates that a simpler transaction paying less XRP can be more
likely to get into the open ledger, or be sorted earlier in the queue
than a more complex transaction paying more XRP.
This demonstrates that a simpler transaction paying less XRP can be more likely to get into the open ledger, or be sorted earlier in the queue than a more complex transaction paying more XRP.
### Load Fee
@@ -188,112 +78,34 @@ Each xrpld server maintains a minimum cost threshold based on its current load.
### Reference Transaction
In this document, a "Reference Transaction" is any currently implemented
single-signed transaction (eg. Payment, Account Set, Offer Create, etc)
that requires a fee.
In this document, a "Reference Transaction" is any currently implemented single-signed transaction (eg. Payment, Account Set, Offer Create, etc) that requires a fee.
In the future, there may be other transaction types that require
more (or less) work for xrpld to process. Those transactions may have
a higher (or lower) base fee, requiring a correspondingly higher (or
lower) fee to get into the same position as a reference transaction.
In the future, there may be other transaction types that require more (or less) work for xrpld to process. Those transactions may have a higher (or lower) base fee, requiring a correspondingly higher (or lower) fee to get into the same position as a reference transaction.
### Consensus Health
For consensus to be considered healthy, the peers on the network
should largely remain in sync with one another. It is particularly
important for the validators to remain in sync, because that is required
for participation in consensus. However, the network tolerates some
validators being out of sync. Fundamentally, network health is a
function of validators reaching consensus on sets of recently submitted
transactions.
For consensus to be considered healthy, the peers on the network should largely remain in sync with one another. It is particularly important for the validators to remain in sync, because that is required for participation in consensus. However, the network tolerates some validators being out of sync. Fundamentally, network health is a function of validators reaching consensus on sets of recently submitted transactions.
Another factor to consider is
the duration of the consensus process itself. This generally takes
under 5 seconds on the main network under low volume. This is based on
historical observations. However factors such as transaction volume
can increase consensus duration. This is because xrpld performs
more work as transaction volume increases. Under sufficient load this
tends to increase consensus duration. It's possible that relatively high
consensus duration indicates a problem, but it is not appropriate to
conclude so without investigation. The upper limit for consensus
duration should be roughly 20 seconds. That is far above the normal.
If the network takes this long to close ledgers, then it is almost
certain that there is a problem with the network. This circumstance
often coincides with new ledgers with zero transactions.
Another factor to consider is the duration of the consensus process itself. This generally takes under 5 seconds on the main network under low volume. This is based on historical observations. However factors such as transaction volume can increase consensus duration. This is because xrpld performs more work as transaction volume increases. Under sufficient load this tends to increase consensus duration. It's possible that relatively high consensus duration indicates a problem, but it is not appropriate to conclude so without investigation. The upper limit for consensus duration should be roughly 20 seconds. That is far above the normal. If the network takes this long to close ledgers, then it is almost certain that there is a problem with the network. This circumstance often coincides with new ledgers with zero transactions.
### Other Constants
- _Base fee transaction limit per ledger_. The minimum value of 5 was
chosen to ensure the limit never gets so small that the ledger becomes
unusable. The "target" value of 50 was chosen so the limit never gets large
enough to invite abuse, but keeps up if the network stays healthy and
active. These exact values were chosen experimentally, and can easily
change in the future.
- _Expected ledger size growth and reduction percentages_. The growth
value of 20% was chosen to allow the limit to grow quickly as load
increases, but not so quickly as to allow bad actors to run unrestricted.
The reduction value of 50% was chosen to cause the limit to drop
significantly, but not so drastically that the limit cannot quickly
recover if the problem is temporary. These exact values were chosen
experimentally, and can easily change in the future.
- _Minimum `lastLedgerMedianFeeLevel`_. The value of 500 was chosen to
ensure that the first escalated fee was more significant and noticeable
than what the default would allow. This exact value was chosen
experimentally, and can easily change in the future.
- _Transaction queue size limit_. The limit is computed based on the
base fee transaction limit per ledger, so that the queue can grow
automatically as the network's performance improves, allowing
more transactions per second, and thus more transactions per ledger
to process successfully. The limit of 20 ledgers was used to provide
a balance between resource (specifically memory) usage, and giving
transactions a realistic chance to be processed. The minimum size of
2000 transactions was chosen to allow a decent functional backlog during
network congestion conditions. These exact values were
chosen experimentally, and can easily change in the future.
- _Maximum retries_. A transaction in the queue can attempt to apply
to the open ledger, but get a retry (`ter`) code up to 10 times, at
which point, it will be removed from the queue and dropped. The
value was chosen to be large enough to allow temporary failures to clear
up, but small enough that the queue doesn't fill up with stale
transactions which prevent lower fee level, but more likely to succeed,
transactions from queuing.
- _Maximum transactions per account_. A single account can have up to 10
transactions in the queue at any given time. This is primarily to
mitigate the lost cost of broadcasting multiple transactions if one of
the earlier ones fails or is otherwise removed from the queue without
being applied to the open ledger. The value was chosen arbitrarily, and
can easily change in the future.
- _Minimum last ledger sequence buffer_. If a transaction has a
`LastLedgerSequence` value, and cannot be processed into the open
ledger, that `LastLedgerSequence` must be at least 2 more than the
sequence number of the open ledger to be considered for the queue. The
value was chosen to provide a balance between letting the user control
the lifespan of the transaction, and giving a queued transaction a
chance to get processed out of the queue before getting discarded,
particularly since it may have dependent transactions also in the queue,
which will never succeed if this one is discarded.
- _Replaced transaction fee increase_. Any transaction in the queue can be
replaced by another transaction with the same sequence number and at
least a 25% higher fee level. The 25% increase is intended to cover the
resource cost incurred by broadcasting the original transaction to the
network. This value was chosen experimentally, and can easily change in
the future.
- _Base fee transaction limit per ledger_. The minimum value of 5 was chosen to ensure the limit never gets so small that the ledger becomes unusable. The "target" value of 50 was chosen so the limit never gets large enough to invite abuse, but keeps up if the network stays healthy and active. These exact values were chosen experimentally, and can easily change in the future.
- _Expected ledger size growth and reduction percentages_. The growth value of 20% was chosen to allow the limit to grow quickly as load increases, but not so quickly as to allow bad actors to run unrestricted. The reduction value of 50% was chosen to cause the limit to drop significantly, but not so drastically that the limit cannot quickly recover if the problem is temporary. These exact values were chosen experimentally, and can easily change in the future.
- _Minimum `lastLedgerMedianFeeLevel`_. The value of 500 was chosen to ensure that the first escalated fee was more significant and noticeable than what the default would allow. This exact value was chosen experimentally, and can easily change in the future.
- _Transaction queue size limit_. The limit is computed based on the base fee transaction limit per ledger, so that the queue can grow automatically as the network's performance improves, allowing more transactions per second, and thus more transactions per ledger to process successfully. The limit of 20 ledgers was used to provide a balance between resource (specifically memory) usage, and giving transactions a realistic chance to be processed. The minimum size of 2000 transactions was chosen to allow a decent functional backlog during network congestion conditions. These exact values were chosen experimentally, and can easily change in the future.
- _Maximum retries_. A transaction in the queue can attempt to apply to the open ledger, but get a retry (`ter`) code up to 10 times, at which point, it will be removed from the queue and dropped. The value was chosen to be large enough to allow temporary failures to clear up, but small enough that the queue doesn't fill up with stale transactions which prevent lower fee level, but more likely to succeed, transactions from queuing.
- _Maximum transactions per account_. A single account can have up to 10 transactions in the queue at any given time. This is primarily to mitigate the lost cost of broadcasting multiple transactions if one of the earlier ones fails or is otherwise removed from the queue without being applied to the open ledger. The value was chosen arbitrarily, and can easily change in the future.
- _Minimum last ledger sequence buffer_. If a transaction has a `LastLedgerSequence` value, and cannot be processed into the open ledger, that `LastLedgerSequence` must be at least 2 more than the sequence number of the open ledger to be considered for the queue. The value was chosen to provide a balance between letting the user control the lifespan of the transaction, and giving a queued transaction a chance to get processed out of the queue before getting discarded, particularly since it may have dependent transactions also in the queue, which will never succeed if this one is discarded.
- _Replaced transaction fee increase_. Any transaction in the queue can be replaced by another transaction with the same sequence number and at least a 25% higher fee level. The 25% increase is intended to cover the resource cost incurred by broadcasting the original transaction to the network. This value was chosen experimentally, and can easily change in the future.
### `fee` command
**The `fee` RPC and WebSocket command is still experimental, and may
change without warning.**
**The `fee` RPC and WebSocket command is still experimental, and may change without warning.**
`fee` takes no parameters, and returns information about the current local
[fee escalation](#fee-escalation) and [transaction queue](#transaction-queue)
state as both fee levels and drops. The drop values assume a
single-singed reference transaction. It is up to the user to compute the
necessary fees for other types of transactions. (E.g. multiply all drop
values by 5 for a multi-signed transaction with 4 signatures.)
`fee` takes no parameters, and returns information about the current local [fee escalation](#fee-escalation) and [transaction queue](#transaction-queue) state as both fee levels and drops. The drop values assume a single-singed reference transaction. It is up to the user to compute the necessary fees for other types of transactions. (E.g. multiply all drop values by 5 for a multi-signed transaction with 4 signatures.)
The `fee` result is always instantaneous, and relates to the open
ledger. It includes the sequence number of the current open ledger,
but may not make sense if xrpld is not synced to the network.
The `fee` result is always instantaneous, and relates to the open ledger. It includes the sequence number of the current open ledger, but may not make sense if xrpld is not synced to the network.
Result format:
@@ -323,49 +135,23 @@ Result format:
### [`server_info`](https://xrpl.org/server_info.html) command
**The fields listed here are still experimental, and may change
without warning.**
**The fields listed here are still experimental, and may change without warning.**
Up to two fields in `server_info` output are related to fee escalation.
1. `load_factor_fee_escalation`: The factor on base transaction cost
that a transaction must pay to get into the open ledger. This value can
change quickly as transactions are processed from the network and
ledgers are closed. If not escalated, the value is 1, so will not be
returned.
2. `load_factor_fee_queue`: If the queue is full, this is the factor on
base transaction cost that a transaction must pay to get into the queue.
If not full, the value is 1, so will not be returned.
1. `load_factor_fee_escalation`: The factor on base transaction cost that a transaction must pay to get into the open ledger. This value can change quickly as transactions are processed from the network and ledgers are closed. If not escalated, the value is 1, so will not be returned.
2. `load_factor_fee_queue`: If the queue is full, this is the factor on base transaction cost that a transaction must pay to get into the queue. If not full, the value is 1, so will not be returned.
In all cases, the transaction fee must be high enough to overcome both
`load_factor_fee_queue` and `load_factor` to be considered. It does not
need to overcome `load_factor_fee_escalation`, though if it does not, it
is more likely to be queued than immediately processed into the open
ledger.
In all cases, the transaction fee must be high enough to overcome both `load_factor_fee_queue` and `load_factor` to be considered. It does not need to overcome `load_factor_fee_escalation`, though if it does not, it is more likely to be queued than immediately processed into the open ledger.
### [`server_state`](https://xrpl.org/server_state.html) command
**The fields listed here are still experimental, and may change
without warning.**
**The fields listed here are still experimental, and may change without warning.**
Three fields in `server_state` output are related to fee escalation.
1. `load_factor_fee_escalation`: The factor on base transaction cost
that a transaction must pay to get into the open ledger. This value can
change quickly as transactions are processed from the network and
ledgers are closed. The ratio between this value and
`load_factor_fee_reference` determines the multiplier for transaction
fees to get into the current open ledger.
2. `load_factor_fee_queue`: This is the factor on base transaction cost
that a transaction must pay to get into the queue. The ratio between
this value and `load_factor_fee_reference` determines the multiplier for
transaction fees to get into the transaction queue to be considered for
a later ledger.
3. `load_factor_fee_reference`: Like `load_base`, this is the baseline
that is used to scale fee escalation computations.
1. `load_factor_fee_escalation`: The factor on base transaction cost that a transaction must pay to get into the open ledger. This value can change quickly as transactions are processed from the network and ledgers are closed. The ratio between this value and `load_factor_fee_reference` determines the multiplier for transaction fees to get into the current open ledger.
2. `load_factor_fee_queue`: This is the factor on base transaction cost that a transaction must pay to get into the queue. The ratio between this value and `load_factor_fee_reference` determines the multiplier for transaction fees to get into the transaction queue to be considered for a later ledger.
3. `load_factor_fee_reference`: Like `load_base`, this is the baseline that is used to scale fee escalation computations.
In all cases, the transaction fee must be high enough to overcome both
`load_factor_fee_queue` and `load_factor` to be considered. It does not
need to overcome `load_factor_fee_escalation`, though if it does not, it
is more likely to be queued than immediately processed into the open
ledger.
In all cases, the transaction fee must be high enough to overcome both `load_factor_fee_queue` and `load_factor` to be considered. It does not need to overcome `load_factor_fee_escalation`, though if it does not, it is more likely to be queued than immediately processed into the open ledger.

View File

@@ -1,155 +1,73 @@
# Fee Voting
The XRPL payment protocol enforces a fee schedule expressed in units of the
native currency, XRP. Fees for transactions are paid directly from the account
owner. There are also reserve requirements for each item that occupies storage
in the ledger. The reserve fee schedule contains both a per-account reserve,
and a per-owned-item reserve. The items an account may own include active
offers, trust lines, and tickets.
The XRPL payment protocol enforces a fee schedule expressed in units of the native currency, XRP. Fees for transactions are paid directly from the account owner. There are also reserve requirements for each item that occupies storage in the ledger. The reserve fee schedule contains both a per-account reserve, and a per-owned-item reserve. The items an account may own include active offers, trust lines, and tickets.
Validators may vote to increase fees if they feel that the network is charging
too little. They may also vote to decrease fees if the fees are too costly
relative to the value the network provides. One common case where a validator
may want to change fees is when the value of the native currency XRP fluctuates
relative to other currencies.
Validators may vote to increase fees if they feel that the network is charging too little. They may also vote to decrease fees if the fees are too costly relative to the value the network provides. One common case where a validator may want to change fees is when the value of the native currency XRP fluctuates relative to other currencies.
The fee voting mechanism takes place every 256 ledgers ("voting ledgers"). In
a voting ledger, each validator takes a position on what they think the fees
should be. The consensus process converges on the majority position, and in
subsequent ledgers a new fee schedule is enacted.
The fee voting mechanism takes place every 256 ledgers ("voting ledgers"). In a voting ledger, each validator takes a position on what they think the fees should be. The consensus process converges on the majority position, and in subsequent ledgers a new fee schedule is enacted.
## Consensus
The XRPL consensus algorithm allows distributed participants to arrive at
the same answer for yes/no questions. The canonical case for consensus is
whether or not a particular transaction is included in the ledger. Fees
present a more difficult challenge, since the decision on the new fee is not
a yes or no question.
The XRPL consensus algorithm allows distributed participants to arrive at the same answer for yes/no questions. The canonical case for consensus is whether or not a particular transaction is included in the ledger. Fees present a more difficult challenge, since the decision on the new fee is not a yes or no question.
To convert validators' positions on fees into a yes or no question that can
be converged in the consensus process, the following algorithm is used:
To convert validators' positions on fees into a yes or no question that can be converged in the consensus process, the following algorithm is used:
- In the ledger before a voting ledger, validators send proposals which also
include the values they think the network should use for the new fee schedule.
- In the ledger before a voting ledger, validators send proposals which also include the values they think the network should use for the new fee schedule.
- In the voting ledger, validators examine the proposals from other validators
and choose a new fee schedule which moves the fees in a direction closer to
the validator's ideal fee schedule and is also likely to be accepted. A fee
amount is likely to be accepted if a majority of validators agree on the
number.
- In the voting ledger, validators examine the proposals from other validators and choose a new fee schedule which moves the fees in a direction closer to the validator's ideal fee schedule and is also likely to be accepted. A fee amount is likely to be accepted if a majority of validators agree on the number.
- Each validator injects a "pseudo transaction" into their proposed ledger
which sets the fees to the chosen schedule.
- Each validator injects a "pseudo transaction" into their proposed ledger which sets the fees to the chosen schedule.
- The consensus process is applied to these fee-setting transactions as normal.
Each transaction is either included in the ledger or not. In most cases, one
fee setting transaction will make it in while the others are rejected. In
some rare cases more than one fee setting transaction will make it in. The
last one to be applied will take effect. This is harmless since a majority
of validators still agreed on it.
- The consensus process is applied to these fee-setting transactions as normal. Each transaction is either included in the ledger or not. In most cases, one fee setting transaction will make it in while the others are rejected. In some rare cases more than one fee setting transaction will make it in. The last one to be applied will take effect. This is harmless since a majority of validators still agreed on it.
- After the voting ledger has been validated, future pseudo transactions
before the next voting ledger are rejected as fee setting transactions may
only appear in voting ledgers.
- After the voting ledger has been validated, future pseudo transactions before the next voting ledger are rejected as fee setting transactions may only appear in voting ledgers.
## Configuration
A validating instance of xrpld uses information in the configuration file
to determine how it wants to vote on the fee schedule. It is the responsibility
of the administrator to set these values.
A validating instance of xrpld uses information in the configuration file to determine how it wants to vote on the fee schedule. It is the responsibility of the administrator to set these values.
---
# Amendment
An Amendment is a new or proposed change to a ledger rule. Ledger rules affect
transaction processing and consensus; peers must use the same set of rules for
consensus to succeed, otherwise different instances of xrpld will get
different results. Amendments can be almost anything but they must be accepted
by a network majority through a consensus process before they are utilized. An
Amendment must receive at least an 80% approval rate from validating nodes for
a period of two weeks before being accepted. The following example outlines the
process of an Amendment from its conception to approval and usage.
An Amendment is a new or proposed change to a ledger rule. Ledger rules affect transaction processing and consensus; peers must use the same set of rules for consensus to succeed, otherwise different instances of xrpld will get different results. Amendments can be almost anything but they must be accepted by a network majority through a consensus process before they are utilized. An Amendment must receive at least an 80% approval rate from validating nodes for a period of two weeks before being accepted. The following example outlines the process of an Amendment from its conception to approval and usage.
- A community member proposes to change transaction processing in some way.
The proposal is discussed amongst the community and receives its support
creating a community or human consensus.
- A community member proposes to change transaction processing in some way. The proposal is discussed amongst the community and receives its support creating a community or human consensus.
- Some members contribute their time and work to develop the Amendment.
- A pull request is created and the new code is folded into an xrpld build
and made available for use.
- A pull request is created and the new code is folded into an xrpld build and made available for use.
- The consensus process begins with the validating nodes.
- If the Amendment holds an 80% majority for a two week period, nodes will begin
including the transaction to enable it in their initial sets.
- If the Amendment holds an 80% majority for a two week period, nodes will begin including the transaction to enable it in their initial sets.
Nodes may veto Amendments they consider undesirable by never announcing their
support for those Amendments. Just a few nodes vetoing an Amendment will normally
keep it from being accepted. Nodes could also vote yes on an Amendments even
before it obtains a super-majority. This might make sense for a critical bug fix.
Nodes may veto Amendments they consider undesirable by never announcing their support for those Amendments. Just a few nodes vetoing an Amendment will normally keep it from being accepted. Nodes could also vote yes on an Amendments even before it obtains a super-majority. This might make sense for a critical bug fix.
Validators that support an amendment that is not yet enabled announce their
support in their validations. If 80% support is achieved, they will introduce
a pseudo-transaction to track the amendment's majority status in the ledger.
Validators that support an amendment that is not yet enabled announce their support in their validations. If 80% support is achieved, they will introduce a pseudo-transaction to track the amendment's majority status in the ledger.
If an amendment whose majority status is reported in a ledger loses that
majority status, validators will introduce pseudo-transactions to remove
the majority status from the ledger.
If an amendment whose majority status is reported in a ledger loses that majority status, validators will introduce pseudo-transactions to remove the majority status from the ledger.
If an amendment holds majority status for two weeks, validators will
introduce a pseudo-transaction to enable the amendment.
If an amendment holds majority status for two weeks, validators will introduce a pseudo-transaction to enable the amendment.
All amendments are assumed to be critical and irreversible. Thus there
is no mechanism to disable or revoke an amendment, nor is there a way
for a server to operate while an amendment it does not understand is
enabled.
All amendments are assumed to be critical and irreversible. Thus there is no mechanism to disable or revoke an amendment, nor is there a way for a server to operate while an amendment it does not understand is enabled.
---
# SHAMapStore: Online Delete
Optional online deletion happens through the SHAMapStore. Records are deleted
from disk based on ledger sequence number. These records reside in the
key-value database as well as in the SQLite ledger and transaction databases.
Without online deletion storage usage grows without bounds. It can only
be pruned by stopping, manually deleting data, and restarting the server.
Online deletion requires less operator intervention to manage the server.
Optional online deletion happens through the SHAMapStore. Records are deleted from disk based on ledger sequence number. These records reside in the key-value database as well as in the SQLite ledger and transaction databases. Without online deletion storage usage grows without bounds. It can only be pruned by stopping, manually deleting data, and restarting the server. Online deletion requires less operator intervention to manage the server.
The main mechanism to delete data from the key-value database is to keep two
databases open at all times. One database has all writes directed to it. The
other database has recent archival data from just prior to that from the current
writable database.
Upon rotation, the archival database is deleted. The writable database becomes
archival, and a brand new database becomes writable. To ensure that no
necessary data for transaction processing is lost, a variety of steps occur,
including copying the contents of an entire ledger's account state map,
clearing caches, and copying the contents of (freshening) other caches.
The main mechanism to delete data from the key-value database is to keep two databases open at all times. One database has all writes directed to it. The other database has recent archival data from just prior to that from the current writable database. Upon rotation, the archival database is deleted. The writable database becomes archival, and a brand new database becomes writable. To ensure that no necessary data for transaction processing is lost, a variety of steps occur, including copying the contents of an entire ledger's account state map, clearing caches, and copying the contents of (freshening) other caches.
Deleting from SQLite involves more straight-forward SQL DELETE queries from
the respective tables, with a rudimentary back-off algorithm to do portions
of the deletions at a time. This back-off is in place so that the database
lock is not held excessively. The SQLite database is not configured to
delete on-disk storage, so it will grow over time. However, with online delete
enabled, it grows at a very small rate compared with the key-value store.
Deleting from SQLite involves more straight-forward SQL DELETE queries from the respective tables, with a rudimentary back-off algorithm to do portions of the deletions at a time. This back-off is in place so that the database lock is not held excessively. The SQLite database is not configured to delete on-disk storage, so it will grow over time. However, with online delete enabled, it grows at a very small rate compared with the key-value store.
The online delete routine aborts its current activities if it fails periodic
server health checks. This minimizes impact of I/O and locking of critical
objects. If interrupted, the routine will start again at the next validated
ledger close. Likewise, the routine will continue in a similar fashion if the
server restarts.
The online delete routine aborts its current activities if it fails periodic server health checks. This minimizes impact of I/O and locking of critical objects. If interrupted, the routine will start again at the next validated ledger close. Likewise, the routine will continue in a similar fashion if the server restarts.
Configuration:
- In the [node_db] configuration section, an optional online_delete parameter is
set. If not set or if set to 0, online delete is disabled. Otherwise, the
setting defines number of ledgers between deletion cycles.
- Another optional parameter in [node_db] is that for advisory_delete. It is
disabled by default. If set to non-zero, requires an RPC call to activate the
deletion routine.
- In the [node_db] configuration section, an optional online_delete parameter is set. If not set or if set to 0, online delete is disabled. Otherwise, the setting defines number of ledgers between deletion cycles.
- Another optional parameter in [node_db] is that for advisory_delete. It is disabled by default. If set to non-zero, requires an RPC call to activate the deletion routine.
- online_delete must not be greater than the [ledger_history] parameter.
- [fetch_depth] will be silently set to equal the online_delete setting if
online_delete is greater than fetch_depth.
- In the [node_db] section, there is a performance tuning option, delete_batch,
which sets the maximum size in ledgers for each SQL DELETE query.
- [fetch_depth] will be silently set to equal the online_delete setting if online_delete is greater than fetch_depth.
- In the [node_db] section, there is a performance tuning option, delete_batch, which sets the maximum size in ledgers for each SQL DELETE query.

View File

@@ -46,16 +46,16 @@ src/xrpld/app/rdb/
### File Contents
| File | Contents |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Node.[h\|cpp]` | Defines/Implements methods used by `SQLiteDatabase` for interacting with SQLite node databases |
| File | Contents |
| --- | --- |
| `Node.[h\|cpp]` | Defines/Implements methods used by `SQLiteDatabase` for interacting with SQLite node databases |
| `SQLiteDatabase.[h\|cpp]` | Defines/Implements the class `SQLiteDatabase`/`SQLiteDatabaseImp` which inherits from `RelationalDatabase` and is used to operate on the main stores |
| `PeerFinder.[h\|cpp]` | Defines/Implements methods for interacting with the PeerFinder SQLite database |
| `RelationalDatabase.cpp` | Implements the static method `RelationalDatabase::init` which is used to initialize an instance of `RelationalDatabase` |
| `RelationalDatabase.h` | Defines the abstract class `RelationalDatabase`, the primary class of the Relational Database Interface |
| `State.[h\|cpp]` | Defines/Implements methods for interacting with the State SQLite database which concerns ledger deletion and database rotation |
| `Vacuum.[h\|cpp]` | Defines/Implements a method for performing the `VACUUM` operation on SQLite databases |
| `Wallet.[h\|cpp]` | Defines/Implements methods for interacting with Wallet SQLite databases |
| `PeerFinder.[h\|cpp]` | Defines/Implements methods for interacting with the PeerFinder SQLite database |
| `RelationalDatabase.cpp` | Implements the static method `RelationalDatabase::init` which is used to initialize an instance of `RelationalDatabase` |
| `RelationalDatabase.h` | Defines the abstract class `RelationalDatabase`, the primary class of the Relational Database Interface |
| `State.[h\|cpp]` | Defines/Implements methods for interacting with the State SQLite database which concerns ledger deletion and database rotation |
| `Vacuum.[h\|cpp]` | Defines/Implements a method for performing the `VACUUM` operation on SQLite databases |
| `Wallet.[h\|cpp]` | Defines/Implements methods for interacting with Wallet SQLite databases |
## Classes

View File

@@ -2,55 +2,31 @@
## Introduction
The _XRP Ledger network_ consists of a collection of _peers_ running
**`xrpld`** or other compatible software. Each peer maintains multiple
outgoing connections and optional incoming connections to other peers.
These connections are made over both the public Internet and private local
area networks. This network defines a connected directed graph of nodes
where vertices are instances of `xrpld` and edges are persistent TCP/IP
connections. Peers send and receive messages to other connected peers. This
peer to peer network, layered on top of the public and private Internet,
forms an [_overlay network_][overlay_network]. The contents of the messages
and the behavior of peers in response to the messages, plus the information
exchanged during the handshaking phase of connection establishment, defines
the _XRP Ledger peer protocol_ (or _protocol_ in this context).
The _XRP Ledger network_ consists of a collection of _peers_ running **`xrpld`** or other compatible software. Each peer maintains multiple outgoing connections and optional incoming connections to other peers. These connections are made over both the public Internet and private local area networks. This network defines a connected directed graph of nodes where vertices are instances of `xrpld` and edges are persistent TCP/IP connections. Peers send and receive messages to other connected peers. This peer to peer network, layered on top of the public and private Internet, forms an [_overlay network_][overlay_network]. The contents of the messages and the behavior of peers in response to the messages, plus the information exchanged during the handshaking phase of connection establishment, defines the _XRP Ledger peer protocol_ (or _protocol_ in this context).
## Overview
Each connection is represented by a _Peer_ object. The Overlay manager
establishes, receives, and maintains connections to peers. Protocol
messages are exchanged between peers and serialized using
[_Google Protocol Buffers_][protocol_buffers].
Each connection is represented by a _Peer_ object. The Overlay manager establishes, receives, and maintains connections to peers. Protocol messages are exchanged between peers and serialized using [_Google Protocol Buffers_][protocol_buffers].
### Structure
Each connection between peers is identified by its connection type, which
affects the behavior of message routing. At present, only a single connection
type is supported: **Peer**.
Each connection between peers is identified by its connection type, which affects the behavior of message routing. At present, only a single connection type is supported: **Peer**.
## Handshake
To establish a protocol connection, a peer makes an outgoing TLS encrypted
connection to a remote peer, then sends an HTTP request with no message body.
To establish a protocol connection, a peer makes an outgoing TLS encrypted connection to a remote peer, then sends an HTTP request with no message body.
### HTTP
The HTTP [request](https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html) must:
- Use HTTP version 1.1.
- Specify a request URI consisting of a single forward slash character ("/")
indicating the server root. Requests using different URIs are reserved for
future protocol implementations.
- Use the [_HTTP/1.1 Upgrade_][upgrade_header] mechanism with additional custom
fields to communicate protocol specific information related to the upgrade.
- Specify a request URI consisting of a single forward slash character ("/") indicating the server root. Requests using different URIs are reserved for future protocol implementations.
- Use the [_HTTP/1.1 Upgrade_][upgrade_header] mechanism with additional custom fields to communicate protocol specific information related to the upgrade.
HTTP requests which do not conform to this requirements must generate an
appropriate HTTP error and result in the connection being closed.
HTTP requests which do not conform to this requirements must generate an appropriate HTTP error and result in the connection being closed.
Upon receipt of a well-formed HTTP upgrade request, and validation of the
protocol specific parameters, a peer will either send back a HTTP 101 response
and switch to the requested protocol, or a message indicating that the request
failed (e.g. by sending HTTP 400 "Bad Request" or HTTP 503 "Service Unavailable").
Upon receipt of a well-formed HTTP upgrade request, and validation of the protocol specific parameters, a peer will either send back a HTTP 101 response and switch to the requested protocol, or a message indicating that the request failed (e.g. by sending HTTP 400 "Bad Request" or HTTP 503 "Service Unavailable").
##### Example HTTP Upgrade Request
@@ -105,10 +81,7 @@ Content-Type: application/json
| ------------ | :----------------: | :------: |
| `User-Agent` | :heavy_check_mark: | |
The `User-Agent` field indicates the version of the software that the
peer that is making the HTTP request is using. No semantic meaning is
assigned to the value in this field but it is recommended that implementations
specify the version of the software that is used.
The `User-Agent` field indicates the version of the software that the peer that is making the HTTP request is using. No semantic meaning is assigned to the value in this field but it is recommended that implementations specify the version of the software that is used.
See [RFC2616 &sect;14.43](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43).
@@ -116,10 +89,7 @@ See [RFC2616 &sect;14.43](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.htm
| ---------- | :-----: | :----------------: |
| `Server` | | :heavy_check_mark: |
The `Server` field indicates the version of the software that the
peer that is processing the HTTP request is using. No semantic meaning is
assigned to the value in this field but it is recommended that implementations
specify the version of the software that is used.
The `Server` field indicates the version of the software that the peer that is processing the HTTP request is using. No semantic meaning is assigned to the value in this field but it is recommended that implementations specify the version of the software that is used.
See [RFC2616 &sect;14.38](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.38).
@@ -127,8 +97,7 @@ See [RFC2616 &sect;14.38](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.htm
| ------------ | :----------------: | :----------------: |
| `Connection` | :heavy_check_mark: | :heavy_check_mark: |
The `Connection` field should have a value of `Upgrade` to indicate that a
request to upgrade the connection is being performed.
The `Connection` field should have a value of `Upgrade` to indicate that a request to upgrade the connection is being performed.
See [RFC2616 &sect;14.10](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.10).
@@ -136,22 +105,13 @@ See [RFC2616 &sect;14.10](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.htm
| ---------- | :----------------: | :----------------: |
| `Upgrade` | :heavy_check_mark: | :heavy_check_mark: |
The `Upgrade` field is part of the standard connection upgrade mechanism and
must be present in both requests and responses. It is used to negotiate the
version of the protocol that will be used after the upgrade request completes.
The `Upgrade` field is part of the standard connection upgrade mechanism and must be present in both requests and responses. It is used to negotiate the version of the protocol that will be used after the upgrade request completes.
For requests, it should consist of a comma delimited list of at least one
element, where each element specifies a protocol version that the requesting
server is willing to use.
For requests, it should consist of a comma delimited list of at least one element, where each element specifies a protocol version that the requesting server is willing to use.
For responses, it should a consist of _single element_ matching one of the
elements provided in the corresponding request. If the server does not understand
any of the available protocol versions, the upgrade request should fail with an
appropriate HTTP error code (e.g. by sending an HTTP 400 "Bad Request" response).
For responses, it should a consist of _single element_ matching one of the elements provided in the corresponding request. If the server does not understand any of the available protocol versions, the upgrade request should fail with an appropriate HTTP error code (e.g. by sending an HTTP 400 "Bad Request" response).
Protocol versions are string of the form `XRPL/` followed by a dotted major
and minor protocol version number, where the major number is greater than or
equal to 2 and the minor is greater than or equal to 0.
Protocol versions are string of the form `XRPL/` followed by a dotted major and minor protocol version number, where the major number is greater than or equal to 2 and the minor is greater than or equal to 0.
See [RFC 2616 &sect;14.42](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.42)
@@ -161,77 +121,56 @@ See [RFC 2616 &sect;14.42](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.ht
| ------------ | :----------------: | :----------------: |
| `Connect-As` | :heavy_check_mark: | :heavy_check_mark: |
The mandatory `Connect-As` field is used to specify that type of connection
that is being requested.
The mandatory `Connect-As` field is used to specify that type of connection that is being requested.
For requests the value consists of a comma delimited list of elements, where
each element describes a possible connection type. Only one connection types
is supported at present: **`peer`**.
For requests the value consists of a comma delimited list of elements, where each element describes a possible connection type. Only one connection types is supported at present: **`peer`**.
For responses, the value must consist of exactly one element from the list of
elements specified in the request. If a server processing a request does not
recognize any of the connection types, the request should fail with an
appropriate HTTP error code (e.g. by sending an HTTP 400 "Bad Request" response).
For responses, the value must consist of exactly one element from the list of elements specified in the request. If a server processing a request does not recognize any of the connection types, the request should fail with an appropriate HTTP error code (e.g. by sending an HTTP 400 "Bad Request" response).
| Field Name | Request | Response |
| ----------- | :----------------: | :----------------: |
| `Remote-IP` | :white_check_mark: | :white_check_mark: |
The optional `Remote-IP` field contains the string representation of the IP
address of the remote end of the connection as seen from the peer that is
sending the field.
The optional `Remote-IP` field contains the string representation of the IP address of the remote end of the connection as seen from the peer that is sending the field.
By observing values of this field from a sufficient number of different
servers, a peer making outgoing connections can deduce its own IP address.
By observing values of this field from a sufficient number of different servers, a peer making outgoing connections can deduce its own IP address.
| Field Name | Request | Response |
| ---------- | :----------------: | :----------------: |
| `Local-IP` | :white_check_mark: | :white_check_mark: |
The optional `Local-IP` field contains the string representation of the IP
address that the peer sending the field believes to be its own.
The optional `Local-IP` field contains the string representation of the IP address that the peer sending the field believes to be its own.
Servers receiving this field can detect IP address mismatches, which may
indicate a potential man-in-the-middle attack.
Servers receiving this field can detect IP address mismatches, which may indicate a potential man-in-the-middle attack.
| Field Name | Request | Response |
| ------------ | :----------------: | :----------------: |
| `Network-ID` | :white_check_mark: | :white_check_mark: |
The optional `Network-ID` can be used to identify to which of several
[parallel networks](https://xrpl.org/parallel-networks.html) the server
sending the field is joined.
The optional `Network-ID` can be used to identify to which of several [parallel networks](https://xrpl.org/parallel-networks.html) the server sending the field is joined.
The value, if the field is present, is a 32-bit unsigned integer. The
following well-known values are in use:
The value, if the field is present, is a 32-bit unsigned integer. The following well-known values are in use:
- **0**: The "main net"
- **1**: The Ripple-operated [Test Net](https://xrpl.org/xrp-test-net-faucet.html).
If a server configured to join one network receives a connection request from a
server configured to join another network, the request should fail with an
appropriate HTTP error code (e.g. by sending an HTTP 400 "Bad Request" response).
If a server configured to join one network receives a connection request from a server configured to join another network, the request should fail with an appropriate HTTP error code (e.g. by sending an HTTP 400 "Bad Request" response).
| Field Name | Request | Response |
| -------------- | :----------------: | :----------------: |
| `Network-Time` | :white_check_mark: | :white_check_mark: |
The optional `Network-Time` field reports the current [time](https://xrpl.org/basic-data-types.html#specifying-time)
according to sender's internal clock.
The optional `Network-Time` field reports the current [time](https://xrpl.org/basic-data-types.html#specifying-time) according to sender's internal clock.
Servers should fail a connection if their clocks are not within 20 seconds of
each other with an appropriate HTTP error code (e.g. by sending an HTTP 400
"Bad Request" response).
Servers should fail a connection if their clocks are not within 20 seconds of each other with an appropriate HTTP error code (e.g. by sending an HTTP 400 "Bad Request" response).
It is highly recommended that servers synchronize their clocks using time
synchronization software. For more on this topic, please visit [ntp.org](http://www.ntp.org/).
It is highly recommended that servers synchronize their clocks using time synchronization software. For more on this topic, please visit [ntp.org](http://www.ntp.org/).
| Field Name | Request | Response |
| ------------ | :----------------: | :----------------: |
| `Public-Key` | :heavy_check_mark: | :heavy_check_mark: |
The mandatory `Public-Key` field identifies the sending server's public key,
encoded in base58 using the standard encoding for node public keys.
The mandatory `Public-Key` field identifies the sending server's public key, encoded in base58 using the standard encoding for node public keys.
See: https://xrpl.org/base58-encodings.html
@@ -239,15 +178,9 @@ See: https://xrpl.org/base58-encodings.html
| --------------- | :----------------: | :----------------: |
| `Server-Domain` | :white_check_mark: | :white_check_mark: |
The optional `Server-Domain` field allows a server to report the domain that
it is operating under. The value is configured by the server administrator in
the configuration file using the `[server_domain]` key.
The optional `Server-Domain` field allows a server to report the domain that it is operating under. The value is configured by the server administrator in the configuration file using the `[server_domain]` key.
The value is advisory and is not used by the code at this time, except for
reporting purposes. External tools should verify this value prior to using
it by attempting to locate a [TOML file](https://xrpl.org/xrp-ledger-toml.html)
under the specified domain and locating the public key of this server under the
`[NODES]` key.
The value is advisory and is not used by the code at this time, except for reporting purposes. External tools should verify this value prior to using it by attempting to locate a [TOML file](https://xrpl.org/xrp-ledger-toml.html) under the specified domain and locating the public key of this server under the `[NODES]` key.
Sending a malformed domain will prevent a connection from being established.
@@ -255,11 +188,9 @@ Sending a malformed domain will prevent a connection from being established.
| ------------------- | :----------------: | :----------------: |
| `Session-Signature` | :heavy_check_mark: | :heavy_check_mark: |
The `Session-Signature` field is mandatory and is used to secure the peer link
against certain types of attack. For more details see "Session Signature" below.
The `Session-Signature` field is mandatory and is used to secure the peer link against certain types of attack. For more details see "Session Signature" below.
The value is presently encoded using **Base64** encoding, but implementations
should support both **Base64** and **HEX** encoding for this value.
The value is presently encoded using **Base64** encoding, but implementations should support both **Base64** and **HEX** encoding for this value.
For more details on this field, please see **Session Signature** below.
@@ -267,15 +198,12 @@ For more details on this field, please see **Session Signature** below.
| ---------- | :----------------: | :----------------: |
| `Crawl` | :white_check_mark: | :white_check_mark: |
The optional `Crawl` field can be used by a server to indicate whether peers
should include it in crawl reports.
The optional `Crawl` field can be used by a server to indicate whether peers should include it in crawl reports.
The field can take two values:
- **`Public`**: The server's IP address and port should be included in crawl
reports.
- **`Private`**: The server's IP address and port should not be included in
crawl reports. _This is the default, if the field is omitted._
- **`Public`**: The server's IP address and port should be included in crawl reports.
- **`Private`**: The server's IP address and port should not be included in crawl reports. _This is the default, if the field is omitted._
For more on the Peer Crawler, please visit https://xrpl.org/peer-crawler.html.
@@ -283,106 +211,57 @@ For more on the Peer Crawler, please visit https://xrpl.org/peer-crawler.html.
| --------------- | :----------------: | :----------------: |
| `Closed-Ledger` | :white_check_mark: | :white_check_mark: |
If present, identifies the hash of the last ledger that the sending server
considers to be closed.
If present, identifies the hash of the last ledger that the sending server considers to be closed.
The value is encoded as **HEX**, but implementations should support both
**Base64** and **HEX** encoding for this value for legacy purposes.
The value is encoded as **HEX**, but implementations should support both **Base64** and **HEX** encoding for this value for legacy purposes.
| Field Name | Request | Response |
| ----------------- | :----------------: | :----------------: |
| `Previous-Ledger` | :white_check_mark: | :white_check_mark: |
If present, identifies the hash of the parent ledger that the sending server
considers to be closed.
If present, identifies the hash of the parent ledger that the sending server considers to be closed.
The value is presently encoded using **Base64** encoding, but implementations
should support both **Base64** and **HEX** encoding for this value.
The value is presently encoded using **Base64** encoding, but implementations should support both **Base64** and **HEX** encoding for this value.
#### Additional Headers
An implementation or operator may specify additional, optional fields
and values in both requests and responses.
An implementation or operator may specify additional, optional fields and values in both requests and responses.
Implementations should not reject requests because of the presence of fields
that they do not understand.
Implementations should not reject requests because of the presence of fields that they do not understand.
### Session Signature
Even for SSL/TLS encrypted connections, it is possible for an attacker to mount
relatively inexpensive MITM attacks that can be extremely hard to detect and
may afford the attacker the ability to intelligently tamper with messages
exchanged between the two endpoints.
Even for SSL/TLS encrypted connections, it is possible for an attacker to mount relatively inexpensive MITM attacks that can be extremely hard to detect and may afford the attacker the ability to intelligently tamper with messages exchanged between the two endpoints.
This risk can be mitigated if at least one side has a certificate from a certificate
authority trusted by the other endpoint, but having a certificate is not always
possible (or even desirable) in a decentralized and permissionless network.
This risk can be mitigated if at least one side has a certificate from a certificate authority trusted by the other endpoint, but having a certificate is not always possible (or even desirable) in a decentralized and permissionless network.
Ultimately, the goal is to ensure that two endpoints A and B know that they are
talking directly to each other over a single end-to-end SSL/TLS session instead
of two separate SSL/TLS sessions, with an attacker acting as a proxy.
Ultimately, the goal is to ensure that two endpoints A and B know that they are talking directly to each other over a single end-to-end SSL/TLS session instead of two separate SSL/TLS sessions, with an attacker acting as a proxy.
The XRP Ledger protocol prevents this attack by leveraging the fact that the two
servers each have a node identity, in the form of **`secp256k1`** keypairs, and
use that to strongly bind the SSL/TLS session to the node identities of each of
the two servers at the end of the SSL/TLS session.
The XRP Ledger protocol prevents this attack by leveraging the fact that the two servers each have a node identity, in the form of **`secp256k1`** keypairs, and use that to strongly bind the SSL/TLS session to the node identities of each of the two servers at the end of the SSL/TLS session.
To do this we "reach into" the SSL/TLS session, and extract the **`finished`**
messages for the local and remote endpoints, and combine them to generate a unique
"fingerprint". By design, this fingerprint should be the same for both SSL/TLS
endpoints.
To do this we "reach into" the SSL/TLS session, and extract the **`finished`** messages for the local and remote endpoints, and combine them to generate a unique "fingerprint". By design, this fingerprint should be the same for both SSL/TLS endpoints.
That fingerprint is calculated by each endpoint independently, so the
fingerprint is never transmitted over the network. Each server then utilizes its
private key to sign the fingerprint. This is the same keypair that determines
the server's public `secp256k1` node identity. The signature is transferred over
the secure SSL/TLS encrypted link during the protocol's initial handshake phase.
That fingerprint is calculated by each endpoint independently, so the fingerprint is never transmitted over the network. Each server then utilizes its private key to sign the fingerprint. This is the same keypair that determines the server's public `secp256k1` node identity. The signature is transferred over the secure SSL/TLS encrypted link during the protocol's initial handshake phase.
Each side of the link will verify that the provided signature is from the claimed
public key against the session's unique fingerprint. If this signature check fails
then the link **MUST** be dropped.
Each side of the link will verify that the provided signature is from the claimed public key against the session's unique fingerprint. If this signature check fails then the link **MUST** be dropped.
If an attacker, Eve, establishes two separate SSL sessions with Alice and Bob, the
fingerprints of the two sessions will be different, and Eve will not be able to
sign the fingerprint of her session with Bob with Alice's private key, or the
fingerprint of her session with Alice with Bob's private key, and so both A and
B will know that an active MITM attack is in progress and will close their
connections.
If an attacker, Eve, establishes two separate SSL sessions with Alice and Bob, the fingerprints of the two sessions will be different, and Eve will not be able to sign the fingerprint of her session with Bob with Alice's private key, or the fingerprint of her session with Alice with Bob's private key, and so both A and B will know that an active MITM attack is in progress and will close their connections.
If Eve simply proxies the raw bytes, she will be unable to decrypt the data being
transferred between A and B and will not be able to intelligently tamper with the
message stream between Alice and Bob, although she may be still be able to inject
delays or terminate the link.
If Eve simply proxies the raw bytes, she will be unable to decrypt the data being transferred between A and B and will not be able to intelligently tamper with the message stream between Alice and Bob, although she may be still be able to inject delays or terminate the link.
# XRPL clustering
A cluster consists of more than one XRPL server under common
administration that share load information, distribute cryptography
operations, and provide greater response consistency.
A cluster consists of more than one XRPL server under common administration that share load information, distribute cryptography operations, and provide greater response consistency.
Cluster nodes are identified by their public node keys. Cluster nodes
exchange information about endpoints that are imposing load upon them.
Cluster nodes share information about their internal load status. Cluster
nodes do not have to verify the cryptographic signatures on messages
received from other cluster nodes.
Cluster nodes are identified by their public node keys. Cluster nodes exchange information about endpoints that are imposing load upon them. Cluster nodes share information about their internal load status. Cluster nodes do not have to verify the cryptographic signatures on messages received from other cluster nodes.
## Configuration
A server's public key can be determined from the output of the `server_info`
command. The key is in the `pubkey_node` value, and is a text string
beginning with the letter `n`. The key is maintained across runs in a
database.
A server's public key can be determined from the output of the `server_info` command. The key is in the `pubkey_node` value, and is a text string beginning with the letter `n`. The key is maintained across runs in a database.
Cluster members are configured in the `xrpld.cfg` file under
`[cluster_nodes]`. Each member should be configured on a line beginning
with the node public key, followed optionally by a space and a friendly
name.
Cluster members are configured in the `xrpld.cfg` file under `[cluster_nodes]`. Each member should be configured on a line beginning with the node public key, followed optionally by a space and a friendly name.
Because cluster members can introduce other cluster members, it is not
necessary to configure every cluster member on every other cluster member.
If a hub and spoke system is used, it is sufficient to configure every
cluster member on the hub(s) and only configure the hubs on the spokes.
That is, each spoke does not need to be configured on every other spoke.
Because cluster members can introduce other cluster members, it is not necessary to configure every cluster member on every other cluster member. If a hub and spoke system is used, it is sufficient to configure every cluster member on the hub(s) and only configure the hubs on the spokes. That is, each spoke does not need to be configured on every other spoke.
New spokes can be added as follows:
@@ -394,53 +273,27 @@ New spokes can be added as follows:
## Transaction Behavior
When a transaction is received from a cluster member, several normal checks
are bypassed:
When a transaction is received from a cluster member, several normal checks are bypassed:
Signature checking is bypassed because we trust that a cluster member would
not relay a transaction with an incorrect signature. Validators may wish to
disable this feature, preferring the additional load to get the additional
security of having validators check each transaction.
Signature checking is bypassed because we trust that a cluster member would not relay a transaction with an incorrect signature. Validators may wish to disable this feature, preferring the additional load to get the additional security of having validators check each transaction.
Local checks for transaction checking are also bypassed. For example, a
server will not reject a transaction from a cluster peer because the fee
does not meet its current relay fee. It is preferable to keep the cluster
in agreement and permit confirmation from one cluster member to more
reliably indicate the transaction's acceptance by the cluster.
Local checks for transaction checking are also bypassed. For example, a server will not reject a transaction from a cluster peer because the fee does not meet its current relay fee. It is preferable to keep the cluster in agreement and permit confirmation from one cluster member to more reliably indicate the transaction's acceptance by the cluster.
## Server Load Information
Cluster members exchange information on their server's load level. The load
level is essentially the amount by which the normal fee levels are multiplied
to get the server's fee for relaying transactions.
Cluster members exchange information on their server's load level. The load level is essentially the amount by which the normal fee levels are multiplied to get the server's fee for relaying transactions.
A server's effective load level, and the one it uses to determine its relay
fee, is the highest of its local load level, the network load level, and the
cluster load level. The cluster load level is the median load level reported
by a cluster member.
A server's effective load level, and the one it uses to determine its relay fee, is the highest of its local load level, the network load level, and the cluster load level. The cluster load level is the median load level reported by a cluster member.
## Gossip
Gossip is the mechanism by which cluster members share information about
endpoints (typically IPv4 addresses) that are imposing unusually high load
on them. The endpoint load manager takes into account gossip to reduce the
amount of load the endpoint is permitted to impose on the local server
before it is warned, disconnected, or banned.
Gossip is the mechanism by which cluster members share information about endpoints (typically IPv4 addresses) that are imposing unusually high load on them. The endpoint load manager takes into account gossip to reduce the amount of load the endpoint is permitted to impose on the local server before it is warned, disconnected, or banned.
Suppose, for example, that an attacker controls a large number of IP
addresses, and with these, he can send sufficient requests to overload a
server. Without gossip, he could use these same addresses to overload all
the servers in a cluster. With gossip, if he chooses to use the same IP
address to impose load on more than one server, he will find that the amount
of load he can impose before getting disconnected is much lower.
Suppose, for example, that an attacker controls a large number of IP addresses, and with these, he can send sufficient requests to overload a server. Without gossip, he could use these same addresses to overload all the servers in a cluster. With gossip, if he chooses to use the same IP address to impose load on more than one server, he will find that the amount of load he can impose before getting disconnected is much lower.
## Monitoring
The `peers` command will report on the status of the cluster. The `cluster`
object will contain one entry for each member of the cluster (either configured
or introduced by another cluster member). The `age` field is the number of
seconds since the server was last heard from. If the server is reporting an
elevated cluster fee, that will be reported as well.
The `peers` command will report on the status of the cluster. The `cluster` object will contain one entry for each member of the cluster (either configured or introduced by another cluster member). The `age` field is the number of seconds since the server was last heard from. If the server is reporting an elevated cluster fee, that will be reported as well.
In the `peers` object, cluster members will contain a `cluster` field set to `true`.

View File

@@ -2,36 +2,19 @@
## Introduction
The _XRPL payment network_ consists of a collection of _peers_ running the
**xrpld software**. Each peer maintains multiple outgoing connections and
optional incoming connections to other peers. These connections are made over
both the public Internet and private local area networks. This network defines
a fully connected directed graph of nodes. Peers send and receive messages to
other connected peers. This peer to peer network, layered on top of the public
and private Internet, forms an [_overlay network_][overlay_network].
The _XRPL payment network_ consists of a collection of _peers_ running the **xrpld software**. Each peer maintains multiple outgoing connections and optional incoming connections to other peers. These connections are made over both the public Internet and private local area networks. This network defines a fully connected directed graph of nodes. Peers send and receive messages to other connected peers. This peer to peer network, layered on top of the public and private Internet, forms an [_overlay network_][overlay_network].
## Bootstrapping
When a peer comes online it needs a set of IP addresses to connect to in order to
gain initial entry into the overlay in a process called _bootstrapping_. Once they
have established an initial set of these outbound peer connections, they need to
gain additional addresses to establish more outbound peer connections until the
desired limit is reached. Furthermore, they need a mechanism to advertise their
IP address to new or existing peers in the overlay so they may receive inbound
connections up to some desired limit. And finally, they need a mechanism to provide
inbound connection requests with an alternate set of IP addresses to try when they
have already reached their desired maximum number of inbound connections.
When a peer comes online it needs a set of IP addresses to connect to in order to gain initial entry into the overlay in a process called _bootstrapping_. Once they have established an initial set of these outbound peer connections, they need to gain additional addresses to establish more outbound peer connections until the desired limit is reached. Furthermore, they need a mechanism to advertise their IP address to new or existing peers in the overlay so they may receive inbound connections up to some desired limit. And finally, they need a mechanism to provide inbound connection requests with an alternate set of IP addresses to try when they have already reached their desired maximum number of inbound connections.
PeerFinder is a self contained module that provides these services, along with some
additional overlay network management services such as _fixed slots_ and _cluster
slots_.
PeerFinder is a self contained module that provides these services, along with some additional overlay network management services such as _fixed slots_ and _cluster slots_.
## Features
PeerFinder has these responsibilities
- Maintain a persistent set of endpoint addresses suitable for bootstrapping
into the peer to peer overlay, ranked by relative locally observed utility.
- Maintain a persistent set of endpoint addresses suitable for bootstrapping into the peer to peer overlay, ranked by relative locally observed utility.
- Send and receive protocol messages for discovery of endpoint addresses.
@@ -41,8 +24,7 @@ PeerFinder has these responsibilities
- Impose limits on the various slots consumed by peer connections.
- Initiate outgoing connection attempts to endpoint addresses to maintain the
overlay connectivity and fixed peer policies.
- Initiate outgoing connection attempts to endpoint addresses to maintain the overlay connectivity and fixed peer policies.
- Verify the connectivity of neighbors who advertise inbound connection slots.
@@ -54,29 +36,19 @@ PeerFinder has these responsibilities
## Manager
The `Manager` is an application singleton which provides the primary interface
to interaction with the PeerFinder.
The `Manager` is an application singleton which provides the primary interface to interaction with the PeerFinder.
### Autoconnect
The Autoconnect feature of PeerFinder automatically establishes outgoing
connections using addresses learned from various sources including the
configuration file, the result of domain name lookups, and messages received
from the overlay itself.
The Autoconnect feature of PeerFinder automatically establishes outgoing connections using addresses learned from various sources including the configuration file, the result of domain name lookups, and messages received from the overlay itself.
### Callback
PeerFinder is an isolated code module with few external dependencies. To perform
socket specific activities such as establishing outgoing connections or sending
messages to connected peers, the Manager is constructed with an abstract
interface called the `Callback`. An instance of this interface performs the
actual required operations, making PeerFinder independent of the calling code.
PeerFinder is an isolated code module with few external dependencies. To perform socket specific activities such as establishing outgoing connections or sending messages to connected peers, the Manager is constructed with an abstract interface called the `Callback`. An instance of this interface performs the actual required operations, making PeerFinder independent of the calling code.
### Config
The `Config` structure defines the operational parameters of the PeerFinder.
Some values come from the configuration file while others are calculated via
tuned heuristics. The fields are as follows:
The `Config` structure defines the operational parameters of the PeerFinder. Some values come from the configuration file while others are calculated via tuned heuristics. The fields are as follows:
- `autoConnect`
@@ -84,150 +56,77 @@ tuned heuristics. The fields are as follows:
- `wantIncoming`
A flag indicating whether or not the peer desires inbound connections. When
this flag is turned off, a peer will not advertise itself in Endpoint
messages.
A flag indicating whether or not the peer desires inbound connections. When this flag is turned off, a peer will not advertise itself in Endpoint messages.
- `listeningPort`
The port number to use when creating the listening socket for peer
connections.
The port number to use when creating the listening socket for peer connections.
- `maxPeers`
The largest number of active peer connections to allow. This includes inbound
and outbound connections, but excludes fixed and cluster peers. There is an
implementation defined floor on this value.
The largest number of active peer connections to allow. This includes inbound and outbound connections, but excludes fixed and cluster peers. There is an implementation defined floor on this value.
- `outPeers`
The number of automatic outbound connections that PeerFinder will maintain
when the Autoconnect feature is enabled. The value is computed with fractional
precision as an implementation defined percentage of `maxPeers` subject to
an implementation defined floor. An instance of the PeerFinder rounds the
fractional part up or down using a uniform random number generated at
program startup. This allows the out-degree of the overlay network to be
controlled with fractional precision, ensuring that all inbound network
connection slots are not consumed (which would make it difficult for new
participants to enter the network).
The number of automatic outbound connections that PeerFinder will maintain when the Autoconnect feature is enabled. The value is computed with fractional precision as an implementation defined percentage of `maxPeers` subject to an implementation defined floor. An instance of the PeerFinder rounds the fractional part up or down using a uniform random number generated at program startup. This allows the out-degree of the overlay network to be controlled with fractional precision, ensuring that all inbound network connection slots are not consumed (which would make it difficult for new participants to enter the network).
Here's an example of how the network might be structured with a fractional
value for outPeers:
Here's an example of how the network might be structured with a fractional value for outPeers:
**(Need example here)**
### Livecache
The Livecache holds relayed IP addresses that have been received recently in
the form of Endpoint messages via the peer to peer overlay. A peer periodically
broadcasts the Endpoint message to its neighbors when it has open inbound
connection slots. Peers store these messages in the Livecache and periodically
forward their neighbors a handful of random entries from their Livecache, with
an incremented hop count for each forwarded entry.
The Livecache holds relayed IP addresses that have been received recently in the form of Endpoint messages via the peer to peer overlay. A peer periodically broadcasts the Endpoint message to its neighbors when it has open inbound connection slots. Peers store these messages in the Livecache and periodically forward their neighbors a handful of random entries from their Livecache, with an incremented hop count for each forwarded entry.
The algorithm for sending a neighbor a set of Endpoint messages chooses evenly
from all available hop counts on each send. This ensures that each peer
will see some entries with the farthest hops at each iteration. The result is
to expand a peer's horizon with respect to which overlay endpoints are visible.
This is designed to force the overlay to become highly connected and reduce
the network diameter with each connection establishment.
The algorithm for sending a neighbor a set of Endpoint messages chooses evenly from all available hop counts on each send. This ensures that each peer will see some entries with the farthest hops at each iteration. The result is to expand a peer's horizon with respect to which overlay endpoints are visible. This is designed to force the overlay to become highly connected and reduce the network diameter with each connection establishment.
When a peer receives an Endpoint message that originates from a neighbor
(identified by a hop count of zero) for the first time, it performs an incoming
connection test on that neighbor by initiating an outgoing connection to the
remote IP address as seen on the connection combined with the port advertised
in the Endpoint message. If the test fails, then the peer considers its neighbor
firewalled (intentionally or due to misconfiguration) and not forward neighbor
endpoint in Endpoint messages. This prevents poor quality un-connectable
addresses from landing in the caches. If the incoming connection test passes,
then the peer fills in the Endpoint message with the remote address as seen on
the connection before storing it in its cache and forwarding it to other peers.
This relieves the neighbor from the responsibility of knowing its own IP address
before it can start receiving incoming connections.
When a peer receives an Endpoint message that originates from a neighbor (identified by a hop count of zero) for the first time, it performs an incoming connection test on that neighbor by initiating an outgoing connection to the remote IP address as seen on the connection combined with the port advertised in the Endpoint message. If the test fails, then the peer considers its neighbor firewalled (intentionally or due to misconfiguration) and not forward neighbor endpoint in Endpoint messages. This prevents poor quality un-connectable addresses from landing in the caches. If the incoming connection test passes, then the peer fills in the Endpoint message with the remote address as seen on the connection before storing it in its cache and forwarding it to other peers. This relieves the neighbor from the responsibility of knowing its own IP address before it can start receiving incoming connections.
Livecache entries expire quickly. Since a peer stops advertising itself when
it no longer has available inbound slots, its address will shortly after stop
being handed out by other peers. Livecache entries are very likely to result
in both a successful connection establishment and the acquisition of an active
outbound slot. Compare this with Bootcache addresses, which are very likely to
be connectable but unlikely to have an open slot.
Livecache entries expire quickly. Since a peer stops advertising itself when it no longer has available inbound slots, its address will shortly after stop being handed out by other peers. Livecache entries are very likely to result in both a successful connection establishment and the acquisition of an active outbound slot. Compare this with Bootcache addresses, which are very likely to be connectable but unlikely to have an open slot.
Because entries in the Livecache are ephemeral, they are not persisted across
launches in the database. The Livecache is continually updated and expired as
Endpoint messages are received from the overlay over time.
Because entries in the Livecache are ephemeral, they are not persisted across launches in the database. The Livecache is continually updated and expired as Endpoint messages are received from the overlay over time.
### Bootcache
The `Bootcache` stores IP addresses useful for gaining initial connections.
Each address is associated with the following metadata:
The `Bootcache` stores IP addresses useful for gaining initial connections. Each address is associated with the following metadata:
- **Valence**
A signed integer which represents the number of successful
consecutive connection attempts when positive, and the number of
failed consecutive connection attempts when negative. If an outgoing
connection attempt to the corresponding IP address fails to complete the
handshake the valence is reset to negative one. This harsh penalty is
intended to prevent popular servers from forever remaining top ranked in
all peer databases.
A signed integer which represents the number of successful consecutive connection attempts when positive, and the number of failed consecutive connection attempts when negative. If an outgoing connection attempt to the corresponding IP address fails to complete the handshake the valence is reset to negative one. This harsh penalty is intended to prevent popular servers from forever remaining top ranked in all peer databases.
When choosing addresses from the boot cache for the purpose of
establishing outgoing connections, addresses are ranked in decreasing order of
valence. The Bootcache is persistent. Entries are periodically inserted and
updated in the corresponding SQLite database during program operation. When
**xrpld** is launched, the existing Bootcache database data is accessed and
loaded to accelerate the bootstrap process.
When choosing addresses from the boot cache for the purpose of establishing outgoing connections, addresses are ranked in decreasing order of valence. The Bootcache is persistent. Entries are periodically inserted and updated in the corresponding SQLite database during program operation. When **xrpld** is launched, the existing Bootcache database data is accessed and loaded to accelerate the bootstrap process.
Desirable entries in the Bootcache are addresses for servers which are known to
have high uptimes, and for which connection attempts usually succeed. However,
these servers do not necessarily have available inbound connection slots.
However, it is assured that these servers will have a well populated Livecache
since they will have moved towards the core of the overlay over their high
uptime. When a connected server is full it will return a handful of new
addresses from its Livecache and gracefully close the connection. Addresses
from the Livecache are highly likely to have inbound connection slots and be
connectable.
Desirable entries in the Bootcache are addresses for servers which are known to have high uptimes, and for which connection attempts usually succeed. However, these servers do not necessarily have available inbound connection slots. However, it is assured that these servers will have a well populated Livecache since they will have moved towards the core of the overlay over their high uptime. When a connected server is full it will return a handful of new addresses from its Livecache and gracefully close the connection. Addresses from the Livecache are highly likely to have inbound connection slots and be connectable.
For security, all information that contributes to the ranking of Bootcache
entries is observed locally. PeerFinder never trusts external sources of information.
For security, all information that contributes to the ranking of Bootcache entries is observed locally. PeerFinder never trusts external sources of information.
### Slot
Each TCP/IP socket that can participate in the peer to peer overlay occupies
a slot. Slots have properties and state associated with them:
Each TCP/IP socket that can participate in the peer to peer overlay occupies a slot. Slots have properties and state associated with them:
#### State (Slot)
The slot state represents the current stage of the connection as it passes
through the business logic for establishing peer connections.
The slot state represents the current stage of the connection as it passes through the business logic for establishing peer connections.
- `accept`
The accept state is an initial state resulting from accepting an incoming
connection request on a listening socket. The remote IP address and port
are known, and a handshake is expected next.
The accept state is an initial state resulting from accepting an incoming connection request on a listening socket. The remote IP address and port are known, and a handshake is expected next.
- `connect`
The connect state is an initial state used when actively establishing outbound
connection attempts. The desired remote IP address and port are known.
The connect state is an initial state used when actively establishing outbound connection attempts. The desired remote IP address and port are known.
- `connected`
When an outbound connection attempt succeeds, it moves to the connected state.
The handshake is initiated but not completed.
When an outbound connection attempt succeeds, it moves to the connected state. The handshake is initiated but not completed.
- `active`
The state becomes Active when a connection in either the Accepted or Connected
state completes the handshake process, and a slot is available based on the
properties. If no slot is available when the handshake completes, the socket
is gracefully closed.
The state becomes Active when a connection in either the Accepted or Connected state completes the handshake process, and a slot is available based on the properties. If no slot is available when the handshake completes, the socket is gracefully closed.
- `closing`
The Closing state represents a connected socket in the process of being
gracefully closed.
The Closing state represents a connected socket in the process of being gracefully closed.
#### Properties (Slot)
@@ -235,48 +134,27 @@ Slot properties may be combined and are not mutually exclusive.
- **Inbound**
An inbound slot is the condition of a socket which has accepted an incoming
connection request. A connection which is not inbound is by definition
outbound.
An inbound slot is the condition of a socket which has accepted an incoming connection request. A connection which is not inbound is by definition outbound.
- **Fixed**
A fixed slot is a desired connection to a known peer identified by IP address,
usually entered manually in the configuration file. For the purpose of
establishing outbound connections, the peer also has an associated port number
although only the IP address is checked to determine if the fixed peer is
already connected. Fixed slots do not count towards connection limits.
A fixed slot is a desired connection to a known peer identified by IP address, usually entered manually in the configuration file. For the purpose of establishing outbound connections, the peer also has an associated port number although only the IP address is checked to determine if the fixed peer is already connected. Fixed slots do not count towards connection limits.
- **Cluster**
A cluster slot is a connection which has completed the handshake stage, whose
public key matches a known public key usually entered manually in the
configuration file or learned through overlay messages from other trusted
peers. Cluster slots do not count towards connection limits.
A cluster slot is a connection which has completed the handshake stage, whose public key matches a known public key usually entered manually in the configuration file or learned through overlay messages from other trusted peers. Cluster slots do not count towards connection limits.
- **Superpeer** (forthcoming)
A superpeer slot is a connection to a peer which can accept incoming
connections, meets certain resource availability requirements (such as
bandwidth, CPU, and storage capacity), and operates full duplex in the
overlay. Connections which are not superpeers are by definition leaves. A
leaf slot is a connection to a peer which does not route overlay messages to
other peers, and operates in a partial half duplex fashion in the overlay.
A superpeer slot is a connection to a peer which can accept incoming connections, meets certain resource availability requirements (such as bandwidth, CPU, and storage capacity), and operates full duplex in the overlay. Connections which are not superpeers are by definition leaves. A leaf slot is a connection to a peer which does not route overlay messages to other peers, and operates in a partial half duplex fashion in the overlay.
#### Fixed Slots
Fixed slots are identified by IP address and set up during the initialization
of the Manager, usually from the configuration file. The Logic will always make
outgoing connection attempts to each fixed slot which is not currently
connected. If we receive an inbound connection from an endpoint whose address
portion (without port) matches a fixed slot address, we consider the fixed
slot to be connected.
Fixed slots are identified by IP address and set up during the initialization of the Manager, usually from the configuration file. The Logic will always make outgoing connection attempts to each fixed slot which is not currently connected. If we receive an inbound connection from an endpoint whose address portion (without port) matches a fixed slot address, we consider the fixed slot to be connected.
#### Cluster Slots
Cluster slots are identified by the public key and set up during the
initialization of the manager or discovered upon receipt of messages in the
overlay from trusted connections.
Cluster slots are identified by the public key and set up during the initialization of the manager or discovered upon receipt of messages in the overlay from trusted connections.
---
@@ -284,62 +162,35 @@ overlay from trusted connections.
## Connection Strategy
The _Connection Strategy_ applies the configuration settings to establish
desired outbound connections. It runs periodically and progresses through a
series of stages, remaining in each stage until a condition is met
The _Connection Strategy_ applies the configuration settings to establish desired outbound connections. It runs periodically and progresses through a series of stages, remaining in each stage until a condition is met
### Stage 1: Fixed Slots
This stage is invoked when the number of active fixed connections is below the
number of fixed connections specified in the configuration, and one of the
following is true:
This stage is invoked when the number of active fixed connections is below the number of fixed connections specified in the configuration, and one of the following is true:
- There are eligible fixed addresses to try
- Any outbound connection attempts are in progress
Each fixed address is associated with a retry timer. On a fixed connection
failure, the timer is reset so that the address is not tried for some amount
of time, which increases according to a scheduled sequence up to some maximum
which is currently set to approximately one hour between retries. A fixed
address is considered eligible if we are not currently connected or attempting
the address, and its retry timer has expired.
Each fixed address is associated with a retry timer. On a fixed connection failure, the timer is reset so that the address is not tried for some amount of time, which increases according to a scheduled sequence up to some maximum which is currently set to approximately one hour between retries. A fixed address is considered eligible if we are not currently connected or attempting the address, and its retry timer has expired.
The PeerFinder makes its best effort to become fully connected to the fixed
addresses specified in the configuration file before moving on to establish
outgoing connections to foreign peers. This security feature helps xrpld
establish itself with a trusted set of peers first before accepting untrusted
data from the network.
The PeerFinder makes its best effort to become fully connected to the fixed addresses specified in the configuration file before moving on to establish outgoing connections to foreign peers. This security feature helps xrpld establish itself with a trusted set of peers first before accepting untrusted data from the network.
### Stage 2: Livecache
The Livecache is invoked when Stage 1 is not active, autoconnect is enabled,
and the number of active outbound connections is below the number desired. The
stage remains active while:
The Livecache is invoked when Stage 1 is not active, autoconnect is enabled, and the number of active outbound connections is below the number desired. The stage remains active while:
- The Livecache has addresses to try
- Any outbound connection attempts are in progress
PeerFinder makes its best effort to exhaust addresses in the Livecache before
moving on to the Bootcache, because Livecache addresses are highly likely
to be connectable (since they are known to have been online within the last
minute), and highly likely to have an open slot for an incoming connection
(because peers only advertise themselves in the Livecache when they have
open slots).
PeerFinder makes its best effort to exhaust addresses in the Livecache before moving on to the Bootcache, because Livecache addresses are highly likely to be connectable (since they are known to have been online within the last minute), and highly likely to have an open slot for an incoming connection (because peers only advertise themselves in the Livecache when they have open slots).
### Stage 3: Bootcache
The Bootcache is invoked when Stage 1 and Stage 2 are not active, autoconnect
is enabled, and the number of active outbound connections is below the number
desired. The stage remains active while:
The Bootcache is invoked when Stage 1 and Stage 2 are not active, autoconnect is enabled, and the number of active outbound connections is below the number desired. The stage remains active while:
- There are addresses in the cache that have not been tried recently.
Entries in the Bootcache are ranked, with highly connectable addresses preferred
over others. Connection attempts to Bootcache addresses are very likely to
succeed but unlikely to produce an active connection since the peers likely do
not have open slots. Before the remote peer closes the connection it will send
a handful of addresses from its Livecache to help the new peer coming online
obtain connections.
Entries in the Bootcache are ranked, with highly connectable addresses preferred over others. Connection attempts to Bootcache addresses are very likely to succeed but unlikely to produce an active connection since the peers likely do not have open slots. Before the remote peer closes the connection it will send a handful of addresses from its Livecache to help the new peer coming online obtain connections.
---
@@ -347,8 +198,7 @@ obtain connections.
Much of the work in PeerFinder was inspired by earlier work in Gnutella:
[Revised Gnutella Ping Pong Scheme](http://rfc-gnutella.sourceforge.net/src/pong-caching.html)<br>
_By Christopher Rohrs and Vincent Falco_
[Revised Gnutella Ping Pong Scheme](http://rfc-gnutella.sourceforge.net/src/pong-caching.html)<br> _By Christopher Rohrs and Vincent Falco_
[Gnutella 0.6 Protocol:](http://rfc-gnutella.sourceforge.net/src/rfc-0_6-draft.html) Sections:

View File

@@ -2,72 +2,49 @@
## Introduction.
By default, an RPC handler runs as an uninterrupted task on the JobQueue. This
is fine for commands that are fast to compute but might not be acceptable for
tasks that require multiple parts or are large, like a full ledger.
By default, an RPC handler runs as an uninterrupted task on the JobQueue. This is fine for commands that are fast to compute but might not be acceptable for tasks that require multiple parts or are large, like a full ledger.
For this purpose, the xrpld RPC handler allows _suspension with continuation_
- a request to suspend execution of the RPC response and to continue it after
some function or job has been executed. A default continuation is supplied
which simply reschedules the job on the JobQueue, or the programmer can supply
their own.
- a request to suspend execution of the RPC response and to continue it after some function or job has been executed. A default continuation is supplied which simply reschedules the job on the JobQueue, or the programmer can supply their own.
## The classes.
Suspension with continuation uses four `std::function`s in the `xrpl::RPC`
namespace:
Suspension with continuation uses four `std::function`s in the `xrpl::RPC` namespace:
using Callback = std::function <void ()>;
using Continuation = std::function <void (Callback const&)>;
using Suspend = std::function <void (Continuation const&)>;
using Coroutine = std::function <void (Suspend const&)>;
A `Callback` is a generic 0-argument function. A given `Callback` might or might
not block. Unless otherwise advised, do not hold locks or any resource that
would prevent any other task from making forward progress when you call a
`Callback`.
A `Callback` is a generic 0-argument function. A given `Callback` might or might not block. Unless otherwise advised, do not hold locks or any resource that would prevent any other task from making forward progress when you call a `Callback`.
A `Continuation` is a function that is given a `Callback` and promises to call
it later. A `Continuation` guarantees to call the `Callback` exactly once at
some point in the future, but it does not have to be immediately or even in the
current thread.
A `Continuation` is a function that is given a `Callback` and promises to call it later. A `Continuation` guarantees to call the `Callback` exactly once at some point in the future, but it does not have to be immediately or even in the current thread.
A `Suspend` is a function belonging to a `Coroutine`. A `Suspend` runs a
`Continuation`, passing it a `Callback` that continues execution of the
`Coroutine`.
A `Suspend` is a function belonging to a `Coroutine`. A `Suspend` runs a `Continuation`, passing it a `Callback` that continues execution of the `Coroutine`.
And finally, a `Coroutine` is a `std::function` which is given a
`Suspend`. This is what the RPC handler gives to the coroutine manager,
expecting to get called back with a `Suspend` and to be able to start execution.
And finally, a `Coroutine` is a `std::function` which is given a `Suspend`. This is what the RPC handler gives to the coroutine manager, expecting to get called back with a `Suspend` and to be able to start execution.
## The flow of control.
Given these functions, the flow of RPC control when using coroutines is
straight-forward.
Given these functions, the flow of RPC control when using coroutines is straight-forward.
1. The instance of `ServerHandler` receives an RPC request.
2. It creates a `Coroutine` and gives it to the coroutine manager.
3. The coroutine manager creates a `Coroutine`, starts it up, and then calls
the `Coroutine` with a `Suspend`.
3. The coroutine manager creates a `Coroutine`, starts it up, and then calls the `Coroutine` with a `Suspend`.
4. Now the RPC response starts to be calculated.
5. When the RPC handler wants to suspend, it calls the `Suspend` function with
a `Continuation`.
5. When the RPC handler wants to suspend, it calls the `Suspend` function with a `Continuation`.
6. Coroutine execution is suspended.
7. The `Continuation` is called with a `Callback` that the coroutine manager
creates.
7. The `Continuation` is called with a `Callback` that the coroutine manager creates.
8. The `Continuation` may choose to execute immediately, defer execution on the
job queue, or wait for some resource to be free.
8. The `Continuation` may choose to execute immediately, defer execution on the job queue, or wait for some resource to be free.
9. When the `Continuation` is finished, it calls the `Callback` that the
coroutine manager gave it, perhaps a long time ago.
9. When the `Continuation` is finished, it calls the `Callback` that the coroutine manager gave it, perhaps a long time ago.
10. This `Callback` continues execution on the suspended `Coroutine` from where
it left off.
10. This `Callback` continues execution on the suspended `Coroutine` from where it left off.

View File

@@ -1,5 +1,3 @@
# Integration tests
This directory contains integration tests for the project. These tests are run
against the `libxrpl` library or `xrpld` binary to verify they are working as
expected.
This directory contains integration tests for the project. These tests are run against the `libxrpl` library or `xrpld` binary to verify they are working as expected.