Merge branch 'xrplf/sponsor' into xrplf/sponsor-supported

This commit is contained in:
Mayukha Vadari
2026-06-30 18:02:50 -04:00
committed by GitHub
177 changed files with 6025 additions and 7005 deletions

View File

@@ -153,6 +153,7 @@ Checks: "-*,
readability-use-std-min-max
"
# ---
# bugprone-narrowing-conversions, # this will break a lot of code but we should enable it in the future because it can eliminate a lot of bugs
# readability-inconsistent-declaration-parameter-name, # In this codebase this check will break a lot of arg names
# readability-static-accessed-through-instance, # this check is probably unnecessary. It makes the code less readable
# ---

View File

@@ -14,7 +14,6 @@ libxrpl.ledger > xrpl.json
libxrpl.ledger > xrpl.ledger
libxrpl.ledger > xrpl.nodestore
libxrpl.ledger > xrpl.protocol
libxrpl.ledger > xrpl.server
libxrpl.ledger > xrpl.shamap
libxrpl.net > xrpl.basics
libxrpl.net > xrpl.net
@@ -221,7 +220,6 @@ xrpl.core > xrpl.protocol
xrpl.json > xrpl.basics
xrpl.ledger > xrpl.basics
xrpl.ledger > xrpl.protocol
xrpl.ledger > xrpl.server
xrpl.ledger > xrpl.shamap
xrpl.net > xrpl.basics
xrpl.nodestore > xrpl.basics

View File

@@ -20,8 +20,6 @@ _SANITIZER_SUFFIX: dict[str, str] = {
def get_cmake_args(build_type: str, extra_args: str) -> str:
"""Get the full list of CMake arguments for a config."""
args = _BASE_CMAKE_ARGS.copy()
if build_type == "Release":
args.append("-Dassert=ON")
if extra_args:
args.extend(extra_args.split())
return " ".join(args)

View File

@@ -1,5 +1,5 @@
{
"image_tag": "sha-63ffdc3",
"image_tag": "sha-fe4c8ae",
"configs": {
"ubuntu": [
{
@@ -68,7 +68,7 @@
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-63ffdc3"
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745"
}
],
@@ -77,7 +77,7 @@
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-63ffdc3"
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745"
}
]
}

View File

@@ -9,12 +9,20 @@ on:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "bin/check-tools.sh"
- "bin/install-sanitizer-libs.sh"
pull_request:
paths:
- ".github/workflows/build-nix-images.yml"
- "flake.nix"
- "flake.lock"
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "bin/check-tools.sh"
- "bin/install-sanitizer-libs.sh"
workflow_dispatch:
concurrency:
@@ -46,9 +54,9 @@ jobs:
base_image: debian:bookworm
- name: rhel
base_image: registry.access.redhat.com/ubi9/ubi:latest
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@ee03d31bcc4501d7599dc1b1ecd7a34af582ad1c
with:
image_name: ghcr.io/xrplf/xrpld/nix-${{ matrix.distro.name }}
image_name: xrpld/nix-${{ matrix.distro.name }}
dockerfile: nix/docker/Dockerfile
base_image: ${{ matrix.distro.base_image }}
push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
push: ${{ github.event_name == 'push' }}

View File

@@ -38,9 +38,9 @@ jobs:
base_image: debian:bookworm
- name: rhel
base_image: registry.access.redhat.com/ubi9/ubi:latest
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@ee03d31bcc4501d7599dc1b1ecd7a34af582ad1c
with:
image_name: ghcr.io/xrplf/xrpld/packaging-${{ matrix.distro.name }}
image_name: xrpld/packaging-${{ matrix.distro.name }}
dockerfile: package/Dockerfile
base_image: ${{ matrix.distro.base_image }}
push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
push: ${{ github.event_name == 'push' }}

View File

@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Write PR body to file
env:

View File

@@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Determine changed files
# This step checks whether any files have changed that should
# cause the next jobs to run. We do it this way rather than

View File

@@ -14,7 +14,7 @@ on:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@312aaab296060ff89d7f798dcab59f019bea6e02
uses: XRPLF/actions/.github/workflows/pre-commit.yml@e06d4138c9ec8dceeb7c818645faa38087ea9e3d
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'

View File

@@ -41,10 +41,10 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8

View File

@@ -110,7 +110,7 @@ jobs:
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
@@ -121,6 +121,11 @@ jobs:
if: ${{ inputs.ccache_enabled && runner.debug == '1' }}
run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >>"${GITHUB_ENV}"
- name: Check tools
env:
CHECK_TOOLS_SKIP_CLONE: "1"
run: ./bin/check-tools.sh
- name: Print build environment
uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Check levelization
run: python .github/scripts/levelization/generate.py
- name: Check for differences

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Check definitions
run: .github/scripts/rename/definitions.sh .
- name: Check copyright notices

View File

@@ -20,29 +20,32 @@ env:
BUILD_DIR: build
BUILD_TYPE: Debug # Debug so that ASSERTS and such participate in clang-tidy check
OUTPUT_FILE: clang-tidy-output.txt
DIFF_FILE: clang-tidy-git-diff.txt
ISSUE_FILE: clang-tidy-issue.md
OUTPUT_FILE: /tmp/clang-tidy-output.txt
FILTERED_OUTPUT_FILE: /tmp/clang-tidy-filtered-output.txt
DIFF_FILE: /tmp/clang-tidy-git-diff.txt
ISSUE_FILE: /tmp/clang-tidy-issue.md
COMPILER: clang
jobs:
determine-files:
if: ${{ inputs.check_only_changed }}
permissions:
contents: read
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@312aaab296060ff89d7f798dcab59f019bea6e02
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@c7045074aafe9fb92fa537aa4446f81fbfc17e8b
run-clang-tidy:
name: Run clang tidy
needs: [determine-files]
if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-63ffdc3"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-fe4c8ae"
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
@@ -59,7 +62,7 @@ jobs:
- name: Set compiler environment
uses: ./.github/actions/set-compiler-env
with:
compiler: clang
compiler: ${{ env.COMPILER }}
- name: Setup Conan
uses: ./.github/actions/setup-conan
@@ -150,21 +153,21 @@ jobs:
run: |
if [ -f "${OUTPUT_FILE}" ]; then
# Extract lines containing 'error:', 'warning:', or 'note:'
grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >filtered-output.txt || true
grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true
# If filtered output is empty, use original (might be a different error format)
if [ ! -s filtered-output.txt ]; then
cp "${OUTPUT_FILE}" filtered-output.txt
if [ ! -s "${FILTERED_OUTPUT_FILE}" ]; then
cp "${OUTPUT_FILE}" "${FILTERED_OUTPUT_FILE}"
fi
# Truncate if too large
head -c 60000 filtered-output.txt >>"${ISSUE_FILE}"
if [ "$(wc -c <filtered-output.txt)" -gt 60000 ]; then
head -c 60000 "${FILTERED_OUTPUT_FILE}" >>"${ISSUE_FILE}"
if [ "$(wc -c <"${FILTERED_OUTPUT_FILE}")" -gt 60000 ]; then
echo "" >>"${ISSUE_FILE}"
echo "... (output truncated, see artifacts for full output)" >>"${ISSUE_FILE}"
fi
rm filtered-output.txt
rm "${FILTERED_OUTPUT_FILE}"
else
echo "No output file found" >>"${ISSUE_FILE}"
fi

View File

@@ -27,7 +27,7 @@ jobs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -45,7 +45,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
sparse-checkout: |
.github/actions/generate-version
@@ -69,7 +69,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Download pre-built binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

View File

@@ -23,7 +23,7 @@ jobs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0

View File

@@ -40,10 +40,10 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Generate build version number
id: version

View File

@@ -65,7 +65,7 @@ jobs:
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8

391
BUILD.md
View File

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

View File

@@ -14,9 +14,9 @@ The following branches exist in the main project repository:
- `develop`: The latest set of unreleased features, and the most common
starting point for contributions.
- `release`: The latest beta release or release candidate.
- `master`: The latest stable release.
- `gh-pages`: The documentation for this project, built by Doxygen.
- `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
@@ -130,11 +130,9 @@ tl;dr
## Pull requests
In general, pull requests use `develop` as the base branch.
The exceptions are
- Fixes and improvements to a release candidate use `release` as the
base.
- Hotfixes use `master` 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.
@@ -216,7 +214,7 @@ coherent rather than a set of _thou shalt not_ commandments.
## Formatting
All code must conform to `clang-format` version 21,
All code must conform to `clang-format` version 22,
according to the settings in [`.clang-format`](./.clang-format),
unless the result would be unreasonably difficult to read or maintain.
To demarcate lines that should be left as-is, surround them with comments like
@@ -261,7 +259,7 @@ This ensures that configuration changes don't introduce new warnings across the
### Installing clang-tidy
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for platform-specific installation instructions.
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
### Running clang-tidy locally

158
bin/check-tools.sh Executable file
View File

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

View File

@@ -109,6 +109,7 @@ words:
- enabled
- enablerepo
- endmacro
- envrc
- exceptioned
- EXPECT_STREQ
- Falco
@@ -318,6 +319,7 @@ words:
- unserviced
- unshareable
- unshares
- unsponsored
- unsquelch
- unsquelched
- unsquelching

193
docs/build/advanced_conan.md vendored Normal file
View File

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

2
docs/build/conan.md vendored
View File

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

View File

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

45
docs/build/nix.md vendored
View File

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

61
docs/build/nix_troubleshooting.md vendored Normal file
View File

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

View File

@@ -1,29 +0,0 @@
#pragma once
#if XRPL_ROCKSDB_AVAILABLE
// #include <rocksdb2/port/port_posix.h>
#include <rocksdb/cache.h>
#include <rocksdb/compaction_filter.h>
#include <rocksdb/comparator.h>
#include <rocksdb/convenience.h>
#include <rocksdb/db.h>
#include <rocksdb/env.h>
#include <rocksdb/filter_policy.h>
#include <rocksdb/flush_block_policy.h>
#include <rocksdb/iterator.h>
#include <rocksdb/memtablerep.h>
#include <rocksdb/merge_operator.h>
#include <rocksdb/options.h>
#include <rocksdb/perf_context.h>
#include <rocksdb/slice.h>
#include <rocksdb/slice_transform.h>
#include <rocksdb/statistics.h>
#include <rocksdb/status.h>
#include <rocksdb/table.h>
#include <rocksdb/table_properties.h>
#include <rocksdb/transaction_log.h>
#include <rocksdb/types.h>
#include <rocksdb/universal_compaction.h>
#include <rocksdb/write_batch.h>
#endif

View File

@@ -4,7 +4,7 @@
/*
ASAN flags some false positives with sudden jumps in control flow, like
exceptions, or when encountering coroutine stack switches. This macro can be used to disable ASAN
intrumentation for specific functions.
instrumentation for specific functions.
*/
#if defined(__GNUC__) || defined(__clang__)
#define XRPL_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address", "hwaddress")))

View File

@@ -1,49 +0,0 @@
#pragma once
#include <xrpl/protocol/MultiApiJson.h>
#include <xrpl/server/InfoSub.h>
#include <memory>
#include <mutex>
namespace xrpl {
/** Listen to public/subscribe messages from a book. */
class BookListeners
{
public:
using pointer = std::shared_ptr<BookListeners>;
BookListeners() = default;
/** Add a new subscription for this book
*/
void
addSubscriber(InfoSub::ref sub);
/** Stop publishing to a subscriber
*/
void
removeSubscriber(std::uint64_t sub);
/** Publish a transaction to subscribers
Publish a transaction to clients subscribed to changes on this book.
Uses havePublished to prevent sending duplicate transactions to clients
that have subscribed to multiple books.
@param jvObj JSON transaction data to publish
@param havePublished InfoSub sequence numbers that have already
published this transaction.
*/
void
publish(MultiApiJson const& jvObj, hash_set<std::uint64_t>& havePublished);
private:
std::recursive_mutex lock_;
hash_map<std::uint64_t, InfoSub::wptr> listeners_;
};
} // namespace xrpl

View File

@@ -1,11 +1,11 @@
#pragma once
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/AcceptedLedgerTx.h>
#include <xrpl/ledger/BookListeners.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/MultiApiJson.h>
#include <xrpl/protocol/UintTypes.h>
#include <memory>
@@ -77,34 +77,24 @@ public:
*/
virtual bool
isBookToXRP(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
/**
* Process a transaction for order book tracking.
* @param ledger The ledger the transaction was applied to
* @param alTx The transaction to process
* @param jvObj The JSON object of the transaction
*/
virtual void
processTxn(
std::shared_ptr<ReadView const> const& ledger,
AcceptedLedgerTx const& alTx,
MultiApiJson const& jvObj) = 0;
/**
* Get the book listeners for a book.
* @param book The book to get the listeners for
* @return The book listeners for the book
*/
virtual BookListeners::pointer
getBookListeners(Book const&) = 0;
/**
* Create a new book listeners for a book.
* @param book The book to create the listeners for
* @return The new book listeners for the book
*/
virtual BookListeners::pointer
makeBookListeners(Book const&) = 0;
};
/** Extract the set of books affected by a transaction.
*
* Walks the transaction's metadata nodes and collects every order book
* whose offers were created, modified, or deleted. Used by NetworkOPs to
* fan transaction notifications out to book subscribers.
*
* @param alTx The accepted ledger transaction to inspect.
* @param j Journal used to log per-node parsing failures. Inspecting an
* offer node can throw if a required field is missing; in that
* case the bad node is skipped and a warn-level message is
* emitted via @p j. Other affected books in the same transaction
* are still returned.
* @return The set of books whose offers were created, modified, or
* deleted. May be empty for non-offer transactions.
*/
hash_set<Book>
affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j);
} // namespace xrpl

View File

@@ -37,6 +37,8 @@ reduceOffer(auto const& amount)
enum class IsDeposit : bool { No = false, Yes = true };
inline Number const kAMMInvariantRelativeTolerance{1, -11};
/** Calculate LP Tokens given AMM pool reserves.
* @param asset1 AMM one side of the pool reserve
* @param asset2 AMM another side of the pool reserve
@@ -738,6 +740,30 @@ ammPoolHolds(
AuthHandling authHandling,
beast::Journal const j);
/** Check AMM pool product invariant after an AMM operation that changes LP tokens
* (deposit/withdraw/clawback) from an already calculated pool product mean.
* Returns tecPRECISION_LOSS if poolProductMean < newLPTokenBalance beyond the
* invariant tolerance,
* tesSUCCESS otherwise. Skips check when newLPTokenBalance is zero (last withdrawal).
*/
TER
checkAMMPrecisionLoss(Number const& poolProductMean, STAmount const& newLPTokenBalance);
/** Check AMM pool product invariant after an AMM operation that changes LP tokens
* (deposit/withdraw/clawback).
* Returns tecPRECISION_LOSS if sqrt(asset1 * asset2) < newLPTokenBalance beyond
* the invariant tolerance,
* tesSUCCESS otherwise. Skips check when newLPTokenBalance is zero (last withdrawal).
*/
TER
checkAMMPrecisionLoss(
ReadView const& view,
AccountID const& ammAccountID,
Asset const& asset1,
Asset const& asset2,
STAmount const& newLPTokenBalance,
beast::Journal const j);
/** Get AMM pool and LP token balances. If both optIssue are
* provided then they are used as the AMM token pair issues.
* Otherwise the missing issues are fetched from ammSle.

View File

@@ -21,46 +21,92 @@ namespace xrpl {
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, AccountID const& issuer);
// Calculate liquid XRP balance for an account.
// This function may be used to calculate the amount of XRP that
// the holder is able to freely spend. It subtracts reserve requirements.
//
// ownerCountAdj adjusts the owner count in case the caller calculates
// before ledger entries are added or removed. Positive to add, negative
// to subtract.
//
// @param ownerCountAdj positive to add to count, negative to reduce count.
/** Calculate liquid XRP balance for an account.
*
* This function may be used to calculate the amount of XRP that
* the holder is able to freely spend. It subtracts reserve requirements.
*
* ownerCountAdj adjusts the owner count in case the caller calculates
* before ledger entries are added or removed. Positive to add, negative
* to subtract.
*
* @param view The ledger view to read from
* @param id The account ID to check
* @param ownerCountAdj Positive to add to count, negative to reduce count
* @param j Journal for logging
* @return The liquid XRP amount available to the account
*/
[[nodiscard]] XRPAmount
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j);
/** Returns the account reserve, in drops.
Actual owner count can be adjusted by delta in ownerCountAdj
The reserve is calculated as
(ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) *
increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base"
*/
*
* Actual owner count can be adjusted by delta in ownerCountAdj
* Actual reserve count can be adjusted by delta in accountCountAdj
* The reserve is calculated as:
* (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) *
* increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base"
*
* @param view The ledger view to read from
* @param sle The ledger entry for the account
* @param j Journal for logging
* @param ownerCountAdj Adjustment to the owner count (default: 0)
* @param accountCountAdj Adjustment to the account count (default: 0)
* @return The account reserve amount in drops
*/
[[nodiscard]] XRPAmount
accountReserve(
ReadView const& view,
SLE::const_ref sle,
beast::Journal j,
std::int32_t ownerCountAdj = 0,
std::int32_t reserveCountAdj = 0);
std::int32_t accountCountAdj = 0);
/** Convenience overload that accepts AccountID instead of SLE.
*
* @param view The ledger view to read from
* @param id The account ID
* @param j Journal for logging
* @param ownerCountAdj Adjustment to the owner count (default: 0)
* @param accountCountAdj Adjustment to the account count (default: 0)
* @return The account reserve amount in drops
*/
[[nodiscard]] inline XRPAmount
accountReserve(
ReadView const& view,
AccountID const& id,
beast::Journal j,
std::int32_t ownerCountAdj = 0,
std::int32_t reserveCountAdj = 0)
std::int32_t accountCountAdj = 0)
{
return accountReserve(view, view.read(keylet::account(id)), j, ownerCountAdj, reserveCountAdj);
return accountReserve(view, view.read(keylet::account(id)), j, ownerCountAdj, accountCountAdj);
}
/** @brief Return the hypothetical reserve required by an account with the provided counters.
*
* @param view The ledger view to read from
* @param ownerCount Number of objects for which the account will be responsible.
* @param accountCount Number of accounts for which the account will be responsible.
* Defaults to 1, as normally every account is responsible for its own reserve.
* Can be 0 if the account is sponsored.
* Can be greater than 1 if the account is sponsoring other accounts.
* @return The hypothetical reserve amount
*/
XRPAmount
baseAccountReserve(ReadView const& view, std::int32_t ownerCount);
baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t accountCount = 1);
/** Check if an account has insufficient reserve.
*
* @param view The ledger view to read from
* @param tx The transaction being processed
* @param accSle The account's ledger entry
* @param accBalance The account's balance
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param ownerCountAdj Adjustment to the owner count
* @param accountCountAdj Adjustment to the account count (default: 0)
* @param j Journal for logging (default: null sink)
* @return Transaction result code
*/
[[nodiscard]] TER
checkInsufficientReserve(
ReadView const& view,
@@ -68,60 +114,97 @@ checkInsufficientReserve(
SLE::const_ref accSle,
STAmount const& accBalance,
SLE::const_ref sponsorSle,
std::int32_t ownerCountDelta,
std::int32_t reserveCountDelta = 0,
std::int32_t ownerCountAdj,
std::int32_t accountCountAdj = 0,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()});
/** Return number of the objects which reserve is covered by the account(sle) (so called "owner
* count"). Actual owner count can be adjusted by delta in ownerCountAdj.
*
* @param sle The account's ledger entry
* @param j Journal for logging
* @param ownerCountAdj Adjustment to the owner count (default: 0)
* @return The adjusted owner count
*/
std::uint32_t
ownerCount(
ReadView const& view,
SLE::const_ref sle,
beast::Journal j,
std::int32_t ownerCountAdj = 0);
ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj = 0);
/** Adjust the owner count up or down. */
/** Adjust the owner counters of the account up or down. If sponsor provided adjust its counters
* too.
*
* @param view The apply view for making changes
* @param accountSle The account's ledger entry
* @param sponsorSle The sponsor's ledger entry (if applicable)
* @param accountCountAdj Adjustment amount for the account count
* @param j Journal for logging (default: null sink)
*/
void
adjustOwnerCount(
ApplyView& view,
SLE::ref accountSle,
SLE::ref sponsorSle,
std::int32_t amount,
std::int32_t accountCountAdj,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()});
/** Convenience overload that accepts AccountID instead of SLE references.
*
* @param view The apply view for making changes
* @param account The account ID
* @param sponsor The optional sponsor account ID
* @param accountCountAdj Adjustment amount for the account count
* @param j Journal for logging (default: null sink)
*/
inline void
adjustOwnerCount(
ApplyView& view,
AccountID const& account,
std::optional<AccountID> const& sponsor,
std::int32_t amount,
std::int32_t accountCountAdj,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
{
adjustOwnerCount(
view,
view.peek(keylet::account(account)),
sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(),
amount,
accountCountAdj,
j);
}
/** Adjust the owner counters of the account up or down. If object has sponsor adjust its counters
* too. Used primarily just before deleting the object.
*
* @param view The apply view for making changes
* @param accountSle The account's ledger entry
* @param objectSle The object's ledger entry
* @param accountCountAdj Adjustment amount for the account count
* @param j Journal for logging (default: null sink)
*/
void
adjustOwnerCountObj(
ApplyView& view,
SLE::ref accountSle,
SLE::ref objectSle,
std::int32_t amount,
std::int32_t accountCountAdj,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()});
/** Convenience overload that accepts AccountID instead of account SLE reference.
*
* @param view The apply view for making changes
* @param account The account ID
* @param objectSle The object's ledger entry
* @param accountCountAdj Adjustment amount for the account count
* @param j Journal for logging (default: null sink)
*/
inline void
adjustOwnerCountObj(
ApplyView& view,
AccountID const& account,
SLE::ref objectSle,
std::int32_t amount,
std::int32_t accountCountAdj,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
{
SLE::ref accountSle = view.peek(keylet::account(account));
adjustOwnerCountObj(view, accountSle, objectSle, amount, j);
adjustOwnerCountObj(view, accountSle, objectSle, accountCountAdj, j);
}
/** Returns IOU issuer transfer fee as Rate. Rate specifies
@@ -142,25 +225,25 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
/** Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account
if set.
The list is constructed during initialization and is const after that.
Pseudo-account designator fields MUST be maintained by including the
SField::sMD_PseudoAccount flag in the SField definition.
The list is constructed during initialization and is const after that.
Pseudo-account designator fields MUST be maintained by including the
SField::sMD_PseudoAccount flag in the SField definition.
*/
[[nodiscard]] std::vector<SField const*> const&
getPseudoAccountFields();
/** Returns true if and only if sleAcct is a pseudo-account or specific
pseudo-accounts in pseudoFieldFilter.
Returns false if sleAcct is:
- NOT a pseudo-account OR
- NOT a ltACCOUNT_ROOT OR
- null pointer
*/
/** Convenience overload that reads the account from the view. */
[[nodiscard]] bool
isPseudoAccount(SLE::const_ref sleAcct, std::set<SField const*> const& pseudoFieldFilter = {});
/** Convenience overload that reads the account from the view. */
/** Convenience overload that reads the account from the view.
*
* @param view The ledger view to read from
* @param accountId The account ID to check
* @param pseudoFieldFilter Optional set of specific pseudo-account fields to filter (default:
* empty)
* @return true if the account is a pseudo-account (or matches the filter), false otherwise
*/
[[nodiscard]] inline bool
isPseudoAccount(
ReadView const& view,
@@ -183,8 +266,8 @@ createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const
/** Checks the destination and tag.
- Checks that the SLE is not null.
- If the SLE requires a destination tag, checks that there is a tag.
- Checks that the SLE is not null.
- If the SLE requires a destination tag, checks that there is a tag.
*/
[[nodiscard]] TER
checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag);

View File

@@ -36,13 +36,13 @@ checkFields(STTx const& tx, beast::Journal j);
TER
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j);
// Check if subject has any credential maching the given domain. If you call it
// Check if subject has any credential matching the given domain. If you call it
// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in
// doApply. This will ensure that expired credentials are deleted.
TER
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject);
// This function is only called when we about to return tecNO_PERMISSION
// This function is only called when we are about to return tecNO_PERMISSION
// because all the checks for the DepositPreauth authorization failed.
TER
authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst);
@@ -58,7 +58,7 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
} // namespace credentials
// Check expired credentials and for credentials maching DomainID of the ledger
// Check expired credentials and for credentials matching DomainID of the ledger
// object
TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);

View File

@@ -23,13 +23,9 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx);
* @param delegate The delegate account.
* @param type Used to determine which granted granular permissions to load,
* based on the transaction type.
* @param granularPermissions Granted granular permissions tied to the
* transaction type.
* @return the granted granular permissions tied to the transaction type.
*/
void
loadGranularPermission(
SLE::const_ref delegate,
TxType const& type,
std::unordered_set<GranularPermissionType>& granularPermissions);
std::unordered_set<GranularPermissionType>
getGranularPermission(SLE::const_ref delegate, TxType const& type);
} // namespace xrpl

View File

@@ -39,7 +39,7 @@ findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftoken
/** Insert the token in the owner's token directory. */
TER
insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft);
insertToken(ApplyView& view, AccountID owner, STObject&& nft);
/** Remove the token from the owner's token directory. */
TER
@@ -107,7 +107,6 @@ tokenOfferCreatePreclaim(
TER
tokenOfferCreateApply(
ApplyView& view,
STTx const& tx,
AccountID const& acctID,
STAmount const& amount,
std::optional<AccountID> const& dest,

View File

@@ -0,0 +1,19 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace xrpl {
constexpr uint32_t kMinOracleReserveCount = 1;
constexpr uint32_t kMaxOracleReserveCount = 2;
constexpr std::size_t kOracleReserveCountThreshold = 5;
inline uint32_t
calculateOracleReserve(std::size_t priceDataSeriesCount)
{
return priceDataSeriesCount > kOracleReserveCountThreshold ? kMaxOracleReserveCount
: kMinOracleReserveCount;
}
} // namespace xrpl

View File

@@ -5,9 +5,45 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
#include <memory>
#include <optional>
namespace xrpl {
/** Close a payment channel and return its remaining funds to the channel owner.
*
* @param slep The SLE for the PayChannel object to close.
* @param view The apply view in which ledger state modifications are made.
* @param key The ledger key identifying the PayChannel entry.
* @param j Journal used for fatal-level diagnostic messages.
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
* fails; tefINTERNAL if the source account SLE cannot be found.
*/
TER
closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j);
/** Add two uint32_t values with saturation at UINT32_MAX.
*
* @param rules The current ledger rules used to check amendment status.
* @param lhs Left-hand operand.
* @param rhs Right-hand operand.
* @return @p lhs + @p rhs, saturated at UINT32_MAX when the amendment
* is active.
*/
uint32_t
saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs);
/** Determine whether a payment channel time field represents an expired time.
*
* @param view The apply view providing the parent close time and rules.
* @param timeField The optional expiry timestamp (seconds since the XRP
* Ledger epoch). If empty, the function returns false.
* @return @c true if @p timeField is set and the indicated time is
* in the past relative to the view's parent close time;
* @c false otherwise.
*/
bool
isChannelExpired(ApplyView const& view, std::optional<std::uint32_t> timeField);
} // namespace xrpl

View File

@@ -3,6 +3,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/OracleHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
@@ -13,17 +14,15 @@
namespace xrpl {
inline bool
isReserveSponsored(STTx const& tx)
isFeeSponsored(STTx const& tx)
{
return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u;
return (tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u;
}
inline bool
isSponsorReserveCoSigning(STTx const& tx)
isReserveSponsored(STTx const& tx)
{
if (!tx.isFieldPresent(sfSponsorSignature))
return false;
return isReserveSponsored(tx);
return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u;
}
inline std::optional<AccountID>
@@ -125,13 +124,99 @@ removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor)
sle->makeFieldAbsent(field);
}
// namespace sponsor
// {
// // Accessing the ledger to check if provided sponsor is valid.
// [[nodiscard]] TER
// valid(ReadView const& view, STTx const& tx, beast::Journal j)
// {
// }
// }
template <typename T>
inline std::optional<AccountID>
getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account)
{
switch (sle->getType())
{
case ltCHECK:
case ltESCROW:
case ltPAYCHAN:
case ltMPTOKEN:
case ltDELEGATE:
case ltDEPOSIT_PREAUTH:
return sle->getAccountID(sfAccount);
case ltMPTOKEN_ISSUANCE:
return sle->getAccountID(sfIssuer);
case ltSIGNER_LIST: {
auto const signerList = view.read(keylet::signers(account));
if (!signerList)
return std::nullopt;
if (signerList->key() == sle->key())
return account;
return std::nullopt;
}
case ltCREDENTIAL: {
if (sle->isFlag(lsfAccepted))
return sle->getAccountID(sfSubject);
return sle->getAccountID(sfIssuer);
}
case ltRIPPLE_STATE: {
if (sle->isFlag(lsfHighReserve))
{
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
if (highAccount == account)
return highAccount;
}
if (sle->isFlag(lsfLowReserve))
{
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
if (lowAccount == account)
return lowAccount;
}
return std::nullopt;
}
default:
UNREACHABLE("Object is not supported by sponsorship.");
return std::nullopt;
};
}
template <typename T>
inline std::uint32_t
getLedgerEntryOwnerCount(T const& sle)
{
switch (sle->getType())
{
case ltORACLE: {
return calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size());
}
// Vaults require 2 owner counts (the vault and a pseudo-account)
case ltVAULT:
return 2;
default:
return 1;
}
};
template <typename T>
inline SF_ACCOUNT const&
getLedgerEntrySponsorField(T const& sle, AccountID const& owner)
{
switch (sle->getType())
{
case ltRIPPLE_STATE: {
if (sle->isFlag(lsfHighReserve))
{
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
if (highAccount == owner)
return sfHighSponsor;
}
if (sle->isFlag(lsfLowReserve))
{
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
if (lowAccount == owner)
return sfLowSponsor;
}
// LCOV_EXCL_START
UNREACHABLE("Should not happen. Owner should be checked before calling this function.");
return sfSponsor;
// LCOV_EXCL_STOP
}
default:
return sfSponsor;
}
};
} // namespace xrpl

View File

@@ -102,25 +102,32 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled)
json::Value const maxVersion(
betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion);
if (jv.isObject())
if (!jv.isObject() || !jv.isMember(jss::api_version))
return RPC::kApiVersionIfUnspecified;
try
{
if (jv.isMember(jss::api_version))
auto const& rawVersion = jv[jss::api_version];
switch (rawVersion.type())
{
auto const specifiedVersion = jv[jss::api_version];
if (!specifiedVersion.isInt() && !specifiedVersion.isUInt())
{
return RPC::kApiInvalidVersion;
case json::ValueType::Int:
if (rawVersion.asInt() < 0)
return RPC::kApiInvalidVersion;
[[fallthrough]];
case json::ValueType::UInt: {
auto const apiVersion = rawVersion.asUInt();
if (apiVersion < kMinVersion || apiVersion > maxVersion)
return RPC::kApiInvalidVersion;
return apiVersion;
}
auto const specifiedVersionInt = specifiedVersion.asInt();
if (specifiedVersionInt < kMinVersion || specifiedVersionInt > maxVersion)
{
default:
return RPC::kApiInvalidVersion;
}
return specifiedVersionInt;
}
}
return RPC::kApiVersionIfUnspecified;
catch (...)
{
return RPC::kApiInvalidVersion;
}
}
} // namespace RPC

View File

@@ -153,7 +153,7 @@ signers(AccountID const& account) noexcept;
/** A Sponsorship */
Keylet
sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept;
sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept;
/** A Check */
/** @{ */

View File

@@ -180,12 +180,12 @@ enum LedgerEntryType : std::uint16_t {
LSF_FLAG(lsfMPTCanClawback, 0x00000040)) \
\
LEDGER_OBJECT(MPTokenIssuanceMutable, \
LSF_FLAG(lsmfMPTCanMutateCanLock, 0x00000002) \
LSF_FLAG(lsmfMPTCanMutateRequireAuth, 0x00000004) \
LSF_FLAG(lsmfMPTCanMutateCanEscrow, 0x00000008) \
LSF_FLAG(lsmfMPTCanMutateCanTrade, 0x00000010) \
LSF_FLAG(lsmfMPTCanMutateCanTransfer, 0x00000020) \
LSF_FLAG(lsmfMPTCanMutateCanClawback, 0x00000040) \
LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002) \
LSF_FLAG(lsmfMPTCanEnableRequireAuth, 0x00000004) \
LSF_FLAG(lsmfMPTCanEnableCanEscrow, 0x00000008) \
LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010) \
LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020) \
LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040) \
LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \
LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \
\

View File

@@ -7,8 +7,13 @@
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace xrpl {
class STTx;
/**
* We have both transaction type permissions and granular type permissions.
* Since we will reuse the TransactionFormats to parse the Transaction
@@ -19,15 +24,15 @@ namespace xrpl {
// Macro-generated, complex
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum GranularPermissionType : std::uint32_t {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define PERMISSION(type, txType, value) type = (value),
#define GRANULAR_PERMISSION(name, txType, value, ...) name = (value),
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
};
// Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor
@@ -40,15 +45,30 @@ class Permission
private:
Permission();
std::unordered_map<std::uint16_t, uint256> txFeatureMap_;
struct GranularPermissionEntry
{
std::string name;
TxType txType;
std::uint32_t permittedFlags;
SOTemplate permittedFields;
std::unordered_map<std::uint16_t, Delegation> delegableTx_;
GranularPermissionEntry(
std::string name,
TxType txType,
std::uint32_t permittedFlags,
std::vector<SOElement> fields);
};
std::unordered_map<std::string, GranularPermissionType> granularPermissionMap_;
struct TxDelegationEntry
{
uint256 amendment;
Delegation delegable{NotDelegable};
};
std::unordered_map<GranularPermissionType, std::string> granularNameMap_;
std::unordered_map<GranularPermissionType, TxType> granularTxTypeMap_;
std::unordered_set<TxType> granularTxTypes_;
std::unordered_map<TxType, TxDelegationEntry> txDelegationMap_;
std::unordered_map<std::string, GranularPermissionType> granularPermissionsByName_;
std::unordered_map<GranularPermissionType, GranularPermissionEntry> granularPermissions_;
public:
static Permission const&
@@ -59,30 +79,52 @@ public:
operator=(Permission const&) = delete;
[[nodiscard]] std::optional<std::string>
getPermissionName(std::uint32_t const value) const;
getPermissionName(std::uint32_t value) const;
[[nodiscard]] std::optional<std::uint32_t>
getGranularValue(std::string const& name) const;
[[nodiscard]] std::optional<std::string>
getGranularName(GranularPermissionType const& value) const;
getGranularName(GranularPermissionType value) const;
[[nodiscard]] std::optional<TxType>
getGranularTxType(GranularPermissionType const& gpType) const;
getGranularTxType(GranularPermissionType gpType) const;
// Returns a reference to avoid copying uint256 - 32 bytes. std::optional
// cannot hold references directly, so std::reference_wrapper is used.
[[nodiscard]] std::optional<std::reference_wrapper<uint256 const>>
getTxFeature(TxType txType) const;
[[nodiscard]] bool
isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const;
isDelegable(std::uint32_t permissionValue, Rules const& rules) const;
[[nodiscard]] bool
hasGranularPermissions(TxType txType) const;
// for tx level permission, permission value is equal to tx type plus one
static uint32_t
txToPermissionType(TxType const& type);
[[nodiscard]] static uint32_t
txToPermissionType(TxType type);
// tx type value is permission value minus one
static TxType
permissionToTxType(uint32_t const& value);
[[nodiscard]] static TxType
permissionToTxType(std::uint32_t value);
/**
* @brief Verifies a delegated transaction against its granular permission template.
*
* @note WARNING: Do not move this check before standard transaction-level
* format checks, which is in preclaim. This function assumes the transaction's
* base structural integrity (fees, sequence, signatures) has already been
* validated.
*
* @param tx The transaction to verify.
* @param heldPermissions The granular permissions that the sender hold.
* @return true if the transaction fields and flags comply with the granular template.
*/
[[nodiscard]] bool
checkGranularSandbox(
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldPermissions) const;
};
} // namespace xrpl

View File

@@ -84,7 +84,7 @@ public:
getSeqValue() const;
AccountID
getFeePayer() const;
getInitiator() const;
boost::container::flat_set<AccountID>
getMentionedAccounts() const;

View File

@@ -353,44 +353,35 @@ getAllTxFlags()
inline constexpr FlagValue tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment);
inline constexpr FlagValue tfTrustSetPermissionMask =
~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze);
inline constexpr FlagValue tfSponsorshipSetPermissionMask =
~(tfUniversal | tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve |
tfSponsorshipClearRequireSignForFee | tfSponsorshipClearRequireSignForReserve);
// MPTokenIssuanceCreate MutableFlags:
// Indicating specific fields or flags may be changed after issuance.
inline constexpr FlagValue tmfMPTCanMutateCanLock = lsmfMPTCanMutateCanLock;
inline constexpr FlagValue tmfMPTCanMutateRequireAuth = lsmfMPTCanMutateRequireAuth;
inline constexpr FlagValue tmfMPTCanMutateCanEscrow = lsmfMPTCanMutateCanEscrow;
inline constexpr FlagValue tmfMPTCanMutateCanTrade = lsmfMPTCanMutateCanTrade;
inline constexpr FlagValue tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTransfer;
inline constexpr FlagValue tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback;
inline constexpr FlagValue tmfMPTCanEnableCanLock = lsmfMPTCanEnableCanLock;
inline constexpr FlagValue tmfMPTCanEnableRequireAuth = lsmfMPTCanEnableRequireAuth;
inline constexpr FlagValue tmfMPTCanEnableCanEscrow = lsmfMPTCanEnableCanEscrow;
inline constexpr FlagValue tmfMPTCanEnableCanTrade = lsmfMPTCanEnableCanTrade;
inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTransfer;
inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback;
inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask =
~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow |
tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback |
~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow |
tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback |
tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee);
// MPTokenIssuanceSet MutableFlags:
// Set or Clear flags.
// Enable mutable capability flags. These flags are one-way: once enabled,
// the corresponding capability cannot be disabled by MPTokenIssuanceSet.
inline constexpr FlagValue tmfMPTSetCanLock = 0x00000001;
inline constexpr FlagValue tmfMPTClearCanLock = 0x00000002;
inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000004;
inline constexpr FlagValue tmfMPTClearRequireAuth = 0x00000008;
inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000010;
inline constexpr FlagValue tmfMPTClearCanEscrow = 0x00000020;
inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000040;
inline constexpr FlagValue tmfMPTClearCanTrade = 0x00000080;
inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000100;
inline constexpr FlagValue tmfMPTClearCanTransfer = 0x00000200;
inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000400;
inline constexpr FlagValue tmfMPTClearCanClawback = 0x00000800;
inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = ~(
tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth |
tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade |
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback | tmfMPTClearCanClawback);
inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000002;
inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004;
inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008;
inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010;
inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020;
inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask =
~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade |
tmfMPTSetCanTransfer | tmfMPTSetCanClawback);
// Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a
// TrustLine to be added to the issuer of that token without explicit permission from that issuer.
@@ -463,34 +454,11 @@ getAsfFlagMap()
#pragma pop_macro("ACCOUNTSET_FLAG_TO_MAP")
#pragma pop_macro("ACCOUNTSET_FLAGS")
#pragma push_macro("SPONSOR_FLAGS")
#pragma push_macro("SPONSOR_FLAG_TO_VALUE")
#pragma push_macro("SPONSOR_FLAG_TO_MAP")
// Sponsor flags (spf)
// Sponsor Flag values
#define SPONSOR_FLAGS(SPF_FLAG) \
SPF_FLAG(spfSponsorFee, 1) \
SPF_FLAG(spfSponsorReserve, 2)
#define SPONSOR_FLAG_TO_VALUE(name, value) inline constexpr FlagValue name = value;
#define SPONSOR_FLAG_TO_MAP(name, value) {#name, value},
SPONSOR_FLAGS(SPONSOR_FLAG_TO_VALUE)
inline std::map<std::string, FlagValue> const&
getspfFlagMap()
{
static std::map<std::string, FlagValue> const flags = {SPONSOR_FLAGS(SPONSOR_FLAG_TO_MAP)};
return flags;
}
#undef SPONSOR_FLAG_TO_VALUE
#undef SPONSOR_FLAG_TO_MAP
#undef SPONSOR_FLAGS
#pragma pop_macro("SPONSOR_FLAG_TO_VALUE")
#pragma pop_macro("SPONSOR_FLAG_TO_MAP")
#pragma pop_macro("SPONSOR_FLAGS")
inline constexpr FlagValue spfSponsorFee = 1;
inline constexpr FlagValue spfSponsorReserve = 2;
inline constexpr FlagValue spfSponsorFlagMask = ~(spfSponsorFee | spfSponsorReserve);
} // namespace xrpl

View File

@@ -22,10 +22,10 @@ XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultN
XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes)
XRPL_FIX (BatchInnerSigs, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (IncludeKeyletFields, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicMPT, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicMPT, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (TokenEscrowV1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (MPTDeliveredAmount, Supported::Yes, VoteBehavior::DefaultNo)

View File

@@ -127,32 +127,32 @@ LEDGER_ENTRY(ltTICKET, 0x0054, Ticket, ticket, ({
\sa keylet::account
*/
LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfAccount, SoeRequired},
{sfSequence, SoeRequired},
{sfBalance, SoeRequired},
{sfOwnerCount, SoeRequired},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfAccountTxnID, SoeOptional},
{sfRegularKey, SoeOptional},
{sfEmailHash, SoeOptional},
{sfWalletLocator, SoeOptional},
{sfWalletSize, SoeOptional},
{sfMessageKey, SoeOptional},
{sfTransferRate, SoeOptional},
{sfDomain, SoeOptional},
{sfTickSize, SoeOptional},
{sfTicketCount, SoeOptional},
{sfNFTokenMinter, SoeOptional},
{sfMintedNFTokens, SoeDefault},
{sfBurnedNFTokens, SoeDefault},
{sfFirstNFTokenSequence, SoeOptional},
{sfAMMID, SoeOptional}, // pseudo-account designator
{sfVaultID, SoeOptional}, // pseudo-account designator
{sfLoanBrokerID, SoeOptional}, // pseudo-account designator
{sfSponsoredOwnerCount, SoeDefault},
{sfSponsoringOwnerCount, SoeDefault},
{sfSponsoringAccountCount,SoeDefault},
{sfAccount, SoeRequired},
{sfSequence, SoeRequired},
{sfBalance, SoeRequired},
{sfOwnerCount, SoeRequired},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfAccountTxnID, SoeOptional},
{sfRegularKey, SoeOptional},
{sfEmailHash, SoeOptional},
{sfWalletLocator, SoeOptional},
{sfWalletSize, SoeOptional},
{sfMessageKey, SoeOptional},
{sfTransferRate, SoeOptional},
{sfDomain, SoeOptional},
{sfTickSize, SoeOptional},
{sfTicketCount, SoeOptional},
{sfNFTokenMinter, SoeOptional},
{sfMintedNFTokens, SoeDefault},
{sfBurnedNFTokens, SoeDefault},
{sfFirstNFTokenSequence, SoeOptional},
{sfSponsoredOwnerCount, SoeDefault},
{sfSponsoringOwnerCount, SoeDefault},
{sfSponsoringAccountCount, SoeDefault},
{sfAMMID, SoeOptional}, // pseudo-account designator
{sfVaultID, SoeOptional}, // pseudo-account designator
{sfLoanBrokerID, SoeOptional}, // pseudo-account designator
}))
/** A ledger object which contains a list of object identifiers.
@@ -613,7 +613,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
}))
/** A ledger object representing a sponsorship.
\sa keylet::sponsor
\sa keylet::sponsorship
*/
LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({
{sfPreviousTxnID, SoeRequired},
@@ -622,7 +622,7 @@ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({
{sfSponsee, SoeRequired},
{sfFeeAmount, SoeOptional},
{sfMaxFee, SoeOptional},
{sfReserveCount, SoeDefault},
{sfRemainingOwnerCount, SoeDefault},
{sfOwnerNode, SoeRequired},
{sfSponseeNode, SoeRequired},
}))

View File

@@ -1,55 +1,87 @@
#if !defined(PERMISSION)
#error "undefined macro: PERMISSION"
#if !defined(GRANULAR_PERMISSION)
#error "undefined macro: GRANULAR_PERMISSION"
#endif
/**
* PERMISSION(name, type, txType, value)
* GRANULAR_PERMISSION(name, txType, value, allowedFlags, allowedFields)
*
* This macro defines a permission:
* name: the name of the permission.
* type: the GranularPermissionType enum.
* txType: the corresponding TxType for this permission.
* value: the uint32 numeric value for the enum type.
* Defines a granular permission:
* name: the granular permission name.
* txType: the corresponding TxType for this permission.
* value: the uint32 numeric value for the enum type.
* allowedFlags: transaction flags permitted under this permission.
* allowedFields: transaction fields permitted under this permission.
*/
/** This permission grants the delegated account the ability to authorize a trustline. */
PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537)
/** Grants the ability to authorize a trustline. */
GRANULAR_PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537, tfUniversal | tfSetfAuth,
({{sfLimitAmount, SoeRequired}}))
/** This permission grants the delegated account the ability to freeze a trustline. */
PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538)
/** Grants the ability to freeze a trustline. */
GRANULAR_PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538, tfUniversal | tfSetFreeze,
({{sfLimitAmount, SoeRequired}}))
/** This permission grants the delegated account the ability to unfreeze a trustline. */
PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539)
/** Grants the ability to unfreeze a trustline. */
GRANULAR_PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539, tfUniversal | tfClearFreeze,
({{sfLimitAmount, SoeRequired}}))
/** This permission grants the delegated account the ability to set Domain. */
PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540)
/** Grants the ability to set Domain. */
GRANULAR_PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540, tfUniversal,
({{sfDomain, SoeOptional}}))
/** This permission grants the delegated account the ability to set EmailHashSet. */
PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541)
/** Grants the ability to set EmailHash. */
GRANULAR_PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541, tfUniversal,
({{sfEmailHash, SoeOptional}}))
/** This permission grants the delegated account the ability to set MessageKey. */
PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542)
/** Grants the ability to set MessageKey. */
GRANULAR_PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542, tfUniversal,
({{sfMessageKey, SoeOptional}}))
/** This permission grants the delegated account the ability to set TransferRate. */
PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543)
/** Grants the ability to set TransferRate. */
GRANULAR_PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543, tfUniversal,
({{sfTransferRate, SoeOptional}}))
/** This permission grants the delegated account the ability to set TickSize. */
PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544)
/** Grants the ability to set TickSize. */
GRANULAR_PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544, tfUniversal,
({{sfTickSize, SoeOptional}}))
/** This permission grants the delegated account the ability to mint payment, which means sending a payment for a currency where the sending account is the issuer. */
PERMISSION(PaymentMint, ttPAYMENT, 65545)
/** Grants the ability to mint payment (sending account is the issuer). Cross-currency payments are disallowed. */
GRANULAR_PERMISSION(PaymentMint, ttPAYMENT, 65545, tfUniversal,
({{sfDestination, SoeRequired},
{sfAmount, SoeRequired},
{sfSendMax, SoeOptional},
{sfInvoiceID, SoeOptional},
{sfDestinationTag, SoeOptional},
{sfCredentialIDs, SoeOptional}}))
/** This permission grants the delegated account the ability to burn payment, which means sending a payment for a currency where the destination account is the issuer */
PERMISSION(PaymentBurn, ttPAYMENT, 65546)
/** Grants the ability to burn payment (destination account is the issuer). Cross-currency payments are disallowed. */
GRANULAR_PERMISSION(PaymentBurn, ttPAYMENT, 65546, tfUniversal,
({{sfDestination, SoeRequired},
{sfAmount, SoeRequired},
{sfSendMax, SoeOptional},
{sfInvoiceID, SoeOptional},
{sfDestinationTag, SoeOptional},
{sfCredentialIDs, SoeOptional}}))
/** This permission grants the delegated account the ability to lock MPToken. */
PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547)
/** Grants the ability to lock an MPToken. */
GRANULAR_PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547, tfUniversal | tfMPTLock,
({{sfMPTokenIssuanceID, SoeRequired},
{sfHolder, SoeOptional}}))
/** This permission grants the delegated account the ability to unlock MPToken. */
PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548)
/** Grants the ability to unlock an MPToken. */
GRANULAR_PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548, tfUniversal | tfMPTUnlock,
({{sfMPTokenIssuanceID, SoeRequired},
{sfHolder, SoeOptional}}))
/** This permission grants the delegated account the ability to set SponsorFee. */
PERMISSION(SponsorFee, ttSPONSORSHIP_SET, 65549)
GRANULAR_PERMISSION(SponsorFee, ttSPONSORSHIP_SET, 65549,
tfUniversal | tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee,
({{sfSponsee, SoeOptional},
{sfFeeAmount, SoeOptional},
{sfMaxFee, SoeOptional}}))
/** This permission grants the delegated account the ability to set SponsorReserve. */
PERMISSION(SponsorReserve, ttSPONSORSHIP_SET, 65550)
GRANULAR_PERMISSION(SponsorReserve, ttSPONSORSHIP_SET, 65550,
tfUniversal | tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForReserve,
({{sfSponsee, SoeOptional},
{sfRemainingOwnerCount, SoeOptional}}))

View File

@@ -116,7 +116,7 @@ TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bi
TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 69)
TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 70)
TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 71)
TYPED_SFIELD(sfReserveCount, UINT32, 72)
TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 72)
TYPED_SFIELD(sfSponsorFlags, UINT32, 73)
// 64-bit integers (common)

View File

@@ -1084,7 +1084,7 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay,
TRANSACTION(ttSPONSORSHIP_TRANSFER, 85, SponsorshipTransfer,
Delegation::Delegable,
featureSponsor,
NoPriv,
MayModifyVault,
({
{sfObjectID, SoeOptional},
{sfSponsee, SoeOptional},
@@ -1103,7 +1103,7 @@ TRANSACTION(ttSPONSORSHIP_SET, 86, SponsorshipSet,
{sfSponsee, SoeOptional},
{sfFeeAmount, SoeOptional},
{sfMaxFee, SoeOptional},
{sfReserveCount, SoeOptional},
{sfRemainingOwnerCount, SoeOptional},
}))
/** This system-generated transaction type is used to update the status of the various amendments.

View File

@@ -447,78 +447,6 @@ public:
return this->sle_->isFieldPresent(sfFirstNFTokenSequence);
}
/**
* @brief Get sfAMMID (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT256::type::value_type>
getAMMID() const
{
if (hasAMMID())
return this->sle_->at(sfAMMID);
return std::nullopt;
}
/**
* @brief Check if sfAMMID is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAMMID() const
{
return this->sle_->isFieldPresent(sfAMMID);
}
/**
* @brief Get sfVaultID (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT256::type::value_type>
getVaultID() const
{
if (hasVaultID())
return this->sle_->at(sfVaultID);
return std::nullopt;
}
/**
* @brief Check if sfVaultID is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasVaultID() const
{
return this->sle_->isFieldPresent(sfVaultID);
}
/**
* @brief Get sfLoanBrokerID (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT256::type::value_type>
getLoanBrokerID() const
{
if (hasLoanBrokerID())
return this->sle_->at(sfLoanBrokerID);
return std::nullopt;
}
/**
* @brief Check if sfLoanBrokerID is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasLoanBrokerID() const
{
return this->sle_->isFieldPresent(sfLoanBrokerID);
}
/**
* @brief Get sfSponsoredOwnerCount (SoeDefault)
* @return The field value, or std::nullopt if not present.
@@ -590,6 +518,78 @@ public:
{
return this->sle_->isFieldPresent(sfSponsoringAccountCount);
}
/**
* @brief Get sfAMMID (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT256::type::value_type>
getAMMID() const
{
if (hasAMMID())
return this->sle_->at(sfAMMID);
return std::nullopt;
}
/**
* @brief Check if sfAMMID is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAMMID() const
{
return this->sle_->isFieldPresent(sfAMMID);
}
/**
* @brief Get sfVaultID (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT256::type::value_type>
getVaultID() const
{
if (hasVaultID())
return this->sle_->at(sfVaultID);
return std::nullopt;
}
/**
* @brief Check if sfVaultID is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasVaultID() const
{
return this->sle_->isFieldPresent(sfVaultID);
}
/**
* @brief Get sfLoanBrokerID (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT256::type::value_type>
getLoanBrokerID() const
{
if (hasLoanBrokerID())
return this->sle_->at(sfLoanBrokerID);
return std::nullopt;
}
/**
* @brief Check if sfLoanBrokerID is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasLoanBrokerID() const
{
return this->sle_->isFieldPresent(sfLoanBrokerID);
}
};
/**
@@ -858,39 +858,6 @@ public:
return *this;
}
/**
* @brief Set sfAMMID (SoeOptional)
* @return Reference to this builder for method chaining.
*/
AccountRootBuilder&
setAMMID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfAMMID] = value;
return *this;
}
/**
* @brief Set sfVaultID (SoeOptional)
* @return Reference to this builder for method chaining.
*/
AccountRootBuilder&
setVaultID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfVaultID] = value;
return *this;
}
/**
* @brief Set sfLoanBrokerID (SoeOptional)
* @return Reference to this builder for method chaining.
*/
AccountRootBuilder&
setLoanBrokerID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfLoanBrokerID] = value;
return *this;
}
/**
* @brief Set sfSponsoredOwnerCount (SoeDefault)
* @return Reference to this builder for method chaining.
@@ -924,6 +891,39 @@ public:
return *this;
}
/**
* @brief Set sfAMMID (SoeOptional)
* @return Reference to this builder for method chaining.
*/
AccountRootBuilder&
setAMMID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfAMMID] = value;
return *this;
}
/**
* @brief Set sfVaultID (SoeOptional)
* @return Reference to this builder for method chaining.
*/
AccountRootBuilder&
setVaultID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfVaultID] = value;
return *this;
}
/**
* @brief Set sfLoanBrokerID (SoeOptional)
* @return Reference to this builder for method chaining.
*/
AccountRootBuilder&
setLoanBrokerID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfLoanBrokerID] = value;
return *this;
}
/**
* @brief Build and return the completed AccountRoot wrapper.
* @param index The ledger entry index.

View File

@@ -138,27 +138,27 @@ public:
}
/**
* @brief Get sfReserveCount (SoeDefault)
* @brief Get sfRemainingOwnerCount (SoeDefault)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getReserveCount() const
getRemainingOwnerCount() const
{
if (hasReserveCount())
return this->sle_->at(sfReserveCount);
if (hasRemainingOwnerCount())
return this->sle_->at(sfRemainingOwnerCount);
return std::nullopt;
}
/**
* @brief Check if sfReserveCount is present.
* @brief Check if sfRemainingOwnerCount is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasReserveCount() const
hasRemainingOwnerCount() const
{
return this->sle_->isFieldPresent(sfReserveCount);
return this->sle_->isFieldPresent(sfRemainingOwnerCount);
}
/**
@@ -297,13 +297,13 @@ public:
}
/**
* @brief Set sfReserveCount (SoeDefault)
* @brief Set sfRemainingOwnerCount (SoeDefault)
* @return Reference to this builder for method chaining.
*/
SponsorshipBuilder&
setReserveCount(std::decay_t<typename SF_UINT32::type::value_type> const& value)
setRemainingOwnerCount(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfReserveCount] = value;
object_[sfRemainingOwnerCount] = value;
return *this;
}

View File

@@ -152,29 +152,29 @@ public:
}
/**
* @brief Get sfReserveCount (SoeOptional)
* @brief Get sfRemainingOwnerCount (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getReserveCount() const
getRemainingOwnerCount() const
{
if (hasReserveCount())
if (hasRemainingOwnerCount())
{
return this->tx_->at(sfReserveCount);
return this->tx_->at(sfRemainingOwnerCount);
}
return std::nullopt;
}
/**
* @brief Check if sfReserveCount is present.
* @brief Check if sfRemainingOwnerCount is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasReserveCount() const
hasRemainingOwnerCount() const
{
return this->tx_->isFieldPresent(sfReserveCount);
return this->tx_->isFieldPresent(sfRemainingOwnerCount);
}
};
@@ -263,13 +263,13 @@ public:
}
/**
* @brief Set sfReserveCount (SoeOptional)
* @brief Set sfRemainingOwnerCount (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SponsorshipSetBuilder&
setReserveCount(std::decay_t<typename SF_UINT32::type::value_type> const& value)
setRemainingOwnerCount(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfReserveCount] = value;
object_[sfRemainingOwnerCount] = value;
return *this;
}

View File

@@ -21,7 +21,7 @@ class SponsorshipTransferBuilder;
* Type: ttSPONSORSHIP_TRANSFER (85)
* Delegable: Delegation::Delegable
* Amendment: featureSponsor
* Privileges: NoPriv
* Privileges: MayModifyVault
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SponsorshipTransferBuilder to construct new transactions.

View File

@@ -1,6 +1,7 @@
#pragma once
#include <xrpl/basics/CountedObject.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/ErrorCodes.h>
@@ -26,6 +27,19 @@ public:
};
/** Manages a client's subscription to data feeds.
*
* An InfoSub holds a non-owning reference to its `Source` (typically the
* process-wide `NetworkOPsImp`). The destructor reaches back into the
* `Source` to remove this subscriber from every server-side subscription
* map.
*
* @note Lifetime contract: every `InfoSub` instance MUST be destroyed
* before the backing `Source`. NetworkOPsImp shutdown drops all
* subscriber strong refs before its own teardown to satisfy this.
* @note Thread-safety: per-instance state is guarded by `lock_`. The
* destructor reads tracking sets without taking `lock_` because
* the strong-pointer ref-count is zero at destruction time, so
* no other thread can be calling the public mutators.
*/
class InfoSub : public CountedObject<InfoSub>
{
@@ -117,8 +131,43 @@ public:
virtual bool
subBook(ref ispListener, Book const&) = 0;
/**
* Remove a book subscription for a live subscriber.
*
* Clears the book from the subscriber's own tracking set
* (InfoSub::bookSubscriptions_) and then removes the server-side
* entry from subBook_. Call this from RPC unsubscribe handlers.
*
* @param ispListener The subscriber requesting removal.
* @param book The order book to unsubscribe from.
* @return true if the entry was present and removed, false if the
* subscriber was not subscribed to @p book.
*
* @note Thread-safety: acquires subLock_ internally.
* @note Do NOT call from ~InfoSub(). Use unsubBookInternal instead
* to avoid a redundant write-back to bookSubscriptions_ on a
* partially-destroyed object.
*/
virtual bool
unsubBook(std::uint64_t uListener, Book const&) = 0;
unsubBook(ref ispListener, Book const&) = 0;
/**
* Remove a book subscription during InfoSub teardown.
*
* Removes only the server-side entry from subBook_. Does NOT touch
* InfoSub::bookSubscriptions_ because the InfoSub is being destroyed.
* Called by ~InfoSub() for each book in bookSubscriptions_.
*
* @param uListener The sequence number of the subscriber being torn down.
* @param book The order book entry to remove.
* @return true if the entry was present and removed, false otherwise
* (e.g., already removed by a concurrent RPC unsubscribe).
*
* @note Thread-safety: acquires subLock_ internally.
*/
virtual bool
unsubBookInternal(std::uint64_t uListener, Book const&) = 0;
virtual bool
subTransactions(ref ispListener) = 0;
@@ -158,6 +207,13 @@ public:
addRpcSub(std::string const& strUrl, ref rspEntry) = 0;
virtual bool
tryRemoveRpcSub(std::string const& strUrl) = 0;
/** Journal used by InfoSub for diagnostics that occur after the
* owning subsystem (e.g. application-level Logs) is the only
* surviving sink — primarily destructor-time cleanup failures.
*/
[[nodiscard]] virtual beast::Journal const&
journal() const = 0;
};
public:
@@ -184,6 +240,31 @@ public:
void
deleteSubAccountInfo(AccountID const& account, bool rt);
/** Record that this subscriber is following @p book.
*
* Called by NetworkOPsImp::subBook so that ~InfoSub() can issue a
* matching unsubBook for every book this subscriber is tracking,
* keeping per-subscriber state symmetric with the server-side map.
*
* @param book The order book this subscriber has just subscribed to.
* @note Idempotent: re-inserting an already-tracked book is a no-op.
* @note Thread-safe: takes InfoSub::lock_.
*/
void
insertBookSubscription(Book const& book);
/** Stop tracking @p book for this subscriber.
*
* Called by the unsubscribe RPC handler so that the book is not
* re-unsubscribed by ~InfoSub(). Pairs with insertBookSubscription.
*
* @param book The order book to forget.
* @note No-op if @p book was not previously inserted.
* @note Thread-safe: takes InfoSub::lock_.
*/
void
deleteBookSubscription(Book const& book);
// return false if already subscribed to this account
bool
insertSubAccountHistory(AccountID const& account);
@@ -217,6 +298,7 @@ private:
std::shared_ptr<InfoSubRequest> request_;
std::uint64_t seq_;
hash_set<AccountID> accountHistorySubscriptions_;
hash_set<Book> bookSubscriptions_;
unsigned int apiVersion_ = 0;
static int

View File

@@ -249,6 +249,19 @@ public:
virtual void
stateAccounting(json::Value& obj) = 0;
/** Total number of (book, subscriber) entries currently tracked.
*
* Counts every weak_ptr stored across every book in subBook_, NOT the
* number of distinct subscribers and NOT the number of distinct
* books: a single subscriber following N books contributes N entries.
*
* @note Diagnostic accessor; intended for tests and operator visibility
* into per-book subscription state. The returned value is a
* snapshot under the subscription lock.
*/
virtual std::size_t
getBookSubscribersCount() = 0;
};
} // namespace xrpl

View File

@@ -8,6 +8,7 @@
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/applySteps.h>
#include <tuple>
#include <utility>
namespace xrpl {
@@ -237,8 +238,63 @@ public:
return tesSUCCESS;
}
/**
* This function can be overridden to introduce additional semantic constraints beyond the
* granular template validation for granular permissions. It is called by the base
* invokeCheckPermission method only after the transaction has successfully passed
* checkGranularSandbox.
*/
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
{
return tesSUCCESS;
}
/**
* Checks whether the transaction is authorized to be executed by the delegated account.
* This function enforces the strict permission check hierarchy. It is explicitly
* designed NOT to be overridden. Derived transactors must instead implement
* checkGranularSemantics to add custom validation logic for granular permissions.
*
* The evaluation proceeds as follows:
* - If transaction-level permission is granted, the function immediately returns tesSUCCESS.
* - If transaction-level permission is not granted, the function checks whether the transaction
* matches the granular permission template defined in permissions.macro. If it does, it then
* calls checkGranularSemantics to perform any additional, fine-grained validation.
*
*/
template <class T>
static NotTEC
invokeCheckPermission(ReadView const& view, STTx const& tx)
{
// heldGranularPermissions is passed by reference into checkPermission.
// It is populated with the senders granular permissions only when the sender
// lacks tx-level permission but has granular permissions that satisfy the
// granular permission template.
//
// - result is terNO_DELEGATE_PERMISSION: return immediately.
// - result is tesSUCCESS and heldGranularPermissions is empty: tx-level permission was
// granted, so we returned success before populating it.
// - result is tesSUCCESS and heldGranularPermissions is not empty: tx-level permission was
// not granted, but the held granular permissions passed checkGranularSandbox, so we proceed
// to checkGranularSemantics.
//
// WARNING: Do not simplify checkPermission to return only
// heldGranularPermissions or the ter code. Both the result and the
// populated set are required to enforce the strict permission hierarchy
// described above.
std::unordered_set<GranularPermissionType> heldGranularPermissions;
if (NotTEC const result = checkPermission(view, tx, heldGranularPermissions);
!isTesSuccess(result) || heldGranularPermissions.empty())
{
return result;
}
return T::checkGranularSemantics(view, tx, heldGranularPermissions);
}
static NotTEC
checkSponsor(ReadView const& view, STTx const& tx);
@@ -371,6 +427,12 @@ protected:
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
private:
static NotTEC
checkPermission(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions);
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
@@ -379,8 +441,13 @@ private:
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
std::tuple<TER, XRPAmount, bool>
processPersistentChanges(TER result, XRPAmount fee);
static NotTEC
checkSingleSign(
ReadView const& view,
@@ -388,6 +455,7 @@ private:
AccountID const& idAccount,
SLE::const_pointer sleAccount,
beast::Journal const j);
static NotTEC
checkMultiSign(
ReadView const& view,

View File

@@ -15,7 +15,9 @@ class ValidAMM
std::optional<AccountID> ammAccount_;
std::optional<STAmount> lptAMMBalanceAfter_;
std::optional<STAmount> lptAMMBalanceBefore_;
std::optional<STAmount> lptAMMBalanceBeforeDeletion_;
bool ammPoolChanged_{false};
bool ammDeleted_{false};
public:
enum class ZeroAllowed : bool { No = false, Yes = true };
@@ -35,12 +37,17 @@ private:
[[nodiscard]] bool
finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
[[nodiscard]] bool
finalizeDelete(bool enforce, TER res, beast::Journal const&) const;
finalizeDelete(bool enforce, bool enforceAMMDelete, TER res, beast::Journal const&) const;
[[nodiscard]] bool
finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
// Includes clawback
[[nodiscard]] bool
finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
finalizeWithdraw(
STTx const&,
ReadView const&,
bool enforce,
bool enforceAMMDelete,
beast::Journal const&) const;
[[nodiscard]] bool
finalizeDEX(bool enforce, beast::Journal const&) const;
[[nodiscard]] bool

View File

@@ -22,7 +22,7 @@ class SponsorshipOwnerCountsMatch
std::int64_t deltaSponsoredOwnerCount_ = 0;
std::int64_t deltaSponsoringOwnerCount_ = 0;
std::int64_t deltaSponsoredObjectOwnerCount_ = 0;
std::uint64_t invalidOwnerCountLessThanSponsoredOwnerCount_ = 0;
std::uint64_t ownerCountBelowSponsored_ = 0;
public:
void

View File

@@ -23,9 +23,6 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -100,7 +100,6 @@ public:
static std::tuple<TER, STAmount, STAmount, std::optional<STAmount>>
equalWithdrawTokens(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const account,
AccountID const& ammAccount,
@@ -135,7 +134,6 @@ public:
static std::tuple<TER, STAmount, STAmount, std::optional<STAmount>>
withdraw(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const& ammAccount,
AccountID const& account,
@@ -179,7 +177,6 @@ private:
std::pair<TER, STAmount>
withdraw(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
@@ -205,7 +202,6 @@ private:
std::pair<TER, STAmount>
equalWithdrawTokens(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
@@ -231,7 +227,6 @@ private:
std::pair<TER, STAmount>
equalWithdrawLimit(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
@@ -254,7 +249,6 @@ private:
std::pair<TER, STAmount>
singleWithdraw(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
@@ -276,7 +270,6 @@ private:
std::pair<TER, STAmount>
singleWithdrawTokens(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
@@ -299,7 +292,6 @@ private:
std::pair<TER, STAmount>
singleWithdrawEPrice(
Sandbox& view,
STTx const& tx,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,

View File

@@ -22,12 +22,6 @@ public:
{
}
static uint32_t
calculateOracleReserve(std::size_t count)
{
return count > 5 ? 2 : 1;
}
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -32,7 +32,10 @@ public:
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -19,9 +19,6 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -22,9 +22,6 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -21,7 +21,10 @@ public:
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions);
static TER
preclaim(PreclaimContext const& ctx);

View File

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

90
nix/docker/README.md Normal file
View File

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

View File

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

View File

@@ -19,8 +19,10 @@ in
gh
git
git-cliff
git-lfs
gnumake
gnupg # needed for signing commits & codecov/codecov-action
graphviz
llvmPackages_22.clang-tools
less # needed for git diff
mold
@@ -33,5 +35,6 @@ in
python3
runClangTidy
vim
zip
];
}

View File

@@ -15,7 +15,6 @@ package/
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
xrpld.tmpfiles tmpfiles.d config (used by both RPM and DEB)
xrpld.logrotate logrotate config (installed to /etc/logrotate.d/xrpld)
update-xrpld auto-update script (installed to /usr/libexec/xrpld/, run by update-xrpld.timer)
```
## Prerequisites

View File

@@ -114,10 +114,11 @@ VER_BASE="${VERSION%%-*}"
VER_SUFFIX="${VERSION#*-}"
[[ "${VER_SUFFIX}" == "${VERSION}" ]] && VER_SUFFIX=""
# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). The RPM
# Release field forbids '-', and the convention here is single-token suffixes
# like b1 or rc2. Fail early with a clear message rather than letting either
# rpmbuild blow up or silently mangling dashes into dots.
# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). Neither an
# RPM Version nor a Debian upstream version may contain '-' (it's the NVR /
# version-revision separator), and the convention here is single-token
# suffixes like b1 or rc2. Fail early with a clear message rather than letting
# the package tooling blow up or silently mangle dashes.
if [[ "${VER_SUFFIX}" == *-* ]]; then
echo "build_pkg.sh: multi-segment pre-release in VERSION='${VERSION}' (suffix '${VER_SUFFIX}')." >&2
echo "Use single-token suffixes like 3.2.0-b1 or 3.2.0-rc2." >&2
@@ -142,9 +143,6 @@ stage_common() {
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
cp "${SHARED}/update-xrpld" "${dest}/update-xrpld"
cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service"
cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer"
cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset"
}
@@ -156,20 +154,18 @@ build_rpm() {
cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec"
stage_common "${topdir}/SOURCES"
# RPM Version can't contain '-'. A pre-release goes in Release with a
# leading "0." so 3.2.0-b1 sorts before the final 3.2.0-<pkg_release>.
# The order is "0.<pkg_release>.<suffix>" (e.g. 0.1.b6) — the Fedora/EPEL
# convention. Reversing to "0.<suffix>.<pkg_release>" (e.g. 0.b6.1) breaks
# rpmvercmp against the former because numeric segments outrank alphabetic
# ones, so "0.1.b5" would sort newer than "0.b6.1".
local rpm_release="${PKG_RELEASE}"
[[ -n "${VER_SUFFIX}" ]] && rpm_release="0.${PKG_RELEASE}.${VER_SUFFIX}"
# Pre-releases use the modern rpm '~' convention (rpm >= 4.10): the suffix
# goes in Version (e.g. 3.2.0~b1), which rpmvercmp sorts *before* the final
# 3.2.0 — identical semantics to Debian's '~'. Release is just the package
# release number. This replaces the older "0.<release>.<suffix>" Release
# hack and keeps the RPM and DEB version strings symmetric.
local rpm_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}"
set -x
rpmbuild -bb \
--define "_topdir ${topdir}" \
--define "xrpld_version ${VER_BASE}" \
--define "xrpld_release ${rpm_release}" \
--define "xrpld_version ${rpm_version}" \
--define "xrpld_release ${PKG_RELEASE}" \
"${topdir}/SPECS/xrpld.spec"
}
@@ -181,13 +177,10 @@ build_deb() {
stage_common "${staging}"
cp -r "${DEBIAN_DIR}" "${staging}/debian"
# Debhelper auto-discovers these only from debian/.
cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service"
cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers"
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate"
cp "${staging}/update-xrpld.service" "${staging}/debian/xrpld.update-xrpld.service"
cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer"
# Debian '~' marks a pre-release; 3.2.0~b1 sorts before 3.2.0.
local deb_full_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}-${PKG_RELEASE}"

View File

@@ -10,7 +10,6 @@ override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
override_dh_installsystemd:
dh_installsystemd --no-stop-on-upgrade xrpld.service
dh_installsystemd --name=update-xrpld --no-enable --no-start update-xrpld.service update-xrpld.timer
execute_before_dh_installtmpfiles:
dh_installsysusers
@@ -21,7 +20,6 @@ override_dh_install:
install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld
install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg
install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt
install -D -m 0755 update-xrpld debian/xrpld/usr/libexec/xrpld/update-xrpld
override_dh_dwz:
@:

View File

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

View File

@@ -35,8 +35,6 @@ install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{
# systemd units, sysusers, tmpfiles, preset
install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
install -Dm0644 %{_sourcedir}/update-xrpld.service %{buildroot}%{_unitdir}/update-xrpld.service
install -Dm0644 %{_sourcedir}/update-xrpld.timer %{buildroot}%{_unitdir}/update-xrpld.timer
install -Dm0644 %{_sourcedir}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
install -Dm0644 %{_sourcedir}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50-xrpld.preset
@@ -44,9 +42,6 @@ install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50-
# Logrotate config
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name}
# Update helper
install -Dm0755 %{_sourcedir}/update-xrpld %{buildroot}%{_libexecdir}/%{name}/update-xrpld
# Docs
install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md
install -Dm0644 %{_sourcedir}/README.md %{buildroot}%{_docdir}/%{name}/README.md
@@ -61,10 +56,10 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
%post
systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%systemd_post xrpld.service update-xrpld.timer
%systemd_post xrpld.service
%preun
%systemd_preun xrpld.service update-xrpld.timer
%systemd_preun xrpld.service
%postun
%systemd_postun_with_restart xrpld.service
@@ -74,7 +69,6 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%doc %{_docdir}/%{name}/README.md
%dir %{_sysconfdir}/%{name}
%dir %{_libexecdir}/%{name}
%{_bindir}/%{name}
@@ -82,18 +76,13 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt
%config(noreplace) %{_sysconfdir}/logrotate.d/%{name}
%{_libexecdir}/%{name}/update-xrpld
%{_unitdir}/xrpld.service
%{_unitdir}/update-xrpld.service
%{_unitdir}/update-xrpld.timer
%{_presetdir}/50-xrpld.preset
%{_sysusersdir}/xrpld.conf
%{_tmpfilesdir}/xrpld.conf
%ghost %dir /var/lib/%{name}
%ghost %dir /var/log/%{name}
%ghost %dir /var/lib/xrpld
%ghost %dir /var/log/xrpld
# Legacy compatibility for pre-FHS package layouts.
# TODO: remove after rippled fully deprecated.

View File

@@ -1,4 +1,2 @@
# /usr/lib/systemd/system-preset/50-xrpld.preset
enable xrpld.service
# Don't enable automatic updates
disable update-xrpld.timer

View File

@@ -1,152 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Optional: also write logs to a legacy file in addition to journald.
# By default, this script logs to systemd/journald, viewable via:
# journalctl -t update-xrpld
#
# Uncomment the line below if you need a flat file for compatibility with
# external tooling, manual inspection, or environments where journald logs
# are not persisted or easily accessible.
#
# Note: This duplicates all output (stdout/stderr) to both journald and the file.
# It is generally not needed on modern systems and may cause log file growth
# if left enabled long-term.
#
# Requires /var/log/xrpld/ to exist and be writable by the service (root).
#
# exec > >(tee -a /var/log/xrpld/update.log) 2>&1
PATH=/usr/sbin:/usr/bin:/sbin:/bin
PKG_NAME=${PKG_NAME:-xrpld}
log() {
# If running under systemd/journald, let it handle timestamps.
if [[ -n "${JOURNAL_STREAM:-}" ]]; then
printf '%s\n' "$*"
else
printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"
fi
}
require_root() {
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
log "RESULT: failed reason=not-root"
exit 1
fi
}
get_installed_version() {
if command -v dpkg-query >/dev/null 2>&1; then
dpkg-query -W -f='${Version}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
elif command -v rpm >/dev/null 2>&1; then
rpm -q --qf '%{VERSION}-%{RELEASE}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
else
printf 'unknown'
fi
}
trap 'log "RESULT: failed reason=script-error exit_code=$?"' ERR
apt_can_update() {
apt-get update -qq
apt-get -s --only-upgrade install "$PKG_NAME" 2>/dev/null | grep -q "^Inst ${PKG_NAME}\b"
}
apt_apply_update() {
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
"$PKG_NAME"
}
get_rpm_pm() {
if command -v dnf >/dev/null 2>&1; then
printf 'dnf\n'
elif command -v yum >/dev/null 2>&1; then
printf 'yum\n'
else
return 1
fi
}
rpm_refresh_metadata() {
local pm=$1
if [[ "$pm" == "dnf" ]]; then
dnf makecache --refresh -q >/dev/null
else
yum clean expire-cache -q >/dev/null
fi
}
rpm_can_update() {
local pm=$1
rpm_refresh_metadata "$pm"
local rc=0
set +e
"$pm" check-update -q "$PKG_NAME" >/dev/null 2>&1
rc=$?
set -e
if [[ $rc -eq 100 ]]; then
return 0
elif [[ $rc -eq 0 ]]; then
return 1
else
log "$pm check-update failed with exit code ${rc}."
exit 1
fi
}
rpm_apply_update() {
local pm=$1
"$pm" update -y "$PKG_NAME"
}
restart_service() {
# Preserve the operator's prior service state: if xrpld was intentionally
# stopped before the update, don't bring it back up just because the
# auto-update timer fired.
if systemctl is-active --quiet "${PKG_NAME}.service"; then
systemctl restart "${PKG_NAME}.service"
log "${PKG_NAME} service restarted successfully."
else
log "${PKG_NAME} service was not running; skipping restart to preserve prior state."
fi
}
main() {
require_root
if command -v apt-get >/dev/null 2>&1; then
log "Checking for ${PKG_NAME} updates via apt"
if apt_can_update; then
log "Update available; installing."
apt_apply_update
restart_service
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
else
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
fi
return
fi
local rpm_pm=""
if rpm_pm="$(get_rpm_pm)"; then
log "Checking for ${PKG_NAME} updates via ${rpm_pm}"
if rpm_can_update "$rpm_pm"; then
log "Update available; installing"
rpm_apply_update "$rpm_pm"
restart_service
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
else
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
fi
return
fi
log "RESULT: failed reason=no-package-manager"
exit 1
}
main "$@"

View File

@@ -1,16 +0,0 @@
[Unit]
Description=Check for and install xrpld package updates
Documentation=man:systemd.service(5)
Wants=network-online.target
After=network-online.target
ConditionPathExists=/usr/libexec/xrpld/update-xrpld
ConditionPathExists=/usr/bin/xrpld
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/lock/xrpld-update.lock /usr/libexec/xrpld/update-xrpld
StandardOutput=journal
StandardError=journal
SyslogIdentifier=update-xrpld
TimeoutStartSec=30min
PrivateTmp=true

View File

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

View File

@@ -17,7 +17,16 @@ ProtectHome=true
PrivateTmp=true
User=xrpld
Group=xrpld
StateDirectory=xrpld
StateDirectoryMode=0750
LogsDirectory=xrpld
LogsDirectoryMode=0750
LimitNOFILE=65536
SystemCallArchitectures=native
# Uncomment both lines to allow xrpld to bind to privileged ports (<1024)
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
#AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target

View File

@@ -1,55 +0,0 @@
#include <xrpl/ledger/BookListeners.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/MultiApiJson.h>
#include <xrpl/server/InfoSub.h>
#include <cstdint>
#include <mutex>
namespace xrpl {
void
BookListeners::addSubscriber(InfoSub::ref sub)
{
std::scoped_lock const sl(lock_);
listeners_[sub->getSeq()] = sub;
}
void
BookListeners::removeSubscriber(std::uint64_t seq)
{
std::scoped_lock const sl(lock_);
listeners_.erase(seq);
}
void
BookListeners::publish(MultiApiJson const& jvObj, hash_set<std::uint64_t>& havePublished)
{
std::scoped_lock const sl(lock_);
auto it = listeners_.cbegin();
while (it != listeners_.cend())
{
InfoSub::pointer p = it->second.lock();
if (p)
{
// Only publish jvObj if this is the first occurrence
if (havePublished.emplace(p->getSeq()).second)
{
jvObj.visit(
p->getApiVersion(), //
[&](json::Value const& jv) { p->send(jv, true); });
}
++it;
}
else
{
it = listeners_.erase(it);
}
}
}
} // namespace xrpl

View File

@@ -433,6 +433,43 @@ ammPoolHolds(
return std::make_pair(assetInBalance, assetOutBalance);
}
TER
checkAMMPrecisionLoss(Number const& poolProductMean, STAmount const& newLPTokenBalance)
{
if (newLPTokenBalance <= beast::kZero)
return tesSUCCESS;
if (poolProductMean >= newLPTokenBalance)
return tesSUCCESS;
// Strong check failed. Allow the same relative tolerance as the invariant
// checker's weak check. Only return tecPRECISION_LOSS when both fail.
if (withinRelativeDistance(
poolProductMean, Number{newLPTokenBalance}, kAMMInvariantRelativeTolerance))
return tesSUCCESS;
return tecPRECISION_LOSS;
}
TER
checkAMMPrecisionLoss(
ReadView const& view,
AccountID const& ammAccountID,
Asset const& asset1,
Asset const& asset2,
STAmount const& newLPTokenBalance,
beast::Journal const j)
{
if (newLPTokenBalance <= beast::kZero)
return tesSUCCESS;
auto const [amount, amount2] = ammPoolHolds(
view,
ammAccountID,
asset1,
asset2,
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
return checkAMMPrecisionLoss(root2(amount * amount2), newLPTokenBalance);
}
std::expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
ReadView const& view,

View File

@@ -51,141 +51,103 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer)
// Returns adjusted owner count.
static std::uint32_t
confineOwnerCount(
std::uint32_t current,
std::int32_t adjustment,
std::uint32_t currentOwnerCount,
std::int32_t ownerCountAdj,
std::optional<AccountID> const& id = std::nullopt,
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
{
std::uint32_t adjusted{current + adjustment};
if (adjustment > 0)
std::uint32_t totalOwnerCount{currentOwnerCount + ownerCountAdj};
if (ownerCountAdj > 0)
{
// Overflow is well defined on unsigned
if (adjusted < current)
if (totalOwnerCount < currentOwnerCount)
{
if (id)
{
JLOG(j.fatal()) << "Account " << *id << " owner count exceeds max!";
}
adjusted = std::numeric_limits<std::uint32_t>::max();
totalOwnerCount = std::numeric_limits<std::uint32_t>::max();
}
}
else
{
// Underflow is well defined on unsigned
if (adjusted > current)
if (totalOwnerCount > currentOwnerCount)
{
if (id)
{
JLOG(j.fatal()) << "Account " << *id << " owner count set below 0!";
}
adjusted = 0;
totalOwnerCount = 0;
XRPL_ASSERT(!id, "xrpl::confineOwnerCount : id is not set");
}
}
return adjusted;
return totalOwnerCount;
}
// Return number of the accounts which reserve is covered by current account (so called "reserve
// count")
static std::uint32_t
ownerCountHlp(
ReadView const& view,
SLE::const_ref sle,
std::int32_t adjustment,
bool reportConfine,
beast::Journal j)
accountCountImpl(SLE::const_ref sle, std::int32_t accountCountAdj, beast::Journal j)
{
AccountID const id = sle->getAccountID(sfAccount);
std::uint32_t const savedCount = sle->at(sfOwnerCount);
std::uint32_t const hookedCount = view.ownerCountHook(id, savedCount);
bool const isSponsored = sle->isFieldPresent(sfSponsor);
std::int64_t const sponsoringAccountCount = sle->getFieldU32(sfSponsoringAccountCount);
std::int64_t const currentAccountCount = (isSponsored ? 0 : 1) + sponsoringAccountCount;
std::uint32_t const sponsoredCount = sle->at(sfSponsoredOwnerCount);
std::uint32_t const sponsoringCount = sle->at(sfSponsoringOwnerCount);
if (hookedCount < sponsoredCount)
std::int64_t totalAccountCount{currentAccountCount + accountCountAdj};
if (totalAccountCount > std::numeric_limits<std::uint32_t>::max())
{
Throw<std::logic_error>(
"xrpl::ownerCountHlp : OwnerCount must be greater than or equal to "
"SponsoredOwnerCount");
JLOG(j.error()) << "Reserve count exceeds max!";
totalAccountCount = std::numeric_limits<std::uint32_t>::max();
}
else if (totalAccountCount < 0)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::accountCountImpl : Reserve count set below 0");
JLOG(j.fatal()) << "Reserve count set below 0";
totalAccountCount = 0;
// LCOV_EXCL_STOP
}
return totalAccountCount;
}
std::uint32_t
ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj)
{
XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::ownerCount : sle is account root");
AccountID const id = sle->getAccountID(sfAccount);
std::uint32_t const currentOwnerCount = sle->at(sfOwnerCount);
std::uint32_t const sponsoredOwnerCount = sle->at(sfSponsoredOwnerCount);
std::uint32_t const sponsoringOwnerCount = sle->at(sfSponsoringOwnerCount);
XRPL_ASSERT(
currentOwnerCount >= sponsoredOwnerCount,
"xrpl::ownerCount : OwnerCount must be greater than or equal to "
"SponsoredOwnerCount");
std::int64_t deltaCount =
static_cast<std::int64_t>(adjustment) - sponsoredCount + sponsoringCount;
static_cast<std::int64_t>(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount;
if (deltaCount > std::numeric_limits<std::int32_t>::max())
{
deltaCount = std::numeric_limits<std::int32_t>::max();
JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, "
<< "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount
<< ", sponsoringOwnerCount: " << sponsoringCount;
<< "adjustment: " << ownerCountAdj
<< ", sponsoredCount: " << sponsoredOwnerCount
<< ", sponsoringOwnerCount: " << sponsoringOwnerCount;
}
else if (deltaCount < std::numeric_limits<std::int32_t>::min())
{
deltaCount = std::numeric_limits<std::int32_t>::min();
JLOG(j.fatal()) << "Account " << id << " delta count exceeds min, "
<< "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount
<< ", sponsoringCount: " << sponsoringCount;
JLOG(j.fatal()) << "Account " << id << " delta count is below min, "
<< "adjustment: " << ownerCountAdj
<< ", sponsoredCount: " << sponsoredOwnerCount
<< ", sponsoringCount: " << sponsoringOwnerCount;
}
std::uint32_t const confinedCount = reportConfine
? confineOwnerCount(hookedCount, deltaCount, id, j)
: confineOwnerCount(hookedCount, deltaCount);
return confinedCount;
}
static std::uint32_t
reserveCountHlp(SLE::const_ref sle, std::int32_t adjustment, beast::Journal j)
{
bool const isSponsored = sle->isFieldPresent(sfSponsor);
std::uint32_t const sponsoringCount = sle->getFieldU32(sfSponsoringAccountCount);
std::uint32_t const reserveCount = (isSponsored ? 0 : 1) + sponsoringCount;
std::uint32_t adjusted{reserveCount + adjustment};
if (adjustment > 0)
{
// Overflow is well defined on unsigned
if (adjusted < reserveCount)
{
JLOG(j.fatal()) << "Reserve count exceeds max!";
adjusted = std::numeric_limits<std::uint32_t>::max();
}
}
else
{
// Underflow is well defined on unsigned
if (adjusted > reserveCount)
{
JLOG(j.fatal()) << "Reserve count set below 0!";
adjusted = 0;
}
}
return adjusted;
}
static inline XRPAmount
baseReserveHlp(ReadView const& view, std::uint32_t ownerCount, std::uint32_t reserveCount)
{
auto const& fees = view.fees();
return (fees.reserve * reserveCount) + (fees.increment * ownerCount);
}
static XRPAmount
reserveHlp(
ReadView const& view,
SLE::const_ref sle,
std::uint32_t ownerCount,
std::uint32_t reserveCount)
{
// Pseudo-accounts have no reserve requirement
if (isPseudoAccount(sle))
return XRPAmount(0);
auto const reserve = baseReserveHlp(view, ownerCount, reserveCount);
return reserve;
}
std::uint32_t
ownerCount(ReadView const& view, SLE::const_ref sle, beast::Journal j, std::int32_t adjustment)
{
return ownerCountHlp(view, sle, adjustment, true, j);
return confineOwnerCount(currentOwnerCount, deltaCount);
}
XRPAmount
@@ -195,9 +157,15 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj,
if (sle == nullptr)
return beast::kZero;
std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, false, j);
std::uint32_t const reserveCount = reserveCountHlp(sle, 0, j);
auto const reserve = reserveHlp(view, sle, ownerCount, reserveCount);
// Return balance minus reserve
std::uint32_t const currentOwnerCount =
confineOwnerCount(view.ownerCountHook(id, ownerCount(sle, j)), ownerCountAdj);
std::uint32_t const currentAccountCount = accountCountImpl(sle, 0, j);
// Pseudo-accounts have no reserve requirement
auto const reserve = isPseudoAccount(sle)
? XRPAmount{0}
: baseAccountReserve(view, currentOwnerCount, currentAccountCount);
auto const fullBalance = sle->getFieldAmount(sfBalance);
@@ -209,7 +177,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj,
<< " amount=" << amount.getFullText()
<< " fullBalance=" << fullBalance.getFullText()
<< " balance=" << balance.getFullText() << " reserve=" << reserve
<< " ownerCount=" << ownerCount << " ownerCountAdj=" << ownerCountAdj;
<< " ownerCount=" << currentOwnerCount << " ownerCountAdj=" << ownerCountAdj;
return amount.xrp();
}
@@ -226,20 +194,21 @@ transferRate(ReadView const& view, AccountID const& issuer)
}
static void
adjustOwnerCountHlp(
adjustOwnerCountImpl(
ApplyView& view,
SLE::ref sle,
SF_UINT32 const& sfield,
AccountID const& accID,
std::int32_t adjustment,
std::int32_t ownerCountAdj,
beast::Journal j,
bool callHook = true)
{
std::uint32_t const current = sle->at(sfield);
std::uint32_t const adjusted = confineOwnerCount(current, adjustment, accID, j);
std::uint32_t const currentOwnerCount = sle->at(sfield);
std::uint32_t const totalOwnerCount =
confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j);
if (callHook)
view.adjustOwnerCountHook(accID, current, adjusted);
sle->at(sfield) = adjusted;
view.adjustOwnerCountHook(accID, currentOwnerCount, totalOwnerCount);
sle->at(sfield) = totalOwnerCount;
view.update(sle);
}
@@ -248,7 +217,7 @@ adjustOwnerCount(
ApplyView& view,
SLE::ref accountSle,
SLE::ref sponsorSle,
std::int32_t adjustment,
std::int32_t ownerCountAdj,
beast::Journal j)
{
if (!accountSle)
@@ -260,8 +229,8 @@ adjustOwnerCount(
if (!validType)
Throw<std::logic_error>("xrpl::adjustOwnerCount : valid account sle type");
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input");
if (adjustment == 0)
XRPL_ASSERT(ownerCountAdj, "xrpl::adjustOwnerCount : nonzero ownerCountAdj input");
if (ownerCountAdj == 0)
return;
auto const accountID = accountSle->getAccountID(sfAccount);
@@ -271,19 +240,21 @@ adjustOwnerCount(
Throw<std::logic_error>("xrpl::adjustOwnerCount : valid sponsor sle type");
auto const sponsorID = sponsorSle->getAccountID(sfAccount);
adjustOwnerCountHlp(view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j);
adjustOwnerCountHlp(view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j);
adjustOwnerCountImpl(view, accountSle, sfSponsoredOwnerCount, accountID, ownerCountAdj, j);
adjustOwnerCountImpl(view, sponsorSle, sfSponsoringOwnerCount, sponsorID, ownerCountAdj, j);
auto sponsorObjSle = view.peek(keylet::sponsor(sponsorID, accountID));
if (sponsorObjSle && adjustment > 0)
auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID));
if (sponsorshipSle && ownerCountAdj > 0)
{
// update the pre-funded ReserveCount on Sponsorship ledger object
// Reserve count moves opposite to adjustment: +adjustment => consume reserve (-),
adjustOwnerCountHlp(
view, sponsorObjSle, sfReserveCount, sponsorID, -adjustment, j, false);
// Only decrease the pre-funded ReserveCount on Sponsorship if we assign new objects.
// Removing/reassigning ownership of the object doesn't increase RemainingOwnerCount
// back. Don't call hook because this counter is not something that require reserve
// (like other sf...OwnerCounts do).
adjustOwnerCountImpl(
view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -ownerCountAdj, j, false);
}
}
adjustOwnerCountHlp(view, accountSle, sfOwnerCount, accountID, adjustment, j);
adjustOwnerCountImpl(view, accountSle, sfOwnerCount, accountID, ownerCountAdj, j);
}
void
@@ -291,7 +262,7 @@ adjustOwnerCountObj(
ApplyView& view,
SLE::ref accountSle,
SLE::ref objectSle,
std::int32_t amount,
std::int32_t accountCountAdj,
beast::Journal j)
{
if (!objectSle)
@@ -300,7 +271,7 @@ adjustOwnerCountObj(
Throw<std::logic_error>("xrpl::adjustOwnerCount : valid object sle type");
SLE::ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle);
adjustOwnerCount(view, accountSle, sponsorSle, amount, j);
adjustOwnerCount(view, accountSle, sponsorSle, accountCountAdj, j);
}
XRPAmount
@@ -309,24 +280,24 @@ accountReserve(
SLE::const_ref sle,
beast::Journal j,
std::int32_t ownerCountAdj,
std::int32_t reserveCountAdj)
std::int32_t accountCountAdj)
{
if (!sle)
Throw<std::runtime_error>("xrpl::accountReserve : valid sle");
if (sle->getType() != ltACCOUNT_ROOT)
Throw<std::logic_error>("xrpl::accountReserve : valid sle type");
std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, true, j);
std::uint32_t const reserveCount = reserveCountHlp(sle, reserveCountAdj, j);
std::uint32_t const currentOwnerCount = ownerCount(sle, j, ownerCountAdj);
std::uint32_t const currentAccountCount = accountCountImpl(sle, accountCountAdj, j);
return reserveHlp(view, sle, ownerCount, reserveCount);
return baseAccountReserve(view, currentOwnerCount, currentAccountCount);
}
XRPAmount
baseAccountReserve(ReadView const& view, std::int32_t ownerCount)
baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t accountCount)
{
auto const reserve = baseReserveHlp(view, ownerCount, 1);
return reserve;
auto const& fees = view.fees();
return (fees.reserve * accountCount) + (fees.increment * ownerCount);
}
TER
@@ -336,39 +307,39 @@ checkInsufficientReserve(
SLE::const_ref accSle,
STAmount const& accBalance,
SLE::const_ref sponsorSle,
std::int32_t ownerCountDelta,
std::int32_t reserveCountDelta,
std::int32_t ownerCountAdj,
std::int32_t accountCountAdj,
beast::Journal j)
{
if (sponsorSle)
{
auto const isCoSigning = isSponsorReserveCoSigning(tx);
auto const sle = view.read(
keylet::sponsor(sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount)));
keylet::sponsorship(
sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount)));
// prefunded sponsor should have a sponsorship entry
if (!isCoSigning && !sle)
// A reserve-sponsored tx must carry a sponsor signature
// (cosigning path) and/or have a pre-existing sponsorship SLE
// (prefunded path). Absence of both is an internal invariant break.
if (isReserveSponsored(tx) && !sle && !tx.isFieldPresent(sfSponsorSignature))
return tecINTERNAL; // LCOV_EXCL_LINE
if (sle)
{
auto const ownerCountAllowed = sle->getFieldU32(sfReserveCount);
if (ownerCountAllowed < ownerCountDelta)
auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount);
if (ownerCountAllowed < ownerCountAdj)
return tecINSUFFICIENT_RESERVE;
}
auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance);
STAmount const sponsorReserve =
accountReserve(view, sponsorSle, j, ownerCountDelta, reserveCountDelta);
accountReserve(view, sponsorSle, j, ownerCountAdj, accountCountAdj);
if (sponsorBalance < sponsorReserve)
return tecINSUFFICIENT_RESERVE;
}
else
{
STAmount const reserve =
accountReserve(view, accSle, j, ownerCountDelta, reserveCountDelta);
STAmount const reserve = accountReserve(view, accSle, j, ownerCountAdj, accountCountAdj);
if (accBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}

View File

@@ -97,7 +97,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j)
}
if (isOwner)
adjustOwnerCountObj(view, sleAccount, sleCredential, -1, j);
adjustOwnerCount(view, sleAccount, {}, -1, j);
return tesSUCCESS;
};

View File

@@ -35,6 +35,7 @@
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
namespace xrpl {
@@ -144,7 +145,7 @@ addEmptyHolding(
if (accountID == mptIssue.getIssuer())
return tesSUCCESS;
return authorizeMPToken(view, tx, priorBalance, mptID, accountID, journal);
return authorizeMPToken(view, tx, priorBalance, mptID, accountID, journal, 0, std::nullopt);
}
[[nodiscard]] TER
@@ -193,24 +194,27 @@ authorizeMPToken(
// - add the new mptokenKey to the owner directory
// - create the MPToken object for the holder
auto const sponsorSle = getTxReserveSponsor(view, tx);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
auto const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(tx);
SLE::pointer sponsorSle;
if (account == tx[sfAccount])
{
auto sle = getTxReserveSponsor(view, tx);
if (!sle)
return sle.error(); // LCOV_EXCL_LINE
sponsorSle = std::move(*sle);
}
// The reserve that is required to create the MPToken. Note
// that although the reserve increases with every item
// an account owns, in the case of MPTokens we only
// *enforce* a reserve if the user owns more than two
// items. This is similar to the reserve requirements of trust lines.
// If PreFunded Sponsor, it must be checked whether sufficient
// ReserveCount exists.
if (ownerCount(view, *sponsorSle ? *sponsorSle : sleAcct, journal) >= 2 ||
isSponsoredAndPreFunded)
// The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on
// the tx — the sponsor must always cover the reserve (via balance or prefunded
// budget), so this check always runs for sponsored transactions.
if (sponsorSle || ownerCount(sleAcct, journal) >= 2)
{
if (auto const ret = checkInsufficientReserve(
view, tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal);
view, tx, sleAcct, priorBalance, sponsorSle, 1, 0, journal);
!isTesSuccess(ret))
return ret;
}
@@ -237,8 +241,8 @@ authorizeMPToken(
view.insert(mptoken);
// Update owner count.
adjustOwnerCount(view, sleAcct, *sponsorSle, 1, journal);
addSponsorToLedgerEntry(mptoken, *sponsorSle);
adjustOwnerCount(view, sleAcct, sponsorSle, 1, journal);
addSponsorToLedgerEntry(mptoken, sponsorSle);
return tesSUCCESS;
}

View File

@@ -4,14 +4,12 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -23,7 +21,6 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -35,7 +32,6 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <functional>
#include <iterator>
#include <memory>
@@ -71,15 +67,12 @@ locatePage(ApplyView& view, AccountID const& owner, uint256 const& id)
Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
}
static std::expected<SLE::pointer, TER>
static SLE::pointer
getPageForToken(
ApplyView& view,
STTx const& tx,
AccountID const& owner,
SLE::ref sponsorSle,
uint256 const& id,
std::function<TER(ApplyView&, STTx const&, SLE::ref, AccountID const&, SLE::ref)> const&
createCallback)
std::function<void(ApplyView&, AccountID const&)> const& createCallback)
{
auto const base = keylet::nftpageMin(owner);
auto const first = keylet::nftpage(base, id);
@@ -98,9 +91,7 @@ getPageForToken(
cp = std::make_shared<SLE>(last);
cp->setFieldArray(sfNFTokens, arr);
view.insert(cp);
if (auto const ret = createCallback(view, tx, cp, owner, sponsorSle); !isTesSuccess(ret))
return std::unexpected(ret);
createCallback(view, owner);
return cp;
}
@@ -213,8 +204,7 @@ getPageForToken(
cp->setFieldH256(sfPreviousPageMin, np->key());
view.update(cp);
if (auto const ret = createCallback(view, tx, np, owner, sponsorSle); ret != tesSUCCESS)
return std::unexpected(ret);
createCallback(view, owner);
return (first.key < np->key()) ? np : cp;
}
@@ -270,55 +260,33 @@ changeTokenURI(
/** Insert the token in the owner's token directory. */
TER
insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft)
insertToken(ApplyView& view, AccountID owner, STObject&& nft)
{
XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token");
// First, we need to locate the page the NFT belongs to, creating it
// if necessary. This operation may fail if it is impossible to insert
// the NFT.
auto createCallback = [](ApplyView& view,
STTx const& tx,
std::shared_ptr<SLE> const& newPage,
AccountID const& owner,
SLE::ref sponsorSle) -> TER {
if (isReserveSponsored(tx))
{
auto const ownerSle = view.read(keylet::account(owner));
auto const ownerBalance = ownerSle->getFieldAmount(sfBalance);
if (auto const ret =
checkInsufficientReserve(view, tx, ownerSle, ownerBalance, sponsorSle, 1);
!isTesSuccess(ret))
return ret;
}
SLE::pointer const page =
getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) {
adjustOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
});
adjustOwnerCount(view, view.peek(keylet::account(owner)), sponsorSle, 1);
addSponsorToLedgerEntry(newPage, sponsorSle);
return tesSUCCESS;
};
auto const page =
getPageForToken(view, tx, owner, sponsorSle, nft[sfNFTokenID], createCallback);
if (!page.has_value())
return page.error();
if (!(*page))
if (!page)
return tecNO_SUITABLE_NFTOKEN_PAGE;
{
auto arr = (*page)->getFieldArray(sfNFTokens);
auto arr = page->getFieldArray(sfNFTokens);
arr.pushBack(std::move(nft));
arr.sort([](STObject const& o1, STObject const& o2) {
return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID));
});
(*page)->setFieldArray(sfNFTokens, arr);
page->setFieldArray(sfNFTokens, arr);
}
view.update((*page));
view.update(page);
return tesSUCCESS;
}
@@ -439,11 +407,18 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
curr->setFieldArray(sfNFTokens, arr);
view.update(curr);
int cnt = 0;
if (prev && mergePages(view, prev, curr))
adjustOwnerCountObj(view, owner, prev, -1);
cnt--;
if (next && mergePages(view, curr, next))
adjustOwnerCountObj(view, owner, curr, -1);
cnt--;
if (cnt != 0)
{
adjustOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()});
}
return tesSUCCESS;
}
@@ -477,7 +452,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
curr->makeFieldAbsent(sfPreviousPageMin);
}
adjustOwnerCountObj(view, owner, prev, -1);
adjustOwnerCount(view, owner, {}, -1, beast::Journal{beast::Journal::getNullSink()});
view.update(curr);
view.erase(prev);
@@ -513,10 +488,10 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
view.update(next);
}
adjustOwnerCountObj(view, owner, curr, -1);
view.erase(curr);
int cnt = 1;
// Since we're here, try to consolidate the previous and current pages
// of the page we removed (if any) into one. mergePages() _should_
// always return false. Since tokens are burned one at a time, there
@@ -530,9 +505,9 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
view,
view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())),
view.peek(Keylet(ltNFTOKEN_PAGE, next->key()))))
{
adjustOwnerCountObj(view, owner, prev, -1);
}
cnt++;
adjustOwnerCount(view, owner, {}, -1 * cnt, beast::Journal{beast::Journal::getNullSink()});
return tesSUCCESS;
}
@@ -648,7 +623,7 @@ deleteTokenOffer(ApplyView& view, SLE::ref offer)
false))
return false;
adjustOwnerCountObj(view, owner, offer, -1);
adjustOwnerCount(view, owner, {}, -1, beast::Journal{beast::Journal::getNullSink()});
view.erase(offer);
return true;
@@ -753,7 +728,7 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
{
Throw<std::runtime_error>(
"NFTokenPage directory for " + to_string(owner) +
" cannot be repaired. std::unexpected link problem.");
" cannot be repaired. Unexpected link problem.");
}
newPrev->at(sfNextPageMin) = nextPage->key();
view.update(newPrev);
@@ -927,7 +902,6 @@ tokenOfferCreatePreclaim(
TER
tokenOfferCreateApply(
ApplyView& view,
STTx const& tx,
AccountID const& acctID,
STAmount const& amount,
std::optional<AccountID> const& dest,
@@ -939,14 +913,8 @@ tokenOfferCreateApply(
std::uint32_t txFlags)
{
Keylet const acctKeylet = keylet::account(acctID);
auto const acct = view.read(acctKeylet);
auto const sponsorSle = getTxReserveSponsor(view, tx);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
if (auto const ret =
checkInsufficientReserve(view, tx, acct, priorBalance, *sponsorSle, 1, 0, j);
!isTesSuccess(ret))
return ret;
if (auto const acct = view.read(acctKeylet); priorBalance < accountReserve(view, acct, j, 1))
return tecINSUFFICIENT_RESERVE;
auto const offerID = keylet::nftoffer(acctID, seqProxy.value());
@@ -993,13 +961,11 @@ tokenOfferCreateApply(
if (dest)
(*offer)[sfDestination] = *dest;
addSponsorToLedgerEntry(offer, *sponsorSle);
view.insert(offer);
}
// Update owner count.
adjustOwnerCount(view, view.peek(acctKeylet), *sponsorSle, 1, j);
adjustOwnerCount(view, acctID, {}, 1, j);
return tesSUCCESS;
}

View File

@@ -5,13 +5,20 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <algorithm>
#include <cstdint>
#include <limits>
#include <optional>
namespace xrpl {
TER
@@ -59,4 +66,28 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal
return tesSUCCESS;
}
uint32_t
saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs)
{
if (rules.enabled(fixCleanup3_2_0))
{
static constexpr auto kUint32Max =
static_cast<uint64_t>(std::numeric_limits<uint32_t>::max());
uint64_t const saturatedResult = std::min(uint64_t{lhs} + rhs, kUint32Max);
return static_cast<uint32_t>(saturatedResult);
}
return lhs + rhs;
}
bool
isChannelExpired(ApplyView const& view, std::optional<uint32_t> timeField)
{
if (!timeField)
return false;
if (view.rules().enabled(fixCleanup3_2_0))
return after(view.header().parentCloseTime, *timeField);
return view.header().parentCloseTime.time_since_epoch().count() >= *timeField;
}
} // namespace xrpl

View File

@@ -665,7 +665,9 @@ addEmptyHolding(
return tecDUPLICATE;
SLE::pointer sponsorSle;
if (!isPseudoAccount(sleDst))
// A reserve sponsor only covers tx.Account's own objects.
if (!isPseudoAccount(sleDst) && accountID == tx[sfAccount])
{
auto sle = getTxReserveSponsor(view, tx);
if (!sle)

View File

@@ -45,8 +45,10 @@ ManagerImp::missingBackend()
// the Factory classes is an undefined behaviour.
void
registerNuDBFactory(Manager& manager);
#if XRPL_ROCKSDB_AVAILABLE
void
registerRocksDBFactory(Manager& manager);
#endif
void
registerNullFactory(Manager& manager);
void
@@ -55,7 +57,9 @@ registerMemoryFactory(Manager& manager);
ManagerImp::ManagerImp()
{
registerNuDBFactory(*this);
#if XRPL_ROCKSDB_AVAILABLE
registerRocksDBFactory(*this);
#endif
registerNullFactory(*this);
registerMemoryFactory(*this);
}

View File

@@ -1,13 +1,23 @@
#if XRPL_ROCKSDB_AVAILABLE
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/core/CurrentThreadName.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/config/Constants.h>
#include <xrpl/nodestore/Backend.h>
#include <xrpl/nodestore/Factory.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <xrpl/nodestore/detail/BatchWriter.h>
#include <xrpl/nodestore/detail/DecodedBlob.h>
#include <xrpl/nodestore/detail/EncodedBlob.h>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
@@ -25,26 +35,14 @@
#include <rocksdb/table.h>
#include <rocksdb/write_batch.h>
#include <atomic>
#include <bit>
#include <cstddef>
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#if XRPL_ROCKSDB_AVAILABLE
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/core/CurrentThreadName.h>
#include <xrpl/nodestore/Factory.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/nodestore/detail/BatchWriter.h>
#include <xrpl/nodestore/detail/DecodedBlob.h>
#include <xrpl/nodestore/detail/EncodedBlob.h>
#include <atomic>
#include <memory>
namespace xrpl::NodeStore {
class RocksDBEnv : public rocksdb::EnvWrapper

View File

@@ -23,7 +23,7 @@ namespace {
//------------------------------------------------------------------------------
// clang-format off
// NOLINTNEXTLINE(readability-identifier-naming)
char const* const versionString = "3.2.0-rc3"
char const* const versionString = "3.3.0-b0"
// clang-format on
;

View File

@@ -320,7 +320,7 @@ signers(AccountID const& account) noexcept
}
Keylet
sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept
sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept
{
return {ltSPONSORSHIP, indexHash(LedgerNameSpace::Sponsorship, sponsor, sponsee)};
}

View File

@@ -1,91 +1,136 @@
#include <xrpl/protocol/Permissions.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Feature.h> // IWYU pragma: keep
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFlags.h> // IWYU pragma: keep
#include <xrpl/protocol/TxFormats.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl {
Permission::GranularPermissionEntry::GranularPermissionEntry(
std::string name,
TxType txType,
std::uint32_t permittedFlags,
std::vector<SOElement> permittedFields)
: name(std::move(name))
, txType(txType)
, permittedFlags(permittedFlags)
, permittedFields(std::move(permittedFields), TxFormats::getCommonFields())
{
}
Permission::Permission()
{
txFeatureMap_ = {
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, delegable, amendment, ...) {value, amendment},
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
};
delegableTx_ = {
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, delegable, ...) {value, delegable},
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
};
granularPermissionMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {#type, type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
granularNameMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, #type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
granularTxTypeMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, txType},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
XRPL_ASSERT(
txFeatureMap_.size() == delegableTx_.size(),
"xrpl::Permission : txFeatureMap_ and delegableTx_ must have same "
"size");
for ([[maybe_unused]] auto const& permission : granularPermissionMap_)
{
XRPL_ASSERT(
permission.second > UINT16_MAX,
"xrpl::Permission::granularPermissionMap_ : granular permission "
"value must not exceed the maximum uint16_t value.");
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, delegable, amendment, ...) \
txDelegationMap_[static_cast<TxType>(value)] = {amendment, delegable};
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
}
granularPermissionsByName_ = {
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define GRANULAR_PERMISSION(type, ...) {#type, type},
#include <xrpl/protocol/detail/permissions.macro>
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
};
{
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
// NOLINTBEGIN(bugprone-macro-parentheses)
#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \
granularPermissions_.emplace( \
std::piecewise_construct, \
std::forward_as_tuple(GranularPermissionType::type), \
std::forward_as_tuple( \
#type, txType, static_cast<std::uint32_t>(flags), std::vector<SOElement> fields));
// NOLINTEND(bugprone-macro-parentheses)
#include <xrpl/protocol/detail/permissions.macro>
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
}
if (granularPermissionsByName_.size() != granularPermissions_.size())
{
// LCOV_EXCL_START
Throw<std::logic_error>(
"granularPermissionsByName_ and granularPermissions_ must have same size");
// LCOV_EXCL_STOP
}
for (auto const& [name, type] : granularPermissionsByName_)
{
if (type <= UINT16_MAX)
{
// LCOV_EXCL_START
Throw<std::logic_error>(
"Granular permission value must exceed the maximum uint16_t value: " + name);
// LCOV_EXCL_STOP
}
}
for (auto const& [type, entry] : granularPermissions_)
granularTxTypes_.insert(entry.txType);
// Validate that all fields listed in permissions.macro exist in the
// corresponding transaction type's format, catching typos at startup.
for (auto const& [type, entry] : granularPermissions_)
{
if (!txDelegationMap_.contains(entry.txType))
{
// LCOV_EXCL_START
Throw<std::logic_error>("Invalid granular permission txType in txDelegationMap_");
// LCOV_EXCL_STOP
}
auto const* fmt = TxFormats::getInstance().findByType(entry.txType);
if (fmt == nullptr)
{
// LCOV_EXCL_START
Throw<std::logic_error>("Invalid granular permission txType");
// LCOV_EXCL_STOP
}
for (auto const& field : entry.permittedFields)
{
if (fmt->getSOTemplate().getIndex(field.sField()) == -1)
{
// LCOV_EXCL_START
Throw<std::logic_error>("Invalid granular permission field");
// LCOV_EXCL_STOP
}
}
}
}
@@ -97,8 +142,11 @@ Permission::getInstance()
}
std::optional<std::string>
Permission::getPermissionName(std::uint32_t const value) const
Permission::getPermissionName(std::uint32_t value) const
{
if (value == 0)
return std::nullopt;
auto const permissionValue = static_cast<GranularPermissionType>(value);
if (auto const granular = getGranularName(permissionValue))
return granular;
@@ -114,90 +162,131 @@ Permission::getPermissionName(std::uint32_t const value) const
std::optional<std::uint32_t>
Permission::getGranularValue(std::string const& name) const
{
auto const it = granularPermissionMap_.find(name);
if (it != granularPermissionMap_.end())
auto const it = granularPermissionsByName_.find(name);
if (it != granularPermissionsByName_.end())
return static_cast<uint32_t>(it->second);
return std::nullopt;
}
std::optional<std::string>
Permission::getGranularName(GranularPermissionType const& value) const
Permission::getGranularName(GranularPermissionType value) const
{
auto const it = granularNameMap_.find(value);
if (it != granularNameMap_.end())
return it->second;
auto const it = granularPermissions_.find(value);
if (it != granularPermissions_.end())
return it->second.name;
return std::nullopt;
}
std::optional<TxType>
Permission::getGranularTxType(GranularPermissionType const& gpType) const
Permission::getGranularTxType(GranularPermissionType gpType) const
{
auto const it = granularTxTypeMap_.find(gpType);
if (it != granularTxTypeMap_.end())
return it->second;
auto const it = granularPermissions_.find(gpType);
if (it != granularPermissions_.end())
return it->second.txType;
return std::nullopt;
}
bool
Permission::hasGranularPermissions(TxType txType) const
{
return granularTxTypes_.contains(txType);
}
std::optional<std::reference_wrapper<uint256 const>>
Permission::getTxFeature(TxType txType) const
{
auto const txFeaturesIt = txFeatureMap_.find(txType);
auto const it = txDelegationMap_.find(txType);
XRPL_ASSERT(
txFeaturesIt != txFeatureMap_.end(),
"xrpl::Permissions::getTxFeature : tx exists in txFeatureMap_");
it != txDelegationMap_.end(),
"xrpl::Permission::getTxFeature : tx exists in txDelegationMap_");
if (txFeaturesIt->second == uint256{})
if (it->second.amendment == uint256{})
return std::nullopt;
return txFeaturesIt->second;
return std::optional{std::cref(it->second.amendment)};
}
bool
Permission::isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const
Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const
{
auto const granularPermission =
getGranularName(static_cast<GranularPermissionType>(permissionValue));
if (granularPermission)
if (permissionValue == 0)
return false; // LCOV_EXCL_LINE
auto const amendmentEnabled = [&rules](TxDelegationEntry const& entry) {
return entry.amendment == uint256{} || rules.enabled(entry.amendment);
};
// Granular permissions may authorize a limited subset of a tx type even
// when the full tx type is not delegable. They still require the
// underlying transaction amendment to be enabled.
if (auto const granularIt =
granularPermissions_.find(static_cast<GranularPermissionType>(permissionValue));
granularIt != granularPermissions_.end())
{
// granular permissions are always allowed to be delegated
return true;
auto const txIt = txDelegationMap_.find(granularIt->second.txType);
return txIt != txDelegationMap_.end() && amendmentEnabled(txIt->second);
}
auto const txType = permissionToTxType(permissionValue);
auto const it = delegableTx_.find(txType);
auto const txIt = txDelegationMap_.find(txType);
if (it == delegableTx_.end())
return false;
auto const txFeaturesIt = txFeatureMap_.find(txType);
XRPL_ASSERT(
txFeaturesIt != txFeatureMap_.end(),
"xrpl::Permissions::isDelegable : tx exists in txFeatureMap_");
// Delegation is only allowed if the required amendment for the transaction
// is enabled. For transactions that do not require an amendment, delegation
// is always allowed.
if (txFeaturesIt->second != uint256{} && !rules.enabled(txFeaturesIt->second))
return false;
if (it->second == Delegation::NotDelegable)
return false;
return true;
// Tx-level permissions require the transaction type itself to be delegable, and
// the corresponding amendment enabled.
return txIt != txDelegationMap_.end() && txIt->second.delegable != NotDelegable &&
amendmentEnabled(txIt->second);
}
uint32_t
Permission::txToPermissionType(TxType const& type)
Permission::txToPermissionType(TxType const type)
{
return static_cast<uint32_t>(type) + 1;
}
TxType
Permission::permissionToTxType(uint32_t const& value)
Permission::permissionToTxType(uint32_t value)
{
XRPL_ASSERT(value > 0, "xrpl::Permission::permissionToTxType : value is greater than 0");
return static_cast<TxType>(value - 1);
}
bool
Permission::checkGranularSandbox(
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldPermissions) const
{
// Build union of flags upfront to enable an early exit. Fields are not stored and
// grouped in advance to avoid heap allocation.
std::uint32_t unionFlags = 0;
for (auto const& gp : heldPermissions)
{
auto const it = granularPermissions_.find(gp);
if (it != granularPermissions_.end())
unionFlags |= it->second.permittedFlags;
}
// Check if flags are permitted
if ((tx.getFlags() & ~unionFlags) != 0)
return false;
// Check if fields are permitted. Every present field must appear in at least one held
// permission's template. The common fields are included in the constructor.
for (auto const& field : tx)
{
if (field.getSType() == STI_NOTPRESENT)
continue;
if (!std::ranges::any_of(heldPermissions, [&](auto const& gp) {
auto const it = granularPermissions_.find(gp);
return it != granularPermissions_.end() &&
it->second.permittedFields.getIndex(field.getFName()) != -1;
}))
return false;
}
return true;
}
} // namespace xrpl

View File

@@ -213,11 +213,11 @@ STTx::getSeqValue() const
}
AccountID
STTx::getFeePayer() const
STTx::getInitiator() const
{
// If sfDelegate is present, the delegate account is the payer
// note: if a delegate is specified, its authorization to act on behalf of the account is
// enforced in `Transactor::checkPermission`
// enforced in `Transactor::invokeCheckPermission`
// cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`)
if (isFieldPresent(sfDelegate))
return getAccountID(sfDelegate);
@@ -546,7 +546,7 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
// For delegated transactions sfDelegate is the account whose signer list is checked,
// the delegate account itself can not be among the signers.
auto const txnAccountID =
&sigObject != this ? std::nullopt : std::optional<AccountID>(getFeePayer());
&sigObject != this ? std::nullopt : std::optional<AccountID>(getInitiator());
// We can ease the computational load inside the loop a bit by
// pre-constructing part of the data that we hash. Fill a Serializer

View File

@@ -1,15 +1,47 @@
#include <xrpl/server/InfoSub.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/resource/Consumer.h>
#include <cstdint>
#include <exception>
#include <memory>
#include <mutex>
namespace xrpl {
namespace {
// Wraps a Source teardown call so that an exception from one cleanup
// step does not prevent the subsequent steps from running. Source methods
// acquire a lock and can throw std::system_error; a throw out of ~InfoSub
// during stack unwinding would terminate the process. Failures are
// reported through the Source's Journal so they reach the configured log
// sinks; JLOG itself cannot throw, so the noexcept guarantee holds.
template <typename F>
void
safeUnsub(std::uint64_t seq, F&& f, beast::Journal j) noexcept
{
try
{
f();
}
catch (std::exception const& e)
{
JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: " << e.what();
}
catch (...)
{
JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: unknown exception";
}
}
} // namespace
// This is the primary interface into the "client" portion of the program.
// Code that wants to do normal operations on the network such as
// creating and monitoring accounts, creating transactions, and so on
@@ -32,25 +64,44 @@ InfoSub::InfoSub(Source& source, Consumer consumer)
InfoSub::~InfoSub()
{
source_.unsubTransactions(seq_);
source_.unsubRTTransactions(seq_);
source_.unsubLedger(seq_);
source_.unsubManifests(seq_);
source_.unsubServer(seq_);
source_.unsubValidations(seq_);
source_.unsubPeerStatus(seq_);
source_.unsubConsensus(seq_);
// Each Source teardown call below acquires a server-side lock and
// can throw. Wrap each independent call so partial failure does not
// skip the remaining teardown steps.
auto const& j = source_.journal();
safeUnsub(seq_, [&] { source_.unsubTransactions(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubRTTransactions(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubLedger(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubManifests(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubServer(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubValidations(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j);
safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j);
// Use the internal unsubscribe so that it won't call
// back to us and modify its own parameter
if (!realTimeSubscriptions_.empty())
source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true);
{
safeUnsub(
seq_, [&] { source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true); }, j);
}
if (!normalSubscriptions_.empty())
source_.unsubAccountInternal(seq_, normalSubscriptions_, false);
{
safeUnsub(
seq_, [&] { source_.unsubAccountInternal(seq_, normalSubscriptions_, false); }, j);
}
for (auto const& account : accountHistorySubscriptions_)
source_.unsubAccountHistoryInternal(seq_, account, false);
{
safeUnsub(seq_, [&] { source_.unsubAccountHistoryInternal(seq_, account, false); }, j);
}
for (auto const& book : bookSubscriptions_)
{
safeUnsub(seq_, [&] { source_.unsubBookInternal(seq_, book); }, j);
}
}
Resource::Consumer&
@@ -114,6 +165,20 @@ InfoSub::deleteSubAccountHistory(AccountID const& account)
accountHistorySubscriptions_.erase(account);
}
void
InfoSub::insertBookSubscription(Book const& book)
{
std::scoped_lock const sl(lock_);
bookSubscriptions_.insert(book);
}
void
InfoSub::deleteBookSubscription(Book const& book)
{
std::scoped_lock const sl(lock_);
bookSubscriptions_.erase(book);
}
void
InfoSub::clearRequest()
{

View File

@@ -23,6 +23,7 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rules.h>
@@ -34,6 +35,7 @@
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/server/LoadFeeTrack.h>
@@ -46,8 +48,11 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <map>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -176,13 +181,6 @@ preflight1Sponsor(PreflightContext const& ctx, AccountID const& id)
if ((hasSponsor || hasSponsorFlags || hasSponsorSig) && !ctx.rules.enabled(featureSponsor))
return temDISABLED;
if (hasSponsorFlags &&
((ctx.tx.getFieldU32(sfSponsorFlags) & ~(spfSponsorFee | spfSponsorReserve)) != 0u))
{
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
return temINVALID_FLAG;
}
if (!hasSponsor)
{
if (hasSponsorFlags)
@@ -200,11 +198,51 @@ preflight1Sponsor(PreflightContext const& ctx, AccountID const& id)
else if (hasSponsorFlags)
{
auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags);
if (((sponsorFlags & ~(spfSponsorFee | spfSponsorReserve)) != 0u) || sponsorFlags == 0)
if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0)
{
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
return temINVALID_FLAG;
}
// Reserve sponsorship is only permitted for an explicit allow-list of
// transaction types, for v1. All other tx types reject spfSponsorReserve here.
if ((sponsorFlags & spfSponsorReserve) != 0u)
{
static std::unordered_set<TxType> const kReserveSponsorAllowed = {
// Explicitly allow-listed for v1.
ttDELEGATE_SET,
ttDEPOSIT_PREAUTH,
ttPAYMENT,
ttSIGNER_LIST_SET,
ttCHECK_CANCEL,
ttCHECK_CASH,
ttCHECK_CREATE,
ttESCROW_CANCEL,
ttESCROW_CREATE,
ttESCROW_FINISH,
ttPAYCHAN_CLAIM,
ttPAYCHAN_CREATE,
ttPAYCHAN_FUND,
ttCLAWBACK,
ttMPTOKEN_AUTHORIZE,
ttMPTOKEN_ISSUANCE_CREATE,
ttMPTOKEN_ISSUANCE_DESTROY,
ttMPTOKEN_ISSUANCE_SET,
ttTRUST_SET,
ttCREDENTIAL_ACCEPT,
ttCREDENTIAL_CREATE,
ttCREDENTIAL_DELETE,
ttACCOUNT_SET,
ttREGULAR_KEY_SET,
ttSPONSORSHIP_TRANSFER,
};
if (!kReserveSponsorAllowed.contains(ctx.tx.getTxnType()))
{
JLOG(ctx.j.debug())
<< "preflight1: spfSponsorReserve not allowed for this transaction type";
return temINVALID_FLAG;
}
}
}
else
{
@@ -233,6 +271,16 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
if (ctx.tx[sfDelegate] == ctx.tx[sfAccount])
return temBAD_SIGNER;
auto const& perm = Permission::getInstance();
auto const txType = ctx.tx.getTxnType();
// If the transaction is not delegable and does not have granular permissions, fail earlier
// with temINVALID. This is to prevent transactions that are not delegable at all from
// being processed further in the invokeCheckPermission function.
if (!perm.isDelegable(Permission::txToPermissionType(txType), ctx.rules) &&
!perm.hasGranularPermissions(txType))
return temINVALID;
}
if (auto const ret = preflight0(ctx, flagMask))
@@ -356,19 +404,33 @@ Transactor::preflightSigValidated(PreflightContext const& ctx)
}
NotTEC
Transactor::checkPermission(ReadView const& view, STTx const& tx)
Transactor::checkPermission(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
auto const sle = view.read(keylet::delegate(tx[sfAccount], *delegate));
if (!sle)
return terNO_DELEGATE_PERMISSION;
return checkTxPermission(sle, tx);
if (isTesSuccess(checkTxPermission(sle, tx)))
return tesSUCCESS;
if (!Permission::getInstance().hasGranularPermissions(tx.getTxnType()))
return terNO_DELEGATE_PERMISSION;
heldGranularPermissions = getGranularPermission(sle, tx.getTxnType());
if (heldGranularPermissions.empty())
return terNO_DELEGATE_PERMISSION;
if (!Permission::getInstance().checkGranularSandbox(tx, heldGranularPermissions))
return terNO_DELEGATE_PERMISSION;
return tesSUCCESS;
}
NotTEC
@@ -386,20 +448,16 @@ Transactor::checkSponsor(ReadView const& view, STTx const& tx)
return tesSUCCESS;
auto const sponsorshipSle =
view.read(keylet::sponsor(tx.getAccountID(sfSponsor), tx.getAccountID(sfAccount)));
view.read(keylet::sponsorship(tx.getAccountID(sfSponsor), tx.getAccountID(sfAccount)));
// sponsorship object missing for pre-funded tx
if (!sponsorshipSle)
return terNO_SPONSORSHIP;
auto const sponsorFlags = tx.getFieldU32(sfSponsorFlags);
if (((sponsorFlags & spfSponsorFee) != 0u) &&
sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee))
if (isFeeSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee))
return terNO_SPONSORSHIP;
if (((sponsorFlags & spfSponsorReserve) != 0u) &&
sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve))
if (isReserveSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve))
return terNO_SPONSORSHIP;
return tesSUCCESS;
@@ -582,7 +640,28 @@ Transactor::payFee()
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const feeAmountAfter = sle->getFieldAmount(feePayer.balanceField) - feePaid;
if (feePaid == beast::kZero)
return tesSUCCESS;
XRPAmount balance = beast::kZero;
if (sle->isFieldPresent(feePayer.balanceField))
{
balance = sle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return tefINTERNAL; // LCOV_EXCL_LINE
}
if (feePaid > balance)
{
if ((balance > beast::kZero) && !view().open())
return tecINSUFF_FEE;
return terINSUF_FEE_B;
}
auto const feeAmountAfter = balance - feePaid;
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
{
@@ -867,10 +946,9 @@ Transactor::checkSign(
if (!sigObject.isFieldPresent(sfSponsor))
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sponsorAccountID = sigObject.getAccountID(sfSponsor);
auto const sponsorID = sigObject.getAccountID(sfSponsor);
auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature);
if (auto const ret =
checkSign(view, flags, std::nullopt, sponsorAccountID, sponsorSignature, j);
if (auto const ret = checkSign(view, flags, std::nullopt, sponsorID, sponsorSignature, j);
!isTesSuccess(ret))
return ret;
}
@@ -1222,26 +1300,6 @@ removeDeletedTrustLines(
}
}
static void
removeDeletedMPTs(ApplyView& view, std::vector<uint256> const& mpts, beast::Journal viewJ)
{
// There could be at most two MPTs - one for each side of AMM pool
if (mpts.size() > 2)
{
JLOG(viewJ.error()) << "removeDeletedMPTs: deleted mpts exceed 2 " << mpts.size();
return;
}
for (auto const& index : mpts)
{
if (auto const sleState = view.peek({ltMPTOKEN, index}); sleState &&
deleteAMMMPToken(view, sleState, (*sleState)[sfIssuer], viewJ) != tesSUCCESS)
{
JLOG(viewJ.error()) << "removeDeletedMPTs: failed to delete AMM MPT";
}
}
}
/** Reset the context, discarding any changes made and adjust the fee.
@param fee The transaction fee to be charged.
@@ -1265,7 +1323,15 @@ Transactor::reset(XRPAmount fee)
if (!payerSle)
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
auto const balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
XRPAmount balance = beast::kZero;
if (payerSle->isFieldPresent(feePayer.balanceField))
{
balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
}
if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee))
{
@@ -1317,19 +1383,19 @@ Transactor::reset(XRPAmount fee)
FeePayer
Transactor::getFeePayer(ReadView const& view, STTx const& tx)
{
if (tx.isFieldPresent(sfSponsor) && ((tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u))
if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx))
{
auto const sponsorAccountID = tx.getAccountID(sfSponsor);
auto const sponseeAccountID = tx.getAccountID(sfAccount);
auto const sponsorID = tx.getAccountID(sfSponsor);
auto const sponseeID = tx.getAccountID(sfAccount);
auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature);
auto const sponsorshipKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID);
auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID);
// if pre-funded sponsorship exists, prefer it
if (hasSponsorSignature && !view.exists(sponsorshipKeylet))
{
// co-signed
return FeePayer{
.entry = keylet::account(sponsorAccountID),
.entry = keylet::account(sponsorID),
.balanceField = sfBalance,
.type = FeePayerType::SponsorCoSigned};
}
@@ -1341,7 +1407,7 @@ Transactor::getFeePayer(ReadView const& view, STTx const& tx)
.type = FeePayerType::SponsorPreFunded};
}
auto const payerAccountKeylet = keylet::account(tx.getFeePayer());
auto const payerAccountKeylet = keylet::account(tx.getInitiator());
auto const payerType =
tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account;
@@ -1356,6 +1422,118 @@ Transactor::trapTransaction(uint256 txHash) const
JLOG(j_.debug()) << "Transaction trapped: " << txHash;
}
std::tuple<TER, XRPAmount, bool>
Transactor::processPersistentChanges(TER result, XRPAmount fee)
{
JLOG(j_.trace()) << "reapplying because of " << transToken(result);
// FIXME: This mechanism for doing work while returning a `tec` is
// awkward and very limiting. A more general purpose approach
// should be used, making it possible to do more useful work
// when transactions fail with a `tec` code.
auto typesForResult = [](TER const ter) {
std::unordered_set<LedgerEntryType> types;
if ((ter == tecOVERSIZE) || (ter == tecKILLED))
{
types.insert(ltOFFER);
}
else if (ter == tecINCOMPLETE)
{
types.insert(ltRIPPLE_STATE);
}
else if (ter == tecEXPIRED)
{
types.insert(ltNFTOKEN_OFFER);
types.insert(ltCREDENTIAL);
}
return types;
};
// Build a list of ledger entry types to collect, based on the
// result code. Only deleted objects of these types will be
// re-applied after the context is reset.
auto const typesToCollect = typesForResult(result);
std::map<LedgerEntryType, std::vector<uint256>> deletedObjects;
if (!typesToCollect.empty())
{
ctx_.visit(
[&typesToCollect, &deletedObjects](
uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
if (isDelete)
{
XRPL_ASSERT(
before && after,
"xrpl::Transactor::processPersistentChanges : non-null "
"SLE inputs");
if (before && after)
{
auto const type = before->getType();
if (typesToCollect.contains(type))
{
// For offers, only collect unfunded removals
// (where TakerPays is unchanged)
if (type == ltOFFER &&
before->getFieldAmount(sfTakerPays) !=
after->getFieldAmount(sfTakerPays))
return;
deletedObjects[type].push_back(index);
}
}
}
});
}
// Reset the context, potentially adjusting the fee.
{
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
}
// Re-apply the collected deletions, but only if the reset succeeded
// and the post-reset result still allows the same deletion type.
auto const typesToApply = typesForResult(result);
if (isTecClaim(result) && !typesToApply.empty())
{
auto const viewJ = ctx_.registry.get().getJournal("View");
for (auto const& [type, ids] : deletedObjects)
{
if (ids.empty() || !typesToApply.contains(type))
continue;
switch (type)
{
case ltOFFER:
removeUnfundedOffers(view(), ids, viewJ);
break;
case ltNFTOKEN_OFFER:
removeExpiredNFTokenOffers(view(), ids, viewJ);
break;
case ltRIPPLE_STATE:
removeDeletedTrustLines(view(), ids, viewJ);
break;
case ltCREDENTIAL:
removeExpiredCredentials(view(), ids, viewJ);
break;
// LCOV_EXCL_START
default:
UNREACHABLE(
"xrpl::Transactor::processPersistentChanges() : "
"unexpected type");
break;
// LCOV_EXCL_STOP
}
}
}
return {result, fee, isTecClaim(result)};
}
[[nodiscard]] TER
Transactor::checkTransactionInvariants(TER result, XRPAmount fee)
{
@@ -1405,6 +1583,7 @@ Transactor::checkInvariants(TER result, XRPAmount fee)
*/
return ctx_.checkInvariants(result, fee);
}
//------------------------------------------------------------------------------
ApplyResult
Transactor::operator()()
@@ -1471,108 +1650,7 @@ Transactor::operator()()
(result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) ||
(result == tecEXPIRED) || (isTecClaimHardFail(result, view().flags())))
{
JLOG(j_.trace()) << "reapplying because of " << transToken(result);
// FIXME: This mechanism for doing work while returning a `tec` is
// awkward and very limiting. A more general purpose approach
// should be used, making it possible to do more useful work
// when transactions fail with a `tec` code.
std::vector<uint256> removedOffers;
std::vector<uint256> removedTrustLines;
std::vector<uint256> removedMPTs;
std::vector<uint256> expiredNFTokenOffers;
std::vector<uint256> expiredCredentials;
bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED));
bool const doLinesOrMPTs = (result == tecINCOMPLETE);
bool const doNFTokenOffers = (result == tecEXPIRED);
bool const doCredentials = (result == tecEXPIRED);
if (doOffers || doLinesOrMPTs || doNFTokenOffers || doCredentials)
{
ctx_.visit([doOffers,
&removedOffers,
doLinesOrMPTs,
&removedTrustLines,
&removedMPTs,
doNFTokenOffers,
&expiredNFTokenOffers,
doCredentials,
&expiredCredentials](
uint256 const& index,
bool isDelete,
SLE::const_ref before,
SLE::const_ref after) {
if (isDelete)
{
XRPL_ASSERT(
before && after,
"xrpl::Transactor::operator()::visit : non-null SLE "
"inputs");
if (doOffers && before && after && (before->getType() == ltOFFER) &&
(before->getFieldAmount(sfTakerPays) == after->getFieldAmount(sfTakerPays)))
{
// Removal of offer found or made unfunded
removedOffers.push_back(index);
}
if (doLinesOrMPTs && before && after)
{
// Removal of obsolete AMM trust line
if (before->getType() == ltRIPPLE_STATE)
{
removedTrustLines.push_back(index);
}
else if (before->getType() == ltMPTOKEN)
{
removedMPTs.push_back(index);
}
}
if (doNFTokenOffers && before && after &&
(before->getType() == ltNFTOKEN_OFFER))
expiredNFTokenOffers.push_back(index);
if (doCredentials && before && after && (before->getType() == ltCREDENTIAL))
expiredCredentials.push_back(index);
}
});
}
// Reset the context, potentially adjusting the fee.
{
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
}
// If necessary, remove any offers found unfunded during processing
if ((result == tecOVERSIZE) || (result == tecKILLED))
{
removeUnfundedOffers(view(), removedOffers, ctx_.registry.get().getJournal("View"));
}
if (result == tecEXPIRED)
{
removeExpiredNFTokenOffers(
view(), expiredNFTokenOffers, ctx_.registry.get().getJournal("View"));
}
if (result == tecINCOMPLETE)
{
removeDeletedTrustLines(
view(), removedTrustLines, ctx_.registry.get().getJournal("View"));
removeDeletedMPTs(view(), removedMPTs, ctx_.registry.get().getJournal("View"));
}
if (result == tecEXPIRED)
{
removeExpiredCredentials(
view(), expiredCredentials, ctx_.registry.get().getJournal("View"));
}
applied = isTecClaim(result);
std::tie(result, fee, applied) = processPersistentChanges(result, fee);
}
if (applied)

View File

@@ -184,7 +184,8 @@ invokePreclaim(PreclaimContext const& ctx)
if (NotTEC const result = T::checkSponsor(ctx.view, ctx.tx))
return result;
if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx))
if (NotTEC const result =
Transactor::invokeCheckPermission<T>(ctx.view, ctx.tx))
return result;
if (NotTEC const result = T::checkSign(ctx))

View File

@@ -27,7 +27,14 @@ void
ValidAMM::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
if (isDelete)
{
if (before && before->getType() == ltAMM)
{
ammDeleted_ = true;
lptAMMBalanceBeforeDeletion_ = before->getFieldAmount(sfLPTokenBalance);
}
return;
}
if (after)
{
@@ -166,18 +173,60 @@ ValidAMM::finalizeCreate(
}
bool
ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
ValidAMM::finalizeDelete(bool enforce, bool enforceAMMDelete, TER res, beast::Journal const& j)
const
{
if (ammAccount_)
{
// LCOV_EXCL_START
std::string const msg = (isTesSuccess(res)) ? "AMM object is not deleted on tesSUCCESS"
: "AMM object is changed on tecINCOMPLETE";
std::string const msg = (isTesSuccess(res)) ? "AMM object remained on tesSUCCESS"
: "AMM object changed on tecINCOMPLETE";
JLOG(j.error()) << "Invariant failed: AMMDelete failed, " << msg;
if (enforce)
return false;
// LCOV_EXCL_STOP
}
if (enforceAMMDelete)
{
if (isTesSuccess(res))
{
if (!ammDeleted_)
{
// LCOV_EXCL_START
JLOG(j.error())
<< "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS";
return false;
// LCOV_EXCL_STOP
}
if (!lptAMMBalanceBeforeDeletion_)
{
// LCOV_EXCL_START
JLOG(j.error())
<< "Invariant failed: AMMDelete failed, AMM object deleted without LP balance";
return false;
// LCOV_EXCL_STOP
}
if (*lptAMMBalanceBeforeDeletion_ != beast::kZero)
{
// LCOV_EXCL_START
JLOG(j.error())
<< "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP "
"balance: "
<< *lptAMMBalanceBeforeDeletion_;
return false;
// LCOV_EXCL_STOP
}
}
else if (ammDeleted_)
{
// AMM should only be fully deleted when AMMDelete returns tesSUCCESS.
// LCOV_EXCL_START
JLOG(j.error()) << "Invariant failed: AMMDelete failed, AMM object deleted when result "
"is not tesSUCCESS";
return false;
// LCOV_EXCL_STOP
}
}
return true;
}
@@ -221,13 +270,8 @@ ValidAMM::generalInvariant(
auto const poolProductMean = root2(amount * amount2);
bool const nonNegativeBalances =
validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
// Allow for a small relative error if strongInvariantCheck fails
auto weakInvariantCheck = [&]() {
return *lptAMMBalanceAfter_ != beast::kZero &&
withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
};
if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck()))
auto const precisionLoss = checkAMMPrecisionLoss(poolProductMean, *lptAMMBalanceAfter_);
if (!nonNegativeBalances || !isTesSuccess(precisionLoss))
{
JLOG(j.error()) << "Invariant failed: AMM " << tx.getTxnType() << " "
<< tx.getHash(HashPrefix::TransactionId) << " " << ammPoolChanged_ << " "
@@ -271,16 +315,20 @@ ValidAMM::finalizeWithdraw(
xrpl::STTx const& tx,
xrpl::ReadView const& view,
bool enforce,
bool enforceAMMDelete,
beast::Journal const& j) const
{
if (!ammAccount_)
if (enforceAMMDelete && ammDeleted_)
{
// Last Withdraw or Clawback deleted AMM
// Last Withdraw or Clawback can delete the AMM. We don't have to check
// the LPToken balance because a final AMMWithdraw or AMMClawback can
// redeem the remaining LP tokens and delete the AMM entry in the same
// transaction.
return true;
}
else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j))
if (ammAccount_ && !generalInvariant(tx, view, ZeroAllowed::Yes, j) && enforce)
{
if (enforce)
return false;
return false;
}
return true;
@@ -300,6 +348,25 @@ ValidAMM::finalize(
return true;
bool const enforce = view.rules().enabled(fixAMMv1_3);
bool const enforceAMMDelete = view.rules().enabled(fixCleanup3_3_0);
// AMM can only be deleted by AMMWithdraw, AMMClawback, and AMMDelete
if (enforceAMMDelete && ammDeleted_)
{
switch (tx.getTxnType())
{
case ttAMM_WITHDRAW:
case ttAMM_CLAWBACK:
case ttAMM_DELETE:
break;
default:
// LCOV_EXCL_START
JLOG(j.error()) << "Invariant failed: AMM failed, unexpected AMM deletion by "
<< tx.getTxnType();
return false;
// LCOV_EXCL_STOP
}
}
switch (tx.getTxnType())
{
@@ -309,13 +376,13 @@ ValidAMM::finalize(
return finalizeDeposit(tx, view, enforce, j);
case ttAMM_CLAWBACK:
case ttAMM_WITHDRAW:
return finalizeWithdraw(tx, view, enforce, j);
return finalizeWithdraw(tx, view, enforce, enforceAMMDelete, j);
case ttAMM_BID:
return finalizeBid(enforce, j);
case ttAMM_VOTE:
return finalizeVote(enforce, j);
case ttAMM_DELETE:
return finalizeDelete(enforce, result, j);
return finalizeDelete(enforce, enforceAMMDelete, result, j);
case ttCHECK_CASH:
case ttOFFER_CREATE:
case ttPAYMENT:

View File

@@ -4,13 +4,13 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/OracleHelpers.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/oracle/OracleSet.h>
#include <cstdint>
#include <memory>
@@ -62,7 +62,7 @@ SponsorshipOwnerCountsMatch::visitEntry(
if (!sle->isFieldPresent(sfSponsor))
return 0;
auto const priceDataSeries = sle->getFieldArray(sfPriceDataSeries);
return OracleSet::calculateOracleReserve(priceDataSeries.size());
return calculateOracleReserve(priceDataSeries.size());
}
case ltVAULT: {
if (!sle->isFieldPresent(sfSponsor))
@@ -93,7 +93,7 @@ SponsorshipOwnerCountsMatch::visitEntry(
(afterSponsoredObjectOwnerCount - beforeSponsoredObjectOwnerCount);
if (getOwnerCount(after) < getSponsored(after))
invalidOwnerCountLessThanSponsoredOwnerCount_ += 1;
ownerCountBelowSponsored_ += 1;
}
bool
@@ -111,6 +111,13 @@ SponsorshipOwnerCountsMatch::finalize(
return false;
}
if (ownerCountBelowSponsored_ > 0)
{
JLOG(j.fatal())
<< "Invariant failed: OwnerCount must be greater than or equal to SponsoredOwnerCount.";
return false;
}
if (deltaSponsoredObjectOwnerCount_ != deltaSponsoredOwnerCount_)
{
JLOG(j.fatal()) << "Invariant failed: SponsoredObjectOwnerCount does not "
@@ -118,13 +125,6 @@ SponsorshipOwnerCountsMatch::finalize(
return false;
}
if (invalidOwnerCountLessThanSponsoredOwnerCount_ > 0)
{
JLOG(j.fatal())
<< "Invariant failed: OwnerCount must be greater than or equal to SponsoredOwnerCount.";
return false;
}
return true;
}

Some files were not shown because too many files have changed in this diff Show More